Skip to content

Commit

Permalink
[msi] support for downloading/installing .msi files (#50)
Browse files Browse the repository at this point in the history
Fixes: #31

This will help us in .NET 6 development.

We need to download and install .msi files as part of CI.

This also cleaned up a few other things:
* `NotSupportedException` would now be thrown on Linux.
* Better `async` usage in `PrintLogFileAndDelete()`
* Fixed a potential `System.FormatException` in Cake.Boots. This was happening with `.msi` files.
* Other general code cleanup.
  • Loading branch information
jonathanpeppers authored Oct 26, 2020
1 parent adc4986 commit 4bcfbb9
Show file tree
Hide file tree
Showing 11 changed files with 116 additions and 29 deletions.
23 changes: 20 additions & 3 deletions Boots.Core/Bootstrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ public class Bootstrapper

public Product? Product { get; set; }

public FileType? FileType { get; set; }

public string Url { get; set; } = "";

public TextWriter Logger { get; set; } = Console.Out;
Expand All @@ -35,10 +37,25 @@ public class Bootstrapper
}

Installer installer;
if (Helpers.IsWindows) {
installer = new VsixInstaller (this);
} else {
if (Helpers.IsMac) {
installer = new PkgInstaller (this);
} else if (Helpers.IsWindows) {
if (FileType == null) {
if (Url.EndsWith (".msi", StringComparison.OrdinalIgnoreCase)) {
FileType = global::FileType.msi;
Logger.WriteLine ("Inferring .msi from URL.");
} else if (Url.EndsWith (".vsix", StringComparison.OrdinalIgnoreCase)) {
FileType = global::FileType.vsix;
Logger.WriteLine ("Inferring .vsix from URL.");
}
}
if (FileType == global::FileType.msi) {
installer = new MsiInstaller (this);
} else {
installer = new VsixInstaller (this);
}
} else {
throw new NotSupportedException ("Unsupported platform, neither macOS or Windows detected.");
}

using (var downloader = new Downloader (this, installer.Extension)) {
Expand Down
6 changes: 6 additions & 0 deletions Boots.Core/FileType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
public enum FileType
{
vsix,
pkg,
msi
}
15 changes: 15 additions & 0 deletions Boots.Core/Installer.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.IO;
using System.Threading;
using System.Threading.Tasks;

Expand All @@ -15,5 +16,19 @@ public Installer (Bootstrapper boots)
public abstract string Extension { get; }

public abstract Task Install (string file, CancellationToken token = new CancellationToken ());

protected async Task PrintLogFileAndDelete (string log, CancellationToken token)
{
if (File.Exists (log)) {
using (var reader = File.OpenText (log)) {
while (!reader.EndOfStream && !token.IsCancellationRequested) {
Boots.Logger.WriteLine (await reader.ReadLineAsync ());
}
}
File.Delete (log);
} else {
Boots.Logger.WriteLine ($"Log file did not exist: {log}");
}
}
}
}
35 changes: 35 additions & 0 deletions Boots.Core/MsiInstaller.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;

namespace Boots.Core
{
class MsiInstaller : Installer
{
public MsiInstaller (Bootstrapper boots) : base (boots) { }

public override string Extension => ".msi";

public async override Task Install (string file, CancellationToken token = default)
{
if (string.IsNullOrEmpty (file))
throw new ArgumentException (nameof (file));
if (!File.Exists (file))
throw new FileNotFoundException ($"{Extension} file did not exist: {file}", file);

var log = Path.GetTempFileName ();
try {
using (var proc = new AsyncProcess (Boots) {
Command = "msiexec",
Arguments = $"/i \"{file}\" /qn /L*V \"{log}\"",
Elevate = true,
}) {
await proc.RunAsync (token);
}
} finally {
await PrintLogFileAndDelete (log, token);
}
}
}
}
2 changes: 1 addition & 1 deletion Boots.Core/PkgInstaller.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public PkgInstaller (Bootstrapper boots) : base (boots) { }

public override string Extension => ".pkg";

public async override Task Install (string file, CancellationToken token = new CancellationToken ())
public async override Task Install (string file, CancellationToken token = default)
{
if (string.IsNullOrEmpty (file))
throw new ArgumentException (nameof (file));
Expand Down
20 changes: 2 additions & 18 deletions Boots.Core/VsixInstaller.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public VsixInstaller (Bootstrapper boots) : base (boots) { }

public override string Extension => ".vsix";

public async override Task Install (string file, CancellationToken token = new CancellationToken ())
public async override Task Install (string file, CancellationToken token = default)
{
if (string.IsNullOrEmpty (file))
throw new ArgumentException (nameof (file));
Expand All @@ -41,26 +41,10 @@ public VsixInstaller (Bootstrapper boots) : base (boots) { }
}
}
} finally {
await ReadLogFile (log, token);
await PrintLogFileAndDelete (log, token);
}
}

