Skip MSBuild on run-aspire.cmd when binaries are still newer than sources.
dotnet run always evaluates the Aspire graph. A second launch after Ctrl+C should not wait on that. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -48,12 +48,14 @@ npm --prefix src/HSchool.Client run build
|
||||
dotnet run --project src/HSchool.AppHost
|
||||
```
|
||||
|
||||
`run-aspire.cmd` is the same command for Windows users who want a double-clickable entry point —
|
||||
keep the two in sync if the AppHost path ever moves.
|
||||
`run-aspire.cmd` is the Windows entry point: it skips MSBuild when AppHost and Server dlls are
|
||||
newer than C# / csproj / props. Pass `--rebuild` to force a build. Keep the script in sync with
|
||||
`tools/apphost-uptodate.ps1` if the AppHost path ever moves.
|
||||
|
||||
`dotnet run --project src/HSchool.AppHost` starts the server *and* the Vite dev server and opens
|
||||
the Aspire dashboard. The Vite port is assigned per run (`npm run dev -- --port <random>`), so read
|
||||
the client URL off the dashboard instead of assuming 5173.
|
||||
`dotnet run --project src/HSchool.AppHost` still compiles every time — that is for a dirty tree,
|
||||
not a fast relaunch. It starts the server *and* the Vite dev server and opens the Aspire
|
||||
dashboard. The Vite port is assigned per run (`npm run dev -- --port <random>`), so read the
|
||||
client URL off the dashboard instead of assuming 5173.
|
||||
|
||||
Do not start a dev server with a bare `npm run dev` when you meant to run the whole app — the
|
||||
client only finds the backend through the Aspire-injected `SERVER_HTTP` environment variable, or
|
||||
@@ -130,6 +132,8 @@ say so explicitly in the change description.
|
||||
is empty. Reset deletes through the API, which deletes the save files. Headless AppHost
|
||||
sets `HSchool:AllowSaveReload` so tests can `POST /api/dev/reload-schools` without killing
|
||||
the shared fixture.
|
||||
- Launch-script up-to-date checks belong in `tests/HSchool.AppHost.Tests` and must not use
|
||||
`AppHostFixture`. Keep `LaunchBuildStamp` and `tools/apphost-uptodate.ps1` in sync.
|
||||
- Screen logic is covered in Vitest under happy-dom (`ui/*.test.ts`): people filters and the
|
||||
pager, the create dialog (core stays on, map reset, submit busy), planner rejection text, and
|
||||
the payroll-cap message. Layout and styles are still verified by running the app. Dictionaries
|
||||
@@ -184,3 +188,7 @@ say so explicitly in the change description.
|
||||
the clock does not advance, so occupancy (often empty) and lesson labels from the timetable
|
||||
keep going out. Opening sends one extra snapshot. A test that waits for *people* on pause
|
||||
will hang; a test that waits for a lesson label will not.
|
||||
- **`dotnet run` on the AppHost always evaluates MSBuild**, even when nothing changed.
|
||||
`run-aspire.cmd` skips that with `--no-build` when AppHost and Server dlls are newer than
|
||||
C# / csproj / props. `--rebuild` forces a build. Bare `dotnet run --project src/HSchool.AppHost`
|
||||
still compiles.
|
||||
|
||||
@@ -32,7 +32,8 @@ dotnet run --project src/HSchool.AppHost
|
||||
|
||||
On Windows `run-aspire.cmd` does the same and can be double-clicked; it checks that the .NET SDK
|
||||
and Node are on PATH first and passes any arguments through
|
||||
(`run-aspire.cmd --launch-profile http`).
|
||||
(`run-aspire.cmd --launch-profile http`). A second launch with no C# changes skips MSBuild;
|
||||
`run-aspire.cmd --rebuild` forces a build.
|
||||
|
||||
The Aspire dashboard opens with two resources: `server` (ASP.NET Core) and `client` (Vite dev
|
||||
server). Aspire assigns the client a random port on every run, so take its URL from the dashboard
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
# Вне очереди
|
||||
|
||||
Задачи, которых нет в срезах 1–9. Это срез без номера, чтобы не спорить со срезом 10.
|
||||
Каждый раздел — отдельная договорённость; фазы живут в индексе в секции «Вне очереди».
|
||||
|
||||
## Пропуск сборки при запуске
|
||||
|
||||
### Зачем
|
||||
|
||||
`run-aspire.cmd` каждый раз вызывает `dotnet run`, а тот — MSBuild графа Aspire, даже когда
|
||||
исходники не менялись. Повторный запуск после Ctrl+C стоит десятки секунд впустую: SDK
|
||||
пересчитывает хост, хотя dll на месте.
|
||||
|
||||
### Было / Стало / Почему
|
||||
|
||||
**Было.** Скрипт всегда `dotnet run --project src/HSchool.AppHost`. Аргументы проходили насквозь.
|
||||
|
||||
**Стало.** Если `HSchool.AppHost.dll` и `HSchool.Server.dll` новее входов (C#, csproj, props,
|
||||
`global.json`), скрипт делает `dotnet run --no-build`. Иначе — `dotnet build`, потом `--no-build`.
|
||||
`--rebuild` собирает всегда. Конфигурация (`-c` / `--configuration`) выбирает, какие dll сравнивать;
|
||||
по умолчанию Debug, как у `dotnet run`.
|
||||
|
||||
**Почему.** Инкрементальный MSBuild всё равно оценивает Aspire SDK. `--no-build` это пропускает.
|
||||
Клиентский TypeScript и JSONC модов не входы: Vite и каталог читают диск (`ContentRoot`), а не
|
||||
`bin`. Тестовые проекты AppHost не ссылается — их правки запуск не пересобирают.
|
||||
|
||||
Правила «устарел ли билд» живут в `tools/apphost-uptodate.ps1` (то, что вызывает `.cmd`) и в
|
||||
`LaunchBuildStamp` (то, что проверяют тесты). Менять надо оба.
|
||||
|
||||
### Что не входит
|
||||
|
||||
Голый `dotnet run --project src/HSchool.AppHost` по-прежнему собирает — так удобнее агентам с
|
||||
грязным деревом. Linux-обёртки нет: точка входа Windows — `.cmd`. Протокол, HTTP и сейв не
|
||||
трогаем.
|
||||
@@ -0,0 +1,45 @@
|
||||
# Фаза 49. Пропуск сборки при запуске
|
||||
|
||||
## Зависимости
|
||||
|
||||
Нет. Скрипт запуска уже есть.
|
||||
|
||||
## Зачем
|
||||
|
||||
Повторный `run-aspire.cmd` без правок в C# не должен ждать MSBuild графа Aspire. Сборка нужна
|
||||
только когда входы новее dll или когда просили `--rebuild`.
|
||||
|
||||
## Задачи
|
||||
|
||||
- [x] `tools/apphost-uptodate.ps1`: выход 0, если AppHost и Server dll новее входов; 1 — если
|
||||
нет dll или вход новее. Входы: `*.cs` / `*.csproj` / `*.props` под `src/` без `bin` /
|
||||
`obj` / `node_modules`, плюс `global.json`, `Directory.Build.props`, `Directory.Packages.props`.
|
||||
TypeScript и JSONC не входы
|
||||
- [x] `run-aspire.cmd` вызывает проверку; при 0 — `dotnet run --no-build` и пишет, что сборка
|
||||
пропущена; иначе `dotnet build`, затем `--no-build`. `--rebuild` всегда собирает и не
|
||||
уходит в AppHost. `-c` / `--configuration` выбирают папку `bin`
|
||||
- [x] `LaunchBuildStamp` с теми же правилами, тесты на временных папках без `AppHostFixture`
|
||||
- [x] `AGENTS.md` и `README.md`: повторный запуск без правок — `run-aspire.cmd`; голый
|
||||
`dotnet run` по-прежнему собирает. `--rebuild` упомянут
|
||||
|
||||
## Тесты, без которых фаза не закрыта
|
||||
|
||||
- [x] Нет dll — нужно собирать
|
||||
- [x] Dll новее всех входов — пропускать
|
||||
- [x] `.cs` новее dll — собирать
|
||||
- [x] `.ts` или `.jsonc` новее dll — пропускать
|
||||
- [x] Файл в `bin` / `obj` / `node_modules` не считается входом
|
||||
- [x] `Directory.Packages.props` новее dll — собирать
|
||||
- [x] `-c Release` смотрит `bin/Release`, а не Debug
|
||||
|
||||
## Критерий готовности
|
||||
|
||||
- Два запуска `run-aspire.cmd` подряд без правок: второй пишет, что сборка пропущена, и
|
||||
стартует AppHost
|
||||
- `run-aspire.cmd --rebuild` собирает даже со свежими dll
|
||||
- `dotnet test` проходит; тесты штампа не поднимают хост
|
||||
|
||||
## Стоп
|
||||
|
||||
Не менять Aspire-граф, протокол, сейвы. Не вшивать `--no-build` в голый `dotnet run`. Не
|
||||
сканировать `tests/` и не считать клиентский TS поводом пересобрать сервер.
|
||||
@@ -222,3 +222,11 @@
|
||||
| [47. Пак romance](47-romance-pack.md) | ⬜ | Ориентация, симпатия, пара 18+, патч тем |
|
||||
|
||||
46 стоит на 37 и 42; 47 — на 42 и 23. `example` не трогать.
|
||||
|
||||
## Вне очереди
|
||||
|
||||
Не срез 10: задачи вне игровых срезов. Дизайн: [`../design/off-queue.md`](../design/off-queue.md).
|
||||
|
||||
| Фаза | Статус | Зачем |
|
||||
| --- | --- | --- |
|
||||
| [49. Пропуск сборки при запуске](49-skip-stale-build.md) | 🔄 | `run-aspire.cmd` не гоняет MSBuild, если dll свежие |
|
||||
|
||||
+60
-7
@@ -1,8 +1,10 @@
|
||||
@echo off
|
||||
setlocal
|
||||
setlocal EnableDelayedExpansion
|
||||
|
||||
rem Starts the whole app: game server, Vite client and the Aspire dashboard.
|
||||
rem Arguments are passed through, e.g. run-aspire.cmd --launch-profile http
|
||||
rem Skips MSBuild when AppHost and Server dlls are newer than C# / csproj / props.
|
||||
rem run-aspire.cmd --launch-profile http
|
||||
rem run-aspire.cmd --rebuild
|
||||
|
||||
cd /d "%~dp0"
|
||||
|
||||
@@ -21,19 +23,70 @@ if errorlevel 1 (
|
||||
echo.
|
||||
)
|
||||
|
||||
set "CONFIG=Debug"
|
||||
set "REBUILD=0"
|
||||
set "ARGS="
|
||||
|
||||
:parse
|
||||
if "%~1"=="" goto parsed
|
||||
if /I "%~1"=="--rebuild" (
|
||||
set "REBUILD=1"
|
||||
shift
|
||||
goto parse
|
||||
)
|
||||
if /I "%~1"=="-c" if not "%~2"=="" set "CONFIG=%~2"
|
||||
if /I "%~1"=="--configuration" if not "%~2"=="" set "CONFIG=%~2"
|
||||
set "ARGS=!ARGS! %1"
|
||||
shift
|
||||
goto parse
|
||||
|
||||
:parsed
|
||||
|
||||
if "%REBUILD%"=="1" (
|
||||
echo [run-aspire] --rebuild: building.
|
||||
goto build
|
||||
)
|
||||
|
||||
where powershell >nul 2>&1
|
||||
if errorlevel 1 (
|
||||
echo [run-aspire] PowerShell not found, building.
|
||||
goto build
|
||||
)
|
||||
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0tools\apphost-uptodate.ps1" -RepoRoot "%CD%" -Configuration "%CONFIG%"
|
||||
if errorlevel 1 (
|
||||
echo [run-aspire] sources changed or binaries missing, building.
|
||||
goto build
|
||||
)
|
||||
|
||||
echo [run-aspire] binaries are up to date, skipping build.
|
||||
goto run
|
||||
|
||||
:build
|
||||
echo.
|
||||
dotnet build "src\HSchool.AppHost\HSchool.AppHost.csproj" -c "%CONFIG%"
|
||||
if errorlevel 1 (
|
||||
echo.
|
||||
echo [run-aspire] build failed.
|
||||
call :maybe_pause
|
||||
exit /b 1
|
||||
)
|
||||
echo.
|
||||
|
||||
:run
|
||||
echo [run-aspire] Starting the Aspire AppHost. Press Ctrl+C to shut everything down.
|
||||
echo.
|
||||
|
||||
dotnet run --project "src\HSchool.AppHost\HSchool.AppHost.csproj" %*
|
||||
set "EXITCODE=%ERRORLEVEL%"
|
||||
dotnet run --project "src\HSchool.AppHost\HSchool.AppHost.csproj" --no-build --no-restore !ARGS!
|
||||
set "EXITCODE=!ERRORLEVEL!"
|
||||
|
||||
if not "%EXITCODE%"=="0" (
|
||||
if not "!EXITCODE!"=="0" (
|
||||
echo.
|
||||
echo [run-aspire] AppHost exited with code %EXITCODE%.
|
||||
echo [run-aspire] AppHost exited with code !EXITCODE!.
|
||||
)
|
||||
|
||||
call :maybe_pause
|
||||
endlocal & exit /b %EXITCODE%
|
||||
exit /b !EXITCODE!
|
||||
|
||||
rem Keeps the window open when the file was double-clicked from Explorer.
|
||||
:maybe_pause
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
namespace HSchool.AppHost.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Same rules as <c>tools/apphost-uptodate.ps1</c>. Keep the two in sync.
|
||||
/// </summary>
|
||||
internal static class LaunchBuildStamp
|
||||
{
|
||||
public const string TargetFramework = "net10.0";
|
||||
|
||||
public static readonly string[] InputExtensions = [".cs", ".csproj", ".props"];
|
||||
|
||||
public static readonly string[] NamedInputs =
|
||||
[
|
||||
"global.json",
|
||||
"Directory.Build.props",
|
||||
"Directory.Packages.props",
|
||||
];
|
||||
|
||||
public static readonly string[] SkipDirectoryNames = ["bin", "obj", "node_modules"];
|
||||
|
||||
public static readonly string[] OutputProjects = ["HSchool.AppHost", "HSchool.Server"];
|
||||
|
||||
public static bool NeedsBuild(string repoRoot, string configuration = "Debug")
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(repoRoot);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(configuration);
|
||||
|
||||
DateTime? oldestOutput = null;
|
||||
foreach (var project in OutputProjects)
|
||||
{
|
||||
var dll = Path.Combine(
|
||||
repoRoot,
|
||||
"src",
|
||||
project,
|
||||
"bin",
|
||||
configuration,
|
||||
TargetFramework,
|
||||
project + ".dll");
|
||||
if (!File.Exists(dll))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var written = File.GetLastWriteTimeUtc(dll);
|
||||
if (oldestOutput is null || written < oldestOutput.Value)
|
||||
{
|
||||
oldestOutput = written;
|
||||
}
|
||||
}
|
||||
|
||||
if (oldestOutput is null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var cutoff = oldestOutput.Value;
|
||||
|
||||
foreach (var name in NamedInputs)
|
||||
{
|
||||
var path = Path.Combine(repoRoot, name);
|
||||
if (File.Exists(path) && File.GetLastWriteTimeUtc(path) > cutoff)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
var src = Path.Combine(repoRoot, "src");
|
||||
return !Directory.Exists(src) || HasNewerInput(src, cutoff);
|
||||
}
|
||||
|
||||
private static bool HasNewerInput(string directory, DateTime cutoff)
|
||||
{
|
||||
foreach (var file in Directory.EnumerateFiles(directory))
|
||||
{
|
||||
if (!IsInputExtension(Path.GetExtension(file)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (File.GetLastWriteTimeUtc(file) > cutoff)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var child in Directory.EnumerateDirectories(directory))
|
||||
{
|
||||
var leaf = Path.GetFileName(child);
|
||||
if (SkipDirectoryNames.Contains(leaf, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (HasNewerInput(child, cutoff))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsInputExtension(string extension) =>
|
||||
InputExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
namespace HSchool.AppHost.Tests;
|
||||
|
||||
public class LaunchBuildStampTests
|
||||
{
|
||||
private static readonly DateTime Inputs = new(2026, 1, 1, 12, 0, 0, DateTimeKind.Utc);
|
||||
private static readonly DateTime Outputs = new(2026, 1, 1, 12, 5, 0, DateTimeKind.Utc);
|
||||
private static readonly DateTime Newer = new(2026, 1, 1, 12, 10, 0, DateTimeKind.Utc);
|
||||
|
||||
[Fact]
|
||||
public void MissingDll_NeedsBuild()
|
||||
{
|
||||
using var repo = FreshRepo();
|
||||
File.Delete(repo.Dll("HSchool.Server"));
|
||||
|
||||
Assert.True(LaunchBuildStamp.NeedsBuild(repo.Root));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FreshDlls_SkipBuild()
|
||||
{
|
||||
using var repo = FreshRepo();
|
||||
|
||||
Assert.False(LaunchBuildStamp.NeedsBuild(repo.Root));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NewerCs_NeedsBuild()
|
||||
{
|
||||
using var repo = FreshRepo();
|
||||
repo.Touch("src/HSchool.Server/Program.cs", Newer);
|
||||
|
||||
Assert.True(LaunchBuildStamp.NeedsBuild(repo.Root));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NewerTsOrJsonc_SkipBuild()
|
||||
{
|
||||
using var repo = FreshRepo();
|
||||
repo.Write("src/HSchool.Client/src/main.ts", "export {}");
|
||||
repo.Write("src/HSchool.Server/mods/core/defs/foo.jsonc", "{}");
|
||||
repo.Touch("src/HSchool.Client/src/main.ts", Newer);
|
||||
repo.Touch("src/HSchool.Server/mods/core/defs/foo.jsonc", Newer);
|
||||
|
||||
Assert.False(LaunchBuildStamp.NeedsBuild(repo.Root));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FilesInBinObjNodeModules_AreNotInputs()
|
||||
{
|
||||
using var repo = FreshRepo();
|
||||
repo.Write("src/HSchool.Server/bin/Debug/net10.0/Generated.cs", "class G {}");
|
||||
repo.Write("src/HSchool.Server/obj/Debug/net10.0/Generated.cs", "class G {}");
|
||||
repo.Write("src/HSchool.Client/node_modules/pkg/index.cs", "class N {}");
|
||||
repo.Touch("src/HSchool.Server/bin/Debug/net10.0/Generated.cs", Newer);
|
||||
repo.Touch("src/HSchool.Server/obj/Debug/net10.0/Generated.cs", Newer);
|
||||
repo.Touch("src/HSchool.Client/node_modules/pkg/index.cs", Newer);
|
||||
|
||||
Assert.False(LaunchBuildStamp.NeedsBuild(repo.Root));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NewerDirectoryPackagesProps_NeedsBuild()
|
||||
{
|
||||
using var repo = FreshRepo();
|
||||
repo.Touch("Directory.Packages.props", Newer);
|
||||
|
||||
Assert.True(LaunchBuildStamp.NeedsBuild(repo.Root));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReleaseLooksAtReleaseBin()
|
||||
{
|
||||
using var repo = FreshRepo();
|
||||
|
||||
Assert.False(LaunchBuildStamp.NeedsBuild(repo.Root, "Debug"));
|
||||
Assert.True(LaunchBuildStamp.NeedsBuild(repo.Root, "Release"));
|
||||
|
||||
repo.WriteDll("HSchool.AppHost", "Release");
|
||||
repo.WriteDll("HSchool.Server", "Release");
|
||||
repo.TouchDlls("Release", Outputs);
|
||||
|
||||
Assert.False(LaunchBuildStamp.NeedsBuild(repo.Root, "Release"));
|
||||
}
|
||||
|
||||
private static TempRepo FreshRepo()
|
||||
{
|
||||
var repo = new TempRepo();
|
||||
repo.Write("global.json", "{}");
|
||||
repo.Write("Directory.Build.props", "<Project />");
|
||||
repo.Write("Directory.Packages.props", "<Project />");
|
||||
repo.Write("src/HSchool.AppHost/AppHost.cs", "class A {}");
|
||||
repo.Write("src/HSchool.AppHost/HSchool.AppHost.csproj", "<Project />");
|
||||
repo.Write("src/HSchool.Server/Program.cs", "class S {}");
|
||||
repo.Write("src/HSchool.Server/HSchool.Server.csproj", "<Project />");
|
||||
repo.WriteDll("HSchool.AppHost");
|
||||
repo.WriteDll("HSchool.Server");
|
||||
repo.Touch("global.json", Inputs);
|
||||
repo.Touch("Directory.Build.props", Inputs);
|
||||
repo.Touch("Directory.Packages.props", Inputs);
|
||||
repo.Touch("src/HSchool.AppHost/AppHost.cs", Inputs);
|
||||
repo.Touch("src/HSchool.AppHost/HSchool.AppHost.csproj", Inputs);
|
||||
repo.Touch("src/HSchool.Server/Program.cs", Inputs);
|
||||
repo.Touch("src/HSchool.Server/HSchool.Server.csproj", Inputs);
|
||||
repo.TouchDlls("Debug", Outputs);
|
||||
return repo;
|
||||
}
|
||||
|
||||
private sealed class TempRepo : IDisposable
|
||||
{
|
||||
public string Root { get; } = Path.Combine(Path.GetTempPath(), "h-school-stamp-" + Guid.NewGuid().ToString("N"));
|
||||
|
||||
public TempRepo() => Directory.CreateDirectory(Root);
|
||||
|
||||
public string Write(string relative, string contents)
|
||||
{
|
||||
var path = PathUnder(relative);
|
||||
var dir = Path.GetDirectoryName(path);
|
||||
if (dir is not null)
|
||||
{
|
||||
Directory.CreateDirectory(dir);
|
||||
}
|
||||
|
||||
File.WriteAllText(path, contents);
|
||||
return path;
|
||||
}
|
||||
|
||||
public string WriteDll(string project, string configuration = "Debug") =>
|
||||
Write($"src/{project}/bin/{configuration}/{LaunchBuildStamp.TargetFramework}/{project}.dll", "dll");
|
||||
|
||||
public string Dll(string project, string configuration = "Debug") =>
|
||||
PathUnder($"src/{project}/bin/{configuration}/{LaunchBuildStamp.TargetFramework}/{project}.dll");
|
||||
|
||||
public void Touch(string relative, DateTime utc) => File.SetLastWriteTimeUtc(PathUnder(relative), utc);
|
||||
|
||||
public void TouchDlls(string configuration, DateTime utc)
|
||||
{
|
||||
foreach (var project in LaunchBuildStamp.OutputProjects)
|
||||
{
|
||||
Touch($"src/{project}/bin/{configuration}/{LaunchBuildStamp.TargetFramework}/{project}.dll", utc);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.Delete(Root, recursive: true);
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private string PathUnder(string relative) =>
|
||||
Path.Combine(Root, relative.Replace('/', Path.DirectorySeparatorChar));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
# Same rules as tests/HSchool.AppHost.Tests/LaunchBuildStamp.cs. Keep the two in sync.
|
||||
# Exit 0: AppHost and Server dlls are newer than inputs. Exit 1: need a build.
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string] $RepoRoot,
|
||||
[string] $Configuration = "Debug"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$tfm = "net10.0"
|
||||
$outputs = @(
|
||||
Join-Path $RepoRoot "src\HSchool.AppHost\bin\$Configuration\$tfm\HSchool.AppHost.dll"
|
||||
Join-Path $RepoRoot "src\HSchool.Server\bin\$Configuration\$tfm\HSchool.Server.dll"
|
||||
)
|
||||
|
||||
foreach ($dll in $outputs) {
|
||||
if (-not (Test-Path -LiteralPath $dll)) {
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
$oldest = $null
|
||||
foreach ($dll in $outputs) {
|
||||
$written = (Get-Item -LiteralPath $dll).LastWriteTimeUtc
|
||||
if ($null -eq $oldest -or $written -lt $oldest) {
|
||||
$oldest = $written
|
||||
}
|
||||
}
|
||||
|
||||
$named = @(
|
||||
"global.json"
|
||||
"Directory.Build.props"
|
||||
"Directory.Packages.props"
|
||||
)
|
||||
foreach ($name in $named) {
|
||||
$path = Join-Path $RepoRoot $name
|
||||
if ((Test-Path -LiteralPath $path) -and (Get-Item -LiteralPath $path).LastWriteTimeUtc -gt $oldest) {
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
$src = Join-Path $RepoRoot "src"
|
||||
if (-not (Test-Path -LiteralPath $src)) {
|
||||
exit 1
|
||||
}
|
||||
|
||||
$extensions = @{
|
||||
".cs" = $true
|
||||
".csproj" = $true
|
||||
".props" = $true
|
||||
}
|
||||
$skip = @{
|
||||
"bin" = $true
|
||||
"obj" = $true
|
||||
"node_modules" = $true
|
||||
}
|
||||
|
||||
$stack = New-Object System.Collections.Stack
|
||||
$stack.Push($src)
|
||||
while ($stack.Count -gt 0) {
|
||||
$dir = [string]$stack.Pop()
|
||||
foreach ($child in [System.IO.Directory]::EnumerateDirectories($dir)) {
|
||||
$leaf = Split-Path -Path $child -Leaf
|
||||
if ($skip.ContainsKey($leaf.ToLowerInvariant())) {
|
||||
continue
|
||||
}
|
||||
$stack.Push($child)
|
||||
}
|
||||
foreach ($file in [System.IO.Directory]::EnumerateFiles($dir)) {
|
||||
$ext = [System.IO.Path]::GetExtension($file).ToLowerInvariant()
|
||||
if (-not $extensions.ContainsKey($ext)) {
|
||||
continue
|
||||
}
|
||||
if ((Get-Item -LiteralPath $file).LastWriteTimeUtc -gt $oldest) {
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exit 0
|
||||
Reference in New Issue
Block a user