How to add a WiX custom action that happens only on uninstall (via MSI)?
Asked Answered
P

8

184

I would like to modify an MSI installer (created through WiX) to delete an entire directory on uninstall.

I understand the RemoveFile and RemoveFolder options in WiX, but these are not robust enough to recursively delete an entire folder that has content created after the installation.

I noticed the similar Stack Overflow question Removing files when uninstalling WiX, but I was wondering if this could be done more simply using a call to a batch script to delete the folder.

This is my first time using WiX, and I'm still getting the hang of custom actions. What would be a basic example of a custom action that will run a batch script on uninstall?

Proust answered 26/11, 2008 at 14:41 Comment(1)
For a complete guide see resources.flexera.com/web/pdf/archive/…Vaticinate
L
207

EDIT: Perhaps look at the answer currently immediately below.


This topic has been a headache for long time. I finally figured it out. There are some solutions online, but none of them really works. And of course there is no documentation. So in the chart below there are several properties that are suggested to use and the values they have for various installation scenarios:

alt text

So in my case I wanted a CA that will run only on uninstalls - not upgrades, not repairs or modifies. According to the table above I had to use

<Custom Action='CA_ID' Before='other_CA_ID'>
        (NOT UPGRADINGPRODUCTCODE) AND (REMOVE="ALL")</Custom>

And it worked!

Lekishalela answered 8/4, 2009 at 20:34 Comment(9)
Are the values in that chart correct? Why would you need to add REMOVE="ALL"? NOT UPGRADINGPRODUCTCODE is only true for an uninstall (according to the chart), so (NOT UPGRADINGPRODUCTCODE) AND (REMOVE="ALL") would also only be true on an uninstall. The REMOVE="ALL" seems unnecessary.Tropology
I agree with @ToddRopog - The example and the truth table don't seem to agree. Is that really correct?Bibliopegy
The truth table is slightly wrong. NOT UPGRADINGPRODUCTCODE is true for a first install as wellStuckup
What's the difference between REMOVE and REMOVE="ALL" seems that just testing REMOVE would be sufficient for running code on an uninstall, wouldn't it?Aalst
Common conditions: alekdavis.blogspot.ru/2013/05/…Worse
Please confirm: Installed and INSTALLED are different things, only Installed is set by Windows Installer. I do not think that INSTALLED works.Tadio
The usage of parentheses (e.g., ( and )) seems required for this to work; just a warning to other users that also thought they didn't need a serving of copy-pasta :)Habanera
1. Some table values are wrong, for example "Not upgradingproductcode/Install". 2. INSTALLED does not exist, the property is called "Installed" (case sensitive). 3. The claim that there is no documentation is wrong: relevant WiX at firegiant.com/wix/tutorial/com-expression-syntax-miscellanea/…, relevant MSI at msdn.microsoft.com/en-us/library/windows/desktop/…Tadio
there is also difference between 'Remove' (Remove button in MSI) and 'Uninstall' (using Windows -> Settings -> Apps -> Uninstall)Laryngeal
B
171

There are multiple problems with yaluna's answer, also property names are case sensitive, Installed is the correct spelling (INSTALLED will not work). The table above should've been this:

enter image description here

Also assuming a full repair & uninstall the actual values of properties could be:

enter image description here

The WiX Expression Syntax documentation says:

In these expressions, you can use property names (remember that they are case sensitive).

The properties are documented at the Windows Installer Guide (e.g. Installed)

EDIT: Small correction to the first table; evidently "Uninstall" can also happen with just REMOVE being True.

