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
|
dotnet run --project src/HSchool.AppHost
|
||||||
```
|
```
|
||||||
|
|
||||||
`run-aspire.cmd` is the same command for Windows users who want a double-clickable entry point —
|
`run-aspire.cmd` is the Windows entry point: it skips MSBuild when AppHost and Server dlls are
|
||||||
keep the two in sync if the AppHost path ever moves.
|
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
|
`dotnet run --project src/HSchool.AppHost` still compiles every time — that is for a dirty tree,
|
||||||
the Aspire dashboard. The Vite port is assigned per run (`npm run dev -- --port <random>`), so read
|
not a fast relaunch. It starts the server *and* the Vite dev server and opens the Aspire
|
||||||
the client URL off the dashboard instead of assuming 5173.
|
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
|
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
|
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
|
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
|
sets `HSchool:AllowSaveReload` so tests can `POST /api/dev/reload-schools` without killing
|
||||||
the shared fixture.
|
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
|
- 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
|
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
|
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
|
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
|
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.
|
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
|
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
|
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
|
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
|
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+, патч тем |
|
| [47. Пак romance](47-romance-pack.md) | ⬜ | Ориентация, симпатия, пара 18+, патч тем |
|
||||||
|
|
||||||
46 стоит на 37 и 42; 47 — на 42 и 23. `example` не трогать.
|
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
|
@echo off
|
||||||
setlocal
|
setlocal EnableDelayedExpansion
|
||||||
|
|
||||||
rem Starts the whole app: game server, Vite client and the Aspire dashboard.
|
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"
|
cd /d "%~dp0"
|
||||||
|
|
||||||
@@ -21,19 +23,70 @@ if errorlevel 1 (
|
|||||||
echo.
|
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 [run-aspire] Starting the Aspire AppHost. Press Ctrl+C to shut everything down.
|
||||||
echo.
|
echo.
|
||||||
|
|
||||||
dotnet run --project "src\HSchool.AppHost\HSchool.AppHost.csproj" %*
|
dotnet run --project "src\HSchool.AppHost\HSchool.AppHost.csproj" --no-build --no-restore !ARGS!
|
||||||
set "EXITCODE=%ERRORLEVEL%"
|
set "EXITCODE=!ERRORLEVEL!"
|
||||||
|
|
||||||
if not "%EXITCODE%"=="0" (
|
if not "!EXITCODE!"=="0" (
|
||||||
echo.
|
echo.
|
||||||
echo [run-aspire] AppHost exited with code %EXITCODE%.
|
echo [run-aspire] AppHost exited with code !EXITCODE!.
|
||||||
)
|
)
|
||||||
|
|
||||||
call :maybe_pause
|
call :maybe_pause
|
||||||
endlocal & exit /b %EXITCODE%
|
exit /b !EXITCODE!
|
||||||
|
|
||||||
rem Keeps the window open when the file was double-clicked from Explorer.
|
rem Keeps the window open when the file was double-clicked from Explorer.
|
||||||
:maybe_pause
|
: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