Skip to content

Commit

Permalink
Merge pull request Squirrel#466 from Squirrel/msi
Browse files Browse the repository at this point in the history
Create IT Administrator-friendly MSI files
  • Loading branch information
anaisbetts committed Oct 14, 2015
2 parents 63a6469 + e96010a commit b6ecc40
Show file tree
Hide file tree
Showing 16 changed files with 164 additions and 35 deletions.
4 changes: 2 additions & 2 deletions src/Squirrel/UpdateManager.ApplyReleases.cs
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ await squirrelAwareApps.ForEachAsync(async exe => {

fixPinnedExecutables(new SemanticVersion(255, 255, 255, 255));

await this.ErrorIfThrows(() => Utility.DeleteDirectoryWithFallbackToNextReboot(rootAppDirectory),
await this.ErrorIfThrows(() => Utility.DeleteDirectoryOrJustGiveUp(rootAppDirectory),
"Failed to delete app directory: " + rootAppDirectory);

// NB: We drop this file here so that --checkInstall will ignore
Expand Down Expand Up @@ -592,7 +592,7 @@ await squirrelApps.ForEachAsync(async exe => {
// Finally, clean up the app-X.Y.Z directories
await toCleanup.ForEachAsync(async x => {
try {
await Utility.DeleteDirectoryWithFallbackToNextReboot(x.FullName);
await Utility.DeleteDirectoryOrJustGiveUp(x.FullName);

if (Directory.Exists(x.FullName)) {
// NB: If we cannot clean up a directory, we need to make
Expand Down
33 changes: 2 additions & 31 deletions src/Squirrel/Utility.cs
Original file line number Diff line number Diff line change
Expand Up @@ -450,42 +450,13 @@ public static void DeleteFileHarder(string path, bool ignoreIfFails = false)
}
}

public static async Task DeleteDirectoryWithFallbackToNextReboot(string dir)
public static async Task DeleteDirectoryOrJustGiveUp(string dir)
{
try {
await Utility.DeleteDirectory(dir);
} catch (Exception ex) {
var message = String.Format("Uninstall failed to delete dir '{0}', punting to next reboot", dir);
LogHost.Default.WarnException(message, ex);

Utility.DeleteDirectoryAtNextReboot(dir);
}
}

public static void DeleteDirectoryAtNextReboot(string directoryPath)
{
var di = new DirectoryInfo(directoryPath);

if (!di.Exists) {
Log().Warn("DeleteDirectoryAtNextReboot: does not exist - {0}", directoryPath);
return;
var message = String.Format("Uninstall failed to delete dir '{0}'", dir);
}

// NB: MoveFileEx blows up if you're a non-admin, so you always need a backup plan
di.GetFiles().ForEach(x => safeDeleteFileAtNextReboot(x.FullName));
di.GetDirectories().ForEach(x => DeleteDirectoryAtNextReboot(x.FullName));

safeDeleteFileAtNextReboot(directoryPath);
}

static void safeDeleteFileAtNextReboot(string name)
{
if (MoveFileEx(name, null, MoveFileFlags.MOVEFILE_DELAY_UNTIL_REBOOT)) return;

// Thank You, http://www.pinvoke.net/default.aspx/coredll.getlasterror
var lastError = Marshal.GetLastWin32Error();

Log().Error("safeDeleteFileAtNextReboot: failed - {0} - {1}", name, lastError);
}

public static void LogIfThrows(this IFullLogger This, LogLevel level, string message, Action block)
Expand Down
29 changes: 29 additions & 0 deletions src/Update/CopStache.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security;
using System.Text;
using System.Threading.Tasks;

namespace Squirrel.Update
{
public static class CopStache
{
public static string Render(string template, Dictionary<string, string> identifiers)
{
var buf = new StringBuilder();

foreach (var line in template.Split('\n')) {
identifiers["RandomGuid"] = (new Guid()).ToString();

foreach (var key in identifiers.Keys) {
buf.Replace("{{" + key + "}}", SecurityElement.Escape(identifiers[key]));
}

buf.AppendLine(line);
}

return buf.ToString();
}
}
}
76 changes: 74 additions & 2 deletions src/Update/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ int executeCommandLine(string[] args)
string icon = default(string);
string shortcutArgs = default(string);
bool shouldWait = false;
bool noMsi = false;

opts = new OptionSet() {
"Usage: Squirrel.exe command [OPTS]",
Expand Down Expand Up @@ -125,6 +126,7 @@ int executeCommandLine(string[] args)
{ "b=|baseUrl=", "Provides a base URL to prefix the RELEASES file packages with", v => baseUrl = v, true},
{ "a=|process-start-args=", "Arguments that will be used when starting executable", v => processStartArgs = v, true},
{ "l=|shortcut-locations=", "Comma-separated string of shortcut locations, e.g. 'Desktop,StartMenu'", v => shortcutArgs = v},
{ "no-msi", "Don't generate an MSI package", v => noMsi = true},
};

opts.Parse(args);
Expand Down Expand Up @@ -161,7 +163,7 @@ int executeCommandLine(string[] args)
UpdateSelf().Wait();
break;
case UpdateAction.Releasify:
Releasify(target, releaseDir, packagesDir, bootstrapperExe, backgroundGif, signingParameters, baseUrl, setupIcon);
Releasify(target, releaseDir, packagesDir, bootstrapperExe, backgroundGif, signingParameters, baseUrl, setupIcon, !noMsi);
break;
case UpdateAction.Shortcut:
Shortcut(target, shortcutArgs, processStartArgs, setupIcon);
Expand Down Expand Up @@ -304,7 +306,7 @@ public async Task Uninstall(string appName = null)
}
}

public void Releasify(string package, string targetDir = null, string packagesDir = null, string bootstrapperExe = null, string backgroundGif = null, string signingOpts = null, string baseUrl = null, string setupIcon = null)
public void Releasify(string package, string targetDir = null, string packagesDir = null, string bootstrapperExe = null, string backgroundGif = null, string signingOpts = null, string baseUrl = null, string setupIcon = null, bool generateMsi = true)
{
if (baseUrl != null) {
if (!Utility.IsHttpUrl(baseUrl)) {
Expand Down Expand Up @@ -417,6 +419,9 @@ public void Releasify(string package, string targetDir = null, string packagesDi
signPEFile(targetSetupExe, signingOpts).Wait();
}

if (generateMsi) {
createMsiPackage(targetSetupExe, new ZipPackage(package)).Wait();
}
}

public void Shortcut(string exeName, string shortcutArgs, string processStartArgs, string icon)
Expand Down Expand Up @@ -635,6 +640,73 @@ static async Task setPEVersionInfoAndIcon(string exePath, IPackage package, stri
}
}

static async Task createMsiPackage(string setupExe, IPackage package)
{
var pathToWix = pathToWixTools();
var setupExeDir = Path.GetDirectoryName(setupExe);

var templateText = File.ReadAllText(Path.Combine(pathToWix, "template.wxs"));
var templateResult = CopStache.Render(templateText, new Dictionary<string, string> {
{ "Id", package.Id },
{ "Title", package.Title },
{ "Author", package.Authors.First() },
{ "Summary", package.Summary ?? package.Description ?? package.Id },
});

var wxsTarget = Path.Combine(setupExeDir, "Setup.wxs");
File.WriteAllText(wxsTarget, templateResult, Encoding.UTF8);

var candleParams = String.Format("-nologo -ext WixNetFxExtension -out \"{0}\" \"{1}\"", wxsTarget.Replace(".wxs", ".wixobj"), wxsTarget);
var processResult = await Utility.InvokeProcessAsync(
Path.Combine(pathToWix, "candle.exe"), candleParams, CancellationToken.None);

if (processResult.Item1 != 0) {
var msg = String.Format(
"Failed to compile WiX template, command invoked was: '{0} {1}'\n\nOutput was:\n{2}",
"candle.exe", candleParams, processResult.Item2);

throw new Exception(msg);
}

var lightParams = String.Format("-ext WixNetFxExtension -sval -out \"{0}\" \"{1}\"", wxsTarget.Replace(".wxs", ".msi"), wxsTarget.Replace(".wxs", ".wixobj"));
processResult = await Utility.InvokeProcessAsync(
Path.Combine(pathToWix, "light.exe"), lightParams, CancellationToken.None);

if (processResult.Item1 != 0) {
var msg = String.Format(
"Failed to link WiX template, command invoked was: '{0} {1}'\n\nOutput was:\n{2}",
"light.exe", lightParams, processResult.Item2);

throw new Exception(msg);
}

var toDelete = new[] {
wxsTarget,
wxsTarget.Replace(".wxs", ".wixobj"),
wxsTarget.Replace(".wxs", ".wixpdb"),
};

await Utility.ForEachAsync(toDelete, x => Utility.DeleteFileHarder(x));
}

static string pathToWixTools()
{
var ourPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);

// Same Directory? (i.e. released)
if (File.Exists(Path.Combine(ourPath, "candle.exe"))) {
return ourPath;
}

// Debug Mode (i.e. in vendor)
var debugPath = Path.Combine(ourPath, "..", "..", "..", "vendor", "wix", "candle.exe");
if (File.Exists(debugPath)) {
return Path.GetFullPath(debugPath);
}

throw new Exception("WiX tools can't be found");
}

static string getAppNameFromDirectory(string path = null)
{
path = path ?? Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
Expand Down
1 change: 1 addition & 0 deletions src/Update/Update.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
<Link>Properties\SolutionAssemblyInfo.cs</Link>
</Compile>
<Compile Include="AnimatedGifWindow.cs" />
<Compile Include="CopStache.cs" />
<Compile Include="Mono.Options\Options.cs" />
<Compile Include="NativeMethods.cs" />
<Compile Include="Program.cs" />
Expand Down
Binary file added vendor/wix/Microsoft.Deployment.Resources.dll
Binary file not shown.
Binary file not shown.
Binary file added vendor/wix/WixNetFxExtension.dll
Binary file not shown.
Binary file added vendor/wix/candle.exe
Binary file not shown.
18 changes: 18 additions & 0 deletions vendor/wix/candle.exe.config
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8" ?>
<!--
<copyright file="app.config" company="Outercurve Foundation">
Copyright (c) 2004, Outercurve Foundation.
This software is released under Microsoft Reciprocal License (MS-RL).
The license and further copyright text can be found in the file
LICENSE.TXT at the root directory of the distribution.
</copyright>
-->
<configuration>
<startup useLegacyV2RuntimeActivationPolicy="true">
<supportedRuntime version="v4.0" />
<supportedRuntime version="v2.0.50727" />
</startup>
<runtime>
<loadFromRemoteSources enabled="true"/>
</runtime>
</configuration>
Binary file added vendor/wix/darice.cub
Binary file not shown.
Binary file added vendor/wix/light.exe
Binary file not shown.
38 changes: 38 additions & 0 deletions vendor/wix/template.wxs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi" xmlns:util="http://schemas.microsoft.com/wix/UtilExtension" xmlns:netfx="http://schemas.microsoft.com/wix/NetFxExtension">
<Product Id="*" Name="{{Title}} Machine-Wide Installer" Language="1033" Version="1.2.4" UpgradeCode="{{RandomGuid}}" Manufacturer="{{Author}}">

<Package Description="#Description" Comments="Comments" InstallerVersion="200" Compressed="yes"/>
<Media Id="1" Cabinet="contents.cab" EmbedCab="yes" CompressionLevel="high"/>

<PropertyRef Id="NETFRAMEWORK45" />

<Condition Message="This application requires .NET Framework 4.5. Please install the .NET Framework then run this installer again.">
<![CDATA[Installed OR NETFRAMEWORK45]]>
</Condition>

<Directory Id="TARGETDIR" Name="SourceDir">
<Directory Id="ProgramFilesFolder">
<Directory Id="APPLICATIONROOTDIRECTORY" Name="{{Title}} Installer" />
</Directory>
</Directory>

<DirectoryRef Id="APPLICATIONROOTDIRECTORY">
<Component Id="{{Id}}.exe" Guid="08AD3BE1-EDA8-4182-BF8F-F47F805E28B8">
<File Id="{{Id}}.exe" Name="{{Id}}.exe" Source="./Setup.exe" KeyPath="yes" />
</Component>
</DirectoryRef>

<DirectoryRef Id="TARGETDIR">
<Component Id="RegistryEntries" Guid="4438A0C1-7F86-4737-B929-8476305E1183">
<RegistryKey Root="HKLM" Key="SOFTWARE\Microsoft\Windows\CurrentVersion\Run">
<RegistryValue Type="expandable" Name="{{Id}}MachineInstaller" Value="%ProgramFiles%\{{Title}} Installer\{{Id}}.exe --checkInstall" />
</RegistryKey>
</Component>
</DirectoryRef>

<Feature Id="MainApplication" Title="Main Application" Level="1">
<ComponentRef Id="{{Id}}.exe" />
<ComponentRef Id="RegistryEntries" />
</Feature>
</Product>
</Wix>
Binary file added vendor/wix/wconsole.dll
Binary file not shown.
Binary file added vendor/wix/winterop.dll
Binary file not shown.
Binary file added vendor/wix/wix.dll
Binary file not shown.

0 comments on commit b6ecc40

Please sign in to comment.