aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorBob Arnson <bob@firegiant.com>2026-01-21 21:20:40 -0500
committerBob Arnson <github@bobs.org>2026-01-26 13:42:00 -0500
commit49924eb413af6f316951fcc1a7deaf28b7fd6e19 (patch)
tree1e88db96b839dbbac5a1f90b42bf178001656745 /src
parent8b9538acf39a353a34bcd0a624fd12155a818f55 (diff)
downloadwix-49924eb413af6f316951fcc1a7deaf28b7fd6e19.tar.gz
wix-49924eb413af6f316951fcc1a7deaf28b7fd6e19.tar.bz2
wix-49924eb413af6f316951fcc1a7deaf28b7fd6e19.zip
Allow patched package to be uninstalled.HEADmain
Normally, a patched package is detected as superseded and therefore a normal uninstall takes no action. This change looks for applied patches and allows a normal uninstall to remove the package. Fixes https://github.com/wixtoolset/issues/issues/6350
Diffstat (limited to 'src')
-rw-r--r--src/burn/engine/msiengine.cpp65
-rw-r--r--src/test/burn/TestData/SlipstreamTests/BundleAv1_0_1/BundleAv1_0_1.wixproj19
-rw-r--r--src/test/burn/TestData/SlipstreamTests/BundleAv1_0_1/BundleAv1_0_1.wxs12
-rw-r--r--src/test/burn/TestData/SlipstreamTests/PatchAMinor/PatchAMinor.wixproj12
-rw-r--r--src/test/burn/TestData/SlipstreamTests/PatchAMinor/PatchAMinor.wxs23
-rw-r--r--src/test/burn/WixToolsetTest.BurnE2E/SlipstreamTests.cs24
-rw-r--r--src/test/sandbox/TestSandbox.wsb5
-rw-r--r--src/test/sandbox/startup.bat8
8 files changed, 164 insertions, 4 deletions
diff --git a/src/burn/engine/msiengine.cpp b/src/burn/engine/msiengine.cpp
index 86fb0d9d..48ec0c81 100644
--- a/src/burn/engine/msiengine.cpp
+++ b/src/burn/engine/msiengine.cpp
@@ -50,7 +50,9 @@ static void RegisterSourceDirectory(
50 __in BURN_PACKAGE* pPackage, 50 __in BURN_PACKAGE* pPackage,
51 __in_z LPCWSTR wzCacheDirectory 51 __in_z LPCWSTR wzCacheDirectory
52 ); 52 );
53 53static BOOL PackageHasAppliedPatch(
54 __in BURN_PACKAGE* pPackage
55 );
54 56
55// function definitions 57// function definitions
56 58
@@ -904,7 +906,22 @@ extern "C" HRESULT MsiEnginePlanCalculatePackage(
904 else if ((BOOTSTRAPPER_REQUEST_STATE_ABSENT == pPackage->requested || BOOTSTRAPPER_REQUEST_STATE_CACHE == pPackage->requested) && 906 else if ((BOOTSTRAPPER_REQUEST_STATE_ABSENT == pPackage->requested || BOOTSTRAPPER_REQUEST_STATE_CACHE == pPackage->requested) &&
905 !pPackage->fPermanent) // removing a package that should be removed. 907 !pPackage->fPermanent) // removing a package that should be removed.
906 { 908 {
907 execute = BOOTSTRAPPER_PACKAGE_STATE_SUPERSEDED == pPackage->currentState ? BOOTSTRAPPER_ACTION_STATE_NONE : BOOTSTRAPPER_ACTION_STATE_UNINSTALL; 909 if (BOOTSTRAPPER_PACKAGE_STATE_SUPERSEDED == pPackage->currentState)
910 {
911 // If the package is superseded, check to see if there's a patch installed.
912 // A minor upgrade patch could be (usually is) the cause of the
913 // supersedence. In that case, we should ignore the supersedence that would
914 // normally prevent the uninstall. There is a gap in this logic: If a minor
915 // upgrade package were installed without a bundle, then a small update patch
916 // (which by definition doesn't change the version number) were installed,
917 // this check would allow the uninstall. If the minor upgrade were installed
918 // by a bundle, dependencies would keep the package installed.
919 execute = PackageHasAppliedPatch(pPackage) ? BOOTSTRAPPER_ACTION_STATE_UNINSTALL : BOOTSTRAPPER_ACTION_STATE_NONE;
920 }
921 else
922 {
923 execute = BOOTSTRAPPER_ACTION_STATE_UNINSTALL;
924 }
908 } 925 }
909 else if (BOOTSTRAPPER_REQUEST_STATE_FORCE_ABSENT == pPackage->requested) 926 else if (BOOTSTRAPPER_REQUEST_STATE_FORCE_ABSENT == pPackage->requested)
910 { 927 {
@@ -2265,6 +2282,7 @@ LExit:
2265 ReleaseStr(sczMspPath); 2282 ReleaseStr(sczMspPath);
2266 ReleaseStr(sczCachedDirectory); 2283 ReleaseStr(sczCachedDirectory);
2267 ReleaseStr(sczPatches); 2284 ReleaseStr(sczPatches);
2285
2268 return hr; 2286 return hr;
2269} 2287}
2270 2288
@@ -2289,6 +2307,47 @@ static void RegisterSourceDirectory(
2289 2307
2290LExit: 2308LExit:
2291 ReleaseStr(sczMsiDirectory); 2309 ReleaseStr(sczMsiDirectory);
2310}
2311
2312static BOOL PackageHasAppliedPatch(
2313 __in BURN_PACKAGE* pPackage
2314)
2315{
2316 HRESULT hr = S_OK;
2317 BOOL fPatched = FALSE;
2318 UINT er = ERROR_SUCCESS;
2319 DWORD iPatch = 0;
2320 WCHAR wzPatchCode[MAX_GUID_CHARS + 1] = {};
2321 WCHAR wzTransforms[MAX_PATH] = {};
2322 DWORD cchTransforms = countof(wzTransforms);
2323 WCHAR wzPatchState[2] = {};
2324 DWORD cchPatchState = countof(wzPatchState);
2325
2326 for (;;)
2327 {
2328 er = ::MsiEnumPatchesW(pPackage->Msi.sczProductCode, iPatch, wzPatchCode, wzTransforms, &cchTransforms);
2329
2330 if (ERROR_NO_MORE_ITEMS == er)
2331 {
2332 ExitFunction();
2333 }
2334 ExitOnWin32Error(er, hr, "Failed to enumerate patches for package %ls, product code %ls.", pPackage->sczId, pPackage->Msi.sczProductCode);
2335
2336 er = ::MsiGetPatchInfoExW(wzPatchCode, pPackage->Msi.sczProductCode, NULL, pPackage->fPerMachine ? MSIINSTALLCONTEXT_MACHINE : MSIINSTALLCONTEXT_USERUNMANAGED
2337 , INSTALLPROPERTY_PATCHSTATE, wzPatchState, &cchPatchState);
2338 ExitOnWin32Error(er, hr, "Failed to get patch info for patch %ls.", wzPatchCode);
2339
2340 if ('1' == wzPatchState[0])
2341 {
2342 fPatched = TRUE;
2343
2344 ExitFunction();
2345 }
2346
2347 ++iPatch;
2348 }
2349
2350LExit:
2351 return fPatched;
2292 2352
2293 return;
2294} 2353}
diff --git a/src/test/burn/TestData/SlipstreamTests/BundleAv1_0_1/BundleAv1_0_1.wixproj b/src/test/burn/TestData/SlipstreamTests/BundleAv1_0_1/BundleAv1_0_1.wixproj
new file mode 100644
index 00000000..ac9cf71e
--- /dev/null
+++ b/src/test/burn/TestData/SlipstreamTests/BundleAv1_0_1/BundleAv1_0_1.wixproj
@@ -0,0 +1,19 @@
1<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. -->
2<Project Sdk="WixToolset.Sdk">
3 <PropertyGroup>
4 <OutputType>Bundle</OutputType>
5 <UpgradeCode>{62C28DAF-A13E-4F55-ACA1-FB843630789C}</UpgradeCode>
6 </PropertyGroup>
7 <ItemGroup>
8 <Compile Include="..\..\Templates\Bundle.wxs" Link="Bundle.wxs" />
9 </ItemGroup>
10 <ItemGroup>
11 <ProjectReference Include="..\PackageAv1\PackageAv1.wixproj" />
12 <ProjectReference Include="..\PatchAMinor\PatchAMinor.wixproj" />
13 <ProjectReference Include="..\..\TestBA\TestBAWixlib\testbawixlib.wixproj" />
14 </ItemGroup>
15 <ItemGroup>
16 <PackageReference Include="WixToolset.BootstrapperApplications.wixext" />
17 <PackageReference Include="WixToolset.NetFx.wixext" />
18 </ItemGroup>
19</Project>
diff --git a/src/test/burn/TestData/SlipstreamTests/BundleAv1_0_1/BundleAv1_0_1.wxs b/src/test/burn/TestData/SlipstreamTests/BundleAv1_0_1/BundleAv1_0_1.wxs
new file mode 100644
index 00000000..a14383d3
--- /dev/null
+++ b/src/test/burn/TestData/SlipstreamTests/BundleAv1_0_1/BundleAv1_0_1.wxs
@@ -0,0 +1,12 @@
1<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. -->
2
3<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
4 <Fragment>
5 <PackageGroup Id="BundlePackages">
6 <MsiPackage Id="PackageA" SourceFile="$(var.PackageAv1.TargetPath)" Compressed="no">
7 <SlipstreamMsp Id="PatchAMinor" />
8 </MsiPackage>
9 <MspPackage Id="PatchAMinor" SourceFile="$(var.PatchAMinor.TargetPath)" Compressed="no" />
10 </PackageGroup>
11 </Fragment>
12</Wix>
diff --git a/src/test/burn/TestData/SlipstreamTests/PatchAMinor/PatchAMinor.wixproj b/src/test/burn/TestData/SlipstreamTests/PatchAMinor/PatchAMinor.wixproj
new file mode 100644
index 00000000..3deb2263
--- /dev/null
+++ b/src/test/burn/TestData/SlipstreamTests/PatchAMinor/PatchAMinor.wixproj
@@ -0,0 +1,12 @@
1<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. -->
2<Project Sdk="WixToolset.Sdk">
3 <PropertyGroup>
4 <OutputType>Patch</OutputType>
5 <TargetExt>.msp</TargetExt>
6 <SuppressSpecificWarnings>1079</SuppressSpecificWarnings>
7 </PropertyGroup>
8 <ItemGroup>
9 <ProjectReference Include="..\PackageAv1\PackageAv1.wixproj" />
10 <ProjectReference Include="..\PackageAv1_0_1\PackageAv1_0_1.wixproj" />
11 </ItemGroup>
12</Project>
diff --git a/src/test/burn/TestData/SlipstreamTests/PatchAMinor/PatchAMinor.wxs b/src/test/burn/TestData/SlipstreamTests/PatchAMinor/PatchAMinor.wxs
new file mode 100644
index 00000000..0f4abd2b
--- /dev/null
+++ b/src/test/burn/TestData/SlipstreamTests/PatchAMinor/PatchAMinor.wxs
@@ -0,0 +1,23 @@
1<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. -->
2
3<?ifndef TestVersion?>
4<?define TestVersion = 1.0.0.0?>
5<?endif?>
6
7<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
8 <Patch AllowRemoval="yes" Classification="Update" Description="Patch A minor upgrade in test $(var.TestGroupName)" DisplayName="$(var.TestGroupName) - Patch A minor upgrade" Manufacturer="Example Corporation" MinorUpdateTargetRTM="yes">
9 <Media Id="100" Cabinet="PatchA" EmbedCab="yes">
10 <PatchBaseline
11 Id="PatchA"
12 BaselineFile="$(var.PackageAv1.TargetDir)$(var.PackageAv1.TargetName).wixpdb"
13 UpdateFile="$(var.PackageAv1_0_1.TargetDir)$(var.PackageAv1_0_1.TargetName).wixpdb"
14 />
15 </Media>
16
17 <PatchFamily Id="A" Version="$(var.TestVersion)" Supersede="yes">
18 <ComponentRef Id="RegistryComponent" />
19 <PropertyRef Id="TestVersion" />
20 <PropertyRef Id="ProductVersion" />
21 </PatchFamily>
22 </Patch>
23</Wix>
diff --git a/src/test/burn/WixToolsetTest.BurnE2E/SlipstreamTests.cs b/src/test/burn/WixToolsetTest.BurnE2E/SlipstreamTests.cs
index d07e80f1..d4cf447d 100644
--- a/src/test/burn/WixToolsetTest.BurnE2E/SlipstreamTests.cs
+++ b/src/test/burn/WixToolsetTest.BurnE2E/SlipstreamTests.cs
@@ -18,7 +18,7 @@ namespace WixToolsetTest.BurnE2E
18 private const string V101 = "1.0.1.0"; 18 private const string V101 = "1.0.1.0";
19 19
20 [RuntimeFact] 20 [RuntimeFact]
21 public void CanInstallBundleWithSlipstreamedPatchThenRemoveIt() 21 public void CanInstallBundleWithSlipstreamedSmallUpdatePatchThenRemoveIt()
22 { 22 {
23 var testRegistryValue = "PackageA"; 23 var testRegistryValue = "PackageA";
24 24
@@ -39,6 +39,28 @@ namespace WixToolsetTest.BurnE2E
39 packageAv1.VerifyTestRegistryRootDeleted(); 39 packageAv1.VerifyTestRegistryRootDeleted();
40 } 40 }
41 41
42 [RuntimeFact]
43 public void CanInstallBundleWithSlipstreamedMinorUpgradePatchThenRemoveIt()
44 {
45 var testRegistryValue = "PackageA";
46
47 var packageAv1 = this.CreatePackageInstaller("PackageAv1");
48 var bundleA = this.CreateBundleInstaller("BundleAv1_0_1");
49
50 var packageAv1SourceCodeInstalled = packageAv1.GetInstalledFilePath("Package.wxs");
51 Assert.False(File.Exists(packageAv1SourceCodeInstalled), $"PackageAv1 payload should not be there on test start: {packageAv1SourceCodeInstalled}");
52
53 bundleA.Install();
54 bundleA.VerifyRegisteredAndInPackageCache();
55 Assert.True(File.Exists(packageAv1SourceCodeInstalled), String.Concat("Should have found PackageAv1 payload installed at: ", packageAv1SourceCodeInstalled));
56 packageAv1.VerifyTestRegistryValue(testRegistryValue, V101);
57
58 bundleA.Uninstall();
59 bundleA.VerifyUnregisteredAndRemovedFromPackageCache();
60 Assert.False(File.Exists(packageAv1SourceCodeInstalled), String.Concat("PackageAv1 payload should have been removed by uninstall from: ", packageAv1SourceCodeInstalled));
61 packageAv1.VerifyTestRegistryRootDeleted();
62 }
63
42 /// <summary> 64 /// <summary>
43 /// BundleA installs PackageA with slipstreamed PatchA. 65 /// BundleA installs PackageA with slipstreamed PatchA.
44 /// BundleOnlyPatchA is installed which contains PatchA (which should be a no-op). 66 /// BundleOnlyPatchA is installed which contains PatchA (which should be a no-op).
diff --git a/src/test/sandbox/TestSandbox.wsb b/src/test/sandbox/TestSandbox.wsb
index 01e11e6b..97c3c759 100644
--- a/src/test/sandbox/TestSandbox.wsb
+++ b/src/test/sandbox/TestSandbox.wsb
@@ -5,6 +5,11 @@
5 <SandboxFolder>C:\build</SandboxFolder> 5 <SandboxFolder>C:\build</SandboxFolder>
6 <ReadOnly>true</ReadOnly> 6 <ReadOnly>true</ReadOnly>
7 </MappedFolder> 7 </MappedFolder>
8 <MappedFolder>
9 <HostFolder>..\..\..\build\logs</HostFolder>
10 <SandboxFolder>C:\logs</SandboxFolder>
11 <ReadOnly>false</ReadOnly>
12 </MappedFolder>
8 <MappedFolder> 13 <MappedFolder>
9 <HostFolder>.\</HostFolder> 14 <HostFolder>.\</HostFolder>
10 <SandboxFolder>C:\sandbox</SandboxFolder> 15 <SandboxFolder>C:\sandbox</SandboxFolder>
diff --git a/src/test/sandbox/startup.bat b/src/test/sandbox/startup.bat
index 52f550ee..90f04f5f 100644
--- a/src/test/sandbox/startup.bat
+++ b/src/test/sandbox/startup.bat
@@ -1,5 +1,13 @@
1@setlocal 1@setlocal
2@echo off 2@echo off
3
4
5::// Fix Sandbox glacial perf (24H2-???)
6:://
7REG ADD HKLM\SYSTEM\CurrentControlSet\Control\CI\Policy /v VerifiedAndReputablePolicyState /t REG_DWORD /d 0 /f
8CITOOL -r -j
9
10
3SET DOTNET_VERSION=8.0 11SET DOTNET_VERSION=8.0
4SET SANDBOX_FILES=C:\sandbox 12SET SANDBOX_FILES=C:\sandbox
5 13