oleg-shilo / wixsharp

Framework for building a complete MSI or WiX source code by using script files written with C# syntax.
MIT License
1.12k stars 175 forks source link

Repair not working #1515

Closed grizoood closed 3 months ago

grizoood commented 7 months ago

I'm using Custom UI WPF.

I am trying to carry out a repair, for this I first install the program, then I delete the installed folders, I restart the msi, I am on the MaintenanceTypeDialog window, I click on Repair, I expect that 'it copies the missing files again but it doesn't do it. Another thing I noticed is that if I do it from the control panel, and I click on Repair it works.

I replaced this:

public void Repair()
{
    if (session != null)
    {
        session["MODIFY_ACTION"] = "Repair";
        JumpToProgressDialog();
    }
}

by this:

public void Repair()
{
    if (session != null)
    {
        session["REINSTALL"] = "ALL";
        session["REINSTALLMODE"] = "f";
        session["MODIFY_ACTION"] = "Repair";
        JumpToProgressDialog();
    }
}

I don't know if this is the right way to do it?

I have the impression that session["MODIFY_ACTION"] = "Repair"; does nothing at all.

https://learn.microsoft.com/fr-fr/windows-server/administration/windows-commands/msiexec

### Repair options
You can use this command to repair an installed package.

#### Syntax
msiexec.exe [/f{p|o|e|d|c|a|u|m|s|v}] <product_code>

#### Parameters
| Parameter | Description |
| --------- | ----------- |
| /fp       | Repairs the package if a file is missing. |
| /fo       | Repairs the package if a file is missing, or if an older version is installed. |
| /fe       | Repairs the package if file is missing, or if an equal or older version is installed. |
| /fd       | Repairs the package if file is missing, or if a different version is installed. |
| /fc       | Repairs the package if file is missing, or if checksum does not match the calculated value. |
| /fa       | Forces all files to be reinstalled. |
| /fu       | Repairs all the required user-specific registry entries. |
| /fm       | Repairs all the required computer-specific registry entries. |
| /fs       | Repairs all existing shortcuts. |
| /fv       | Runs from source and re-caches the local package. |

Here is the command executed by Windows (Task manager select "Details") when clicking on the “Repair” button:

"C:\WINDOWS\System32\msiexec.exe" /f "C:\Users\{USERNAME}\Downloads\wixsharp-2.1.5.0\wixsharp-2.1.5.0\Source\src\WixSharp.Samples\Wix# Samples\Managed Setup\CustomUI.WPF.Sequence\bin\Debug\ManagedSetup.msi"
Torchok19081986 commented 7 months ago

hiho, in msi AFAIK you have to specify reinstallmode. From view of Msi , is Repair - reintall, real Repair-Installation not happens. Standard is -omus, something like that. And there is also another way to use repair from msiexec. For example Adobe Reader DC , write reinstall command to registry and execute it if user click repair.

MS link to it : https://learn.microsoft.com/en-us/windows/win32/msi/reinstallmode

grizoood commented 7 months ago

What should I do ?

This :

 session["REINSTALLMODE"] = "o";
public void Repair()
{
    if (session != null)
    {
        session["REINSTALL"] = "ALL";
        session["REINSTALLMODE"] = "o";
        session["MODIFY_ACTION"] = "Repair";
        JumpToProgressDialog();
    }
}
Torchok19081986 commented 7 months ago

probably, "omus" or just "o". Try it. also looks in your msi description if reininstallmode spefifyed there. msiproject.ReinstallMode Property should be set, which one, you can choose or tested, what do u want. Important is : MajorUpgradeStrategy shoud be also set.

info from wix# self

The REINSTALLMODE property is a string that contains letters specifying the type // of reinstall to perform. Options are case-insensitive and order-independent. // This property should normally always be used in conjunction with the REINSTALL // property. // // Note, REINSTALLMODE property will be created only in the automatically produced // WiX definition file only if WixSharp.Project.MajorUpgrade is set.

grizoood commented 7 months ago