Bi answered 12/7, 2013 at 5:36 Comment(10)
REMOVE also appears to be set for ChangeOvertrick
The 'Upgrade' column, is that during the uninstall sequence of the old version or the install sequence of the new version?Joust
@NickWhaley: I haven't looked into it for a while but I believe that the "Upgrade" option is only when installing a version greater than the one already installed.Bi
@ahmd0, well of course. But there is a nested install that occurs within RemoveExistingProducts that has a totally different set of properties. That is what is in your 'Upgrade' column. The rest of the upgrade is identical to the 'Install' column.Joust
@NickWhaley: The REMOVE option will be true for Major Upgrades, ie 1.0.0 to 2.0.0, not 1.0.0 to 1.1.0, during execution of the previous version's uninstaller. To run a Custom Action during a Major Upgrade in the new versions install you'll need to reference the ActionProperty defined in your Upgrade MSI table for that version upgrade. symantec.com/connect/articles/msi-upgrade-overview msdn.microsoft.com/en-us/library/aa372379%28v=vs.85%29.aspxPeltz
Thanks for the table, I also found that REMOVE was true when I clicked "Repair" (has anyone else run into this?)Septemberseptembrist
@m1m1k: Can you provide more details when REMOVE was true for a repair?Mert
So to actually answer the question (which this answer doesn't directly do), does the condition for the custom action that will only run on a removal become: <Custom Action='CA_ID' Before='other_CA_ID'>(NOT UPGRADINGPRODUCTCODE) AND REMOVE AND Installed</Custom>Loud
@DeweyVozel The AND Installed part of your condition is redundant according to this answer's truth table.Tyrannosaur
@Septemberseptembrist I believe that's why you should check for (REMOVE = "ALL") instead of just REMOVE.Tyrannosaur
C
54

You can do this with a custom action. You can add a refrence to your custom action under <InstallExecuteSequence>:

<InstallExecuteSequence>
...
  <Custom Action="FileCleaner" After='InstallFinalize'>
          Installed AND NOT UPGRADINGPRODUCTCODE</Custom>

Then you will also have to define your Action under <Product>:

<Product> 
...
  <CustomAction Id='FileCleaner' BinaryKey='FileCleanerEXE' 
                ExeCommand='' Return='asyncNoWait'  />

Where FileCleanerEXE is a binary (in my case a little c++ program that does the custom action) which is also defined under <Product>:

<Product> 
...
  <Binary Id="FileCleanerEXE" SourceFile="path\to\fileCleaner.exe" />

The real trick to this is the Installed AND NOT UPGRADINGPRODUCTCODE condition on the Custom Action, with out that your action will get run on every upgrade (since an upgrade is really an uninstall then reinstall). Which if you are deleting files is probably not want you want during upgrading.

On a side note: I recommend going through the trouble of using something like C++ program to do the action, instead of a batch script because of the power and control it provides -- and you can prevent the "cmd prompt" window from flashing while your installer runs.

Chaddie answered 26/11, 2008 at 18:44 Comment(4)
25 upvotes but not an accepted answer. Welcome to the world of installers! :)Condescending
This does not really work. When you want to execute a fileCleaner.exe, that is installed in your own installation folder, this will be an chicken-and-egg problem: The CustomAction will be executed "After='InstallFinalize'". At this point, all files are removed from the Installation folder. Also the fileCleaner.exe. So you are not able to execute it via an CustomAction. This answer is simply wrong. I am wondering about the 42 upvotes!Sylas
@Sylas All of that can be easily prevented by using a DLL that isn't part of the installed files instead of an EXE.Tyrannosaur
The real problem with this answer is that Installed AND NOT UPGRADINGPRODUCTCODE will also evaluate to true during Change and Repair procedures, assuming that the table in @ahmd0's answer above is correct. Solution: Use (REMOVE = "ALL") AND NOT UPGRADINGPRODUCTCODETyrannosaur
H
42

The biggest problem with a batch script is handling rollback when the user clicks cancel (or something goes wrong during your install). The correct way to handle this scenario is to create a CustomAction that adds temporary rows to the RemoveFiles table. That way the Windows Installer handles the rollback cases for you. It is insanely simpler when you see the solution.

Anyway, to have an action only execute during uninstall add a Condition element with:

REMOVE ~= "ALL"

the ~= says compare case insensitive (even though I think ALL is always uppercaesd). See the MSI SDK documentation about Conditions Syntax for more information.

PS: There has never been a case where I sat down and thought, "Oh, batch file would be a good solution in an installation package." Actually, finding an installation package that has a batch file in it would only encourage me to return the product for a refund.

Hexylresorcinol answered 26/11, 2008 at 19:29 Comment(1)
I was about to use a batch script and then saw the PS section. Thanks for saving me : ) The Remove ~="ALL" worked for me.Confounded
F
18

Here's a set of properties i made that feel more intuitive to use than the built in stuff. The conditions are based off of the truth table supplied above by ahmd0.

