From 4806977ee28e0ef14f6b5a10bbd5441bd19d6a2e Mon Sep 17 00:00:00 2001 From: Rob Mensching Date: Thu, 28 Jan 2016 01:46:03 -0800 Subject: [PATCH] Create bundle for Windows Creates a nice looking bundle that installs the .NET Core MSI package. Prerequisites and additional packages can be added as needed. Resolves #1064 --- .../Dotnet.Cli.Msi.Tests/InstallFixture.cs | 4 + .../Dotnet.Cli.Msi.Tests/InstallationTests.cs | 12 +- .../Dotnet.Cli.Msi.Tests/MsiManager.cs | 28 ++++- packaging/windows/bundle.thm | 107 ++++++++++++++++++ packaging/windows/bundle.wxl | 59 ++++++++++ packaging/windows/bundle.wxs | 57 ++++++++++ packaging/windows/dotnet.wxs | 2 +- packaging/windows/generatemsi.ps1 | 102 +++++++++++++++-- 8 files changed, 350 insertions(+), 21 deletions(-) create mode 100644 packaging/windows/bundle.thm create mode 100644 packaging/windows/bundle.wxl create mode 100644 packaging/windows/bundle.wxs diff --git a/packaging/windows/Dotnet.Cli.Msi.Tests/InstallFixture.cs b/packaging/windows/Dotnet.Cli.Msi.Tests/InstallFixture.cs index 96b599ca2..0feb379f5 100644 --- a/packaging/windows/Dotnet.Cli.Msi.Tests/InstallFixture.cs +++ b/packaging/windows/Dotnet.Cli.Msi.Tests/InstallFixture.cs @@ -14,6 +14,10 @@ namespace Dotnet.Cli.Msi.Tests public InstallFixture() { string msiFile = Environment.GetEnvironmentVariable("CLI_MSI"); + if(string.IsNullOrEmpty(msiFile)) + { + throw new InvalidOperationException("%CLI_MSI% must point to the msi that is to be tested"); + } _msiMgr = new MsiManager(msiFile); diff --git a/packaging/windows/Dotnet.Cli.Msi.Tests/InstallationTests.cs b/packaging/windows/Dotnet.Cli.Msi.Tests/InstallationTests.cs index 893ee1fc3..d19c2b07c 100644 --- a/packaging/windows/Dotnet.Cli.Msi.Tests/InstallationTests.cs +++ b/packaging/windows/Dotnet.Cli.Msi.Tests/InstallationTests.cs @@ -10,22 +10,20 @@ namespace Dotnet.Cli.Msi.Tests { public class InstallationTests : IDisposable { - private string _msiFile; private MsiManager _msiMgr; public InstallationTests() { // all the tests assume that the msi to be tested is available via environment variable %CLI_MSI% - _msiFile = Environment.GetEnvironmentVariable("CLI_MSI"); - if(string.IsNullOrEmpty(_msiFile)) + var msiFile = Environment.GetEnvironmentVariable("CLI_MSI"); + if(string.IsNullOrEmpty(msiFile)) { throw new InvalidOperationException("%CLI_MSI% must point to the msi that is to be tested"); } - _msiMgr = new MsiManager(_msiFile); + _msiMgr = new MsiManager(msiFile); } - [Theory] [InlineData("")] [InlineData(@"%SystemDrive%\dotnet")] @@ -34,14 +32,14 @@ namespace Dotnet.Cli.Msi.Tests installLocation = Environment.ExpandEnvironmentVariables(installLocation); string expectedInstallLocation = string.IsNullOrEmpty(installLocation) ? Environment.ExpandEnvironmentVariables(@"%ProgramFiles%\dotnet") : - installLocation; + installLocation; // make sure that the msi is not already installed, if so the machine is in a bad state Assert.False(_msiMgr.IsInstalled, "The dotnet CLI msi is already installed"); Assert.False(Directory.Exists(expectedInstallLocation)); _msiMgr.Install(installLocation); - Assert.True(_msiMgr.IsInstalled); + Assert.True(_msiMgr.IsInstalled); Assert.True(Directory.Exists(expectedInstallLocation)); _msiMgr.UnInstall(); diff --git a/packaging/windows/Dotnet.Cli.Msi.Tests/MsiManager.cs b/packaging/windows/Dotnet.Cli.Msi.Tests/MsiManager.cs index cd56d6f08..a944a2a44 100644 --- a/packaging/windows/Dotnet.Cli.Msi.Tests/MsiManager.cs +++ b/packaging/windows/Dotnet.Cli.Msi.Tests/MsiManager.cs @@ -1,14 +1,16 @@ using System; using System.Collections.Generic; +using System.Diagnostics; +using System.IO; using System.Linq; using Microsoft.Deployment.WindowsInstaller; using Microsoft.Deployment.WindowsInstaller.Package; - namespace Dotnet.Cli.Msi.Tests { public class MsiManager { + private string _bundleFile; private string _msiFile; private string _productCode; private InstallPackage _installPackage; @@ -48,6 +50,7 @@ namespace Dotnet.Cli.Msi.Tests public MsiManager(string msiFile) { + _bundleFile = Path.ChangeExtension(msiFile, "exe"); _msiFile = msiFile; var ispackage = Installer.VerifyPackage(msiFile); @@ -67,8 +70,8 @@ namespace Dotnet.Cli.Msi.Tests { dotnetHome = $"DOTNETHOME={customLocation}"; } - Installer.SetInternalUI(InstallUIOptions.Silent); - Installer.InstallProduct(_msiFile, $"ACTION=INSTALL ALLUSERS=2 ACCEPTEULA=1 {dotnetHome}"); + + RunBundle(dotnetHome); return IsInstalled; } @@ -80,10 +83,25 @@ namespace Dotnet.Cli.Msi.Tests throw new InvalidOperationException($"UnInstall Error: Msi at {_msiFile} is not installed."); } - Installer.SetInternalUI(InstallUIOptions.Silent); - Installer.InstallProduct(_msiFile, "REMOVE=ALL"); + RunBundle("/uninstall"); return !IsInstalled; } + + private void RunBundle(string additionalArguments) + { + var arguments = $"/q /norestart {additionalArguments}"; + var process = Process.Start(_bundleFile, arguments); + + if (!process.WaitForExit(5 * 60 * 1000)) + { + throw new InvalidOperationException($"Failed to wait for the installation operation to complete. Check to see if the installation process is still running. Command line: {_bundleFile} {arguments}"); + } + + else if (0 != process.ExitCode) + { + throw new InvalidOperationException($"The installation operation failed with exit code: {process.ExitCode}. Command line: {_bundleFile} {arguments}"); + } + } } } diff --git a/packaging/windows/bundle.thm b/packaging/windows/bundle.thm new file mode 100644 index 000000000..16839fa68 --- /dev/null +++ b/packaging/windows/bundle.thm @@ -0,0 +1,107 @@ + + + #(loc.Caption) + Segoe UI + Segoe UI + Segoe UI + Segoe UI + Segoe UI + Segoe UI + + #(loc.Title) + + + + + #(loc.HelpHeader) + #(loc.HelpText) + + + + + + + + v[DisplayVersion] + [ReleaseSuffix] [BuildType] + #(loc.Motto) + + + + #(loc.InstallAcceptCheckbox) + + + + + + + + + #(loc.OptionsHeader) + #(loc.OptionsLocationLabel) + + + + + + + + + #(loc.FilesInUseHeader) + #(loc.FilesInUseLabel) + A + + + + + + + + + + + + + #(loc.ProgressHeader) + #(loc.ProgressLabel) + #(loc.OverallProgressPackageText) + + + + + + + + #(loc.ModifyHeader) + + + + + + + + + #(loc.SuccessHeader) + #(loc.SuccessInstallHeader) + #(loc.SuccessRepairHeader) + #(loc.SuccessUninstallHeader) + + #(loc.SuccessRestartText) + + + + + + + + #(loc.FailureHeader) + #(loc.FailureInstallHeader) + #(loc.FailureUninstallHeader) + #(loc.FailureRepairHeader) + #(loc.FailureHyperlinkLogText) + + #(loc.FailureRestartText) + + + + \ No newline at end of file diff --git a/packaging/windows/bundle.wxl b/packaging/windows/bundle.wxl new file mode 100644 index 000000000..deb315ee4 --- /dev/null +++ b/packaging/windows/bundle.wxl @@ -0,0 +1,59 @@ + + + [WixBundleName] Setup + Microsoft Dotnet CLI for Windows + You just need a shell, a text editor and 10 minutes of your time. + +Ready? Set? Let's go! + Are you sure you want to cancel? + Previous version + Setup Help + /install | /repair | /uninstall | /layout [directory] - installs, repairs, uninstalls or + creates a complete local copy of the bundle in directory. Install is the default. + +/passive | /quiet - displays minimal UI with no prompts or displays no UI and + no prompts. By default UI and all prompts are displayed. + +/norestart - suppress any attempts to restart. By default UI will prompt before restart. +/log log.txt - logs to a specific file. By default a log file is created in %TEMP%. + &Close + I &agree to the license terms and conditions + &Options + &Install + &Close + Setup Options + Install location: + &Browse + &OK + &Cancel + Setup Progress + Processing: + Initializing... + &Cancel + Modify Setup + &Repair + &Uninstall + &Close + Repair Successfully Completed + Uninstall Successfully Completed + Installation Successfully Completed + Setup Successful + &Launch + You must restart your computer before you can use the software. + &Restart + &Close + Setup Failed + Setup Failed + Uninstall Failed + Repair Failed + One or more issues caused the setup to fail. Please fix the issues and then retry setup. For more information see the <a href="#">log file</a>. + You must restart your computer to complete the rollback of the software. + &Restart + &Close + Files In Use + The following applications are using files that need to be updated: + Close the &applications and attempt to restart them. + &Do not close applications. A reboot will be required. + &OK + &Cancel + diff --git a/packaging/windows/bundle.wxs b/packaging/windows/bundle.wxs new file mode 100644 index 000000000..6999e8a3c --- /dev/null +++ b/packaging/windows/bundle.wxs @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eula.rtf + + + + + diff --git a/packaging/windows/dotnet.wxs b/packaging/windows/dotnet.wxs index f978de721..561a469ab 100644 --- a/packaging/windows/dotnet.wxs +++ b/packaging/windows/dotnet.wxs @@ -6,7 +6,7 @@ - + diff --git a/packaging/windows/generatemsi.ps1 b/packaging/windows/generatemsi.ps1 index 6310cd370..ce1c09d46 100644 --- a/packaging/windows/generatemsi.ps1 +++ b/packaging/windows/generatemsi.ps1 @@ -8,6 +8,7 @@ param( . "$PSScriptRoot\..\..\scripts\common\_common.ps1" $DotnetMSIOutput = "" +$DotnetBundleOutput = "" $WixRoot = "" $InstallFileswsx = "install-files.wxs" $InstallFilesWixobj = "install-files.wixobj" @@ -19,7 +20,7 @@ function AcquireWixTools Write-Host Restoring Wixtools.. $result = $env:TEMP - + .\dotnet restore $RepoRoot\packaging\windows\WiXTools --packages $result | Out-Null if($LastExitCode -ne 0) @@ -32,14 +33,14 @@ function AcquireWixTools $result = Join-Path $result WiX\3.10.0.2103-pre1\tools } - popd + popd return $result } function RunHeat { - $result = $true - pushd "$WixRoot" + $result = $true + pushd "$WixRoot" Write-Host Running heat.. @@ -63,7 +64,8 @@ function RunCandle Write-Host Running candle.. $AuthWsxRoot = Join-Path $RepoRoot "packaging\windows" - .\candle.exe -dDotnetSrc="$inputDir" ` + .\candle.exe -nologo ` + -dDotnetSrc="$inputDir" ` -dMicrosoftEula="$RepoRoot\packaging\osx\resources\en.lproj\eula.rtf" ` -dBuildVersion="$env:DOTNET_MSI_VERSION" ` -dDisplayVersion="$env:DOTNET_CLI_VERSION" ` @@ -92,14 +94,20 @@ function RunLight pushd "$WixRoot" Write-Host Running light.. + $CabCache = Join-Path $WixRoot "cabcache" + $AuthWsxRoot = Join-Path $RepoRoot "packaging\windows" - .\light -ext WixUIExtension -ext WixDependencyExtension -ext WixUtilExtension ` + .\light.exe -nologo -ext WixUIExtension -ext WixDependencyExtension -ext WixUtilExtension ` -cultures:en-us ` dotnet.wixobj ` provider.wixobj ` registrykeys.wixobj ` checkbuildtype.wixobj ` $InstallFilesWixobj ` + -b "$inputDir" ` + -b "$AuthWsxRoot" ` + -reusecab ` + -cc "$CabCache" ` -out $DotnetMSIOutput | Out-Host if($LastExitCode -ne 0) @@ -112,19 +120,80 @@ function RunLight return $result } +function RunCandleForBundle +{ + $result = $true + pushd "$WixRoot" + + Write-Host Running candle for bundle.. + $AuthWsxRoot = Join-Path $RepoRoot "packaging\windows" + + .\candle.exe -nologo ` + -dDotnetSrc="$inputDir" ` + -dMicrosoftEula="$RepoRoot\packaging\osx\resources\en.lproj\eula.rtf" ` + -dBuildVersion="$env:DOTNET_MSI_VERSION" ` + -dDisplayVersion="$env:DOTNET_CLI_VERSION" ` + -dReleaseSuffix="$env:ReleaseSuffix" ` + -dMsiSourcePath="$DotnetMSIOutput" ` + -arch x64 ` + -ext WixBalExtension.dll ` + -ext WixUtilExtension.dll ` + -ext WixTagExtension.dll ` + "$AuthWsxRoot\bundle.wxs" | Out-Host + + if($LastExitCode -ne 0) + { + $result = $false + Write-Host "Candle failed with exit code $LastExitCode." + } + + popd + return $result +} + +function RunLightForBundle +{ + $result = $true + pushd "$WixRoot" + + Write-Host Running light for bundle.. + $AuthWsxRoot = Join-Path $RepoRoot "packaging\windows" + + .\light.exe -nologo ` + -cultures:en-us ` + bundle.wixobj ` + -ext WixBalExtension.dll ` + -ext WixUtilExtension.dll ` + -ext WixTagExtension.dll ` + -b "$AuthWsxRoot" ` + -out $DotnetBundleOutput | Out-Host + + if($LastExitCode -ne 0) + { + $result = $false + Write-Host "Light failed with exit code $LastExitCode." + } + + popd + return $result +} + + if(!(Test-Path $inputDir)) { throw "$inputDir not found" } -if(!(Test-Path $PackageDir)) +if(!(Test-Path $PackageDir)) { mkdir $PackageDir | Out-Null } $DotnetMSIOutput = Join-Path $PackageDir "dotnet-win-x64.$env:DOTNET_CLI_VERSION.msi" +$DotnetBundleOutput = Join-Path $PackageDir "dotnet-win-x64.$env:DOTNET_CLI_VERSION.exe" Write-Host "Creating dotnet MSI at $DotnetMSIOutput" +Write-Host "Creating dotnet Bundle at $DotnetBundleOutput" $WixRoot = AcquireWixTools @@ -135,7 +204,7 @@ if([string]::IsNullOrEmpty($WixRoot)) } if(-Not (RunHeat)) -{ +{ Exit -1 } @@ -144,18 +213,35 @@ if(-Not (RunCandle)) Exit -1 } +if(-Not (RunCandleForBundle)) +{ + Exit -1 +} + if(-Not (RunLight)) { Exit -1 } +if(-Not (RunLightForBundle)) +{ + Exit -1 +} + if(!(Test-Path $DotnetMSIOutput)) { throw "Unable to create the dotnet msi." Exit -1 } +if(!(Test-Path $DotnetBundleOutput)) +{ + throw "Unable to create the dotnet bundle." + Exit -1 +} + Write-Host -ForegroundColor Green "Successfully created dotnet MSI - $DotnetMSIOutput" +Write-Host -ForegroundColor Green "Successfully created dotnet bundle - $DotnetBundleOutput" _ $PSScriptRoot\testmsi.ps1 @("$DotnetMSIOutput")