The default is "omus".

//
// Résumé :
//     The REINSTALLMODE property is a string that contains letters specifying the type
//     of reinstall to perform. Options are case-insensitive and order-independent.
//     This property should normally always be used in conjunction with the REINSTALL
//     property.
//
//     Note, REINSTALLMODE property will be created only in the automatically produced
//     WiX definition file only if WixSharp.Project.MajorUpgrade is set.
//
//     Read more: https://docs.microsoft.com/en-us/windows/desktop/msi/reinstallmode
public string ReinstallMode = "omus";

So I don't need to write this:

public void Repair()
{
    if (session != null)
    {
        session["REINSTALL"] = "ALL";
        session["REINSTALLMODE"] = "o";
        session["MODIFY_ACTION"] = "Repair";
        JumpToProgressDialog();
    }
}

But this is enough, you just have to specify Reinstall

 session["REINSTALL"] = "ALL";
public void Repair()
{
    if (session != null)
    {
        session["REINSTALL"] = "ALL";
        session["MODIFY_ACTION"] = "Repair";
        JumpToProgressDialog();
    }
}
oleg-shilo commented 7 months ago

Hi @grizoood. You wrote:

I have the impression that session["MODIFY_ACTION"] = "Repair"; does nothing at all.

Syntax

msiexec.exe [/f{p|o|e|d|c|a|u|m|s|v}]

This makes me think that you are expecting MODIFY_ACTION to be set to one of the msiexec args. While this may even work it is not something that should not be assumed. session and msiexec are very different creatures and by default we have to assume that they do not share the interface. Unless of course, you found some evidence of sharing in the documentation.

Anyway, I decided to test your assumption directly. The assumption of session["MODIFY_ACTION"] = "Repair"; not triggering the repair option.

I just created a fresh project from the VS template (wix4 WPF) and it seems to repair correctly either from msi execution or from ARP. I have attached the working sample for you.

So quite possibly your problem is caused by something else, not by the incorrect value of the MODIFY_ACTION property.

WixSharp Setup2.zip

grizoood commented 7 months ago

Hello @oleg-shilo

Having a problem with Wix4 for the %AppData% or %CommonAppData% folder (https://github.com/oleg-shilo/wixsharp/issues/1503), I am currently using the wix3 WPF template.

Here are the 2 screens on Wix4 of the access path: image image

I don't have access to version 2.1.6, I couldn't test it.

image

I carried out some tests using %ProgramFiles% in Wix3 and Wix4, it launches the reinstallation correctly (it copies the missing files) I also tried %CommonAppData% in Wix3, it launches the reinstallation fine.

But if I use %AppData%, (I was only able to do it in Wix3 because I had a problem with AppData in Wix4), it does nothing.

I simply modified the example to put %AppData%:

//string rootFolder = "%ProgramFiles%";
//string rootFolder = "%CommonAppData%";
string rootFolder = "%AppData%";

var project = new ManagedProject("MyProductWix3",
                  new Dir($@"{rootFolder}\My Company\My Product Wix3",
                      new File("Program.cs")));

project.InstallScope = InstallScope.perUser;

I am attaching the 2 projects Wix4 and Wix3. WixSharp.Setup.zip

oleg-shilo commented 7 months ago

Yeah, the #1503 fix (v.2.1.6) is not released yet. It's only available on my machine. My bad. I'll do the release right away.

grizoood commented 7 months ago

Can you try with %AppData% and project.InstallScope = InstallScope.perUser with version 2.6.1 and wix4? Does it work?

oleg-shilo commented 7 months ago

Sorry, I think we are better off keeping focus and not jumping over all possible issues. :)

This very issue #1515 I cannot reproduce the problem. When I built a fresh project it works fine. I shared the sample with you. The sample is referencing the package that is not released yet so you can downgrade it to v2.1.5. It still works as expected. I just tested that very sample I shared with you.

Another reported but unrelated issue #1503 It's fixed but not released yet. I intend to release it right away. %AppData% problem should be discussed there, where it is reported. But I will retest it again before releasing it.