Task ReadLogFile (string log, CancellationToken token)
{
return Task.Factory.StartNew (() => {
if (File.Exists (log)) {
using (var reader = File.OpenText (log)) {
while (!reader.EndOfStream) {
Boots.Logger.WriteLine (reader.ReadLine ());
}
}
File.Delete (log);
} else {
Boots.Logger.WriteLine ($"Log file did not exist: {log}");
}
}, token);
}

async Task<string> GetVisualStudioDirectory (CancellationToken token)
{
if (visualStudioDirectory != null)
Expand Down
11 changes: 11 additions & 0 deletions Boots.Tests/BootstrapperTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,17 @@ public async Task SimpleInstall ()
await boots.Install ();
}

[SkippableFact]
public async Task InstallMsi ()
{
Skip.If (!Helpers.IsWindows, ".msis are only supported on Windows");
boots.FileType = FileType.msi;
boots.Url = "https://download-installer.cdn.mozilla.net/pub/firefox/releases/82.0/win64/en-US/Firefox%20Setup%2082.0.msi";
await boots.Install ();
// Two installs back-to-back should be fine
await boots.Install ();
}

[SkippableFact]
public async Task InvalidInstallerFile ()
{
Expand Down
3 changes: 2 additions & 1 deletion Boots.Tests/InstallerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public class InstallerTests
[Theory]
[InlineData (typeof (VsixInstaller))]
[InlineData (typeof (PkgInstaller))]

[InlineData (typeof (MsiInstaller))]
public async Task NoFilePath (Type type)
{
var installer = (Installer) Activator.CreateInstance (type, new Bootstrapper ());
Expand All @@ -21,6 +21,7 @@ public async Task NoFilePath (Type type)
[Theory]
[InlineData (typeof (VsixInstaller))]
[InlineData (typeof (PkgInstaller))]
[InlineData (typeof (MsiInstaller))]
public async Task FileDoesNotExist (Type type)
{
var installer = (Installer) Activator.CreateInstance (type, new Bootstrapper ());
Expand Down
11 changes: 9 additions & 2 deletions Boots/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,17 @@ static async Task Main (string [] args)
{
Argument = new Argument<string>("product")
},
new Option(
"--file-type",
$"Specifies the type of file to be installed such as vsix, pkg, or msi. Defaults to vsix on Windows and pkg on macOS.")
{
Argument = new Argument<FileType>("file-type")
},
};
rootCommand.Name = "boots";
rootCommand.AddValidator (Validator);
rootCommand.Description = $"boots {Version} File issues at: https://github.com/jonathanpeppers/boots/issues";
rootCommand.Handler = CommandHandler.Create <string, string, string> (Run);
rootCommand.Handler = CommandHandler.Create <string, string, string, FileType?> (Run);
await rootCommand.InvokeAsync (args);
}

Expand All @@ -66,13 +72,14 @@ static string Validator (CommandResult result)
return "";
}

static async Task Run (string url, string stable = "", string preview = "")
static async Task Run (string url, string stable = "", string preview = "", FileType? fileType = null)
{
var cts = new CancellationTokenSource ();
Console.CancelKeyPress += (sender, e) => cts.Cancel ();

var boots = new Bootstrapper {
Url = url,
FileType = fileType,
};
SetChannelAndProduct (boots, preview, ReleaseChannel.Preview);
SetChannelAndProduct (boots, stable, ReleaseChannel.Stable);
Expand Down
10 changes: 8 additions & 2 deletions Cake.Boots/BootsAddin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@ namespace Cake.Boots
public static class BootsAddin
{
[CakeMethodAlias]
public static async Task Boots (this ICakeContext context, string url)
public static async Task Boots (this ICakeContext context, string url, FileType? fileType = default)
{
var boots = new Bootstrapper {
Url = url,
FileType = fileType,
Logger = new CakeWriter (context)
};

Expand Down Expand Up @@ -49,7 +50,12 @@ public CakeWriter (ICakeContext context)

public override void WriteLine (string value)
{
context.Log.Write (verbosity, level, value ?? "");
value ??= "";

// avoid System.FormatException from string.Format
value = value.Replace ("{", "{{").Replace ("}", "}}");

context.Log.Write (verbosity, level, value);
}

public override void WriteLine (string format, params object [] args)
Expand Down
9 changes: 7 additions & 2 deletions Cake.Boots/build.cake
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,13 @@ Task("Boots")

await Boots (url);

if (!IsRunningOnWindows()) {
//Let's really run through the gauntlet and install 6 .pkg files
if (IsRunningOnWindows()) {
// Install a Firefox .msi twice
var firefox = "https://download-installer.cdn.mozilla.net/pub/firefox/releases/82.0/win64/en-US/Firefox%20Setup%2082.0.msi";
await Boots (firefox);
await Boots (firefox, fileType: FileType.msi);
} else {
// Let's really run through the gauntlet and install 6 .pkg files
await Boots (Product.XamariniOS, ReleaseChannel.Stable);
await Boots (Product.XamarinMac, ReleaseChannel.Stable);
await Boots (Product.XamarinAndroid, ReleaseChannel.Stable);
Expand Down

0 comments on commit 4bcfbb9

Please sign in to comment.