diff --git a/Build/LocalLibraries.Tests.ps1 b/Build/LocalLibraries.Tests.ps1 new file mode 100644 index 0000000000..f6d4034f53 --- /dev/null +++ b/Build/LocalLibraries.Tests.ps1 @@ -0,0 +1,133 @@ +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." + } + + 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') ` + '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 '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') ` + '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 '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')) ` + '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..9612d3f23d 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,51 @@ 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]@{} + $localRestoreSourceArg = $null + 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 + } + } + $localRestoreSourceArg = + "/p:RestoreAdditionalProjectSources=$($env:LOCAL_NUGET_REPO)" + } + # ============================================================================= # Build Configuration # ============================================================================= @@ -616,6 +671,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 +715,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 +754,21 @@ 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])" + } + if ($localRestoreSourceArg) { + $restoreArgs += $localRestoreSourceArg + } + & 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')) {