oleg-shilo / wixsharp

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

Installing dotnetcore3.1 using wixsharp #837

Closed tig closed 4 years ago

tig commented 4 years ago

This should be simple. I've searched the Internet and read as much as I could, but I can't find anything simple. I can't be the only person using wixsharp to install a dotnetcore3.1 based Windows app and needing the installer to ensure dotnetcore3.1 is installed.

Perhaps I'm just missing a key piece of understanding. What I'm hoping for is someone can literally just say "Charlie, just add the following 3 lines to your setup.cs file:

Here's my wixsharp installer:

using System;
using System.Diagnostics;
using WixSharp;

namespace WinPrintInstaller {
    internal class Install {
        public static readonly Guid UpgradeCode = new Guid("{0002A500-0000-0000-C000-000000000046}");
        public static readonly Guid ProductCode = new Guid("{0002A501-0000-0000-C000-000000000046}");

        private static void Main() {
            const string sourceBaseDir = @"..\..\release";
            const string outDir = @"..\..\install";
            var versionFile = $"{sourceBaseDir}\\WinPrint.Core.dll";
            Debug.WriteLine($"version path: {versionFile}");
            var info = FileVersionInfo.GetVersionInfo(versionFile);
            var feature = new Feature(new Id("winprint"));

            var project = new Project(info.ProductName, new EnvironmentVariable("PATH", "[INSTALLDIR]") { Part = EnvVarPart.last }) {

                RegValues = new[] {
                    new RegValue(feature, RegistryHive.LocalMachine, $@"Software\{info.CompanyName}\{info.ProductName}", "Telemetry", "[TELEMETRY]") {
                        Win64 = true,
                        // https://github.com/oleg-shilo/wixsharp/issues/818#issuecomment-597058371
                        AttributesDefinition = "Type=integer"
                    }
                },

                Dirs = new[] {
                    new Dir(feature, $"%ProgramFiles%\\{info.CompanyName}\\{info.ProductName}",
                        new File(@"pygmentize.exe"),
                        new Files(@"*.dll"),
                        new Files(@"*.deps.json"),
                        new Files(@"*.runtimeconfig.json"),
                        new File(new Id("winprintgui_exe"), @"winprintgui.exe",
                            new FileShortcut("winprint", "INSTALLDIR") { AttributesDefinition = "Advertise=yes"} ),
                        new ExeFileShortcut("Uninstall winprint", "[System64Folder]msiexec.exe", "/x [ProductCode]")),
                    new Dir(feature, $"%AppData%\\{info.CompanyName}\\{info.ProductName}"),
                    new Dir(feature, $"%ProgramMenu%\\{info.CompanyName}\\{info.ProductName}",
                        new ExeFileShortcut("WinPrint", "[INSTALLDIR]winprintgui.exe", arguments: ""))
                 },

                Properties = new[]{
                    //setting property to be used in install condition
                    new Property("ALLUSERS", "1"),
                    new Property("TELEMETRY", "1"),
                }
            };

            // See Core.Models.
            project.GUID = ProductCode;
            project.UpgradeCode = UpgradeCode;
            project.SourceBaseDir = sourceBaseDir;
            project.OutDir = outDir;

            project.Version = new Version(info.ProductVersion);
            project.MajorUpgrade = new MajorUpgrade {
                Schedule = UpgradeSchedule.afterInstallInitialize,
                AllowSameVersionUpgrades = true,
                DowngradeErrorMessage = "A later version of [ProductName] is already installed. Setup will now exit."
            };
            project.Platform = Platform.x64;

            project.ControlPanelInfo.Comments = $"winprint by Charlie Kindel";
            project.ControlPanelInfo.Readme = "https://tig.github.io/winprint";
            project.ControlPanelInfo.HelpLink = "https://tig.github.io/winprint";
            project.ControlPanelInfo.UrlInfoAbout = "https://tig.github.io/winprint";
            project.ControlPanelInfo.UrlUpdateInfo = "https://tig.github.io/winprint";
            project.ControlPanelInfo.Manufacturer = info.CompanyName;
            project.ControlPanelInfo.InstallLocation = "[INSTALLDIR]";
            project.ControlPanelInfo.NoModify = true;

            project.PreserveTempFiles = true;

            //project.SetNetFxPrerequisite("NETFRAMEWORK20='#1'");

            project.EmbeddedUI = new EmbeddedAssembly(System.Reflection.Assembly.GetExecutingAssembly().Location);
            project.PreserveTempFiles = true;

            project.CAConfigFile = "CustomAction.config";
            project.BuildMsi();
        }
    }
}
oleg-shilo commented 4 years ago

