Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[msis] add new feature for .msi installers #50

Merged
merged 1 commit into from
Oct 26, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -11,10 +11,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 @@ -48,7 +49,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