├── Cake.Boots ├── .gitignore ├── tools │ └── packages.config ├── BootsSettings.cs ├── Cake.Boots.csproj ├── build.cake └── BootsAddin.cs ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ └── actions.yml ├── icon.png ├── .vscode ├── settings.json ├── launch.json └── tasks.json ├── global.json ├── .gitattributes ├── Boots.Core ├── FileType.cs ├── Product.cs ├── Boots.Core.csproj ├── Helpers.cs ├── UrlResolver.cs ├── ReleaseChannel.cs ├── PkgInstaller.cs ├── Installer.cs ├── MsiInstaller.cs ├── Downloader.cs ├── MacUrlResolver.cs ├── HttpClientWithPolicy.cs ├── Bootstrapper.cs ├── AsyncProcess.cs ├── WindowsUrlResolver.cs └── VsixInstaller.cs ├── docs ├── AppCenter.png ├── GetMonoVersion.png ├── GetMacIosVersion.png ├── GetAndroidVersion.png └── HowToFindBuilds.md ├── samples ├── HelloForms │ ├── AssemblyInfo.cs │ ├── HelloForms.csproj │ ├── App.xaml │ ├── MainPage.xaml.cs │ ├── App.xaml.cs │ └── MainPage.xaml ├── HelloForms.iOS │ ├── Resources │ │ ├── Default.png │ │ ├── Default@2x.png │ │ ├── Default-568h@2x.png │ │ ├── Default-Portrait.png │ │ ├── Default-Portrait@2x.png │ │ └── LaunchScreen.storyboard │ ├── Assets.xcassets │ │ └── AppIcon.appiconset │ │ │ ├── Icon20.png │ │ │ ├── Icon29.png │ │ │ ├── Icon40.png │ │ │ ├── Icon58.png │ │ │ ├── Icon60.png │ │ │ ├── Icon76.png │ │ │ ├── Icon80.png │ │ │ ├── Icon87.png │ │ │ ├── Icon1024.png │ │ │ ├── Icon120.png │ │ │ ├── Icon152.png │ │ │ ├── Icon167.png │ │ │ ├── Icon180.png │ │ │ └── Contents.json │ ├── Entitlements.plist │ ├── Main.cs │ ├── AppDelegate.cs │ ├── Info.plist │ ├── Properties │ │ └── AssemblyInfo.cs │ └── HelloForms.iOS.csproj ├── HelloForms.Android │ ├── Resources │ │ ├── mipmap-hdpi │ │ │ ├── icon.png │ │ │ └── launcher_foreground.png │ │ ├── mipmap-mdpi │ │ │ ├── icon.png │ │ │ └── launcher_foreground.png │ │ ├── mipmap-xhdpi │ │ │ ├── icon.png │ │ │ └── launcher_foreground.png │ │ ├── mipmap-xxhdpi │ │ │ ├── icon.png │ │ │ └── launcher_foreground.png │ │ ├── mipmap-xxxhdpi │ │ │ ├── icon.png │ │ │ └── launcher_foreground.png │ │ ├── mipmap-anydpi-v26 │ │ │ ├── icon.xml │ │ │ └── icon_round.xml │ │ ├── values │ │ │ ├── colors.xml │ │ │ └── styles.xml │ │ └── AboutResources.txt │ ├── appcenter-pre-build.sh │ ├── Properties │ │ ├── AndroidManifest.xml │ │ └── AssemblyInfo.cs │ ├── Assets │ │ └── AboutAssets.txt │ ├── MainActivity.cs │ └── HelloForms.Android.csproj └── HelloForms.sln ├── nuget.config ├── appveyor.yml ├── scripts ├── xa-version.targets └── build-and-test.yaml ├── Boots ├── Boots.csproj └── Program.cs ├── Boots.Tests ├── Boots.Tests.csproj ├── Utils.cs ├── InstallerTests.cs ├── DownloaderTests.cs ├── MainTests.cs ├── AsyncProcessTests.cs ├── BootstrapperTests.cs ├── UrlResolverTests.cs └── HttpClientWithPolicyTests.cs ├── azure-pipelines.yml ├── LICENSE ├── bitrise.yml ├── Directory.Build.targets ├── Directory.Build.props ├── boots.sln ├── .gitignore ├── .editorconfig └── README.md /Cake.Boots/.gitignore: -------------------------------------------------------------------------------- 1 | tools -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [jonathanpeppers] 2 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathanpeppers/boots/HEAD/icon.png -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "dotnet.defaultSolution": "boots.sln" 3 | } -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "rollForward": "major" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /Boots.Core/FileType.cs: -------------------------------------------------------------------------------- 1 | public enum FileType 2 | { 3 | vsix, 4 | pkg, 5 | msi 6 | } 7 | -------------------------------------------------------------------------------- /docs/AppCenter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathanpeppers/boots/HEAD/docs/AppCenter.png -------------------------------------------------------------------------------- /docs/GetMonoVersion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathanpeppers/boots/HEAD/docs/GetMonoVersion.png -------------------------------------------------------------------------------- /docs/GetMacIosVersion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathanpeppers/boots/HEAD/docs/GetMacIosVersion.png -------------------------------------------------------------------------------- /docs/GetAndroidVersion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathanpeppers/boots/HEAD/docs/GetAndroidVersion.png -------------------------------------------------------------------------------- /Boots.Core/Product.cs: -------------------------------------------------------------------------------- 1 | public enum Product 2 | { 3 | Mono, 4 | XamarinAndroid, 5 | XamariniOS, 6 | XamarinMac, 7 | } 8 | -------------------------------------------------------------------------------- /samples/HelloForms/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using Xamarin.Forms.Xaml; 2 | 3 | [assembly: XamlCompilation (XamlCompilationOptions.Compile)] -------------------------------------------------------------------------------- /samples/HelloForms.iOS/Resources/Default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathanpeppers/boots/HEAD/samples/HelloForms.iOS/Resources/Default.png -------------------------------------------------------------------------------- /Cake.Boots/tools/packages.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /samples/HelloForms.iOS/Resources/Default@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathanpeppers/boots/HEAD/samples/HelloForms.iOS/Resources/Default@2x.png -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "nuget" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | -------------------------------------------------------------------------------- /samples/HelloForms.iOS/Resources/Default-568h@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathanpeppers/boots/HEAD/samples/HelloForms.iOS/Resources/Default-568h@2x.png -------------------------------------------------------------------------------- /samples/HelloForms.iOS/Resources/Default-Portrait.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathanpeppers/boots/HEAD/samples/HelloForms.iOS/Resources/Default-Portrait.png -------------------------------------------------------------------------------- /samples/HelloForms.Android/Resources/mipmap-hdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathanpeppers/boots/HEAD/samples/HelloForms.Android/Resources/mipmap-hdpi/icon.png -------------------------------------------------------------------------------- /samples/HelloForms.Android/Resources/mipmap-mdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathanpeppers/boots/HEAD/samples/HelloForms.Android/Resources/mipmap-mdpi/icon.png -------------------------------------------------------------------------------- /samples/HelloForms.Android/Resources/mipmap-xhdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathanpeppers/boots/HEAD/samples/HelloForms.Android/Resources/mipmap-xhdpi/icon.png -------------------------------------------------------------------------------- /samples/HelloForms.iOS/Resources/Default-Portrait@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathanpeppers/boots/HEAD/samples/HelloForms.iOS/Resources/Default-Portrait@2x.png -------------------------------------------------------------------------------- /samples/HelloForms.Android/Resources/mipmap-xxhdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathanpeppers/boots/HEAD/samples/HelloForms.Android/Resources/mipmap-xxhdpi/icon.png -------------------------------------------------------------------------------- /samples/HelloForms.Android/Resources/mipmap-xxxhdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathanpeppers/boots/HEAD/samples/HelloForms.Android/Resources/mipmap-xxxhdpi/icon.png -------------------------------------------------------------------------------- /samples/HelloForms.iOS/Assets.xcassets/AppIcon.appiconset/Icon20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathanpeppers/boots/HEAD/samples/HelloForms.iOS/Assets.xcassets/AppIcon.appiconset/Icon20.png -------------------------------------------------------------------------------- /samples/HelloForms.iOS/Assets.xcassets/AppIcon.appiconset/Icon29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathanpeppers/boots/HEAD/samples/HelloForms.iOS/Assets.xcassets/AppIcon.appiconset/Icon29.png -------------------------------------------------------------------------------- /samples/HelloForms.iOS/Assets.xcassets/AppIcon.appiconset/Icon40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathanpeppers/boots/HEAD/samples/HelloForms.iOS/Assets.xcassets/AppIcon.appiconset/Icon40.png -------------------------------------------------------------------------------- /samples/HelloForms.iOS/Assets.xcassets/AppIcon.appiconset/Icon58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathanpeppers/boots/HEAD/samples/HelloForms.iOS/Assets.xcassets/AppIcon.appiconset/Icon58.png -------------------------------------------------------------------------------- /samples/HelloForms.iOS/Assets.xcassets/AppIcon.appiconset/Icon60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathanpeppers/boots/HEAD/samples/HelloForms.iOS/Assets.xcassets/AppIcon.appiconset/Icon60.png -------------------------------------------------------------------------------- /samples/HelloForms.iOS/Assets.xcassets/AppIcon.appiconset/Icon76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathanpeppers/boots/HEAD/samples/HelloForms.iOS/Assets.xcassets/AppIcon.appiconset/Icon76.png -------------------------------------------------------------------------------- /samples/HelloForms.iOS/Assets.xcassets/AppIcon.appiconset/Icon80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathanpeppers/boots/HEAD/samples/HelloForms.iOS/Assets.xcassets/AppIcon.appiconset/Icon80.png -------------------------------------------------------------------------------- /samples/HelloForms.iOS/Assets.xcassets/AppIcon.appiconset/Icon87.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathanpeppers/boots/HEAD/samples/HelloForms.iOS/Assets.xcassets/AppIcon.appiconset/Icon87.png -------------------------------------------------------------------------------- /samples/HelloForms.iOS/Assets.xcassets/AppIcon.appiconset/Icon1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathanpeppers/boots/HEAD/samples/HelloForms.iOS/Assets.xcassets/AppIcon.appiconset/Icon1024.png -------------------------------------------------------------------------------- /samples/HelloForms.iOS/Assets.xcassets/AppIcon.appiconset/Icon120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathanpeppers/boots/HEAD/samples/HelloForms.iOS/Assets.xcassets/AppIcon.appiconset/Icon120.png -------------------------------------------------------------------------------- /samples/HelloForms.iOS/Assets.xcassets/AppIcon.appiconset/Icon152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathanpeppers/boots/HEAD/samples/HelloForms.iOS/Assets.xcassets/AppIcon.appiconset/Icon152.png -------------------------------------------------------------------------------- /samples/HelloForms.iOS/Assets.xcassets/AppIcon.appiconset/Icon167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathanpeppers/boots/HEAD/samples/HelloForms.iOS/Assets.xcassets/AppIcon.appiconset/Icon167.png -------------------------------------------------------------------------------- /samples/HelloForms.iOS/Assets.xcassets/AppIcon.appiconset/Icon180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathanpeppers/boots/HEAD/samples/HelloForms.iOS/Assets.xcassets/AppIcon.appiconset/Icon180.png -------------------------------------------------------------------------------- /samples/HelloForms.Android/Resources/mipmap-hdpi/launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathanpeppers/boots/HEAD/samples/HelloForms.Android/Resources/mipmap-hdpi/launcher_foreground.png -------------------------------------------------------------------------------- /samples/HelloForms.Android/Resources/mipmap-mdpi/launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathanpeppers/boots/HEAD/samples/HelloForms.Android/Resources/mipmap-mdpi/launcher_foreground.png -------------------------------------------------------------------------------- /samples/HelloForms.Android/Resources/mipmap-xhdpi/launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathanpeppers/boots/HEAD/samples/HelloForms.Android/Resources/mipmap-xhdpi/launcher_foreground.png -------------------------------------------------------------------------------- /samples/HelloForms.Android/Resources/mipmap-xxhdpi/launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathanpeppers/boots/HEAD/samples/HelloForms.Android/Resources/mipmap-xxhdpi/launcher_foreground.png -------------------------------------------------------------------------------- /samples/HelloForms.Android/Resources/mipmap-xxxhdpi/launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathanpeppers/boots/HEAD/samples/HelloForms.Android/Resources/mipmap-xxxhdpi/launcher_foreground.png -------------------------------------------------------------------------------- /nuget.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /samples/HelloForms.iOS/Entitlements.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /samples/HelloForms.Android/Resources/mipmap-anydpi-v26/icon.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /samples/HelloForms.Android/Resources/mipmap-anydpi-v26/icon_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /samples/HelloForms.Android/Resources/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFFFFF 4 | #3F51B5 5 | #303F9F 6 | #FF4081 7 | 8 | -------------------------------------------------------------------------------- /Boots.Core/Boots.Core.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /samples/HelloForms.Android/appcenter-pre-build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # App Center custom build scripts: https://aka.ms/docs/build/custom/scripts 3 | 4 | export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=true 5 | 6 | dotnet tool install --global boots --version 1.0.2.421 7 | boots --preview Mono 8 | boots --preview XamarinAndroid 9 | boots --preview XamariniOS 10 | -------------------------------------------------------------------------------- /samples/HelloForms/HelloForms.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netstandard2.0 4 | true 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /Boots.Core/Helpers.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace Boots.Core 4 | { 5 | static class Helpers 6 | { 7 | public static bool IsWindows => RuntimeInformation.IsOSPlatform (OSPlatform.Windows); 8 | 9 | public static bool IsMac => RuntimeInformation.IsOSPlatform (OSPlatform.OSX); 10 | 11 | public static bool IsLinux => RuntimeInformation.IsOSPlatform (OSPlatform.Linux); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Cake.Boots/BootsSettings.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | public class BootsSettings 4 | { 5 | public TimeSpan? Timeout { get; set; } 6 | 7 | public TimeSpan? ReadWriteTimeout { get; set; } 8 | 9 | public int? NetworkRetries { get; set; } 10 | 11 | public ReleaseChannel? Channel { get; set; } 12 | 13 | public Product? Product { get; set; } 14 | 15 | public FileType? FileType { get; set; } 16 | 17 | public string? Url { get; set; } 18 | } 19 | -------------------------------------------------------------------------------- /Boots.Core/UrlResolver.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | 4 | namespace Boots.Core 5 | { 6 | abstract class UrlResolver 7 | { 8 | protected readonly Bootstrapper Boots; 9 | 10 | public UrlResolver (Bootstrapper boots) 11 | { 12 | Boots = boots; 13 | } 14 | 15 | public abstract Task Resolve (ReleaseChannel channel, Product product, CancellationToken token = new CancellationToken ()); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /samples/HelloForms.Android/Properties/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: '{build}' 2 | branches: 3 | only: 4 | - main 5 | - /.+appveyor.+/ 6 | image: macOS 7 | test: false 8 | environment: 9 | Configuration: Release 10 | Verbosity: Diagnostic 11 | build_script: 12 | - sh: >- 13 | export PATH="$PATH:~/.dotnet/tools" && 14 | dotnet tool update --global Cake.Tool && 15 | dotnet build Cake.Boots/Cake.Boots.csproj && 16 | cd Cake.Boots && 17 | dotnet cake --target=Mono --verbosity=$Verbosity && 18 | dotnet cake --verbosity=$Verbosity 19 | -------------------------------------------------------------------------------- /samples/HelloForms/App.xaml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Boots.Core/ReleaseChannel.cs: -------------------------------------------------------------------------------- 1 | public enum ReleaseChannel 2 | { 3 | /// 4 | /// The "stable" channel, or https://aka.ms/vs/17/release/channel on Windows, and "Stable" on Mac. 5 | /// 6 | Stable, 7 | /// 8 | /// The "preview" channel, or https://aka.ms/vs/17/pre/channel on Windows, and "Beta" on Mac. 9 | /// 10 | Preview, 11 | /// 12 | /// This channel is only valid for Visual Studio for Mac, it resolves to Preview on Windows. 13 | /// 14 | Alpha, 15 | } 16 | -------------------------------------------------------------------------------- /samples/HelloForms/MainPage.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using Xamarin.Forms; 8 | 9 | namespace HelloForms 10 | { 11 | // Learn more about making custom code visible in the Xamarin.Forms previewer 12 | // by visiting https://aka.ms/xamarinforms-previewer 13 | [DesignTimeVisible (false)] 14 | public partial class MainPage : ContentPage 15 | { 16 | public MainPage () 17 | { 18 | InitializeComponent (); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /samples/HelloForms.iOS/Main.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | using Foundation; 6 | using UIKit; 7 | 8 | namespace HelloForms.iOS 9 | { 10 | public class Application 11 | { 12 | // This is the main entry point of the application. 13 | static void Main(string[] args) 14 | { 15 | // if you want to use a different Application Delegate class from "AppDelegate" 16 | // you can specify it here. 17 | UIApplication.Main(args, null, "AppDelegate"); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /samples/HelloForms/App.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Xamarin.Forms; 3 | using Xamarin.Forms.Xaml; 4 | 5 | namespace HelloForms 6 | { 7 | public partial class App : Application 8 | { 9 | public App () 10 | { 11 | InitializeComponent (); 12 | 13 | MainPage = new MainPage (); 14 | } 15 | 16 | protected override void OnStart () 17 | { 18 | // Handle when your app starts 19 | } 20 | 21 | protected override void OnSleep () 22 | { 23 | // Handle when your app sleeps 24 | } 25 | 26 | protected override void OnResume () 27 | { 28 | // Handle when your app resumes 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /scripts/xa-version.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 13 | 17 | 18 | -------------------------------------------------------------------------------- /samples/HelloForms/MainPage.xaml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /Boots/Boots.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp3.1 6 | Major 7 | true 8 | boots 9 | boots 10 | false 11 | true 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /samples/HelloForms.Android/Assets/AboutAssets.txt: -------------------------------------------------------------------------------- 1 | Any raw assets you want to be deployed with your application can be placed in 2 | this directory (and child directories) and given a Build Action of "AndroidAsset". 3 | 4 | These files will be deployed with you package and will be accessible using Android's 5 | AssetManager, like this: 6 | 7 | public class ReadAsset : Activity 8 | { 9 | protected override void OnCreate (Bundle bundle) 10 | { 11 | base.OnCreate (bundle); 12 | 13 | InputStream input = Assets.Open ("my_asset.txt"); 14 | } 15 | } 16 | 17 | Additionally, some Android functions will automatically load asset files: 18 | 19 | Typeface tf = Typeface.CreateFromAsset (Context.Assets, "fonts/samplefont.ttf"); 20 | -------------------------------------------------------------------------------- /Boots.Tests/Boots.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | false 6 | disable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /Boots.Tests/Utils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Text; 4 | using Xunit.Abstractions; 5 | 6 | namespace Boots.Tests 7 | { 8 | public class TestWriter : TextWriter 9 | { 10 | readonly ITestOutputHelper output; 11 | 12 | public TestWriter (ITestOutputHelper output) 13 | { 14 | this.output = output; 15 | } 16 | 17 | public override Encoding Encoding => Encoding.Default; 18 | 19 | public override void WriteLine (string value) 20 | { 21 | if (value != null) { 22 | output.WriteLine (value); 23 | Console.WriteLine (value); 24 | } 25 | } 26 | 27 | public override void WriteLine (string format, params object [] args) 28 | { 29 | output.WriteLine (format, args); 30 | Console.WriteLine (format, args); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": ".NET Core Launch (console)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | "program": "${workspaceFolder}/Boots/bin/Debug/netcoreapp3.1/Boots.dll", 13 | "args": [], 14 | "cwd": "${workspaceFolder}/Boots", 15 | "console": "internalConsole", 16 | "stopAtEntry": false 17 | }, 18 | { 19 | "name": ".NET Core Attach", 20 | "type": "coreclr", 21 | "request": "attach" 22 | } 23 | ] 24 | } -------------------------------------------------------------------------------- /Boots.Core/PkgInstaller.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace Boots.Core 7 | { 8 | class PkgInstaller : Installer 9 | { 10 | public PkgInstaller (Bootstrapper boots) : base (boots) { } 11 | 12 | public override string Extension => ".pkg"; 13 | 14 | public async override Task Install (string file, CancellationToken token = default) 15 | { 16 | if (string.IsNullOrEmpty (file)) 17 | throw new ArgumentException (nameof (file)); 18 | if (!File.Exists (file)) 19 | throw new FileNotFoundException ($"{Extension} file did not exist: {file}", file); 20 | 21 | using var proc = new AsyncProcess (Boots) { 22 | Command = "/usr/sbin/installer", 23 | Arguments = $"-verbose -dumplog -pkg \"{file}\" -target /", 24 | Elevate = true, 25 | }; 26 | await proc.RunAsync (token); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Boots.Core/Installer.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace Boots.Core 6 | { 7 | abstract class Installer 8 | { 9 | protected readonly Bootstrapper Boots; 10 | 11 | public Installer (Bootstrapper boots) 12 | { 13 | Boots = boots; 14 | } 15 | 16 | public abstract string Extension { get; } 17 | 18 | public abstract Task Install (string file, CancellationToken token = new CancellationToken ()); 19 | 20 | protected async Task PrintLogFileAndDelete (string log, CancellationToken token) 21 | { 22 | if (File.Exists (log)) { 23 | using (var reader = File.OpenText (log)) { 24 | while (!reader.EndOfStream && !token.IsCancellationRequested) { 25 | Boots.Logger.WriteLine (await reader.ReadLineAsync ()); 26 | } 27 | } 28 | File.Delete (log); 29 | } else { 30 | Boots.Logger.WriteLine ($"Log file did not exist: {log}"); 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Boots.Tests/InstallerTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Threading.Tasks; 4 | using Boots.Core; 5 | using Xunit; 6 | 7 | namespace Boots.Tests 8 | { 9 | public class InstallerTests 10 | { 11 | [Theory] 12 | [InlineData (typeof (VsixInstaller))] 13 | [InlineData (typeof (PkgInstaller))] 14 | [InlineData (typeof (MsiInstaller))] 15 | public async Task NoFilePath (Type type) 16 | { 17 | var installer = (Installer) Activator.CreateInstance (type, new Bootstrapper ()); 18 | await Assert.ThrowsAsync (() => installer.Install (null)); 19 | } 20 | 21 | [Theory] 22 | [InlineData (typeof (VsixInstaller))] 23 | [InlineData (typeof (PkgInstaller))] 24 | [InlineData (typeof (MsiInstaller))] 25 | public async Task FileDoesNotExist (Type type) 26 | { 27 | var installer = (Installer) Activator.CreateInstance (type, new Bootstrapper ()); 28 | await Assert.ThrowsAsync (() => installer.Install ("asdf")); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Boots.Core/MsiInstaller.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace Boots.Core 7 | { 8 | class MsiInstaller : Installer 9 | { 10 | public MsiInstaller (Bootstrapper boots) : base (boots) { } 11 | 12 | public override string Extension => ".msi"; 13 | 14 | public async override Task Install (string file, CancellationToken token = default) 15 | { 16 | if (string.IsNullOrEmpty (file)) 17 | throw new ArgumentException (nameof (file)); 18 | if (!File.Exists (file)) 19 | throw new FileNotFoundException ($"{Extension} file did not exist: {file}", file); 20 | 21 | var log = Path.GetTempFileName (); 22 | try { 23 | using var proc = new AsyncProcess (Boots) { 24 | Command = "msiexec", 25 | Arguments = $"/i \"{file}\" /qn /L*V \"{log}\"", 26 | Elevate = true, 27 | }; 28 | await proc.RunAsync (token); 29 | } finally { 30 | await PrintLogFileAndDelete (log, token); 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Boots.Core/Downloader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Net.Http; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace Boots.Core 8 | { 9 | public class Downloader : IDisposable 10 | { 11 | readonly Bootstrapper boots; 12 | readonly Uri uri; 13 | 14 | public Downloader (Bootstrapper boots, string extension = "") 15 | { 16 | this.boots = boots; 17 | uri = new Uri (boots.Url); 18 | 19 | TempFile = Path.Combine (Path.GetTempPath (), Path.GetRandomFileName () + extension); 20 | } 21 | 22 | public string TempFile { get; private set; } 23 | 24 | public async Task Download (CancellationToken token = new CancellationToken ()) 25 | { 26 | boots.Logger.WriteLine ($"Downloading {uri}"); 27 | using var client = new HttpClientWithPolicy (boots); 28 | await client.DownloadAsync (uri, TempFile, token); 29 | } 30 | 31 | public void Dispose () 32 | { 33 | if (File.Exists (TempFile)) { 34 | boots.Logger.WriteLine ($"Deleting {TempFile}"); 35 | File.Delete (TempFile); 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | 2 | # https://aka.ms/yaml 3 | 4 | name: $(BuildID) 5 | 6 | trigger: 7 | - main 8 | 9 | variables: 10 | Configuration: Release 11 | BootsVersion: 1.1.0 12 | BootsSuffix: '' 13 | PackageVersion: $(BootsVersion).$(Build.BuildNumber)$(BootsSuffix) 14 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true 15 | DOTNET_CLI_TELEMETRY_OPTOUT: true 16 | 17 | jobs: 18 | 19 | - job: windows 20 | pool: 21 | vmImage: windows-2022 22 | demands: msbuild 23 | steps: 24 | 25 | - template: scripts/build-and-test.yaml 26 | parameters: 27 | name: windows 28 | 29 | - powershell: dotnet cake 30 | displayName: Cake test 31 | workingDirectory: Cake.Boots 32 | 33 | - job: mac 34 | pool: 35 | vmImage: macOS-latest 36 | demands: msbuild 37 | steps: 38 | 39 | - script: echo '##vso[task.setvariable variable=JI_JAVA_HOME]$(JAVA_HOME_11_X64)' 40 | displayName: set JI_JAVA_HOME 41 | 42 | - template: scripts/build-and-test.yaml 43 | parameters: 44 | name: mac 45 | 46 | - bash: dotnet cake --target=Mono && dotnet cake 47 | workingDirectory: Cake.Boots 48 | displayName: Cake test 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Jonathan Peppers 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /samples/HelloForms.iOS/AppDelegate.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | using Foundation; 6 | using UIKit; 7 | 8 | namespace HelloForms.iOS 9 | { 10 | // The UIApplicationDelegate for the application. This class is responsible for launching the 11 | // User Interface of the application, as well as listening (and optionally responding) to 12 | // application events from iOS. 13 | [Register("AppDelegate")] 14 | public partial class AppDelegate : global::Xamarin.Forms.Platform.iOS.FormsApplicationDelegate 15 | { 16 | // 17 | // This method is invoked when the application has loaded and is ready to run. In this 18 | // method you should instantiate the window, load the UI into it and then make the window 19 | // visible. 20 | // 21 | // You have 17 seconds to return from this method, or iOS will terminate your application. 22 | // 23 | public override bool FinishedLaunching(UIApplication app, NSDictionary options) 24 | { 25 | global::Xamarin.Forms.Forms.Init(); 26 | LoadApplication(new App()); 27 | 28 | return base.FinishedLaunching(app, options); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/boots.sln", 11 | "/property:GenerateFullPaths=true", 12 | "/consoleloggerparameters:NoSummary" 13 | ], 14 | "problemMatcher": "$msCompile" 15 | }, 16 | { 17 | "label": "publish", 18 | "command": "dotnet", 19 | "type": "process", 20 | "args": [ 21 | "publish", 22 | "${workspaceFolder}/boots.sln", 23 | "/property:GenerateFullPaths=true", 24 | "/consoleloggerparameters:NoSummary" 25 | ], 26 | "problemMatcher": "$msCompile" 27 | }, 28 | { 29 | "label": "watch", 30 | "command": "dotnet", 31 | "type": "process", 32 | "args": [ 33 | "watch", 34 | "run", 35 | "--project", 36 | "${workspaceFolder}/boots.sln" 37 | ], 38 | "problemMatcher": "$msCompile" 39 | } 40 | ] 41 | } -------------------------------------------------------------------------------- /samples/HelloForms.Android/MainActivity.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using Android.App; 4 | using Android.Content.PM; 5 | using Android.Runtime; 6 | using Android.Views; 7 | using Android.Widget; 8 | using Android.OS; 9 | 10 | namespace HelloForms.Droid 11 | { 12 | [Activity(Label = "HelloForms", Icon = "@mipmap/icon", Theme = "@style/MainTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation)] 13 | public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsAppCompatActivity 14 | { 15 | protected override void OnCreate(Bundle savedInstanceState) 16 | { 17 | base.OnCreate(savedInstanceState); 18 | 19 | Xamarin.Essentials.Platform.Init(this, savedInstanceState); 20 | global::Xamarin.Forms.Forms.Init(this, savedInstanceState); 21 | LoadApplication(new App()); 22 | } 23 | 24 | public override void OnRequestPermissionsResult(int requestCode, string[] permissions, [GeneratedEnum] Android.Content.PM.Permission[] grantResults) 25 | { 26 | Xamarin.Essentials.Platform.OnRequestPermissionsResult(requestCode, permissions, grantResults); 27 | 28 | base.OnRequestPermissionsResult(requestCode, permissions, grantResults); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.github/workflows/actions.yml: -------------------------------------------------------------------------------- 1 | name: GitHub Actions 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | paths: 11 | - '.github/**' 12 | 13 | jobs: 14 | macOS: 15 | runs-on: macOS-latest 16 | env: 17 | DOTNET_CLI_TELEMETRY_OPTOUT: 'true' 18 | steps: 19 | - uses: actions/checkout@v1 20 | - uses: actions/setup-dotnet@v1 21 | with: 22 | dotnet-version: '3.0.x' 23 | - name: build 24 | shell: bash 25 | run: | 26 | export PATH="$PATH:~/.dotnet/tools" 27 | dotnet tool install --global boots 28 | boots --preview Mono 29 | boots --preview XamarinAndroid 30 | export JI_JAVA_HOME="$JAVA_HOME_11_X64" 31 | msbuild ./samples/HelloForms.Android/HelloForms.Android.csproj /restore /t:SignAndroidPackage 32 | 33 | windows: 34 | runs-on: windows-latest 35 | env: 36 | DOTNET_CLI_TELEMETRY_OPTOUT: 'true' 37 | steps: 38 | - uses: actions/checkout@v1 39 | - uses: microsoft/setup-msbuild@v1.0.2 40 | - name: build 41 | shell: pwsh 42 | run: | 43 | dotnet tool install --global boots 44 | boots --preview XamarinAndroid 45 | msbuild ./samples/HelloForms.Android/HelloForms.Android.csproj /restore /t:SignAndroidPackage 46 | -------------------------------------------------------------------------------- /bitrise.yml: -------------------------------------------------------------------------------- 1 | --- 2 | format_version: '8' 3 | default_step_lib_source: https://github.com/bitrise-io/bitrise-steplib.git 4 | project_type: xamarin 5 | trigger_map: 6 | - push_branch: main 7 | workflow: primary 8 | workflows: 9 | primary: 10 | steps: 11 | - activate-ssh-key@4.0.5: 12 | run_if: '{{getenv "SSH_RSA_PRIVATE_KEY" | ne ""}}' 13 | - git-clone@4.0.25: {} 14 | - set-java-version@1: 15 | inputs: 16 | - set_java_version: '8' 17 | - android-sdk-update@1: 18 | inputs: 19 | - sdk_version: '30' 20 | - script@1.1.6: 21 | title: Do anything with Script step 22 | inputs: 23 | - content: |- 24 | #!/usr/bin/env bash 25 | set -e 26 | set -x 27 | dotnet tool install --global boots 28 | boots --stable Mono 29 | boots --preview Xamarin.Android 30 | msbuild ./samples/HelloForms.Android/HelloForms.Android.csproj -restore -t:SignAndroidPackage 31 | - deploy-to-bitrise-io@1.12.0: {} 32 | app: 33 | envs: 34 | - opts: 35 | is_expand: false 36 | BITRISE_PROJECT_PATH: samples/HelloForms.sln 37 | - opts: 38 | is_expand: false 39 | BITRISE_XAMARIN_CONFIGURATION: Release 40 | - opts: 41 | is_expand: false 42 | BITRISE_XAMARIN_PLATFORM: Any CPU 43 | -------------------------------------------------------------------------------- /Boots.Tests/DownloaderTests.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Threading.Tasks; 3 | using Boots.Core; 4 | using Xunit; 5 | 6 | namespace Boots.Tests 7 | { 8 | public class DownloaderTests 9 | { 10 | [Fact] 11 | public async Task Get () 12 | { 13 | string tempFile; 14 | using (var downloader = new Downloader (new Bootstrapper { 15 | Url = "http://httpbin.org/json", 16 | })) { 17 | tempFile = downloader.TempFile; 18 | Assert.False (File.Exists (tempFile), $"{tempFile} should *not* exist!"); 19 | await downloader.Download (); 20 | Assert.True (File.Exists (tempFile), $"{tempFile} should exist!"); 21 | } 22 | Assert.False (File.Exists (tempFile), $"{tempFile} should *not* exist!"); 23 | } 24 | 25 | [Fact] 26 | public void VsixFilePath () 27 | { 28 | var downloader = new Downloader (new Bootstrapper { 29 | Url = "https://marketplace.visualstudio.com/_apis/public/gallery/publishers/VisualStudioProductTeam/vsextensions/ProjectSystemTools/1.0.1.1927902/vspackage" 30 | }, ".vsix"); 31 | Assert.Equal (".vsix", Path.GetExtension (downloader.TempFile)); 32 | } 33 | 34 | [Fact] 35 | public void PkgFilePath () 36 | { 37 | var downloader = new Downloader (new Bootstrapper { 38 | Url = "https://aka.ms/objective-sharpie" 39 | }, ".pkg"); 40 | Assert.Equal (".pkg", Path.GetExtension (downloader.TempFile)); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /samples/HelloForms.Android/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | using Android.App; 5 | 6 | // General Information about an assembly is controlled through the following 7 | // set of attributes. Change these attribute values to modify the information 8 | // associated with an assembly. 9 | [assembly: AssemblyTitle("HelloForms.Android")] 10 | [assembly: AssemblyDescription("")] 11 | [assembly: AssemblyConfiguration("")] 12 | [assembly: AssemblyCompany("")] 13 | [assembly: AssemblyProduct("HelloForms.Android")] 14 | [assembly: AssemblyCopyright("Copyright © 2014")] 15 | [assembly: AssemblyTrademark("")] 16 | [assembly: AssemblyCulture("")] 17 | [assembly: ComVisible(false)] 18 | 19 | // Version information for an assembly consists of the following four values: 20 | // 21 | // Major Version 22 | // Minor Version 23 | // Build Number 24 | // Revision 25 | // 26 | // You can specify all the values or you can default the Build and Revision Numbers 27 | // by using the '*' as shown below: 28 | // [assembly: AssemblyVersion("1.0.*")] 29 | [assembly: AssemblyVersion("1.0.0.0")] 30 | [assembly: AssemblyFileVersion("1.0.0.0")] 31 | 32 | // Add some common permissions, these can be removed if not needed 33 | [assembly: UsesPermission(Android.Manifest.Permission.Internet)] 34 | [assembly: UsesPermission(Android.Manifest.Permission.WriteExternalStorage)] 35 | -------------------------------------------------------------------------------- /Boots.Tests/MainTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Reflection; 4 | using System.Threading.Tasks; 5 | using Xunit; 6 | 7 | namespace Boots.Tests 8 | { 9 | public class MainTests : IDisposable 10 | { 11 | const string DefaultErrorMessage = "At least one of --url, --stable, --preview, or --alpha must be used"; 12 | readonly TextWriter consoleError; 13 | readonly StringWriter stderr; 14 | readonly MethodInfo main; 15 | 16 | public MainTests () 17 | { 18 | consoleError = Console.Error; 19 | Console.SetError (stderr = new StringWriter ()); 20 | 21 | var program = Type.GetType ("Boots.Program, Boots", throwOnError: true); 22 | main = program.GetMethod ("Main", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); 23 | Assert.NotNull (main); 24 | } 25 | 26 | public void Dispose () 27 | { 28 | Console.SetError (consoleError); 29 | } 30 | 31 | [Fact] 32 | public async Task Empty () 33 | { 34 | var args = Array.Empty (); 35 | var task = (Task) main.Invoke (null, new object [] { args }); 36 | await task; 37 | Assert.Contains (DefaultErrorMessage, stderr.ToString ()); 38 | } 39 | 40 | [Fact] 41 | public async Task Stable () 42 | { 43 | var args = new [] { "--stable", "Xamarin.Android" }; 44 | var task = (Task) main.Invoke (null, new object [] { args }); 45 | await task; 46 | Assert.DoesNotContain (DefaultErrorMessage, stderr.ToString ()); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Directory.Build.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $(PackageId) 5 | true 6 | $(MSBuildThisFileDirectory)bin 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | all 20 | runtime; build; native; contentfiles; analyzers; buildtransitive 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /Boots.Tests/AsyncProcessTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Boots.Core; 6 | using Xunit; 7 | using Xunit.Abstractions; 8 | 9 | namespace Boots.Tests 10 | { 11 | public class AsyncProcessTests 12 | { 13 | readonly Bootstrapper boots = new Bootstrapper (); 14 | 15 | public AsyncProcessTests (ITestOutputHelper output) 16 | { 17 | boots.Logger = new TestWriter (output); 18 | } 19 | 20 | [SkippableFact] 21 | public async Task EchoShouldNotThrow () 22 | { 23 | using (var proc = new AsyncProcess (boots) { 24 | Command = Helpers.IsWindows ? "cmd" : "echo", 25 | Arguments = Helpers.IsWindows ? "/C echo test" : "test" 26 | }) { 27 | await proc.RunAsync (new CancellationToken ()); 28 | } 29 | } 30 | 31 | [Fact] 32 | public async Task NonExistingCommandShouldThrow () 33 | { 34 | using (var proc = new AsyncProcess (boots) { 35 | Command = Guid.NewGuid ().ToString () 36 | }) { 37 | await Assert.ThrowsAsync (() => proc.RunAsync (new CancellationToken ())); 38 | } 39 | } 40 | 41 | [Fact] 42 | public async Task RunWithOutput () 43 | { 44 | using (var proc = new AsyncProcess (boots) { 45 | Command = Helpers.IsWindows ? "cmd" : "echo", 46 | Arguments = Helpers.IsWindows ? "/C echo test" : "test" 47 | }) { 48 | var text = await proc.RunWithOutputAsync (new CancellationToken ()); 49 | Assert.Equal ("test", text.Trim ()); 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 1.1.0 4 | 5 | 6 | $(BUILD_BUILDNUMBER) 7 | 1 8 | $(BootsVersion).$(BuildNumber) 9 | $(BootsVersion).$(BuildNumber) 10 | $(BootsVersion).$(BuildNumber)$(BootsSuffix) 11 | Jonathan Peppers, Peter Collins 12 | $([System.DateTime]::Now.Year) $(Authors) 13 | icon.png 14 | LICENSE 15 | https://github.com/jonathanpeppers/boots 16 | Bootstrap CI Xamarin Cake Azure DevOps 17 | boots is a dotnet global tool for "bootstrapping" vsix & pkg files. 18 | latest 19 | enable 20 | true 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /samples/HelloForms.iOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UIDeviceFamily 6 | 7 | 1 8 | 2 9 | 10 | UISupportedInterfaceOrientations 11 | 12 | UIInterfaceOrientationPortrait 13 | UIInterfaceOrientationLandscapeLeft 14 | UIInterfaceOrientationLandscapeRight 15 | 16 | UISupportedInterfaceOrientations~ipad 17 | 18 | UIInterfaceOrientationPortrait 19 | UIInterfaceOrientationPortraitUpsideDown 20 | UIInterfaceOrientationLandscapeLeft 21 | UIInterfaceOrientationLandscapeRight 22 | 23 | MinimumOSVersion 24 | 8.0 25 | CFBundleDisplayName 26 | HelloForms 27 | CFBundleIdentifier 28 | com.companyname.HelloForms 29 | CFBundleVersion 30 | 1.0 31 | UILaunchStoryboardName 32 | LaunchScreen 33 | CFBundleName 34 | HelloForms 35 | XSAppIconAssets 36 | Assets.xcassets/AppIcon.appiconset 37 | 38 | 39 | -------------------------------------------------------------------------------- /samples/HelloForms.Android/Resources/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 26 | 27 | 30 | 31 | -------------------------------------------------------------------------------- /samples/HelloForms.iOS/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("HelloForms.iOS")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("HelloForms.iOS")] 13 | [assembly: AssemblyCopyright("Copyright © 2014")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("72bdc44f-c588-44f3-b6df-9aace7daafdd")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /scripts/build-and-test.yaml: -------------------------------------------------------------------------------- 1 | parameters: 2 | name: '' 3 | xaversion: 13.2 4 | steps: 5 | 6 | - task: UseDotNet@2 7 | displayName: install .NET 6 8 | inputs: 9 | version: 6.0.x 10 | 11 | - script: dotnet tool update --global Cake.Tool 12 | displayName: install Cake 13 | 14 | - script: dotnet build boots.sln -bl:$(System.DefaultWorkingDirectory)/bin/build.binlog 15 | displayName: dotnet build 16 | 17 | - script: dotnet test Boots.Tests/bin/$(Configuration)/net6.0/Boots.Tests.dll --logger:"trx;verbosity=normal" --logger:"console;verbosity=normal" 18 | displayName: dotnet test 19 | 20 | - task: PublishTestResults@2 21 | displayName: publish test results 22 | inputs: 23 | testResultsFormat: VSTest 24 | testResultsFiles: TestResults/*.trx 25 | testRunTitle: ${{ parameters.name }} 26 | failTaskOnFailedTests: true 27 | condition: succeededOrFailed() 28 | 29 | - script: dotnet Boots/bin/$(Configuration)/netcoreapp3.1/Boots.dll --alpha Xamarin.Android 30 | displayName: install xamarin-android 31 | 32 | - task: MSBuild@1 33 | displayName: verify 34 | inputs: 35 | solution: scripts/xa-version.targets 36 | msbuildArguments: '/v:minimal /nologo /t:Check /p:Expected=${{ parameters.xaversion }} /bl:$(System.DefaultWorkingDirectory)/bin/verify.binlog' 37 | 38 | - task: MSBuild@1 39 | displayName: build sample 40 | inputs: 41 | solution: samples/HelloForms.Android/HelloForms.Android.csproj 42 | msbuildArguments: '/restore /t:SignAndroidPackage /p:AndroidPackageFormat=aab /bl:$(System.DefaultWorkingDirectory)/bin/sample.binlog' 43 | 44 | - task: PublishPipelineArtifact@0 45 | displayName: artifacts 46 | inputs: 47 | artifactName: ${{ parameters.name }} 48 | targetPath: bin 49 | condition: succeededOrFailed() 50 | -------------------------------------------------------------------------------- /samples/HelloForms.Android/Resources/AboutResources.txt: -------------------------------------------------------------------------------- 1 | Images, layout descriptions, binary blobs and string dictionaries can be included 2 | in your application as resource files. Various Android APIs are designed to 3 | operate on the resource IDs instead of dealing with images, strings or binary blobs 4 | directly. 5 | 6 | For example, a sample Android app that contains a user interface layout (main.xml), 7 | an internationalization string table (strings.xml) and some icons (drawable-XXX/icon.png) 8 | would keep its resources in the "Resources" directory of the application: 9 | 10 | Resources/ 11 | drawable-hdpi/ 12 | icon.png 13 | 14 | drawable-ldpi/ 15 | icon.png 16 | 17 | drawable-mdpi/ 18 | icon.png 19 | 20 | layout/ 21 | main.xml 22 | 23 | values/ 24 | strings.xml 25 | 26 | In order to get the build system to recognize Android resources, set the build action to 27 | "AndroidResource". The native Android APIs do not operate directly with filenames, but 28 | instead operate on resource IDs. When you compile an Android application that uses resources, 29 | the build system will package the resources for distribution and generate a class called 30 | "Resource" that contains the tokens for each one of the resources included. For example, 31 | for the above Resources layout, this is what the Resource class would expose: 32 | 33 | public class Resource { 34 | public class drawable { 35 | public const int icon = 0x123; 36 | } 37 | 38 | public class layout { 39 | public const int main = 0x456; 40 | } 41 | 42 | public class strings { 43 | public const int first_string = 0xabc; 44 | public const int second_string = 0xbcd; 45 | } 46 | } 47 | 48 | You would then use R.drawable.icon to reference the drawable/icon.png file, or Resource.layout.main 49 | to reference the layout/main.xml file, or Resource.strings.first_string to reference the first 50 | string in the dictionary file values/strings.xml. 51 | -------------------------------------------------------------------------------- /Cake.Boots/Cake.Boots.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | Cake.Boots 6 | true 7 | $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb 8 | <_NuGetPackagePath>lib\$(TargetFramework) 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /Cake.Boots/build.cake: -------------------------------------------------------------------------------- 1 | var target = Argument("target", "Default"); 2 | 3 | // NOTE: only Release builds work 4 | #r "bin/Release/netstandard2.0/Cake.Boots.dll" 5 | 6 | // NOTE: always update Mono in a separate process, run Cake twice. 7 | Task("Mono") 8 | .Does(async () => 9 | { 10 | await Boots (Product.Mono, ReleaseChannel.Preview); 11 | }); 12 | 13 | Task("Boots") 14 | .Does(async () => 15 | { 16 | string vs = Environment.GetEnvironmentVariable("AGENT_JOBNAME") == "vs2019" ? "2019" : "2022"; 17 | var url = IsRunningOnWindows() ? 18 | $"https://github.com/codecadwallader/codemaid/releases/download/v12.0/CodeMaid.VS{vs}.v12.0.300.vsix" : 19 | "https://aka.ms/objective-sharpie"; 20 | 21 | await Boots (url); 22 | 23 | if (IsRunningOnWindows()) { 24 | // Install a Firefox .msi twice 25 | var firefox = "https://download-installer.cdn.mozilla.net/pub/firefox/releases/82.0/win64/en-US/Firefox%20Setup%2082.0.msi"; 26 | await Boots (firefox); 27 | await Boots (firefox, fileType: FileType.msi); 28 | } else { 29 | // Let's really run through the gauntlet and install 6 .pkg files 30 | await Boots (Product.XamariniOS, ReleaseChannel.Stable); 31 | await Boots (Product.XamarinMac, ReleaseChannel.Stable); 32 | await Boots (Product.XamariniOS, ReleaseChannel.Preview); 33 | await Boots (Product.XamarinMac, ReleaseChannel.Preview); 34 | await Boots (Product.XamariniOS, ReleaseChannel.Alpha); 35 | await Boots (Product.XamarinMac, ReleaseChannel.Alpha); 36 | 37 | var settings = new BootsSettings { 38 | Channel = ReleaseChannel.Stable, 39 | Product = Product.XamarinAndroid, 40 | Timeout = TimeSpan.FromSeconds (200), 41 | ReadWriteTimeout = TimeSpan.FromMinutes (10), 42 | NetworkRetries = 1, 43 | }; 44 | await Boots (settings); 45 | settings.Channel = ReleaseChannel.Preview; 46 | await Boots (settings); 47 | settings.Channel = ReleaseChannel.Alpha; 48 | await Boots (settings); 49 | } 50 | }); 51 | 52 | Task("Default") 53 | .IsDependentOn("Boots"); 54 | 55 | RunTarget(target); 56 | -------------------------------------------------------------------------------- /Boots.Tests/BootstrapperTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Boots.Core; 4 | using Xunit; 5 | using Xunit.Abstractions; 6 | 7 | namespace Boots.Tests 8 | { 9 | public class BootstrapperTests 10 | { 11 | readonly Bootstrapper boots = new Bootstrapper (); 12 | 13 | public BootstrapperTests (ITestOutputHelper output) 14 | { 15 | boots.Logger = new TestWriter (output); 16 | } 17 | 18 | string GetCodeMaidUrl() 19 | { 20 | string vs = Environment.GetEnvironmentVariable("AGENT_JOBNAME") == "vs2019" ? "2019" : "2022"; 21 | return $"https://github.com/codecadwallader/codemaid/releases/download/v12.0/CodeMaid.VS{vs}.v12.0.300.vsix"; 22 | } 23 | 24 | [SkippableFact] 25 | public async Task SimpleInstall () 26 | { 27 | if (Helpers.IsWindows) { 28 | boots.Url = GetCodeMaidUrl(); 29 | } else if (Helpers.IsMac) { 30 | boots.Url = "https://aka.ms/objective-sharpie"; 31 | } else { 32 | Skip.If (true, "Not supported on Linux yet"); 33 | } 34 | await boots.Install (); 35 | // Two installs back-to-back should be fine 36 | await boots.Install (); 37 | } 38 | 39 | [SkippableFact] 40 | public async Task DowngradeFirst () 41 | { 42 | Skip.If (!Helpers.IsWindows, "DowngradeFirst is only applicable on Windows"); 43 | boots.Url = GetCodeMaidUrl(); 44 | await boots.Install (); 45 | // NOTE: this only does something for .vsix files on Windows 46 | boots.DowngradeFirst = true; 47 | await boots.Install (); 48 | } 49 | 50 | [SkippableFact] 51 | public async Task InstallMsi () 52 | { 53 | Skip.If (!Helpers.IsWindows, ".msis are only supported on Windows"); 54 | boots.FileType = FileType.msi; 55 | boots.Url = "https://download-installer.cdn.mozilla.net/pub/firefox/releases/82.0/win64/en-US/Firefox%20Setup%2082.0.msi"; 56 | await boots.Install (); 57 | // Two installs back-to-back should be fine 58 | await boots.Install (); 59 | } 60 | 61 | [SkippableFact] 62 | public async Task InvalidInstallerFile () 63 | { 64 | Skip.If (!Helpers.IsMac && !Helpers.IsWindows, "Not supported on Linux yet"); 65 | boots.Url = "https://i.kym-cdn.com/entries/icons/mobile/000/018/012/this_is_fine.jpg"; 66 | await Assert.ThrowsAsync (() => boots.Install ()); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Boots.Core/MacUrlResolver.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using System.Xml; 6 | 7 | namespace Boots.Core 8 | { 9 | class MacUrlResolver : UrlResolver 10 | { 11 | const string Url = "https://software.xamarin.com/Service/Updates?v=2&pv964ebddd-1ffe-47e7-8128-5ce17ffffb05=0&pv4569c276-1397-4adb-9485-82a7696df22e=0&pvd1ec039f-f3db-468b-a508-896d7c382999=0&pv0ab364ff-c0e9-43a8-8747-3afb02dc7731=0&level="; 12 | static readonly Dictionary ProductIds = new Dictionary { 13 | { Product.Mono, "964ebddd-1ffe-47e7-8128-5ce17ffffb05" }, 14 | { Product.XamarinAndroid, "d1ec039f-f3db-468b-a508-896d7c382999" }, 15 | { Product.XamariniOS, "4569c276-1397-4adb-9485-82a7696df22e" }, 16 | { Product.XamarinMac, "0ab364ff-c0e9-43a8-8747-3afb02dc7731" }, 17 | }; 18 | 19 | public MacUrlResolver (Bootstrapper boots) : base (boots) { } 20 | 21 | public async override Task Resolve (ReleaseChannel channel, Product product, CancellationToken token = new CancellationToken ()) 22 | { 23 | using var httpClient = new HttpClientWithPolicy (Boots); 24 | string level = GetLevel (channel); 25 | string productId = GetProductId (product); 26 | 27 | var uri = new Uri (Url + level); 28 | Boots.Logger.WriteLine ($"Querying {uri}"); 29 | 30 | var document = await httpClient.GetXmlDocumentAsync (uri, token); 31 | var node = document.SelectSingleNode ($"/UpdateInfo/Application[@id='{productId}']/Update/@url"); 32 | if (node == null) { 33 | throw new XmlException ($"Did not find {product}, at channel {channel}"); 34 | } 35 | 36 | string url = node.InnerText; 37 | if (string.IsNullOrEmpty (url)) { 38 | throw new XmlException ($"Did not find {product}, at channel {channel}"); 39 | } 40 | 41 | // Just let this throw if it is an invalid Uri 42 | new Uri (url); 43 | return url; 44 | } 45 | 46 | string GetLevel (ReleaseChannel channel) 47 | { 48 | return channel switch { 49 | ReleaseChannel.Stable => "Stable", 50 | ReleaseChannel.Preview => "Beta", 51 | ReleaseChannel.Alpha => "Alpha", 52 | _ => throw new NotImplementedException ($"Unexpected value for release: {channel}"), 53 | }; 54 | } 55 | 56 | string GetProductId (Product product) 57 | { 58 | if (!ProductIds.TryGetValue (product, out string id)) { 59 | throw new NotImplementedException ($"Unexpected value for product: {product}"); 60 | } 61 | return id; 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Boots.Core/HttpClientWithPolicy.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Net.Http; 4 | using System.Text.Json; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using System.Xml; 8 | 9 | namespace Boots.Core 10 | { 11 | class HttpClientWithPolicy : IDisposable 12 | { 13 | readonly Bootstrapper boots; 14 | readonly HttpClient client; 15 | 16 | public TimeSpan Timeout => client.Timeout; 17 | 18 | public HttpClientWithPolicy (Bootstrapper boots) 19 | { 20 | this.boots = boots; 21 | client = new HttpClient (); 22 | if (boots.Timeout != null) { 23 | client.Timeout = boots.Timeout.Value; 24 | } 25 | } 26 | 27 | public void Dispose () => client.Dispose (); 28 | 29 | public Task DownloadAsync (Uri uri, string tempFile, CancellationToken token) => 30 | boots.ActivePolicy.ExecuteAsync (t => DoDownloadAsync (uri, tempFile, t), token); 31 | 32 | protected async virtual Task DoDownloadAsync (Uri uri, string tempFile, CancellationToken token) 33 | { 34 | var request = new HttpRequestMessage (HttpMethod.Get, uri); 35 | var response = await client.SendAsync (request, HttpCompletionOption.ResponseHeadersRead, token); 36 | response.EnsureSuccessStatusCode (); 37 | using var httpStream = await response.Content.ReadAsStreamAsync (); 38 | using var fileStream = File.Create (tempFile); 39 | boots.Logger.WriteLine ($"Writing to {tempFile}"); 40 | await httpStream.CopyToAsync (fileStream, 8 * 1024, token); 41 | } 42 | 43 | public virtual Task GetJsonAsync (Uri uri, CancellationToken token) => 44 | boots.ActivePolicy.ExecuteAsync (t => DoGetJsonAsync (uri, t), token); 45 | 46 | protected async virtual Task DoGetJsonAsync (Uri uri, CancellationToken token) 47 | { 48 | var response = await client.GetAsync (uri, token); 49 | response.EnsureSuccessStatusCode (); 50 | using var stream = await response.Content.ReadAsStreamAsync (); 51 | return await JsonSerializer.DeserializeAsync (stream, cancellationToken: token); 52 | } 53 | 54 | public virtual Task GetXmlDocumentAsync (Uri uri, CancellationToken token) => 55 | boots.ActivePolicy.ExecuteAsync (t => DoGetXmlDocumentAsync (uri, t), token); 56 | 57 | protected async virtual Task DoGetXmlDocumentAsync (Uri uri, CancellationToken token) 58 | { 59 | var response = await client.GetAsync (uri, token); 60 | response.EnsureSuccessStatusCode (); 61 | var document = new XmlDocument (); 62 | using var stream = await response.Content.ReadAsStreamAsync (); 63 | await Task.Factory.StartNew (() => document.Load (stream), token); 64 | return document; 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /boots.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29102.190 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Boots.Core", "Boots.Core\Boots.Core.csproj", "{977D3870-832A-4E80-BB7D-EBADD0C1DE3A}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Boots", "Boots\Boots.csproj", "{5108C889-ABB7-4B95-BAD2-3DF11FDD05EB}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Boots.Tests", "Boots.Tests\Boots.Tests.csproj", "{393FF93B-5A0B-490E-9E28-4B38E579AF7C}" 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cake.Boots", "Cake.Boots\Cake.Boots.csproj", "{10B653C6-675D-49D1-8BA6-DC62F2131742}" 13 | EndProject 14 | Global 15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 16 | Debug|Any CPU = Debug|Any CPU 17 | Release|Any CPU = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 20 | {977D3870-832A-4E80-BB7D-EBADD0C1DE3A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {977D3870-832A-4E80-BB7D-EBADD0C1DE3A}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {977D3870-832A-4E80-BB7D-EBADD0C1DE3A}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {977D3870-832A-4E80-BB7D-EBADD0C1DE3A}.Release|Any CPU.Build.0 = Release|Any CPU 24 | {5108C889-ABB7-4B95-BAD2-3DF11FDD05EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {5108C889-ABB7-4B95-BAD2-3DF11FDD05EB}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {5108C889-ABB7-4B95-BAD2-3DF11FDD05EB}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {5108C889-ABB7-4B95-BAD2-3DF11FDD05EB}.Release|Any CPU.Build.0 = Release|Any CPU 28 | {393FF93B-5A0B-490E-9E28-4B38E579AF7C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {393FF93B-5A0B-490E-9E28-4B38E579AF7C}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {393FF93B-5A0B-490E-9E28-4B38E579AF7C}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {393FF93B-5A0B-490E-9E28-4B38E579AF7C}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {10B653C6-675D-49D1-8BA6-DC62F2131742}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {10B653C6-675D-49D1-8BA6-DC62F2131742}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {10B653C6-675D-49D1-8BA6-DC62F2131742}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {10B653C6-675D-49D1-8BA6-DC62F2131742}.Release|Any CPU.Build.0 = Release|Any CPU 36 | EndGlobalSection 37 | GlobalSection(SolutionProperties) = preSolution 38 | HideSolutionNode = FALSE 39 | EndGlobalSection 40 | GlobalSection(ExtensibilityGlobals) = postSolution 41 | SolutionGuid = {72B45E6F-88B9-4166-871D-A0423C6E063E} 42 | EndGlobalSection 43 | EndGlobal 44 | -------------------------------------------------------------------------------- /Cake.Boots/BootsAddin.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Text; 3 | using System.Threading.Tasks; 4 | using Boots.Core; 5 | using Cake.Core; 6 | using Cake.Core.Annotations; 7 | using Cake.Core.Diagnostics; 8 | 9 | namespace Cake.Boots 10 | { 11 | [CakeAliasCategory("Boots")] 12 | public static class BootsAddin 13 | { 14 | [CakeMethodAlias] 15 | public static async Task Boots (this ICakeContext context, string url, FileType? fileType = default) 16 | { 17 | var boots = new Bootstrapper { 18 | Url = url, 19 | FileType = fileType, 20 | Logger = new CakeWriter (context) 21 | }; 22 | 23 | await boots.Install (); 24 | } 25 | 26 | [CakeMethodAlias] 27 | public static async Task Boots (this ICakeContext context, Product product, ReleaseChannel channel = ReleaseChannel.Stable) 28 | { 29 | var boots = new Bootstrapper { 30 | Channel = channel, 31 | Product = product, 32 | Logger = new CakeWriter (context) 33 | }; 34 | 35 | await boots.Install (); 36 | } 37 | 38 | [CakeMethodAlias] 39 | public static async Task Boots (this ICakeContext context, BootsSettings settings) 40 | { 41 | var boots = new Bootstrapper { 42 | Logger = new CakeWriter (context) 43 | }; 44 | 45 | if (settings.Timeout != null) 46 | boots.Timeout = settings.Timeout; 47 | if (settings.ReadWriteTimeout != null) 48 | boots.ReadWriteTimeout = settings.ReadWriteTimeout.Value; 49 | if (settings.NetworkRetries != null) 50 | boots.NetworkRetries = settings.NetworkRetries.Value; 51 | if (settings.Channel != null) 52 | boots.Channel = settings.Channel.Value; 53 | if (settings.Product != null) 54 | boots.Product = settings.Product.Value; 55 | if (settings.FileType != null) 56 | boots.FileType = settings.FileType; 57 | if (settings.Url != null) 58 | boots.Url = settings.Url; 59 | 60 | await boots.Install (); 61 | } 62 | 63 | class CakeWriter : TextWriter 64 | { 65 | const Verbosity verbosity = Verbosity.Normal; 66 | const LogLevel level = LogLevel.Information; 67 | readonly ICakeContext context; 68 | 69 | public CakeWriter (ICakeContext context) 70 | { 71 | this.context = context; 72 | } 73 | 74 | public override Encoding Encoding => Encoding.Default; 75 | 76 | public override void WriteLine (string value) 77 | { 78 | value ??= ""; 79 | 80 | // avoid System.FormatException from string.Format 81 | value = value.Replace ("{", "{{").Replace ("}", "}}"); 82 | 83 | context.Log.Write (verbosity, level, value); 84 | } 85 | 86 | public override void WriteLine (string format, params object [] args) 87 | { 88 | context.Log.Write (verbosity, level, format, args); 89 | } 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /samples/HelloForms.iOS/Resources/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /samples/HelloForms.iOS/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images": [ 3 | { 4 | "scale": "2x", 5 | "size": "20x20", 6 | "idiom": "iphone", 7 | "filename": "Icon40.png" 8 | }, 9 | { 10 | "scale": "3x", 11 | "size": "20x20", 12 | "idiom": "iphone", 13 | "filename": "Icon60.png" 14 | }, 15 | { 16 | "scale": "2x", 17 | "size": "29x29", 18 | "idiom": "iphone", 19 | "filename": "Icon58.png" 20 | }, 21 | { 22 | "scale": "3x", 23 | "size": "29x29", 24 | "idiom": "iphone", 25 | "filename": "Icon87.png" 26 | }, 27 | { 28 | "scale": "2x", 29 | "size": "40x40", 30 | "idiom": "iphone", 31 | "filename": "Icon80.png" 32 | }, 33 | { 34 | "scale": "3x", 35 | "size": "40x40", 36 | "idiom": "iphone", 37 | "filename": "Icon120.png" 38 | }, 39 | { 40 | "scale": "2x", 41 | "size": "60x60", 42 | "idiom": "iphone", 43 | "filename": "Icon120.png" 44 | }, 45 | { 46 | "scale": "3x", 47 | "size": "60x60", 48 | "idiom": "iphone", 49 | "filename": "Icon180.png" 50 | }, 51 | { 52 | "scale": "1x", 53 | "size": "20x20", 54 | "idiom": "ipad", 55 | "filename": "Icon20.png" 56 | }, 57 | { 58 | "scale": "2x", 59 | "size": "20x20", 60 | "idiom": "ipad", 61 | "filename": "Icon40.png" 62 | }, 63 | { 64 | "scale": "1x", 65 | "size": "29x29", 66 | "idiom": "ipad", 67 | "filename": "Icon29.png" 68 | }, 69 | { 70 | "scale": "2x", 71 | "size": "29x29", 72 | "idiom": "ipad", 73 | "filename": "Icon58.png" 74 | }, 75 | { 76 | "scale": "1x", 77 | "size": "40x40", 78 | "idiom": "ipad", 79 | "filename": "Icon40.png" 80 | }, 81 | { 82 | "scale": "2x", 83 | "size": "40x40", 84 | "idiom": "ipad", 85 | "filename": "Icon80.png" 86 | }, 87 | { 88 | "scale": "1x", 89 | "size": "76x76", 90 | "idiom": "ipad", 91 | "filename": "Icon76.png" 92 | }, 93 | { 94 | "scale": "2x", 95 | "size": "76x76", 96 | "idiom": "ipad", 97 | "filename": "Icon152.png" 98 | }, 99 | { 100 | "scale": "2x", 101 | "size": "83.5x83.5", 102 | "idiom": "ipad", 103 | "filename": "Icon167.png" 104 | }, 105 | { 106 | "scale": "1x", 107 | "size": "1024x1024", 108 | "idiom": "ios-marketing", 109 | "filename": "Icon1024.png" 110 | } 111 | ], 112 | "properties": {}, 113 | "info": { 114 | "version": 1, 115 | "author": "xcode" 116 | } 117 | } -------------------------------------------------------------------------------- /Boots.Core/Bootstrapper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Net.Http; 4 | using System.Runtime.CompilerServices; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using Polly; 8 | using Polly.Timeout; 9 | 10 | [assembly: InternalsVisibleTo ("Boots.Tests")] 11 | 12 | namespace Boots.Core 13 | { 14 | public class Bootstrapper 15 | { 16 | public TimeSpan? Timeout { get; set; } 17 | 18 | public TimeSpan ReadWriteTimeout { get; set; } = TimeSpan.FromMinutes (5); 19 | 20 | public int NetworkRetries { get; set; } = 3; 21 | 22 | public ReleaseChannel? Channel { get; set; } 23 | 24 | public Product? Product { get; set; } 25 | 26 | public FileType? FileType { get; set; } 27 | 28 | public string Url { get; set; } = ""; 29 | 30 | public bool DowngradeFirst { get; set; } 31 | 32 | public TextWriter Logger { get; set; } = Console.Out; 33 | 34 | internal AsyncPolicy ActivePolicy { get; set; } = Policy.NoOpAsync (); 35 | 36 | internal void UpdateActivePolicy () 37 | { 38 | ActivePolicy = Policy 39 | .Handle () 40 | .Or () 41 | .Or () 42 | .RetryAsync (NetworkRetries, 43 | (exc, count) => Logger.WriteLine ($"Retry attempt {count}: {exc}")) 44 | .WrapAsync (Policy.TimeoutAsync (ReadWriteTimeout)); 45 | } 46 | 47 | public async Task Install (CancellationToken token = new CancellationToken ()) 48 | { 49 | UpdateActivePolicy (); 50 | 51 | if (string.IsNullOrEmpty (Url)) { 52 | if (Channel == null) 53 | throw new ArgumentNullException (nameof (Channel)); 54 | if (Product == null) 55 | throw new ArgumentNullException (nameof (Product)); 56 | 57 | var resolver = Helpers.IsMac ? 58 | (UrlResolver) new MacUrlResolver (this) : 59 | (UrlResolver) new WindowsUrlResolver (this); 60 | Url = await resolver.Resolve (Channel.Value, Product.Value); 61 | } 62 | 63 | Installer installer; 64 | if (Helpers.IsMac) { 65 | installer = new PkgInstaller (this); 66 | } else if (Helpers.IsWindows) { 67 | if (FileType == null) { 68 | if (Url.EndsWith (".msi", StringComparison.OrdinalIgnoreCase)) { 69 | FileType = global::FileType.msi; 70 | Logger.WriteLine ("Inferring .msi from URL."); 71 | } else if (Url.EndsWith (".vsix", StringComparison.OrdinalIgnoreCase)) { 72 | FileType = global::FileType.vsix; 73 | Logger.WriteLine ("Inferring .vsix from URL."); 74 | } 75 | } 76 | if (FileType == global::FileType.msi) { 77 | installer = new MsiInstaller (this); 78 | } else { 79 | installer = new VsixInstaller (this); 80 | } 81 | } else { 82 | throw new NotSupportedException ("Unsupported platform, neither macOS or Windows detected."); 83 | } 84 | 85 | using var downloader = new Downloader (this, installer.Extension); 86 | await downloader.Download (token); 87 | await installer.Install (downloader.TempFile, token); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /docs/HowToFindBuilds.md: -------------------------------------------------------------------------------- 1 | # How To Find Builds 2 | 3 | Boots is all about specifying the version of Xamarin / Mono you want to use, but how do you find out about the available versions and where do you get them from? 4 | 5 | ## Latest Builds 6 | 7 | To use the latest stable / preview builds you can simply use boots, by passing in the following arguments: 8 | 9 | | Argument | Description | 10 | |-------------|---------------------------------| 11 | | `--stable` | Uses the latest stable version | 12 | | `--preview` | Uses the latest preview version | 13 | 14 | ``` 15 | boots --stable Mono 16 | boots --stable Xamarin.Android 17 | boots --stable Xamarin.iOS 18 | boots --stable Xamarin.Mac 19 | ``` 20 | 21 | > Note: this feature is only availale for boots versions 1.0.2.X onwards 22 | 23 | 24 | ## Manual Builds 25 | 26 | If you want to specify a specific version to use, you will have to find the specific package url that you wish to use. The package urls are different for every platform. 27 | 28 | ### Mono 29 | 30 | The latest Mono version can be found on [Mono Downloads](https://www.mono-project.com/download/stable/). Head over to the page and copy the link address for the channel you wish to use: 31 | 32 | ![How to find mono version](GetMonoVersion.png) 33 | 34 | The link should look similar to: 35 | 36 | **Mono Version 6.12.0** 37 | 38 | ``` 39 | https://download.mono-project.com/archive/6.10.0/macos-10-universal/MonoFramework-MDK-6.10.0.104.macos10.xamarin.universal.pkg 40 | ``` 41 | 42 | 43 | 44 | ### Xamarin.Android 45 | 46 | To find the available Xamarin.Android versions you will need to head over to the official [Xamarin.Android repository](https://github.com/xamarin/xamarin-android). You can find the available versions in the [downloads](https://github.com/xamarin/xamarin-android#Downloads) section of the [README.md](https://github.com/xamarin/xamarin-android/blob/master/README.md). 47 | 48 | ![How to find android version](GetAndroidVersion.png) 49 | 50 | The link should look similar to: 51 | 52 | **Commercial Xamarin.Android 11.0 (d16-7) for Windows** 53 | 54 | ``` 55 | https://aka.ms/xamarin-android-commercial-d16-7-windows 56 | ``` 57 | 58 | 59 | 60 | 61 | 62 | ### Xamarin.iOS & Xamarin.Mac 63 | 64 | To find the available Xamarin.iOS / Xamarin.Mac versions you will need to head over to the official [Xamarin.iOS & Xamarin.Mac repository](https://github.com/xamarin/xamarin-macios). You can find the available versions in the [downloads](https://github.com/xamarin/xamarin-macios#downloads) section of the [README.md](https://github.com/xamarin/xamarin-macios/blob/main/README.md). 65 | 66 | ![How to find mac ios version](GetMacIosVersion.png) 67 | 68 | Select the version you wish to install & copy the link. 69 | 70 | The link should look similar to: 71 | 72 | **Xamarin.iOS d16.7** 73 | 74 | ``` 75 | https://download.visualstudio.microsoft.com/download/pr/c939bb72-556b-4e8a-a9b4-0f90e9b5e336/f906a6ce183fb73f1bcd945ac32f984b/xamarin.ios-14.0.0.0.pkg 76 | ``` 77 | -------------------------------------------------------------------------------- /Boots.Tests/UrlResolverTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using System.Threading.Tasks; 4 | using Boots.Core; 5 | using Xunit; 6 | 7 | namespace Boots.Tests 8 | { 9 | public class UrlResolverTests 10 | { 11 | HttpClient client = new HttpClient (); 12 | 13 | [Theory] 14 | [InlineData (typeof (MacUrlResolver), ReleaseChannel.Stable, Product.XamarinAndroid)] 15 | [InlineData (typeof (MacUrlResolver), ReleaseChannel.Stable, Product.XamariniOS)] 16 | [InlineData (typeof (MacUrlResolver), ReleaseChannel.Stable, Product.XamarinMac)] 17 | [InlineData (typeof (MacUrlResolver), ReleaseChannel.Stable, Product.Mono)] 18 | [InlineData (typeof (MacUrlResolver), ReleaseChannel.Preview, Product.XamarinAndroid)] 19 | [InlineData (typeof (MacUrlResolver), ReleaseChannel.Preview, Product.XamariniOS)] 20 | [InlineData (typeof (MacUrlResolver), ReleaseChannel.Preview, Product.XamarinMac)] 21 | [InlineData (typeof (MacUrlResolver), ReleaseChannel.Preview, Product.Mono)] 22 | [InlineData (typeof (MacUrlResolver), ReleaseChannel.Alpha, Product.XamarinAndroid)] 23 | [InlineData (typeof (MacUrlResolver), ReleaseChannel.Alpha, Product.XamariniOS)] 24 | [InlineData (typeof (MacUrlResolver), ReleaseChannel.Alpha, Product.XamarinMac)] 25 | [InlineData (typeof (MacUrlResolver), ReleaseChannel.Alpha, Product.Mono)] 26 | [InlineData (typeof (WindowsUrlResolver), ReleaseChannel.Stable, Product.XamarinAndroid)] 27 | [InlineData (typeof (WindowsUrlResolver), ReleaseChannel.Preview, Product.XamarinAndroid)] 28 | [InlineData (typeof (WindowsUrlResolver), ReleaseChannel.Alpha, Product.XamarinAndroid)] 29 | 30 | public async Task Resolve (Type type, ReleaseChannel channel, Product product) 31 | { 32 | var resolver = (UrlResolver) Activator.CreateInstance (type, new Bootstrapper ()); 33 | var url = await resolver.Resolve (channel, product); 34 | var response = await client.GetAsync (url, HttpCompletionOption.ResponseHeadersRead); 35 | response.EnsureSuccessStatusCode (); 36 | } 37 | 38 | [Theory] 39 | [InlineData (typeof (WindowsUrlResolver), ReleaseChannel.Stable, Product.XamariniOS)] 40 | [InlineData (typeof (WindowsUrlResolver), ReleaseChannel.Stable, Product.XamarinMac)] 41 | [InlineData (typeof (WindowsUrlResolver), ReleaseChannel.Stable, Product.Mono)] 42 | [InlineData (typeof (WindowsUrlResolver), ReleaseChannel.Preview, Product.XamariniOS)] 43 | [InlineData (typeof (WindowsUrlResolver), ReleaseChannel.Preview, Product.XamarinMac)] 44 | [InlineData (typeof (WindowsUrlResolver), ReleaseChannel.Preview, Product.Mono)] 45 | [InlineData (typeof (WindowsUrlResolver), ReleaseChannel.Alpha, Product.XamariniOS)] 46 | [InlineData (typeof (WindowsUrlResolver), ReleaseChannel.Alpha, Product.XamarinMac)] 47 | [InlineData (typeof (WindowsUrlResolver), ReleaseChannel.Alpha, Product.Mono)] 48 | [InlineData (typeof (WindowsUrlResolver), (ReleaseChannel) 9999, Product.Mono)] 49 | [InlineData (typeof (WindowsUrlResolver), ReleaseChannel.Preview, (Product) 9999)] 50 | 51 | public async Task NotImplemented (Type type, ReleaseChannel channel, Product product) 52 | { 53 | var resolver = (UrlResolver) Activator.CreateInstance (type, new Bootstrapper ()); 54 | await Assert.ThrowsAsync (() => resolver.Resolve (channel, product)); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Boots.Core/AsyncProcess.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Text; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace Boots.Core 8 | { 9 | class AsyncProcess : IDisposable 10 | { 11 | readonly Bootstrapper boots; 12 | 13 | public string Command { get; set; } = ""; 14 | public string Arguments { get; set; } = ""; 15 | public bool Elevate { get; set; } = false; 16 | 17 | Process? process; 18 | 19 | public AsyncProcess (Bootstrapper boots) 20 | { 21 | this.boots = boots; 22 | } 23 | 24 | public AsyncProcess (Bootstrapper boots, string cmd, params string [] argumentList) 25 | : this (boots) 26 | { 27 | Command = cmd; 28 | Arguments = string.Join (" ", argumentList); 29 | } 30 | 31 | Process CreateProcess () 32 | { 33 | if (!Helpers.IsWindows && Elevate) { 34 | Arguments = $"{Command} {Arguments}"; 35 | Command = "/usr/bin/sudo"; 36 | } 37 | return new Process { 38 | StartInfo = new ProcessStartInfo { 39 | FileName = Command, 40 | Arguments = Arguments, 41 | UseShellExecute = false, 42 | RedirectStandardError = true, 43 | RedirectStandardOutput = true, 44 | } 45 | }; 46 | } 47 | 48 | Task StartAndWait (Process process, CancellationToken token) 49 | { 50 | process.Start (); 51 | process.BeginErrorReadLine (); 52 | process.BeginOutputReadLine (); 53 | return Task.Run (process.WaitForExit, token); 54 | } 55 | 56 | async Task Run (CancellationToken token) 57 | { 58 | process = CreateProcess (); 59 | process.ErrorDataReceived += (sender, e) => { 60 | if (e.Data != null) 61 | boots.Logger.WriteLine (e.Data); 62 | }; 63 | process.OutputDataReceived += (sender, e) => { 64 | if (e.Data != null) 65 | boots.Logger.WriteLine (e.Data); 66 | }; 67 | 68 | await StartAndWait (process, token); 69 | return process.ExitCode; 70 | } 71 | 72 | public async Task RunAsync (CancellationToken token, bool throwOnError = true) 73 | { 74 | int exitCode = await Run (token); 75 | if (throwOnError && exitCode != 0) 76 | ThrowForExitCode (exitCode); 77 | return exitCode; 78 | } 79 | 80 | public void ThrowForExitCode (int exitCode) => 81 | throw new Exception ($"'{Command}' with arguments '{Arguments}' exited with code {exitCode}"); 82 | 83 | public async Task RunWithOutputAsync (CancellationToken token) 84 | { 85 | var builder = new StringBuilder (); 86 | process = CreateProcess (); 87 | process.ErrorDataReceived += (sender, e) => { 88 | if (e.Data != null) 89 | builder.AppendLine (e.Data); 90 | }; 91 | process.OutputDataReceived += (sender, e) => { 92 | if (e.Data != null) 93 | builder.AppendLine (e.Data); 94 | }; 95 | 96 | await StartAndWait (process, token); 97 | if (process.ExitCode != 0) 98 | throw new Exception ($"'{Command}' with arguments '{Arguments}' exited with code {process.ExitCode}"); 99 | return builder.ToString (); 100 | } 101 | 102 | public async Task TryRunAsync (CancellationToken token) 103 | { 104 | return await Run (token); 105 | } 106 | 107 | public void Dispose () 108 | { 109 | process?.Dispose (); 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /Boots.Core/WindowsUrlResolver.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace Boots.Core 7 | { 8 | class WindowsUrlResolver : UrlResolver 9 | { 10 | const string ReleaseUrl = "https://aka.ms/vs/17/release/channel"; 11 | const string PreviewUrl = "https://aka.ms/vs/17/pre/channel"; 12 | 13 | public WindowsUrlResolver (Bootstrapper boots) : base (boots) { } 14 | 15 | public async override Task Resolve (ReleaseChannel channel, Product product, CancellationToken token = new CancellationToken ()) 16 | { 17 | using var httpClient = new HttpClientWithPolicy (Boots); 18 | Uri uri = GetUri (channel); 19 | string channelId = GetChannelId (channel); 20 | string productId = GetProductId (product); 21 | string payloadManifestUrl = await GetPayloadManifestUrl (httpClient, uri, channelId, token); 22 | return await GetPayloadUrl (httpClient, payloadManifestUrl, productId, token); 23 | } 24 | 25 | async Task GetPayloadManifestUrl (HttpClientWithPolicy httpClient, Uri uri, string channelId, CancellationToken token) 26 | { 27 | Boots.Logger.WriteLine ($"Querying {uri}"); 28 | 29 | var manifest = await httpClient.GetJsonAsync (uri, token); 30 | var channelItem = manifest?.channelItems?.FirstOrDefault (c => c.id == channelId); 31 | if (channelItem == null) { 32 | throw new InvalidOperationException ($"Did not find '{channelId}' at: {uri}"); 33 | } 34 | 35 | var payloadManifestUrl = channelItem.payloads?.Select (p => p.url)?.FirstOrDefault (); 36 | if (payloadManifestUrl == null || payloadManifestUrl == "") { 37 | throw new InvalidOperationException ($"Did not find manifest url for '{channelId}' at: {uri}"); 38 | } 39 | return payloadManifestUrl; 40 | } 41 | 42 | async Task GetPayloadUrl (HttpClientWithPolicy httpClient, string payloadManifestUrl, string productId, CancellationToken token) 43 | { 44 | var uri = new Uri (payloadManifestUrl); 45 | Boots.Logger.WriteLine ($"Querying {uri}"); 46 | 47 | var payload = await httpClient.GetJsonAsync (uri, token); 48 | var url = payload?.packages?.FirstOrDefault (p => p.id == productId)?.payloads?.Select (p => p.url).FirstOrDefault (); 49 | if (url == null || url == "") { 50 | throw new InvalidOperationException ($"Did not find payload url for '{productId}' at: {uri}"); 51 | } 52 | 53 | // Just let this throw if it is an invalid Uri 54 | new Uri (url); 55 | return url; 56 | } 57 | 58 | Uri GetUri (ReleaseChannel channel) 59 | { 60 | switch (channel) { 61 | case ReleaseChannel.Stable: 62 | return new Uri (ReleaseUrl); 63 | case ReleaseChannel.Preview: 64 | case ReleaseChannel.Alpha: 65 | return new Uri (PreviewUrl); 66 | default: 67 | throw new NotImplementedException ($"Unexpected value for {nameof (ReleaseChannel)}: {channel}"); ; 68 | } 69 | } 70 | 71 | string GetChannelId (ReleaseChannel channel) 72 | { 73 | switch (channel) { 74 | case ReleaseChannel.Stable: 75 | return "Microsoft.VisualStudio.Manifests.VisualStudio"; 76 | case ReleaseChannel.Preview: 77 | case ReleaseChannel.Alpha: 78 | return "Microsoft.VisualStudio.Manifests.VisualStudioPreview"; 79 | default: 80 | throw new NotImplementedException ($"Unexpected value for release: {channel}"); ; 81 | } 82 | } 83 | 84 | string GetProductId (Product product) 85 | { 86 | switch (product) { 87 | case Product.XamarinAndroid: 88 | return "Xamarin.Android.Sdk"; 89 | case Product.Mono: 90 | case Product.XamariniOS: 91 | case Product.XamarinMac: 92 | throw new NotImplementedException ($"Value for product not implemented on Windows: {product}"); 93 | default: 94 | throw new NotImplementedException ($"Unexpected value for product: {product}"); 95 | } 96 | } 97 | 98 | class VSManifest 99 | { 100 | public VSPackage []? channelItems { get; set; } 101 | } 102 | 103 | class VSPayload 104 | { 105 | public string? url { get; set; } 106 | } 107 | 108 | class VSPayloadManifest 109 | { 110 | public VSPackage []? packages { get; set; } 111 | } 112 | 113 | class VSPackage 114 | { 115 | public string? id { get; set; } 116 | 117 | public VSPayload []? payloads { get; set; } 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /Boots.Tests/HttpClientWithPolicyTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Net.Http; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using System.Xml; 7 | using Boots.Core; 8 | using Polly.Timeout; 9 | using Xunit; 10 | 11 | namespace Boots.Tests 12 | { 13 | public class HttpClientWithPolicyTests 14 | { 15 | [Fact] 16 | public void InvalidTimeout () 17 | { 18 | var boots = new Bootstrapper { 19 | Timeout = TimeSpan.FromSeconds (-1), 20 | }; 21 | Assert.Throws (() => new HttpClientWithPolicy (boots)); 22 | } 23 | 24 | [Fact] 25 | public void DefaultTimeout () 26 | { 27 | // Mainly validates the 100-second default: 28 | // https://docs.microsoft.com/dotnet/api/system.net.http.httpclient.timeout#remarks 29 | var boots = new Bootstrapper (); 30 | using var client = new HttpClientWithPolicy (boots); 31 | Assert.Equal (TimeSpan.FromSeconds (100), client.Timeout); 32 | } 33 | 34 | [Fact] 35 | public async Task Cancelled () 36 | { 37 | var boots = new Bootstrapper (); 38 | boots.UpdateActivePolicy (); 39 | 40 | using var client = new AlwaysTimeoutClient (boots); 41 | var token = new CancellationToken (canceled: true); 42 | await Assert.ThrowsAsync (() => 43 | client.DownloadAsync (new Uri ("http://google.com"), "", token)); 44 | Assert.Equal (0, client.TimesCalled); 45 | } 46 | 47 | [Theory] 48 | [InlineData (typeof (AlwaysTimeoutClient), typeof (TimeoutRejectedException))] 49 | [InlineData (typeof (AlwaysThrowsClient), typeof (HttpRequestException))] 50 | [InlineData (typeof (AlwaysThrowsClient), typeof (IOException))] 51 | [InlineData (typeof (AlwaysThrowsClient), typeof (NotImplementedException), 0)] 52 | [InlineData (typeof (AlwaysThrowsClient), typeof (NullReferenceException), 0)] 53 | public async Task TimeoutPolicy (Type clientType, Type exceptionType, int expectedRetries = 5) 54 | { 55 | var writer = new StringWriter (); 56 | var boots = new Bootstrapper { 57 | NetworkRetries = 5, 58 | ReadWriteTimeout = TimeSpan.FromMilliseconds (1), 59 | Logger = writer, 60 | }; 61 | boots.UpdateActivePolicy (); 62 | 63 | using var client = (TestClient) Activator.CreateInstance (clientType, new object [] { boots }); 64 | client.ExceptionType = exceptionType; 65 | await Assert.ThrowsAsync (exceptionType, () => 66 | client.DownloadAsync (new Uri ("http://google.com"), "", CancellationToken.None)); 67 | Assert.Equal (expectedRetries + 1, client.TimesCalled); 68 | for (int i = 1; i <= expectedRetries; i++) { 69 | Assert.Contains ($"Retry attempt {i}: {exceptionType.FullName}", writer.ToString ()); 70 | } 71 | } 72 | 73 | class TestClient : HttpClientWithPolicy 74 | { 75 | public TestClient (Bootstrapper boots) : base (boots) { } 76 | 77 | public int TimesCalled { get; set; } 78 | 79 | public Type ExceptionType { get; set; } 80 | } 81 | 82 | class AlwaysTimeoutClient : TestClient 83 | { 84 | public AlwaysTimeoutClient (Bootstrapper boots) : base (boots) { } 85 | 86 | async Task Forever (CancellationToken token) 87 | { 88 | TimesCalled++; 89 | await Task.Delay (System.Threading.Timeout.Infinite, token); 90 | return default; 91 | } 92 | 93 | protected override Task DoDownloadAsync (Uri uri, string tempFile, CancellationToken token) => Forever (token); 94 | 95 | protected override Task DoGetJsonAsync (Uri uri, CancellationToken token) => Forever (token); 96 | 97 | protected override Task DoGetXmlDocumentAsync (Uri uri, CancellationToken token) => Forever (token); 98 | } 99 | 100 | class AlwaysThrowsClient : TestClient 101 | { 102 | public AlwaysThrowsClient (Bootstrapper boots) : base (boots) { } 103 | 104 | Task Throw () 105 | { 106 | TimesCalled++; 107 | throw (Exception) Activator.CreateInstance (ExceptionType); 108 | } 109 | 110 | protected override Task DoDownloadAsync (Uri uri, string tempFile, CancellationToken token) => Throw (); 111 | 112 | protected override Task DoGetJsonAsync (Uri uri, CancellationToken token) => Throw (); 113 | 114 | protected override Task DoGetXmlDocumentAsync (Uri uri, CancellationToken token) => Throw (); 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /Boots.Core/VsixInstaller.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.IO.Compression; 4 | using System.Text.Json; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | namespace Boots.Core 9 | { 10 | class VsixInstaller : Installer 11 | { 12 | /// 13 | /// See: https://stackoverflow.com/a/28212173 14 | /// 15 | const int AlreadyInstalledException = 1001; 16 | 17 | string? visualStudioDirectory; 18 | 19 | public VsixInstaller (Bootstrapper boots) : base (boots) { } 20 | 21 | public override string Extension => ".vsix"; 22 | 23 | public async override Task Install (string file, CancellationToken token = default) 24 | { 25 | if (string.IsNullOrEmpty (file)) 26 | throw new ArgumentException (nameof (file)); 27 | if (!File.Exists (file)) 28 | throw new FileNotFoundException ($"{Extension} file did not exist: {file}", file); 29 | 30 | var vs = await GetVisualStudioDirectory (token); 31 | var vsixInstaller = Path.Combine (vs, "Common7", "IDE", "VSIXInstaller.exe"); 32 | var log = Path.GetTempFileName (); 33 | try { 34 | if (Boots.DowngradeFirst) { 35 | await DowngradeVsix (file, vsixInstaller, token); 36 | } 37 | 38 | using var process = new AsyncProcess (Boots) { 39 | Command = vsixInstaller, 40 | Arguments = $"/quiet /logFile:{log} \"{file}\"", 41 | }; 42 | int exitCode = await process.RunAsync (token, throwOnError: false); 43 | if (exitCode == AlreadyInstalledException) { 44 | Boots.Logger.WriteLine ("VSIX already installed."); 45 | } else if (exitCode != 0) { 46 | process.ThrowForExitCode (exitCode); 47 | } 48 | } finally { 49 | await PrintLogFileAndDelete (log, token); 50 | } 51 | } 52 | 53 | async Task DowngradeVsix (string file, string vsixInstaller, CancellationToken token) 54 | { 55 | var log = Path.GetTempFileName (); 56 | 57 | try { 58 | if (GetVsixID (file) is not string id) { 59 | Boots.Logger.WriteLine ("Could not determine VSIX id to downgrade."); 60 | } else { 61 | Boots.Logger.WriteLine ($"Downgrading VSIX id {id}."); 62 | 63 | using var downgrade = new AsyncProcess (Boots) { 64 | Command = vsixInstaller, 65 | Arguments = $"/quiet /downgrade:{id} /logFile:{log}" 66 | }; 67 | 68 | var exitCode = await downgrade.RunAsync (token, throwOnError: false); 69 | 70 | if (exitCode != 0) { 71 | Boots.Logger.WriteLine ($"Downgrade failed, exit code {exitCode}."); 72 | } 73 | } 74 | } finally { 75 | await PrintLogFileAndDelete (log, token); 76 | } 77 | } 78 | 79 | async Task GetVisualStudioDirectory (CancellationToken token) 80 | { 81 | if (visualStudioDirectory != null) 82 | return visualStudioDirectory; 83 | 84 | var vsInstallDir = Environment.GetEnvironmentVariable ("VSINSTALLDIR"); 85 | if (string.IsNullOrEmpty (vsInstallDir)) { 86 | var programFiles = Environment.GetFolderPath (Environment.SpecialFolder.ProgramFilesX86); 87 | var vswhere = Path.Combine (programFiles, "Microsoft Visual Studio", "Installer", "vswhere.exe"); 88 | if (!File.Exists (vswhere)) 89 | throw new FileNotFoundException ("Cannot find vswhere.exe!", vswhere); 90 | using var process = new AsyncProcess (Boots) { 91 | Command = vswhere, 92 | Arguments = "-latest -products * -property installationPath", 93 | }; 94 | visualStudioDirectory = await process.RunWithOutputAsync (token); 95 | visualStudioDirectory = visualStudioDirectory.Trim (); 96 | if (!Directory.Exists (visualStudioDirectory)) { 97 | throw new DirectoryNotFoundException ($"vswhere.exe result returned a directory that did not exist: {visualStudioDirectory}"); 98 | } 99 | Boots.Logger.WriteLine ($"Using path from vswhere: {visualStudioDirectory}"); 100 | return visualStudioDirectory; 101 | } else { 102 | Boots.Logger.WriteLine ($"Using path from %VSINSTALLDIR%: {visualStudioDirectory}"); 103 | return visualStudioDirectory = vsInstallDir; 104 | } 105 | } 106 | 107 | string? GetVsixID (string vsix) 108 | { 109 | using var zip = ZipFile.OpenRead (vsix); 110 | 111 | var entry = zip.GetEntry ("manifest.json"); 112 | 113 | using var stream = entry.Open (); 114 | 115 | using var doc = JsonDocument.Parse (stream); 116 | 117 | return doc.RootElement.GetProperty ("id").GetString (); 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /Boots/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.CommandLine; 3 | using System.CommandLine.Invocation; 4 | using System.CommandLine.Parsing; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using Boots.Core; 8 | 9 | namespace Boots 10 | { 11 | class Program 12 | { 13 | static async Task Main (string [] args) 14 | { 15 | if (args.Length == 1 && IsUrl (args[0])) { 16 | await Run (args [0]); 17 | return; 18 | } 19 | 20 | const string options = "Options include: Xamarin.Android, Xamarin.iOS, Xamarin.Mac, and Mono."; 21 | var rootCommand = new RootCommand 22 | { 23 | new Option( 24 | "--url", 25 | "A URL to a pkg or vsix file to install") 26 | { 27 | Argument = new Argument() 28 | }, 29 | new Option( 30 | "--stable", 31 | $"Install the latest *stable* version of a product from VS manifests. {options}") 32 | { 33 | Argument = new Argument("product") 34 | }, 35 | new Option( 36 | "--preview", 37 | $"Install the latest *preview* version of a product from VS manifests. {options}") 38 | { 39 | Argument = new Argument("product") 40 | }, 41 | new Option( 42 | "--alpha", 43 | $"Install the latest *alpha* version of a product from VS manifests. This is only valid for Visual Studio for Mac. {options}") 44 | { 45 | Argument = new Argument("product") 46 | }, 47 | new Option( 48 | "--file-type", 49 | $"Specifies the type of file to be installed such as vsix, pkg, or msi. Defaults to vsix on Windows and pkg on macOS.") 50 | { 51 | Argument = new Argument("file-type") 52 | }, 53 | new Option ("--timeout", 54 | $"Specifies a timeout for HttpClient. If omitted, uses the .NET default of 100 seconds.") 55 | { 56 | Argument = new Argument("seconds") 57 | }, 58 | new Option ("--read-write-timeout", 59 | $"Specifies a timeout for reading/writing from a HttpClient stream. If omitted, uses a default of 300 seconds.") 60 | { 61 | Argument = new Argument("seconds") 62 | }, 63 | new Option ("--retries", 64 | $"Specifies a number of retries for HttpClient failures. If omitted, uses a default of 3 retries.") 65 | { 66 | Argument = new Argument("int") 67 | }, 68 | new Option ("--downgrade-first", 69 | $"Downgrades the existing vsix back to the shipped VS default before install. This allows an earlier vsix to be installed."), 70 | }; 71 | rootCommand.Name = "boots"; 72 | rootCommand.AddValidator (Validator); 73 | rootCommand.Description = $"boots {Version} File issues at: https://github.com/jonathanpeppers/boots/issues"; 74 | rootCommand.Handler = CommandHandler.Create (Run); 75 | await rootCommand.InvokeAsync (args); 76 | } 77 | 78 | static bool IsUrl (string value) 79 | { 80 | try { 81 | new Uri (value); 82 | return true; 83 | } catch (UriFormatException) { 84 | return false; 85 | } 86 | } 87 | 88 | static string Validator (CommandResult result) 89 | { 90 | if (result.OptionResult ("--url") == null && 91 | result.OptionResult ("--stable") == null && 92 | result.OptionResult ("--preview") == null && 93 | result.OptionResult ("--alpha") == null) { 94 | return "At least one of --url, --stable, --preview, or --alpha must be used"; 95 | } 96 | return ""; 97 | } 98 | 99 | static async Task Run ( 100 | string url, 101 | string stable = "", 102 | string preview = "", 103 | string alpha = "", 104 | FileType? fileType = null, 105 | double? timeout = null, 106 | double? readWriteTimeout = null, 107 | int? retries = null, 108 | bool downgradeFirst = false) 109 | { 110 | var cts = new CancellationTokenSource (); 111 | Console.CancelKeyPress += (sender, e) => cts.Cancel (); 112 | 113 | var boots = new Bootstrapper { 114 | Url = url, 115 | FileType = fileType, 116 | }; 117 | if (timeout != null) { 118 | boots.Timeout = TimeSpan.FromSeconds (timeout.Value); 119 | } 120 | if (readWriteTimeout != null) { 121 | boots.ReadWriteTimeout = TimeSpan.FromSeconds (readWriteTimeout.Value); 122 | } 123 | if (retries != null) { 124 | boots.NetworkRetries = retries.Value; 125 | } 126 | boots.DowngradeFirst = downgradeFirst; 127 | SetChannelAndProduct (boots, alpha, ReleaseChannel.Alpha); 128 | SetChannelAndProduct (boots, preview, ReleaseChannel.Preview); 129 | SetChannelAndProduct (boots, stable, ReleaseChannel.Stable); 130 | await boots.Install (cts.Token); 131 | } 132 | 133 | static void SetChannelAndProduct (Bootstrapper boots, string product, ReleaseChannel channel) 134 | { 135 | if (!string.IsNullOrEmpty (product)) { 136 | boots.Channel = channel; 137 | boots.Product = Enum.Parse (product.Replace (".", "")); 138 | } 139 | } 140 | 141 | static string? Version => typeof (Program).Assembly.GetName ().Version?.ToString (); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /samples/HelloForms.Android/HelloForms.Android.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Debug 5 | AnyCPU 6 | {85347CF0-7820-4941-A007-1209FC5A4F1C} 7 | {EFBA0AD7-5A72-4C68-AF49-83D382785DCF};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 8 | {c9e5eea5-ca05-42a1-839b-61506e0a37df} 9 | Library 10 | HelloForms.Droid 11 | HelloForms.Android 12 | True 13 | Resources\Resource.designer.cs 14 | Resource 15 | Properties\AndroidManifest.xml 16 | Resources 17 | Assets 18 | false 19 | v13.0 20 | true 21 | Xamarin.Android.Net.AndroidClientHandler 22 | 23 | 24 | 25 | 26 | true 27 | portable 28 | false 29 | bin\Debug 30 | DEBUG; 31 | prompt 32 | 4 33 | None 34 | 35 | 36 | true 37 | portable 38 | true 39 | bin\Release 40 | prompt 41 | 4 42 | true 43 | false 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | {DBD7A6AC-4854-4E4E-BE26-0F72FCF1F9F1} 94 | HelloForms 95 | 96 | 97 | 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /samples/HelloForms.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29102.190 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HelloForms.Android", "HelloForms.Android\HelloForms.Android.csproj", "{85347CF0-7820-4941-A007-1209FC5A4F1C}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HelloForms.iOS", "HelloForms.iOS\HelloForms.iOS.csproj", "{4FAC7CB7-B933-47C0-AE03-A69C9078ECE0}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HelloForms", "HelloForms\HelloForms.csproj", "{37B592BE-A3D9-47C9-813B-DFF003470775}" 11 | EndProject 12 | Global 13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 14 | Debug|Any CPU = Debug|Any CPU 15 | Debug|iPhone = Debug|iPhone 16 | Debug|iPhoneSimulator = Debug|iPhoneSimulator 17 | Release|Any CPU = Release|Any CPU 18 | Release|iPhone = Release|iPhone 19 | Release|iPhoneSimulator = Release|iPhoneSimulator 20 | EndGlobalSection 21 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 22 | {85347CF0-7820-4941-A007-1209FC5A4F1C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {85347CF0-7820-4941-A007-1209FC5A4F1C}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {85347CF0-7820-4941-A007-1209FC5A4F1C}.Debug|Any CPU.Deploy.0 = Debug|Any CPU 25 | {85347CF0-7820-4941-A007-1209FC5A4F1C}.Debug|iPhone.ActiveCfg = Debug|Any CPU 26 | {85347CF0-7820-4941-A007-1209FC5A4F1C}.Debug|iPhone.Build.0 = Debug|Any CPU 27 | {85347CF0-7820-4941-A007-1209FC5A4F1C}.Debug|iPhone.Deploy.0 = Debug|Any CPU 28 | {85347CF0-7820-4941-A007-1209FC5A4F1C}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU 29 | {85347CF0-7820-4941-A007-1209FC5A4F1C}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU 30 | {85347CF0-7820-4941-A007-1209FC5A4F1C}.Debug|iPhoneSimulator.Deploy.0 = Debug|Any CPU 31 | {85347CF0-7820-4941-A007-1209FC5A4F1C}.Release|Any CPU.ActiveCfg = Release|Any CPU 32 | {85347CF0-7820-4941-A007-1209FC5A4F1C}.Release|Any CPU.Build.0 = Release|Any CPU 33 | {85347CF0-7820-4941-A007-1209FC5A4F1C}.Release|Any CPU.Deploy.0 = Release|Any CPU 34 | {85347CF0-7820-4941-A007-1209FC5A4F1C}.Release|iPhone.ActiveCfg = Release|Any CPU 35 | {85347CF0-7820-4941-A007-1209FC5A4F1C}.Release|iPhone.Build.0 = Release|Any CPU 36 | {85347CF0-7820-4941-A007-1209FC5A4F1C}.Release|iPhone.Deploy.0 = Release|Any CPU 37 | {85347CF0-7820-4941-A007-1209FC5A4F1C}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU 38 | {85347CF0-7820-4941-A007-1209FC5A4F1C}.Release|iPhoneSimulator.Build.0 = Release|Any CPU 39 | {85347CF0-7820-4941-A007-1209FC5A4F1C}.Release|iPhoneSimulator.Deploy.0 = Release|Any CPU 40 | {4FAC7CB7-B933-47C0-AE03-A69C9078ECE0}.Debug|Any CPU.ActiveCfg = Debug|iPhoneSimulator 41 | {4FAC7CB7-B933-47C0-AE03-A69C9078ECE0}.Debug|Any CPU.Build.0 = Debug|iPhoneSimulator 42 | {4FAC7CB7-B933-47C0-AE03-A69C9078ECE0}.Debug|Any CPU.Deploy.0 = Debug|iPhoneSimulator 43 | {4FAC7CB7-B933-47C0-AE03-A69C9078ECE0}.Debug|iPhone.ActiveCfg = Debug|iPhone 44 | {4FAC7CB7-B933-47C0-AE03-A69C9078ECE0}.Debug|iPhone.Build.0 = Debug|iPhone 45 | {4FAC7CB7-B933-47C0-AE03-A69C9078ECE0}.Debug|iPhone.Deploy.0 = Debug|iPhone 46 | {4FAC7CB7-B933-47C0-AE03-A69C9078ECE0}.Debug|iPhoneSimulator.ActiveCfg = Debug|iPhoneSimulator 47 | {4FAC7CB7-B933-47C0-AE03-A69C9078ECE0}.Debug|iPhoneSimulator.Build.0 = Debug|iPhoneSimulator 48 | {4FAC7CB7-B933-47C0-AE03-A69C9078ECE0}.Debug|iPhoneSimulator.Deploy.0 = Debug|iPhoneSimulator 49 | {4FAC7CB7-B933-47C0-AE03-A69C9078ECE0}.Release|Any CPU.ActiveCfg = Release|iPhone 50 | {4FAC7CB7-B933-47C0-AE03-A69C9078ECE0}.Release|Any CPU.Build.0 = Release|iPhone 51 | {4FAC7CB7-B933-47C0-AE03-A69C9078ECE0}.Release|Any CPU.Deploy.0 = Release|iPhone 52 | {4FAC7CB7-B933-47C0-AE03-A69C9078ECE0}.Release|iPhone.ActiveCfg = Release|iPhone 53 | {4FAC7CB7-B933-47C0-AE03-A69C9078ECE0}.Release|iPhone.Build.0 = Release|iPhone 54 | {4FAC7CB7-B933-47C0-AE03-A69C9078ECE0}.Release|iPhone.Deploy.0 = Release|iPhone 55 | {4FAC7CB7-B933-47C0-AE03-A69C9078ECE0}.Release|iPhoneSimulator.ActiveCfg = Release|iPhoneSimulator 56 | {4FAC7CB7-B933-47C0-AE03-A69C9078ECE0}.Release|iPhoneSimulator.Build.0 = Release|iPhoneSimulator 57 | {4FAC7CB7-B933-47C0-AE03-A69C9078ECE0}.Release|iPhoneSimulator.Deploy.0 = Release|iPhoneSimulator 58 | {37B592BE-A3D9-47C9-813B-DFF003470775}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 59 | {37B592BE-A3D9-47C9-813B-DFF003470775}.Debug|Any CPU.Build.0 = Debug|Any CPU 60 | {37B592BE-A3D9-47C9-813B-DFF003470775}.Debug|Any CPU.Deploy.0 = Debug|Any CPU 61 | {37B592BE-A3D9-47C9-813B-DFF003470775}.Debug|iPhone.ActiveCfg = Debug|Any CPU 62 | {37B592BE-A3D9-47C9-813B-DFF003470775}.Debug|iPhone.Build.0 = Debug|Any CPU 63 | {37B592BE-A3D9-47C9-813B-DFF003470775}.Debug|iPhone.Deploy.0 = Debug|Any CPU 64 | {37B592BE-A3D9-47C9-813B-DFF003470775}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU 65 | {37B592BE-A3D9-47C9-813B-DFF003470775}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU 66 | {37B592BE-A3D9-47C9-813B-DFF003470775}.Debug|iPhoneSimulator.Deploy.0 = Debug|Any CPU 67 | {37B592BE-A3D9-47C9-813B-DFF003470775}.Release|Any CPU.ActiveCfg = Release|Any CPU 68 | {37B592BE-A3D9-47C9-813B-DFF003470775}.Release|Any CPU.Build.0 = Release|Any CPU 69 | {37B592BE-A3D9-47C9-813B-DFF003470775}.Release|Any CPU.Deploy.0 = Release|Any CPU 70 | {37B592BE-A3D9-47C9-813B-DFF003470775}.Release|iPhone.ActiveCfg = Release|Any CPU 71 | {37B592BE-A3D9-47C9-813B-DFF003470775}.Release|iPhone.Build.0 = Release|Any CPU 72 | {37B592BE-A3D9-47C9-813B-DFF003470775}.Release|iPhone.Deploy.0 = Release|Any CPU 73 | {37B592BE-A3D9-47C9-813B-DFF003470775}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU 74 | {37B592BE-A3D9-47C9-813B-DFF003470775}.Release|iPhoneSimulator.Build.0 = Release|Any CPU 75 | {37B592BE-A3D9-47C9-813B-DFF003470775}.Release|iPhoneSimulator.Deploy.0 = Release|Any CPU 76 | EndGlobalSection 77 | GlobalSection(SolutionProperties) = preSolution 78 | HideSolutionNode = FALSE 79 | EndGlobalSection 80 | GlobalSection(ExtensibilityGlobals) = postSolution 81 | SolutionGuid = {30AE2AC7-F48F-4ED3-B48A-7A577830F940} 82 | EndGlobalSection 83 | EndGlobal 84 | -------------------------------------------------------------------------------- /samples/HelloForms.iOS/HelloForms.iOS.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Debug 5 | iPhoneSimulator 6 | 8.0.30703 7 | 2.0 8 | {4FAC7CB7-B933-47C0-AE03-A69C9078ECE0} 9 | {FEACFBD2-3405-455C-9665-78FE426C6842};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 10 | {6143fdea-f3c2-4a09-aafa-6e230626515e} 11 | Exe 12 | HelloForms.iOS 13 | Resources 14 | HelloForms.iOS 15 | true 16 | NSUrlSessionHandler 17 | 18 | 19 | true 20 | full 21 | false 22 | bin\iPhoneSimulator\Debug 23 | DEBUG 24 | prompt 25 | 4 26 | false 27 | x86_64 28 | None 29 | true 30 | 31 | 32 | none 33 | true 34 | bin\iPhoneSimulator\Release 35 | prompt 36 | 4 37 | None 38 | x86_64 39 | false 40 | 41 | 42 | true 43 | full 44 | false 45 | bin\iPhone\Debug 46 | DEBUG 47 | prompt 48 | 4 49 | false 50 | ARM64 51 | iPhone Developer 52 | true 53 | Entitlements.plist 54 | 55 | 56 | none 57 | true 58 | bin\iPhone\Release 59 | prompt 60 | 4 61 | ARM64 62 | false 63 | iPhone Developer 64 | Entitlements.plist 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | false 77 | 78 | 79 | false 80 | 81 | 82 | false 83 | 84 | 85 | false 86 | 87 | 88 | false 89 | 90 | 91 | false 92 | 93 | 94 | false 95 | 96 | 97 | false 98 | 99 | 100 | false 101 | 102 | 103 | false 104 | 105 | 106 | false 107 | 108 | 109 | false 110 | 111 | 112 | false 113 | 114 | 115 | false 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | {DBD7A6AC-4854-4E4E-BE26-0F72FCF1F9F1} 134 | HelloForms 135 | 136 | 137 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | 33 | # Visual Studio 2015/2017 cache/options directory 34 | .vs/ 35 | # Uncomment if you have tasks that create the project's static files in wwwroot 36 | #wwwroot/ 37 | 38 | # Visual Studio 2017 auto generated files 39 | Generated\ Files/ 40 | 41 | # MSTest test Results 42 | [Tt]est[Rr]esult*/ 43 | [Bb]uild[Ll]og.* 44 | 45 | # NUnit 46 | *.VisualState.xml 47 | TestResult.xml 48 | nunit-*.xml 49 | 50 | # Build Results of an ATL Project 51 | [Dd]ebugPS/ 52 | [Rr]eleasePS/ 53 | dlldata.c 54 | 55 | # Benchmark Results 56 | BenchmarkDotNet.Artifacts/ 57 | 58 | # .NET Core 59 | project.lock.json 60 | project.fragment.lock.json 61 | artifacts/ 62 | 63 | # StyleCop 64 | StyleCopReport.xml 65 | 66 | # Files built by Visual Studio 67 | *_i.c 68 | *_p.c 69 | *_h.h 70 | *.ilk 71 | *.meta 72 | *.obj 73 | *.iobj 74 | *.pch 75 | *.pdb 76 | *.ipdb 77 | *.pgc 78 | *.pgd 79 | *.rsp 80 | *.sbr 81 | *.tlb 82 | *.tli 83 | *.tlh 84 | *.tmp 85 | *.tmp_proj 86 | *_wpftmp.csproj 87 | *.log 88 | *.vspscc 89 | *.vssscc 90 | .builds 91 | *.pidb 92 | *.svclog 93 | *.scc 94 | 95 | # Chutzpah Test files 96 | _Chutzpah* 97 | 98 | # Visual C++ cache files 99 | ipch/ 100 | *.aps 101 | *.ncb 102 | *.opendb 103 | *.opensdf 104 | *.sdf 105 | *.cachefile 106 | *.VC.db 107 | *.VC.VC.opendb 108 | 109 | # Visual Studio profiler 110 | *.psess 111 | *.vsp 112 | *.vspx 113 | *.sap 114 | 115 | # Visual Studio Trace Files 116 | *.e2e 117 | 118 | # TFS 2012 Local Workspace 119 | $tf/ 120 | 121 | # Guidance Automation Toolkit 122 | *.gpState 123 | 124 | # ReSharper is a .NET coding add-in 125 | _ReSharper*/ 126 | *.[Rr]e[Ss]harper 127 | *.DotSettings.user 128 | 129 | # JustCode is a .NET coding add-in 130 | .JustCode 131 | 132 | # TeamCity is a build add-in 133 | _TeamCity* 134 | 135 | # DotCover is a Code Coverage Tool 136 | *.dotCover 137 | 138 | # AxoCover is a Code Coverage Tool 139 | .axoCover/* 140 | !.axoCover/settings.json 141 | 142 | # Visual Studio code coverage results 143 | *.coverage 144 | *.coveragexml 145 | 146 | # NCrunch 147 | _NCrunch_* 148 | .*crunch*.local.xml 149 | nCrunchTemp_* 150 | 151 | # MightyMoose 152 | *.mm.* 153 | AutoTest.Net/ 154 | 155 | # Web workbench (sass) 156 | .sass-cache/ 157 | 158 | # Installshield output folder 159 | [Ee]xpress/ 160 | 161 | # DocProject is a documentation generator add-in 162 | DocProject/buildhelp/ 163 | DocProject/Help/*.HxT 164 | DocProject/Help/*.HxC 165 | DocProject/Help/*.hhc 166 | DocProject/Help/*.hhk 167 | DocProject/Help/*.hhp 168 | DocProject/Help/Html2 169 | DocProject/Help/html 170 | 171 | # Click-Once directory 172 | publish/ 173 | 174 | # Publish Web Output 175 | *.[Pp]ublish.xml 176 | *.azurePubxml 177 | # Note: Comment the next line if you want to checkin your web deploy settings, 178 | # but database connection strings (with potential passwords) will be unencrypted 179 | *.pubxml 180 | *.publishproj 181 | 182 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 183 | # checkin your Azure Web App publish settings, but sensitive information contained 184 | # in these scripts will be unencrypted 185 | PublishScripts/ 186 | 187 | # NuGet Packages 188 | *.nupkg 189 | # NuGet Symbol Packages 190 | *.snupkg 191 | # The packages folder can be ignored because of Package Restore 192 | **/[Pp]ackages/* 193 | # except build/, which is used as an MSBuild target. 194 | !**/[Pp]ackages/build/ 195 | # Uncomment if necessary however generally it will be regenerated when needed 196 | #!**/[Pp]ackages/repositories.config 197 | # NuGet v3's project.json files produces more ignorable files 198 | *.nuget.props 199 | *.nuget.targets 200 | 201 | # Microsoft Azure Build Output 202 | csx/ 203 | *.build.csdef 204 | 205 | # Microsoft Azure Emulator 206 | ecf/ 207 | rcf/ 208 | 209 | # Windows Store app package directories and files 210 | AppPackages/ 211 | BundleArtifacts/ 212 | Package.StoreAssociation.xml 213 | _pkginfo.txt 214 | *.appx 215 | *.appxbundle 216 | *.appxupload 217 | 218 | # Visual Studio cache files 219 | # files ending in .cache can be ignored 220 | *.[Cc]ache 221 | # but keep track of directories ending in .cache 222 | !?*.[Cc]ache/ 223 | 224 | # Others 225 | ClientBin/ 226 | ~$* 227 | *~ 228 | *.dbmdl 229 | *.dbproj.schemaview 230 | *.jfm 231 | *.pfx 232 | *.publishsettings 233 | orleans.codegen.cs 234 | 235 | # Including strong name files can present a security risk 236 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 237 | #*.snk 238 | 239 | # Since there are multiple workflows, uncomment next line to ignore bower_components 240 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 241 | #bower_components/ 242 | 243 | # RIA/Silverlight projects 244 | Generated_Code/ 245 | 246 | # Backup & report files from converting an old project file 247 | # to a newer Visual Studio version. Backup files are not needed, 248 | # because we have git ;-) 249 | _UpgradeReport_Files/ 250 | Backup*/ 251 | UpgradeLog*.XML 252 | UpgradeLog*.htm 253 | ServiceFabricBackup/ 254 | *.rptproj.bak 255 | 256 | # SQL Server files 257 | *.mdf 258 | *.ldf 259 | *.ndf 260 | 261 | # Business Intelligence projects 262 | *.rdl.data 263 | *.bim.layout 264 | *.bim_*.settings 265 | *.rptproj.rsuser 266 | *- [Bb]ackup.rdl 267 | *- [Bb]ackup ([0-9]).rdl 268 | *- [Bb]ackup ([0-9][0-9]).rdl 269 | 270 | # Microsoft Fakes 271 | FakesAssemblies/ 272 | 273 | # GhostDoc plugin setting file 274 | *.GhostDoc.xml 275 | 276 | # Node.js Tools for Visual Studio 277 | .ntvs_analysis.dat 278 | node_modules/ 279 | 280 | # Visual Studio 6 build log 281 | *.plg 282 | 283 | # Visual Studio 6 workspace options file 284 | *.opt 285 | 286 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 287 | *.vbw 288 | 289 | # Visual Studio LightSwitch build output 290 | **/*.HTMLClient/GeneratedArtifacts 291 | **/*.DesktopClient/GeneratedArtifacts 292 | **/*.DesktopClient/ModelManifest.xml 293 | **/*.Server/GeneratedArtifacts 294 | **/*.Server/ModelManifest.xml 295 | _Pvt_Extensions 296 | 297 | # Paket dependency manager 298 | .paket/paket.exe 299 | paket-files/ 300 | 301 | # FAKE - F# Make 302 | .fake/ 303 | 304 | # CodeRush personal settings 305 | .cr/personal 306 | 307 | # Python Tools for Visual Studio (PTVS) 308 | __pycache__/ 309 | *.pyc 310 | 311 | # Cake - Uncomment if you are using it 312 | # tools/** 313 | # !tools/packages.config 314 | 315 | # Tabs Studio 316 | *.tss 317 | 318 | # Telerik's JustMock configuration file 319 | *.jmconfig 320 | 321 | # BizTalk build output 322 | *.btp.cs 323 | *.btm.cs 324 | *.odx.cs 325 | *.xsd.cs 326 | 327 | # OpenCover UI analysis results 328 | OpenCover/ 329 | 330 | # Azure Stream Analytics local run output 331 | ASALocalRun/ 332 | 333 | # MSBuild Binary and Structured Log 334 | *.binlog 335 | 336 | # NVidia Nsight GPU debugger configuration file 337 | *.nvuser 338 | 339 | # MFractors (Xamarin productivity tool) working folder 340 | .mfractor/ 341 | 342 | # Local History for Visual Studio 343 | .localhistory/ 344 | 345 | # BeatPulse healthcheck temp database 346 | healthchecksdb 347 | 348 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 349 | MigrationBackup/ 350 | 351 | ### Code ### 352 | .vscode/* 353 | !.vscode/settings.json 354 | !.vscode/tasks.json 355 | !.vscode/launch.json 356 | !.vscode/extensions.json 357 | 358 | # Xamarin 359 | Resource.designer.cs 360 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | ############################### 2 | # Core EditorConfig Options # 3 | ############################### 4 | 5 | root = true 6 | 7 | # All files 8 | [*] 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | 12 | # MSBuild 13 | [*.{csproj,proj,projitems,shproj,fsproj,targets,props}] 14 | indent_style = space 15 | indent_size = 2 16 | 17 | # XML config files 18 | [*.{xml,axml,xaml,config,nuspec,resx}] 19 | indent_style = space 20 | indent_size = 2 21 | 22 | # JSON files 23 | [*.json] 24 | indent_style = space 25 | indent_size = 2 26 | 27 | # F# files 28 | [*.{fs, fsx, fsi}] 29 | indent_style = space 30 | indent_size = 4 31 | 32 | # Code files 33 | [*.{cs,csx,vb,vbx}] 34 | insert_final_newline = true 35 | indent_style = tab 36 | tab_width = 8 37 | indent_size = 8 38 | max_line_length = 180 39 | 40 | ############################### 41 | # .NET Coding Conventions # 42 | ############################### 43 | 44 | [*.{cs,vb}] 45 | # Organize usings 46 | dotnet_sort_system_directives_first = true 47 | dotnet_separate_import_directive_groups = false 48 | 49 | # Avoid "this." and "Me." if not necessary 50 | dotnet_style_qualification_for_field = false:suggestion 51 | dotnet_style_qualification_for_property = false:suggestion 52 | dotnet_style_qualification_for_method = false:suggestion 53 | dotnet_style_qualification_for_event = false:suggestion 54 | 55 | # Use language keywords instead of framework type names for type references 56 | dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion 57 | dotnet_style_predefined_type_for_member_access = true:suggestion 58 | 59 | # Suggest more modern language features when available 60 | dotnet_style_object_initializer = true:suggestion 61 | dotnet_style_collection_initializer = true:suggestion 62 | dotnet_style_coalesce_expression = true:suggestion 63 | dotnet_style_null_propagation = true:suggestion 64 | dotnet_style_explicit_tuple_names = true:suggestion 65 | 66 | # Parentheses preferences 67 | dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent 68 | dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent 69 | dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent 70 | dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent 71 | 72 | # Avoid redundant accessibility modifiers when they're default 73 | dotnet_style_require_accessibility_modifiers = omit_if_default:suggestion 74 | dotnet_style_readonly_field = true:suggestion 75 | 76 | # Expression-level preferences 77 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion 78 | dotnet_style_prefer_inferred_tuple_names = true:suggestion 79 | dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion 80 | dotnet_style_prefer_auto_properties = true:silent 81 | dotnet_style_prefer_conditional_expression_over_assignment = true:silent 82 | dotnet_style_prefer_conditional_expression_over_return = true:silent 83 | 84 | ############################### 85 | # Naming Conventions # 86 | ############################### 87 | 88 | # Style Definitions 89 | dotnet_naming_style.pascal_case_style.capitalization = pascal_case 90 | 91 | dotnet_naming_style.underline_separator.word_separator = _ 92 | dotnet_naming_style.underline_separator.capitalization = all_lower 93 | 94 | # Symbol Definitions 95 | dotnet_naming_symbols.parameters.applicable_kinds = parameter 96 | dotnet_naming_symbols.parameters.applicable_accessibilities = * 97 | 98 | dotnet_naming_symbols.fields.applicable_kinds = field 99 | 100 | dotnet_naming_symbols.constant_fields.applicable_kinds = field 101 | dotnet_naming_symbols.constant_fields.applicable_accessibilities = * 102 | dotnet_naming_symbols.constant_fields.required_modifiers = const 103 | 104 | # Use CamelCase for parameters 105 | dotnet_naming_rule.method_parameters_should_be_camel_case.severity = suggestion 106 | dotnet_naming_rule.method_parameters_should_be_camel_case.symbols = parameters 107 | dotnet_naming_rule.method_parameters_should_be_camel_case.style = camel_case 108 | 109 | # Use PascalCase for constant fields 110 | dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion 111 | dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields 112 | dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style 113 | 114 | # Use underline separator for instance fields 115 | dotnet_naming_rule.fields_should_be_underline_separator.severity = suggestion 116 | dotnet_naming_rule.fields_should_be_underline_separator.symbols = fields 117 | dotnet_naming_rule.fields_should_be_underline_separator.style = underline_separator 118 | 119 | 120 | ############################### 121 | # C# Code Style Rules # 122 | ############################### 123 | 124 | [*.cs] 125 | # var preferences 126 | csharp_style_var_for_built_in_types = true:silent 127 | csharp_style_var_when_type_is_apparent = true:silent 128 | csharp_style_var_elsewhere = true:silent 129 | 130 | # Expression-bodied members 131 | csharp_style_expression_bodied_methods = false:silent 132 | csharp_style_expression_bodied_constructors = false:silent 133 | csharp_style_expression_bodied_operators = false:silent 134 | csharp_style_expression_bodied_properties = true:silent 135 | csharp_style_expression_bodied_indexers = true:silent 136 | csharp_style_expression_bodied_accessors = true:silent 137 | 138 | # Pattern-matching preferences 139 | csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion 140 | csharp_style_pattern_matching_over_as_with_null_check = true:suggestion 141 | 142 | # Null-checking preferences 143 | csharp_style_throw_expression = true:suggestion 144 | csharp_style_conditional_delegate_call = true:suggestion 145 | 146 | # Modifier preferences 147 | csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion 148 | 149 | # Expression-level preferences 150 | csharp_prefer_braces = true:silent 151 | csharp_style_deconstructed_variable_declaration = true:suggestion 152 | csharp_prefer_simple_default_expression = true:suggestion 153 | csharp_style_pattern_local_over_anonymous_function = true:suggestion 154 | csharp_style_inlined_variable_declaration = true:suggestion 155 | 156 | ############################### 157 | # C# Formatting Rules # 158 | ############################### 159 | 160 | # Newline settings 161 | csharp_new_line_before_open_brace = methods,types 162 | csharp_new_line_before_else = false 163 | csharp_new_line_before_catch = false 164 | csharp_new_line_before_finally = false 165 | csharp_new_line_before_members_in_object_initializers = true 166 | csharp_new_line_before_members_in_anonymous_types = true 167 | 168 | # Indentation preferences 169 | csharp_indent_switch_labels = false 170 | csharp_indent_case_contents = true 171 | csharp_indent_switch_labels = true 172 | 173 | # Space preferences 174 | csharp_space_after_cast = true 175 | csharp_space_after_keywords_in_control_flow_statements = true 176 | csharp_space_between_method_call_parameter_list_parentheses = false 177 | csharp_space_between_method_declaration_parameter_list_parentheses = false 178 | csharp_space_between_parentheses = false 179 | csharp_space_before_colon_in_inheritance_clause = true 180 | csharp_space_after_colon_in_inheritance_clause = true 181 | csharp_space_around_binary_operators = before_and_after 182 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false 183 | csharp_space_between_method_call_name_and_opening_parenthesis = true 184 | csharp_space_between_method_call_empty_parameter_list_parentheses = false 185 | csharp_space_between_method_declaration_name_and_open_parenthesis = true 186 | csharp_space_after_keywords_in_control_flow_statements = true 187 | csharp_space_before_open_square_brackets = true 188 | 189 | # Wrapping preferences 190 | csharp_preserve_single_line_statements = true 191 | csharp_preserve_single_line_blocks = true 192 | 193 | ################################## 194 | # Visual Basic Code Style Rules # 195 | ################################## 196 | 197 | [*.vb] 198 | # Modifier preferences 199 | visual_basic_preferred_modifier_order = Partial,Default,Private,Protected,Public,Friend,NotOverridable,Overridable,MustOverride,Overloads,Overrides,MustInherit,NotInheritable,Static,Shared,Shadows,ReadOnly,WriteOnly,Dim,Const,WithEvents,Widening,Narrowing,Custom,Async:suggestion 200 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # boots 2 | 3 | ![boots](icon.png) 4 | 5 | | NuGet | 6 | | -- | 7 | | `boots`
[![NuGet](https://img.shields.io/nuget/dt/boots.svg)](https://www.nuget.org/packages/boots) | 8 | | `Cake.Boots`
[![NuGet](https://img.shields.io/nuget/dt/Cake.Boots.svg)](https://www.nuget.org/packages/Cake.Boots) | 9 | 10 | | Azure DevOps | App Center | Github Actions | Bitrise | 11 | | -- | -- | -- | -- | 12 | | [![Build Status](https://dev.azure.com/jonathanpeppers/boots/_apis/build/status/jonathanpeppers.boots?branchName=main)](https://dev.azure.com/jonathanpeppers/boots/_build/latest?definitionId=1&branchName=main) | [![AppCenter](https://build.appcenter.ms/v0.1/apps/87931b9c-e617-4fb7-bfa9-9bfd74f39abb/branches/main/badge)][appcenter] | [![Github Actions](https://github.com/jonathanpeppers/boots/workflows/GitHub%20Actions/badge.svg)](https://github.com/jonathanpeppers/boots/actions) | [![Bitrise](https://app.bitrise.io/app/bb148b2cc62339da/status.svg?token=TEhuHdoNElmh2w8uQ-mYcQ&branch=main)](https://app.bitrise.io/app/bb148b2cc62339da) | 13 | 14 | `boots` is a [.NET global tool](https://docs.microsoft.com/en-us/dotnet/core/tools/global-tools) for "bootstrapping" vsix & pkg files. 15 | 16 | `boots` is useful for pinning a version of Mono, Xamarin, etc. when building projects on [Azure DevOps Hosted Agents](https://docs.microsoft.com/en-us/azure/devops/pipelines/agents/agents?view=azure-devops). You don't get to _choose_ what versions of things are installed on each agent, so it makes sense to install things yourself for reproducible builds. It also allows you to install preview versions of things (or more recent!) before they come preinstalled on Hosted build agents. 17 | 18 | ## Use it 19 | 20 | ```bash 21 | dotnet tool install --global boots 22 | boots https://url/to/your/package 23 | ``` 24 | 25 | `boots` currently supports Windows & Mac OSX, therefore: 26 | 27 | * On Windows - assumes the file is a `.vsix` and installs it into all instances of Visual Studio via `VSIXInstaller.exe`. 28 | * On Mac OSX - assumes the file is a `.pkg` and installs it 29 | 30 | ### Builds from Stable & Preview Visual Studio Channels 31 | 32 | By querying the Visual Studio updater manifests, `boots` 1.0.2 or 33 | higher allows you to install the latest versions of Xamarin or Mono 34 | from the stable or preview channels. 35 | 36 | Some examples: 37 | 38 | ```bash 39 | boots --stable Mono 40 | boots --preview Xamarin.Android 41 | boots --preview Xamarin.iOS 42 | boots --preview Xamarin.Mac 43 | ``` 44 | 45 | This would install the latest stable Mono and the latest previews for Xamarin.Android, Xamarin.iOS, and Xamarin.Mac. 46 | 47 | ### Cake 48 | 49 | You can also use `boots` from a [Cake][cake] script: 50 | 51 | ```csharp 52 | #addin nuget:?package=Cake.Boots&version=1.0.3.556 53 | 54 | Task("Boots") 55 | .Does(async () => 56 | { 57 | if (!IsRunningOnWindows ()) { 58 | await Boots (Product.XamarinMac, ReleaseChannel.Stable); 59 | await Boots (Product.XamariniOS, ReleaseChannel.Preview); 60 | } 61 | await Boots (Product.XamarinAndroid, ReleaseChannel.Preview); 62 | }); 63 | ``` 64 | 65 | If you omit the second `ReleaseChannel` parameter, it will default to `ReleaseChannel.Stable`. 66 | 67 | > NOTE! if you need to install Mono, do this in a separate process from the rest of your Cake build. 68 | 69 | For example: 70 | 71 | ```csharp 72 | Task("Mono") 73 | .Does(async () => 74 | { 75 | await Boots (Product.Mono); 76 | }); 77 | ``` 78 | 79 | Then invoke Cake twice: 80 | 81 | ```bash 82 | dotnet cake --target=Mono 83 | dotnet cake --target=Boots 84 | ``` 85 | 86 | [cake]: https://cakebuild.net/ 87 | 88 | ### Network Resiliency 89 | 90 | CI systems are somewhat notorious for random networking failures. 91 | Starting in boots 1.0.4, you can control some of this behavior: 92 | 93 | ``` 94 | --timeout Specifies a timeout for HttpClient. If omitted, uses the .NET default of 100 seconds. 95 | --read-write-timeout Specifies a timeout for reading/writing from a HttpClient stream. If omitted, uses a default of 300 seconds. 96 | --retries Specifies a number of retries for HttpClient failures. If omitted, uses a default of 3 retries. 97 | ``` 98 | 99 | This can also be defined in a [Cake][cake] script with the 100 | `BootsSettings` class: 101 | 102 | ```csharp 103 | var settings = new BootsSettings { 104 | Channel = ReleaseChannel.Stable, 105 | Product = Product.XamarinAndroid, 106 | Timeout = TimeSpan.FromSeconds (100), 107 | ReadWriteTimeout = TimeSpan.FromMinutes (5), 108 | NetworkRetries = 3, 109 | }; 110 | await Boots (settings); 111 | ``` 112 | 113 | ### Use the Azure Pipeline Extension Task 114 | 115 | Install the extension into your DevOps instance and add the task to a build or release, or use it from YAML: 116 | 117 | ```yaml 118 | steps: 119 | - task: Boots@1 120 | displayName: Install Xamarin.Android 121 | inputs: 122 | uri: https://aka.ms/xamarin-android-commercial-d16-4-windows 123 | ``` 124 | 125 | You can install the [Boots Extension from the VS Marketplace](https://marketplace.visualstudio.com/items?itemName=pjcollins.azp-utilities-boots). 126 | 127 | See the [Boots Task Extension Source](https://github.com/pjcollins/azure-web-extensions#use-in-your-yaml-pipeline) for more details. 128 | 129 | If you don't want to use the extension, alternatively you can: 130 | 131 | ```yaml 132 | variables: 133 | DOTNET_CLI_TELEMETRY_OPTOUT: true 134 | steps: 135 | - script: | 136 | dotnet tool install --global boots 137 | boots https://aka.ms/xamarin-android-commercial-d16-4-windows 138 | ``` 139 | 140 | `DOTNET_CLI_TELEMETRY_OPTOUT` is optional. 141 | 142 | `DOTNET_SKIP_FIRST_TIME_EXPERIENCE` is also a good idea if you are running on a .NET Core older than 3.0. 143 | 144 | ## Finding Builds 145 | 146 | For more information on how to source urls for each platform, see [how to find builds](docs/HowToFindBuilds.md) 147 | 148 | ## Some Examples 149 | 150 | Install Mono, Xamarin.Android, and Xamarin.iOS on Mac OSX: 151 | 152 | ```bash 153 | boots https://download.mono-project.com/archive/6.4.0/macos-10-universal/MonoFramework-MDK-6.4.0.198.macos10.xamarin.universal.pkg 154 | boots https://aka.ms/xamarin-android-commercial-d16-4-macos 155 | boots https://download.visualstudio.microsoft.com/download/pr/5a678460-107f-4fcf-8764-80419bc874a0/3f78c6826132f6f8569524690322adba/xamarin.ios-13.8.1.17.pkg 156 | ``` 157 | 158 | Install Xamarin.Android on Windows: 159 | 160 | ```cmd 161 | boots https://aka.ms/xamarin-android-commercial-d16-4-windows 162 | ``` 163 | 164 | ### System.CommandLine 165 | 166 | `boots` now uses `System.CommandLine`, so we get rich help text for free: 167 | 168 | ``` 169 | > boots --help 170 | boots: 171 | boots 1.0.x File issues at: https://github.com/jonathanpeppers/boots/issues 172 | 173 | Usage: 174 | boots [options] 175 | 176 | Options: 177 | --url A URL to a pkg or vsix file to install 178 | --stable Install the latest *stable* version of a product from VS manifests. Options include: Xamarin.Android, Xamarin.iOS, Xamarin.Mac, and Mono. 179 | --preview Install the latest *preview* version of a product from VS manifests. Options include: Xamarin.Android, Xamarin.iOS, Xamarin.Mac, and Mono. 180 | --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. 181 | --timeout Specifies a timeout for HttpClient. If omitted, uses the .NET default of 100 seconds. 182 | --read-write-timeout Specifies a timeout for reading/writing from a HttpClient stream. If omitted, uses a default of 300 seconds. 183 | --retries Specifies a number of retries for HttpClient failures. If omitted, uses a default of 3 retries. 184 | --version Show version information 185 | -?, -h, --help Show help and usage information 186 | ``` 187 | 188 | ### App Center 189 | 190 | `samples/HelloForms.sln` is a "Hello World" Xamarin.Forms project configured with `boots` installing newer versions than what is available on [App Center][appcenter]: 191 | 192 | ![AppCenter](docs/AppCenter.png) 193 | 194 | See [`appcenter-pre-build.sh`](samples/HelloForms.Android/appcenter-pre-build.sh) in this repo for an example of setting up `boots`. See the [App Center docs](https://aka.ms/docs/build/custom/scripts) for further detail about custom build scripts. 195 | 196 | ### GitHub Actions 197 | 198 | I was able to get `boots` to work on both Windows & macOS agents in [Github Actions][actions]. 199 | 200 | See [`actions.yml`](.github/workflows/actions.yml) for an example. 201 | 202 | ### Other CI Systems 203 | 204 | `boots` has been tested, and appears to work fine on: 205 | 206 | * [Azure DevOps](https://azure.microsoft.com/en-us/services/devops/) 207 | * [App Center][appcenter] 208 | * [AppVeyor](https://www.appveyor.com/) 209 | * [Bitrise](https://www.bitrise.io/) 210 | * [Github Actions][actions] 211 | * [Travis CI](https://travis-ci.org/) 212 | 213 | Any build environment that can be configured to run .NET Core 2.1, can run `boots`. If you have success on other CI systems, let us know! 214 | 215 | [appcenter]: https://appcenter.ms 216 | [actions]: https://github.com/features/actions 217 | --------------------------------------------------------------------------------