Charlie, just adding 3 lines to your setup.cs is unlikely to help 😄

WixSharp is an MSI builder and as such it is bound to the all MSI constraints/limitations. One of which is that MSI can only install a single product. You are trying to install two: .net and your product. The only thing that a single MSI allows you to do is to detect the presense of .NET Core but not to install it.

Thus it is a task for a bootstrapper, that would install .NET Core and then your MSI.

WixSharp supports two types of bootstrapping. MSI/WiX and NSIS. The code samples demonstrate both (WixBootsstrapper and NsisBootstrapper):

var bootstrapper =
        new Bundle("My Product",
            // new PackageGroupRef("NetFx40Web"),
            new ExePackage(@"..\redist\dotNetFx40_Full_x86_x64.exe") 
            {
                 Name = "Microsoft .NET Framework 4.0",
                 InstallCommand = "/passive /norestart",
                 Permanent = true,
                 Vital = true,
                 DetectCondition = "Netfx4FullVersion AND (NOT VersionNT64 OR Netfx4x64FullVersion)",
                 Compressed = true
            },
            new MsiPackage("<path_to_your_msi>"),
. . .
bootstrapper.Build();

Thus the WiX bootstrapper sample above shows to to embed .NET 4.0 setup executable in your bootstrapper. The commented out code shows how to include .NET 4.0 web setup that is already developed by WiX team.

Both techniques can be extended to .NET Core though you will need to find out how much of prework is done by WiX team. If none (e.g. detect condition is not available for .net core) then your best chance is to embed the .NET core setup executable as above but instead of the DetectCondition use custom BA where you can implement detection by yourself. WixBootstrapper_NoUI sample shows how to create a custom BA.

Alternative bootstrapper NSIS may in fact help you to solve the problem in a simpler way. I am not fluent in NSIS so cannot halp you much but Tigran @geghamyan (is the one who contributed NSIS WixSharp binding to NSIS bootstrapper) probably can.

tig commented 4 years ago

Super helpful. Thanks.

I found this: https://github.com/wixtoolset/issues/issues/6099

Now all I need to do is to figure out how to add the PackageGroupRef XML definition found there (https://github.com/wixtoolset/issues/issues/6099#issuecomment-588698243) to a wixsharp project...

oleg-shilo commented 4 years ago

Oh you can totally add ExePackage. It's fully supported:

new Bundle("My Product Suite",
            new ExePackage(@"Samples\Setup1.exe")
            {
                Id = "package_net_core",
                Name = "aspnetcore-runtime-3.1.2-win-x64.exe",
                InstallCommand = "/install /quiet /norestart /log \"[AspNetCoreRuntime31Log]\"",
                RepairCommand = "/repair /quiet /norestart /log \"[AspNetCoreRuntime31Log]\"",
                UninstallCommand = "/uninstall /quiet /norestart /log \"[AspNetCoreRuntime31Log]\"",
                Permanent = true,
                . . .
                RemotePayloads = new[]
                {
                    new RemotePayload
                    {
                        CertificatePublicKey="6ADD0C9D1AC70DA3668644B1C78884E82E3F3457",
                        CertificateThumbprint="711AF71DC4C4952C8ED65BB4BA06826ED3922A32",
                        Description="Microsoft ASP.NET Core 3.1.2 - Shared Framework",
                        Hash="B8EDDD91C0DFD9E47EB7DD7EFED9541340607ADC",
                        ProductName="Microsoft ASP.NET Core 3.1.2 - Shared Framework",
                        Size=7812680,
                        Version="3.1.2.20068".ToRawVersion()
                    }
                },
            },
            new MsiPackage(productMsi)
            . . .
geghamyan commented 4 years ago

Hi Charlie, Building a fully-rounded, correctly handing upgrades, preventing downgrades etc burn bootstrapper with a custom UI is not a trivial task. We have products built with the wix burn bootstrapper and with the NSIS bootstrapper and I can tell you from experience we had to add much more code to the wix burn samples to tailor it to our needs.

If you are okay with increasing the size of your final setup package, you can use NSIS bootstrapper and embed the dotnetcore package into it.

tig commented 4 years ago

Here's what I ended up doing, since I already have a WPF EmbeddedUI:

Gets me 95% of where I need to be for this project:

       private bool IsDotNetCore31Installed() {
            try {
                RegistryKey localMachine64 = RegistryKey.OpenBaseKey(Microsoft.Win32.RegistryHive.LocalMachine, RegistryView.Registry64);
                RegistryKey lKey = localMachine64.OpenSubKey(@"SOFTWARE\dotnet\Setup\InstalledVersions\x64\sharedhost\", false);
                Version installed = new Version((string)lKey.GetValue("Version"));
                Version required = new Version(Session["RequiredDotNetCoreVersion"]);
                return installed.CompareTo(required) > 0;
            }
            catch {
                return false;
            }
        }

        private void Window_Loaded(object sender, RoutedEventArgs e) {
            if (IsDotNetCore31Installed()) {
            }
            else {

                // Configure message box
                string message = $"winprint requires .NET Core {Session["RequiredDotNetCoreVersion"]} to run.\n\nClick OK to download and install.";
                string caption = this.Title;
                MessageBoxButton buttons = MessageBoxButton.OK;
                MessageBoxImage icon = MessageBoxImage.Information;
                MessageBoxResult defaultResult = MessageBoxResult.OK;
                // Show message box
                MessageBoxResult result = MessageBox.Show(this, message, caption, buttons, icon, defaultResult);
                Process.Start("https://dotnet.microsoft.com/download/dotnet-core/current/runtime");
            }
        }
geghamyan commented 4 years ago

Yes, it basically similar to an approach from here: wixsharp\Source\src\WixSharp.Samples\Wix# Samples\Bootstrapper\Simplified Bootstrapper

Just take into account that UI and your code won't execute if you run the installer in the silent mode.

yurislav commented 4 years ago

What you need to do is:

  1. wrap your MSI project into bootstrapper application
  2. look up whether .net core is already installed (e.g. by registry search) and put result into variable
  3. add conditional .net core installation to bootstrapper application based on result from previous step

I've made some extensions for wixsharp. Altough, my project depends on .net core bundled with hosting, you may need alter the registry search to suit your needs.

Hope I have time to clean up my project soon and share it on github, here are at least some help for now:

var bootstrapper = new Bundle("product name here")
            {
                UpgradeCode = BootstrapperUpgradeCode,
                Version = version,
                Chain = new List<ChainItem>()
                {
                    // TODO: add the .net core online dependency with condition set to "NOT DOT_NET_CORE_AND_HOSTING_DETECTED"
                    // TODO: add your MSI project
                }
            };

bootstrapper
    .HideFromAddRemovePrograms()
    .SuppressApplicationOptionsUI()
    .IncludeWixUtilExtension()
    .AddRegistrySearchAspNetCoreExists("DOT_NET_CORE_AND_HOSTING_DETECTED")

The extension methods used above are available here: https://gist.github.com/yurislav/d5d22097870f55657b15c78557417475

oleg-shilo commented 4 years ago

@yurislav, just by looking at this:

bootstrapper
    .HideFromAddRemovePrograms()
    .SuppressApplicationOptionsUI()
    .IncludeWixUtilExtension()
    .AddRegistrySearchAspNetCoreExists(. . .)

I can tell that you have created an interesting extension suite.

Is it something that you would like to become a part of WixSharp codebase?

yurislav commented 4 years ago

@yurislav, just by looking at this:

bootstrapper
    .HideFromAddRemovePrograms()
    .SuppressApplicationOptionsUI()
    .IncludeWixUtilExtension()
    .AddRegistrySearchAspNetCoreExists(. . .)

I can tell that you have created an interesting extension suite.

Is it something that you would like to become a part of WixSharp codebase?

Sure, I will separate my project-specific code with these extensions as an public repository and you can integrate relevant methods to wixsharp, because there are also temporary hotfixes for already resolved bugs or missing features.

oleg-shilo commented 4 years ago

Great, thank you

yurislav commented 4 years ago

Hi @oleg-shilo, as I promised, my project-specific code is separated and rest is on github repo. ~I'll try to add readme with examples soon~.

Edit: example installer for asp.net core 3.1 application .

oleg-shilo commented 4 years ago

Great. Thank you. Will have a look.

oleg-shilo commented 4 years ago

It does seem like a comprehensive extension to the default WixSharp functionality.

I am happy to include it to WixSharp codebase and I will indicate you as a contributor. But...

Considering the scale of your contribution, are you sure that you don't want to make it into your own dedicated NuGet package? IE WixSharp.extensions.

yurislav commented 4 years ago

Yes, that's what I already did (https://www.nuget.org/packages/NineDigit.WixSharpExtensions). Leaving the code as truly separated "extension" makes sense too.

oleg-shilo commented 4 years ago

Fantastic. Then I will put the recommendation and the link in one of the WixSharp main pages. Txs

oleg-shilo commented 4 years ago

Done: https://github.com/oleg-shilo/wixsharp/wiki/Developer's-Guide#nuget-packages

yurislav commented 4 years ago

Great, thank you :)