From d446789521576429c420a51e3089d848a9c962b0 Mon Sep 17 00:00:00 2001 From: John Lambert Date: Fri, 21 Aug 2026 16:28:21 -0400 Subject: [PATCH 1/4] Docs: record per-build local library design Capture the one-invocation selection contract and test-first plan. Refs LT-22728 --- .../2026-08-21-local-library-selection.md | 167 ++++++++++++++++++ ...26-08-21-local-library-selection-design.md | 61 +++++++ 2 files changed, 228 insertions(+) create mode 100644 Docs/superpowers/plans/2026-08-21-local-library-selection.md create mode 100644 Docs/superpowers/specs/2026-08-21-local-library-selection-design.md diff --git a/Docs/superpowers/plans/2026-08-21-local-library-selection.md b/Docs/superpowers/plans/2026-08-21-local-library-selection.md new file mode 100644 index 0000000000..da8a88dcba --- /dev/null +++ b/Docs/superpowers/plans/2026-08-21-local-library-selection.md @@ -0,0 +1,167 @@ +# Per-build Local Library Selection Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Make local SIL dependency selection explicit for one FieldWorks build and automatically remove unselected local packages. + +**Architecture:** A shared PowerShell module owns library metadata and provenance-aware cleanup. `build.ps1` orchestrates cleanup, packing, version overrides, and restore sources without modifying tracked version properties. + +**Tech Stack:** Windows PowerShell 5.1-compatible PowerShell, MSBuild global properties, NuGet PackageReference metadata, fixture-based script tests. + +--- + +### Task 1: Prove provenance-aware cleanup + +**Files:** +- Create: `Build/LocalLibraries.Tests.ps1` +- Create: `Build/LocalLibraries.psm1` + +- [ ] **Step 1: Write the failing fixture test** + +Create local and HTTP-sourced `.nupkg.metadata` files for Machine, create +matching feed packages, invoke `Clear-FieldWorksLocalLibraries`, and assert +that the local cache and managed feed files disappear while the HTTP cache and +an unrelated package remain. + +- [ ] **Step 2: Verify the test fails** + +Run: `pwsh -File Build/LocalLibraries.Tests.ps1` + +Expected: failure because `Build/LocalLibraries.psm1` or its exported cleanup +function does not exist. + +- [ ] **Step 3: Implement the shared catalogue and cleanup** + +Export `Get-FieldWorksLocalLibraryConfig` and +`Clear-FieldWorksLocalLibraries`. The catalogue contains the five existing +library keys, version properties, environment variables, package prefixes, +PDB directories, pack order, and Machine's project list. Cleanup treats only +filesystem metadata sources as local and scopes feed deletion to configured +package prefixes. + +- [ ] **Step 4: Verify the test passes** + +Run: `pwsh -File Build/LocalLibraries.Tests.ps1` + +Expected: all fixture assertions pass with exit code 0. + +### Task 2: Return packed versions without persistent version edits + +**Files:** +- Modify: `Build/Manage-LocalLibraries.ps1` +- Modify: `Build/LocalLibraries.Tests.ps1` + +- [ ] **Step 1: Add a failing script-contract test** + +Assert that pack orchestration accepts `-VersionOutputPath`, sources its +catalogue from `LocalLibraries.psm1`, and routes cache invalidation through the +shared module. + +- [ ] **Step 2: Verify the contract test fails** + +Run: `pwsh -File Build/LocalLibraries.Tests.ps1` + +Expected: failure naming the missing version-output contract. + +- [ ] **Step 3: Implement version output mode** + +Import the shared module, add `-VersionOutputPath`, collect each packed +library's `VersionProperty` and detected version, serialize that map as JSON, +and skip `SilVersions.props` edits in this mode. Preserve SetVersion mode and +direct pack behavior for existing callers. + +- [ ] **Step 4: Verify the tests pass** + +Run: `pwsh -File Build/LocalLibraries.Tests.ps1` + +Expected: all tests pass. + +### Task 3: Make local selection a build parameter + +**Files:** +- Modify: `build.ps1` +- Modify: `nuget.config` +- Modify: `Build/LocalLibraries.Tests.ps1` + +- [ ] **Step 1: Add failing build-contract tests** + +Assert that `build.ps1` exposes a validated `LocalLibraries` array, cleans +local state before restore, invokes the manager for selected libraries, and +adds detected versions to both restore and MSBuild arguments. Assert that +`nuget.config` clears inherited package sources. + +- [ ] **Step 2: Verify the contract tests fail** + +Run: `pwsh -File Build/LocalLibraries.Tests.ps1` + +Expected: failures for the missing parameter and source isolation. + +- [ ] **Step 3: Implement orchestration** + +Add `-LocalLibraries` with the five catalogue keys. Before restore, clean all +managed local artifacts. When selections exist, invoke the manager with the +corresponding switches and a temporary JSON output path, append +`/p:=` to restore and build arguments, and add the +local feed as an invocation-scoped restore source. Add `` before the +repository NuGet source. + +- [ ] **Step 4: Verify tests and PowerShell compatibility** + +Run: `pwsh -File Build/LocalLibraries.Tests.ps1` + +Run: `Build/Agent/powershell-compat.ps1` + +Expected: both commands exit 0. + +### Task 4: Replace the persistent workflow documentation + +**Files:** +- Modify: `Docs/architecture/local-library-debugging.md` +- Modify: `Docs/architecture/dependencies.md` +- Modify: `Build/Manage-LocalLibraries.ps1` + +- [ ] **Step 1: Document the invocation contract** + +Make `build.ps1 -LocalLibraries ` the public workflow, state that each +selected library is repacked on every invocation, explain automatic cleanup, +and include all five library names and environment variables. + +- [ ] **Step 2: Run comment hygiene and whitespace checks** + +Run: `Build/Agent/comment-hygiene.ps1 -FailOnViolation` + +Run: `git diff --check` + +Expected: both commands exit 0. + +### Task 5: Validate and publish + +**Files:** +- Modify or delete working design/plan Markdown according to PR triage. + +- [ ] **Step 1: Run repository validation** + +Run: `.\build.ps1 -CommentHygiene` + +Run: `.\test.ps1 -CommentHygiene` + +Expected: both commands exit 0 with no comment-hygiene violations. + +- [ ] **Step 2: Review the branch against `origin/main`** + +Inspect all four FieldWorks review passes, resolve findings, and record any +validation gaps in `.review/summary.md`. + +- [ ] **Step 3: Commit with Jira linkage** + +Use a subject no longer than 72 characters, wrap every body line below 80 +characters, include `Refs LT-22728`, and run: + +`gitlint --ignore body-is-missing --commits origin/main..HEAD` + +- [ ] **Step 4: Push and open the PR** + +Push `LT-22728-local-library-selection` and create a PR titled +`LT-22728: Make local library selection build-scoped`. The body leads with +the reviewer decision, identifies `build.ps1` as the entry point, reports +fresh validation, and ends with one `Next:` line. diff --git a/Docs/superpowers/specs/2026-08-21-local-library-selection-design.md b/Docs/superpowers/specs/2026-08-21-local-library-selection-design.md new file mode 100644 index 0000000000..d573b811a4 --- /dev/null +++ b/Docs/superpowers/specs/2026-08-21-local-library-selection-design.md @@ -0,0 +1,61 @@ +# Per-build local library selection + +## Problem + +`Build/Manage-LocalLibraries.ps1` turns a one-build choice into persistent +machine and checkout state. It registers a user-level NuGet source, writes the +selected version into `Build/SilVersions.props`, and leaves the restored package +under `packages/`. NuGet identifies a package only by ID and version, so a local +build with the same version as a published package remains valid after its +source `.nupkg` is removed. + +The affected path is shared by palaso, lcm, chorus, machine, and l10nsharp. + +## User contract + +Local selection belongs to one `build.ps1` invocation: + +```powershell +.\build.ps1 -LocalLibraries machine +.\build.ps1 -LocalLibraries palaso,chorus +``` + +The selected libraries are packed before restore and used by that invocation. +Libraries omitted from `-LocalLibraries` are not eligible for restore. A build +without the parameter uses published packages and removes stale local packages +before restore. The local source path and library source paths continue to come +from the existing environment variables. + +## Design + +`Build/LocalLibraries.psm1` owns the library catalogue and cleanup behavior so +`build.ps1` and `Manage-LocalLibraries.ps1` cannot drift. Cache cleanup reads +each managed package version's `.nupkg.metadata`; only entries whose source is a +filesystem path are removed. Entries restored from HTTP sources remain cached. +Matching packages in `LOCAL_NUGET_REPO` are removed before each build, then the +explicitly selected libraries are repacked. + +`Manage-LocalLibraries.ps1` gains an invocation mode that reports the detected +version properties without editing `Build/SilVersions.props`. `build.ps1` +passes those versions as global MSBuild properties to both restore and build. +This keeps the tracked version file clean and makes interruption safe. + +The repository NuGet configuration clears inherited package sources. Ordinary +builds therefore use the repository's published source even if an older run +registered a user-level source named `local`. A local build adds +`LOCAL_NUGET_REPO` to its restore command only for that invocation. + +## Failure behavior + +An explicit local selection fails before restore when its source environment +variable or `LOCAL_NUGET_REPO` is missing, its checkout does not exist, packing +fails, or its packages do not share one version. Cleanup tolerates absent feed +and cache directories. Invalid cache metadata is left in place and reported as +a warning rather than guessed to be local. + +## Verification + +A fixture-based PowerShell test creates temporary local and published cache +entries and proves that cleanup removes only local entries and managed feed +packages for all five library groups. Script compatibility, the repository +build, and the repository test entry point run with `-CommentHygiene`. From fdde8a2d6e54d80fbc9848257b4ecb338f6fbf0c Mon Sep 17 00:00:00 2001 From: John Lambert Date: Fri, 21 Aug 2026 16:51:21 -0400 Subject: [PATCH 2/4] Build: scope local libraries to one invocation Pack explicitly selected SIL dependencies before restore and pass their versions as global properties instead of editing SilVersions.props. Remove filesystem-sourced NuGet cache entries and managed local feed packages before each build while retaining published package caches. Refs LT-22728 --- Build/LocalLibraries.Tests.ps1 | 124 +++++++++++++ Build/LocalLibraries.psm1 | 175 +++++++++++++++++++ Build/Manage-LocalLibraries.ps1 | 122 ++++--------- Docs/architecture/dependencies.md | 9 +- Docs/architecture/local-library-debugging.md | 160 ++++++----------- build.ps1 | 89 ++++++++-- nuget.config | 4 +- test.ps1 | 6 + 8 files changed, 478 insertions(+), 211 deletions(-) create mode 100644 Build/LocalLibraries.Tests.ps1 create mode 100644 Build/LocalLibraries.psm1 diff --git a/Build/LocalLibraries.Tests.ps1 b/Build/LocalLibraries.Tests.ps1 new file mode 100644 index 0000000000..b0301a63b8 --- /dev/null +++ b/Build/LocalLibraries.Tests.ps1 @@ -0,0 +1,124 @@ +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$failures = New-Object System.Collections.ArrayList +$tempRoot = Join-Path ([System.IO.Path]::GetTempPath()) ( + 'FieldWorksLocalLibrariesTests_' + [System.Guid]::NewGuid().ToString('N')) + +function Assert-True { + param([bool]$Condition, [string]$Message) + if (-not $Condition) { + [void]$script:failures.Add("FAIL: $Message") + } +} + +function Write-PackageMetadata { + param([string]$VersionDirectory, [string]$Source) + New-Item -ItemType Directory -Path $VersionDirectory -Force | Out-Null + @{ version = 2; contentHash = 'test'; source = $Source } | + ConvertTo-Json | Set-Content -LiteralPath ( + Join-Path $VersionDirectory '.nupkg.metadata') -Encoding UTF8 +} + +try { + $packagesDirectory = Join-Path $tempRoot 'packages' + $localRepository = Join-Path $tempRoot 'feed' + New-Item -ItemType Directory -Path $localRepository -Force | Out-Null + + $localMachine = Join-Path $packagesDirectory 'sil.machine\3.9.2' + $publishedMachine = Join-Path $packagesDirectory 'sil.machine\3.9.3' + $unrelatedPackage = Join-Path $packagesDirectory 'example.package\1.0.0' + Write-PackageMetadata -VersionDirectory $localMachine -Source $localRepository + Write-PackageMetadata -VersionDirectory $publishedMachine ` + -Source 'https://api.nuget.org/v3/index.json' + Write-PackageMetadata -VersionDirectory $unrelatedPackage -Source $localRepository + + Set-Content -LiteralPath (Join-Path $localRepository 'SIL.Machine.3.9.2.nupkg') ` + -Value 'local package' + Set-Content -LiteralPath ( + Join-Path $localRepository 'SIL.Machine.Morphology.HermitCrab.3.9.2.snupkg') ` + -Value 'local symbols' + Set-Content -LiteralPath (Join-Path $localRepository 'Example.Package.1.0.0.nupkg') ` + -Value 'unrelated package' + $managedFeedPackages = @( + 'SIL.Core.18.0.0.nupkg', + 'SIL.LCModel.11.0.0.nupkg', + 'SIL.Chorus.LibChorus.6.0.0.nupkg', + 'L10NSharp.10.0.0.nupkg' + ) + foreach ($packageName in $managedFeedPackages) { + Set-Content -LiteralPath (Join-Path $localRepository $packageName) ` + -Value 'managed package' + } + + Import-Module (Join-Path $PSScriptRoot 'LocalLibraries.psm1') -Force + $config = Get-FieldWorksLocalLibraryConfig + Assert-True ($config.Keys.Count -eq 5) 'The catalogue should contain five libraries.' + foreach ($library in @('palaso', 'lcm', 'chorus', 'machine', 'l10nsharp')) { + Assert-True $config.Contains($library) "The catalogue should contain $library." + } + + Clear-FieldWorksLocalLibraries -PackagesDirectory $packagesDirectory ` + -LocalRepository $localRepository + + Assert-True (-not (Test-Path $localMachine)) ` + 'Cleanup should remove cache entries restored from a filesystem source.' + Assert-True (Test-Path $publishedMachine) ` + 'Cleanup should preserve cache entries restored from an HTTP source.' + Assert-True (Test-Path $unrelatedPackage) ` + 'Cleanup should preserve packages outside the managed library catalogue.' + Assert-True (-not (Test-Path ( + Join-Path $localRepository 'SIL.Machine.3.9.2.nupkg'))) ` + 'Cleanup should remove managed packages from the local feed.' + Assert-True (-not (Test-Path ( + Join-Path $localRepository 'SIL.Machine.Morphology.HermitCrab.3.9.2.snupkg'))) ` + 'Cleanup should remove managed symbol packages from the local feed.' + Assert-True (Test-Path (Join-Path $localRepository 'Example.Package.1.0.0.nupkg')) ` + 'Cleanup should preserve unrelated packages in the local feed.' + foreach ($packageName in $managedFeedPackages) { + Assert-True (-not (Test-Path (Join-Path $localRepository $packageName))) ` + "Cleanup should remove $packageName." + } + + $managerText = Get-Content -LiteralPath ( + Join-Path $PSScriptRoot 'Manage-LocalLibraries.ps1') -Raw + Assert-True ($managerText -match '\$VersionOutputPath') ` + 'Manage-LocalLibraries should accept a packed-version output path.' + Assert-True ($managerText -match 'Import-Module.+LocalLibraries\.psm1') ` + 'Manage-LocalLibraries should import the shared library catalogue.' + Assert-True ($managerText -match 'Clear-FieldWorksLocalLibraries') ` + 'Manage-LocalLibraries should use the shared cache cleanup.' + Assert-True ($managerText -match 'Packing local libraries is build-scoped') ` + 'Direct pack mode should direct callers to build.ps1.' + Assert-True ($managerText -notmatch 'dotnet nuget add source') ` + 'Manage-LocalLibraries should not persist a user-level NuGet source.' + Assert-True ($managerText -match 'RestoreAdditionalProjectSources=\$LocalRepo') ` + 'Local pack restores should receive the local feed for dependent libraries.' + + $buildText = Get-Content -LiteralPath (Join-Path $PSScriptRoot '..\build.ps1') -Raw + Assert-True ($buildText -match '\[string\[\]\]\$LocalLibraries') ` + 'build.ps1 should accept a LocalLibraries array.' + Assert-True ($buildText -match 'Clear-FieldWorksLocalLibraries') ` + 'build.ps1 should clean unselected local libraries before restore.' + Assert-True ($buildText -match 'VersionOutputPath') ` + 'build.ps1 should consume non-persistent packed version output.' + Assert-True (($buildText -match 'LOCAL_NUGET_REPO') -and ` + ($buildText -match "'--source'")) ` + 'build.ps1 should add the local feed only to an explicit local restore.' + + [xml]$nugetConfig = Get-Content -LiteralPath (Join-Path $PSScriptRoot '..\nuget.config') + Assert-True ($null -ne $nugetConfig.SelectSingleNode('/configuration/packageSources/clear')) ` + 'nuget.config should clear inherited user-level package sources.' +} +finally { + if (Test-Path $tempRoot) { + Remove-Item -LiteralPath $tempRoot -Recurse -Force + } +} + +if ($failures.Count -gt 0) { + $failures | ForEach-Object { Write-Error $_ } + exit 1 +} + +Write-Host 'Local library tests passed.' -ForegroundColor Green diff --git a/Build/LocalLibraries.psm1 b/Build/LocalLibraries.psm1 new file mode 100644 index 0000000000..6af23c196e --- /dev/null +++ b/Build/LocalLibraries.psm1 @@ -0,0 +1,175 @@ +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$script:LibraryConfig = [ordered]@{ + palaso = @{ + VersionProperty = 'SilLibPalasoVersion' + PdbRelativeDir = 'output/Debug/net462' + CachePrefixes = @( + 'sil.core', 'sil.windows', 'sil.dblbundle', 'sil.writingsystems', + 'sil.dictionary', 'sil.lift', 'sil.lexicon', 'sil.archiving', + 'sil.media', 'sil.scripture', 'sil.testutilities' + ) + EnvVar = 'LIBPALASO_PATH' + } + l10nsharp = @{ + VersionProperty = 'L10NSharpVersion' + PdbRelativeDir = 'output/Debug/net462' + CachePrefixes = @('l10nsharp') + EnvVar = 'L10NSHARP_PATH' + } + lcm = @{ + VersionProperty = 'SilLcmVersion' + PdbRelativeDir = 'artifacts/Debug/net462' + CachePrefixes = @('sil.lcmodel') + EnvVar = 'LIBLCM_PATH' + } + chorus = @{ + VersionProperty = 'SilChorusVersion' + PdbRelativeDir = 'output/Debug/net462' + CachePrefixes = @('sil.chorus') + EnvVar = 'LIBCHORUS_PATH' + } + machine = @{ + VersionProperty = 'SilMachineVersion' + PdbRelativeDir = 'bin/Debug/netstandard2.0' + CachePrefixes = @('sil.machine') + EnvVar = 'SILMACHINE_PATH' + PackProjects = @( + 'src/SIL.Machine/SIL.Machine.csproj', + 'src/SIL.Machine.Morphology.HermitCrab/SIL.Machine.Morphology.HermitCrab.csproj' + ) + } +} + +function Test-ManagedPackageName { + param([string]$Name, [string[]]$Prefixes) + $normalizedName = $Name.ToLowerInvariant() + foreach ($prefix in $Prefixes) { + if ($normalizedName -eq $prefix -or $normalizedName.StartsWith("$prefix.")) { + return $true + } + } + return $false +} + +function Test-FilesystemPackageSource { + param([string]$Source) + if ([string]::IsNullOrWhiteSpace($Source)) { + return $false + } + $uri = $null + if ([System.Uri]::TryCreate($Source, [System.UriKind]::Absolute, [ref]$uri)) { + return $uri.IsFile + } + return [System.IO.Path]::IsPathRooted($Source) +} + +function Get-SelectedPrefixes { + param([string[]]$Libraries) + $selected = if ($Libraries -and $Libraries.Count -gt 0) { + $Libraries + } + else { + @($script:LibraryConfig.Keys) + } + $prefixes = foreach ($library in $selected) { + if (-not $script:LibraryConfig.Contains($library)) { + throw "Unknown local library '$library'." + } + $script:LibraryConfig[$library].CachePrefixes + } + return @($prefixes | Sort-Object -Unique) +} + +<# +.SYNOPSIS + Removes every cached version for the selected local-library groups. +#> +function Clear-FieldWorksLibraryPackageCache { + param([string]$PackagesDirectory, [string[]]$Libraries) + if (-not (Test-Path -LiteralPath $PackagesDirectory)) { + return + } + $prefixes = Get-SelectedPrefixes -Libraries $Libraries + $packageDirectories = @(Get-ChildItem -LiteralPath $PackagesDirectory -Directory | + Where-Object { Test-ManagedPackageName -Name $_.Name -Prefixes $prefixes }) + foreach ($packageDirectory in $packageDirectories) { + Remove-Item -LiteralPath $packageDirectory.FullName -Recurse -Force + } + if ($packageDirectories.Count -gt 0) { + Write-Host ("Cleared {0} package cache folders." -f $packageDirectories.Count) ` + -ForegroundColor Yellow + } +} + +<# +.SYNOPSIS + Returns the configuration for FieldWorks-supported local libraries. +#> +function Get-FieldWorksLocalLibraryConfig { + return $script:LibraryConfig +} + +<# +.SYNOPSIS + Removes locally sourced cache entries and managed packages from a local feed. +#> +function Clear-FieldWorksLocalLibraries { + param( + [string]$PackagesDirectory, + [string]$LocalRepository, + [string[]]$Libraries + ) + + $prefixes = Get-SelectedPrefixes -Libraries $Libraries + $cacheRemovalCount = 0 + $feedRemovalCount = 0 + + if (Test-Path -LiteralPath $PackagesDirectory) { + $packageDirectories = @(Get-ChildItem -LiteralPath $PackagesDirectory -Directory | + Where-Object { Test-ManagedPackageName -Name $_.Name -Prefixes $prefixes }) + foreach ($packageDirectory in $packageDirectories) { + foreach ($versionDirectory in @(Get-ChildItem -LiteralPath $packageDirectory.FullName -Directory)) { + $metadataPath = Join-Path $versionDirectory.FullName '.nupkg.metadata' + if (-not (Test-Path -LiteralPath $metadataPath)) { + continue + } + try { + $metadata = Get-Content -LiteralPath $metadataPath -Raw | ConvertFrom-Json + } + catch { + Write-Warning "Could not read NuGet metadata at '$metadataPath'; preserving it." + continue + } + if (Test-FilesystemPackageSource -Source $metadata.source) { + Remove-Item -LiteralPath $versionDirectory.FullName -Recurse -Force + $cacheRemovalCount++ + } + } + if (@(Get-ChildItem -LiteralPath $packageDirectory.FullName -Force).Count -eq 0) { + Remove-Item -LiteralPath $packageDirectory.FullName -Force + } + } + } + + if ($LocalRepository -and (Test-Path -LiteralPath $LocalRepository)) { + $feedPackages = @(Get-ChildItem -LiteralPath $LocalRepository -File | + Where-Object { + $_.Extension -in @('.nupkg', '.snupkg') -and + (Test-ManagedPackageName -Name $_.BaseName -Prefixes $prefixes) + }) + foreach ($feedPackage in $feedPackages) { + Remove-Item -LiteralPath $feedPackage.FullName -Force + $feedRemovalCount++ + } + } + + if ($cacheRemovalCount -gt 0 -or $feedRemovalCount -gt 0) { + Write-Host ("Cleared {0} local cache entries and {1} local feed packages." -f ` + $cacheRemovalCount, $feedRemovalCount) -ForegroundColor Yellow + } +} + +Export-ModuleMember -Function Get-FieldWorksLocalLibraryConfig, + Clear-FieldWorksLocalLibraries, Clear-FieldWorksLibraryPackageCache diff --git a/Build/Manage-LocalLibraries.ps1 b/Build/Manage-LocalLibraries.ps1 index 2f5e7eb1e5..f0f523ef23 100644 --- a/Build/Manage-LocalLibraries.ps1 +++ b/Build/Manage-LocalLibraries.ps1 @@ -5,11 +5,11 @@ .DESCRIPTION Two modes of operation: - Pack mode (one or more source paths provided): - Packs local checkouts of liblcm, libpalaso, chorus, and/or machine into the - local NuGet feed using each library's own version. Detects the version - from produced packages, updates SilVersions.props to match, copies - PDBs, and clears stale cached packages. + Build pack mode (one or more source paths and -VersionOutputPath provided): + Packs local checkouts of liblcm, libpalaso, chorus, machine, and/or + L10NSharp into the local NuGet feed using each library's own version. + Detects the versions, writes them for build.ps1, copies PDBs, and clears + stale cached packages. Multiple libraries can be packed in a single call. libpalaso is always packed first (other libraries may depend on it). @@ -66,13 +66,16 @@ Sets the version in SilVersions.props (SetVersion mode). Use to revert to an upstream version. Not used in pack mode. +.PARAMETER VersionOutputPath + JSON output consumed by build.ps1 for invocation-scoped version overrides. + .EXAMPLE - .\Build\Manage-LocalLibraries.ps1 -Palaso -PalasoPath C:\Repos\libpalaso - Packs libpalaso, detects its version, and updates SilVersions.props. + .\build.ps1 -LocalLibraries palaso + Rebuilds libpalaso from LIBPALASO_PATH for this FieldWorks build. .EXAMPLE - .\Build\Manage-LocalLibraries.ps1 -Palaso -Chorus -ChorusPath C:\Repos\chorus - Packs libpalaso (from env var) and then chorus from the given path. + .\build.ps1 -LocalLibraries palaso,chorus + Rebuilds libpalaso and chorus from their configured paths for this build. .EXAMPLE .\Build\Manage-LocalLibraries.ps1 -Library palaso -Version 17.0.0 @@ -98,59 +101,16 @@ param( [ValidateSet('palaso', 'lcm', 'chorus', 'machine', 'l10nsharp')] [string]$Library, - [string]$Version + [string]$Version, + + [string]$VersionOutputPath ) $ErrorActionPreference = "Stop" - -# --------------------------------------------------------------------------- -# Library-specific configuration -# --------------------------------------------------------------------------- - -$LibraryConfig = @{ - palaso = @{ - VersionProperty = 'SilLibPalasoVersion' - PdbRelativeDir = 'output/Debug/net462' - CachePrefixes = @( - 'sil.core', 'sil.windows', 'sil.dblbundle', 'sil.writingsystems', - 'sil.dictionary', 'sil.lift', 'sil.lexicon', 'sil.archiving', - 'sil.media', 'sil.scripture', 'sil.testutilities' - ) - EnvVar = 'LIBPALASO_PATH' - } - lcm = @{ - VersionProperty = 'SilLcmVersion' - PdbRelativeDir = 'artifacts/Debug/net462' - CachePrefixes = @('sil.lcmodel') - EnvVar = 'LIBLCM_PATH' - } - chorus = @{ - VersionProperty = 'SilChorusVersion' - PdbRelativeDir = 'output/Debug/net462' - CachePrefixes = @('sil.chorus') - EnvVar = 'LIBCHORUS_PATH' - } - machine = @{ - VersionProperty = 'SilMachineVersion' - PdbRelativeDir = 'bin/Debug/netstandard2.0' - CachePrefixes = @('sil.machine') - EnvVar = 'SILMACHINE_PATH' - # Pack only the projects FieldWorks uses (avoids native CMake deps) - PackProjects = @( - 'src/SIL.Machine/SIL.Machine.csproj', - 'src/SIL.Machine.Morphology.HermitCrab/SIL.Machine.Morphology.HermitCrab.csproj' - ) - } - l10nsharp = @{ - VersionProperty = 'L10NSharpVersion' - PdbRelativeDir = 'output/Debug/net462' - CachePrefixes = @('l10nsharp') - EnvVar = 'L10NSHARP_PATH' - } -} - -# Pack order: libpalaso first (other libraries may depend on it) -$PackOrder = @('palaso', 'l10nsharp', 'lcm', 'chorus', 'machine') +Import-Module (Join-Path $PSScriptRoot 'LocalLibraries.psm1') -Force +$LibraryConfig = Get-FieldWorksLocalLibraryConfig +$PackOrder = @($LibraryConfig.Keys) +$packedVersions = [ordered]@{} # --------------------------------------------------------------------------- # Read SilVersions.props @@ -205,15 +165,8 @@ function Update-VersionAndClearCache { Write-Host "Updated SilVersions.props ($($cfg.VersionProperty) = $NewVersion)" -ForegroundColor Yellow - $packagesDir = Join-Path $repoRoot "packages" - if (Test-Path $packagesDir) { - $patterns = $cfg.CachePrefixes | ForEach-Object { "$packagesDir/$_*" } - $stale = @(Get-ChildItem -Path $patterns -Directory -ErrorAction SilentlyContinue) - if ($stale.Count -gt 0) { - $stale | Remove-Item -Recurse -Force - Write-Host "Cleared $($stale.Count) stale package folder(s) from packages/." -ForegroundColor Yellow - } - } + Clear-FieldWorksLibraryPackageCache -PackagesDirectory (Join-Path $repoRoot 'packages') ` + -Libraries @($LibName) } # --------------------------------------------------------------------------- @@ -261,6 +214,7 @@ function Invoke-PackLibrary { '-c', 'Debug' "-p:IncludeSymbols=true" "-p:SymbolPackageFormat=snupkg" + "-p:RestoreAdditionalProjectSources=$LocalRepo" '--output', $LocalRepo ) @@ -318,9 +272,9 @@ function Invoke-PackLibrary { Write-Host "" Write-Host "Pack complete ($($newPackages.Count) package(s), version $packVersion)." -ForegroundColor Green - # Update SilVersions.props and clear cache - Update-VersionAndClearCache -LibName $LibName -NewVersion $packVersion - Write-Host "To revert: git checkout Build/SilVersions.props" -ForegroundColor Yellow + $script:packedVersions[$cfg.VersionProperty] = $packVersion + Clear-FieldWorksLibraryPackageCache -PackagesDirectory (Join-Path $repoRoot 'packages') ` + -Libraries @($LibName) # Copy PDB files to Output/Debug/ and Downloads/ $pdbSourceDir = Join-Path $SourceDir $cfg.PdbRelativeDir @@ -397,6 +351,9 @@ if ($toPack.Count -gt 0) { if ($Version) { Write-Host "WARNING: -Version is ignored in pack mode (version is detected from produced packages)." -ForegroundColor Yellow } + if (-not $VersionOutputPath) { + throw "Packing local libraries is build-scoped. Run .\build.ps1 -LocalLibraries ." + } $localRepo = $env:LOCAL_NUGET_REPO if (-not $localRepo) { @@ -406,17 +363,8 @@ if ($toPack.Count -gt 0) { Write-Host "Creating local NuGet repo folder: $localRepo" -ForegroundColor Yellow New-Item -Path $localRepo -ItemType Directory -Force | Out-Null } - - # Ensure local NuGet source is registered (user-level config) - $sourceList = & dotnet nuget list source 2>&1 - $normalizedRepo = [System.IO.Path]::GetFullPath($localRepo).TrimEnd('\', '/') - $alreadyRegistered = $sourceList | Where-Object { - $_.Trim() -replace '[\\/]$', '' -ieq $normalizedRepo - } - if (-not $alreadyRegistered) { - & dotnet nuget add source $localRepo --name local 2>&1 | Out-Null - Write-Host "Added local NuGet source: $localRepo" -ForegroundColor Yellow - } + Clear-FieldWorksLocalLibraries -PackagesDirectory (Join-Path $repoRoot 'packages') ` + -LocalRepository $localRepo -Libraries @($toPack.Keys) Write-Host "" Write-Host "Libraries to pack: $($toPack.Keys -join ', ')" -ForegroundColor Cyan @@ -424,10 +372,16 @@ if ($toPack.Count -gt 0) { foreach ($lib in $toPack.Keys) { Invoke-PackLibrary -LibName $lib -SourceDir $toPack[$lib] -LocalRepo $localRepo } + $versionOutputDirectory = Split-Path $VersionOutputPath -Parent + if ($versionOutputDirectory -and -not (Test-Path $versionOutputDirectory)) { + New-Item -Path $versionOutputDirectory -ItemType Directory -Force | Out-Null + } + $packedVersions | ConvertTo-Json | Set-Content -LiteralPath $VersionOutputPath ` + -Encoding UTF8 Write-Host "" Write-Host "========================================" -ForegroundColor Green - Write-Host "[OK] All libraries packed. Run .\build.ps1 to build." -ForegroundColor Green + Write-Host "[OK] Selected local libraries packed for this build." -ForegroundColor Green Write-Host "========================================" -ForegroundColor Green } elseif ($Library -and $Version) { @@ -450,5 +404,5 @@ elseif ($Library -and $Version) { Write-Host "Run .\build.ps1 to restore and build with the new version." -ForegroundColor Cyan } else { - throw "Nothing to do. Use -Palaso/-Lcm/-Chorus/-Machine/-L10nSharp switches to pack, or -Library and -Version to set a version.`nExamples:`n .\Build\Manage-LocalLibraries.ps1 -Palaso -PalasoPath C:\Repos\libpalaso`n .\Build\Manage-LocalLibraries.ps1 -Palaso -Chorus`n .\Build\Manage-LocalLibraries.ps1 -Machine -MachinePath C:\Repos\machine`n .\Build\Manage-LocalLibraries.ps1 -Library l10nsharp -Version 10.0.0`n .\Build\Manage-LocalLibraries.ps1 -Library palaso -Version 17.0.0" + throw "Nothing to do. Use .\build.ps1 -LocalLibraries to pack local libraries, or -Library and -Version to set a version.`nExamples:`n .\build.ps1 -LocalLibraries palaso`n .\build.ps1 -LocalLibraries palaso,chorus`n .\build.ps1 -LocalLibraries machine`n .\Build\Manage-LocalLibraries.ps1 -Library l10nsharp -Version 10.0.0`n .\Build\Manage-LocalLibraries.ps1 -Library palaso -Version 17.0.0" } diff --git a/Docs/architecture/dependencies.md b/Docs/architecture/dependencies.md index 140ae06601..ca8e16dfb3 100644 --- a/Docs/architecture/dependencies.md +++ b/Docs/architecture/dependencies.md @@ -34,14 +34,17 @@ By default, dependencies are downloaded as NuGet packages during the build. The ## Building and Debugging Dependencies Locally -If you need to debug into or modify a dependency library, use the `Build/Manage-LocalLibraries.ps1` script. It packs a local checkout into a local NuGet feed, detects the produced version, and updates `SilVersions.props` to match. +If you need to debug into or modify a dependency library, select it with +`build.ps1 -LocalLibraries`. The build packs the local checkout into a local NuGet +feed and uses its detected version for that invocation without changing +`SilVersions.props`. Quick start: ```powershell $env:LOCAL_NUGET_REPO = "C:\localnugetpackages" -.\Build\Manage-LocalLibraries.ps1 -Palaso -PalasoPath C:\Repos\libpalaso -.\build.ps1 +$env:LIBPALASO_PATH = "C:\Repos\libpalaso" +.\build.ps1 -LocalLibraries palaso ``` For the full workflow (setup, pack, build, debug, revert), see **[Local Library Debugging](local-library-debugging.md)**. diff --git a/Docs/architecture/local-library-debugging.md b/Docs/architecture/local-library-debugging.md index 02b2fba90d..d2e1e94e28 100644 --- a/Docs/architecture/local-library-debugging.md +++ b/Docs/architecture/local-library-debugging.md @@ -1,149 +1,91 @@ # Local Library Debugging -This document describes how to debug locally-modified versions of **liblcm**, **libpalaso**, **chorus**, or **machine** (SIL.Machine) in FieldWorks using a local NuGet feed. +Use `build.ps1 -LocalLibraries` to rebuild locally modified SIL libraries and +use them for one FieldWorks build. A later build that does not select a library +automatically removes its local packages and restores the published version. -## Overview +## One-time setup -The workflow uses a single PowerShell script (`Build/Manage-LocalLibraries.ps1`) that: - -1. Adds a local NuGet source to `nuget.config` (pointing to your `LOCAL_NUGET_REPO` folder). -2. Runs `dotnet pack` in Debug configuration with symbols, letting the library use its own version. -3. Detects the version from the produced packages. -4. Updates `SilVersions.props` so FieldWorks resolves that exact version. -5. Places `.nupkg` / `.snupkg` in your local NuGet feed folder. -6. Copies PDB files to `Output/Debug/` and `Downloads/` for debugger access. -7. Clears stale cached packages so the next restore picks up the local build. - -This approach works identically for all three libraries. - -## Setup (one-time) - -### 1. Create a local NuGet folder - -Pick any folder, for example: - -``` -C:\localnugetpackages -``` - -### 2. Set the `LOCAL_NUGET_REPO` environment variable +Choose a folder for locally packed NuGet packages and set its environment +variable: ```powershell -# Current session $env:LOCAL_NUGET_REPO = "C:\localnugetpackages" - -# Persistent (user-level) -[System.Environment]::SetEnvironmentVariable("LOCAL_NUGET_REPO", "C:\localnugetpackages", "User") -``` - -The script automatically registers this folder as a NuGet source in your user-level NuGet config when you pack. The repo's `nuget.config` is not modified. - -### 3. Clone the library you need - -```powershell -git clone https://github.com/sillsdev/liblcm.git -git clone https://github.com/sillsdev/libpalaso.git -git clone https://github.com/sillsdev/chorus.git -git clone https://github.com/sillsdev/machine.git -``` - -## Pack a local library - -```powershell -# Single library — explicit path -.\Build\Manage-LocalLibraries.ps1 -Palaso -PalasoPath C:\Repos\libpalaso - -# Multiple libraries (libpalaso is always packed first) -.\Build\Manage-LocalLibraries.ps1 -Palaso -PalasoPath C:\Repos\libpalaso -Chorus -ChorusPath C:\Repos\chorus +[System.Environment]::SetEnvironmentVariable( + "LOCAL_NUGET_REPO", "C:\localnugetpackages", "User") ``` -Or set environment variables so you can omit the paths: +Set the path variable for each local checkout you use: ```powershell $env:LIBPALASO_PATH = "C:\Repos\libpalaso" $env:LIBLCM_PATH = "C:\Repos\liblcm" $env:LIBCHORUS_PATH = "C:\Repos\chorus" $env:SILMACHINE_PATH = "C:\Repos\machine" - -# Switches still required — env vars only provide the path -.\Build\Manage-LocalLibraries.ps1 -Palaso -Chorus +$env:L10NSHARP_PATH = "C:\Repos\L10NSharp" ``` -The script: -- Lets the library build with its own version (no version override). -- Detects the produced version and updates `Build/SilVersions.props` to match. -- Produces `.snupkg` symbol packages (same format as production). -- Copies PDB files to `Output/Debug/` and `Downloads/` for the debugger. -- Clears stale packages from the `packages/` cache. +## Build with local libraries -## Build FieldWorks +Name every local library that this invocation should use: ```powershell -.\build.ps1 -``` - -The build will print a yellow message listing any local packages detected in `LOCAL_NUGET_REPO`. NuGet restore will use your local packages because `SilVersions.props` was updated to request the exact version produced by the library. +# Rebuild and use Machine locally for this build. +.\build.ps1 -LocalLibraries machine -## Debug - -1. Open FieldWorks in Visual Studio. -2. PDB files are already in `Output/Debug/` — the debugger will find them automatically. -3. If breakpoints show "No symbols loaded", disable **Debug > Options > Enable Just My Code**. -4. You can also open the library solution side-by-side and use **Debug > Attach to Process**. - -## Iterating - -After each change to the library: - -1. Re-run `Manage-LocalLibraries.ps1` (~30-60 seconds). -2. Re-run `.\build.ps1`. +# Rebuild and use Palaso and Chorus locally for this build. +.\build.ps1 -LocalLibraries palaso,chorus +``` -## Setting a specific version +Every selected library is repacked from its configured checkout. The detected +package versions are passed to restore and MSBuild without modifying +`Build/SilVersions.props`. The local feed is a restore source only for that +invocation. -Use `-Version` to set any library to a specific version in `SilVersions.props` without packing: +An ordinary build selects no local libraries: ```powershell -# Revert libpalaso to an upstream version -.\Build\Manage-LocalLibraries.ps1 -Library libpalaso -Version 17.0.0 - -# Set liblcm to a specific pre-release version -.\Build\Manage-LocalLibraries.ps1 -Library liblcm -Version 11.0.0-beta0159 +.\build.ps1 ``` -This updates `SilVersions.props` and clears stale cached packages. Run `.\build.ps1` afterward to restore and build with the new version. +Before restore, every build removes managed packages from `LOCAL_NUGET_REPO` +and removes managed cache versions whose NuGet metadata identifies a filesystem +source. Published packages restored from an HTTP source stay cached, so an +ordinary build does not redownload them every time. Repository `nuget.config` +also ignores package sources inherited from user-level configuration. -## Reverting to upstream packages +## Debug and iterate -Use `-Version` to set the library back to its upstream version: +The local build copies PDBs to `Output/Debug/` and `Downloads/`. If Visual +Studio reports that symbols are not loaded, disable **Debug > Options > Enable +Just My Code**. -```powershell -.\Build\Manage-LocalLibraries.ps1 -Library libpalaso -Version 17.0.0 -``` +After each local-library change, rerun `build.ps1` with the same +`-LocalLibraries` selection. Omit a library whenever FieldWorks should return to +its published package. -Or revert all libraries at once: +## Set an explicit published version -```powershell -git checkout Build/SilVersions.props -Remove-Item -Recurse packages/sil.* -.\build.ps1 -``` - -To also remove the user-level local source: +The lower-level script still supports changing `SilVersions.props` deliberately: ```powershell -dotnet nuget remove source local +.\Build\Manage-LocalLibraries.ps1 -Library palaso -Version 17.0.0 ``` +Local packing through `Manage-LocalLibraries.ps1` is build-internal. Run +`build.ps1 -LocalLibraries` instead. + ## Supported libraries -| Library | Switch | Path parameter | Version property | Env var fallback | -|---------|--------|---------------|------------------|-----------------| -| liblcm | `-Lcm` | `-LcmPath` | `SilLcmVersion` | `LIBLCM_PATH` | -| libpalaso | `-Palaso` | `-PalasoPath` | `SilLibPalasoVersion` | `LIBPALASO_PATH` | -| chorus | `-Chorus` | `-ChorusPath` | `SilChorusVersion` | `LIBCHORUS_PATH` | -| machine | `-Machine` | `-MachinePath` | `SilMachineVersion` | `SILMACHINE_PATH` | +| Library | Selection | Version property | Checkout environment variable | +|---------|-----------|------------------|-------------------------------| +| libpalaso | `palaso` | `SilLibPalasoVersion` | `LIBPALASO_PATH` | +| L10NSharp | `l10nsharp` | `L10NSharpVersion` | `L10NSHARP_PATH` | +| liblcm | `lcm` | `SilLcmVersion` | `LIBLCM_PATH` | +| chorus | `chorus` | `SilChorusVersion` | `LIBCHORUS_PATH` | +| Machine | `machine` | `SilMachineVersion` | `SILMACHINE_PATH` | -## See Also +## See also -- [Dependencies](dependencies.md) — overview of external dependencies -- [Build Instructions](../../.github/instructions/build.instructions.md) — building FieldWorks +- [Dependencies](dependencies.md) +- [Build Instructions](../../.github/instructions/build.instructions.md) diff --git a/build.ps1 b/build.ps1 index 3ff6eaa985..88942d846e 100644 --- a/build.ps1 +++ b/build.ps1 @@ -114,6 +114,10 @@ Path to the local liblcm repository. Defaults to ../liblcm relative to the FieldWorks repo root. Only used when -UseLocalLcm is specified. +.PARAMETER LocalLibraries + Local SIL libraries to rebuild and use for this invocation. Supported values are + palaso, lcm, chorus, machine, and l10nsharp. Omitted libraries are cleaned before restore. + .PARAMETER StartedBy Optional actor label written to the worktree lock metadata (for example: user or agent). Defaults to the FW_BUILD_STARTED_BY environment variable when set, otherwise 'unknown'. @@ -153,6 +157,10 @@ .\build.ps1 -UseLocalLcm Builds FieldWorks, then builds liblcm from ../liblcm and copies DLLs into Output. +.EXAMPLE + .\build.ps1 -LocalLibraries machine + Rebuilds Machine from SILMACHINE_PATH and uses it only for this build. + .NOTES FieldWorks is x64-only. The x86 platform is no longer supported. #> @@ -191,6 +199,8 @@ param( [switch]$EnableTracing, [switch]$UseLocalLcm, [string]$LocalLcmPath, + [ValidateSet('palaso', 'lcm', 'chorus', 'machine', 'l10nsharp')] + [string[]]$LocalLibraries = @(), [ValidateSet('user', 'agent', 'unknown')] [string]$StartedBy = 'unknown', [switch]$SkipWorktreeLock, @@ -575,6 +585,53 @@ try { & $staleDllScript -OutputDir $outputDir -RepoRoot $PSScriptRoot -Verbose:$VerbosePreference } + $localLibrariesModule = Join-Path $PSScriptRoot 'Build/LocalLibraries.psm1' + Import-Module $localLibrariesModule -Force + $packagesDir = Join-Path $PSScriptRoot 'packages' + Clear-FieldWorksLocalLibraries -PackagesDirectory $packagesDir ` + -LocalRepository $env:LOCAL_NUGET_REPO + + $localVersionProperties = [ordered]@{} + $localRestoreSourceArgs = @() + if ($LocalLibraries.Count -gt 0) { + if ($SkipRestore) { + throw '-LocalLibraries cannot be combined with -SkipRestore.' + } + if ($UseLocalLcm -and $LocalLibraries -contains 'lcm') { + throw 'Choose either -LocalLibraries lcm or -UseLocalLcm, not both.' + } + if ([string]::IsNullOrWhiteSpace($env:LOCAL_NUGET_REPO)) { + throw 'LOCAL_NUGET_REPO must be set when -LocalLibraries is used.' + } + + $versionOutputPath = Join-Path ([System.IO.Path]::GetTempPath()) ` + ("FieldWorksLocalVersions_{0}.json" -f [System.Guid]::NewGuid().ToString('N')) + $managerArgs = @{ VersionOutputPath = $versionOutputPath } + foreach ($localLibrary in $LocalLibraries) { + $managerArgs[$localLibrary] = $true + } + try { + & (Join-Path $PSScriptRoot 'Build/Manage-LocalLibraries.ps1') @managerArgs + if ($LASTEXITCODE -ne 0) { + throw 'Local library packing failed.' + } + $versionOutput = Get-Content -LiteralPath $versionOutputPath -Raw | + ConvertFrom-Json + foreach ($property in $versionOutput.PSObject.Properties) { + $localVersionProperties[$property.Name] = [string]$property.Value + } + } + finally { + if (Test-Path -LiteralPath $versionOutputPath) { + Remove-Item -LiteralPath $versionOutputPath -Force + } + } + $localRestoreSourceArgs = @( + '--source', $env:LOCAL_NUGET_REPO, + '--source', 'https://api.nuget.org/v3/index.json' + ) + } + # ============================================================================= # Build Configuration # ============================================================================= @@ -616,6 +673,9 @@ try { # Properties $finalMsBuildArgs += "/p:Configuration=$Configuration" $finalMsBuildArgs += "/p:Platform=$Platform" + foreach ($propertyName in $localVersionProperties.Keys) { + $finalMsBuildArgs += "/p:$propertyName=$($localVersionProperties[$propertyName])" + } if ($SkipNative) { $finalMsBuildArgs += "/p:SkipNative=true" } @@ -657,18 +717,9 @@ try { Write-Host "Including optional FieldWorks executables" -ForegroundColor Yellow } - # Report local library packages when LOCAL_NUGET_REPO is configured - if ($env:LOCAL_NUGET_REPO -and (Test-Path $env:LOCAL_NUGET_REPO)) { - $localPkgs = Get-ChildItem -Path $env:LOCAL_NUGET_REPO -Filter "SIL.*.nupkg" -File -ErrorAction SilentlyContinue - if ($localPkgs.Count -gt 0) { - Write-Host "" - Write-Host "Local library packages detected in $($env:LOCAL_NUGET_REPO):" -ForegroundColor Yellow - foreach ($pkg in $localPkgs) { - Write-Host " $($pkg.Name)" -ForegroundColor Yellow - } - Write-Host "These will shadow upstream NuGet packages during restore." -ForegroundColor Yellow - Write-Host "" - } + if ($LocalLibraries.Count -gt 0) { + Write-Host "Using local libraries: $($LocalLibraries -join ', ')" ` + -ForegroundColor Yellow } # Bootstrap: Build FwBuildTasks first (required by SetupInclude.targets) @@ -705,7 +756,19 @@ try { if (-not (Test-Path $packagesDir)) { New-Item -Path $packagesDir -ItemType Directory -Force | Out-Null } - & dotnet restore "$PSScriptRoot\FieldWorks.sln" /p:NoWarn=NU1903 /p:DisableWarnForInvalidRestoreProjects=true "/p:Configuration=$Configuration" "/p:Platform=$Platform" --verbosity quiet + $restoreArgs = @( + "$PSScriptRoot\FieldWorks.sln", + '/p:NoWarn=NU1903', + '/p:DisableWarnForInvalidRestoreProjects=true', + "/p:Configuration=$Configuration", + "/p:Platform=$Platform", + '--verbosity', 'quiet' + ) + foreach ($propertyName in $localVersionProperties.Keys) { + $restoreArgs += "/p:$propertyName=$($localVersionProperties[$propertyName])" + } + $restoreArgs += $localRestoreSourceArgs + & dotnet restore @restoreArgs if ($LASTEXITCODE -ne 0) { throw "NuGet package restore failed for FieldWorks.sln" } diff --git a/nuget.config b/nuget.config index 3d86798cf0..a322c2c0d0 100644 --- a/nuget.config +++ b/nuget.config @@ -28,10 +28,10 @@ + diff --git a/test.ps1 b/test.ps1 index 10f99419ef..5636831d75 100644 --- a/test.ps1 +++ b/test.ps1 @@ -125,6 +125,12 @@ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } +$localLibrariesTestPath = Join-Path $PSScriptRoot "Build/LocalLibraries.Tests.ps1" +& $localLibrariesTestPath +if ($LASTEXITCODE -ne 0) { + exit $LASTEXITCODE +} + if (-not $PSBoundParameters.ContainsKey('StartedBy') -and -not [string]::IsNullOrWhiteSpace($env:FW_BUILD_STARTED_BY)) { $startedByFromEnv = $env:FW_BUILD_STARTED_BY.ToLowerInvariant() if ($startedByFromEnv -in @('user', 'agent', 'unknown')) { From 9ed94ca8808bdaf29be91e077859ba51d3b6fd2d Mon Sep 17 00:00:00 2001 From: John Lambert Date: Fri, 21 Aug 2026 16:51:44 -0400 Subject: [PATCH 3/4] Docs: move LT-22728 working notes to PR Keep the durable workflow in the architecture guide and carry review decisions and verification evidence in the pull request. Refs LT-22728 --- .../2026-08-21-local-library-selection.md | 167 ------------------ ...26-08-21-local-library-selection-design.md | 61 ------- 2 files changed, 228 deletions(-) delete mode 100644 Docs/superpowers/plans/2026-08-21-local-library-selection.md delete mode 100644 Docs/superpowers/specs/2026-08-21-local-library-selection-design.md diff --git a/Docs/superpowers/plans/2026-08-21-local-library-selection.md b/Docs/superpowers/plans/2026-08-21-local-library-selection.md deleted file mode 100644 index da8a88dcba..0000000000 --- a/Docs/superpowers/plans/2026-08-21-local-library-selection.md +++ /dev/null @@ -1,167 +0,0 @@ -# Per-build Local Library Selection Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Make local SIL dependency selection explicit for one FieldWorks build and automatically remove unselected local packages. - -**Architecture:** A shared PowerShell module owns library metadata and provenance-aware cleanup. `build.ps1` orchestrates cleanup, packing, version overrides, and restore sources without modifying tracked version properties. - -**Tech Stack:** Windows PowerShell 5.1-compatible PowerShell, MSBuild global properties, NuGet PackageReference metadata, fixture-based script tests. - ---- - -### Task 1: Prove provenance-aware cleanup - -**Files:** -- Create: `Build/LocalLibraries.Tests.ps1` -- Create: `Build/LocalLibraries.psm1` - -- [ ] **Step 1: Write the failing fixture test** - -Create local and HTTP-sourced `.nupkg.metadata` files for Machine, create -matching feed packages, invoke `Clear-FieldWorksLocalLibraries`, and assert -that the local cache and managed feed files disappear while the HTTP cache and -an unrelated package remain. - -- [ ] **Step 2: Verify the test fails** - -Run: `pwsh -File Build/LocalLibraries.Tests.ps1` - -Expected: failure because `Build/LocalLibraries.psm1` or its exported cleanup -function does not exist. - -- [ ] **Step 3: Implement the shared catalogue and cleanup** - -Export `Get-FieldWorksLocalLibraryConfig` and -`Clear-FieldWorksLocalLibraries`. The catalogue contains the five existing -library keys, version properties, environment variables, package prefixes, -PDB directories, pack order, and Machine's project list. Cleanup treats only -filesystem metadata sources as local and scopes feed deletion to configured -package prefixes. - -- [ ] **Step 4: Verify the test passes** - -Run: `pwsh -File Build/LocalLibraries.Tests.ps1` - -Expected: all fixture assertions pass with exit code 0. - -### Task 2: Return packed versions without persistent version edits - -**Files:** -- Modify: `Build/Manage-LocalLibraries.ps1` -- Modify: `Build/LocalLibraries.Tests.ps1` - -- [ ] **Step 1: Add a failing script-contract test** - -Assert that pack orchestration accepts `-VersionOutputPath`, sources its -catalogue from `LocalLibraries.psm1`, and routes cache invalidation through the -shared module. - -- [ ] **Step 2: Verify the contract test fails** - -Run: `pwsh -File Build/LocalLibraries.Tests.ps1` - -Expected: failure naming the missing version-output contract. - -- [ ] **Step 3: Implement version output mode** - -Import the shared module, add `-VersionOutputPath`, collect each packed -library's `VersionProperty` and detected version, serialize that map as JSON, -and skip `SilVersions.props` edits in this mode. Preserve SetVersion mode and -direct pack behavior for existing callers. - -- [ ] **Step 4: Verify the tests pass** - -Run: `pwsh -File Build/LocalLibraries.Tests.ps1` - -Expected: all tests pass. - -### Task 3: Make local selection a build parameter - -**Files:** -- Modify: `build.ps1` -- Modify: `nuget.config` -- Modify: `Build/LocalLibraries.Tests.ps1` - -- [ ] **Step 1: Add failing build-contract tests** - -Assert that `build.ps1` exposes a validated `LocalLibraries` array, cleans -local state before restore, invokes the manager for selected libraries, and -adds detected versions to both restore and MSBuild arguments. Assert that -`nuget.config` clears inherited package sources. - -- [ ] **Step 2: Verify the contract tests fail** - -Run: `pwsh -File Build/LocalLibraries.Tests.ps1` - -Expected: failures for the missing parameter and source isolation. - -- [ ] **Step 3: Implement orchestration** - -Add `-LocalLibraries` with the five catalogue keys. Before restore, clean all -managed local artifacts. When selections exist, invoke the manager with the -corresponding switches and a temporary JSON output path, append -`/p:=` to restore and build arguments, and add the -local feed as an invocation-scoped restore source. Add `` before the -repository NuGet source. - -- [ ] **Step 4: Verify tests and PowerShell compatibility** - -Run: `pwsh -File Build/LocalLibraries.Tests.ps1` - -Run: `Build/Agent/powershell-compat.ps1` - -Expected: both commands exit 0. - -### Task 4: Replace the persistent workflow documentation - -**Files:** -- Modify: `Docs/architecture/local-library-debugging.md` -- Modify: `Docs/architecture/dependencies.md` -- Modify: `Build/Manage-LocalLibraries.ps1` - -- [ ] **Step 1: Document the invocation contract** - -Make `build.ps1 -LocalLibraries ` the public workflow, state that each -selected library is repacked on every invocation, explain automatic cleanup, -and include all five library names and environment variables. - -- [ ] **Step 2: Run comment hygiene and whitespace checks** - -Run: `Build/Agent/comment-hygiene.ps1 -FailOnViolation` - -Run: `git diff --check` - -Expected: both commands exit 0. - -### Task 5: Validate and publish - -**Files:** -- Modify or delete working design/plan Markdown according to PR triage. - -- [ ] **Step 1: Run repository validation** - -Run: `.\build.ps1 -CommentHygiene` - -Run: `.\test.ps1 -CommentHygiene` - -Expected: both commands exit 0 with no comment-hygiene violations. - -- [ ] **Step 2: Review the branch against `origin/main`** - -Inspect all four FieldWorks review passes, resolve findings, and record any -validation gaps in `.review/summary.md`. - -- [ ] **Step 3: Commit with Jira linkage** - -Use a subject no longer than 72 characters, wrap every body line below 80 -characters, include `Refs LT-22728`, and run: - -`gitlint --ignore body-is-missing --commits origin/main..HEAD` - -- [ ] **Step 4: Push and open the PR** - -Push `LT-22728-local-library-selection` and create a PR titled -`LT-22728: Make local library selection build-scoped`. The body leads with -the reviewer decision, identifies `build.ps1` as the entry point, reports -fresh validation, and ends with one `Next:` line. diff --git a/Docs/superpowers/specs/2026-08-21-local-library-selection-design.md b/Docs/superpowers/specs/2026-08-21-local-library-selection-design.md deleted file mode 100644 index d573b811a4..0000000000 --- a/Docs/superpowers/specs/2026-08-21-local-library-selection-design.md +++ /dev/null @@ -1,61 +0,0 @@ -# Per-build local library selection - -## Problem - -`Build/Manage-LocalLibraries.ps1` turns a one-build choice into persistent -machine and checkout state. It registers a user-level NuGet source, writes the -selected version into `Build/SilVersions.props`, and leaves the restored package -under `packages/`. NuGet identifies a package only by ID and version, so a local -build with the same version as a published package remains valid after its -source `.nupkg` is removed. - -The affected path is shared by palaso, lcm, chorus, machine, and l10nsharp. - -## User contract - -Local selection belongs to one `build.ps1` invocation: - -```powershell -.\build.ps1 -LocalLibraries machine -.\build.ps1 -LocalLibraries palaso,chorus -``` - -The selected libraries are packed before restore and used by that invocation. -Libraries omitted from `-LocalLibraries` are not eligible for restore. A build -without the parameter uses published packages and removes stale local packages -before restore. The local source path and library source paths continue to come -from the existing environment variables. - -## Design - -`Build/LocalLibraries.psm1` owns the library catalogue and cleanup behavior so -`build.ps1` and `Manage-LocalLibraries.ps1` cannot drift. Cache cleanup reads -each managed package version's `.nupkg.metadata`; only entries whose source is a -filesystem path are removed. Entries restored from HTTP sources remain cached. -Matching packages in `LOCAL_NUGET_REPO` are removed before each build, then the -explicitly selected libraries are repacked. - -`Manage-LocalLibraries.ps1` gains an invocation mode that reports the detected -version properties without editing `Build/SilVersions.props`. `build.ps1` -passes those versions as global MSBuild properties to both restore and build. -This keeps the tracked version file clean and makes interruption safe. - -The repository NuGet configuration clears inherited package sources. Ordinary -builds therefore use the repository's published source even if an older run -registered a user-level source named `local`. A local build adds -`LOCAL_NUGET_REPO` to its restore command only for that invocation. - -## Failure behavior - -An explicit local selection fails before restore when its source environment -variable or `LOCAL_NUGET_REPO` is missing, its checkout does not exist, packing -fails, or its packages do not share one version. Cleanup tolerates absent feed -and cache directories. Invalid cache metadata is left in place and reported as -a warning rather than guessed to be local. - -## Verification - -A fixture-based PowerShell test creates temporary local and published cache -entries and proves that cleanup removes only local entries and managed feed -packages for all five library groups. Script compatibility, the repository -build, and the repository test entry point run with `-CommentHygiene`. From 260593ec3f856c202bbd23ca61268e6ef9c104c3 Mon Sep 17 00:00:00 2001 From: John Lambert Date: Sat, 22 Aug 2026 04:36:47 -0400 Subject: [PATCH 4/4] LT-22728: Keep configured sources during local restore Add the local feed through RestoreAdditionalProjectSources so the repository's configured nuget.org source remains valid during solution restore. Cover selected-family eviction of same-version published cache entries. --- Build/LocalLibraries.Tests.ps1 | 13 +++++++++++-- build.ps1 | 12 ++++++------ 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/Build/LocalLibraries.Tests.ps1 b/Build/LocalLibraries.Tests.ps1 index b0301a63b8..f6d4034f53 100644 --- a/Build/LocalLibraries.Tests.ps1 +++ b/Build/LocalLibraries.Tests.ps1 @@ -80,6 +80,13 @@ try { "Cleanup should remove $packageName." } + Clear-FieldWorksLibraryPackageCache -PackagesDirectory $packagesDirectory ` + -Libraries @('machine') + Assert-True (-not (Test-Path $publishedMachine)) ` + 'Selected packing should evict a published cache entry with the same version.' + Assert-True (Test-Path $unrelatedPackage) ` + 'Selected packing should preserve cache entries outside its package family.' + $managerText = Get-Content -LiteralPath ( Join-Path $PSScriptRoot 'Manage-LocalLibraries.ps1') -Raw Assert-True ($managerText -match '\$VersionOutputPath') ` @@ -88,6 +95,8 @@ try { 'Manage-LocalLibraries should import the shared library catalogue.' Assert-True ($managerText -match 'Clear-FieldWorksLocalLibraries') ` 'Manage-LocalLibraries should use the shared cache cleanup.' + Assert-True ($managerText -match 'Clear-FieldWorksLibraryPackageCache') ` + 'Pack mode should evict selected package families before local restore.' Assert-True ($managerText -match 'Packing local libraries is build-scoped') ` 'Direct pack mode should direct callers to build.ps1.' Assert-True ($managerText -notmatch 'dotnet nuget add source') ` @@ -103,8 +112,8 @@ try { Assert-True ($buildText -match 'VersionOutputPath') ` 'build.ps1 should consume non-persistent packed version output.' Assert-True (($buildText -match 'LOCAL_NUGET_REPO') -and ` - ($buildText -match "'--source'")) ` - 'build.ps1 should add the local feed only to an explicit local restore.' + ($buildText -match 'RestoreAdditionalProjectSources')) ` + 'build.ps1 should add the local feed to configured restore sources.' [xml]$nugetConfig = Get-Content -LiteralPath (Join-Path $PSScriptRoot '..\nuget.config') Assert-True ($null -ne $nugetConfig.SelectSingleNode('/configuration/packageSources/clear')) ` diff --git a/build.ps1 b/build.ps1 index 88942d846e..9612d3f23d 100644 --- a/build.ps1 +++ b/build.ps1 @@ -592,7 +592,7 @@ try { -LocalRepository $env:LOCAL_NUGET_REPO $localVersionProperties = [ordered]@{} - $localRestoreSourceArgs = @() + $localRestoreSourceArg = $null if ($LocalLibraries.Count -gt 0) { if ($SkipRestore) { throw '-LocalLibraries cannot be combined with -SkipRestore.' @@ -626,10 +626,8 @@ try { Remove-Item -LiteralPath $versionOutputPath -Force } } - $localRestoreSourceArgs = @( - '--source', $env:LOCAL_NUGET_REPO, - '--source', 'https://api.nuget.org/v3/index.json' - ) + $localRestoreSourceArg = + "/p:RestoreAdditionalProjectSources=$($env:LOCAL_NUGET_REPO)" } # ============================================================================= @@ -767,7 +765,9 @@ try { foreach ($propertyName in $localVersionProperties.Keys) { $restoreArgs += "/p:$propertyName=$($localVersionProperties[$propertyName])" } - $restoreArgs += $localRestoreSourceArgs + if ($localRestoreSourceArg) { + $restoreArgs += $localRestoreSourceArg + } & dotnet restore @restoreArgs if ($LASTEXITCODE -ne 0) { throw "NuGet package restore failed for FieldWorks.sln"