<!-- truth table for installer varables (install vs uninstall vs repair vs upgrade) https://mcmap.net/q/104510/-how-to-add-a-wix-custom-action-that-happens-only-on-uninstall-via-msi -->
 <SetProperty Id="_INSTALL"   After="FindRelatedProducts" Value="1"><![CDATA[Installed="" AND PREVIOUSVERSIONSINSTALLED=""]]></SetProperty>
 <SetProperty Id="_UNINSTALL" After="FindRelatedProducts" Value="1"><![CDATA[PREVIOUSVERSIONSINSTALLED="" AND REMOVE="ALL"]]></SetProperty>
 <SetProperty Id="_CHANGE"    After="FindRelatedProducts" Value="1"><![CDATA[Installed<>"" AND REINSTALL="" AND PREVIOUSVERSIONSINSTALLED<>"" AND REMOVE=""]]></SetProperty>
 <SetProperty Id="_REPAIR"    After="FindRelatedProducts" Value="1"><![CDATA[REINSTALL<>""]]></SetProperty>
 <SetProperty Id="_UPGRADE"   After="FindRelatedProducts" Value="1"><![CDATA[PREVIOUSVERSIONSINSTALLED<>"" ]]></SetProperty>

Here's some sample usage:

  <Custom Action="CaptureExistingLocalSettingsValues" After="InstallInitialize">NOT _UNINSTALL</Custom>
  <Custom Action="GetConfigXmlToPersistFromCmdLineArgs" After="InstallInitialize">_INSTALL OR _UPGRADE</Custom>
  <Custom Action="ForgetProperties" Before="InstallFinalize">_UNINSTALL OR _UPGRADE</Custom>
  <Custom Action="SetInstallCustomConfigSettingsArgs" Before="InstallCustomConfigSettings">NOT _UNINSTALL</Custom>
  <Custom Action="InstallCustomConfigSettings" Before="InstallFinalize">NOT _UNINSTALL</Custom>

Issues:

Frustration answered 6/7, 2018 at 17:31 Comment(4)
This is a great solution. Remember to also consider PATCH and MSIPATCHREMOVE conditions.Tenement
In your truth table, did you mean to use PREVIOUSVERSIONSINSTALLED instead of UPGRADINGPRODUCTCODE as is used by ahmd0? I'm not seeing any reference to PREVIOUSVERSIONSINSTALLED on the MSI property reference page (learn.microsoft.com/en-us/windows/win32/msi/property-reference).Counterproductive
Several of the predicates for your properties don't take into account all the rows in ahmd0's table (Installed, REINSTALL, UPGRADINGPRODUCTCODE, and REMOVE). Would you please explain why?Counterproductive
@Counterproductive IIRC the conditions in ahmd0's table failed dev testing and so I had tweaked them to improve the accuracy of the new property labels. However, that was quite some time ago and so the details are lost without repeating the tests. I'd be curious to hear if anyone runs into scenarios where the properties I've defined do not operate as intended.Frustration
S
1

To execute an action only on Uninstall, neither on repair, nor upgrade nor install, you can use:

<Custom Action="ActionName" Before="InstallFinalize"><![CDATA[Installed AND NOT UPGRADINGPRODUCTCODE]]></Custom>
Spontoon answered 11/10, 2022 at 4:52 Comment(1)
Helpful. I think, the Action will also be called, when you unselect a Feature with ChangeCriseldacrisey
D
0

I used Custom Action separately coded in C++ DLL and used the DLL to call appropriate function on Uninstalling using this syntax :

<CustomAction Id="Uninstall" BinaryKey="Dll_Name" 
              DllEntry="Function_Name" Execute="deferred" />

Using the above code block, I was able to run any function defined in C++ DLL on uninstall. FYI, my uninstall function had code regarding Clearing current user data and registry entries.

Diena answered 25/4, 2014 at 12:51 Comment(0)
U
0

Because i had to struggle with the same issue, the version which works for me with Wix v5 is:

    <InstallExecuteSequence>
       <Custom Action="DeleteFolderAction" Before="RemoveFiles" Condition="REMOVE"/>
    </InstallExecuteSequence>
Unnecessarily answered 17/4 at 7:57 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.