oleg-shilo commented 7 months ago

Done. v2.1.6 has been released

grizoood commented 6 months ago

@oleg-shilo ,

The repair still does not work with %AppData%. Please test the project. (https://github.com/oleg-shilo/wixsharp/issues/1515#issuecomment-2088217398)

oleg-shilo commented 6 months ago

Sorry, it seems like two problems with the package not only that you describe

  1. NuGet v2.1.6 was packaged with the wrong binaries. %AppData% is not expended in the installDir even though it works OK in the codebase. I will need to find out what happened to the package and rerelease it.

  2. Even the msi that is build with the codebase binaries does not Repair correctly if it is %AppData% but does for the %ProgramFiles%. I have no explanation for that. BUt let me focus on the first problem first.

oleg-shilo commented 6 months ago

Problem 1 is a false alarm. I had a wrong NuGet cache on my PC so after clearing it it's all worked. Basically it was the case of "it does not work on my machine" :)

Problem 2 is a real problem caused by the differences in the wxs generated for %progfiles% and %appdata%. This difference was required because the appdata is a user profile location and it requires certain extra elements to be injected in wxs. Otherwise the ICE validation of msi fails. I am looking if disabling or partialy lifting these extra elements can address the problem.

oleg-shilo commented 6 months ago

Indeed disabling injection UserProfile element solves the problem. You can do it by selling the value of the AutoKey property before calling Project.Build.Msi():

AutoElements.DisableAutoUserProfileRegistry = true;

Just in case I have attached the sample project. WixSharp Setup1.zip

When it comes for the DisableAutoUserProfileRegistry it insertion of the UserProfile registry it is required for the scenarios/samples listed in the property documentation:

    //
    // Summary:
    //     Disables automatic insertion of user profile registry elements. Required for:
    //     AllInOne, ConditionalInstallation, CustomAttributes, ReleaseFolder, Shortcuts,
    //     Shortcuts (advertised), Shortcuts-2, WildCardFiles samples.
    //
    //     Can also be managed by disabling ICE validation via Light.exe command line arguments.
    public static bool DisableAutoUserProfileRegistry = false;

Since your report is the very first case of the reported side effect of such technique I will need to decide if it should be enabled by default or not. I am not yet sure what the decision will be.

grizoood commented 6 months ago

It seems to work by adding this:

AutoElements.DisableAutoUserProfileRegistry = true;

I no longer need to add this in MaintenanceTypeDialogModel :

session["REINSTALL"] = "ALL";
session["MODIFY_ACTION"] = "Repair";

Just this works with DisableAutoUserProfileRegistry = true

public void Repair()
{
    if (session != null)
    {
        session["MODIFY_ACTION"] = "Repair";

        JumpToProgressDialog();
    }
}

Thks @oleg-shilo

grizoood commented 6 months ago

Allow me to reopen the issue.

I subscribed to the AfterInstall event. In this event I have to create a file if (e.IsInstalling || e.IsRepairing) = true and delete the file if e.IsUninstalling = true

project.AfterInstall += e =>
{

    Debugger.Launch();

    MessageBox.Show(e.ToString());

    foreach (var year in YEARS)
    {
        string directory = IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), $@"Autodesk\Revit\Addins\{year}");
        string addinPath = IO.Path.Combine(directory, AddinBuilder.AddinName);
        if (e.IsInstalling || e.IsRepairing)
        {
            if (!IO.Directory.Exists(directory))
                IO.Directory.CreateDirectory(directory);

            string dllPath = IO.Path.Combine(e.InstallDir, $"{year}");
            var builder = new AddinBuilder(dllPath);
            builder.Build(addinPath);
        }
        else if (e.IsUninstalling)
        {
            if (IO.File.Exists(addinPath))
                IO.File.Delete(addinPath);
        }
    }
};

Following the click on the Repair button of the WPF interface :

image

I expect e.IsRepairing to be true and it is not.

