From b43a70e8bd299b6f52cbe79c18e5780c038895e0 Mon Sep 17 00:00:00 2001 From: John Lambert Date: Mon, 21 Sep 2026 09:38:24 -0400 Subject: [PATCH 1/6] LT-22801: Keep Fluent in patches and check patches keep components Patches 9.3.11.2649 to 9.3.12.2718 on base 1452 shipped Avalonia.Themes.Fluent.dll. The Semi.Avalonia switch dropped it, so patches from 9.3.12.2752 on do not contain its component. Each patch supersedes the one before, so Windows Installer sees only base plus newest patch while machines still have Fluent's component registered. It logs SELMGR, treats feature Complete as advertised, copies nothing and returns 0, and the version users see still advances. Restore the component with a zero-byte RescuePatching placeholder at the same path, as already done for StructureMap.dll. LT-22807 tracks removing it before the next base. pyro only compares a patch with the base, so it cannot see a patch dropping a component an earlier patch added. Add two checks to the patch workflow, before signing: - Test-PatchComponentLedger.ps1 compares the new patch with every component published patches on the base have added: a committed seed ledger for base 1452, plus a ledger now published next to each .msp. It names the dropped file and the fix. - Test-PatchInstall.ps1 installs the published base and the latest patch, applies the new patch with MSIENFORCEUPGRADECOMPONENTRULES=1 (error 2771 instead of silent success), and checks that FieldWorks.exe on disk carries the new version. All 53 patches published on base 1452 were read into the seed. Fluent is the only component any of them dropped. Docs/workflows/patch-component-removal.md explains the failure, the fix, and the steps before cutting a new base. Co-Authored-By: Claude Opus 5 --- .github/workflows/patch-installer-cd.yml | 40 +++- Build/Installer.legacy.targets | 11 +- Docs/workflows/patch-component-removal.md | 124 +++++++++++ FLExInstaller/AGENTS.md | 1 + FLExInstaller/PatchComponentLedger/b1452.tsv | 120 ++++++++++ scripts/Installer/PatchComponentLedger.psm1 | 207 ++++++++++++++++++ .../Installer/Test-PatchComponentLedger.ps1 | 91 ++++++++ scripts/Installer/Test-PatchInstall.ps1 | 127 +++++++++++ 8 files changed, 716 insertions(+), 5 deletions(-) create mode 100644 Docs/workflows/patch-component-removal.md create mode 100644 FLExInstaller/PatchComponentLedger/b1452.tsv create mode 100644 scripts/Installer/PatchComponentLedger.psm1 create mode 100644 scripts/Installer/Test-PatchComponentLedger.ps1 create mode 100644 scripts/Installer/Test-PatchInstall.ps1 diff --git a/.github/workflows/patch-installer-cd.yml b/.github/workflows/patch-installer-cd.yml index 9541bc27fc..1ae830f877 100644 --- a/.github/workflows/patch-installer-cd.yml +++ b/.github/workflows/patch-installer-cd.yml @@ -39,7 +39,7 @@ on: # on Jenkins-built bases. base_release: description: 'The github release for the base build artifacts (separate only for bootstrapping; should be removed after 9.3 is the stable)' - default: 'build-1452' # When updating this, update base_build_number and their fallbacks below, too. + default: 'build-1452' # When updating this, update base_build_number and their fallbacks below, too. Before cutting a new base, follow Docs/workflows/patch-component-removal.md. base_build_number: description: 'The base build number' required: false @@ -258,6 +258,37 @@ jobs: $patchPath = $patch.FullName "patch_file=$patchPath" >> $env:GITHUB_OUTPUT + "patch_version=$(($patch.Name -split '_')[1])" >> $env:GITHUB_OUTPUT + "ledger_file=$($patchPath -replace '\.msp$', '_components.tsv')" >> $env:GITHUB_OUTPUT + "update_msi=$((Get-ChildItem PatchableInstaller/CreateUpdatePatch/Update -Filter '*.msi' | Select-Object -First 1).FullName)" >> $env:GITHUB_OUTPUT + "master_msi=$((Get-ChildItem PatchableInstaller/CreateUpdatePatch/Master -Filter '*.msi' | Select-Object -First 1).FullName)" >> $env:GITHUB_OUTPUT + + # A patch must keep every component any patch on this base has shipped, or Windows + # Installer silently installs nothing (LT-22801). See Docs/workflows/patch-component-removal.md. + - name: Check patch keeps every published component + shell: pwsh + run: | + .\scripts\Installer\Test-PatchComponentLedger.ps1 ` + -MasterMsi "${{ steps.find_patch.outputs.master_msi }}" ` + -UpdateMsi "${{ steps.find_patch.outputs.update_msi }}" ` + -BaseBuildNumber $env:BASE_BUILD_NUMBER ` + -PatchVersion "${{ steps.find_patch.outputs.patch_version }}" ` + -SeedLedger "FLExInstaller/PatchComponentLedger/b$($env:BASE_BUILD_NUMBER).tsv" ` + -OutLedger "${{ steps.find_patch.outputs.ledger_file }}" + + - name: Test patch install over the latest published patch + shell: pwsh + timeout-minutes: 60 + run: | + $ledgers = @("FLExInstaller/PatchComponentLedger/b$($env:BASE_BUILD_NUMBER).tsv", "${{ steps.find_patch.outputs.ledger_file }}") + $ledgers += @(Get-ChildItem (Join-Path ([IO.Path]::GetTempPath()) "fw-patch-ledgers-b$($env:BASE_BUILD_NUMBER)") -Filter '*.tsv' -ErrorAction SilentlyContinue | ForEach-Object { $_.FullName }) + .\scripts\Installer\Test-PatchInstall.ps1 ` + -Patch "${{ steps.find_patch.outputs.patch_file }}" ` + -PatchVersion "${{ steps.find_patch.outputs.patch_version }}" ` + -BaseBuildNumber $env:BASE_BUILD_NUMBER ` + -UpdateMsi "${{ steps.find_patch.outputs.update_msi }}" ` + -LedgerPaths $ledgers ` + -WorkDir "${{ runner.temp }}/patch-install-test" - name: Sign Patch if: github.event_name != 'pull_request' @@ -291,6 +322,11 @@ jobs: $s3Key = "jobs/FieldWorks-Win-all-Release-Patch/$($env:FW_BUILD_NUMBER)/$patchFile" aws s3 cp $patchPath "s3://flex-updates/$s3Key" Write-Host "Uploaded to s3://flex-updates/$s3Key" + # Later patches on this base are checked against every published ledger. + $ledgerPath = "${{ steps.find_patch.outputs.ledger_file }}" + $ledgerKey = "jobs/FieldWorks-Win-all-Release-Patch/$($env:FW_BUILD_NUMBER)/$(Split-Path $ledgerPath -Leaf)" + aws s3 cp $ledgerPath "s3://flex-updates/$ledgerKey" + Write-Host "Uploaded to s3://flex-updates/$ledgerKey" - name: Upload Build Logs uses: actions/upload-artifact@v7 @@ -305,3 +341,5 @@ jobs: ./Output/**/*.log.stderr ./PatchableInstaller/CreateUpdatePatch/Master/AppHarvest.wxs ./PatchableInstaller/CreateUpdatePatch/Update/AppHarvest.wxs + ./BuildDir/*_components.tsv + ${{ runner.temp }}/patch-install-test/*.log diff --git a/Build/Installer.legacy.targets b/Build/Installer.legacy.targets index ce2a6acb03..320e1cab6f 100644 --- a/Build/Installer.legacy.targets +++ b/Build/Installer.legacy.targets @@ -105,10 +105,8 @@ /> @@ -116,6 +114,11 @@ + + .tsv` plus the + `*_components.tsv` ledger published next to each `.msp`. `pyro` already rejects + dropping a file the base itself ships (`PYRO0305`), so this check covers the + patch-added files `pyro` cannot see. +- **Install test** (`scripts/Installer/Test-PatchInstall.ps1`). Installs the published + base, applies the latest published patch, then applies the new patch with + `MSIENFORCEUPGRADECOMPONENTRULES=1`, which turns the silent failure into error 2771. + It also checks that `FieldWorks.exe` on disk carries the new patch's version. + +Both name the dropped file: + +``` +Patch 9.3.12.2754 drops component {B3A225EB-3642-5FE4-8ED4-B2DAE6F8AC9B} + file: Avalonia.Themes.Fluent.dll (feature Complete) + shipped: patches 9.3.12.2718 to 9.3.12.2718 on base 1452 +Fix: restore the file to the output, or add a component stand-in (see + Docs/workflows/patch-component-removal.md), as well as an issue to remove + the file before cutting the next base build. +``` + +## When a check fails + +Do both of the following. + +### 1. Keep the component in the patch + +**Restore the file (the default).** Add it to `RemovedSinceLastBase` in the +`RescuePatching` target of `Build/Installer.legacy.targets`, at the **same path** it +shipped from: + +```xml + +``` + +The target writes a zero-byte placeholder there, so the component stays in every patch. +Machines that already have the real file keep it, because Windows Installer does not +replace a versioned file with an unversioned one. The dead file costs a few bytes until +the next base. + +**Component stand-in (only when the old file must not stay on disk).** Use this when +leaving the file behind is unsafe, for example a plugin that FieldWorks finds by +scanning a directory. Keep the placeholder above, and author a `RemoveFile` entry for +that path in `FLExInstaller/CustomComponents.wxi` so the real file is deleted. This +has not yet been validated on a patch line. Test it by installing the base, the +previous patch and the new patch before relying on it. Cutting a new base is the safe +alternative. + +### 2. File an issue to remove the file before the next base + +Create an LT issue with the label `remove-before-next-base`: + +``` +Summary: Remove placeholder before the next base build +Labels: remove-before-next-base + + (component , feature ) left the build output in , +after patches to on base shipped it. It is kept alive by a +RemovedSinceLastBase placeholder in Build/Installer.legacy.targets so patches on +base keep working. + +Before the next base build: +- Remove the RemovedSinceLastBase entry. +- If any published patch on base already dropped the component, machines + that applied it keep an orphaned copy that an upgrade will not remove; add a + RemoveFile for its path to the new base. +``` + +## Before cutting a new base + +A new base is the one point where dropping files is legal: it has a new ProductCode, +and it removes the old product completely before installing (`RemoveExistingProducts` +runs before `InstallInitialize`). + +1. Resolve every open issue labelled `remove-before-next-base`. The base build warns + while `RemovedSinceLastBase` still has entries. +2. Add a `RemoveFile` for every file an already-published patch orphaned. An upgrade + only removes what the old product's current patch still lists. +3. A new base needs no seed ledger: its patches publish their own ledgers from the + first patch on. + +## Running the checks locally + +```powershell +.\scripts\Installer\Test-PatchComponentLedger.ps1 -MasterMsi -UpdateMsi ` + -BaseBuildNumber 1452 -PatchVersion 9.3.12.2761 ` + -SeedLedger FLExInstaller\PatchComponentLedger\b1452.tsv -OutLedger out.tsv +``` + +The install test changes the machine's FieldWorks installation; run it only on a +disposable machine, from an elevated prompt. diff --git a/FLExInstaller/AGENTS.md b/FLExInstaller/AGENTS.md index 91abd4a475..494aae9833 100644 --- a/FLExInstaller/AGENTS.md +++ b/FLExInstaller/AGENTS.md @@ -17,6 +17,7 @@ Minimal installer guidance for agents. - **Stopgap fix (patch against the existing base):** add the dropped file to the **`RemovedSinceLastBase`** item list in the **`RescuePatching`** target of **`Build/Installer.legacy.targets`** (runs via `BuildProduct`). It writes a zero-byte placeholder into the build output (**`$(dir-outputBase)`**) so the file appears in both harvests and `pyro` treats it as *changed*, not *removed*. Mirror the existing entries (`ManagedVwWindow.manifest`, `SimpleRootSite.manifest`) and add **only** the basenames actually dropped — usually just the `*.manifest`, not a still-shipping `.dll`. - **Permanent fix:** cut a new **Base** build so the file is absent from Master too, then delete the now-stale `RemovedSinceLastBase` entries (the target comment notes a base build should warn when these exist). - **Do not** add it to **`PatchableInstallerHeatExclude.xml`** — that list is for artifacts that must never be harvested (build-output dedup / test-only files), not for reconciling files removed since the base. +- **Patch drops a component an earlier patch shipped:** `pyro` cannot see this (it only compares against the base), and the patch installs nothing while returning success. The patch workflow's component-ledger check and install test catch it; the fix is the same `RemovedSinceLastBase` placeholder plus a `remove-before-next-base` issue. See **`Docs/workflows/patch-component-removal.md`**. ## Constraints diff --git a/FLExInstaller/PatchComponentLedger/b1452.tsv b/FLExInstaller/PatchComponentLedger/b1452.tsv new file mode 100644 index 0000000000..1dcb09a5c0 --- /dev/null +++ b/FLExInstaller/PatchComponentLedger/b1452.tsv @@ -0,0 +1,120 @@ +# Components patches published on base 1452 added, from every .msp on the update bucket +# ComponentId Component File Feature FirstShipped LastShipped +{DA0940BD-0FA2-5C72-BC6F-EFADFC01AF8A} cmp8F63659DC44FBC0F7703F8C24F1DF4DC AIExportInstructions.md Complete 9.3.11.2693 9.3.12.2760 +{D3F3D4D1-690D-5570-90C3-07AC15DBF944} cmp6F532A88487F846B64ADC73FD2069DC9 Avalonia.Base.dll Complete 9.3.11.2649 9.3.12.2760 +{48987B23-7456-544F-A320-AD7F5DD13B67} cmp295D431BF73CBADDD7FCD13AC811E14D Avalonia.Controls.dll Complete 9.3.11.2649 9.3.12.2760 +{644EFFE3-9DFF-507A-8D53-17A1167496F8} cmp515AEF8E04CEE9DBD98D566FFBFC5465 Avalonia.DesignerSupport.dll Complete 9.3.11.2649 9.3.12.2760 +{DB85E238-F9C9-55B7-8C76-FA2CC185A8E3} cmp60F16D63CA4BC46430A7ECB89F46FA18 Avalonia.Desktop.dll Complete 9.3.11.2649 9.3.12.2760 +{DBF8B74B-D91F-5F23-B4D1-E55DC755DCE7} cmpFA9EACE595AFE0745CF5143237D52623 Avalonia.Dialogs.dll Complete 9.3.11.2649 9.3.12.2760 +{B7C5DCDD-0DF8-5436-AF56-55A83851A666} cmp660B3182149886A1D06AF308784F3CBC Avalonia.dll Complete 9.3.11.2649 9.3.12.2760 +{91ABBE9E-0556-5EC8-9221-482000C1A71C} cmpF938C1EDEF7D137C951EE38003E863C8 Avalonia.FreeDesktop.dll Complete 9.3.11.2649 9.3.12.2760 +{2DA9E2A1-1CC9-5E58-A5BD-2B6C11CF4040} cmpB34F12C6C13ACAA9D4391C4044F7C1A4 Avalonia.Headless.dll Complete 9.3.11.2649 9.3.12.2760 +{BECB2C60-A212-524F-8EE7-E42C5491F78E} cmp49FE4DA9C83AA91FE4179EAD8590BF5F Avalonia.Headless.NUnit.dll Complete 9.3.11.2649 9.3.12.2760 +{59864563-443D-544D-BCDC-7C0D85EF7D44} cmp078E9C2E631026E5BBBDBBB010B6D7D0 Avalonia.Markup.dll Complete 9.3.11.2649 9.3.12.2760 +{5719C05E-839F-5F34-BBEC-AB1D3F0209F8} cmp538664F9813F44D6D4610976AC1D20B5 Avalonia.Markup.Xaml.dll Complete 9.3.11.2649 9.3.12.2760 +{A844E72E-3716-5E1E-91F6-789F62C9AC5B} cmpF639D8A1AAA4AC4F63933E45B12FAD75 Avalonia.Metal.dll Complete 9.3.11.2649 9.3.12.2760 +{03A04376-78E7-5146-9381-B8BC765D0E87} cmp7DD0D4584C33CDF6F05FD47176EC792A Avalonia.MicroCom.dll Complete 9.3.11.2649 9.3.12.2760 +{056CF987-318F-53BB-A76C-3ABFD17DA11B} cmp19619B1BF81C2A500731F072FC6EEDA7 Avalonia.Native.dll Complete 9.3.11.2649 9.3.12.2760 +{264A8059-4F17-5B37-95DA-F5E26A11902E} cmp58CBF46A4A82BB2422AA8BD590F27D92 Avalonia.OpenGL.dll Complete 9.3.11.2649 9.3.12.2760 +{E8EC545A-7485-5166-9315-E972C22E04D0} cmpE54DA5F709820A816226FCBC2E8F9B5B Avalonia.Remote.Protocol.dll Complete 9.3.11.2649 9.3.12.2760 +{8216A4FF-C3C9-5F6A-BBB3-97862E97EAD2} cmp0B0E423744DFBF7DB1AD90D3E5B94209 Avalonia.Skia.dll Complete 9.3.11.2649 9.3.12.2760 +{B3A225EB-3642-5FE4-8ED4-B2DAE6F8AC9B} cmpF906DFA4FA548A236D02C0A143CCEF15 Avalonia.Themes.Fluent.dll Complete 9.3.11.2649 9.3.12.2718 +{4239BA48-B86B-52D5-99AD-0F0AFFB034BD} cmp6BFB5ADF4855D99B80240BFB4127E6F5 Avalonia.Vulkan.dll Complete 9.3.11.2649 9.3.12.2760 +{B2E449AC-85AC-540C-83A0-7C8CB9005FB0} cmpFC099FB902BFDDE81E4773FB637CD110 Avalonia.Win32.Automation.dll Complete 9.3.11.2649 9.3.12.2760 +{C358C2B8-04D4-5E0A-AE61-8D436EDC1860} cmp47567C3CF2B33B158D6003CCA591A2C0 Avalonia.Win32.dll Complete 9.3.11.2649 9.3.12.2760 +{481D8C24-66DE-5F14-ABB1-C80300D372AA} cmpD97DACBAF1F92AF60E34BCB54D4891A6 Avalonia.Win32.Interoperability.dll Complete 9.3.11.2649 9.3.12.2760 +{C0DBE567-DE9D-595A-85F7-48B64DE5B3DB} cmp7519FFB6B965DC9DDB696DB54C3365A9 Avalonia.X11.dll Complete 9.3.11.2649 9.3.12.2760 +{9C145F8E-5738-503C-87CE-710C2DEE46B8} cmpBA8AFE637D8BF45BE83C8678EFE986F5 CommunityToolkit.Mvvm.dll Complete 9.3.11.2649 9.3.12.2760 +{138EDD05-02B0-5C55-9DBA-6A0CC9726DA3} cmpE3398985DA0F0DBF2A03ADA8BABB122E FwAvalonia.dll Complete 9.3.11.2649 9.3.12.2760 +{8F71DC11-6DCD-5578-897F-05A3A507BD50} cmp7F8FEA8233D91ADDE7F99189DEEE06E7 FwAvalonia.dll.config Complete 9.3.11.2649 9.3.12.2760 +{EC9D9EB3-991F-5563-AEF3-7B290DD448D9} cmp4B58D41AC8D441E39C67C9FE6E6F4B85 FwAvalonia.pdb Complete 9.3.11.2649 9.3.12.2760 +{00FD6E6A-496C-5310-AD67-45189D67AF41} cmpC744447C9E3167AF5E91CD4C5D78C756 FwAvalonia.resources.dll Spanish 9.3.11.2649 9.3.12.2760 +{095448E5-5112-5EEF-92F5-FB28D4FCDDC0} cmp5D3F85D73A47D90E315C4469F29EA51B FwAvalonia.resources.dll French 9.3.11.2649 9.3.12.2760 +{0D6418A5-F6D6-5090-A3D1-8FEB78E449D3} cmpC6AF4718DDA44D0F92E882FA0FA083BC FwAvalonia.resources.dll Vietnamese 9.3.11.2649 9.3.12.2760 +{16380557-C64E-51F8-B35A-0B92F957BF35} cmpF9A97A3D1E5F46CC75623A11AF8B64D9 FwAvalonia.resources.dll Malay 9.3.11.2649 9.3.12.2760 +{1DAFF7DC-57B1-500B-A643-563C8015367A} cmp3D7CC9B577D9CE4B86D7EBB3E918BC61 FwAvalonia.resources.dll Urdu 9.3.11.2649 9.3.12.2760 +{21B4E444-8349-54D4-88E7-84936048C2A4} cmp5E829FE1D852405FCE4A88CC0E6D8721 FwAvalonia.resources.dll Azerbaijani 9.3.11.2649 9.3.12.2760 +{255AD1F7-CA36-5D6C-B903-A887CFAD66F1} cmpDCEA9861FD22B1B0B313AB6ED5FF566A FwAvalonia.resources.dll Hindi 9.3.11.2649 9.3.12.2760 +{27549925-9A72-5265-9DA2-5F5524D9ED0D} cmp64F38BDDCA32FB2EB6BD554E10E3F1BE FwAvalonia.resources.dll Malay 9.3.11.2649 9.3.12.2760 +{3E327A7D-94CD-5BD8-827C-38635FCF53E9} cmpC204756496A4292493EE634E2478E7D9 FwAvalonia.resources.dll Swahili 9.3.11.2649 9.3.12.2760 +{410A087A-45EF-5C59-AF88-BD60A54FFC28} cmpD6A8E5A8331698ECB9A1A4B73E4027FA FwAvalonia.resources.dll Portuguese 9.3.11.2649 9.3.12.2760 +{4ABB6A21-6F95-58B2-8C16-38B74CBD220F} cmp87AFA172D4ED6657F2F23425A93039DC FwAvalonia.resources.dll Yoruba 9.3.11.2649 9.3.12.2760 +{4B40E5CB-30D5-5677-A13C-F62EB4A6C9EF} cmp186CD5D44F2C490314A17B8B12F1E458 FwAvalonia.resources.dll Chinese 9.3.11.2649 9.3.12.2760 +{55F6DA1E-A17F-5608-88B9-61512E42CDF8} cmp79796BEE4D10F8D4A1A7E3E37CA2F4A1 FwAvalonia.resources.dll Korean 9.3.11.2649 9.3.12.2760 +{6E063CEC-98DE-598E-80D4-4708FCA0B412} cmp6830D5813172E2AB5E8303BEE2E0D6B6 FwAvalonia.resources.dll Russian 9.3.11.2649 9.3.12.2760 +{794FB841-8C1A-502E-88DA-2E32D9296248} cmp3F935CA18EB62F02F358BBE9FDED913A FwAvalonia.resources.dll Khmer 9.3.11.2649 9.3.12.2760 +{7997DB88-EE95-5A10-8CF7-DC77008DC0AC} cmp0F87125353CEA8E8B6313529DD7C6BD6 FwAvalonia.resources.dll Nepali 9.3.11.2649 9.3.12.2760 +{7A5ED619-AA06-5F23-944F-1279F28BCABB} cmp7206F0E70BCBF954AE4922BFF706BB3E FwAvalonia.resources.dll Thai 9.3.11.2649 9.3.12.2760 +{8A908B5D-2495-5A62-8808-ABC896FA4846} cmp5E731BC4EE8DF18A3F911E4310268089 FwAvalonia.resources.dll Bengali 9.3.11.2649 9.3.12.2760 +{9C8D0766-C594-522A-B364-E73AF641AC0E} cmp0A8A05DD0C9C50DC093C17291DF69487 FwAvalonia.resources.dll Arabic 9.3.11.2649 9.3.12.2760 +{A7530E25-7D26-5C02-849F-F1FC632EE39B} cmpF8D6A786D009147E667F3BD4E45726BA FwAvalonia.resources.dll Telugu 9.3.11.2649 9.3.12.2760 +{C2E15D6E-5AF8-563D-95F7-2BDC22918634} cmpFAAF6F77C41BD26E21712EBD302909FC FwAvalonia.resources.dll Malayalam 9.3.11.2649 9.3.12.2760 +{C3CD0E9D-6C19-5863-A3E3-D1577FB57C66} cmp3AB81E523E57BB90F1BDB4311D8BDF4A FwAvalonia.resources.dll Hungarian 9.3.11.2649 9.3.12.2760 +{CB6C07D2-C204-546F-890C-49AE15F6FE32} cmp88CA64FE2DAD35DB3F64594BDF4BB4D6 FwAvalonia.resources.dll Persian 9.3.11.2649 9.3.12.2760 +{CC2F72DF-B827-5792-B766-B08667192EF5} cmp340F48912B1B687BCD4BD984A600BB75 FwAvalonia.resources.dll German 9.3.11.2649 9.3.12.2760 +{DD37BF7D-3A4E-5B2A-8798-4D596EFE60B8} cmp25D6A9F9FFEDC837E8298EB5BB3F9B8B FwAvalonia.resources.dll Turkish 9.3.11.2649 9.3.12.2760 +{E367A90A-C4A6-5896-B326-44C546900329} cmp801B580FFEEAEF4B4D1F0E2135AB3F37 FwAvalonia.resources.dll Indonesian 9.3.11.2649 9.3.12.2760 +{E6B9C3BB-1AEE-5A2D-B322-031AF8E32FD0} cmp3BFF24169B2639842124EDA8D54B6A60 FwAvalonia.resources.dll Kinyarwanda 9.3.11.2649 9.3.12.2760 +{E6FA6A1F-FC77-5110-A4BE-575606D2F057} cmpB13758157B2FFEACC849C5CA49BD3E87 FwAvalonia.resources.dll Tamil 9.3.11.2649 9.3.12.2760 +{F554DE59-8F6E-53EE-B238-97924BED4754} cmp945ABA08FFF1DB122D7B99A11C274AA7 FwAvalonia.resources.dll Burmese 9.3.11.2649 9.3.12.2760 +{2E1C86F7-F728-5D5E-BDCC-64FF868A46B5} cmp92ECD687E7DEA3EA930AE1FA471F3062 FwAvaloniaDialogs.dll Complete 9.3.11.2649 9.3.12.2760 +{46FF9909-6F68-58B3-A907-0550222ACD29} cmpF54565E87EF3FD52203F26835085A430 FwAvaloniaDialogs.dll.config Complete 9.3.11.2649 9.3.12.2760 +{F2BB0A37-FE52-53C8-8696-EBE0F4D47B97} cmpCD1512CFB467FC5C896C997AA6D869DB FwAvaloniaDialogs.pdb Complete 9.3.11.2649 9.3.12.2760 +{025484A1-AA3B-55C6-95C9-73ED057EC6E9} cmp82D0BAE64C8AE203065262909273FD30 FwAvaloniaDialogs.resources.dll Swahili 9.3.11.2649 9.3.12.2760 +{096C45C2-0865-5070-AA31-CC96DF0D5B1C} cmpD94D58DC6BF163351A6EFA6F601B0D6C FwAvaloniaDialogs.resources.dll Malayalam 9.3.11.2649 9.3.12.2760 +{0EB6AB4E-F822-5500-997F-A9BE4856EDD4} cmp1F15778C89D148F0DFD7CCD9BFEE602A FwAvaloniaDialogs.resources.dll German 9.3.11.2649 9.3.12.2760 +{1D35E836-98AB-53E1-83C2-70BC05A2080D} cmp229E2BA7226AE09E8C70783A6AE9ABFC FwAvaloniaDialogs.resources.dll Indonesian 9.3.11.2649 9.3.12.2760 +{1D47C484-8E7C-547A-A735-349E27ED325C} cmp47826A817F068F909056FE903637B0EA FwAvaloniaDialogs.resources.dll Burmese 9.3.11.2649 9.3.12.2760 +{1D4C2AF2-1935-5C95-8222-7C00123A78C8} cmp4A4AA69EF39D1241CB0C302CDC811063 FwAvaloniaDialogs.resources.dll Malay 9.3.11.2649 9.3.12.2760 +{1DBB57F5-5373-581C-A456-A9F845FF4874} cmp26917791EA6C95E2841DBCBBEE622324 FwAvaloniaDialogs.resources.dll Korean 9.3.11.2649 9.3.12.2760 +{2D2E0509-0ED6-5178-8FA4-A2D5AD592ED1} cmp01D0C349B81F490963ED6D773A0770FA FwAvaloniaDialogs.resources.dll Vietnamese 9.3.11.2649 9.3.12.2760 +{37663C83-A7D4-5024-ACD5-C7B6C0C2B068} cmp2BBC973D8DDF0B18438CA6A491393FE8 FwAvaloniaDialogs.resources.dll Urdu 9.3.11.2649 9.3.12.2760 +{49755DFC-89B1-593E-9E91-E388A48DE600} cmpDCFF496901CBF45AD09A85AFCB24021A FwAvaloniaDialogs.resources.dll Turkish 9.3.11.2649 9.3.12.2760 +{4D129082-FC11-56CA-B192-6B7566F4A34A} cmp31824781D8DC1AEED4F3ABA81DE48FD6 FwAvaloniaDialogs.resources.dll Azerbaijani 9.3.11.2649 9.3.12.2760 +{50F3C03B-659A-5539-87FF-935A26F987C1} cmpFD1E1A0F89D09757FD956A3DB67ADCD5 FwAvaloniaDialogs.resources.dll Nepali 9.3.11.2649 9.3.12.2760 +{53B372C4-1023-5014-A058-578E747D66D0} cmpD443A7DFF977E28AC44D447D0E4ACA28 FwAvaloniaDialogs.resources.dll Chinese 9.3.11.2649 9.3.12.2760 +{64770BF4-4611-528B-B7FC-FC111E3716D8} cmp4A898A1A999245077C43B9DE7492E771 FwAvaloniaDialogs.resources.dll Tamil 9.3.11.2649 9.3.12.2760 +{72531CD7-CC5E-533A-AF39-6D483D1ED03C} cmp65F49F9673D480828B04BF10B610B57E FwAvaloniaDialogs.resources.dll Malay 9.3.11.2649 9.3.12.2760 +{745A883C-E47D-59DF-952F-10AF51A18C74} cmp6E086A438A2AACEF005D057CE578D9CB FwAvaloniaDialogs.resources.dll Bengali 9.3.11.2649 9.3.12.2760 +{8321A3AA-69DB-589F-B83C-082377704A96} cmp1F13154539E7DBB2EB8C18144FC5F348 FwAvaloniaDialogs.resources.dll Spanish 9.3.11.2649 9.3.12.2760 +{8628C43E-FEB8-5C6D-BB7C-AD7E8BA9FBC1} cmp5D49E4ADE5C1736CEB872B432EDC90C3 FwAvaloniaDialogs.resources.dll Telugu 9.3.11.2649 9.3.12.2760 +{88681579-7969-519F-B1EE-3E1EFE158CB2} cmpD432FEB57D28D2492832BD160AFF67CC FwAvaloniaDialogs.resources.dll Yoruba 9.3.11.2649 9.3.12.2760 +{9D17CCAC-F4D6-557D-B086-5845094BC327} cmpBED07ADBDE4F22C424C02A2E94D50C3E FwAvaloniaDialogs.resources.dll Persian 9.3.11.2649 9.3.12.2760 +{A7A53726-0B9F-5AEE-B4D9-9F8003C1F59C} cmpEF8DD91FEE84E88C37CA4C42D04E90D6 FwAvaloniaDialogs.resources.dll Hungarian 9.3.11.2649 9.3.12.2760 +{AE8A94BA-6C1D-532F-B799-B3DE4D5E9CFB} cmp2CE4A58C7B7E963CAB0BF07C1C4DBEC1 FwAvaloniaDialogs.resources.dll Khmer 9.3.11.2649 9.3.12.2760 +{B62B13E6-14E2-5C8B-8FCF-5CF490563A4D} cmpC627A09DB11908C254EEC3597593421B FwAvaloniaDialogs.resources.dll French 9.3.11.2649 9.3.12.2760 +{B89D76C9-CED6-5BD8-B0F8-D8E6C44877B9} cmp26A637EFBC7F7116A6679D273E518E11 FwAvaloniaDialogs.resources.dll Thai 9.3.11.2649 9.3.12.2760 +{BD56E3F0-0E09-5EF5-A74B-8748F197F499} cmpCBC576B7692DE2BE123CAA7259F8CE52 FwAvaloniaDialogs.resources.dll Portuguese 9.3.11.2649 9.3.12.2760 +{CA22F8D0-D083-5141-93AD-71E6041710EA} cmp23734D77C22D73F0B5DF8C0F2CE427E0 FwAvaloniaDialogs.resources.dll Hindi 9.3.11.2649 9.3.12.2760 +{D49F26B5-8080-5FF3-88BC-1DC75102689C} cmpA0D7F5AFE0D912C3224C272F9208085D FwAvaloniaDialogs.resources.dll Arabic 9.3.11.2649 9.3.12.2760 +{D9A7DB60-7CA4-50B0-920B-8A038759BC74} cmp711B57B27D65861D56230B157C4964D6 FwAvaloniaDialogs.resources.dll Russian 9.3.11.2649 9.3.12.2760 +{EF8358F1-8081-5F3D-B297-50FEC1198EE7} cmp1BA32594D3E185D6D2745ECACB492618 FwAvaloniaDialogs.resources.dll Kinyarwanda 9.3.11.2649 9.3.12.2760 +{D4721227-54F7-5298-ABDE-798F9DF95320} cmp03AA89612A561251F9826F318D4FAF65 FwAvaloniaTheme.dll Complete 9.3.12.2752 9.3.12.2760 +{769C31AE-8E95-5728-9466-6FAE837AD657} cmp5C17B3B30B60CA33F9AABA9F3A10FAEE FwAvaloniaTheme.dll.config Complete 9.3.12.2752 9.3.12.2760 +{591C7319-CC1C-5514-B40F-0FA78CC9719B} cmpA6C9E9D197AF34C0EFF5656327CEEC27 FwAvaloniaTheme.pdb Complete 9.3.12.2752 9.3.12.2760 +{FC64481F-105F-5B2B-949C-54CDE425D396} cmpB76607CAFE1A9394C7D7CAEFD4B08D44 GrammarAndTextsForAI.xml Complete 9.3.11.2693 9.3.12.2760 +{B7B635E2-E51D-5EAC-B1CA-5DE35930FBA7} cmp4069FEDB42291ACEC7EB1440D0F53295 Irihi.Avalonia.Shared.Contracts.dll Complete 9.3.12.2752 9.3.12.2760 +{A65A90B9-142C-5DE4-B98F-EBC0BEECC456} cmp24C294AE5B98E64C128EEF9741042D0B Irihi.Avalonia.Shared.dll Complete 9.3.12.2752 9.3.12.2760 +{03468303-7A73-5B8F-9CB9-12F7B474EC40} cmp5E8EA429E963CFDEE82E15B4D5EEAFF6 libHarfBuzzSharp.so Complete 9.3.11.2649 9.3.12.2760 +{0C2D3395-CC96-5DB2-B7AE-442D28188444} cmp44CC67C4A96CC21E9A1D04B1CDE5EB3E libHarfBuzzSharp.so Complete 9.3.11.2649 9.3.12.2760 +{11F24985-B4F5-528B-8A59-A68F59E5574F} cmp734B1D3171E8EF6DEE9CF002FA3BA092 libHarfBuzzSharp.so Complete 9.3.11.2649 9.3.12.2760 +{1628A355-94ED-50C5-BEBB-76F3868663D7} cmpB9C172DC098B1EBFF4F47862F87A18F7 libHarfBuzzSharp.so Complete 9.3.11.2649 9.3.12.2760 +{24E1EA66-A06A-54CF-B466-E1D7AED52520} cmp07D38210B64F2AA12901CC6E43784185 libHarfBuzzSharp.so Complete 9.3.11.2649 9.3.12.2760 +{2B0CEAD2-F24F-591A-B172-15D0495595A6} cmp6D817951C151B5EF9B904E5F900A923B libHarfBuzzSharp.so Complete 9.3.11.2649 9.3.12.2760 +{900E3373-22F8-5504-9CE7-F7BE3F578548} cmp6566A436A2C4EDA4BAA540418FEE751F libHarfBuzzSharp.so Complete 9.3.11.2649 9.3.12.2760 +{B341AC2F-6FA0-5187-8BD6-6E8404A66637} cmp6CDEB1E669EB925C351C990B06CC5FF3 libHarfBuzzSharp.so Complete 9.3.11.2649 9.3.12.2760 +{D31A4F25-2D4F-5322-A669-38BB67F32D48} cmp439EF9315EA4FB2955A9AE542415FD40 libHarfBuzzSharp.so Complete 9.3.11.2649 9.3.12.2760 +{D85EA165-6DAE-54F0-9E9D-67B043E40574} cmp6E14E1BEB29F1E11962464578D3949AA libHarfBuzzSharp.so Complete 9.3.11.2649 9.3.12.2760 +{F792B87F-4043-560F-86A2-5CD21F6ABAE1} cmp474A6E6E84754B4F96DC4E226D549C2F libHarfBuzzSharp.so Complete 9.3.11.2649 9.3.12.2760 +{1295C1B1-497B-56F1-9E4D-9CD2270A5AC0} cmp17F13403DE0B83277E07E487DAE7CFBA libSkiaSharp.so Complete 9.3.11.2649 9.3.12.2760 +{74B395A4-B965-5711-8635-65ABB93C03F2} cmp7A9E437A0FCC6805D2C515DD961F9F44 libSkiaSharp.so Complete 9.3.11.2649 9.3.12.2760 +{F1466D7B-2CD9-5640-A469-64B9DECC30F4} cmpEFD5913C752979F4635AF3437AD8F561 libSkiaSharp.so Complete 9.3.11.2649 9.3.12.2760 +{FF00E675-2F4C-5CF4-B849-5F0E77316324} cmpA8AC13EE51C735C1656F69C4FC7D825D libSkiaSharp.so Complete 9.3.11.2649 9.3.12.2760 +{B4F7C8B9-56A9-54AD-A952-7F1A3BFFD01B} cmpBF470832EBCBE664093CDA0DCC6F69B2 MicroCom.Runtime.dll Complete 9.3.11.2649 9.3.12.2760 +{971E8516-64AE-5B04-896F-5A6EBFCFDE83} cmp15EFFF7E6DDAD2D5A8D4BFE464D19BE4 Microsoft.Extensions.DependencyInjection.dll Complete 9.3.11.2693 9.3.12.2760 +{95EF5357-59E7-576F-BC09-DCF444C53CA3} cmp2372452C908ABC16D10DC84FA8ED9FF6 Semi.Avalonia.dll Complete 9.3.12.2752 9.3.12.2760 +{3687447F-A73D-5ADC-9151-F821D4378E03} cmpDE89069E1505C2B71CCB874F2FB716E3 System.ComponentModel.Annotations.dll Complete 9.3.11.2649 9.3.12.2760 +{AEE69E7C-003A-5016-B04B-F9AA2B4B1CC0} cmp53BFEC07D81F9142E690F840F70744FE System.Threading.Channels.dll Complete 9.3.11.2649 9.3.12.2760 +{33DBDF8A-46E9-530C-A8CD-5A6F52D8211F} cmp70D4042F70E6492B602EAA442A075F1D Tmds.DBus.Protocol.dll Complete 9.3.11.2649 9.3.12.2760 +{07736C0A-78C4-536D-A467-E099499660EA} cmpBA53C5AE51B4235B8382C7991B2055B4 Ursa.dll Complete 9.3.12.2752 9.3.12.2760 +{AD76097B-0C92-5799-938D-106E11996214} cmpF7F425B4749E9A93F0B1A296F16514BA Ursa.Themes.Semi.dll Complete 9.3.12.2752 9.3.12.2760 diff --git a/scripts/Installer/PatchComponentLedger.psm1 b/scripts/Installer/PatchComponentLedger.psm1 new file mode 100644 index 0000000000..7544abb71b --- /dev/null +++ b/scripts/Installer/PatchComponentLedger.psm1 @@ -0,0 +1,207 @@ +Set-StrictMode -Version Latest + +# Later patches must keep every component a published patch on the base shipped, or Windows +# Installer silently installs nothing. The ledger records those components. + +$script:UpdateBucket = 'https://flex-updates.s3.amazonaws.com' +$script:PatchPrefix = 'jobs/FieldWorks-Win-all-Release-Patch/' +$script:LedgerColumns = @('ComponentId', 'Component', 'File', 'Feature', 'FirstShipped', 'LastShipped') + +function Invoke-MsiQuery { + param( + [Parameter(Mandatory = $true)]$Database, + [Parameter(Mandatory = $true)][string]$Sql, + [Parameter(Mandatory = $true)][int]$Columns + ) + $view = $Database.GetType().InvokeMember('OpenView', 'InvokeMethod', $null, $Database, @($Sql)) + $viewType = $view.GetType() + $viewType.InvokeMember('Execute', 'InvokeMethod', $null, $view, $null) | Out-Null + $rows = New-Object System.Collections.Generic.List[object] + while ($true) { + $record = $viewType.InvokeMember('Fetch', 'InvokeMethod', $null, $view, $null) + if ($null -eq $record) { break } + $values = New-Object string[] $Columns + for ($i = 1; $i -le $Columns; $i++) { + $values[$i - 1] = $record.GetType().InvokeMember('StringData', 'GetProperty', $null, $record, @($i)) + } + $rows.Add($values) + } + $viewType.InvokeMember('Close', 'InvokeMethod', $null, $view, $null) | Out-Null + return , $rows +} + +function Get-MsiComponents { + <# + .SYNOPSIS + Returns the components of an MSI keyed by ComponentId, each with its key-path file name and + feature. + #> + param([Parameter(Mandatory = $true)][string]$MsiPath) + + $installer = New-Object -ComObject WindowsInstaller.Installer + # 0 = msiOpenDatabaseModeReadOnly + $db = $installer.GetType().InvokeMember('OpenDatabase', 'InvokeMethod', $null, $installer, @((Resolve-Path -LiteralPath $MsiPath).Path, 0)) + + $fileNames = @{} + foreach ($row in (Invoke-MsiQuery -Database $db -Sql 'SELECT `File`,`FileName` FROM `File`' -Columns 2)) { + # FileName is "SHORT|Long name" when a short name exists. + $fileNames[$row[0]] = ($row[1] -split '\|')[-1] + } + $features = @{} + foreach ($row in (Invoke-MsiQuery -Database $db -Sql 'SELECT `Feature_`,`Component_` FROM `FeatureComponents`' -Columns 2)) { + $features[$row[1]] = $row[0] + } + $components = @{} + foreach ($row in (Invoke-MsiQuery -Database $db -Sql 'SELECT `Component`,`ComponentId`,`KeyPath` FROM `Component`' -Columns 3)) { + if ([string]::IsNullOrEmpty($row[1])) { continue } + $file = '' + if ($fileNames.ContainsKey($row[2])) { $file = $fileNames[$row[2]] } + $feature = '' + if ($features.ContainsKey($row[0])) { $feature = $features[$row[0]] } + $components[$row[1].ToUpperInvariant()] = [pscustomobject]@{ + ComponentId = $row[1].ToUpperInvariant() + Component = $row[0] + File = $file + Feature = $feature + } + } + [System.Runtime.InteropServices.Marshal]::ReleaseComObject($db) | Out-Null + return $components +} + +function Get-MsiProperty { + param( + [Parameter(Mandatory = $true)][string]$MsiPath, + [Parameter(Mandatory = $true)][string]$Name + ) + $installer = New-Object -ComObject WindowsInstaller.Installer + $db = $installer.GetType().InvokeMember('OpenDatabase', 'InvokeMethod', $null, $installer, @((Resolve-Path -LiteralPath $MsiPath).Path, 0)) + $rows = Invoke-MsiQuery -Database $db -Sql "SELECT ``Value`` FROM ``Property`` WHERE ``Property`` = '$Name'" -Columns 1 + [System.Runtime.InteropServices.Marshal]::ReleaseComObject($db) | Out-Null + if ($rows.Count -eq 0) { return $null } + return $rows[0][0] +} + +function Read-ComponentLedger { + <# + .SYNOPSIS + Merges ledger files into one table keyed by ComponentId, widening each entry's shipped range. + #> + param([string[]]$Path) + + $ledger = @{} + foreach ($file in $Path) { + if (-not (Test-Path -LiteralPath $file)) { continue } + foreach ($line in (Get-Content -LiteralPath $file)) { + if ($line -match '^\s*(#|$)') { continue } + $cells = $line -split "`t" + if ($cells.Count -lt $script:LedgerColumns.Count) { continue } + $id = $cells[0].ToUpperInvariant() + $entry = [pscustomobject]@{ + ComponentId = $id + Component = $cells[1] + File = $cells[2] + Feature = $cells[3] + FirstShipped = $cells[4] + LastShipped = $cells[5] + } + if ($ledger.ContainsKey($id)) { + $known = $ledger[$id] + if ([version]$entry.FirstShipped -lt [version]$known.FirstShipped) { $known.FirstShipped = $entry.FirstShipped } + if ([version]$entry.LastShipped -gt [version]$known.LastShipped) { $known.LastShipped = $entry.LastShipped } + } + else { + $ledger[$id] = $entry + } + } + } + return $ledger +} + +function Write-ComponentLedger { + param( + [Parameter(Mandatory = $true)][string]$Path, + [Parameter(Mandatory = $true)][object[]]$Entries, + [string]$Heading + ) + $lines = New-Object System.Collections.Generic.List[string] + if ($Heading) { $lines.Add("# $Heading") } + $lines.Add('# ' + ($script:LedgerColumns -join "`t")) + foreach ($entry in ($Entries | Sort-Object File, ComponentId)) { + $lines.Add((($script:LedgerColumns | ForEach-Object { $entry.$_ }) -join "`t")) + } + # ASCII keeps the file byte-identical under Windows PowerShell 5.1 and PowerShell 7. + Set-Content -LiteralPath $Path -Value $lines -Encoding Ascii +} + +function Get-UpdateBucketKeys { + <# + .SYNOPSIS + Lists the update-bucket keys under a prefix whose names match a wildcard. + #> + param( + [Parameter(Mandatory = $true)][string]$Prefix, + [Parameter(Mandatory = $true)][string]$Like + ) + $keys = New-Object System.Collections.Generic.List[string] + $token = $null + do { + $url = "$script:UpdateBucket/?list-type=2&prefix=$Prefix" + if ($token) { $url += '&continuation-token=' + [uri]::EscapeDataString($token) } + [xml]$page = (Invoke-WebRequest -Uri $url -UseBasicParsing).Content + foreach ($item in @($page.ListBucketResult.Contents)) { + if ($null -ne $item -and $item.Key -like $Like) { $keys.Add($item.Key) } + } + $token = $null + if ($page.ListBucketResult.IsTruncated -eq 'true') { $token = $page.ListBucketResult.NextContinuationToken } + } while ($token) + return , $keys +} + +function Get-PublishedPatchKeys { + <# + .SYNOPSIS + Lists the patch-job keys published for one base whose names end with a wildcard. + #> + param( + [Parameter(Mandatory = $true)][string]$BaseBuildNumber, + [Parameter(Mandatory = $true)][string]$Wildcard + ) + return , (Get-UpdateBucketKeys -Prefix $script:PatchPrefix -Like "*_b${BaseBuildNumber}_$Wildcard") +} + +function Save-PublishedFile { + param( + [Parameter(Mandatory = $true)][string]$Key, + [Parameter(Mandatory = $true)][string]$Directory + ) + $target = Join-Path $Directory (Split-Path $Key -Leaf) + Invoke-WebRequest -Uri "$script:UpdateBucket/$Key" -OutFile $target -UseBasicParsing + return $target +} + +function Get-PatchVersionFromKey { + param([Parameter(Mandatory = $true)][string]$Key) + # FieldWorks__b_. + return [version]((Split-Path $Key -Leaf) -split '_')[1] +} + +function Format-DroppedComponentMessage { + param( + [Parameter(Mandatory = $true)][string]$PatchVersion, + [Parameter(Mandatory = $true)][string]$BaseBuildNumber, + [Parameter(Mandatory = $true)][object[]]$Dropped + ) + $lines = New-Object System.Collections.Generic.List[string] + foreach ($entry in $Dropped) { + $lines.Add("Patch $PatchVersion drops component $($entry.ComponentId)") + $lines.Add(" file: $($entry.File) (feature $($entry.Feature))") + $lines.Add(" shipped: patches $($entry.FirstShipped) to $($entry.LastShipped) on base $BaseBuildNumber") + } + $lines.Add('Fix: restore the file to the output, or add a component stand-in (see') + $lines.Add(' Docs/workflows/patch-component-removal.md), as well as an issue to remove') + $lines.Add(' the file before cutting the next base build.') + return ($lines -join [Environment]::NewLine) +} + +Export-ModuleMember -Function Get-MsiComponents, Get-MsiProperty, Read-ComponentLedger, Write-ComponentLedger, Get-UpdateBucketKeys, Get-PublishedPatchKeys, Save-PublishedFile, Get-PatchVersionFromKey, Format-DroppedComponentMessage diff --git a/scripts/Installer/Test-PatchComponentLedger.ps1 b/scripts/Installer/Test-PatchComponentLedger.ps1 new file mode 100644 index 0000000000..a6f23cd2fc --- /dev/null +++ b/scripts/Installer/Test-PatchComponentLedger.ps1 @@ -0,0 +1,91 @@ +<# +.SYNOPSIS +Fails a patch build when the new patch drops a component that a patch published on the same base +shipped. + +.DESCRIPTION +Compares the patch's upgraded MSI image against the ledger of every component that +earlier patches on this base added: the committed seed ledger plus the per-patch +ledgers published next to each .msp. Writes this patch's own ledger for publishing. +pyro already rejects dropping a component the base itself ships (PYRO0305), so only +patch-added components are checked here. + +.PARAMETER MasterMsi +The base (Master) MSI rebuilt by the patch build. + +.PARAMETER UpdateMsi +The upgraded (Update) MSI the patch was diffed from. + +.PARAMETER PatchVersion +The new patch's product version, e.g. 9.3.12.2761. + +.PARAMETER SeedLedger +Committed ledger covering patches published before per-patch ledgers existed. Optional. + +.PARAMETER OutLedger +Where to write this patch's ledger. + +.PARAMETER SkipPublished +Check against the seed ledger only, without reading published ledgers from the update bucket. +#> +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)][string]$MasterMsi, + [Parameter(Mandatory = $true)][string]$UpdateMsi, + [Parameter(Mandatory = $true)][string]$BaseBuildNumber, + [Parameter(Mandatory = $true)][string]$PatchVersion, + [string]$SeedLedger, + [Parameter(Mandatory = $true)][string]$OutLedger, + [switch]$SkipPublished +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' +Import-Module (Join-Path $PSScriptRoot 'PatchComponentLedger.psm1') -Force + +$master = Get-MsiComponents -MsiPath $MasterMsi +$update = Get-MsiComponents -MsiPath $UpdateMsi + +$added = New-Object System.Collections.Generic.List[object] +foreach ($id in $update.Keys) { + if (-not $master.ContainsKey($id)) { + $entry = $update[$id] + $added.Add([pscustomobject]@{ + ComponentId = $entry.ComponentId + Component = $entry.Component + File = $entry.File + Feature = $entry.Feature + FirstShipped = $PatchVersion + LastShipped = $PatchVersion + }) + } +} +Write-ComponentLedger -Path $OutLedger -Entries $added.ToArray() -Heading "Components patch $PatchVersion adds to base $BaseBuildNumber" +Write-Output "Patch $PatchVersion adds $($added.Count) components to base $BaseBuildNumber; ledger written to $OutLedger" + +$ledgerFiles = New-Object System.Collections.Generic.List[string] +if ($SeedLedger -and (Test-Path -LiteralPath $SeedLedger)) { $ledgerFiles.Add($SeedLedger) } +if (-not $SkipPublished) { + $downloads = Join-Path ([IO.Path]::GetTempPath()) "fw-patch-ledgers-b$BaseBuildNumber" + New-Item -ItemType Directory -Force -Path $downloads | Out-Null + foreach ($key in (Get-PublishedPatchKeys -BaseBuildNumber $BaseBuildNumber -Wildcard '*_components.tsv')) { + $ledgerFiles.Add((Save-PublishedFile -Key $key -Directory $downloads)) + } +} +$ledger = Read-ComponentLedger -Path $ledgerFiles.ToArray() +Write-Output "Checking against $($ledger.Count) components from $($ledgerFiles.Count) ledger files" + +$dropped = @($ledger.Values | Where-Object { -not $update.ContainsKey($_.ComponentId) } | Sort-Object File) +if ($dropped.Count -eq 0) { + Write-Output '[OK] The patch keeps every component published patches on this base have shipped.' + exit 0 +} + +$message = Format-DroppedComponentMessage -PatchVersion $PatchVersion -BaseBuildNumber $BaseBuildNumber -Dropped $dropped +Write-Output $message +if ($env:GITHUB_ACTIONS -eq 'true') { + foreach ($entry in $dropped) { + Write-Output "::error title=Patch drops a shipped component::Patch $PatchVersion drops $($entry.File) (component $($entry.ComponentId), feature $($entry.Feature)), shipped by patches $($entry.FirstShipped) to $($entry.LastShipped) on base $BaseBuildNumber. See Docs/workflows/patch-component-removal.md." + } +} +exit 1 diff --git a/scripts/Installer/Test-PatchInstall.ps1 b/scripts/Installer/Test-PatchInstall.ps1 new file mode 100644 index 0000000000..a1306aa693 --- /dev/null +++ b/scripts/Installer/Test-PatchInstall.ps1 @@ -0,0 +1,127 @@ +<# +.SYNOPSIS +Installs the published base, applies the latest published patch, then applies the new patch the +way a user would, and fails unless the new patch really installs. + +.DESCRIPTION +Windows Installer's default reaction to a patch that drops a component is to log SELMGR, +install nothing, and still return success. This test applies the new patch with +MSIENFORCEUPGRADECOMPONENTRULES=1, which turns that into error 2771, and also checks that +FieldWorks.exe on disk carries the patch's version. Needs administrator rights and a machine +without FieldWorks installed; a CI runner qualifies. + +.PARAMETER Patch +The new .msp. + +.PARAMETER PatchVersion +The new patch's product version, e.g. 9.3.12.2761. + +.PARAMETER UpdateMsi +The upgraded (Update) MSI, which supplies the product code and the FieldWorks.exe component. + +.PARAMETER LedgerPaths +Ledger files that map the component GUIDs Windows Installer reports to file names. + +.PARAMETER WorkDir +Where downloads and install logs go. +#> +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)][string]$Patch, + [Parameter(Mandatory = $true)][string]$PatchVersion, + [Parameter(Mandatory = $true)][string]$BaseBuildNumber, + [Parameter(Mandatory = $true)][string]$UpdateMsi, + [string[]]$LedgerPaths = @(), + [Parameter(Mandatory = $true)][string]$WorkDir +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' +$ProgressPreference = 'SilentlyContinue' +Import-Module (Join-Path $PSScriptRoot 'PatchComponentLedger.psm1') -Force + +New-Item -ItemType Directory -Force -Path $WorkDir | Out-Null +$failures = New-Object System.Collections.Generic.List[string] + +function Invoke-Logged { + param([string]$FilePath, [string]$Arguments, [string]$Step) + Write-Output "${Step}: $FilePath $Arguments" + $process = Start-Process -FilePath $FilePath -ArgumentList $Arguments -Wait -PassThru + # 3010 = success, restart required. + if ($process.ExitCode -ne 0 -and $process.ExitCode -ne 3010) { + throw "$Step failed with exit code $($process.ExitCode)" + } +} + +$baseKey = @(Get-UpdateBucketKeys -Prefix "jobs/FieldWorks-Win-all-Release-Base/$BaseBuildNumber/" -Like '*_Online_x64.exe') | Select-Object -First 1 +if (-not $baseKey) { throw "No published base installer found for base $BaseBuildNumber" } +$baseInstaller = Save-PublishedFile -Key $baseKey -Directory $WorkDir +Invoke-Logged -FilePath $baseInstaller -Arguments "/quiet /norestart /log `"$WorkDir\01-base.log`"" -Step 'Install base' + +$newVersion = [version]$PatchVersion +$previousKey = @(Get-PublishedPatchKeys -BaseBuildNumber $BaseBuildNumber -Wildcard '*.msp' | + Where-Object { (Get-PatchVersionFromKey $_) -lt $newVersion } | + Sort-Object { Get-PatchVersionFromKey $_ }) | Select-Object -Last 1 +if ($previousKey) { + $previousPatch = Save-PublishedFile -Key $previousKey -Directory $WorkDir + Invoke-Logged -FilePath 'msiexec.exe' -Arguments "/p `"$previousPatch`" /qn /norestart AUTOUPDATE=True /l*vx `"$WorkDir\02-previous-patch.log`"" -Step "Apply published patch $(Get-PatchVersionFromKey $previousKey)" +} +else { + Write-Output "No earlier patch is published on base $BaseBuildNumber; applying the new patch to the base alone." +} + +$newLog = Join-Path $WorkDir '03-new-patch.log' +$process = Start-Process -FilePath 'msiexec.exe' -ArgumentList "/p `"$Patch`" /qn /norestart AUTOUPDATE=True MSIENFORCEUPGRADECOMPONENTRULES=1 /l*vx `"$newLog`"" -Wait -PassThru +Write-Output "Apply new patch ${PatchVersion}: exit code $($process.ExitCode)" +if ($process.ExitCode -ne 0 -and $process.ExitCode -ne 3010) { + $failures.Add("Applying patch $PatchVersion failed with exit code $($process.ExitCode).") +} + +# SELMGR names each component a feature has lost; the ledgers turn those GUIDs into files. +$dropped = New-Object System.Collections.Generic.List[object] +if (Test-Path -LiteralPath $newLog) { + $ledger = Read-ComponentLedger -Path $LedgerPaths + $pattern = "SELMGR: ComponentId '(\{[0-9A-Fa-f-]+\})' is registered to feature '([^']+)'" + foreach ($match in (Select-String -LiteralPath $newLog -Pattern $pattern -AllMatches)) { + foreach ($m in $match.Matches) { + $id = $m.Groups[1].Value.ToUpperInvariant() + if ($ledger.ContainsKey($id)) { + $dropped.Add($ledger[$id]) + } + else { + $dropped.Add([pscustomobject]@{ ComponentId = $id; File = '(not in any ledger)'; Feature = $m.Groups[2].Value; FirstShipped = '?'; LastShipped = '?' }) + } + } + } +} +if ($dropped.Count -gt 0) { + $failures.Add((Format-DroppedComponentMessage -PatchVersion $PatchVersion -BaseBuildNumber $BaseBuildNumber -Dropped $dropped.ToArray())) +} + +# The registered version advances even when no files are copied, so read the binary itself. +$productCode = Get-MsiProperty -MsiPath $UpdateMsi -Name 'ProductCode' +$exeComponent = @((Get-MsiComponents -MsiPath $UpdateMsi).Values | Where-Object { $_.File -eq 'FieldWorks.exe' }) | Select-Object -First 1 +if ($productCode -and $exeComponent) { + $installer = New-Object -ComObject WindowsInstaller.Installer + $exePath = $installer.GetType().InvokeMember('ComponentPath', 'GetProperty', $null, $installer, @($productCode, $exeComponent.ComponentId)) + $onDisk = if ($exePath -and (Test-Path -LiteralPath $exePath)) { (Get-Item -LiteralPath $exePath).VersionInfo.FileVersion } else { '(missing)' } + Write-Output "FieldWorks.exe on disk: $onDisk (expected $PatchVersion)" + if ($onDisk -ne $PatchVersion) { + $failures.Add("FieldWorks.exe on disk is $onDisk after applying patch $PatchVersion; the patch did not install its files.") + } +} +else { + $failures.Add('Could not find the product code or the FieldWorks.exe component in the Update MSI.') +} + +if ($failures.Count -eq 0) { + Write-Output "[OK] Patch $PatchVersion installs on base $BaseBuildNumber over the latest published patch." + exit 0 +} +foreach ($failure in $failures) { + Write-Output "ERROR: $failure" + if ($env:GITHUB_ACTIONS -eq 'true') { + Write-Output ('::error title=Patch install test failed::' + ($failure -replace "`r?`n", ' ')) + } +} +exit 1 From bc316bca705ab30dd7ed22663c3b685ae221a0ac Mon Sep 17 00:00:00 2001 From: John Lambert Date: Mon, 21 Sep 2026 12:31:47 -0400 Subject: [PATCH 2/6] LT-22801: Reuse patch rescue for previous components Use the existing RemovedSinceLastBase stand-ins for components dropped from either the base MSI or the immediately previous patch. Publish a four-column update-minus-base ledger beside each MSP and check the next patch against it. Remove the installer smoke test and make base builds fail with direct placeholder cleanup instructions. --- .github/workflows/patch-installer-cd.yml | 36 +-- Build/Installer.legacy.targets | 8 +- Docs/workflows/patch-component-removal.md | 135 +++------- FLExInstaller/AGENTS.md | 10 +- FLExInstaller/PatchComponentLedger/b1452.tsv | 240 +++++++++--------- .../Installer/PatchComponentLedger.Tests.ps1 | 142 +++++++++++ scripts/Installer/PatchComponentLedger.psm1 | 160 +++++++++--- .../Installer/Test-PatchComponentLedger.ps1 | 74 +++--- scripts/Installer/Test-PatchInstall.ps1 | 127 --------- 9 files changed, 476 insertions(+), 456 deletions(-) create mode 100644 scripts/Installer/PatchComponentLedger.Tests.ps1 delete mode 100644 scripts/Installer/Test-PatchInstall.ps1 diff --git a/.github/workflows/patch-installer-cd.yml b/.github/workflows/patch-installer-cd.yml index 1ae830f877..15b6f36036 100644 --- a/.github/workflows/patch-installer-cd.yml +++ b/.github/workflows/patch-installer-cd.yml @@ -263,9 +263,8 @@ jobs: "update_msi=$((Get-ChildItem PatchableInstaller/CreateUpdatePatch/Update -Filter '*.msi' | Select-Object -First 1).FullName)" >> $env:GITHUB_OUTPUT "master_msi=$((Get-ChildItem PatchableInstaller/CreateUpdatePatch/Master -Filter '*.msi' | Select-Object -First 1).FullName)" >> $env:GITHUB_OUTPUT - # A patch must keep every component any patch on this base has shipped, or Windows - # Installer silently installs nothing (LT-22801). See Docs/workflows/patch-component-removal.md. - - name: Check patch keeps every published component + # Patch component identity must remain stable across the base and previous patch. + - name: Check patch keeps base and previous-patch components shell: pwsh run: | .\scripts\Installer\Test-PatchComponentLedger.ps1 ` @@ -276,20 +275,6 @@ jobs: -SeedLedger "FLExInstaller/PatchComponentLedger/b$($env:BASE_BUILD_NUMBER).tsv" ` -OutLedger "${{ steps.find_patch.outputs.ledger_file }}" - - name: Test patch install over the latest published patch - shell: pwsh - timeout-minutes: 60 - run: | - $ledgers = @("FLExInstaller/PatchComponentLedger/b$($env:BASE_BUILD_NUMBER).tsv", "${{ steps.find_patch.outputs.ledger_file }}") - $ledgers += @(Get-ChildItem (Join-Path ([IO.Path]::GetTempPath()) "fw-patch-ledgers-b$($env:BASE_BUILD_NUMBER)") -Filter '*.tsv' -ErrorAction SilentlyContinue | ForEach-Object { $_.FullName }) - .\scripts\Installer\Test-PatchInstall.ps1 ` - -Patch "${{ steps.find_patch.outputs.patch_file }}" ` - -PatchVersion "${{ steps.find_patch.outputs.patch_version }}" ` - -BaseBuildNumber $env:BASE_BUILD_NUMBER ` - -UpdateMsi "${{ steps.find_patch.outputs.update_msi }}" ` - -LedgerPaths $ledgers ` - -WorkDir "${{ runner.temp }}/patch-install-test" - - name: Sign Patch if: github.event_name != 'pull_request' uses: sillsdev/codesign/trusted-signing-action@v3 @@ -318,15 +303,23 @@ jobs: if (-not (Test-Path $patchPath)) { throw "Patch file not found at $patchPath" } + $ledgerPath = "${{ steps.find_patch.outputs.ledger_file }}" + if (-not (Test-Path $ledgerPath)) { + throw "Ledger file not found at $ledgerPath" + } $patchFile = Split-Path $patchPath -Leaf $s3Key = "jobs/FieldWorks-Win-all-Release-Patch/$($env:FW_BUILD_NUMBER)/$patchFile" - aws s3 cp $patchPath "s3://flex-updates/$s3Key" - Write-Host "Uploaded to s3://flex-updates/$s3Key" - # Later patches on this base are checked against every published ledger. - $ledgerPath = "${{ steps.find_patch.outputs.ledger_file }}" $ledgerKey = "jobs/FieldWorks-Win-all-Release-Patch/$($env:FW_BUILD_NUMBER)/$(Split-Path $ledgerPath -Leaf)" aws s3 cp $ledgerPath "s3://flex-updates/$ledgerKey" + if ($LASTEXITCODE -ne 0) { + throw "Ledger upload failed with exit code $LASTEXITCODE" + } Write-Host "Uploaded to s3://flex-updates/$ledgerKey" + aws s3 cp $patchPath "s3://flex-updates/$s3Key" + if ($LASTEXITCODE -ne 0) { + throw "Patch upload failed with exit code $LASTEXITCODE" + } + Write-Host "Uploaded to s3://flex-updates/$s3Key" - name: Upload Build Logs uses: actions/upload-artifact@v7 @@ -342,4 +335,3 @@ jobs: ./PatchableInstaller/CreateUpdatePatch/Master/AppHarvest.wxs ./PatchableInstaller/CreateUpdatePatch/Update/AppHarvest.wxs ./BuildDir/*_components.tsv - ${{ runner.temp }}/patch-install-test/*.log diff --git a/Build/Installer.legacy.targets b/Build/Installer.legacy.targets index 320e1cab6f..c8904724c2 100644 --- a/Build/Installer.legacy.targets +++ b/Build/Installer.legacy.targets @@ -105,8 +105,8 @@ /> @@ -672,8 +672,8 @@ b$(BASE_BUILD_NUMBER)_ - .tsv` plus the - `*_components.tsv` ledger published next to each `.msp`. `pyro` already rejects - dropping a file the base itself ships (`PYRO0305`), so this check covers the - patch-added files `pyro` cannot see. -- **Install test** (`scripts/Installer/Test-PatchInstall.ps1`). Installs the published - base, applies the latest published patch, then applies the new patch with - `MSIENFORCEUPGRADECOMPONENTRULES=1`, which turns the silent failure into error 2771. - It also checks that `FieldWorks.exe` on disk carries the new patch's version. - -Both name the dropped file: +The diagnostic names every missing component and file, regardless of whether it came from +the base MSI or the previous-patch ledger: ``` -Patch 9.3.12.2754 drops component {B3A225EB-3642-5FE4-8ED4-B2DAE6F8AC9B} +Patch 9.3.12.2761 drops component {B3A225EB-3642-5FE4-8ED4-B2DAE6F8AC9B} file: Avalonia.Themes.Fluent.dll (feature Complete) - shipped: patches 9.3.12.2718 to 9.3.12.2718 on base 1452 -Fix: restore the file to the output, or add a component stand-in (see - Docs/workflows/patch-component-removal.md), as well as an issue to remove - the file before cutting the next base build. -``` - -## When a check fails - -Do both of the following. - -### 1. Keep the component in the patch - -**Restore the file (the default).** Add it to `RemovedSinceLastBase` in the -`RescuePatching` target of `Build/Installer.legacy.targets`, at the **same path** it -shipped from: - -```xml - + base: 1452 +Remediation: +Add each missing file's output path to RemovedSinceLastBase in Build/Installer.legacy.targets, +preserving its relative output path: + +Create an issue to remove the placeholder before the next base build. ``` -The target writes a zero-byte placeholder there, so the component stays in every patch. -Machines that already have the real file keep it, because Windows Installer does not -replace a versioned file with an unversioned one. The dead file costs a few bytes until -the next base. +The `RemovedSinceLastBase` entry makes the build write a zero-byte stand-in at that path. +That component remains in the patch, so machines that already have the real file keep +working while the removal issue is completed. Keep +`Avalonia.Themes.Fluent.dll` in this list for base 1452. -**Component stand-in (only when the old file must not stay on disk).** Use this when -leaving the file behind is unsafe, for example a plugin that FieldWorks finds by -scanning a directory. Keep the placeholder above, and author a `RemoveFile` entry for -that path in `FLExInstaller/CustomComponents.wxi` so the real file is deleted. This -has not yet been validated on a patch line. Test it by installing the base, the -previous patch and the new patch before relying on it. Cutting a new base is the safe -alternative. +## Before creating a base -### 2. File an issue to remove the file before the next base +A base build fails while any `RemovedSinceLastBase` entries remain. The error lists the +stand-in paths and requires both cleanup actions: -Create an LT issue with the label `remove-before-next-base`: +1. Remove each `RemovedSinceLastBase` entry from `Build/Installer.legacy.targets`. +2. Remove each corresponding zero-byte file from the build output. -``` -Summary: Remove placeholder before the next base build -Labels: remove-before-next-base - - (component , feature ) left the build output in , -after patches to on base shipped it. It is kept alive by a -RemovedSinceLastBase placeholder in Build/Installer.legacy.targets so patches on -base keep working. - -Before the next base build: -- Remove the RemovedSinceLastBase entry. -- If any published patch on base already dropped the component, machines - that applied it keep an orphaned copy that an upgrade will not remove; add a - RemoveFile for its path to the new base. -``` +Complete the removal issue before creating the base. A new base establishes the component +set that future patches must preserve. -## Before cutting a new base - -A new base is the one point where dropping files is legal: it has a new ProductCode, -and it removes the old product completely before installing (`RemoveExistingProducts` -runs before `InstallInitialize`). - -1. Resolve every open issue labelled `remove-before-next-base`. The base build warns - while `RemovedSinceLastBase` still has entries. -2. Add a `RemoveFile` for every file an already-published patch orphaned. An upgrade - only removes what the old product's current patch still lists. -3. A new base needs no seed ledger: its patches publish their own ledgers from the - first patch on. - -## Running the checks locally +## Running the check locally ```powershell .\scripts\Installer\Test-PatchComponentLedger.ps1 -MasterMsi -UpdateMsi ` -BaseBuildNumber 1452 -PatchVersion 9.3.12.2761 ` -SeedLedger FLExInstaller\PatchComponentLedger\b1452.tsv -OutLedger out.tsv ``` - -The install test changes the machine's FieldWorks installation; run it only on a -disposable machine, from an elevated prompt. diff --git a/FLExInstaller/AGENTS.md b/FLExInstaller/AGENTS.md index 494aae9833..2e8939a69e 100644 --- a/FLExInstaller/AGENTS.md +++ b/FLExInstaller/AGENTS.md @@ -13,11 +13,11 @@ Minimal installer guidance for agents. - Heat exclusions: **`PatchableInstallerHeatExclude.xml`** is copied to **`PatchableInstaller/BaseInstallerBuild/heat-exclude.xml`** before Heat (see **`Build/Installer.legacy.targets`** `CopyFilesToInstall`). - **`buildMsi.bat`** passes **`-fv`** to **`light.exe`** so **`MsiAssemblyName`** includes **fileVersion** (same intent as MSBuild **`SetMsiAssemblyNameFileVersion=true`**), which helps GAC servicing when **`AssemblyVersion`** is unchanged but the binary’s **file version** increases. - Newtonsoft.Json and similar authored components live in **`CustomComponents.wxi`** (overlays **`PatchableInstaller/Common`**), with definitions guarded by **``** so patch/update authoring omits them when only **`UPDATEBUILDDIR`** is set. Add matching **`ComponentRef`** entries in **`FLExInstaller/CustomFeatures.wxi`** inside the **same** **`...`** so patch builds do not emit dangling refs (**LGHT0094**). WiX 6 **`Framework.wxs`** uses the same pattern for **`Feature Complete`**. Do not use **`FeatureRef Id="Complete"`** from an include that appears before **`Framework.wxs`** defines `Complete` (Light **LGHT0095**). -- **Patch error `PYRO0305: The File '' was removed in the patch`:** a file present in the base/**Master** harvest is missing from the new **Update** harvest, and WiX 3 **`pyro.exe`** forbids removing files in a patch. Typical trigger: a code change stops emitting a file the base release still ships — e.g. reg-free COM manifests dropped by a "reduce COM usage" change (`ManagedLgIcuCollator.manifest`, `ManagedVwWindow.manifest`). - - **Stopgap fix (patch against the existing base):** add the dropped file to the **`RemovedSinceLastBase`** item list in the **`RescuePatching`** target of **`Build/Installer.legacy.targets`** (runs via `BuildProduct`). It writes a zero-byte placeholder into the build output (**`$(dir-outputBase)`**) so the file appears in both harvests and `pyro` treats it as *changed*, not *removed*. Mirror the existing entries (`ManagedVwWindow.manifest`, `SimpleRootSite.manifest`) and add **only** the basenames actually dropped — usually just the `*.manifest`, not a still-shipping `.dll`. - - **Permanent fix:** cut a new **Base** build so the file is absent from Master too, then delete the now-stale `RemovedSinceLastBase` entries (the target comment notes a base build should warn when these exist). - - **Do not** add it to **`PatchableInstallerHeatExclude.xml`** — that list is for artifacts that must never be harvested (build-output dedup / test-only files), not for reconciling files removed since the base. -- **Patch drops a component an earlier patch shipped:** `pyro` cannot see this (it only compares against the base), and the patch installs nothing while returning success. The patch workflow's component-ledger check and install test catch it; the fix is the same `RemovedSinceLastBase` placeholder plus a `remove-before-next-base` issue. See **`Docs/workflows/patch-component-removal.md`**. +- **Patch component removal:** the patch ledger check compares the new **Update** MSI with the **Master** MSI and the complete ledger from the immediately previous published MSP. The base-1452 bootstrap is `PatchComponentLedger/b1452.tsv`; after a ledger-bearing patch exists, its matching ledger must be beside the immediately previous MSP. Later patch versions are ignored. + - A missing component from either source uses the same fix: add its output path, preserving the file's relative output path, to the **`RemovedSinceLastBase`** item list in the **`RescuePatching`** target of **`Build/Installer.legacy.targets`**. The target writes a zero-byte stand-in into **`$(dir-outputBase)`** so the component remains in the patch. + - Create an issue to remove the stand-in before the next base. Keep **`Avalonia.Themes.Fluent.dll`** in the list for base 1452. + - A base build fails while any **`RemovedSinceLastBase`** entries remain. Remove each entry and its corresponding zero-byte file before creating the base. +- **Do not** add a dropped component to **`PatchableInstallerHeatExclude.xml`**. That list is for artifacts that must never be harvested, not for preserving patch component identity. ## Constraints diff --git a/FLExInstaller/PatchComponentLedger/b1452.tsv b/FLExInstaller/PatchComponentLedger/b1452.tsv index 1dcb09a5c0..d80f45b7fc 100644 --- a/FLExInstaller/PatchComponentLedger/b1452.tsv +++ b/FLExInstaller/PatchComponentLedger/b1452.tsv @@ -1,120 +1,120 @@ -# Components patches published on base 1452 added, from every .msp on the update bucket -# ComponentId Component File Feature FirstShipped LastShipped -{DA0940BD-0FA2-5C72-BC6F-EFADFC01AF8A} cmp8F63659DC44FBC0F7703F8C24F1DF4DC AIExportInstructions.md Complete 9.3.11.2693 9.3.12.2760 -{D3F3D4D1-690D-5570-90C3-07AC15DBF944} cmp6F532A88487F846B64ADC73FD2069DC9 Avalonia.Base.dll Complete 9.3.11.2649 9.3.12.2760 -{48987B23-7456-544F-A320-AD7F5DD13B67} cmp295D431BF73CBADDD7FCD13AC811E14D Avalonia.Controls.dll Complete 9.3.11.2649 9.3.12.2760 -{644EFFE3-9DFF-507A-8D53-17A1167496F8} cmp515AEF8E04CEE9DBD98D566FFBFC5465 Avalonia.DesignerSupport.dll Complete 9.3.11.2649 9.3.12.2760 -{DB85E238-F9C9-55B7-8C76-FA2CC185A8E3} cmp60F16D63CA4BC46430A7ECB89F46FA18 Avalonia.Desktop.dll Complete 9.3.11.2649 9.3.12.2760 -{DBF8B74B-D91F-5F23-B4D1-E55DC755DCE7} cmpFA9EACE595AFE0745CF5143237D52623 Avalonia.Dialogs.dll Complete 9.3.11.2649 9.3.12.2760 -{B7C5DCDD-0DF8-5436-AF56-55A83851A666} cmp660B3182149886A1D06AF308784F3CBC Avalonia.dll Complete 9.3.11.2649 9.3.12.2760 -{91ABBE9E-0556-5EC8-9221-482000C1A71C} cmpF938C1EDEF7D137C951EE38003E863C8 Avalonia.FreeDesktop.dll Complete 9.3.11.2649 9.3.12.2760 -{2DA9E2A1-1CC9-5E58-A5BD-2B6C11CF4040} cmpB34F12C6C13ACAA9D4391C4044F7C1A4 Avalonia.Headless.dll Complete 9.3.11.2649 9.3.12.2760 -{BECB2C60-A212-524F-8EE7-E42C5491F78E} cmp49FE4DA9C83AA91FE4179EAD8590BF5F Avalonia.Headless.NUnit.dll Complete 9.3.11.2649 9.3.12.2760 -{59864563-443D-544D-BCDC-7C0D85EF7D44} cmp078E9C2E631026E5BBBDBBB010B6D7D0 Avalonia.Markup.dll Complete 9.3.11.2649 9.3.12.2760 -{5719C05E-839F-5F34-BBEC-AB1D3F0209F8} cmp538664F9813F44D6D4610976AC1D20B5 Avalonia.Markup.Xaml.dll Complete 9.3.11.2649 9.3.12.2760 -{A844E72E-3716-5E1E-91F6-789F62C9AC5B} cmpF639D8A1AAA4AC4F63933E45B12FAD75 Avalonia.Metal.dll Complete 9.3.11.2649 9.3.12.2760 -{03A04376-78E7-5146-9381-B8BC765D0E87} cmp7DD0D4584C33CDF6F05FD47176EC792A Avalonia.MicroCom.dll Complete 9.3.11.2649 9.3.12.2760 -{056CF987-318F-53BB-A76C-3ABFD17DA11B} cmp19619B1BF81C2A500731F072FC6EEDA7 Avalonia.Native.dll Complete 9.3.11.2649 9.3.12.2760 -{264A8059-4F17-5B37-95DA-F5E26A11902E} cmp58CBF46A4A82BB2422AA8BD590F27D92 Avalonia.OpenGL.dll Complete 9.3.11.2649 9.3.12.2760 -{E8EC545A-7485-5166-9315-E972C22E04D0} cmpE54DA5F709820A816226FCBC2E8F9B5B Avalonia.Remote.Protocol.dll Complete 9.3.11.2649 9.3.12.2760 -{8216A4FF-C3C9-5F6A-BBB3-97862E97EAD2} cmp0B0E423744DFBF7DB1AD90D3E5B94209 Avalonia.Skia.dll Complete 9.3.11.2649 9.3.12.2760 -{B3A225EB-3642-5FE4-8ED4-B2DAE6F8AC9B} cmpF906DFA4FA548A236D02C0A143CCEF15 Avalonia.Themes.Fluent.dll Complete 9.3.11.2649 9.3.12.2718 -{4239BA48-B86B-52D5-99AD-0F0AFFB034BD} cmp6BFB5ADF4855D99B80240BFB4127E6F5 Avalonia.Vulkan.dll Complete 9.3.11.2649 9.3.12.2760 -{B2E449AC-85AC-540C-83A0-7C8CB9005FB0} cmpFC099FB902BFDDE81E4773FB637CD110 Avalonia.Win32.Automation.dll Complete 9.3.11.2649 9.3.12.2760 -{C358C2B8-04D4-5E0A-AE61-8D436EDC1860} cmp47567C3CF2B33B158D6003CCA591A2C0 Avalonia.Win32.dll Complete 9.3.11.2649 9.3.12.2760 -{481D8C24-66DE-5F14-ABB1-C80300D372AA} cmpD97DACBAF1F92AF60E34BCB54D4891A6 Avalonia.Win32.Interoperability.dll Complete 9.3.11.2649 9.3.12.2760 -{C0DBE567-DE9D-595A-85F7-48B64DE5B3DB} cmp7519FFB6B965DC9DDB696DB54C3365A9 Avalonia.X11.dll Complete 9.3.11.2649 9.3.12.2760 -{9C145F8E-5738-503C-87CE-710C2DEE46B8} cmpBA8AFE637D8BF45BE83C8678EFE986F5 CommunityToolkit.Mvvm.dll Complete 9.3.11.2649 9.3.12.2760 -{138EDD05-02B0-5C55-9DBA-6A0CC9726DA3} cmpE3398985DA0F0DBF2A03ADA8BABB122E FwAvalonia.dll Complete 9.3.11.2649 9.3.12.2760 -{8F71DC11-6DCD-5578-897F-05A3A507BD50} cmp7F8FEA8233D91ADDE7F99189DEEE06E7 FwAvalonia.dll.config Complete 9.3.11.2649 9.3.12.2760 -{EC9D9EB3-991F-5563-AEF3-7B290DD448D9} cmp4B58D41AC8D441E39C67C9FE6E6F4B85 FwAvalonia.pdb Complete 9.3.11.2649 9.3.12.2760 -{00FD6E6A-496C-5310-AD67-45189D67AF41} cmpC744447C9E3167AF5E91CD4C5D78C756 FwAvalonia.resources.dll Spanish 9.3.11.2649 9.3.12.2760 -{095448E5-5112-5EEF-92F5-FB28D4FCDDC0} cmp5D3F85D73A47D90E315C4469F29EA51B FwAvalonia.resources.dll French 9.3.11.2649 9.3.12.2760 -{0D6418A5-F6D6-5090-A3D1-8FEB78E449D3} cmpC6AF4718DDA44D0F92E882FA0FA083BC FwAvalonia.resources.dll Vietnamese 9.3.11.2649 9.3.12.2760 -{16380557-C64E-51F8-B35A-0B92F957BF35} cmpF9A97A3D1E5F46CC75623A11AF8B64D9 FwAvalonia.resources.dll Malay 9.3.11.2649 9.3.12.2760 -{1DAFF7DC-57B1-500B-A643-563C8015367A} cmp3D7CC9B577D9CE4B86D7EBB3E918BC61 FwAvalonia.resources.dll Urdu 9.3.11.2649 9.3.12.2760 -{21B4E444-8349-54D4-88E7-84936048C2A4} cmp5E829FE1D852405FCE4A88CC0E6D8721 FwAvalonia.resources.dll Azerbaijani 9.3.11.2649 9.3.12.2760 -{255AD1F7-CA36-5D6C-B903-A887CFAD66F1} cmpDCEA9861FD22B1B0B313AB6ED5FF566A FwAvalonia.resources.dll Hindi 9.3.11.2649 9.3.12.2760 -{27549925-9A72-5265-9DA2-5F5524D9ED0D} cmp64F38BDDCA32FB2EB6BD554E10E3F1BE FwAvalonia.resources.dll Malay 9.3.11.2649 9.3.12.2760 -{3E327A7D-94CD-5BD8-827C-38635FCF53E9} cmpC204756496A4292493EE634E2478E7D9 FwAvalonia.resources.dll Swahili 9.3.11.2649 9.3.12.2760 -{410A087A-45EF-5C59-AF88-BD60A54FFC28} cmpD6A8E5A8331698ECB9A1A4B73E4027FA FwAvalonia.resources.dll Portuguese 9.3.11.2649 9.3.12.2760 -{4ABB6A21-6F95-58B2-8C16-38B74CBD220F} cmp87AFA172D4ED6657F2F23425A93039DC FwAvalonia.resources.dll Yoruba 9.3.11.2649 9.3.12.2760 -{4B40E5CB-30D5-5677-A13C-F62EB4A6C9EF} cmp186CD5D44F2C490314A17B8B12F1E458 FwAvalonia.resources.dll Chinese 9.3.11.2649 9.3.12.2760 -{55F6DA1E-A17F-5608-88B9-61512E42CDF8} cmp79796BEE4D10F8D4A1A7E3E37CA2F4A1 FwAvalonia.resources.dll Korean 9.3.11.2649 9.3.12.2760 -{6E063CEC-98DE-598E-80D4-4708FCA0B412} cmp6830D5813172E2AB5E8303BEE2E0D6B6 FwAvalonia.resources.dll Russian 9.3.11.2649 9.3.12.2760 -{794FB841-8C1A-502E-88DA-2E32D9296248} cmp3F935CA18EB62F02F358BBE9FDED913A FwAvalonia.resources.dll Khmer 9.3.11.2649 9.3.12.2760 -{7997DB88-EE95-5A10-8CF7-DC77008DC0AC} cmp0F87125353CEA8E8B6313529DD7C6BD6 FwAvalonia.resources.dll Nepali 9.3.11.2649 9.3.12.2760 -{7A5ED619-AA06-5F23-944F-1279F28BCABB} cmp7206F0E70BCBF954AE4922BFF706BB3E FwAvalonia.resources.dll Thai 9.3.11.2649 9.3.12.2760 -{8A908B5D-2495-5A62-8808-ABC896FA4846} cmp5E731BC4EE8DF18A3F911E4310268089 FwAvalonia.resources.dll Bengali 9.3.11.2649 9.3.12.2760 -{9C8D0766-C594-522A-B364-E73AF641AC0E} cmp0A8A05DD0C9C50DC093C17291DF69487 FwAvalonia.resources.dll Arabic 9.3.11.2649 9.3.12.2760 -{A7530E25-7D26-5C02-849F-F1FC632EE39B} cmpF8D6A786D009147E667F3BD4E45726BA FwAvalonia.resources.dll Telugu 9.3.11.2649 9.3.12.2760 -{C2E15D6E-5AF8-563D-95F7-2BDC22918634} cmpFAAF6F77C41BD26E21712EBD302909FC FwAvalonia.resources.dll Malayalam 9.3.11.2649 9.3.12.2760 -{C3CD0E9D-6C19-5863-A3E3-D1577FB57C66} cmp3AB81E523E57BB90F1BDB4311D8BDF4A FwAvalonia.resources.dll Hungarian 9.3.11.2649 9.3.12.2760 -{CB6C07D2-C204-546F-890C-49AE15F6FE32} cmp88CA64FE2DAD35DB3F64594BDF4BB4D6 FwAvalonia.resources.dll Persian 9.3.11.2649 9.3.12.2760 -{CC2F72DF-B827-5792-B766-B08667192EF5} cmp340F48912B1B687BCD4BD984A600BB75 FwAvalonia.resources.dll German 9.3.11.2649 9.3.12.2760 -{DD37BF7D-3A4E-5B2A-8798-4D596EFE60B8} cmp25D6A9F9FFEDC837E8298EB5BB3F9B8B FwAvalonia.resources.dll Turkish 9.3.11.2649 9.3.12.2760 -{E367A90A-C4A6-5896-B326-44C546900329} cmp801B580FFEEAEF4B4D1F0E2135AB3F37 FwAvalonia.resources.dll Indonesian 9.3.11.2649 9.3.12.2760 -{E6B9C3BB-1AEE-5A2D-B322-031AF8E32FD0} cmp3BFF24169B2639842124EDA8D54B6A60 FwAvalonia.resources.dll Kinyarwanda 9.3.11.2649 9.3.12.2760 -{E6FA6A1F-FC77-5110-A4BE-575606D2F057} cmpB13758157B2FFEACC849C5CA49BD3E87 FwAvalonia.resources.dll Tamil 9.3.11.2649 9.3.12.2760 -{F554DE59-8F6E-53EE-B238-97924BED4754} cmp945ABA08FFF1DB122D7B99A11C274AA7 FwAvalonia.resources.dll Burmese 9.3.11.2649 9.3.12.2760 -{2E1C86F7-F728-5D5E-BDCC-64FF868A46B5} cmp92ECD687E7DEA3EA930AE1FA471F3062 FwAvaloniaDialogs.dll Complete 9.3.11.2649 9.3.12.2760 -{46FF9909-6F68-58B3-A907-0550222ACD29} cmpF54565E87EF3FD52203F26835085A430 FwAvaloniaDialogs.dll.config Complete 9.3.11.2649 9.3.12.2760 -{F2BB0A37-FE52-53C8-8696-EBE0F4D47B97} cmpCD1512CFB467FC5C896C997AA6D869DB FwAvaloniaDialogs.pdb Complete 9.3.11.2649 9.3.12.2760 -{025484A1-AA3B-55C6-95C9-73ED057EC6E9} cmp82D0BAE64C8AE203065262909273FD30 FwAvaloniaDialogs.resources.dll Swahili 9.3.11.2649 9.3.12.2760 -{096C45C2-0865-5070-AA31-CC96DF0D5B1C} cmpD94D58DC6BF163351A6EFA6F601B0D6C FwAvaloniaDialogs.resources.dll Malayalam 9.3.11.2649 9.3.12.2760 -{0EB6AB4E-F822-5500-997F-A9BE4856EDD4} cmp1F15778C89D148F0DFD7CCD9BFEE602A FwAvaloniaDialogs.resources.dll German 9.3.11.2649 9.3.12.2760 -{1D35E836-98AB-53E1-83C2-70BC05A2080D} cmp229E2BA7226AE09E8C70783A6AE9ABFC FwAvaloniaDialogs.resources.dll Indonesian 9.3.11.2649 9.3.12.2760 -{1D47C484-8E7C-547A-A735-349E27ED325C} cmp47826A817F068F909056FE903637B0EA FwAvaloniaDialogs.resources.dll Burmese 9.3.11.2649 9.3.12.2760 -{1D4C2AF2-1935-5C95-8222-7C00123A78C8} cmp4A4AA69EF39D1241CB0C302CDC811063 FwAvaloniaDialogs.resources.dll Malay 9.3.11.2649 9.3.12.2760 -{1DBB57F5-5373-581C-A456-A9F845FF4874} cmp26917791EA6C95E2841DBCBBEE622324 FwAvaloniaDialogs.resources.dll Korean 9.3.11.2649 9.3.12.2760 -{2D2E0509-0ED6-5178-8FA4-A2D5AD592ED1} cmp01D0C349B81F490963ED6D773A0770FA FwAvaloniaDialogs.resources.dll Vietnamese 9.3.11.2649 9.3.12.2760 -{37663C83-A7D4-5024-ACD5-C7B6C0C2B068} cmp2BBC973D8DDF0B18438CA6A491393FE8 FwAvaloniaDialogs.resources.dll Urdu 9.3.11.2649 9.3.12.2760 -{49755DFC-89B1-593E-9E91-E388A48DE600} cmpDCFF496901CBF45AD09A85AFCB24021A FwAvaloniaDialogs.resources.dll Turkish 9.3.11.2649 9.3.12.2760 -{4D129082-FC11-56CA-B192-6B7566F4A34A} cmp31824781D8DC1AEED4F3ABA81DE48FD6 FwAvaloniaDialogs.resources.dll Azerbaijani 9.3.11.2649 9.3.12.2760 -{50F3C03B-659A-5539-87FF-935A26F987C1} cmpFD1E1A0F89D09757FD956A3DB67ADCD5 FwAvaloniaDialogs.resources.dll Nepali 9.3.11.2649 9.3.12.2760 -{53B372C4-1023-5014-A058-578E747D66D0} cmpD443A7DFF977E28AC44D447D0E4ACA28 FwAvaloniaDialogs.resources.dll Chinese 9.3.11.2649 9.3.12.2760 -{64770BF4-4611-528B-B7FC-FC111E3716D8} cmp4A898A1A999245077C43B9DE7492E771 FwAvaloniaDialogs.resources.dll Tamil 9.3.11.2649 9.3.12.2760 -{72531CD7-CC5E-533A-AF39-6D483D1ED03C} cmp65F49F9673D480828B04BF10B610B57E FwAvaloniaDialogs.resources.dll Malay 9.3.11.2649 9.3.12.2760 -{745A883C-E47D-59DF-952F-10AF51A18C74} cmp6E086A438A2AACEF005D057CE578D9CB FwAvaloniaDialogs.resources.dll Bengali 9.3.11.2649 9.3.12.2760 -{8321A3AA-69DB-589F-B83C-082377704A96} cmp1F13154539E7DBB2EB8C18144FC5F348 FwAvaloniaDialogs.resources.dll Spanish 9.3.11.2649 9.3.12.2760 -{8628C43E-FEB8-5C6D-BB7C-AD7E8BA9FBC1} cmp5D49E4ADE5C1736CEB872B432EDC90C3 FwAvaloniaDialogs.resources.dll Telugu 9.3.11.2649 9.3.12.2760 -{88681579-7969-519F-B1EE-3E1EFE158CB2} cmpD432FEB57D28D2492832BD160AFF67CC FwAvaloniaDialogs.resources.dll Yoruba 9.3.11.2649 9.3.12.2760 -{9D17CCAC-F4D6-557D-B086-5845094BC327} cmpBED07ADBDE4F22C424C02A2E94D50C3E FwAvaloniaDialogs.resources.dll Persian 9.3.11.2649 9.3.12.2760 -{A7A53726-0B9F-5AEE-B4D9-9F8003C1F59C} cmpEF8DD91FEE84E88C37CA4C42D04E90D6 FwAvaloniaDialogs.resources.dll Hungarian 9.3.11.2649 9.3.12.2760 -{AE8A94BA-6C1D-532F-B799-B3DE4D5E9CFB} cmp2CE4A58C7B7E963CAB0BF07C1C4DBEC1 FwAvaloniaDialogs.resources.dll Khmer 9.3.11.2649 9.3.12.2760 -{B62B13E6-14E2-5C8B-8FCF-5CF490563A4D} cmpC627A09DB11908C254EEC3597593421B FwAvaloniaDialogs.resources.dll French 9.3.11.2649 9.3.12.2760 -{B89D76C9-CED6-5BD8-B0F8-D8E6C44877B9} cmp26A637EFBC7F7116A6679D273E518E11 FwAvaloniaDialogs.resources.dll Thai 9.3.11.2649 9.3.12.2760 -{BD56E3F0-0E09-5EF5-A74B-8748F197F499} cmpCBC576B7692DE2BE123CAA7259F8CE52 FwAvaloniaDialogs.resources.dll Portuguese 9.3.11.2649 9.3.12.2760 -{CA22F8D0-D083-5141-93AD-71E6041710EA} cmp23734D77C22D73F0B5DF8C0F2CE427E0 FwAvaloniaDialogs.resources.dll Hindi 9.3.11.2649 9.3.12.2760 -{D49F26B5-8080-5FF3-88BC-1DC75102689C} cmpA0D7F5AFE0D912C3224C272F9208085D FwAvaloniaDialogs.resources.dll Arabic 9.3.11.2649 9.3.12.2760 -{D9A7DB60-7CA4-50B0-920B-8A038759BC74} cmp711B57B27D65861D56230B157C4964D6 FwAvaloniaDialogs.resources.dll Russian 9.3.11.2649 9.3.12.2760 -{EF8358F1-8081-5F3D-B297-50FEC1198EE7} cmp1BA32594D3E185D6D2745ECACB492618 FwAvaloniaDialogs.resources.dll Kinyarwanda 9.3.11.2649 9.3.12.2760 -{D4721227-54F7-5298-ABDE-798F9DF95320} cmp03AA89612A561251F9826F318D4FAF65 FwAvaloniaTheme.dll Complete 9.3.12.2752 9.3.12.2760 -{769C31AE-8E95-5728-9466-6FAE837AD657} cmp5C17B3B30B60CA33F9AABA9F3A10FAEE FwAvaloniaTheme.dll.config Complete 9.3.12.2752 9.3.12.2760 -{591C7319-CC1C-5514-B40F-0FA78CC9719B} cmpA6C9E9D197AF34C0EFF5656327CEEC27 FwAvaloniaTheme.pdb Complete 9.3.12.2752 9.3.12.2760 -{FC64481F-105F-5B2B-949C-54CDE425D396} cmpB76607CAFE1A9394C7D7CAEFD4B08D44 GrammarAndTextsForAI.xml Complete 9.3.11.2693 9.3.12.2760 -{B7B635E2-E51D-5EAC-B1CA-5DE35930FBA7} cmp4069FEDB42291ACEC7EB1440D0F53295 Irihi.Avalonia.Shared.Contracts.dll Complete 9.3.12.2752 9.3.12.2760 -{A65A90B9-142C-5DE4-B98F-EBC0BEECC456} cmp24C294AE5B98E64C128EEF9741042D0B Irihi.Avalonia.Shared.dll Complete 9.3.12.2752 9.3.12.2760 -{03468303-7A73-5B8F-9CB9-12F7B474EC40} cmp5E8EA429E963CFDEE82E15B4D5EEAFF6 libHarfBuzzSharp.so Complete 9.3.11.2649 9.3.12.2760 -{0C2D3395-CC96-5DB2-B7AE-442D28188444} cmp44CC67C4A96CC21E9A1D04B1CDE5EB3E libHarfBuzzSharp.so Complete 9.3.11.2649 9.3.12.2760 -{11F24985-B4F5-528B-8A59-A68F59E5574F} cmp734B1D3171E8EF6DEE9CF002FA3BA092 libHarfBuzzSharp.so Complete 9.3.11.2649 9.3.12.2760 -{1628A355-94ED-50C5-BEBB-76F3868663D7} cmpB9C172DC098B1EBFF4F47862F87A18F7 libHarfBuzzSharp.so Complete 9.3.11.2649 9.3.12.2760 -{24E1EA66-A06A-54CF-B466-E1D7AED52520} cmp07D38210B64F2AA12901CC6E43784185 libHarfBuzzSharp.so Complete 9.3.11.2649 9.3.12.2760 -{2B0CEAD2-F24F-591A-B172-15D0495595A6} cmp6D817951C151B5EF9B904E5F900A923B libHarfBuzzSharp.so Complete 9.3.11.2649 9.3.12.2760 -{900E3373-22F8-5504-9CE7-F7BE3F578548} cmp6566A436A2C4EDA4BAA540418FEE751F libHarfBuzzSharp.so Complete 9.3.11.2649 9.3.12.2760 -{B341AC2F-6FA0-5187-8BD6-6E8404A66637} cmp6CDEB1E669EB925C351C990B06CC5FF3 libHarfBuzzSharp.so Complete 9.3.11.2649 9.3.12.2760 -{D31A4F25-2D4F-5322-A669-38BB67F32D48} cmp439EF9315EA4FB2955A9AE542415FD40 libHarfBuzzSharp.so Complete 9.3.11.2649 9.3.12.2760 -{D85EA165-6DAE-54F0-9E9D-67B043E40574} cmp6E14E1BEB29F1E11962464578D3949AA libHarfBuzzSharp.so Complete 9.3.11.2649 9.3.12.2760 -{F792B87F-4043-560F-86A2-5CD21F6ABAE1} cmp474A6E6E84754B4F96DC4E226D549C2F libHarfBuzzSharp.so Complete 9.3.11.2649 9.3.12.2760 -{1295C1B1-497B-56F1-9E4D-9CD2270A5AC0} cmp17F13403DE0B83277E07E487DAE7CFBA libSkiaSharp.so Complete 9.3.11.2649 9.3.12.2760 -{74B395A4-B965-5711-8635-65ABB93C03F2} cmp7A9E437A0FCC6805D2C515DD961F9F44 libSkiaSharp.so Complete 9.3.11.2649 9.3.12.2760 -{F1466D7B-2CD9-5640-A469-64B9DECC30F4} cmpEFD5913C752979F4635AF3437AD8F561 libSkiaSharp.so Complete 9.3.11.2649 9.3.12.2760 -{FF00E675-2F4C-5CF4-B849-5F0E77316324} cmpA8AC13EE51C735C1656F69C4FC7D825D libSkiaSharp.so Complete 9.3.11.2649 9.3.12.2760 -{B4F7C8B9-56A9-54AD-A952-7F1A3BFFD01B} cmpBF470832EBCBE664093CDA0DCC6F69B2 MicroCom.Runtime.dll Complete 9.3.11.2649 9.3.12.2760 -{971E8516-64AE-5B04-896F-5A6EBFCFDE83} cmp15EFFF7E6DDAD2D5A8D4BFE464D19BE4 Microsoft.Extensions.DependencyInjection.dll Complete 9.3.11.2693 9.3.12.2760 -{95EF5357-59E7-576F-BC09-DCF444C53CA3} cmp2372452C908ABC16D10DC84FA8ED9FF6 Semi.Avalonia.dll Complete 9.3.12.2752 9.3.12.2760 -{3687447F-A73D-5ADC-9151-F821D4378E03} cmpDE89069E1505C2B71CCB874F2FB716E3 System.ComponentModel.Annotations.dll Complete 9.3.11.2649 9.3.12.2760 -{AEE69E7C-003A-5016-B04B-F9AA2B4B1CC0} cmp53BFEC07D81F9142E690F840F70744FE System.Threading.Channels.dll Complete 9.3.11.2649 9.3.12.2760 -{33DBDF8A-46E9-530C-A8CD-5A6F52D8211F} cmp70D4042F70E6492B602EAA442A075F1D Tmds.DBus.Protocol.dll Complete 9.3.11.2649 9.3.12.2760 -{07736C0A-78C4-536D-A467-E099499660EA} cmpBA53C5AE51B4235B8382C7991B2055B4 Ursa.dll Complete 9.3.12.2752 9.3.12.2760 -{AD76097B-0C92-5799-938D-106E11996214} cmpF7F425B4749E9A93F0B1A296F16514BA Ursa.Themes.Semi.dll Complete 9.3.12.2752 9.3.12.2760 +# Required update-minus-base components for the first checked patch on base 1452, including Fluent previously shipped on this base +# ComponentId Component File Feature +{DA0940BD-0FA2-5C72-BC6F-EFADFC01AF8A} cmp8F63659DC44FBC0F7703F8C24F1DF4DC AIExportInstructions.md Complete +{D3F3D4D1-690D-5570-90C3-07AC15DBF944} cmp6F532A88487F846B64ADC73FD2069DC9 Avalonia.Base.dll Complete +{48987B23-7456-544F-A320-AD7F5DD13B67} cmp295D431BF73CBADDD7FCD13AC811E14D Avalonia.Controls.dll Complete +{644EFFE3-9DFF-507A-8D53-17A1167496F8} cmp515AEF8E04CEE9DBD98D566FFBFC5465 Avalonia.DesignerSupport.dll Complete +{DB85E238-F9C9-55B7-8C76-FA2CC185A8E3} cmp60F16D63CA4BC46430A7ECB89F46FA18 Avalonia.Desktop.dll Complete +{DBF8B74B-D91F-5F23-B4D1-E55DC755DCE7} cmpFA9EACE595AFE0745CF5143237D52623 Avalonia.Dialogs.dll Complete +{B7C5DCDD-0DF8-5436-AF56-55A83851A666} cmp660B3182149886A1D06AF308784F3CBC Avalonia.dll Complete +{91ABBE9E-0556-5EC8-9221-482000C1A71C} cmpF938C1EDEF7D137C951EE38003E863C8 Avalonia.FreeDesktop.dll Complete +{2DA9E2A1-1CC9-5E58-A5BD-2B6C11CF4040} cmpB34F12C6C13ACAA9D4391C4044F7C1A4 Avalonia.Headless.dll Complete +{BECB2C60-A212-524F-8EE7-E42C5491F78E} cmp49FE4DA9C83AA91FE4179EAD8590BF5F Avalonia.Headless.NUnit.dll Complete +{59864563-443D-544D-BCDC-7C0D85EF7D44} cmp078E9C2E631026E5BBBDBBB010B6D7D0 Avalonia.Markup.dll Complete +{5719C05E-839F-5F34-BBEC-AB1D3F0209F8} cmp538664F9813F44D6D4610976AC1D20B5 Avalonia.Markup.Xaml.dll Complete +{A844E72E-3716-5E1E-91F6-789F62C9AC5B} cmpF639D8A1AAA4AC4F63933E45B12FAD75 Avalonia.Metal.dll Complete +{03A04376-78E7-5146-9381-B8BC765D0E87} cmp7DD0D4584C33CDF6F05FD47176EC792A Avalonia.MicroCom.dll Complete +{056CF987-318F-53BB-A76C-3ABFD17DA11B} cmp19619B1BF81C2A500731F072FC6EEDA7 Avalonia.Native.dll Complete +{264A8059-4F17-5B37-95DA-F5E26A11902E} cmp58CBF46A4A82BB2422AA8BD590F27D92 Avalonia.OpenGL.dll Complete +{E8EC545A-7485-5166-9315-E972C22E04D0} cmpE54DA5F709820A816226FCBC2E8F9B5B Avalonia.Remote.Protocol.dll Complete +{8216A4FF-C3C9-5F6A-BBB3-97862E97EAD2} cmp0B0E423744DFBF7DB1AD90D3E5B94209 Avalonia.Skia.dll Complete +{B3A225EB-3642-5FE4-8ED4-B2DAE6F8AC9B} cmpF906DFA4FA548A236D02C0A143CCEF15 Avalonia.Themes.Fluent.dll Complete +{4239BA48-B86B-52D5-99AD-0F0AFFB034BD} cmp6BFB5ADF4855D99B80240BFB4127E6F5 Avalonia.Vulkan.dll Complete +{B2E449AC-85AC-540C-83A0-7C8CB9005FB0} cmpFC099FB902BFDDE81E4773FB637CD110 Avalonia.Win32.Automation.dll Complete +{C358C2B8-04D4-5E0A-AE61-8D436EDC1860} cmp47567C3CF2B33B158D6003CCA591A2C0 Avalonia.Win32.dll Complete +{481D8C24-66DE-5F14-ABB1-C80300D372AA} cmpD97DACBAF1F92AF60E34BCB54D4891A6 Avalonia.Win32.Interoperability.dll Complete +{C0DBE567-DE9D-595A-85F7-48B64DE5B3DB} cmp7519FFB6B965DC9DDB696DB54C3365A9 Avalonia.X11.dll Complete +{9C145F8E-5738-503C-87CE-710C2DEE46B8} cmpBA8AFE637D8BF45BE83C8678EFE986F5 CommunityToolkit.Mvvm.dll Complete +{138EDD05-02B0-5C55-9DBA-6A0CC9726DA3} cmpE3398985DA0F0DBF2A03ADA8BABB122E FwAvalonia.dll Complete +{8F71DC11-6DCD-5578-897F-05A3A507BD50} cmp7F8FEA8233D91ADDE7F99189DEEE06E7 FwAvalonia.dll.config Complete +{EC9D9EB3-991F-5563-AEF3-7B290DD448D9} cmp4B58D41AC8D441E39C67C9FE6E6F4B85 FwAvalonia.pdb Complete +{00FD6E6A-496C-5310-AD67-45189D67AF41} cmpC744447C9E3167AF5E91CD4C5D78C756 FwAvalonia.resources.dll Spanish +{095448E5-5112-5EEF-92F5-FB28D4FCDDC0} cmp5D3F85D73A47D90E315C4469F29EA51B FwAvalonia.resources.dll French +{0D6418A5-F6D6-5090-A3D1-8FEB78E449D3} cmpC6AF4718DDA44D0F92E882FA0FA083BC FwAvalonia.resources.dll Vietnamese +{16380557-C64E-51F8-B35A-0B92F957BF35} cmpF9A97A3D1E5F46CC75623A11AF8B64D9 FwAvalonia.resources.dll Malay +{1DAFF7DC-57B1-500B-A643-563C8015367A} cmp3D7CC9B577D9CE4B86D7EBB3E918BC61 FwAvalonia.resources.dll Urdu +{21B4E444-8349-54D4-88E7-84936048C2A4} cmp5E829FE1D852405FCE4A88CC0E6D8721 FwAvalonia.resources.dll Azerbaijani +{255AD1F7-CA36-5D6C-B903-A887CFAD66F1} cmpDCEA9861FD22B1B0B313AB6ED5FF566A FwAvalonia.resources.dll Hindi +{27549925-9A72-5265-9DA2-5F5524D9ED0D} cmp64F38BDDCA32FB2EB6BD554E10E3F1BE FwAvalonia.resources.dll Malay +{3E327A7D-94CD-5BD8-827C-38635FCF53E9} cmpC204756496A4292493EE634E2478E7D9 FwAvalonia.resources.dll Swahili +{410A087A-45EF-5C59-AF88-BD60A54FFC28} cmpD6A8E5A8331698ECB9A1A4B73E4027FA FwAvalonia.resources.dll Portuguese +{4ABB6A21-6F95-58B2-8C16-38B74CBD220F} cmp87AFA172D4ED6657F2F23425A93039DC FwAvalonia.resources.dll Yoruba +{4B40E5CB-30D5-5677-A13C-F62EB4A6C9EF} cmp186CD5D44F2C490314A17B8B12F1E458 FwAvalonia.resources.dll Chinese +{55F6DA1E-A17F-5608-88B9-61512E42CDF8} cmp79796BEE4D10F8D4A1A7E3E37CA2F4A1 FwAvalonia.resources.dll Korean +{6E063CEC-98DE-598E-80D4-4708FCA0B412} cmp6830D5813172E2AB5E8303BEE2E0D6B6 FwAvalonia.resources.dll Russian +{794FB841-8C1A-502E-88DA-2E32D9296248} cmp3F935CA18EB62F02F358BBE9FDED913A FwAvalonia.resources.dll Khmer +{7997DB88-EE95-5A10-8CF7-DC77008DC0AC} cmp0F87125353CEA8E8B6313529DD7C6BD6 FwAvalonia.resources.dll Nepali +{7A5ED619-AA06-5F23-944F-1279F28BCABB} cmp7206F0E70BCBF954AE4922BFF706BB3E FwAvalonia.resources.dll Thai +{8A908B5D-2495-5A62-8808-ABC896FA4846} cmp5E731BC4EE8DF18A3F911E4310268089 FwAvalonia.resources.dll Bengali +{9C8D0766-C594-522A-B364-E73AF641AC0E} cmp0A8A05DD0C9C50DC093C17291DF69487 FwAvalonia.resources.dll Arabic +{A7530E25-7D26-5C02-849F-F1FC632EE39B} cmpF8D6A786D009147E667F3BD4E45726BA FwAvalonia.resources.dll Telugu +{C2E15D6E-5AF8-563D-95F7-2BDC22918634} cmpFAAF6F77C41BD26E21712EBD302909FC FwAvalonia.resources.dll Malayalam +{C3CD0E9D-6C19-5863-A3E3-D1577FB57C66} cmp3AB81E523E57BB90F1BDB4311D8BDF4A FwAvalonia.resources.dll Hungarian +{CB6C07D2-C204-546F-890C-49AE15F6FE32} cmp88CA64FE2DAD35DB3F64594BDF4BB4D6 FwAvalonia.resources.dll Persian +{CC2F72DF-B827-5792-B766-B08667192EF5} cmp340F48912B1B687BCD4BD984A600BB75 FwAvalonia.resources.dll German +{DD37BF7D-3A4E-5B2A-8798-4D596EFE60B8} cmp25D6A9F9FFEDC837E8298EB5BB3F9B8B FwAvalonia.resources.dll Turkish +{E367A90A-C4A6-5896-B326-44C546900329} cmp801B580FFEEAEF4B4D1F0E2135AB3F37 FwAvalonia.resources.dll Indonesian +{E6B9C3BB-1AEE-5A2D-B322-031AF8E32FD0} cmp3BFF24169B2639842124EDA8D54B6A60 FwAvalonia.resources.dll Kinyarwanda +{E6FA6A1F-FC77-5110-A4BE-575606D2F057} cmpB13758157B2FFEACC849C5CA49BD3E87 FwAvalonia.resources.dll Tamil +{F554DE59-8F6E-53EE-B238-97924BED4754} cmp945ABA08FFF1DB122D7B99A11C274AA7 FwAvalonia.resources.dll Burmese +{2E1C86F7-F728-5D5E-BDCC-64FF868A46B5} cmp92ECD687E7DEA3EA930AE1FA471F3062 FwAvaloniaDialogs.dll Complete +{46FF9909-6F68-58B3-A907-0550222ACD29} cmpF54565E87EF3FD52203F26835085A430 FwAvaloniaDialogs.dll.config Complete +{F2BB0A37-FE52-53C8-8696-EBE0F4D47B97} cmpCD1512CFB467FC5C896C997AA6D869DB FwAvaloniaDialogs.pdb Complete +{025484A1-AA3B-55C6-95C9-73ED057EC6E9} cmp82D0BAE64C8AE203065262909273FD30 FwAvaloniaDialogs.resources.dll Swahili +{096C45C2-0865-5070-AA31-CC96DF0D5B1C} cmpD94D58DC6BF163351A6EFA6F601B0D6C FwAvaloniaDialogs.resources.dll Malayalam +{0EB6AB4E-F822-5500-997F-A9BE4856EDD4} cmp1F15778C89D148F0DFD7CCD9BFEE602A FwAvaloniaDialogs.resources.dll German +{1D35E836-98AB-53E1-83C2-70BC05A2080D} cmp229E2BA7226AE09E8C70783A6AE9ABFC FwAvaloniaDialogs.resources.dll Indonesian +{1D47C484-8E7C-547A-A735-349E27ED325C} cmp47826A817F068F909056FE903637B0EA FwAvaloniaDialogs.resources.dll Burmese +{1D4C2AF2-1935-5C95-8222-7C00123A78C8} cmp4A4AA69EF39D1241CB0C302CDC811063 FwAvaloniaDialogs.resources.dll Malay +{1DBB57F5-5373-581C-A456-A9F845FF4874} cmp26917791EA6C95E2841DBCBBEE622324 FwAvaloniaDialogs.resources.dll Korean +{2D2E0509-0ED6-5178-8FA4-A2D5AD592ED1} cmp01D0C349B81F490963ED6D773A0770FA FwAvaloniaDialogs.resources.dll Vietnamese +{37663C83-A7D4-5024-ACD5-C7B6C0C2B068} cmp2BBC973D8DDF0B18438CA6A491393FE8 FwAvaloniaDialogs.resources.dll Urdu +{49755DFC-89B1-593E-9E91-E388A48DE600} cmpDCFF496901CBF45AD09A85AFCB24021A FwAvaloniaDialogs.resources.dll Turkish +{4D129082-FC11-56CA-B192-6B7566F4A34A} cmp31824781D8DC1AEED4F3ABA81DE48FD6 FwAvaloniaDialogs.resources.dll Azerbaijani +{50F3C03B-659A-5539-87FF-935A26F987C1} cmpFD1E1A0F89D09757FD956A3DB67ADCD5 FwAvaloniaDialogs.resources.dll Nepali +{53B372C4-1023-5014-A058-578E747D66D0} cmpD443A7DFF977E28AC44D447D0E4ACA28 FwAvaloniaDialogs.resources.dll Chinese +{64770BF4-4611-528B-B7FC-FC111E3716D8} cmp4A898A1A999245077C43B9DE7492E771 FwAvaloniaDialogs.resources.dll Tamil +{72531CD7-CC5E-533A-AF39-6D483D1ED03C} cmp65F49F9673D480828B04BF10B610B57E FwAvaloniaDialogs.resources.dll Malay +{745A883C-E47D-59DF-952F-10AF51A18C74} cmp6E086A438A2AACEF005D057CE578D9CB FwAvaloniaDialogs.resources.dll Bengali +{8321A3AA-69DB-589F-B83C-082377704A96} cmp1F13154539E7DBB2EB8C18144FC5F348 FwAvaloniaDialogs.resources.dll Spanish +{8628C43E-FEB8-5C6D-BB7C-AD7E8BA9FBC1} cmp5D49E4ADE5C1736CEB872B432EDC90C3 FwAvaloniaDialogs.resources.dll Telugu +{88681579-7969-519F-B1EE-3E1EFE158CB2} cmpD432FEB57D28D2492832BD160AFF67CC FwAvaloniaDialogs.resources.dll Yoruba +{9D17CCAC-F4D6-557D-B086-5845094BC327} cmpBED07ADBDE4F22C424C02A2E94D50C3E FwAvaloniaDialogs.resources.dll Persian +{A7A53726-0B9F-5AEE-B4D9-9F8003C1F59C} cmpEF8DD91FEE84E88C37CA4C42D04E90D6 FwAvaloniaDialogs.resources.dll Hungarian +{AE8A94BA-6C1D-532F-B799-B3DE4D5E9CFB} cmp2CE4A58C7B7E963CAB0BF07C1C4DBEC1 FwAvaloniaDialogs.resources.dll Khmer +{B62B13E6-14E2-5C8B-8FCF-5CF490563A4D} cmpC627A09DB11908C254EEC3597593421B FwAvaloniaDialogs.resources.dll French +{B89D76C9-CED6-5BD8-B0F8-D8E6C44877B9} cmp26A637EFBC7F7116A6679D273E518E11 FwAvaloniaDialogs.resources.dll Thai +{BD56E3F0-0E09-5EF5-A74B-8748F197F499} cmpCBC576B7692DE2BE123CAA7259F8CE52 FwAvaloniaDialogs.resources.dll Portuguese +{CA22F8D0-D083-5141-93AD-71E6041710EA} cmp23734D77C22D73F0B5DF8C0F2CE427E0 FwAvaloniaDialogs.resources.dll Hindi +{D49F26B5-8080-5FF3-88BC-1DC75102689C} cmpA0D7F5AFE0D912C3224C272F9208085D FwAvaloniaDialogs.resources.dll Arabic +{D9A7DB60-7CA4-50B0-920B-8A038759BC74} cmp711B57B27D65861D56230B157C4964D6 FwAvaloniaDialogs.resources.dll Russian +{EF8358F1-8081-5F3D-B297-50FEC1198EE7} cmp1BA32594D3E185D6D2745ECACB492618 FwAvaloniaDialogs.resources.dll Kinyarwanda +{D4721227-54F7-5298-ABDE-798F9DF95320} cmp03AA89612A561251F9826F318D4FAF65 FwAvaloniaTheme.dll Complete +{769C31AE-8E95-5728-9466-6FAE837AD657} cmp5C17B3B30B60CA33F9AABA9F3A10FAEE FwAvaloniaTheme.dll.config Complete +{591C7319-CC1C-5514-B40F-0FA78CC9719B} cmpA6C9E9D197AF34C0EFF5656327CEEC27 FwAvaloniaTheme.pdb Complete +{FC64481F-105F-5B2B-949C-54CDE425D396} cmpB76607CAFE1A9394C7D7CAEFD4B08D44 GrammarAndTextsForAI.xml Complete +{B7B635E2-E51D-5EAC-B1CA-5DE35930FBA7} cmp4069FEDB42291ACEC7EB1440D0F53295 Irihi.Avalonia.Shared.Contracts.dll Complete +{A65A90B9-142C-5DE4-B98F-EBC0BEECC456} cmp24C294AE5B98E64C128EEF9741042D0B Irihi.Avalonia.Shared.dll Complete +{03468303-7A73-5B8F-9CB9-12F7B474EC40} cmp5E8EA429E963CFDEE82E15B4D5EEAFF6 libHarfBuzzSharp.so Complete +{0C2D3395-CC96-5DB2-B7AE-442D28188444} cmp44CC67C4A96CC21E9A1D04B1CDE5EB3E libHarfBuzzSharp.so Complete +{11F24985-B4F5-528B-8A59-A68F59E5574F} cmp734B1D3171E8EF6DEE9CF002FA3BA092 libHarfBuzzSharp.so Complete +{1628A355-94ED-50C5-BEBB-76F3868663D7} cmpB9C172DC098B1EBFF4F47862F87A18F7 libHarfBuzzSharp.so Complete +{24E1EA66-A06A-54CF-B466-E1D7AED52520} cmp07D38210B64F2AA12901CC6E43784185 libHarfBuzzSharp.so Complete +{2B0CEAD2-F24F-591A-B172-15D0495595A6} cmp6D817951C151B5EF9B904E5F900A923B libHarfBuzzSharp.so Complete +{900E3373-22F8-5504-9CE7-F7BE3F578548} cmp6566A436A2C4EDA4BAA540418FEE751F libHarfBuzzSharp.so Complete +{B341AC2F-6FA0-5187-8BD6-6E8404A66637} cmp6CDEB1E669EB925C351C990B06CC5FF3 libHarfBuzzSharp.so Complete +{D31A4F25-2D4F-5322-A669-38BB67F32D48} cmp439EF9315EA4FB2955A9AE542415FD40 libHarfBuzzSharp.so Complete +{D85EA165-6DAE-54F0-9E9D-67B043E40574} cmp6E14E1BEB29F1E11962464578D3949AA libHarfBuzzSharp.so Complete +{F792B87F-4043-560F-86A2-5CD21F6ABAE1} cmp474A6E6E84754B4F96DC4E226D549C2F libHarfBuzzSharp.so Complete +{1295C1B1-497B-56F1-9E4D-9CD2270A5AC0} cmp17F13403DE0B83277E07E487DAE7CFBA libSkiaSharp.so Complete +{74B395A4-B965-5711-8635-65ABB93C03F2} cmp7A9E437A0FCC6805D2C515DD961F9F44 libSkiaSharp.so Complete +{F1466D7B-2CD9-5640-A469-64B9DECC30F4} cmpEFD5913C752979F4635AF3437AD8F561 libSkiaSharp.so Complete +{FF00E675-2F4C-5CF4-B849-5F0E77316324} cmpA8AC13EE51C735C1656F69C4FC7D825D libSkiaSharp.so Complete +{B4F7C8B9-56A9-54AD-A952-7F1A3BFFD01B} cmpBF470832EBCBE664093CDA0DCC6F69B2 MicroCom.Runtime.dll Complete +{971E8516-64AE-5B04-896F-5A6EBFCFDE83} cmp15EFFF7E6DDAD2D5A8D4BFE464D19BE4 Microsoft.Extensions.DependencyInjection.dll Complete +{95EF5357-59E7-576F-BC09-DCF444C53CA3} cmp2372452C908ABC16D10DC84FA8ED9FF6 Semi.Avalonia.dll Complete +{3687447F-A73D-5ADC-9151-F821D4378E03} cmpDE89069E1505C2B71CCB874F2FB716E3 System.ComponentModel.Annotations.dll Complete +{AEE69E7C-003A-5016-B04B-F9AA2B4B1CC0} cmp53BFEC07D81F9142E690F840F70744FE System.Threading.Channels.dll Complete +{33DBDF8A-46E9-530C-A8CD-5A6F52D8211F} cmp70D4042F70E6492B602EAA442A075F1D Tmds.DBus.Protocol.dll Complete +{07736C0A-78C4-536D-A467-E099499660EA} cmpBA53C5AE51B4235B8382C7991B2055B4 Ursa.dll Complete +{AD76097B-0C92-5799-938D-106E11996214} cmpF7F425B4749E9A93F0B1A296F16514BA Ursa.Themes.Semi.dll Complete diff --git a/scripts/Installer/PatchComponentLedger.Tests.ps1 b/scripts/Installer/PatchComponentLedger.Tests.ps1 new file mode 100644 index 0000000000..c4177f03f7 --- /dev/null +++ b/scripts/Installer/PatchComponentLedger.Tests.ps1 @@ -0,0 +1,142 @@ +[CmdletBinding()] +param() + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' +Import-Module (Join-Path $PSScriptRoot 'PatchComponentLedger.psm1') -Force + +$script:AssertionCount = 0 +$script:TestFiles = New-Object System.Collections.Generic.List[string] + +function Assert-True { + param([bool]$Condition, [string]$Message) + $script:AssertionCount++ + if (-not $Condition) { throw $Message } +} + +function Assert-Equal { + param($Actual, $Expected, [string]$Message) + $script:AssertionCount++ + if ($Actual -ne $Expected) { + throw "$Message Expected: $Expected Actual: $Actual" + } +} + +function Assert-Contains { + param([string]$Actual, [string]$Expected, [string]$Message) + $script:AssertionCount++ + if (-not $Actual.Contains($Expected)) { + throw "$Message Expected to find: $Expected Actual: $Actual" + } +} + +function Assert-Throws { + param([scriptblock]$Action, [string]$ExpectedText) + $script:AssertionCount++ + try { + & $Action + } + catch { + if ($_.Exception.Message.Contains($ExpectedText)) { return } + throw "Expected an error containing '$ExpectedText', got '$($_.Exception.Message)'." + } + throw "Expected an error containing '$ExpectedText'." +} + +function New-TestLedgerFile { + param([string[]]$Lines) + $path = [IO.Path]::GetTempFileName() + Set-Content -LiteralPath $path -Value $Lines -Encoding Ascii + $script:TestFiles.Add($path) + return $path +} + +try { + $patch2758 = 'jobs/FieldWorks-Win-all-Release-Patch/2758/FieldWorks_9.3.12.2758_b1452_x64.msp' + $patch2760 = 'jobs/FieldWorks-Win-all-Release-Patch/2760/FieldWorks_9.3.12.2760_b1452_x64.msp' + $patch2762 = 'jobs/FieldWorks-Win-all-Release-Patch/2762/FieldWorks_9.3.12.2762_b1452_x64.msp' + $ledger2760 = $patch2760 -replace '\.msp$', '_components.tsv' + $selection = Select-PreviousPublishedPatch -PatchKeys @($patch2758, $patch2760, $patch2762) -LedgerKeys @($ledger2760) -BaseBuildNumber '1452' -PatchVersion '9.3.12.2761' + Assert-Equal "$($selection.PatchKey)|$($selection.LedgerKey)|$($selection.UsesSeedLedger)" "$patch2760|$ledger2760|False" 'The selector must choose the immediate previous version and its matching ledger, ignoring later versions.' + + $orphanLedger = $patch2760 -replace '\.msp$', '_components.tsv' + $bootstrap = Select-PreviousPublishedPatch -PatchKeys @($patch2758) -LedgerKeys @($orphanLedger) -BaseBuildNumber '1452' -PatchVersion '9.3.12.2761' + Assert-True $bootstrap.UsesSeedLedger 'An orphan ledger must not end bootstrap.' + + Assert-Throws { + Select-PreviousPublishedPatch -PatchKeys @($patch2758, $patch2760) -LedgerKeys @($patch2758 -replace '\.msp$', '_components.tsv') -BaseBuildNumber '1452' -PatchVersion '9.3.12.2761' + } 'Bootstrap has ended' + + $master = @{ + '{33333333-3333-3333-3333-333333333333}' = [pscustomobject]@{ + ComponentId = '{33333333-3333-3333-3333-333333333333}' + Component = 'cmpBase' + File = 'Base.dll' + Feature = 'Complete' + } + } + $update = @{ + '{33333333-3333-3333-3333-333333333333}' = $master['{33333333-3333-3333-3333-333333333333}'] + '{44444444-4444-4444-4444-444444444444}' = [pscustomobject]@{ + ComponentId = '{44444444-4444-4444-4444-444444444444}' + Component = 'cmpNew' + File = 'New.dll' + Feature = 'Complete' + } + '{55555555-5555-5555-5555-555555555555}' = [pscustomobject]@{ + ComponentId = '{55555555-5555-5555-5555-555555555555}' + Component = 'cmpOther' + File = 'Other.dll' + Feature = 'Complete' + } + } + $newLedgerEntries = @(Get-UpdateMinusBaseLedgerEntries -Master $master -Update $update) + Assert-Equal (($newLedgerEntries | Sort-Object File | ForEach-Object File) -join ',') 'New.dll,Other.dll' 'The generated ledger must contain every current update-minus-base component.' + $ledgerPath = New-TestLedgerFile + Write-ComponentLedger -Path $ledgerPath -Entries $newLedgerEntries -Heading 'test' + $writtenLines = Get-Content -LiteralPath $ledgerPath + Assert-Equal $writtenLines[1] ('# ComponentId' + [char]9 + 'Component' + [char]9 + 'File' + [char]9 + 'Feature') 'Ledgers must contain only the four component fields.' + $roundTrip = Read-ComponentLedger -Path $ledgerPath + Assert-Equal (($roundTrip.Values | Sort-Object File | ForEach-Object File) -join ',') 'New.dll,Other.dll' 'The complete generated ledger must be readable.' + + $previous = [pscustomobject]@{ + ComponentId = '{66666666-6666-6666-6666-666666666666}' + Component = 'cmpPrevious' + File = 'Previous.dll' + Feature = 'Complete' + } + $required = @{} + foreach ($component in $master.Values) { $required[$component.ComponentId] = $component } + $required[$previous.ComponentId] = $previous + $missing = @(Get-MissingComponents -Required $required -Available $update) + Assert-Equal (($missing | Sort-Object File | ForEach-Object File) -join ',') 'Previous.dll' 'The comparison must detect the component missing from the previous-patch requirements.' + + $message = Format-DroppedComponentMessage -PatchVersion '9.3.12.2761' -BaseBuildNumber '1452' -Dropped @($previous) + Assert-Contains $message 'preserve the relative output path when one exists' 'The remediation must describe relative paths without making a universal exact-path claim.' + Assert-Contains $message '' 'The remediation must provide an actionable stand-in example.' + + $targetText = Get-Content -Raw (Join-Path $PSScriptRoot '..\..\Build\Installer.legacy.targets') + Assert-True ($targetText.Contains(' - param([string[]]$Path) + param([Parameter(Mandatory = $true)][string[]]$Path) $ledger = @{} foreach ($file in $Path) { - if (-not (Test-Path -LiteralPath $file)) { continue } + if (-not (Test-Path -LiteralPath $file -PathType Leaf)) { + throw "Component ledger file not found: $file" + } + $lineNumber = 0 foreach ($line in (Get-Content -LiteralPath $file)) { + $lineNumber++ if ($line -match '^\s*(#|$)') { continue } - $cells = $line -split "`t" - if ($cells.Count -lt $script:LedgerColumns.Count) { continue } - $id = $cells[0].ToUpperInvariant() - $entry = [pscustomobject]@{ - ComponentId = $id - Component = $cells[1] - File = $cells[2] - Feature = $cells[3] - FirstShipped = $cells[4] - LastShipped = $cells[5] + $cells = [regex]::Split($line, [char]9) + if ($cells.Count -ne $script:LedgerColumns.Count -or @($cells | Where-Object { [string]::IsNullOrWhiteSpace($_) }).Count -gt 0) { + throw "Malformed component ledger row in '$file' at line $lineNumber. Expected four non-empty tab-separated fields." } + $id = $cells[0].ToUpperInvariant() if ($ledger.ContainsKey($id)) { - $known = $ledger[$id] - if ([version]$entry.FirstShipped -lt [version]$known.FirstShipped) { $known.FirstShipped = $entry.FirstShipped } - if ([version]$entry.LastShipped -gt [version]$known.LastShipped) { $known.LastShipped = $entry.LastShipped } + throw "Duplicate component ID '$id' in component ledger '$file' at line $lineNumber." } - else { - $ledger[$id] = $entry + $ledger[$id] = [pscustomobject]@{ + ComponentId = $id + Component = $cells[1] + File = $cells[2] + Feature = $cells[3] } } } @@ -186,6 +171,105 @@ function Get-PatchVersionFromKey { return [version]((Split-Path $Key -Leaf) -split '_')[1] } +function Select-PreviousPublishedPatch { + param( + [Parameter(Mandatory = $true)][string[]]$PatchKeys, + [string[]]$LedgerKeys = @(), + [Parameter(Mandatory = $true)][string]$BaseBuildNumber, + [Parameter(Mandatory = $true)][string]$PatchVersion + ) + $baseMarker = "_b${BaseBuildNumber}_" + $targetVersion = [version]$PatchVersion + $eligiblePatches = New-Object System.Collections.Generic.List[object] + foreach ($key in $PatchKeys) { + if (-not $key.Contains($baseMarker)) { continue } + try { $version = Get-PatchVersionFromKey -Key $key } + catch { continue } + if ($version -lt $targetVersion) { + $eligiblePatches.Add([pscustomobject]@{ Key = $key; Version = $version }) + } + } + if ($eligiblePatches.Count -eq 0) { + return [pscustomobject]@{ + PatchKey = $null + LedgerKey = $null + UsesSeedLedger = $true + } + } + $orderedPatches = @($eligiblePatches | Sort-Object Version) + $previous = $orderedPatches[$orderedPatches.Count - 1] + $expectedLedgerKey = $previous.Key -replace '\.msp$', '_components.tsv' + $matchingLedger = @($LedgerKeys | Where-Object { $_ -eq $expectedLedgerKey }) + if ($matchingLedger.Count -gt 0) { + return [pscustomobject]@{ + PatchKey = $previous.Key + LedgerKey = $matchingLedger[0] + UsesSeedLedger = $false + } + } + $earlierLedgerPair = $false + foreach ($candidate in $orderedPatches) { + if ($candidate.Version -ge $previous.Version) { continue } + $candidateLedger = $candidate.Key -replace '\.msp$', '_components.tsv' + if (@($LedgerKeys | Where-Object { $_ -eq $candidateLedger }).Count -gt 0) { + $earlierLedgerPair = $true + break + } + } + if ($earlierLedgerPair) { + throw "Bootstrap has ended: published patch $($previous.Key) has no matching ledger $expectedLedgerKey. Publish the ledger beside that patch before building another patch." + } + return [pscustomobject]@{ + PatchKey = $previous.Key + LedgerKey = $null + UsesSeedLedger = $true + } +} + +function Format-RemovedComponentRemediation { + param([Parameter(Mandatory = $true)][object[]]$Dropped) + $lines = New-Object System.Collections.Generic.List[string] + $lines.Add('Remediation:') + $lines.Add('Add the output path for each missing file to RemovedSinceLastBase in Build/Installer.legacy.targets; preserve the relative output path when one exists:') + foreach ($entry in ($Dropped | Sort-Object File, ComponentId)) { + $path = '$(dir-outputBase)/' + $entry.File + $lines.Add((' ')) + } + $lines.Add('Create an issue to remove the placeholder before the next base build.') + return ($lines -join [Environment]::NewLine) +} + +function Get-UpdateMinusBaseLedgerEntries { + param( + [Parameter(Mandatory = $true)][hashtable]$Master, + [Parameter(Mandatory = $true)][hashtable]$Update + ) + $entries = New-Object System.Collections.Generic.List[object] + foreach ($id in $Update.Keys) { + if ($Master.ContainsKey($id)) { continue } + $entry = $Update[$id] + $entries.Add([pscustomobject]@{ + ComponentId = $entry.ComponentId + Component = $entry.Component + File = $entry.File + Feature = $entry.Feature + }) + } + return $entries.ToArray() +} + +function Get-MissingComponents { + param( + [Parameter(Mandatory = $true)][hashtable]$Required, + [Parameter(Mandatory = $true)][hashtable]$Available + ) + $missing = New-Object System.Collections.Generic.List[object] + foreach ($id in $Required.Keys) { + if (-not $Available.ContainsKey($id)) { $missing.Add($Required[$id]) } + } + return ($missing | Sort-Object File, ComponentId) +} + function Format-DroppedComponentMessage { param( [Parameter(Mandatory = $true)][string]$PatchVersion, @@ -196,12 +280,10 @@ function Format-DroppedComponentMessage { foreach ($entry in $Dropped) { $lines.Add("Patch $PatchVersion drops component $($entry.ComponentId)") $lines.Add(" file: $($entry.File) (feature $($entry.Feature))") - $lines.Add(" shipped: patches $($entry.FirstShipped) to $($entry.LastShipped) on base $BaseBuildNumber") + $lines.Add(" base: $BaseBuildNumber") } - $lines.Add('Fix: restore the file to the output, or add a component stand-in (see') - $lines.Add(' Docs/workflows/patch-component-removal.md), as well as an issue to remove') - $lines.Add(' the file before cutting the next base build.') + $lines.Add((Format-RemovedComponentRemediation -Dropped $Dropped)) return ($lines -join [Environment]::NewLine) } -Export-ModuleMember -Function Get-MsiComponents, Get-MsiProperty, Read-ComponentLedger, Write-ComponentLedger, Get-UpdateBucketKeys, Get-PublishedPatchKeys, Save-PublishedFile, Get-PatchVersionFromKey, Format-DroppedComponentMessage +Export-ModuleMember -Function Get-MsiComponents, Read-ComponentLedger, Write-ComponentLedger, Get-UpdateBucketKeys, Get-PublishedPatchKeys, Save-PublishedFile, Get-PatchVersionFromKey, Select-PreviousPublishedPatch, Get-UpdateMinusBaseLedgerEntries, Get-MissingComponents, Format-RemovedComponentRemediation, Format-DroppedComponentMessage diff --git a/scripts/Installer/Test-PatchComponentLedger.ps1 b/scripts/Installer/Test-PatchComponentLedger.ps1 index a6f23cd2fc..2813989521 100644 --- a/scripts/Installer/Test-PatchComponentLedger.ps1 +++ b/scripts/Installer/Test-PatchComponentLedger.ps1 @@ -1,14 +1,10 @@ <# .SYNOPSIS -Fails a patch build when the new patch drops a component that a patch published on the same base -shipped. +Fails a patch build when the new Update MSI drops a component from the base or previous patch. .DESCRIPTION -Compares the patch's upgraded MSI image against the ledger of every component that -earlier patches on this base added: the committed seed ledger plus the per-patch -ledgers published next to each .msp. Writes this patch's own ledger for publishing. -pyro already rejects dropping a component the base itself ships (PYRO0305), so only -patch-added components are checked here. +Compares the new Update MSI with the base MSI and the complete ledger from the immediately +previous published patch. Writes the complete update-minus-base ledger for this patch. .PARAMETER MasterMsi The base (Master) MSI rebuilt by the patch build. @@ -20,13 +16,11 @@ The upgraded (Update) MSI the patch was diffed from. The new patch's product version, e.g. 9.3.12.2761. .PARAMETER SeedLedger -Committed ledger covering patches published before per-patch ledgers existed. Optional. +Committed snapshot supplies the initial previous-patch component set for a base. .PARAMETER OutLedger Where to write this patch's ledger. -.PARAMETER SkipPublished -Check against the seed ledger only, without reading published ledgers from the update bucket. #> [CmdletBinding()] param( @@ -35,8 +29,7 @@ param( [Parameter(Mandatory = $true)][string]$BaseBuildNumber, [Parameter(Mandatory = $true)][string]$PatchVersion, [string]$SeedLedger, - [Parameter(Mandatory = $true)][string]$OutLedger, - [switch]$SkipPublished + [Parameter(Mandatory = $true)][string]$OutLedger ) Set-StrictMode -Version Latest @@ -46,46 +39,47 @@ Import-Module (Join-Path $PSScriptRoot 'PatchComponentLedger.psm1') -Force $master = Get-MsiComponents -MsiPath $MasterMsi $update = Get-MsiComponents -MsiPath $UpdateMsi -$added = New-Object System.Collections.Generic.List[object] -foreach ($id in $update.Keys) { - if (-not $master.ContainsKey($id)) { - $entry = $update[$id] - $added.Add([pscustomobject]@{ - ComponentId = $entry.ComponentId - Component = $entry.Component - File = $entry.File - Feature = $entry.Feature - FirstShipped = $PatchVersion - LastShipped = $PatchVersion - }) - } -} -Write-ComponentLedger -Path $OutLedger -Entries $added.ToArray() -Heading "Components patch $PatchVersion adds to base $BaseBuildNumber" -Write-Output "Patch $PatchVersion adds $($added.Count) components to base $BaseBuildNumber; ledger written to $OutLedger" +$publishedPatchKeys = @(Get-PublishedPatchKeys -BaseBuildNumber $BaseBuildNumber -Wildcard '*.msp') +$publishedLedgerKeys = @(Get-PublishedPatchKeys -BaseBuildNumber $BaseBuildNumber -Wildcard '*_components.tsv') +$previous = Select-PreviousPublishedPatch ` + -PatchKeys $publishedPatchKeys ` + -LedgerKeys $publishedLedgerKeys ` + -BaseBuildNumber $BaseBuildNumber ` + -PatchVersion $PatchVersion $ledgerFiles = New-Object System.Collections.Generic.List[string] -if ($SeedLedger -and (Test-Path -LiteralPath $SeedLedger)) { $ledgerFiles.Add($SeedLedger) } -if (-not $SkipPublished) { +if ($previous.LedgerKey) { $downloads = Join-Path ([IO.Path]::GetTempPath()) "fw-patch-ledgers-b$BaseBuildNumber" New-Item -ItemType Directory -Force -Path $downloads | Out-Null - foreach ($key in (Get-PublishedPatchKeys -BaseBuildNumber $BaseBuildNumber -Wildcard '*_components.tsv')) { - $ledgerFiles.Add((Save-PublishedFile -Key $key -Directory $downloads)) - } + $ledgerFiles.Add((Save-PublishedFile -Key $previous.LedgerKey -Directory $downloads)) +} +elseif ($SeedLedger -and (Test-Path -LiteralPath $SeedLedger)) { + $ledgerFiles.Add($SeedLedger) +} +elseif ($previous.PatchKey) { + throw "Published patch $($previous.PatchKey) has no matching ledger, and the bootstrap ledger '$SeedLedger' is unavailable." +} + +$previousLedger = Read-ComponentLedger -Path $ledgerFiles.ToArray() +$required = @{} +foreach ($entry in $master.Values) { $required[$entry.ComponentId] = $entry } +foreach ($entry in $previousLedger.Values) { + if (-not $required.ContainsKey($entry.ComponentId)) { $required[$entry.ComponentId] = $entry } } -$ledger = Read-ComponentLedger -Path $ledgerFiles.ToArray() -Write-Output "Checking against $($ledger.Count) components from $($ledgerFiles.Count) ledger files" +$dropped = @(Get-MissingComponents -Required $required -Available $update) +$newLedgerEntries = @(Get-UpdateMinusBaseLedgerEntries -Master $master -Update $update) +Write-ComponentLedger -Path $OutLedger -Entries $newLedgerEntries -Heading "Complete update-minus-base ledger for patch $PatchVersion on base $BaseBuildNumber" +Write-Output "Patch $PatchVersion ledger contains $($newLedgerEntries.Count) update-minus-base components; ledger written to $OutLedger" +Write-Output "Checking against $($required.Count) base and previous-patch components" -$dropped = @($ledger.Values | Where-Object { -not $update.ContainsKey($_.ComponentId) } | Sort-Object File) if ($dropped.Count -eq 0) { - Write-Output '[OK] The patch keeps every component published patches on this base have shipped.' + Write-Output '[OK] The patch keeps every base and immediately previous-patch component.' exit 0 } $message = Format-DroppedComponentMessage -PatchVersion $PatchVersion -BaseBuildNumber $BaseBuildNumber -Dropped $dropped Write-Output $message if ($env:GITHUB_ACTIONS -eq 'true') { - foreach ($entry in $dropped) { - Write-Output "::error title=Patch drops a shipped component::Patch $PatchVersion drops $($entry.File) (component $($entry.ComponentId), feature $($entry.Feature)), shipped by patches $($entry.FirstShipped) to $($entry.LastShipped) on base $BaseBuildNumber. See Docs/workflows/patch-component-removal.md." - } + Write-Output ('::error title=Patch component removal::' + ($message -replace "`r?`n", ' ')) } exit 1 diff --git a/scripts/Installer/Test-PatchInstall.ps1 b/scripts/Installer/Test-PatchInstall.ps1 deleted file mode 100644 index a1306aa693..0000000000 --- a/scripts/Installer/Test-PatchInstall.ps1 +++ /dev/null @@ -1,127 +0,0 @@ -<# -.SYNOPSIS -Installs the published base, applies the latest published patch, then applies the new patch the -way a user would, and fails unless the new patch really installs. - -.DESCRIPTION -Windows Installer's default reaction to a patch that drops a component is to log SELMGR, -install nothing, and still return success. This test applies the new patch with -MSIENFORCEUPGRADECOMPONENTRULES=1, which turns that into error 2771, and also checks that -FieldWorks.exe on disk carries the patch's version. Needs administrator rights and a machine -without FieldWorks installed; a CI runner qualifies. - -.PARAMETER Patch -The new .msp. - -.PARAMETER PatchVersion -The new patch's product version, e.g. 9.3.12.2761. - -.PARAMETER UpdateMsi -The upgraded (Update) MSI, which supplies the product code and the FieldWorks.exe component. - -.PARAMETER LedgerPaths -Ledger files that map the component GUIDs Windows Installer reports to file names. - -.PARAMETER WorkDir -Where downloads and install logs go. -#> -[CmdletBinding()] -param( - [Parameter(Mandatory = $true)][string]$Patch, - [Parameter(Mandatory = $true)][string]$PatchVersion, - [Parameter(Mandatory = $true)][string]$BaseBuildNumber, - [Parameter(Mandatory = $true)][string]$UpdateMsi, - [string[]]$LedgerPaths = @(), - [Parameter(Mandatory = $true)][string]$WorkDir -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' -$ProgressPreference = 'SilentlyContinue' -Import-Module (Join-Path $PSScriptRoot 'PatchComponentLedger.psm1') -Force - -New-Item -ItemType Directory -Force -Path $WorkDir | Out-Null -$failures = New-Object System.Collections.Generic.List[string] - -function Invoke-Logged { - param([string]$FilePath, [string]$Arguments, [string]$Step) - Write-Output "${Step}: $FilePath $Arguments" - $process = Start-Process -FilePath $FilePath -ArgumentList $Arguments -Wait -PassThru - # 3010 = success, restart required. - if ($process.ExitCode -ne 0 -and $process.ExitCode -ne 3010) { - throw "$Step failed with exit code $($process.ExitCode)" - } -} - -$baseKey = @(Get-UpdateBucketKeys -Prefix "jobs/FieldWorks-Win-all-Release-Base/$BaseBuildNumber/" -Like '*_Online_x64.exe') | Select-Object -First 1 -if (-not $baseKey) { throw "No published base installer found for base $BaseBuildNumber" } -$baseInstaller = Save-PublishedFile -Key $baseKey -Directory $WorkDir -Invoke-Logged -FilePath $baseInstaller -Arguments "/quiet /norestart /log `"$WorkDir\01-base.log`"" -Step 'Install base' - -$newVersion = [version]$PatchVersion -$previousKey = @(Get-PublishedPatchKeys -BaseBuildNumber $BaseBuildNumber -Wildcard '*.msp' | - Where-Object { (Get-PatchVersionFromKey $_) -lt $newVersion } | - Sort-Object { Get-PatchVersionFromKey $_ }) | Select-Object -Last 1 -if ($previousKey) { - $previousPatch = Save-PublishedFile -Key $previousKey -Directory $WorkDir - Invoke-Logged -FilePath 'msiexec.exe' -Arguments "/p `"$previousPatch`" /qn /norestart AUTOUPDATE=True /l*vx `"$WorkDir\02-previous-patch.log`"" -Step "Apply published patch $(Get-PatchVersionFromKey $previousKey)" -} -else { - Write-Output "No earlier patch is published on base $BaseBuildNumber; applying the new patch to the base alone." -} - -$newLog = Join-Path $WorkDir '03-new-patch.log' -$process = Start-Process -FilePath 'msiexec.exe' -ArgumentList "/p `"$Patch`" /qn /norestart AUTOUPDATE=True MSIENFORCEUPGRADECOMPONENTRULES=1 /l*vx `"$newLog`"" -Wait -PassThru -Write-Output "Apply new patch ${PatchVersion}: exit code $($process.ExitCode)" -if ($process.ExitCode -ne 0 -and $process.ExitCode -ne 3010) { - $failures.Add("Applying patch $PatchVersion failed with exit code $($process.ExitCode).") -} - -# SELMGR names each component a feature has lost; the ledgers turn those GUIDs into files. -$dropped = New-Object System.Collections.Generic.List[object] -if (Test-Path -LiteralPath $newLog) { - $ledger = Read-ComponentLedger -Path $LedgerPaths - $pattern = "SELMGR: ComponentId '(\{[0-9A-Fa-f-]+\})' is registered to feature '([^']+)'" - foreach ($match in (Select-String -LiteralPath $newLog -Pattern $pattern -AllMatches)) { - foreach ($m in $match.Matches) { - $id = $m.Groups[1].Value.ToUpperInvariant() - if ($ledger.ContainsKey($id)) { - $dropped.Add($ledger[$id]) - } - else { - $dropped.Add([pscustomobject]@{ ComponentId = $id; File = '(not in any ledger)'; Feature = $m.Groups[2].Value; FirstShipped = '?'; LastShipped = '?' }) - } - } - } -} -if ($dropped.Count -gt 0) { - $failures.Add((Format-DroppedComponentMessage -PatchVersion $PatchVersion -BaseBuildNumber $BaseBuildNumber -Dropped $dropped.ToArray())) -} - -# The registered version advances even when no files are copied, so read the binary itself. -$productCode = Get-MsiProperty -MsiPath $UpdateMsi -Name 'ProductCode' -$exeComponent = @((Get-MsiComponents -MsiPath $UpdateMsi).Values | Where-Object { $_.File -eq 'FieldWorks.exe' }) | Select-Object -First 1 -if ($productCode -and $exeComponent) { - $installer = New-Object -ComObject WindowsInstaller.Installer - $exePath = $installer.GetType().InvokeMember('ComponentPath', 'GetProperty', $null, $installer, @($productCode, $exeComponent.ComponentId)) - $onDisk = if ($exePath -and (Test-Path -LiteralPath $exePath)) { (Get-Item -LiteralPath $exePath).VersionInfo.FileVersion } else { '(missing)' } - Write-Output "FieldWorks.exe on disk: $onDisk (expected $PatchVersion)" - if ($onDisk -ne $PatchVersion) { - $failures.Add("FieldWorks.exe on disk is $onDisk after applying patch $PatchVersion; the patch did not install its files.") - } -} -else { - $failures.Add('Could not find the product code or the FieldWorks.exe component in the Update MSI.') -} - -if ($failures.Count -eq 0) { - Write-Output "[OK] Patch $PatchVersion installs on base $BaseBuildNumber over the latest published patch." - exit 0 -} -foreach ($failure in $failures) { - Write-Output "ERROR: $failure" - if ($env:GITHUB_ACTIONS -eq 'true') { - Write-Output ('::error title=Patch install test failed::' + ($failure -replace "`r?`n", ' ')) - } -} -exit 1 From 9abe79670ad426b197bfa1c1726d5f67ead3743f Mon Sep 17 00:00:00 2001 From: John Lambert Date: Mon, 21 Sep 2026 14:16:38 -0400 Subject: [PATCH 3/6] LT-22801: Keep patch ledgers in S3 Use S3 as the sole patch-ledger source. The first release on a patch line creates its ledger, and later patches require the ledger beside the immediately previous MSP. --- .github/workflows/patch-installer-cd.yml | 1 - Docs/workflows/patch-component-removal.md | 10 +- FLExInstaller/AGENTS.md | 2 +- FLExInstaller/PatchComponentLedger/b1452.tsv | 120 ------------------ .../Installer/PatchComponentLedger.Tests.ps1 | 10 +- scripts/Installer/PatchComponentLedger.psm1 | 5 +- .../Installer/Test-PatchComponentLedger.ps1 | 10 -- 7 files changed, 10 insertions(+), 148 deletions(-) delete mode 100644 FLExInstaller/PatchComponentLedger/b1452.tsv diff --git a/.github/workflows/patch-installer-cd.yml b/.github/workflows/patch-installer-cd.yml index 15b6f36036..3fc3df51ee 100644 --- a/.github/workflows/patch-installer-cd.yml +++ b/.github/workflows/patch-installer-cd.yml @@ -272,7 +272,6 @@ jobs: -UpdateMsi "${{ steps.find_patch.outputs.update_msi }}" ` -BaseBuildNumber $env:BASE_BUILD_NUMBER ` -PatchVersion "${{ steps.find_patch.outputs.patch_version }}" ` - -SeedLedger "FLExInstaller/PatchComponentLedger/b$($env:BASE_BUILD_NUMBER).tsv" ` -OutLedger "${{ steps.find_patch.outputs.ledger_file }}" - name: Sign Patch diff --git a/Docs/workflows/patch-component-removal.md b/Docs/workflows/patch-component-removal.md index 877c8a18ca..e1ac64fcf9 100644 --- a/Docs/workflows/patch-component-removal.md +++ b/Docs/workflows/patch-component-removal.md @@ -12,10 +12,10 @@ The check compares the new Update MSI with the union of: - the components in the Master/base MSI; and - the complete update-minus-base ledger published beside the immediately previous MSP. -The committed `FLExInstaller/PatchComponentLedger/b1452.tsv` file bootstraps base 1452 -until a previous published MSP has a ledger. After a ledger-bearing patch exists, the -immediately previous MSP must have its matching `*_components.tsv` file. Version filtering -keeps a release branch from consuming a ledger for a later patch. +The first published patch on a patch line creates the initial S3 ledger. After a +ledger-bearing patch exists, the immediately previous MSP must have its matching +`*_components.tsv` file. Version filtering keeps a release branch from consuming a ledger +for a later patch. Ledgers are release artifacts in S3 and are not stored in this repository. Each successful patch writes its complete update-minus-base component set to a ledger beside the MSP. A later patch uses the ledger beside its immediately previous MSP. @@ -57,5 +57,5 @@ set that future patches must preserve. ```powershell .\scripts\Installer\Test-PatchComponentLedger.ps1 -MasterMsi -UpdateMsi ` -BaseBuildNumber 1452 -PatchVersion 9.3.12.2761 ` - -SeedLedger FLExInstaller\PatchComponentLedger\b1452.tsv -OutLedger out.tsv + -OutLedger out.tsv ``` diff --git a/FLExInstaller/AGENTS.md b/FLExInstaller/AGENTS.md index 2e8939a69e..b4662ea043 100644 --- a/FLExInstaller/AGENTS.md +++ b/FLExInstaller/AGENTS.md @@ -13,7 +13,7 @@ Minimal installer guidance for agents. - Heat exclusions: **`PatchableInstallerHeatExclude.xml`** is copied to **`PatchableInstaller/BaseInstallerBuild/heat-exclude.xml`** before Heat (see **`Build/Installer.legacy.targets`** `CopyFilesToInstall`). - **`buildMsi.bat`** passes **`-fv`** to **`light.exe`** so **`MsiAssemblyName`** includes **fileVersion** (same intent as MSBuild **`SetMsiAssemblyNameFileVersion=true`**), which helps GAC servicing when **`AssemblyVersion`** is unchanged but the binary’s **file version** increases. - Newtonsoft.Json and similar authored components live in **`CustomComponents.wxi`** (overlays **`PatchableInstaller/Common`**), with definitions guarded by **``** so patch/update authoring omits them when only **`UPDATEBUILDDIR`** is set. Add matching **`ComponentRef`** entries in **`FLExInstaller/CustomFeatures.wxi`** inside the **same** **`...`** so patch builds do not emit dangling refs (**LGHT0094**). WiX 6 **`Framework.wxs`** uses the same pattern for **`Feature Complete`**. Do not use **`FeatureRef Id="Complete"`** from an include that appears before **`Framework.wxs`** defines `Complete` (Light **LGHT0095**). -- **Patch component removal:** the patch ledger check compares the new **Update** MSI with the **Master** MSI and the complete ledger from the immediately previous published MSP. The base-1452 bootstrap is `PatchComponentLedger/b1452.tsv`; after a ledger-bearing patch exists, its matching ledger must be beside the immediately previous MSP. Later patch versions are ignored. +- **Patch component removal:** the patch ledger check compares the new **Update** MSI with the **Master** MSI and the complete ledger from the immediately previous published MSP. The first published patch creates the initial ledger in S3; after a ledger-bearing patch exists, its matching ledger must be beside the immediately previous MSP. Ledgers are not stored in this repository. Later patch versions are ignored. - A missing component from either source uses the same fix: add its output path, preserving the file's relative output path, to the **`RemovedSinceLastBase`** item list in the **`RescuePatching`** target of **`Build/Installer.legacy.targets`**. The target writes a zero-byte stand-in into **`$(dir-outputBase)`** so the component remains in the patch. - Create an issue to remove the stand-in before the next base. Keep **`Avalonia.Themes.Fluent.dll`** in the list for base 1452. - A base build fails while any **`RemovedSinceLastBase`** entries remain. Remove each entry and its corresponding zero-byte file before creating the base. diff --git a/FLExInstaller/PatchComponentLedger/b1452.tsv b/FLExInstaller/PatchComponentLedger/b1452.tsv deleted file mode 100644 index d80f45b7fc..0000000000 --- a/FLExInstaller/PatchComponentLedger/b1452.tsv +++ /dev/null @@ -1,120 +0,0 @@ -# Required update-minus-base components for the first checked patch on base 1452, including Fluent previously shipped on this base -# ComponentId Component File Feature -{DA0940BD-0FA2-5C72-BC6F-EFADFC01AF8A} cmp8F63659DC44FBC0F7703F8C24F1DF4DC AIExportInstructions.md Complete -{D3F3D4D1-690D-5570-90C3-07AC15DBF944} cmp6F532A88487F846B64ADC73FD2069DC9 Avalonia.Base.dll Complete -{48987B23-7456-544F-A320-AD7F5DD13B67} cmp295D431BF73CBADDD7FCD13AC811E14D Avalonia.Controls.dll Complete -{644EFFE3-9DFF-507A-8D53-17A1167496F8} cmp515AEF8E04CEE9DBD98D566FFBFC5465 Avalonia.DesignerSupport.dll Complete -{DB85E238-F9C9-55B7-8C76-FA2CC185A8E3} cmp60F16D63CA4BC46430A7ECB89F46FA18 Avalonia.Desktop.dll Complete -{DBF8B74B-D91F-5F23-B4D1-E55DC755DCE7} cmpFA9EACE595AFE0745CF5143237D52623 Avalonia.Dialogs.dll Complete -{B7C5DCDD-0DF8-5436-AF56-55A83851A666} cmp660B3182149886A1D06AF308784F3CBC Avalonia.dll Complete -{91ABBE9E-0556-5EC8-9221-482000C1A71C} cmpF938C1EDEF7D137C951EE38003E863C8 Avalonia.FreeDesktop.dll Complete -{2DA9E2A1-1CC9-5E58-A5BD-2B6C11CF4040} cmpB34F12C6C13ACAA9D4391C4044F7C1A4 Avalonia.Headless.dll Complete -{BECB2C60-A212-524F-8EE7-E42C5491F78E} cmp49FE4DA9C83AA91FE4179EAD8590BF5F Avalonia.Headless.NUnit.dll Complete -{59864563-443D-544D-BCDC-7C0D85EF7D44} cmp078E9C2E631026E5BBBDBBB010B6D7D0 Avalonia.Markup.dll Complete -{5719C05E-839F-5F34-BBEC-AB1D3F0209F8} cmp538664F9813F44D6D4610976AC1D20B5 Avalonia.Markup.Xaml.dll Complete -{A844E72E-3716-5E1E-91F6-789F62C9AC5B} cmpF639D8A1AAA4AC4F63933E45B12FAD75 Avalonia.Metal.dll Complete -{03A04376-78E7-5146-9381-B8BC765D0E87} cmp7DD0D4584C33CDF6F05FD47176EC792A Avalonia.MicroCom.dll Complete -{056CF987-318F-53BB-A76C-3ABFD17DA11B} cmp19619B1BF81C2A500731F072FC6EEDA7 Avalonia.Native.dll Complete -{264A8059-4F17-5B37-95DA-F5E26A11902E} cmp58CBF46A4A82BB2422AA8BD590F27D92 Avalonia.OpenGL.dll Complete -{E8EC545A-7485-5166-9315-E972C22E04D0} cmpE54DA5F709820A816226FCBC2E8F9B5B Avalonia.Remote.Protocol.dll Complete -{8216A4FF-C3C9-5F6A-BBB3-97862E97EAD2} cmp0B0E423744DFBF7DB1AD90D3E5B94209 Avalonia.Skia.dll Complete -{B3A225EB-3642-5FE4-8ED4-B2DAE6F8AC9B} cmpF906DFA4FA548A236D02C0A143CCEF15 Avalonia.Themes.Fluent.dll Complete -{4239BA48-B86B-52D5-99AD-0F0AFFB034BD} cmp6BFB5ADF4855D99B80240BFB4127E6F5 Avalonia.Vulkan.dll Complete -{B2E449AC-85AC-540C-83A0-7C8CB9005FB0} cmpFC099FB902BFDDE81E4773FB637CD110 Avalonia.Win32.Automation.dll Complete -{C358C2B8-04D4-5E0A-AE61-8D436EDC1860} cmp47567C3CF2B33B158D6003CCA591A2C0 Avalonia.Win32.dll Complete -{481D8C24-66DE-5F14-ABB1-C80300D372AA} cmpD97DACBAF1F92AF60E34BCB54D4891A6 Avalonia.Win32.Interoperability.dll Complete -{C0DBE567-DE9D-595A-85F7-48B64DE5B3DB} cmp7519FFB6B965DC9DDB696DB54C3365A9 Avalonia.X11.dll Complete -{9C145F8E-5738-503C-87CE-710C2DEE46B8} cmpBA8AFE637D8BF45BE83C8678EFE986F5 CommunityToolkit.Mvvm.dll Complete -{138EDD05-02B0-5C55-9DBA-6A0CC9726DA3} cmpE3398985DA0F0DBF2A03ADA8BABB122E FwAvalonia.dll Complete -{8F71DC11-6DCD-5578-897F-05A3A507BD50} cmp7F8FEA8233D91ADDE7F99189DEEE06E7 FwAvalonia.dll.config Complete -{EC9D9EB3-991F-5563-AEF3-7B290DD448D9} cmp4B58D41AC8D441E39C67C9FE6E6F4B85 FwAvalonia.pdb Complete -{00FD6E6A-496C-5310-AD67-45189D67AF41} cmpC744447C9E3167AF5E91CD4C5D78C756 FwAvalonia.resources.dll Spanish -{095448E5-5112-5EEF-92F5-FB28D4FCDDC0} cmp5D3F85D73A47D90E315C4469F29EA51B FwAvalonia.resources.dll French -{0D6418A5-F6D6-5090-A3D1-8FEB78E449D3} cmpC6AF4718DDA44D0F92E882FA0FA083BC FwAvalonia.resources.dll Vietnamese -{16380557-C64E-51F8-B35A-0B92F957BF35} cmpF9A97A3D1E5F46CC75623A11AF8B64D9 FwAvalonia.resources.dll Malay -{1DAFF7DC-57B1-500B-A643-563C8015367A} cmp3D7CC9B577D9CE4B86D7EBB3E918BC61 FwAvalonia.resources.dll Urdu -{21B4E444-8349-54D4-88E7-84936048C2A4} cmp5E829FE1D852405FCE4A88CC0E6D8721 FwAvalonia.resources.dll Azerbaijani -{255AD1F7-CA36-5D6C-B903-A887CFAD66F1} cmpDCEA9861FD22B1B0B313AB6ED5FF566A FwAvalonia.resources.dll Hindi -{27549925-9A72-5265-9DA2-5F5524D9ED0D} cmp64F38BDDCA32FB2EB6BD554E10E3F1BE FwAvalonia.resources.dll Malay -{3E327A7D-94CD-5BD8-827C-38635FCF53E9} cmpC204756496A4292493EE634E2478E7D9 FwAvalonia.resources.dll Swahili -{410A087A-45EF-5C59-AF88-BD60A54FFC28} cmpD6A8E5A8331698ECB9A1A4B73E4027FA FwAvalonia.resources.dll Portuguese -{4ABB6A21-6F95-58B2-8C16-38B74CBD220F} cmp87AFA172D4ED6657F2F23425A93039DC FwAvalonia.resources.dll Yoruba -{4B40E5CB-30D5-5677-A13C-F62EB4A6C9EF} cmp186CD5D44F2C490314A17B8B12F1E458 FwAvalonia.resources.dll Chinese -{55F6DA1E-A17F-5608-88B9-61512E42CDF8} cmp79796BEE4D10F8D4A1A7E3E37CA2F4A1 FwAvalonia.resources.dll Korean -{6E063CEC-98DE-598E-80D4-4708FCA0B412} cmp6830D5813172E2AB5E8303BEE2E0D6B6 FwAvalonia.resources.dll Russian -{794FB841-8C1A-502E-88DA-2E32D9296248} cmp3F935CA18EB62F02F358BBE9FDED913A FwAvalonia.resources.dll Khmer -{7997DB88-EE95-5A10-8CF7-DC77008DC0AC} cmp0F87125353CEA8E8B6313529DD7C6BD6 FwAvalonia.resources.dll Nepali -{7A5ED619-AA06-5F23-944F-1279F28BCABB} cmp7206F0E70BCBF954AE4922BFF706BB3E FwAvalonia.resources.dll Thai -{8A908B5D-2495-5A62-8808-ABC896FA4846} cmp5E731BC4EE8DF18A3F911E4310268089 FwAvalonia.resources.dll Bengali -{9C8D0766-C594-522A-B364-E73AF641AC0E} cmp0A8A05DD0C9C50DC093C17291DF69487 FwAvalonia.resources.dll Arabic -{A7530E25-7D26-5C02-849F-F1FC632EE39B} cmpF8D6A786D009147E667F3BD4E45726BA FwAvalonia.resources.dll Telugu -{C2E15D6E-5AF8-563D-95F7-2BDC22918634} cmpFAAF6F77C41BD26E21712EBD302909FC FwAvalonia.resources.dll Malayalam -{C3CD0E9D-6C19-5863-A3E3-D1577FB57C66} cmp3AB81E523E57BB90F1BDB4311D8BDF4A FwAvalonia.resources.dll Hungarian -{CB6C07D2-C204-546F-890C-49AE15F6FE32} cmp88CA64FE2DAD35DB3F64594BDF4BB4D6 FwAvalonia.resources.dll Persian -{CC2F72DF-B827-5792-B766-B08667192EF5} cmp340F48912B1B687BCD4BD984A600BB75 FwAvalonia.resources.dll German -{DD37BF7D-3A4E-5B2A-8798-4D596EFE60B8} cmp25D6A9F9FFEDC837E8298EB5BB3F9B8B FwAvalonia.resources.dll Turkish -{E367A90A-C4A6-5896-B326-44C546900329} cmp801B580FFEEAEF4B4D1F0E2135AB3F37 FwAvalonia.resources.dll Indonesian -{E6B9C3BB-1AEE-5A2D-B322-031AF8E32FD0} cmp3BFF24169B2639842124EDA8D54B6A60 FwAvalonia.resources.dll Kinyarwanda -{E6FA6A1F-FC77-5110-A4BE-575606D2F057} cmpB13758157B2FFEACC849C5CA49BD3E87 FwAvalonia.resources.dll Tamil -{F554DE59-8F6E-53EE-B238-97924BED4754} cmp945ABA08FFF1DB122D7B99A11C274AA7 FwAvalonia.resources.dll Burmese -{2E1C86F7-F728-5D5E-BDCC-64FF868A46B5} cmp92ECD687E7DEA3EA930AE1FA471F3062 FwAvaloniaDialogs.dll Complete -{46FF9909-6F68-58B3-A907-0550222ACD29} cmpF54565E87EF3FD52203F26835085A430 FwAvaloniaDialogs.dll.config Complete -{F2BB0A37-FE52-53C8-8696-EBE0F4D47B97} cmpCD1512CFB467FC5C896C997AA6D869DB FwAvaloniaDialogs.pdb Complete -{025484A1-AA3B-55C6-95C9-73ED057EC6E9} cmp82D0BAE64C8AE203065262909273FD30 FwAvaloniaDialogs.resources.dll Swahili -{096C45C2-0865-5070-AA31-CC96DF0D5B1C} cmpD94D58DC6BF163351A6EFA6F601B0D6C FwAvaloniaDialogs.resources.dll Malayalam -{0EB6AB4E-F822-5500-997F-A9BE4856EDD4} cmp1F15778C89D148F0DFD7CCD9BFEE602A FwAvaloniaDialogs.resources.dll German -{1D35E836-98AB-53E1-83C2-70BC05A2080D} cmp229E2BA7226AE09E8C70783A6AE9ABFC FwAvaloniaDialogs.resources.dll Indonesian -{1D47C484-8E7C-547A-A735-349E27ED325C} cmp47826A817F068F909056FE903637B0EA FwAvaloniaDialogs.resources.dll Burmese -{1D4C2AF2-1935-5C95-8222-7C00123A78C8} cmp4A4AA69EF39D1241CB0C302CDC811063 FwAvaloniaDialogs.resources.dll Malay -{1DBB57F5-5373-581C-A456-A9F845FF4874} cmp26917791EA6C95E2841DBCBBEE622324 FwAvaloniaDialogs.resources.dll Korean -{2D2E0509-0ED6-5178-8FA4-A2D5AD592ED1} cmp01D0C349B81F490963ED6D773A0770FA FwAvaloniaDialogs.resources.dll Vietnamese -{37663C83-A7D4-5024-ACD5-C7B6C0C2B068} cmp2BBC973D8DDF0B18438CA6A491393FE8 FwAvaloniaDialogs.resources.dll Urdu -{49755DFC-89B1-593E-9E91-E388A48DE600} cmpDCFF496901CBF45AD09A85AFCB24021A FwAvaloniaDialogs.resources.dll Turkish -{4D129082-FC11-56CA-B192-6B7566F4A34A} cmp31824781D8DC1AEED4F3ABA81DE48FD6 FwAvaloniaDialogs.resources.dll Azerbaijani -{50F3C03B-659A-5539-87FF-935A26F987C1} cmpFD1E1A0F89D09757FD956A3DB67ADCD5 FwAvaloniaDialogs.resources.dll Nepali -{53B372C4-1023-5014-A058-578E747D66D0} cmpD443A7DFF977E28AC44D447D0E4ACA28 FwAvaloniaDialogs.resources.dll Chinese -{64770BF4-4611-528B-B7FC-FC111E3716D8} cmp4A898A1A999245077C43B9DE7492E771 FwAvaloniaDialogs.resources.dll Tamil -{72531CD7-CC5E-533A-AF39-6D483D1ED03C} cmp65F49F9673D480828B04BF10B610B57E FwAvaloniaDialogs.resources.dll Malay -{745A883C-E47D-59DF-952F-10AF51A18C74} cmp6E086A438A2AACEF005D057CE578D9CB FwAvaloniaDialogs.resources.dll Bengali -{8321A3AA-69DB-589F-B83C-082377704A96} cmp1F13154539E7DBB2EB8C18144FC5F348 FwAvaloniaDialogs.resources.dll Spanish -{8628C43E-FEB8-5C6D-BB7C-AD7E8BA9FBC1} cmp5D49E4ADE5C1736CEB872B432EDC90C3 FwAvaloniaDialogs.resources.dll Telugu -{88681579-7969-519F-B1EE-3E1EFE158CB2} cmpD432FEB57D28D2492832BD160AFF67CC FwAvaloniaDialogs.resources.dll Yoruba -{9D17CCAC-F4D6-557D-B086-5845094BC327} cmpBED07ADBDE4F22C424C02A2E94D50C3E FwAvaloniaDialogs.resources.dll Persian -{A7A53726-0B9F-5AEE-B4D9-9F8003C1F59C} cmpEF8DD91FEE84E88C37CA4C42D04E90D6 FwAvaloniaDialogs.resources.dll Hungarian -{AE8A94BA-6C1D-532F-B799-B3DE4D5E9CFB} cmp2CE4A58C7B7E963CAB0BF07C1C4DBEC1 FwAvaloniaDialogs.resources.dll Khmer -{B62B13E6-14E2-5C8B-8FCF-5CF490563A4D} cmpC627A09DB11908C254EEC3597593421B FwAvaloniaDialogs.resources.dll French -{B89D76C9-CED6-5BD8-B0F8-D8E6C44877B9} cmp26A637EFBC7F7116A6679D273E518E11 FwAvaloniaDialogs.resources.dll Thai -{BD56E3F0-0E09-5EF5-A74B-8748F197F499} cmpCBC576B7692DE2BE123CAA7259F8CE52 FwAvaloniaDialogs.resources.dll Portuguese -{CA22F8D0-D083-5141-93AD-71E6041710EA} cmp23734D77C22D73F0B5DF8C0F2CE427E0 FwAvaloniaDialogs.resources.dll Hindi -{D49F26B5-8080-5FF3-88BC-1DC75102689C} cmpA0D7F5AFE0D912C3224C272F9208085D FwAvaloniaDialogs.resources.dll Arabic -{D9A7DB60-7CA4-50B0-920B-8A038759BC74} cmp711B57B27D65861D56230B157C4964D6 FwAvaloniaDialogs.resources.dll Russian -{EF8358F1-8081-5F3D-B297-50FEC1198EE7} cmp1BA32594D3E185D6D2745ECACB492618 FwAvaloniaDialogs.resources.dll Kinyarwanda -{D4721227-54F7-5298-ABDE-798F9DF95320} cmp03AA89612A561251F9826F318D4FAF65 FwAvaloniaTheme.dll Complete -{769C31AE-8E95-5728-9466-6FAE837AD657} cmp5C17B3B30B60CA33F9AABA9F3A10FAEE FwAvaloniaTheme.dll.config Complete -{591C7319-CC1C-5514-B40F-0FA78CC9719B} cmpA6C9E9D197AF34C0EFF5656327CEEC27 FwAvaloniaTheme.pdb Complete -{FC64481F-105F-5B2B-949C-54CDE425D396} cmpB76607CAFE1A9394C7D7CAEFD4B08D44 GrammarAndTextsForAI.xml Complete -{B7B635E2-E51D-5EAC-B1CA-5DE35930FBA7} cmp4069FEDB42291ACEC7EB1440D0F53295 Irihi.Avalonia.Shared.Contracts.dll Complete -{A65A90B9-142C-5DE4-B98F-EBC0BEECC456} cmp24C294AE5B98E64C128EEF9741042D0B Irihi.Avalonia.Shared.dll Complete -{03468303-7A73-5B8F-9CB9-12F7B474EC40} cmp5E8EA429E963CFDEE82E15B4D5EEAFF6 libHarfBuzzSharp.so Complete -{0C2D3395-CC96-5DB2-B7AE-442D28188444} cmp44CC67C4A96CC21E9A1D04B1CDE5EB3E libHarfBuzzSharp.so Complete -{11F24985-B4F5-528B-8A59-A68F59E5574F} cmp734B1D3171E8EF6DEE9CF002FA3BA092 libHarfBuzzSharp.so Complete -{1628A355-94ED-50C5-BEBB-76F3868663D7} cmpB9C172DC098B1EBFF4F47862F87A18F7 libHarfBuzzSharp.so Complete -{24E1EA66-A06A-54CF-B466-E1D7AED52520} cmp07D38210B64F2AA12901CC6E43784185 libHarfBuzzSharp.so Complete -{2B0CEAD2-F24F-591A-B172-15D0495595A6} cmp6D817951C151B5EF9B904E5F900A923B libHarfBuzzSharp.so Complete -{900E3373-22F8-5504-9CE7-F7BE3F578548} cmp6566A436A2C4EDA4BAA540418FEE751F libHarfBuzzSharp.so Complete -{B341AC2F-6FA0-5187-8BD6-6E8404A66637} cmp6CDEB1E669EB925C351C990B06CC5FF3 libHarfBuzzSharp.so Complete -{D31A4F25-2D4F-5322-A669-38BB67F32D48} cmp439EF9315EA4FB2955A9AE542415FD40 libHarfBuzzSharp.so Complete -{D85EA165-6DAE-54F0-9E9D-67B043E40574} cmp6E14E1BEB29F1E11962464578D3949AA libHarfBuzzSharp.so Complete -{F792B87F-4043-560F-86A2-5CD21F6ABAE1} cmp474A6E6E84754B4F96DC4E226D549C2F libHarfBuzzSharp.so Complete -{1295C1B1-497B-56F1-9E4D-9CD2270A5AC0} cmp17F13403DE0B83277E07E487DAE7CFBA libSkiaSharp.so Complete -{74B395A4-B965-5711-8635-65ABB93C03F2} cmp7A9E437A0FCC6805D2C515DD961F9F44 libSkiaSharp.so Complete -{F1466D7B-2CD9-5640-A469-64B9DECC30F4} cmpEFD5913C752979F4635AF3437AD8F561 libSkiaSharp.so Complete -{FF00E675-2F4C-5CF4-B849-5F0E77316324} cmpA8AC13EE51C735C1656F69C4FC7D825D libSkiaSharp.so Complete -{B4F7C8B9-56A9-54AD-A952-7F1A3BFFD01B} cmpBF470832EBCBE664093CDA0DCC6F69B2 MicroCom.Runtime.dll Complete -{971E8516-64AE-5B04-896F-5A6EBFCFDE83} cmp15EFFF7E6DDAD2D5A8D4BFE464D19BE4 Microsoft.Extensions.DependencyInjection.dll Complete -{95EF5357-59E7-576F-BC09-DCF444C53CA3} cmp2372452C908ABC16D10DC84FA8ED9FF6 Semi.Avalonia.dll Complete -{3687447F-A73D-5ADC-9151-F821D4378E03} cmpDE89069E1505C2B71CCB874F2FB716E3 System.ComponentModel.Annotations.dll Complete -{AEE69E7C-003A-5016-B04B-F9AA2B4B1CC0} cmp53BFEC07D81F9142E690F840F70744FE System.Threading.Channels.dll Complete -{33DBDF8A-46E9-530C-A8CD-5A6F52D8211F} cmp70D4042F70E6492B602EAA442A075F1D Tmds.DBus.Protocol.dll Complete -{07736C0A-78C4-536D-A467-E099499660EA} cmpBA53C5AE51B4235B8382C7991B2055B4 Ursa.dll Complete -{AD76097B-0C92-5799-938D-106E11996214} cmpF7F425B4749E9A93F0B1A296F16514BA Ursa.Themes.Semi.dll Complete diff --git a/scripts/Installer/PatchComponentLedger.Tests.ps1 b/scripts/Installer/PatchComponentLedger.Tests.ps1 index c4177f03f7..47c6215f97 100644 --- a/scripts/Installer/PatchComponentLedger.Tests.ps1 +++ b/scripts/Installer/PatchComponentLedger.Tests.ps1 @@ -57,11 +57,11 @@ try { $patch2762 = 'jobs/FieldWorks-Win-all-Release-Patch/2762/FieldWorks_9.3.12.2762_b1452_x64.msp' $ledger2760 = $patch2760 -replace '\.msp$', '_components.tsv' $selection = Select-PreviousPublishedPatch -PatchKeys @($patch2758, $patch2760, $patch2762) -LedgerKeys @($ledger2760) -BaseBuildNumber '1452' -PatchVersion '9.3.12.2761' - Assert-Equal "$($selection.PatchKey)|$($selection.LedgerKey)|$($selection.UsesSeedLedger)" "$patch2760|$ledger2760|False" 'The selector must choose the immediate previous version and its matching ledger, ignoring later versions.' + Assert-Equal "$($selection.PatchKey)|$($selection.LedgerKey)" "$patch2760|$ledger2760" 'The selector must choose the immediate previous version and its matching ledger, ignoring later versions.' $orphanLedger = $patch2760 -replace '\.msp$', '_components.tsv' $bootstrap = Select-PreviousPublishedPatch -PatchKeys @($patch2758) -LedgerKeys @($orphanLedger) -BaseBuildNumber '1452' -PatchVersion '9.3.12.2761' - Assert-True $bootstrap.UsesSeedLedger 'An orphan ledger must not end bootstrap.' + Assert-Equal "$($bootstrap.PatchKey)|$($bootstrap.LedgerKey)" "$patch2758|" 'A patch line without a published ledger must start by publishing the current patch ledger.' Assert-Throws { Select-PreviousPublishedPatch -PatchKeys @($patch2758, $patch2760) -LedgerKeys @($patch2758 -replace '\.msp$', '_components.tsv') -BaseBuildNumber '1452' -PatchVersion '9.3.12.2761' @@ -118,6 +118,7 @@ try { $targetText = Get-Content -Raw (Join-Path $PSScriptRoot '..\..\Build\Installer.legacy.targets') Assert-True ($targetText.Contains(' - param([Parameter(Mandatory = $true)][string[]]$Path) + param([Parameter(Mandatory = $true)][AllowEmptyCollection()][string[]]$Path) $ledger = @{} foreach ($file in $Path) { @@ -193,7 +193,6 @@ function Select-PreviousPublishedPatch { return [pscustomobject]@{ PatchKey = $null LedgerKey = $null - UsesSeedLedger = $true } } $orderedPatches = @($eligiblePatches | Sort-Object Version) @@ -204,7 +203,6 @@ function Select-PreviousPublishedPatch { return [pscustomobject]@{ PatchKey = $previous.Key LedgerKey = $matchingLedger[0] - UsesSeedLedger = $false } } $earlierLedgerPair = $false @@ -222,7 +220,6 @@ function Select-PreviousPublishedPatch { return [pscustomobject]@{ PatchKey = $previous.Key LedgerKey = $null - UsesSeedLedger = $true } } diff --git a/scripts/Installer/Test-PatchComponentLedger.ps1 b/scripts/Installer/Test-PatchComponentLedger.ps1 index 2813989521..4a408706b3 100644 --- a/scripts/Installer/Test-PatchComponentLedger.ps1 +++ b/scripts/Installer/Test-PatchComponentLedger.ps1 @@ -15,9 +15,6 @@ The upgraded (Update) MSI the patch was diffed from. .PARAMETER PatchVersion The new patch's product version, e.g. 9.3.12.2761. -.PARAMETER SeedLedger -Committed snapshot supplies the initial previous-patch component set for a base. - .PARAMETER OutLedger Where to write this patch's ledger. @@ -28,7 +25,6 @@ param( [Parameter(Mandatory = $true)][string]$UpdateMsi, [Parameter(Mandatory = $true)][string]$BaseBuildNumber, [Parameter(Mandatory = $true)][string]$PatchVersion, - [string]$SeedLedger, [Parameter(Mandatory = $true)][string]$OutLedger ) @@ -53,12 +49,6 @@ if ($previous.LedgerKey) { New-Item -ItemType Directory -Force -Path $downloads | Out-Null $ledgerFiles.Add((Save-PublishedFile -Key $previous.LedgerKey -Directory $downloads)) } -elseif ($SeedLedger -and (Test-Path -LiteralPath $SeedLedger)) { - $ledgerFiles.Add($SeedLedger) -} -elseif ($previous.PatchKey) { - throw "Published patch $($previous.PatchKey) has no matching ledger, and the bootstrap ledger '$SeedLedger' is unavailable." -} $previousLedger = Read-ComponentLedger -Path $ledgerFiles.ToArray() $required = @{} From 1b7727fe2ef6e4cdc405870efd63fb368dcda3a2 Mon Sep 17 00:00:00 2001 From: John Lambert Date: Mon, 21 Sep 2026 15:32:09 -0400 Subject: [PATCH 4/6] LT-22801: Preserve ledger file paths Record APPFOLDER-relative paths so rescue entries remain correct for nested files. Keep ledger artifacts in S3 and narrow checks to file-backed components supported by RemovedSinceLastBase. --- .github/workflows/patch-installer-cd.yml | 3 +- Build/Installer.legacy.targets | 4 +- Docs/workflows/patch-component-removal.md | 25 ++--- FLExInstaller/AGENTS.md | 6 +- .../Installer/PatchComponentLedger.Tests.ps1 | 15 +++ scripts/Installer/PatchComponentLedger.psm1 | 102 ++++++++++++++++-- .../Installer/Test-PatchComponentLedger.ps1 | 20 ++-- 7 files changed, 137 insertions(+), 38 deletions(-) diff --git a/.github/workflows/patch-installer-cd.yml b/.github/workflows/patch-installer-cd.yml index 3fc3df51ee..32e0917cd8 100644 --- a/.github/workflows/patch-installer-cd.yml +++ b/.github/workflows/patch-installer-cd.yml @@ -39,7 +39,7 @@ on: # on Jenkins-built bases. base_release: description: 'The github release for the base build artifacts (separate only for bootstrapping; should be removed after 9.3 is the stable)' - default: 'build-1452' # When updating this, update base_build_number and their fallbacks below, too. Before cutting a new base, follow Docs/workflows/patch-component-removal.md. + default: 'build-1452' # Must match base_build_number and its fallbacks. base_build_number: description: 'The base build number' required: false @@ -333,4 +333,3 @@ jobs: ./Output/**/*.log.stderr ./PatchableInstaller/CreateUpdatePatch/Master/AppHarvest.wxs ./PatchableInstaller/CreateUpdatePatch/Update/AppHarvest.wxs - ./BuildDir/*_components.tsv diff --git a/Build/Installer.legacy.targets b/Build/Installer.legacy.targets index c8904724c2..2ba8e8b436 100644 --- a/Build/Installer.legacy.targets +++ b/Build/Installer.legacy.targets @@ -115,8 +115,8 @@ in SIL.LCModel 11.0.0-beta0176, so nothing copies StructureMap.dll to the output any more. --> diff --git a/Docs/workflows/patch-component-removal.md b/Docs/workflows/patch-component-removal.md index e1ac64fcf9..46e671e482 100644 --- a/Docs/workflows/patch-component-removal.md +++ b/Docs/workflows/patch-component-removal.md @@ -1,32 +1,33 @@ # Removing a file from a patch line -A patch must keep every component shipped by the base or by the immediately previous -published patch. Windows Installer sees the base and newest patch together, so a component -that exists only in the previous patch must remain in the new patch as well. +A patch must keep every file-backed component under MSI APPFOLDER (application output +files) shipped by the base or by the immediately previous published patch. Windows +Installer sees the base and newest patch together, so those file-backed components must +remain in the new patch. ## What CI checks -`patch-installer-cd.yml` runs the component-ledger check before signing or publishing. +`patch-installer-cd.yml` runs the file-backed component ledger check before signing or publishing. The check compares the new Update MSI with the union of: -- the components in the Master/base MSI; and -- the complete update-minus-base ledger published beside the immediately previous MSP. +- the file-backed components under MSI APPFOLDER in the Master/base MSI; and +- the complete file-backed update-minus-base ledger published beside the immediately previous MSP. The first published patch on a patch line creates the initial S3 ledger. After a ledger-bearing patch exists, the immediately previous MSP must have its matching `*_components.tsv` file. Version filtering keeps a release branch from consuming a ledger for a later patch. Ledgers are release artifacts in S3 and are not stored in this repository. -Each successful patch writes its complete update-minus-base component set to a ledger beside +Each successful patch writes its complete update-minus-base file-backed component set under MSI APPFOLDER to a ledger beside the MSP. A later patch uses the ledger beside its immediately previous MSP. ## When the check fails -The diagnostic names every missing component and file, regardless of whether it came from +The diagnostic names every missing file-backed component and file, regardless of whether it came from the base MSI or the previous-patch ledger: ``` -Patch 9.3.12.2761 drops component {B3A225EB-3642-5FE4-8ED4-B2DAE6F8AC9B} +Patch 9.3.12.2761 drops file-backed component {B3A225EB-3642-5FE4-8ED4-B2DAE6F8AC9B} file: Avalonia.Themes.Fluent.dll (feature Complete) base: 1452 Remediation: @@ -37,7 +38,7 @@ Create an issue to remove the placeholder before the next base build. ``` The `RemovedSinceLastBase` entry makes the build write a zero-byte stand-in at that path. -That component remains in the patch, so machines that already have the real file keep +That file-backed component remains in the patch, so machines that already have the real file keep working while the removal issue is completed. Keep `Avalonia.Themes.Fluent.dll` in this list for base 1452. @@ -49,8 +50,8 @@ stand-in paths and requires both cleanup actions: 1. Remove each `RemovedSinceLastBase` entry from `Build/Installer.legacy.targets`. 2. Remove each corresponding zero-byte file from the build output. -Complete the removal issue before creating the base. A new base establishes the component -set that future patches must preserve. +Complete the removal issue before creating the base. A new base establishes the file-backed component +set under MSI APPFOLDER that future patches must preserve. ## Running the check locally diff --git a/FLExInstaller/AGENTS.md b/FLExInstaller/AGENTS.md index b4662ea043..bbbdbc664c 100644 --- a/FLExInstaller/AGENTS.md +++ b/FLExInstaller/AGENTS.md @@ -13,11 +13,11 @@ Minimal installer guidance for agents. - Heat exclusions: **`PatchableInstallerHeatExclude.xml`** is copied to **`PatchableInstaller/BaseInstallerBuild/heat-exclude.xml`** before Heat (see **`Build/Installer.legacy.targets`** `CopyFilesToInstall`). - **`buildMsi.bat`** passes **`-fv`** to **`light.exe`** so **`MsiAssemblyName`** includes **fileVersion** (same intent as MSBuild **`SetMsiAssemblyNameFileVersion=true`**), which helps GAC servicing when **`AssemblyVersion`** is unchanged but the binary’s **file version** increases. - Newtonsoft.Json and similar authored components live in **`CustomComponents.wxi`** (overlays **`PatchableInstaller/Common`**), with definitions guarded by **``** so patch/update authoring omits them when only **`UPDATEBUILDDIR`** is set. Add matching **`ComponentRef`** entries in **`FLExInstaller/CustomFeatures.wxi`** inside the **same** **`...`** so patch builds do not emit dangling refs (**LGHT0094**). WiX 6 **`Framework.wxs`** uses the same pattern for **`Feature Complete`**. Do not use **`FeatureRef Id="Complete"`** from an include that appears before **`Framework.wxs`** defines `Complete` (Light **LGHT0095**). -- **Patch component removal:** the patch ledger check compares the new **Update** MSI with the **Master** MSI and the complete ledger from the immediately previous published MSP. The first published patch creates the initial ledger in S3; after a ledger-bearing patch exists, its matching ledger must be beside the immediately previous MSP. Ledgers are not stored in this repository. Later patch versions are ignored. - - A missing component from either source uses the same fix: add its output path, preserving the file's relative output path, to the **`RemovedSinceLastBase`** item list in the **`RescuePatching`** target of **`Build/Installer.legacy.targets`**. The target writes a zero-byte stand-in into **`$(dir-outputBase)`** so the component remains in the patch. +- **Patch file-backed component removal:** the patch ledger check compares file-backed components under MSI APPFOLDER in the new **Update** MSI and **Master** MSI with the complete file-backed ledger from the immediately previous published MSP. The first published patch creates the initial ledger in S3; after a ledger-bearing patch exists, its matching ledger must be beside the immediately previous MSP. Ledgers are not stored in this repository. Later patch versions are ignored. + - A missing file-backed component from either source uses the same fix: add its output path, preserving the file's relative output path, to the **`RemovedSinceLastBase`** item list in the **`RescuePatching`** target of **`Build/Installer.legacy.targets`**. The target writes a zero-byte stand-in into **`$(dir-outputBase)`** so the file-backed component remains in the patch. - Create an issue to remove the stand-in before the next base. Keep **`Avalonia.Themes.Fluent.dll`** in the list for base 1452. - A base build fails while any **`RemovedSinceLastBase`** entries remain. Remove each entry and its corresponding zero-byte file before creating the base. -- **Do not** add a dropped component to **`PatchableInstallerHeatExclude.xml`**. That list is for artifacts that must never be harvested, not for preserving patch component identity. +- **Do not** add a dropped file-backed component to **`PatchableInstallerHeatExclude.xml`**. That list is for artifacts that must never be harvested, not for preserving file-backed patch component identity. ## Constraints diff --git a/scripts/Installer/PatchComponentLedger.Tests.ps1 b/scripts/Installer/PatchComponentLedger.Tests.ps1 index 47c6215f97..516827f6ce 100644 --- a/scripts/Installer/PatchComponentLedger.Tests.ps1 +++ b/scripts/Installer/PatchComponentLedger.Tests.ps1 @@ -75,6 +75,21 @@ try { Feature = 'Complete' } } + Assert-Equal (Get-MsiDirectoryName -DefaultDir 'short|Long Name:SourceDir') 'Long Name' 'Directory parsing must use the target long name before the source portion.' + Assert-Equal (Get-MsiDirectoryName -DefaultDir 'TargetDir:SourceDir') 'TargetDir' 'Directory parsing must retain a target name without a short name.' + Assert-Equal (Get-MsiDirectoryName -DefaultDir '.') '' 'Dot directories must be transparent.' + $directories = @{ + APPFOLDER = [pscustomobject]@{ Name = 'App'; Parent = '' } + Help = [pscustomobject]@{ Name = 'Help'; Parent = 'APPFOLDER' } + Nested = [pscustomobject]@{ Name = 'Nested'; Parent = 'Help' } + Dot = [pscustomobject]@{ Name = ''; Parent = 'Help' } + Outside = [pscustomobject]@{ Name = 'Outside'; Parent = '' } + } + Assert-Equal (Get-RelativeMsiFilePath -Directories $directories -DirectoryId 'APPFOLDER' -FileName 'Root.dll') 'Root.dll' 'Files directly under APPFOLDER must keep their basename.' + Assert-Equal (Get-RelativeMsiFilePath -Directories $directories -DirectoryId 'Nested' -FileName 'Nested.dll') 'Help/Nested/Nested.dll' 'Nested files must keep their path below APPFOLDER.' + Assert-Equal (Get-RelativeMsiFilePath -Directories $directories -DirectoryId 'Dot' -FileName 'Dot.dll') 'Help/Dot.dll' 'Dot directories must continue to their parent.' + Assert-Equal (Get-RelativeMsiFilePath -Directories $directories -DirectoryId 'Outside' -FileName 'Outside.dll') $null 'Files outside APPFOLDER must be excluded.' + $update = @{ '{33333333-3333-3333-3333-333333333333}' = $master['{33333333-3333-3333-3333-333333333333}'] '{44444444-4444-4444-4444-444444444444}' = [pscustomobject]@{ diff --git a/scripts/Installer/PatchComponentLedger.psm1 b/scripts/Installer/PatchComponentLedger.psm1 index f6e35ca24c..f6a9baf5b9 100644 --- a/scripts/Installer/PatchComponentLedger.psm1 +++ b/scripts/Installer/PatchComponentLedger.psm1 @@ -1,6 +1,7 @@ Set-StrictMode -Version Latest -# Patch component identities from the base and immediate previous patch must remain present. +# File-backed components under MSI APPFOLDER must remain present across the base +# and immediate previous patch. $script:UpdateBucket = 'https://flex-updates.s3.amazonaws.com' $script:PatchPrefix = 'jobs/FieldWorks-Win-all-Release-Patch/' @@ -29,11 +30,52 @@ function Invoke-MsiQuery { return , $rows } +function Get-MsiDirectoryName { + <# + .SYNOPSIS + Extracts the target directory name from an MSI DefaultDir value. + #> + param( + [Parameter(Mandatory = $true)][AllowEmptyString()][string]$DefaultDir + ) + $target = ($DefaultDir -split ':', 2)[0] + if ([string]::IsNullOrEmpty($target) -or $target -eq '.') { return '' } + return ($target -split '\|', 2)[-1] +} + +function Get-RelativeMsiFilePath { + <# + .SYNOPSIS + Resolves a file path relative to the MSI APPFOLDER directory. + #> + param( + [Parameter(Mandatory = $true)][hashtable]$Directories, + [Parameter(Mandatory = $true)][string]$DirectoryId, + [Parameter(Mandatory = $true)][AllowEmptyString()][string]$FileName + ) + if ([string]::IsNullOrWhiteSpace($FileName)) { return $null } + if ($DirectoryId -ieq 'APPFOLDER') { return $FileName } + + $segments = New-Object System.Collections.Generic.List[string] + $visited = @{} + $current = $DirectoryId + while ($current -and $current -ine 'APPFOLDER') { + if ($visited.ContainsKey($current) -or -not $Directories.ContainsKey($current)) { return $null } + $visited[$current] = $true + $directory = $Directories[$current] + if (-not [string]::IsNullOrWhiteSpace($directory.Name)) { $segments.Insert(0, $directory.Name) } + $current = $directory.Parent + } + if ($current -ine 'APPFOLDER') { return $null } + $segments.Add($FileName) + return ($segments -join '/') +} + function Get-MsiComponents { <# .SYNOPSIS - Returns the components of an MSI keyed by ComponentId, each with its key-path file name and - feature. + Returns file-backed components under MSI APPFOLDER keyed by ComponentId, each + with their relative path and feature. #> param([Parameter(Mandatory = $true)][string]$MsiPath) @@ -41,6 +83,12 @@ function Get-MsiComponents { # 0 = msiOpenDatabaseModeReadOnly $db = $installer.GetType().InvokeMember('OpenDatabase', 'InvokeMethod', $null, $installer, @((Resolve-Path -LiteralPath $MsiPath).Path, 0)) + $directories = @{} + foreach ($row in (Invoke-MsiQuery -Database $db -Sql 'SELECT `Directory`,`Directory_Parent`,`DefaultDir` FROM `Directory`' -Columns 3)) { + $name = Get-MsiDirectoryName -DefaultDir ([string]$row[2]) + $directories[$row[0]] = [pscustomobject]@{ Name = $name; Parent = $row[1] } + } + $fileNames = @{} foreach ($row in (Invoke-MsiQuery -Database $db -Sql 'SELECT `File`,`FileName` FROM `File`' -Columns 2)) { # FileName is "SHORT|Long name" when a short name exists. @@ -51,10 +99,10 @@ function Get-MsiComponents { $features[$row[1]] = $row[0] } $components = @{} - foreach ($row in (Invoke-MsiQuery -Database $db -Sql 'SELECT `Component`,`ComponentId`,`KeyPath` FROM `Component`' -Columns 3)) { - if ([string]::IsNullOrEmpty($row[1])) { continue } - $file = '' - if ($fileNames.ContainsKey($row[2])) { $file = $fileNames[$row[2]] } + foreach ($row in (Invoke-MsiQuery -Database $db -Sql 'SELECT `Component`,`ComponentId`,`Directory_`,`KeyPath` FROM `Component`' -Columns 4)) { + if ([string]::IsNullOrEmpty($row[1]) -or [string]::IsNullOrEmpty($row[2]) -or -not $fileNames.ContainsKey($row[3])) { continue } + $file = Get-RelativeMsiFilePath -Directories $directories -DirectoryId $row[2] -FileName $fileNames[$row[3]] + if ($null -eq $file) { continue } $feature = '' if ($features.ContainsKey($row[0])) { $feature = $features[$row[0]] } $components[$row[1].ToUpperInvariant()] = [pscustomobject]@{ @@ -71,7 +119,7 @@ function Get-MsiComponents { function Read-ComponentLedger { <# .SYNOPSIS - Reads one ledger into a table keyed by ComponentId. + Reads a ledger of file-backed components under MSI APPFOLDER keyed by ComponentId. #> param([Parameter(Mandatory = $true)][AllowEmptyCollection()][string[]]$Path) @@ -104,6 +152,10 @@ function Read-ComponentLedger { } function Write-ComponentLedger { + <# + .SYNOPSIS + Writes file-backed components under MSI APPFOLDER to a tab-separated ledger. + #> param( [Parameter(Mandatory = $true)][string]$Path, [Parameter(Mandatory = $true)][object[]]$Entries, @@ -156,6 +208,10 @@ function Get-PublishedPatchKeys { } function Save-PublishedFile { + <# + .SYNOPSIS + Downloads one published file to a local directory. + #> param( [Parameter(Mandatory = $true)][string]$Key, [Parameter(Mandatory = $true)][string]$Directory @@ -166,12 +222,20 @@ function Save-PublishedFile { } function Get-PatchVersionFromKey { + <# + .SYNOPSIS + Parses a patch version from a published object key. + #> param([Parameter(Mandatory = $true)][string]$Key) # FieldWorks__b_. return [version]((Split-Path $Key -Leaf) -split '_')[1] } function Select-PreviousPublishedPatch { + <# + .SYNOPSIS + Selects the previous patch and matching ledger for a base and version. + #> param( [Parameter(Mandatory = $true)][string[]]$PatchKeys, [string[]]$LedgerKeys = @(), @@ -224,6 +288,10 @@ function Select-PreviousPublishedPatch { } function Format-RemovedComponentRemediation { + <# + .SYNOPSIS + Formats remediation for file-backed components under MSI APPFOLDER missing from a patch. + #> param([Parameter(Mandatory = $true)][object[]]$Dropped) $lines = New-Object System.Collections.Generic.List[string] $lines.Add('Remediation:') @@ -237,6 +305,10 @@ function Format-RemovedComponentRemediation { } function Get-UpdateMinusBaseLedgerEntries { + <# + .SYNOPSIS + Returns update file-backed components under MSI APPFOLDER absent from the base MSI. + #> param( [Parameter(Mandatory = $true)][hashtable]$Master, [Parameter(Mandatory = $true)][hashtable]$Update @@ -256,6 +328,11 @@ function Get-UpdateMinusBaseLedgerEntries { } function Get-MissingComponents { + <# + .SYNOPSIS + Finds required file-backed components under MSI APPFOLDER absent from the + available set. + #> param( [Parameter(Mandatory = $true)][hashtable]$Required, [Parameter(Mandatory = $true)][hashtable]$Available @@ -268,6 +345,11 @@ function Get-MissingComponents { } function Format-DroppedComponentMessage { + <# + .SYNOPSIS + Formats a diagnostic for file-backed components under MSI APPFOLDER + dropped by a patch. + #> param( [Parameter(Mandatory = $true)][string]$PatchVersion, [Parameter(Mandatory = $true)][string]$BaseBuildNumber, @@ -275,7 +357,7 @@ function Format-DroppedComponentMessage { ) $lines = New-Object System.Collections.Generic.List[string] foreach ($entry in $Dropped) { - $lines.Add("Patch $PatchVersion drops component $($entry.ComponentId)") + $lines.Add("Patch $PatchVersion drops file-backed component $($entry.ComponentId)") $lines.Add(" file: $($entry.File) (feature $($entry.Feature))") $lines.Add(" base: $BaseBuildNumber") } @@ -283,4 +365,4 @@ function Format-DroppedComponentMessage { return ($lines -join [Environment]::NewLine) } -Export-ModuleMember -Function Get-MsiComponents, Read-ComponentLedger, Write-ComponentLedger, Get-UpdateBucketKeys, Get-PublishedPatchKeys, Save-PublishedFile, Get-PatchVersionFromKey, Select-PreviousPublishedPatch, Get-UpdateMinusBaseLedgerEntries, Get-MissingComponents, Format-RemovedComponentRemediation, Format-DroppedComponentMessage +Export-ModuleMember -Function Get-MsiDirectoryName, Get-RelativeMsiFilePath, Get-MsiComponents, Read-ComponentLedger, Write-ComponentLedger, Get-UpdateBucketKeys, Get-PublishedPatchKeys, Save-PublishedFile, Get-PatchVersionFromKey, Select-PreviousPublishedPatch, Get-UpdateMinusBaseLedgerEntries, Get-MissingComponents, Format-RemovedComponentRemediation, Format-DroppedComponentMessage diff --git a/scripts/Installer/Test-PatchComponentLedger.ps1 b/scripts/Installer/Test-PatchComponentLedger.ps1 index 4a408706b3..598ad1c86f 100644 --- a/scripts/Installer/Test-PatchComponentLedger.ps1 +++ b/scripts/Installer/Test-PatchComponentLedger.ps1 @@ -1,10 +1,12 @@ <# .SYNOPSIS -Fails a patch build when the new Update MSI drops a component from the base or previous patch. +Fails a patch build when the new Update MSI drops a file-backed component under MSI APPFOLDER +from the base or previous patch. .DESCRIPTION -Compares the new Update MSI with the base MSI and the complete ledger from the immediately -previous published patch. Writes the complete update-minus-base ledger for this patch. +Compares the new Update MSI with file-backed components under MSI APPFOLDER from the base +MSI and the complete file-backed ledger from the immediately previous published patch. +Writes the complete update-minus-base file-backed ledger for this patch. .PARAMETER MasterMsi The base (Master) MSI rebuilt by the patch build. @@ -16,7 +18,7 @@ The upgraded (Update) MSI the patch was diffed from. The new patch's product version, e.g. 9.3.12.2761. .PARAMETER OutLedger -Where to write this patch's ledger. +Where to write this patch's file-backed component ledger. #> [CmdletBinding()] @@ -58,18 +60,18 @@ foreach ($entry in $previousLedger.Values) { } $dropped = @(Get-MissingComponents -Required $required -Available $update) $newLedgerEntries = @(Get-UpdateMinusBaseLedgerEntries -Master $master -Update $update) -Write-ComponentLedger -Path $OutLedger -Entries $newLedgerEntries -Heading "Complete update-minus-base ledger for patch $PatchVersion on base $BaseBuildNumber" -Write-Output "Patch $PatchVersion ledger contains $($newLedgerEntries.Count) update-minus-base components; ledger written to $OutLedger" -Write-Output "Checking against $($required.Count) base and previous-patch components" +Write-ComponentLedger -Path $OutLedger -Entries $newLedgerEntries -Heading "Complete update-minus-base file-backed ledger under MSI APPFOLDER for patch $PatchVersion on base $BaseBuildNumber" +Write-Output "Patch $PatchVersion ledger contains $($newLedgerEntries.Count) update-minus-base file-backed components under MSI APPFOLDER; ledger written to $OutLedger" +Write-Output "Checking against $($required.Count) required file-backed components under MSI APPFOLDER" if ($dropped.Count -eq 0) { - Write-Output '[OK] The patch keeps every base and immediately previous-patch component.' + Write-Output '[OK] The patch keeps every required file-backed component under MSI APPFOLDER.' exit 0 } $message = Format-DroppedComponentMessage -PatchVersion $PatchVersion -BaseBuildNumber $BaseBuildNumber -Dropped $dropped Write-Output $message if ($env:GITHUB_ACTIONS -eq 'true') { - Write-Output ('::error title=Patch component removal::' + ($message -replace "`r?`n", ' ')) + Write-Output ('::error title=Patch file-backed component removal::' + ($message -replace "`r?`n", ' ')) } exit 1 From 09d288617c88eac43c183f4b6df76b9e9953110d Mon Sep 17 00:00:00 2001 From: John Lambert Date: Tue, 22 Sep 2026 07:33:56 -0400 Subject: [PATCH 5/6] LT-22801: Address patch ledger review Keep scheduled base verification builds useful while blocking base releases that still contain patch stand-ins. Preserve the generated Master and Update MSIs for the component ledger check, and clarify ledger selection and cleanup guidance. --- .github/workflows/base-installer-cd.yml | 2 +- .github/workflows/patch-installer-cd.yml | 1 + Build/Installer.legacy.targets | 16 ++++++++-------- Docs/workflows/patch-component-removal.md | 10 +++++----- scripts/Installer/PatchComponentLedger.Tests.ps1 | 16 +++++++++------- scripts/Installer/PatchComponentLedger.psm1 | 4 ++-- 6 files changed, 26 insertions(+), 23 deletions(-) diff --git a/.github/workflows/base-installer-cd.yml b/.github/workflows/base-installer-cd.yml index d01a45034d..0d781c258d 100644 --- a/.github/workflows/base-installer-cd.yml +++ b/.github/workflows/base-installer-cd.yml @@ -161,7 +161,7 @@ jobs: id: build shell: powershell run: | - .\build.ps1 -BuildInstaller -Configuration Release -Verbosity detailed -BuildTests -MsBuildArgs @("/bl") *>&1 | Tee-Object -FilePath build.log + .\build.ps1 -BuildInstaller -Configuration Release -Verbosity detailed -BuildTests -MsBuildArgs @("/bl", "/p:FailOnRemovedSinceLastBase=${{ inputs.make_release == 'true' }}") *>&1 | Tee-Object -FilePath build.log - name: Run tests shell: powershell diff --git a/.github/workflows/patch-installer-cd.yml b/.github/workflows/patch-installer-cd.yml index 32e0917cd8..49e97d183c 100644 --- a/.github/workflows/patch-installer-cd.yml +++ b/.github/workflows/patch-installer-cd.yml @@ -61,6 +61,7 @@ jobs: CROWDIN_API_KEY: ${{ secrets.FLEX_CROWDIN_API }} LcmRootDir: ${{ github.workspace }}/Localizations/LCMRepo FILESTOSIGNLATER: ./signExternally + KEEP_INTERMEDIATE_MSI: true GH_TOKEN: ${{ github.token }} BASE_BUILD_NUMBER: ${{ inputs.base_build_number || '1452' }} name: Upload strings for l10n, Build, and run Tests diff --git a/Build/Installer.legacy.targets b/Build/Installer.legacy.targets index 2ba8e8b436..892f9a4a71 100644 --- a/Build/Installer.legacy.targets +++ b/Build/Installer.legacy.targets @@ -105,8 +105,8 @@ /> @@ -114,10 +114,6 @@ - b$(BASE_BUILD_NUMBER)_ + ' 'The remediation must provide an actionable stand-in example.' $targetText = Get-Content -Raw (Join-Path $PSScriptRoot '..\..\Build\Installer.legacy.targets') - Assert-True ($targetText.Contains(' Date: Tue, 22 Sep 2026 20:58:30 -0400 Subject: [PATCH 6/6] LT-22801: Address second patch ledger review Rename Read-ComponentLedger -Path to -LedgerFiles, since it takes several files, and rename the artifact check script to Check-PatchComponentLedger.ps1 so it is not mistaken for a unit test. Make the missing-ledger branch of Select-PreviousPublishedPatch explicit, and make its error name the recovery paths that actually exist: a new base, or a ledger rebuilt by applying the published patch to its base MSI. Add ledger entries directly instead of copying them field by field. Drop the zero-byte output from the base-release warning and error; CI builds start clean, and the docs keep the note for dirty local builds. Ask for a cleanup issue only when none exists for the current base, and remove the base 1452 note from the installer AGENTS guidance. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/patch-installer-cd.yml | 2 +- Build/Installer.legacy.targets | 4 +- Docs/workflows/patch-component-removal.md | 12 +++--- FLExInstaller/AGENTS.md | 4 +- ...ger.ps1 => Check-PatchComponentLedger.ps1} | 2 +- .../Installer/PatchComponentLedger.Tests.ps1 | 10 ++--- scripts/Installer/PatchComponentLedger.psm1 | 41 ++++++++----------- 7 files changed, 32 insertions(+), 43 deletions(-) rename scripts/Installer/{Test-PatchComponentLedger.ps1 => Check-PatchComponentLedger.ps1} (97%) diff --git a/.github/workflows/patch-installer-cd.yml b/.github/workflows/patch-installer-cd.yml index 49e97d183c..b72d7f327e 100644 --- a/.github/workflows/patch-installer-cd.yml +++ b/.github/workflows/patch-installer-cd.yml @@ -268,7 +268,7 @@ jobs: - name: Check patch keeps base and previous-patch components shell: pwsh run: | - .\scripts\Installer\Test-PatchComponentLedger.ps1 ` + .\scripts\Installer\Check-PatchComponentLedger.ps1 ` -MasterMsi "${{ steps.find_patch.outputs.master_msi }}" ` -UpdateMsi "${{ steps.find_patch.outputs.update_msi }}" ` -BaseBuildNumber $env:BASE_BUILD_NUMBER ` diff --git a/Build/Installer.legacy.targets b/Build/Installer.legacy.targets index 892f9a4a71..12a45c4bdd 100644 --- a/Build/Installer.legacy.targets +++ b/Build/Installer.legacy.targets @@ -669,11 +669,11 @@ -Create an issue to remove the placeholder before the next base build. +Create an issue to remove the placeholder before the next base build, unless one already exists for the current base. ``` The `RemovedSinceLastBase` entry makes the build write a zero-byte stand-in at that path. @@ -44,11 +44,9 @@ working while the removal issue is completed. ## Before publishing a base A scheduled base verification build warns while any `RemovedSinceLastBase` entries remain. -A base release build fails. Both messages list the stand-in paths and require two cleanup -actions: - -1. Remove each `RemovedSinceLastBase` entry from `Build/Installer.legacy.targets`. -2. Remove each corresponding zero-byte file from the build output. +A base release build fails. Both messages list the stand-in paths. Remove each +`RemovedSinceLastBase` entry from `Build/Installer.legacy.targets`. CI builds start clean; +a dirty local build must also delete each corresponding zero-byte file from the build output. Complete the removal issue before creating the base. A new base establishes the file-backed component set under MSI APPFOLDER that future patches must preserve. @@ -56,7 +54,7 @@ set under MSI APPFOLDER that future patches must preserve. ## Running the check locally ```powershell -.\scripts\Installer\Test-PatchComponentLedger.ps1 -MasterMsi -UpdateMsi ` +.\scripts\Installer\Check-PatchComponentLedger.ps1 -MasterMsi -UpdateMsi ` -BaseBuildNumber 1452 -PatchVersion 9.3.12.2761 ` -OutLedger out.tsv ``` diff --git a/FLExInstaller/AGENTS.md b/FLExInstaller/AGENTS.md index bbbdbc664c..331add5fa3 100644 --- a/FLExInstaller/AGENTS.md +++ b/FLExInstaller/AGENTS.md @@ -15,8 +15,8 @@ Minimal installer guidance for agents. - Newtonsoft.Json and similar authored components live in **`CustomComponents.wxi`** (overlays **`PatchableInstaller/Common`**), with definitions guarded by **``** so patch/update authoring omits them when only **`UPDATEBUILDDIR`** is set. Add matching **`ComponentRef`** entries in **`FLExInstaller/CustomFeatures.wxi`** inside the **same** **`...`** so patch builds do not emit dangling refs (**LGHT0094**). WiX 6 **`Framework.wxs`** uses the same pattern for **`Feature Complete`**. Do not use **`FeatureRef Id="Complete"`** from an include that appears before **`Framework.wxs`** defines `Complete` (Light **LGHT0095**). - **Patch file-backed component removal:** the patch ledger check compares file-backed components under MSI APPFOLDER in the new **Update** MSI and **Master** MSI with the complete file-backed ledger from the immediately previous published MSP. The first published patch creates the initial ledger in S3; after a ledger-bearing patch exists, its matching ledger must be beside the immediately previous MSP. Ledgers are not stored in this repository. Later patch versions are ignored. - A missing file-backed component from either source uses the same fix: add its output path, preserving the file's relative output path, to the **`RemovedSinceLastBase`** item list in the **`RescuePatching`** target of **`Build/Installer.legacy.targets`**. The target writes a zero-byte stand-in into **`$(dir-outputBase)`** so the file-backed component remains in the patch. - - Create an issue to remove the stand-in before the next base. Keep **`Avalonia.Themes.Fluent.dll`** in the list for base 1452. - - A base build fails while any **`RemovedSinceLastBase`** entries remain. Remove each entry and its corresponding zero-byte file before creating the base. + - Create an issue to remove the stand-in before the next base, unless one already exists for the current base. + - While any **`RemovedSinceLastBase`** entries remain, a scheduled base verification build warns and a base release build fails. Remove each entry before creating the base; a dirty local build must also delete the zero-byte file. - **Do not** add a dropped file-backed component to **`PatchableInstallerHeatExclude.xml`**. That list is for artifacts that must never be harvested, not for preserving file-backed patch component identity. ## Constraints diff --git a/scripts/Installer/Test-PatchComponentLedger.ps1 b/scripts/Installer/Check-PatchComponentLedger.ps1 similarity index 97% rename from scripts/Installer/Test-PatchComponentLedger.ps1 rename to scripts/Installer/Check-PatchComponentLedger.ps1 index 598ad1c86f..4e1c9bf4d3 100644 --- a/scripts/Installer/Test-PatchComponentLedger.ps1 +++ b/scripts/Installer/Check-PatchComponentLedger.ps1 @@ -52,7 +52,7 @@ if ($previous.LedgerKey) { $ledgerFiles.Add((Save-PublishedFile -Key $previous.LedgerKey -Directory $downloads)) } -$previousLedger = Read-ComponentLedger -Path $ledgerFiles.ToArray() +$previousLedger = Read-ComponentLedger -LedgerFiles $ledgerFiles.ToArray() $required = @{} foreach ($entry in $master.Values) { $required[$entry.ComponentId] = $entry } foreach ($entry in $previousLedger.Values) { diff --git a/scripts/Installer/PatchComponentLedger.Tests.ps1 b/scripts/Installer/PatchComponentLedger.Tests.ps1 index f2c579eeb2..0a119fcfd1 100644 --- a/scripts/Installer/PatchComponentLedger.Tests.ps1 +++ b/scripts/Installer/PatchComponentLedger.Tests.ps1 @@ -111,7 +111,7 @@ try { Write-ComponentLedger -Path $ledgerPath -Entries $newLedgerEntries -Heading 'test' $writtenLines = Get-Content -LiteralPath $ledgerPath Assert-Equal $writtenLines[1] ('# ComponentId' + [char]9 + 'Component' + [char]9 + 'File' + [char]9 + 'Feature') 'Ledgers must contain only the four component fields.' - $roundTrip = Read-ComponentLedger -Path $ledgerPath + $roundTrip = Read-ComponentLedger -LedgerFiles $ledgerPath Assert-Equal (($roundTrip.Values | Sort-Object File | ForEach-Object File) -join ',') 'New.dll,Other.dll' 'The complete generated ledger must be readable.' $previous = [pscustomobject]@{ @@ -135,16 +135,16 @@ try { Assert-True ($targetText.Contains(' - param([Parameter(Mandatory = $true)][AllowEmptyCollection()][string[]]$Path) + param([Parameter(Mandatory = $true)][AllowEmptyCollection()][string[]]$LedgerFiles) $ledger = @{} - foreach ($file in $Path) { + foreach ($file in $LedgerFiles) { if (-not (Test-Path -LiteralPath $file -PathType Leaf)) { throw "Component ledger file not found: $file" } @@ -264,26 +264,23 @@ function Select-PreviousPublishedPatch { $expectedLedgerKey = $previous.Key -replace '\.msp$', '_components.tsv' $matchingLedger = @($LedgerKeys | Where-Object { $_ -eq $expectedLedgerKey }) if ($matchingLedger.Count -gt 0) { - return [pscustomobject]@{ - PatchKey = $previous.Key - LedgerKey = $matchingLedger[0] - } + $ledgerKey = $matchingLedger[0] } - $earlierLedgerPair = $false - foreach ($candidate in $orderedPatches) { - if ($candidate.Version -ge $previous.Version) { continue } - $candidateLedger = $candidate.Key -replace '\.msp$', '_components.tsv' - if (@($LedgerKeys | Where-Object { $_ -eq $candidateLedger }).Count -gt 0) { - $earlierLedgerPair = $true - break + else { + # No ledger beside the previous patch is expected only before the first ledger-bearing + # patch. + foreach ($candidate in $orderedPatches) { + if ($candidate.Version -ge $previous.Version) { continue } + $candidateLedger = $candidate.Key -replace '\.msp$', '_components.tsv' + if (@($LedgerKeys | Where-Object { $_ -eq $candidateLedger }).Count -gt 0) { + throw "Bootstrap has ended: published patch $($previous.Key) has no matching ledger $expectedLedgerKey. Build a new base, or reconstruct that ledger by applying the published patch to its base MSI and publish it beside the patch, before building another patch." + } } - } - if ($earlierLedgerPair) { - throw "Bootstrap has ended: published patch $($previous.Key) has no matching ledger $expectedLedgerKey. Publish the ledger beside that patch before building another patch." + $ledgerKey = $null } return [pscustomobject]@{ PatchKey = $previous.Key - LedgerKey = $null + LedgerKey = $ledgerKey } } @@ -300,7 +297,7 @@ function Format-RemovedComponentRemediation { $path = '$(dir-outputBase)/' + $entry.File $lines.Add((' ')) } - $lines.Add('Create an issue to remove the placeholder before the next base build.') + $lines.Add('Create an issue to remove the placeholder before the next base build, unless one already exists for the current base.') return ($lines -join [Environment]::NewLine) } @@ -316,13 +313,7 @@ function Get-UpdateMinusBaseLedgerEntries { $entries = New-Object System.Collections.Generic.List[object] foreach ($id in $Update.Keys) { if ($Master.ContainsKey($id)) { continue } - $entry = $Update[$id] - $entries.Add([pscustomobject]@{ - ComponentId = $entry.ComponentId - Component = $entry.Component - File = $entry.File - Feature = $entry.Feature - }) + $entries.Add($Update[$id]) } return $entries.ToArray() }