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?

Community
  • 1
  • 1
  • For a complete guide see https://resources.flexera.com/web/pdf/archive/IS-CHS-Common-MSI-Conditions.pdf – Bizhan Mar 15 '21 at 13:44

7 Answers7

205

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!

nelsonjchen
  • 365
  • 6
  • 16
  • 26
    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. – Todd Ropog May 14 '10 at 19:02
  • 3
    I agree with @ToddRopog - The example and the truth table don't seem to agree. Is that really correct? – Tim Long Apr 29 '11 at 19:33
  • 22
    The truth table is slightly wrong. NOT UPGRADINGPRODUCTCODE is true for a first install as well – Neil Nov 24 '11 at 15:56
  • 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? – BrainSlugs83 Jan 18 '13 at 04:03
  • 4
    Common conditions: http://alekdavis.blogspot.ru/2013/05/wix-woes-what-is-your-installer-doing.html – KindDragon Aug 11 '14 at 19:14
  • 1
    Please confirm: Installed and INSTALLED are different things, only Installed is set by Windows Installer. I do not think that INSTALLED works. – Micha Wiedenmann Sep 02 '16 at 13:23
  • 1
    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 :) – kayleeFrye_onDeck Dec 05 '17 at 00:41
  • 1
    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 https://www.firegiant.com/wix/tutorial/com-expression-syntax-miscellanea/expression-syntax/, relevant MSI at https://msdn.microsoft.com/en-us/library/windows/desktop/aa369297%28v=vs.85%29.aspx – Micha Wiedenmann Dec 14 '17 at 15:04
  • there is also difference between 'Remove' (Remove button in MSI) and 'Uninstall' (using Windows -> Settings -> Apps -> Uninstall) – Sergii Sep 27 '22 at 06:46
169

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.

kmonsoor
  • 7,600
  • 7
  • 41
  • 55
ahmd0
  • 16,633
  • 33
  • 137
  • 233
  • 3
    REMOVE also appears to be set for Change – szx May 21 '14 at 07:34
  • 3
    The 'Upgrade' column, is that during the uninstall sequence of the old version or the install sequence of the new version? – Nick Whaley Dec 30 '14 at 21:43
  • 1
    @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. – ahmd0 Jan 16 '15 at 18:34
  • 1
    @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. – Nick Whaley Feb 03 '15 at 21:07
  • 1
    @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. http://www.symantec.com/connect/articles/msi-upgrade-overview https://msdn.microsoft.com/en-us/library/aa372379%28v=vs.85%29.aspx – Chaoix Feb 25 '15 at 17:46
  • 1
    Thanks for the table, I also found that REMOVE was true when I clicked "Repair" (has anyone else run into this?) – m1m1k Feb 09 '16 at 05:24
  • 1
    @m1m1k: Can you provide more details when REMOVE was true for a repair? – c00000fd Feb 13 '16 at 23:17
  • 1
    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: (NOT UPGRADINGPRODUCTCODE) AND REMOVE AND Installed – Dewey Vozel Jun 24 '20 at 14:52
  • @DeweyVozel The `AND Installed` part of your condition is redundant according to this answer's truth table. – Will Aug 05 '20 at 23:55
  • @m1m1k I believe that's why you should check for `(REMOVE = "ALL")` instead of just `REMOVE`. – Will Aug 06 '20 at 00:16
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.

Stein Åsmul
  • 39,960
  • 25
  • 91
  • 164
csexton
  • 24,061
  • 15
  • 54
  • 57
  • 3
    25 upvotes but not an accepted answer. Welcome to the world of installers! :) – Christopher Painter Feb 12 '13 at 12:15
  • 4
    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! – Simon Apr 07 '16 at 12:33
  • @Simon All of that can be easily prevented by using a DLL that isn't part of the installed files instead of an EXE. – Will Aug 05 '20 at 23:26
  • 1
    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 UPGRADINGPRODUCTCODE` – Will Aug 06 '20 at 00:15
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.

Olly
  • 5,966
  • 31
  • 60
Rob Mensching
  • 33,834
  • 5
  • 90
  • 130
  • I was about to use a batch script and then saw the PS section. Thanks for saving me : ) The Remove ~="ALL" worked for me. – ArNumb May 10 '19 at 06:59
17

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://stackoverflow.com/a/17608049/1721136 -->
 <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:

Bill Tarbell
  • 4,933
  • 2
  • 32
  • 52
  • This is a great solution. Remember to also consider PATCH and MSIPATCHREMOVE conditions. – Garet Jax Nov 05 '19 at 15:39
  • 1
    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 (https://learn.microsoft.com/en-us/windows/win32/msi/property-reference). – Patrick Nov 06 '19 at 01:07
  • 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? – Patrick Nov 06 '19 at 01:18
  • @Patrick 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. – Bill Tarbell Jan 27 '21 at 16:45
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.

Stein Åsmul
  • 39,960
  • 25
  • 91
  • 164
Sid
  • 4,905
  • 1
  • 17
  • 17
0

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>