I read elsewhere that (https://github.com/oleg-shilo/wixsharp/issues/1129) but the problem is that the modify action is not equal to "REPAIR", it is empty the property.

The results of e.ToString() :

328153871-057b95c1-c8a2-4e68-8180-1cbdbc0dbbbd

I expect that

ModifyAction is equal to "REPAIR" via:

public void Repair()
{
    if (session != null)
    {
        session["MODIFY_ACTION"] = "Repair";

        JumpToProgressDialog();
    }
}
grizoood commented 6 months ago

I have ModifyAction = "Repair" in the Load and BeforeInstall events. So I have to use the BeforeInstall event to perform the processing of adding or deleting a file?

grizoood commented 6 months ago

I did this instead, added empty .addin files for each version (2025, 2024, 2023, ...)

static void AddFiles(this ManagedProject project)
{
    string installDir = @"%AppDataFolder%";

    var files = new Dir($@"{installDir}",
        new InstallDir($@"MyCompany\MyApp\",
            //#### Revit 2025 ####
            new Dir(@"2025\",
                new Dir(@"fr-FR\",
                    new File(new Id("MyApp.resources.dll.fr_FR.2025"), @"..\MyApp\bin\Release R25\fr-FR\MyApp.resources.dll")),
                new File(new Id("MyApp.dll.2025"), @"..\MyApp\bin\Release R25\MyApp.dll")),
            //#### Revit 2024 ####
            new Dir(@"2024\",
                new Dir(@"fr-FR\",
                    new File(new Id("MyApp.resources.dll.fr_FR.2024"), @"..\MyApp\bin\Release R24\fr-FR\MyApp.resources.dll")),
                new File(new Id("MyApp.dll.2024"), @"..\MyApp\bin\Release R24\MyApp.dll")),
            //#### Revit 2023 ####
            new Dir(@"2023\",
                new Dir(@"fr-FR\",
                    new File(new Id("MyApp.resources.dll.fr_FR.2023"), @"..\MyApp\bin\Release R23\fr-FR\MyApp.resources.dll")),
                new File(new Id("MyApp.dll.2023"), @"..\MyApp\bin\Release R23\MyApp.dll")),
            //#### Revit 2022 ####
            new Dir(@"2022\",
                new Dir(@"fr-FR\",
                    new File(new Id("MyApp.resources.dll.fr_FR.2022"), @"..\MyApp\bin\Release R22\fr-FR\MyApp.resources.dll")),
                new File(new Id("MyApp.dll.2022"), @"..\MyApp\bin\Release R22\MyApp.dll")),
            //#### Revit 2021 ####
            new Dir(@"2021\",
                new Dir(@"fr-FR\",
                    new File(new Id("MyApp.resources.dll.fr_FR.2021"), @"..\MyApp\bin\Release R21\fr-FR\MyApp.resources.dll")),
                new File(new Id("MyApp.dll.2021"), @"..\MyApp\bin\Release R21\MyApp.dll")),
            new Dir($@"Resources\",
                 new File(new Id("UserSettings.xml"), @"..\Resources\UserSettings.xml"))),
        new Dir(@"Autodesk\Revit\Addins",
            new Dir(@"2025\",
                new File(new Id("addin.2025"), @"..\Addin\MyApp.addin")),
            new Dir(@"2024\",
                new File(new Id("addin.2024"), @"..\Addin\MyApp.addin")),
            new Dir(@"2023\",
                new File(new Id("addin.2023"), @"..\Addin\MyApp.addin")),
            new Dir(@"2022\",
                new File(new Id("addin.2022"), @"..\Addin\MyApp.addin")),
            new Dir(@"2021\",
                new File(new Id("addin.2021"), @"..\Addin\MyApp.addin")))
       );

    project.AddDir(files);

}

then edited the file in AfterInstall. As in the example: Setup Events :

static void project_AfterInstall(SetupEventArgs e)
{
    //Note AfterInstall is an event based on deferred Custom Action. All properties that have
    //been pushed to e.Session.CustomActionData with project.DefaultDeferredProperties are
    //also set as environment variables just before invoking this event handler.
    //Similarly the all content of e.Data is also pushed to the environment variables.
    MessageBox.Show(e.Session.GetMainWindow(),
                    e.ToString() +
                    "\npersisted_data = " + e.Data["persisted_data"] +
                    "\nADDFEATURES = " + e.Session.Property("ADDFEATURES") +
                    "\n'MyApp_Binaries' enabled = " + e.Session.IsFeatureEnabled("MyApp Binaries") +
                    "\nEnvVar('INSTALLDIR') -> " + Environment.ExpandEnvironmentVariables("%INSTALLDIR%My App.exe"),
                    caption: "AfterInstall ");
    try
    {
        System.IO.File.WriteAllText(@"C:\Program Files (x86)\My Company\My Product\Docs\readme.txt", "test");
    }
    catch { }
}

I simply check if the file exists because if I am uninstalling it means that the file no longer exists so I do not modify it.

project.AfterInstall += e =>
{
    foreach (var year in YEARS)
    {
        string directory = IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), $@"Autodesk\Revit\Addins\{year}");
        string addinPath = IO.Path.Combine(directory, AddinBuilder.AddinName);

        string dllPath = IO.Path.Combine(e.InstallDir, $"{year}");
        var builder = new AddinBuilder(dllPath);

        if (IO.File.Exists(addinPath))
            IO.File.WriteAllText(addinPath, builder.ToString());
    }
};

I need to do this because the "Assembly" property in the addin file changes depending on the year and I need to get the correct directory or its installed files:

<Assembly>C:\Users\USERNAME\AppData\Roaming\MyCompany\MyApp\2024\MyApp.dll</Assembly>
<?xml version="1.0" encoding="utf-8"?>
<RevitAddIns>
  <AddIn Type="Application">
    <Name>MyApp</Name>
    <Assembly>C:\Users\USERNAME\AppData\Roaming\MyCompany\MyApp\2024\MyApp.dll</Assembly>
    <AddInId>61b750c3-0a9a-423c-9c64-472bbe7b05fc</AddInId>
    <FullClassName>MyApp.Application</FullClassName>
    <VendorId>MyCo</VendorId>
    <VendorDescription>My Company, http://www.my-company.com</VendorDescription>
  </AddIn>
</RevitAddIns>
grizoood commented 6 months ago

I have another problem when I edit the "UserSettings.xml". I expect this file to be replaced during reinstallation but that's not the case either :/

new Dir($@"Resources\",
                 new File(new Id("UserSettings.xml"), @"..\Resources\UserSettings.xml"))),

I try this but not work :

new File(new Id("UserSettings.xml"), @"..\Resources\UserSettings.xml") { OverwriteOnInstall = true }
grizoood commented 6 months ago

@oleg-shilo Can you help me resolve this problem?

WixSharp.Setup1 (2).zip

Step to reproduce the problem:

1) Install the program for the first time 2) Go to the %AppData%\My Company\My App\2025 folder 3) Edit the File.txt file (adding text inside). 4) Restart the installation using “Repair”

The File.txt file is not replaced, however when using Repair via the drop-down menu it works.

image

For the Repair function to work from the wix# interface, I have to do this:

   public void Repair()
   {
       if (session != null)
       {
           //session["MODIFY_ACTION"] = "Repair";
           session["REINSTALL"] = "ALL";
           session["MODIFY_ACTION"] = "Repair";

           JumpToProgressDialog();
       }
   }

Is there a problem doing this?

grizoood commented 6 months ago

Using this in Repair function of MaintenanceTypeDialog, it is working

   public void Repair()
   {
       if (session != null)
       {
           //session["MODIFY_ACTION"] = "Repair";
           session["REINSTALL"] = "ALL";
           session["MODIFY_ACTION"] = "Repair";

           JumpToProgressDialog();
       }
   }

Is there a problem using session["REINSTALL"] = "ALL"; in the Repair function?

oleg-shilo commented 6 months ago

@grizoood, this is a default behaviour of MSI install process. If the file has been modified then it's considered as no longer the file that was installed. I am surprized Repair from ARP (conrol panel) works.

The good practice rules require never modifying any installed files. All modifiable files (e.g. config files) are to be created by the product app on the first run. But, I understand, sometimes following rules is difficult.

Anyway, I wanted to suggest setting "OverwriteOnInstall" but you have already discovered this trik. And yes I have tested you sample and OverwriteOnInstall does not help.

Now session["REINSTALL"] = "ALL". When I was implementing maintenance dialog I could not find and guidance anywhere hos to trigger the repair mode from ARP. I was only able to reverse engeneer session["MODIFY_ACTION"] = "Repair";. Thus I do not know if session["REINSTALL"] = "ALL" is going to cause a problem.

I google it right now and again, nothing concrete ablut this. My feeling is that you can use it and it will be just fine.

Potentially it can cause the problems when you have features and "REINSTALL:ALL" will reinstall the all default features and not nesesarely respect user feature selecting from the first session. But I have no evidence of that. :o(

grizoood commented 6 months ago

Thk @oleg-shilo,

When you say: All modifiable files (e.g. config files) are to be created by the product app on the first run.

Do you have an example in the WixSharp.Suite, to do this, I have several xml or json files to create.

oleg-shilo commented 6 months ago

No it's outside of WixSharp. It's even outside of msi.

MS articulated this deployment architecture fist time (long time ago) with the introduction of the "Certified for Vista" badge for SW products. Thus you could only certify if the deployment copies the required files (e.g. progfiles) and the product application never writes to the deployment dir. They were testing the product for that before granting the certification approval.

On the first start of the application itself, it creates the config files in the user profile. This architecture serves a few purposes:

With the approach that you have to use now restoring the XML files is impossible without restoring the binaries even though you do not have the reason for it. Your user only wanted to reset the config...

Hope this explains.

a400ID commented 6 months ago

I did this instead, added empty .addin files for each version (2025, 2024, 2023, ...)

static void AddFiles(this ManagedProject project)
{
    string installDir = @"%AppDataFolder%";

    var files = new Dir($@"{installDir}",
        new InstallDir($@"MyCompany\MyApp\",
            //#### Revit 2025 ####
            new Dir(@"2025\",
                new Dir(@"fr-FR\",
                    new File(new Id("MyApp.resources.dll.fr_FR.2025"), @"..\MyApp\bin\Release R25\fr-FR\MyApp.resources.dll")),
                new File(new Id("MyApp.dll.2025"), @"..\MyApp\bin\Release R25\MyApp.dll")),
            //#### Revit 2024 ####
            new Dir(@"2024\",
                new Dir(@"fr-FR\",
                    new File(new Id("MyApp.resources.dll.fr_FR.2024"), @"..\MyApp\bin\Release R24\fr-FR\MyApp.resources.dll")),
                new File(new Id("MyApp.dll.2024"), @"..\MyApp\bin\Release R24\MyApp.dll")),
            //#### Revit 2023 ####
            new Dir(@"2023\",
                new Dir(@"fr-FR\",
                    new File(new Id("MyApp.resources.dll.fr_FR.2023"), @"..\MyApp\bin\Release R23\fr-FR\MyApp.resources.dll")),
                new File(new Id("MyApp.dll.2023"), @"..\MyApp\bin\Release R23\MyApp.dll")),
            //#### Revit 2022 ####
            new Dir(@"2022\",
                new Dir(@"fr-FR\",
                    new File(new Id("MyApp.resources.dll.fr_FR.2022"), @"..\MyApp\bin\Release R22\fr-FR\MyApp.resources.dll")),
                new File(new Id("MyApp.dll.2022"), @"..\MyApp\bin\Release R22\MyApp.dll")),
            //#### Revit 2021 ####
            new Dir(@"2021\",
                new Dir(@"fr-FR\",
                    new File(new Id("MyApp.resources.dll.fr_FR.2021"), @"..\MyApp\bin\Release R21\fr-FR\MyApp.resources.dll")),
                new File(new Id("MyApp.dll.2021"), @"..\MyApp\bin\Release R21\MyApp.dll")),
            new Dir($@"Resources\",
                 new File(new Id("UserSettings.xml"), @"..\Resources\UserSettings.xml"))),
        new Dir(@"Autodesk\Revit\Addins",
            new Dir(@"2025\",
                new File(new Id("addin.2025"), @"..\Addin\MyApp.addin")),
            new Dir(@"2024\",
                new File(new Id("addin.2024"), @"..\Addin\MyApp.addin")),
            new Dir(@"2023\",
                new File(new Id("addin.2023"), @"..\Addin\MyApp.addin")),
            new Dir(@"2022\",
                new File(new Id("addin.2022"), @"..\Addin\MyApp.addin")),
            new Dir(@"2021\",
                new File(new Id("addin.2021"), @"..\Addin\MyApp.addin")))
       );

    project.AddDir(files);

}

then edited the file in AfterInstall. As in the example: Setup Events :

static void project_AfterInstall(SetupEventArgs e)
{
    //Note AfterInstall is an event based on deferred Custom Action. All properties that have
    //been pushed to e.Session.CustomActionData with project.DefaultDeferredProperties are
    //also set as environment variables just before invoking this event handler.
    //Similarly the all content of e.Data is also pushed to the environment variables.
    MessageBox.Show(e.Session.GetMainWindow(),
                    e.ToString() +
                    "\npersisted_data = " + e.Data["persisted_data"] +
                    "\nADDFEATURES = " + e.Session.Property("ADDFEATURES") +
                    "\n'MyApp_Binaries' enabled = " + e.Session.IsFeatureEnabled("MyApp Binaries") +
                    "\nEnvVar('INSTALLDIR') -> " + Environment.ExpandEnvironmentVariables("%INSTALLDIR%My App.exe"),
                    caption: "AfterInstall ");
    try
    {
        System.IO.File.WriteAllText(@"C:\Program Files (x86)\My Company\My Product\Docs\readme.txt", "test");
    }
    catch { }
}

I simply check if the file exists because if I am uninstalling it means that the file no longer exists so I do not modify it.

project.AfterInstall += e =>
{
    foreach (var year in YEARS)
    {
        string directory = IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), $@"Autodesk\Revit\Addins\{year}");
        string addinPath = IO.Path.Combine(directory, AddinBuilder.AddinName);

        string dllPath = IO.Path.Combine(e.InstallDir, $"{year}");
        var builder = new AddinBuilder(dllPath);

        if (IO.File.Exists(addinPath))
            IO.File.WriteAllText(addinPath, builder.ToString());
    }
};

I need to do this because the "Assembly" property in the addin file changes depending on the year and I need to get the correct directory or its installed files:

<Assembly>C:\Users\USERNAME\AppData\Roaming\MyCompany\MyApp\2024\MyApp.dll</Assembly>
<?xml version="1.0" encoding="utf-8"?>
<RevitAddIns>
  <AddIn Type="Application">
    <Name>MyApp</Name>
    <Assembly>C:\Users\USERNAME\AppData\Roaming\MyCompany\MyApp\2024\MyApp.dll</Assembly>
    <AddInId>61b750c3-0a9a-423c-9c64-472bbe7b05fc</AddInId>
    <FullClassName>MyApp.Application</FullClassName>
    <VendorId>MyCo</VendorId>
    <VendorDescription>My Company, http://www.my-company.com</VendorDescription>
  </AddIn>
</RevitAddIns>

Hi @grizoood you can create that addin file with relative path.

MyApp .\Myaddinfolder\MyApp.dll 61b750c3-0a9a-423c-9c64-472bbe7b05fc MyApp.Application MyCo My Company, http://www.my-company.com