├── .github ├── ISSUE_TEMPLATE │ ├── bug-report.yml │ └── config.yml ├── dependabot.yml └── workflows │ └── main.yml ├── .gitignore ├── Directory.Build.props ├── DotnetRuntimeBootstrapper.AppHost.Cli ├── Bootstrapper.cs ├── DotnetRuntimeBootstrapper.AppHost.Cli.csproj └── Utils │ ├── ConsoleEx.cs │ └── ConsoleProgress.cs ├── DotnetRuntimeBootstrapper.AppHost.Core ├── App.config ├── BootstrapperBase.cs ├── BootstrapperConfiguration.cs ├── BootstrapperException.cs ├── Dotnet │ ├── DotnetHost.cs │ ├── DotnetInstallation.cs │ └── DotnetRuntime.cs ├── DotnetRuntimeBootstrapper.AppHost.Core.csproj ├── Native │ ├── IOCounters.cs │ ├── JobObjectBasicLimitInformation.cs │ ├── JobObjectExtendedLimitInformation.cs │ ├── JobObjectInfoType.cs │ ├── NativeLibrary.cs │ ├── NativeMethods.cs │ ├── NativeResource.cs │ ├── ProcessJob.cs │ ├── SystemInfo.cs │ └── SystemVersionInfo.cs ├── Platform │ ├── OperatingSystemEx.cs │ ├── OperatingSystemVersion.cs │ └── ProcessorArchitecture.cs ├── Prerequisites │ ├── DotnetRuntimePrerequisite.cs │ ├── ExecutablePrerequisiteInstaller.cs │ ├── IPrerequisite.cs │ ├── IPrerequisiteInstaller.cs │ ├── PrerequisiteInstallerResult.cs │ ├── VisualCppPrerequisite.cs │ ├── WindowsUpdate2999226Prerequisite.cs │ ├── WindowsUpdate3063858Prerequisite.cs │ └── WindowsUpdatePrerequisiteInstaller.cs ├── TargetAssembly.cs └── Utils │ ├── CommandLine.cs │ ├── Disposable.cs │ ├── EnvironmentEx.cs │ ├── Extensions │ ├── AssemblyExtensions.cs │ ├── CollectionExtensions.cs │ ├── GenericExtensions.cs │ ├── RegistryExtensions.cs │ └── StringExtensions.cs │ ├── FileEx.cs │ ├── Http.cs │ ├── IconEx.cs │ ├── PathEx.cs │ ├── RandomEx.cs │ ├── Url.cs │ └── VersionEx.cs ├── DotnetRuntimeBootstrapper.AppHost.Gui ├── Bootstrapper.cs ├── DotnetRuntimeBootstrapper.AppHost.Gui.csproj ├── InstallForm.Designer.cs ├── InstallForm.cs ├── PromptForm.Designer.cs ├── PromptForm.cs └── Utils │ └── ApplicationEx.cs ├── DotnetRuntimeBootstrapper.Demo.Cli ├── DotnetRuntimeBootstrapper.Demo.Cli.csproj └── Program.cs ├── DotnetRuntimeBootstrapper.Demo.Gui ├── DotnetRuntimeBootstrapper.Demo.Gui.csproj ├── MainForm.Designer.cs ├── MainForm.cs ├── Manifest.xml └── Program.cs ├── DotnetRuntimeBootstrapper.sln ├── DotnetRuntimeBootstrapper ├── BootstrapperTask.cs ├── DotnetRuntimeBootstrapper.Local.props ├── DotnetRuntimeBootstrapper.csproj ├── DotnetRuntimeBootstrapper.props ├── DotnetRuntimeBootstrapper.targets └── Utils │ └── Extensions │ ├── AssemblyExtensions.cs │ └── CollectionExtensions.cs ├── License.txt ├── NuGet.config ├── Readme.md ├── favicon.ico └── favicon.png /.github/ISSUE_TEMPLATE/bug-report.yml: -------------------------------------------------------------------------------- 1 | name: 🐛 Bug report 2 | description: Report broken functionality. 3 | labels: [bug] 4 | 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | - Avoid generic or vague titles such as "Something's not working" or "A couple of problems" — be as descriptive as possible. 10 | - Keep your issue focused on one single problem. If you have multiple bug reports, please create a separate issue for each of them. 11 | - Issues should represent **complete and actionable** work items. If you are unsure about something or have a question, please start a [discussion](https://github.com/Tyrrrz/DotnetRuntimeBootstrapper/discussions/new) instead. 12 | - Remember that **DotnetRuntimeBootstrapper** is an open-source project funded by the community. If you find it useful, **please consider [donating](https://tyrrrz.me/donate) to support its development**. 13 | 14 | ___ 15 | 16 | - type: input 17 | attributes: 18 | label: Version 19 | description: Which version of the package does this bug affect? Make sure you're not using an outdated version. 20 | placeholder: v1.0.0 21 | validations: 22 | required: true 23 | 24 | - type: dropdown 25 | attributes: 26 | label: Variant 27 | description: Which variant of the bootstrapper does this bug affect? 28 | multiple: true 29 | options: 30 | - GUI (Graphical User Interface) 31 | - CLI (Command-Line Interface) 32 | validations: 33 | required: true 34 | 35 | - type: input 36 | attributes: 37 | label: Platform 38 | description: Which platform do you experience this bug on? 39 | placeholder: .NET 7.0 / Windows 11 40 | validations: 41 | required: true 42 | 43 | - type: textarea 44 | attributes: 45 | label: Steps to reproduce 46 | description: > 47 | Minimum steps required to reproduce the bug, including prerequisites, code snippets, or other relevant items. 48 | The information provided in this field must be readily actionable, meaning that anyone should be able to reproduce the bug by following these steps. 49 | If the reproduction steps are too complex to fit in this field, please provide a link to a repository instead. 50 | placeholder: | 51 | - Step 1 52 | - Step 2 53 | - Step 3 54 | validations: 55 | required: true 56 | 57 | - type: textarea 58 | attributes: 59 | label: Details 60 | description: Clear and thorough explanation of the bug, including any additional information you may find relevant. 61 | placeholder: | 62 | - Expected behavior: ... 63 | - Actual behavior: ... 64 | validations: 65 | required: true 66 | 67 | - type: checkboxes 68 | attributes: 69 | label: Checklist 70 | description: Quick list of checks to ensure that everything is in order. 71 | options: 72 | - label: I have looked through existing issues to make sure that this bug has not been reported before 73 | required: true 74 | - label: I have provided a descriptive title for this issue 75 | required: true 76 | - label: I have made sure that this bug is reproducible on the latest version of the package 77 | required: true 78 | - label: I have provided all the information needed to reproduce this bug as efficiently as possible 79 | required: true 80 | - label: I have sponsored this project 81 | required: false 82 | 83 | - type: markdown 84 | attributes: 85 | value: | 86 | If you are struggling to provide actionable reproduction steps, or if something else is preventing you from creating a complete bug report, please start a [discussion](https://github.com/Tyrrrz/DotnetRuntimeBootstrapper/discussions/new) instead. 87 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: ⚠ Feature request 4 | url: https://github.com/Tyrrrz/.github/blob/master/docs/project-status.md 5 | about: Sorry, but this project is in maintenance mode and no longer accepts new feature requests. 6 | - name: 🗨 Discussions 7 | url: https://github.com/Tyrrrz/DotnetRuntimeBootstrapper/discussions/new 8 | about: Ask and answer questions. 9 | - name: 💬 Discord server 10 | url: https://discord.gg/2SUWKFnHSm 11 | about: Chat with the project community. 12 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: "/" 5 | schedule: 6 | interval: monthly 7 | labels: 8 | - enhancement 9 | groups: 10 | actions: 11 | patterns: 12 | - "*" 13 | - package-ecosystem: nuget 14 | directory: "/" 15 | schedule: 16 | interval: monthly 17 | labels: 18 | - enhancement 19 | groups: 20 | nuget: 21 | patterns: 22 | - "*" 23 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: main 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | package-version: 7 | type: string 8 | description: Package version 9 | required: false 10 | deploy: 11 | type: boolean 12 | description: Deploy package 13 | required: false 14 | default: false 15 | push: 16 | branches: 17 | - master 18 | tags: 19 | - "*" 20 | pull_request: 21 | branches: 22 | - master 23 | 24 | jobs: 25 | main: 26 | uses: Tyrrrz/.github/.github/workflows/nuget.yml@master 27 | with: 28 | windows-only: true 29 | deploy: ${{ inputs.deploy || github.ref_type == 'tag' }} 30 | package-version: ${{ inputs.package-version || (github.ref_type == 'tag' && github.ref_name) || format('0.0.0-ci-{0}', github.sha) }} 31 | dotnet-version: 9.0.x 32 | secrets: 33 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 34 | NUGET_TOKEN: ${{ secrets.NUGET_TOKEN }} 35 | DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # User-specific files 2 | .vs/ 3 | .idea/ 4 | *.suo 5 | *.user 6 | 7 | # Build results 8 | bin/ 9 | obj/ 10 | 11 | # Test results 12 | TestResults/ -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 0.0.0-dev 5 | Tyrrrz 6 | Copyright (C) Oleksii Holub 7 | latest 8 | annotations 9 | true 10 | false 11 | false 12 | 13 | 14 | -------------------------------------------------------------------------------- /DotnetRuntimeBootstrapper.AppHost.Cli/Bootstrapper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using DotnetRuntimeBootstrapper.AppHost.Cli.Utils; 5 | using DotnetRuntimeBootstrapper.AppHost.Core; 6 | using DotnetRuntimeBootstrapper.AppHost.Core.Platform; 7 | using DotnetRuntimeBootstrapper.AppHost.Core.Prerequisites; 8 | using DotnetRuntimeBootstrapper.AppHost.Core.Utils; 9 | 10 | namespace DotnetRuntimeBootstrapper.AppHost.Cli; 11 | 12 | public class Bootstrapper : BootstrapperBase 13 | { 14 | protected override void ReportError(string message) 15 | { 16 | using (ConsoleEx.WithForegroundColor(ConsoleColor.DarkRed)) 17 | Console.Error.WriteLine("ERROR: " + message); 18 | } 19 | 20 | protected override bool Prompt( 21 | TargetAssembly targetAssembly, 22 | IPrerequisite[] missingPrerequisites 23 | ) 24 | { 25 | using (ConsoleEx.WithForegroundColor(ConsoleColor.DarkRed)) 26 | { 27 | Console.Error.WriteLine( 28 | $"Your system is missing runtime components required by {targetAssembly.Name}:" 29 | ); 30 | 31 | foreach (var prerequisite in missingPrerequisites) 32 | Console.Error.WriteLine($" - {prerequisite.DisplayName}"); 33 | 34 | Console.Error.WriteLine(); 35 | } 36 | 37 | // When running in interactive mode, prompt the user directly 38 | if (ConsoleEx.IsInteractive) 39 | { 40 | Console.Error.Write("Would you like to download and install them now?"); 41 | 42 | using (ConsoleEx.WithForegroundColor(ConsoleColor.DarkCyan)) 43 | Console.Error.Write(" [y/n]"); 44 | Console.Error.WriteLine(); 45 | 46 | return Console.ReadKey(true).Key == ConsoleKey.Y; 47 | } 48 | // When not running in interactive mode, instruct the user to set the environment variable instead 49 | else 50 | { 51 | Console.Error.Write( 52 | "To install the missing components automatically, set the environment variable " 53 | ); 54 | 55 | using (ConsoleEx.WithForegroundColor(ConsoleColor.DarkCyan)) 56 | Console.Error.Write(AcceptPromptEnvironmentVariable); 57 | 58 | Console.Error.Write(" to "); 59 | 60 | using (ConsoleEx.WithForegroundColor(ConsoleColor.DarkCyan)) 61 | Console.Error.Write("true"); 62 | 63 | Console.Error.Write(", and then run the application again:"); 64 | Console.Error.WriteLine(); 65 | 66 | using (ConsoleEx.WithForegroundColor(ConsoleColor.White)) 67 | Console.Error.Write($" set {AcceptPromptEnvironmentVariable}=true"); 68 | 69 | Console.Error.Write(" (Command Prompt)"); 70 | Console.Error.WriteLine(); 71 | 72 | using (ConsoleEx.WithForegroundColor(ConsoleColor.White)) 73 | Console.Error.Write($" $env:{AcceptPromptEnvironmentVariable}=\"true\""); 74 | 75 | Console.Error.Write(" (Powershell)"); 76 | Console.Error.WriteLine(); 77 | } 78 | 79 | return false; 80 | } 81 | 82 | protected override bool Install( 83 | TargetAssembly targetAssembly, 84 | IPrerequisite[] missingPrerequisites 85 | ) 86 | { 87 | using (ConsoleEx.WithForegroundColor(ConsoleColor.White)) 88 | Console.Out.WriteLine($"{targetAssembly.Name}: installing prerequisites"); 89 | 90 | var currentStep = 1; 91 | var totalSteps = missingPrerequisites.Length * 2; 92 | 93 | // Download 94 | var installers = new List(); 95 | foreach (var prerequisite in missingPrerequisites) 96 | { 97 | Console.Out.Write($"[{currentStep}/{totalSteps}] "); 98 | Console.Out.Write($"Downloading {prerequisite.DisplayName}... "); 99 | 100 | // Only write progress if running in interactive mode 101 | using ( 102 | var progress = new ConsoleProgress( 103 | ConsoleEx.IsInteractive ? Console.Out : TextWriter.Null 104 | ) 105 | ) 106 | { 107 | var installer = prerequisite.DownloadInstaller(progress.Report); 108 | installers.Add(installer); 109 | } 110 | 111 | Console.Out.Write("Done"); 112 | Console.Out.WriteLine(); 113 | 114 | currentStep++; 115 | } 116 | 117 | // Install 118 | var isRebootRequired = false; 119 | foreach (var installer in installers) 120 | { 121 | Console.Out.Write($"[{currentStep}/{totalSteps}] "); 122 | Console.Out.Write($"Installing {installer.Prerequisite.DisplayName}... "); 123 | 124 | var installationResult = installer.Run(); 125 | 126 | Console.Out.Write("Done"); 127 | Console.Out.WriteLine(); 128 | 129 | FileEx.TryDelete(installer.FilePath); 130 | 131 | if (installationResult == PrerequisiteInstallerResult.RebootRequired) 132 | isRebootRequired = true; 133 | 134 | currentStep++; 135 | } 136 | 137 | using (ConsoleEx.WithForegroundColor(ConsoleColor.White)) 138 | Console.Out.WriteLine("Prerequisites installed successfully."); 139 | Console.Out.WriteLine(); 140 | 141 | // Finalize 142 | if (isRebootRequired) 143 | { 144 | using (ConsoleEx.WithForegroundColor(ConsoleColor.DarkYellow)) 145 | Console.Out.WriteLine( 146 | $"You need to restart Windows before you can run {targetAssembly.Name}." 147 | ); 148 | 149 | // Only prompt for reboot if running in interactive mode 150 | if (ConsoleEx.IsInteractive) 151 | { 152 | Console.Out.WriteLine("Would you like to do it now?"); 153 | using (ConsoleEx.WithForegroundColor(ConsoleColor.DarkCyan)) 154 | Console.Out.Write(" [y/n]"); 155 | Console.Out.WriteLine(); 156 | 157 | var isRebootAccepted = Console.ReadKey(true).Key == ConsoleKey.Y; 158 | if (isRebootAccepted) 159 | OperatingSystemEx.Reboot(); 160 | } 161 | 162 | return false; 163 | } 164 | 165 | return true; 166 | } 167 | 168 | public static int Main(string[] args) => new Bootstrapper().Run(args); 169 | } 170 | -------------------------------------------------------------------------------- /DotnetRuntimeBootstrapper.AppHost.Cli/DotnetRuntimeBootstrapper.AppHost.Cli.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net35 6 | ../favicon.ico 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /DotnetRuntimeBootstrapper.AppHost.Cli/Utils/ConsoleEx.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using DotnetRuntimeBootstrapper.AppHost.Core.Native; 3 | using DotnetRuntimeBootstrapper.AppHost.Core.Utils; 4 | 5 | namespace DotnetRuntimeBootstrapper.AppHost.Cli.Utils; 6 | 7 | internal static class ConsoleEx 8 | { 9 | public static bool IsInteractive => 10 | NativeMethods.GetConsoleWindow() != 0 11 | && NativeMethods.GetFileType(NativeMethods.GetStdHandle(-10)) == 2 12 | && NativeMethods.GetFileType(NativeMethods.GetStdHandle(-11)) == 2 13 | && NativeMethods.GetFileType(NativeMethods.GetStdHandle(-12)) == 2; 14 | 15 | public static IDisposable WithForegroundColor(ConsoleColor color) 16 | { 17 | var lastColor = Console.ForegroundColor; 18 | Console.ForegroundColor = color; 19 | return Disposable.Create(() => Console.ForegroundColor = lastColor); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /DotnetRuntimeBootstrapper.AppHost.Cli/Utils/ConsoleProgress.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace DotnetRuntimeBootstrapper.AppHost.Cli.Utils; 5 | 6 | internal class ConsoleProgress(TextWriter writer) : IDisposable 7 | { 8 | private int _lastLength; 9 | 10 | private void EraseLast() 11 | { 12 | if (_lastLength > 0) 13 | { 14 | // Go back 15 | writer.Write(new string('\b', _lastLength)); 16 | 17 | // Overwrite with whitespace 18 | writer.Write(new string(' ', _lastLength)); 19 | 20 | // Go back again 21 | writer.Write(new string('\b', _lastLength)); 22 | } 23 | } 24 | 25 | private void Write(string text) 26 | { 27 | EraseLast(); 28 | writer.Write(text); 29 | _lastLength = text.Length; 30 | } 31 | 32 | public void Report(double progress) => Write($"{progress:P1}"); 33 | 34 | public void Dispose() => EraseLast(); 35 | } 36 | -------------------------------------------------------------------------------- /DotnetRuntimeBootstrapper.AppHost.Core/App.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /DotnetRuntimeBootstrapper.AppHost.Core/BootstrapperBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Reflection; 6 | using DotnetRuntimeBootstrapper.AppHost.Core.Prerequisites; 7 | using DotnetRuntimeBootstrapper.AppHost.Core.Utils; 8 | 9 | namespace DotnetRuntimeBootstrapper.AppHost.Core; 10 | 11 | public abstract class BootstrapperBase 12 | { 13 | protected const string LegacyAcceptPromptEnvironmentVariable = "DOTNET_INSTALL_PREREQUISITES"; 14 | protected const string AcceptPromptEnvironmentVariable = "DOTNET_ENABLE_BOOTSTRAPPER"; 15 | 16 | protected BootstrapperConfiguration Configuration { get; } = 17 | BootstrapperConfiguration.Resolve(); 18 | 19 | protected abstract void ReportError(string message); 20 | 21 | private void HandleException(Exception exception) 22 | { 23 | // For domain-level exceptions, report only the message 24 | if (exception is BootstrapperException bootstrapperException) 25 | { 26 | try 27 | { 28 | ReportError(bootstrapperException.Message); 29 | } 30 | catch 31 | { 32 | // Ignore 33 | } 34 | } 35 | // For other (unexpected) exceptions, report the full stack trace and 36 | // record the error to the Windows Event Log. 37 | else 38 | { 39 | try 40 | { 41 | ReportError(exception.ToString()); 42 | } 43 | catch 44 | { 45 | // Ignore 46 | } 47 | 48 | // Report to the Windows Event Log. Adapted from: 49 | // https://github.com/dotnet/runtime/blob/57bfe474518ab5b7cfe6bf7424a79ce3af9d6657/src/native/corehost/apphost/apphost.windows.cpp#L37-L51 50 | try 51 | { 52 | var applicationFilePath = Assembly.GetExecutingAssembly().Location; 53 | var applicationName = Path.GetFileName(applicationFilePath); 54 | 55 | var bootstrapperVersion = Assembly 56 | .GetExecutingAssembly() 57 | .GetName() 58 | .Version.ToString(3); 59 | 60 | var content = $""" 61 | Description: Bootstrapper for a .NET application has failed. 62 | Application: {applicationName} 63 | Path: {applicationFilePath} 64 | AppHost: .NET Runtime Bootstrapper v{bootstrapperVersion} 65 | 66 | {exception} 67 | """; 68 | 69 | EventLog.WriteEntry(".NET Runtime", content, EventLogEntryType.Error, 1023); 70 | } 71 | catch 72 | { 73 | // Ignore 74 | } 75 | } 76 | } 77 | 78 | protected abstract bool Prompt( 79 | TargetAssembly targetAssembly, 80 | IPrerequisite[] missingPrerequisites 81 | ); 82 | 83 | protected abstract bool Install( 84 | TargetAssembly targetAssembly, 85 | IPrerequisite[] missingPrerequisites 86 | ); 87 | 88 | private bool PromptAndInstall( 89 | TargetAssembly targetAssembly, 90 | IPrerequisite[] missingPrerequisites 91 | ) 92 | { 93 | // Install prompt can be disabled in bootstrap configuration or via environment variable 94 | var isPromptPreAccepted = 95 | !Configuration.IsPromptRequired 96 | || string.Equals( 97 | Environment.GetEnvironmentVariable(AcceptPromptEnvironmentVariable), 98 | "true", 99 | StringComparison.OrdinalIgnoreCase 100 | ) 101 | || string.Equals( 102 | Environment.GetEnvironmentVariable(LegacyAcceptPromptEnvironmentVariable), 103 | "true", 104 | StringComparison.OrdinalIgnoreCase 105 | ); 106 | 107 | var isPromptAccepted = isPromptPreAccepted || Prompt(targetAssembly, missingPrerequisites); 108 | 109 | return isPromptAccepted && Install(targetAssembly, missingPrerequisites); 110 | } 111 | 112 | private int Run(TargetAssembly targetAssembly, string[] args) 113 | { 114 | try 115 | { 116 | // Hot path: attempt to run the target first without any checks 117 | return targetAssembly.Run(args); 118 | } 119 | // Possible exception causes: 120 | // - .NET host not found (DirectoryNotFoundException) 121 | // - .NET host failed to initialize (BootstrapperException) 122 | catch 123 | { 124 | // Check for missing prerequisites and install them 125 | var missingPrerequisites = targetAssembly.GetMissingPrerequisites(); 126 | if (missingPrerequisites.Any()) 127 | { 128 | var isReadyToRun = PromptAndInstall(targetAssembly, missingPrerequisites); 129 | 130 | // User did not accept the installation or reboot is required 131 | if (!isReadyToRun) 132 | return 0xB007; 133 | 134 | // Reset the environment to update PATH and other variables 135 | // that may have been changed by the installation process. 136 | EnvironmentEx.RefreshEnvironmentVariables(); 137 | 138 | // Attempt to run the target again 139 | return targetAssembly.Run(args); 140 | } 141 | 142 | // There are no missing prerequisites to install, meaning that the 143 | // app failed to run for reasons unrelated to the bootstrapper. 144 | throw; 145 | } 146 | } 147 | 148 | public int Run(string[] args) 149 | { 150 | AppDomain.CurrentDomain.UnhandledException += (_, e) => 151 | HandleException((Exception)e.ExceptionObject); 152 | 153 | try 154 | { 155 | var targetAssembly = TargetAssembly.Resolve( 156 | Path.Combine( 157 | Path.GetDirectoryName(EnvironmentEx.ProcessPath) 158 | ?? AppDomain.CurrentDomain.BaseDirectory, 159 | Configuration.TargetFileName 160 | ) 161 | ); 162 | 163 | return Run(targetAssembly, args); 164 | } 165 | catch (Exception ex) 166 | { 167 | HandleException(ex); 168 | return 0xDEAD; 169 | } 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /DotnetRuntimeBootstrapper.AppHost.Core/BootstrapperConfiguration.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Reflection; 4 | using DotnetRuntimeBootstrapper.AppHost.Core.Utils.Extensions; 5 | 6 | namespace DotnetRuntimeBootstrapper.AppHost.Core; 7 | 8 | public partial class BootstrapperConfiguration 9 | { 10 | public required string TargetFileName { get; init; } 11 | 12 | public required bool IsPromptRequired { get; init; } 13 | } 14 | 15 | public partial class BootstrapperConfiguration 16 | { 17 | public static BootstrapperConfiguration Resolve() 18 | { 19 | var data = Assembly 20 | .GetExecutingAssembly() 21 | .GetManifestResourceString(nameof(BootstrapperConfiguration)); 22 | var parsed = new Dictionary(StringComparer.OrdinalIgnoreCase); 23 | 24 | foreach (var line in data.Split('\n')) 25 | { 26 | var components = line.Split('='); 27 | if (components.Length != 2) 28 | continue; 29 | 30 | var key = components[0].Trim(); 31 | var value = components[1].Trim(); 32 | 33 | parsed[key] = value; 34 | } 35 | 36 | return new BootstrapperConfiguration 37 | { 38 | TargetFileName = parsed[nameof(TargetFileName)], 39 | IsPromptRequired = string.Equals( 40 | parsed[nameof(IsPromptRequired)], 41 | "true", 42 | StringComparison.OrdinalIgnoreCase 43 | ), 44 | }; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /DotnetRuntimeBootstrapper.AppHost.Core/BootstrapperException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DotnetRuntimeBootstrapper.AppHost.Core; 4 | 5 | public class BootstrapperException(string message, Exception? innerException = null) 6 | : Exception(message, innerException); 7 | -------------------------------------------------------------------------------- /DotnetRuntimeBootstrapper.AppHost.Core/Dotnet/DotnetHost.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Runtime.InteropServices; 5 | using System.Text; 6 | using DotnetRuntimeBootstrapper.AppHost.Core.Native; 7 | using DotnetRuntimeBootstrapper.AppHost.Core.Utils; 8 | 9 | namespace DotnetRuntimeBootstrapper.AppHost.Core.Dotnet; 10 | 11 | // Host resolver headers: 12 | // https://github.com/dotnet/runtime/blob/57bfe47451/src/native/corehost/hostfxr.h 13 | 14 | // Muxer implementation: 15 | // https://github.com/dotnet/runtime/blob/57bfe47451/src/native/corehost/fxr/fx_muxer.cpp 16 | 17 | // .NET CLI host implementation: 18 | // https://github.com/dotnet/runtime/blob/57bfe47451/src/native/corehost/corehost.cpp 19 | 20 | internal partial class DotnetHost(NativeLibrary hostResolverLibrary) : IDisposable 21 | { 22 | [UnmanagedFunctionPointer(CallingConvention.Cdecl, CharSet = CharSet.Auto, SetLastError = true)] 23 | private delegate void ErrorWriterFn(string message); 24 | 25 | [UnmanagedFunctionPointer(CallingConvention.Cdecl, SetLastError = true)] 26 | private delegate void SetErrorWriterFn(ErrorWriterFn errorWriterFn); 27 | 28 | private SetErrorWriterFn GetSetErrorWriterFn() => 29 | hostResolverLibrary.GetFunction("hostfxr_set_error_writer"); 30 | 31 | [UnmanagedFunctionPointer(CallingConvention.Cdecl, CharSet = CharSet.Auto, SetLastError = true)] 32 | private delegate int InitializeForCommandLineFn( 33 | int argc, 34 | string[] argv, 35 | nint parameters, 36 | out nint handle 37 | ); 38 | 39 | private InitializeForCommandLineFn GetInitializeForCommandLineFn() => 40 | hostResolverLibrary.GetFunction( 41 | "hostfxr_initialize_for_dotnet_command_line" 42 | ); 43 | 44 | [UnmanagedFunctionPointer(CallingConvention.Cdecl, SetLastError = true)] 45 | private delegate int RunAppFn(nint handle); 46 | 47 | private RunAppFn GetRunAppFn() => hostResolverLibrary.GetFunction("hostfxr_run_app"); 48 | 49 | [UnmanagedFunctionPointer(CallingConvention.Cdecl, SetLastError = true)] 50 | private delegate int CloseFn(nint handle); 51 | 52 | private CloseFn GetCloseFn() => hostResolverLibrary.GetFunction("hostfxr_close"); 53 | 54 | private nint Initialize(string targetFilePath, string[] args) 55 | { 56 | // Route errors to a buffer 57 | var errorBuffer = new StringBuilder(); 58 | GetSetErrorWriterFn()(s => errorBuffer.AppendLine(s)); 59 | 60 | // Initialize the host as if we're running the app from command line 61 | var status = GetInitializeForCommandLineFn()( 62 | args.Length + 1, 63 | args.Prepend(targetFilePath).ToArray(), 64 | 0, 65 | out var handle 66 | ); 67 | 68 | if (status != 0) 69 | { 70 | var error = 71 | errorBuffer.Length > 0 ? errorBuffer.ToString() : "No error messages reported."; 72 | 73 | throw new BootstrapperException( 74 | $""" 75 | Failed to initialize .NET host. 76 | - Target: {targetFilePath} 77 | - Arguments: [{string.Join(", ", args)}] 78 | - Status: {status} 79 | - Error: {error} 80 | """ 81 | ); 82 | } 83 | 84 | return handle; 85 | } 86 | 87 | private int Run(nint handle) 88 | { 89 | try 90 | { 91 | return GetRunAppFn()(handle); 92 | } 93 | catch (SEHException ex) 94 | { 95 | // This is thrown when the app crashes with an unhandled exception. 96 | // Unfortunately, there is no way to get the original exception or its message. 97 | // https://github.com/Tyrrrz/DotnetRuntimeBootstrapper/issues/23 98 | throw new BootstrapperException( 99 | "Application crashed with an unhandled exception. " 100 | + "Unfortunately, it was not possible to retrieve the exception message or its stacktrace. " 101 | + "Please check the Windows Event Viewer to see if the runtime logged any additional information. " 102 | + "If you are the developer of the application, consider adding a global exception handler to provide a more detailed error message to the user.", 103 | ex 104 | ); 105 | } 106 | } 107 | 108 | private void Close(nint handle) => 109 | // Closing the handle doesn't completely unload the host. 110 | // There are some native libraries loaded by the resolver 111 | // that are intentionally leaked to preserve state. 112 | // This means that we can't successfully initialize the host 113 | // twice, but that shouldn't matter since we'd only attempt 114 | // to do it again if the first attempt failed in the first place. 115 | GetCloseFn()(handle); 116 | 117 | public int Run(string targetFilePath, string[] args) 118 | { 119 | var handle = Initialize(targetFilePath, args); 120 | 121 | try 122 | { 123 | return Run(handle); 124 | } 125 | finally 126 | { 127 | Close(handle); 128 | } 129 | } 130 | 131 | public void Dispose() => hostResolverLibrary.Dispose(); 132 | } 133 | 134 | internal partial class DotnetHost 135 | { 136 | private static string GetHostResolverFilePath() 137 | { 138 | // Host resolver (hostfxr) location strategy: 139 | // https://github.com/dotnet/runtime/blob/57bfe474518ab5b7cfe6bf7424a79ce3af9d6657/src/native/corehost/fxr_resolver.cpp#L55-L135 140 | // 1. Find the hostfxr directory containing versioned subdirectories 141 | // 2. Get the hostfxr.dll from the subdirectory with the highest version number 142 | 143 | var hostResolverRootDirPath = PathEx.Combine( 144 | DotnetInstallation.GetDirectoryPath(), 145 | "host", 146 | "fxr" 147 | ); 148 | 149 | if (!Directory.Exists(hostResolverRootDirPath)) 150 | { 151 | throw new DirectoryNotFoundException( 152 | "Failed to locate the host resolver directory ('host/fxr')." 153 | ); 154 | } 155 | 156 | var hostResolverFilePath = ( 157 | from dirPath in Directory.GetDirectories(hostResolverRootDirPath) 158 | let version = VersionEx.TryParse(Path.GetFileName(dirPath)) 159 | let filePath = Path.Combine(dirPath, "hostfxr.dll") 160 | where version is not null 161 | where File.Exists(filePath) 162 | orderby version descending 163 | select filePath 164 | ).FirstOrDefault(); 165 | 166 | return hostResolverFilePath 167 | ?? throw new FileNotFoundException( 168 | "Failed to locate the host resolver file ('hostfxr.dll')." 169 | ); 170 | } 171 | 172 | public static DotnetHost Load() => new(NativeLibrary.Load(GetHostResolverFilePath())); 173 | } 174 | -------------------------------------------------------------------------------- /DotnetRuntimeBootstrapper.AppHost.Core/Dotnet/DotnetInstallation.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using DotnetRuntimeBootstrapper.AppHost.Core.Platform; 4 | using Microsoft.Win32; 5 | 6 | namespace DotnetRuntimeBootstrapper.AppHost.Core.Dotnet; 7 | 8 | internal static class DotnetInstallation 9 | { 10 | private static string? TryGetDirectoryPathFromRegistry() 11 | { 12 | var dotnetRegistryKey = Registry.LocalMachine.OpenSubKey( 13 | ( 14 | OperatingSystemEx.ProcessorArchitecture.Is64Bit() 15 | ? "SOFTWARE\\Wow6432Node\\" 16 | : "SOFTWARE\\" 17 | ) 18 | + "dotnet\\Setup\\InstalledVersions\\" 19 | + OperatingSystemEx.ProcessorArchitecture.GetMoniker(), 20 | false 21 | ); 22 | 23 | var dotnetDirPath = dotnetRegistryKey?.GetValue("InstallLocation", null) as string; 24 | 25 | return !string.IsNullOrEmpty(dotnetDirPath) && Directory.Exists(dotnetDirPath) 26 | ? dotnetDirPath 27 | : null; 28 | } 29 | 30 | private static string? TryGetDirectoryPathFromEnvironment() 31 | { 32 | // Environment.GetFolderPath(ProgramFiles) does not return the correct path 33 | // if the apphost is running in x86 mode on an x64 system, so we rely 34 | // on an environment variable instead. 35 | var programFilesDirPath = 36 | Environment.GetEnvironmentVariable("PROGRAMFILES") 37 | ?? Environment.GetEnvironmentVariable("ProgramW6432") 38 | ?? Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles); 39 | 40 | var dotnetDirPath = Path.Combine(programFilesDirPath, "dotnet"); 41 | 42 | return !string.IsNullOrEmpty(dotnetDirPath) && Directory.Exists(dotnetDirPath) 43 | ? dotnetDirPath 44 | : null; 45 | } 46 | 47 | // .NET installation location design docs: 48 | // https://github.com/dotnet/designs/blob/main/accepted/2020/install-locations.md 49 | public static string GetDirectoryPath() => 50 | // Try to resolve location from registry (covers both custom and default locations) 51 | TryGetDirectoryPathFromRegistry() 52 | ?? 53 | // Try to resolve location from program files (default location) 54 | TryGetDirectoryPathFromEnvironment() 55 | ?? throw new DirectoryNotFoundException( 56 | "Failed to locate the .NET installation directory." 57 | ); 58 | } 59 | -------------------------------------------------------------------------------- /DotnetRuntimeBootstrapper.AppHost.Core/Dotnet/DotnetRuntime.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using DotnetRuntimeBootstrapper.AppHost.Core.Utils; 5 | using DotnetRuntimeBootstrapper.AppHost.Core.Utils.Extensions; 6 | using QuickJson; 7 | 8 | namespace DotnetRuntimeBootstrapper.AppHost.Core.Dotnet; 9 | 10 | internal partial class DotnetRuntime(string name, Version version) 11 | { 12 | public string Name { get; } = name; 13 | 14 | public Version Version { get; } = version; 15 | 16 | public bool IsBase => 17 | string.Equals(Name, "Microsoft.NETCore.App", StringComparison.OrdinalIgnoreCase); 18 | 19 | public bool IsWindowsDesktop => 20 | string.Equals(Name, "Microsoft.WindowsDesktop.App", StringComparison.OrdinalIgnoreCase); 21 | 22 | public bool IsAspNet => 23 | string.Equals(Name, "Microsoft.AspNetCore.App", StringComparison.OrdinalIgnoreCase); 24 | 25 | public bool IsSupersededBy(DotnetRuntime other) => 26 | string.Equals(Name, other.Name, StringComparison.OrdinalIgnoreCase) 27 | && Version.Major == other.Version.Major 28 | && Version <= other.Version; 29 | } 30 | 31 | internal partial class DotnetRuntime 32 | { 33 | public static DotnetRuntime[] GetAllInstalled() 34 | { 35 | var sharedDirPath = Path.Combine(DotnetInstallation.GetDirectoryPath(), "shared"); 36 | 37 | if (!Directory.Exists(sharedDirPath)) 38 | { 39 | throw new DirectoryNotFoundException( 40 | "Failed to find the directory containing .NET runtime binaries." 41 | ); 42 | } 43 | 44 | return ( 45 | from runtimeDirPath in Directory.GetDirectories(sharedDirPath) 46 | let name = Path.GetFileName(runtimeDirPath) 47 | from runtimeVersionDirPath in Directory.GetDirectories(runtimeDirPath) 48 | let version = VersionEx.TryParse(Path.GetFileName(runtimeVersionDirPath)) 49 | where version is not null 50 | select new DotnetRuntime(name, version) 51 | ).ToArray(); 52 | } 53 | 54 | public static DotnetRuntime[] GetAllTargets(string runtimeConfigFilePath) 55 | { 56 | static DotnetRuntime ParseRuntime(JsonNode json) 57 | { 58 | var name = json.TryGetChild("name")?.TryGetString(); 59 | var version = json.TryGetChild("version")?.TryGetString()?.Pipe(VersionEx.TryParse); 60 | 61 | return !string.IsNullOrEmpty(name) && version is not null 62 | ? new DotnetRuntime(name, version) 63 | : throw new InvalidOperationException( 64 | "Failed to extract runtime information from the provided runtime configuration." 65 | ); 66 | } 67 | 68 | var json = 69 | Json.TryParse(File.ReadAllText(runtimeConfigFilePath)) 70 | ?? throw new InvalidOperationException( 71 | $"Failed to parse runtime configuration at '{runtimeConfigFilePath}'." 72 | ); 73 | 74 | return 75 | // Multiple targets 76 | json.TryGetChild("runtimeOptions") 77 | ?.TryGetChild("frameworks") 78 | ?.EnumerateChildren() 79 | .Select(ParseRuntime) 80 | .ToArray() 81 | ?? 82 | // Single target 83 | json.TryGetChild("runtimeOptions") 84 | ?.TryGetChild("framework") 85 | ?.ToSingletonEnumerable() 86 | .Select(ParseRuntime) 87 | .ToArray() 88 | ?? throw new InvalidOperationException( 89 | "Failed to resolve the target runtime from runtime configuration." 90 | ); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /DotnetRuntimeBootstrapper.AppHost.Core/DotnetRuntimeBootstrapper.AppHost.Core.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net35 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /DotnetRuntimeBootstrapper.AppHost.Core/Native/IOCounters.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace DotnetRuntimeBootstrapper.AppHost.Core.Native; 4 | 5 | // ReSharper disable InconsistentNaming 6 | [StructLayout(LayoutKind.Sequential)] 7 | internal struct IOCounters 8 | { 9 | public ulong ReadOperationCount; 10 | public ulong WriteOperationCount; 11 | public ulong OtherOperationCount; 12 | public ulong ReadTransferCount; 13 | public ulong WriteTransferCount; 14 | public ulong OtherTransferCount; 15 | } 16 | -------------------------------------------------------------------------------- /DotnetRuntimeBootstrapper.AppHost.Core/Native/JobObjectBasicLimitInformation.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace DotnetRuntimeBootstrapper.AppHost.Core.Native; 4 | 5 | [StructLayout(LayoutKind.Sequential)] 6 | internal struct JobObjectBasicLimitInformation 7 | { 8 | public long PerProcessUserTimeLimit; 9 | public long PerJobUserTimeLimit; 10 | public uint LimitFlags; 11 | public nint MinimumWorkingSetSize; 12 | public nint MaximumWorkingSetSize; 13 | public uint ActiveProcessLimit; 14 | public nint Affinity; 15 | public uint PriorityClass; 16 | public uint SchedulingClass; 17 | } 18 | -------------------------------------------------------------------------------- /DotnetRuntimeBootstrapper.AppHost.Core/Native/JobObjectExtendedLimitInformation.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace DotnetRuntimeBootstrapper.AppHost.Core.Native; 4 | 5 | [StructLayout(LayoutKind.Sequential)] 6 | internal struct JobObjectExtendedLimitInformation 7 | { 8 | public JobObjectBasicLimitInformation BasicLimitInformation; 9 | public IOCounters IOInfo; 10 | public nint ProcessMemoryLimit; 11 | public nint JobMemoryLimit; 12 | public nint PeakProcessMemoryUsed; 13 | public nint PeakJobMemoryUsed; 14 | } 15 | -------------------------------------------------------------------------------- /DotnetRuntimeBootstrapper.AppHost.Core/Native/JobObjectInfoType.cs: -------------------------------------------------------------------------------- 1 | namespace DotnetRuntimeBootstrapper.AppHost.Core.Native; 2 | 3 | // ReSharper disable InconsistentNaming 4 | internal enum JobObjectInfoType 5 | { 6 | AssociateCompletionPortInformation = 7, 7 | BasicLimitInformation = 2, 8 | BasicUIRestrictions = 4, 9 | EndOfJobTimeInformation = 6, 10 | ExtendedLimitInformation = 9, 11 | SecurityLimitInformation = 5, 12 | GroupInformation = 11, 13 | } 14 | -------------------------------------------------------------------------------- /DotnetRuntimeBootstrapper.AppHost.Core/Native/NativeLibrary.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel; 4 | using System.Runtime.InteropServices; 5 | 6 | namespace DotnetRuntimeBootstrapper.AppHost.Core.Native; 7 | 8 | internal partial class NativeLibrary(nint handle) : NativeResource(handle) 9 | { 10 | private readonly Dictionary _functionsByName = new(StringComparer.Ordinal); 11 | 12 | public TDelegate GetFunction(string functionName) 13 | where TDelegate : Delegate 14 | { 15 | if ( 16 | _functionsByName.TryGetValue(functionName, out var cached) 17 | && cached is TDelegate cachedCasted 18 | ) 19 | return cachedCasted; 20 | 21 | var address = NativeMethods.GetProcAddress(Handle, functionName); 22 | if (address == 0) 23 | throw new Win32Exception(); 24 | 25 | var function = (TDelegate)Marshal.GetDelegateForFunctionPointer(address, typeof(TDelegate)); 26 | _functionsByName[functionName] = function; 27 | 28 | return function; 29 | } 30 | 31 | protected override void Dispose(bool disposing) => NativeMethods.FreeLibrary(Handle); 32 | } 33 | 34 | internal partial class NativeLibrary 35 | { 36 | public static NativeLibrary Load(string filePath) 37 | { 38 | var handle = NativeMethods.LoadLibrary(filePath); 39 | return handle != 0 ? new NativeLibrary(handle) : throw new Win32Exception(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /DotnetRuntimeBootstrapper.AppHost.Core/Native/NativeMethods.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | // ReSharper disable InconsistentNaming 4 | namespace DotnetRuntimeBootstrapper.AppHost.Core.Native; 5 | 6 | internal static class NativeMethods 7 | { 8 | private const string NtDll = "ntdll.dll"; 9 | private const string Kernel32 = "kernel32.dll"; 10 | private const string Shell32 = "shell32.dll"; 11 | 12 | [DllImport(NtDll, SetLastError = true)] 13 | public static extern void RtlGetVersion(ref SystemVersionInfo versionInfo); 14 | 15 | [DllImport(Kernel32, SetLastError = true)] 16 | public static extern void GetNativeSystemInfo(ref SystemInfo lpSystemInfo); 17 | 18 | [DllImport(Kernel32, CharSet = CharSet.Auto, SetLastError = true)] 19 | public static extern nint CreateJobObject(nint hAttributes, string? lpName); 20 | 21 | [DllImport(Kernel32, SetLastError = true)] 22 | public static extern bool SetInformationJobObject( 23 | nint hJob, 24 | JobObjectInfoType infoType, 25 | nint lpJobObjectInfo, 26 | uint cbJobObjectInfoLength 27 | ); 28 | 29 | [DllImport(Kernel32, SetLastError = true)] 30 | public static extern bool AssignProcessToJobObject(nint hJob, nint hProcess); 31 | 32 | [DllImport(Kernel32, SetLastError = true)] 33 | public static extern bool CloseHandle(nint hObject); 34 | 35 | [DllImport(Kernel32, CharSet = CharSet.Auto, SetLastError = true)] 36 | public static extern nint LoadLibrary(string lpFileName); 37 | 38 | [DllImport(Kernel32, SetLastError = true)] 39 | public static extern bool FreeLibrary(nint hModule); 40 | 41 | // This function doesn't come in the Unicode variant 42 | [DllImport(Kernel32, CharSet = CharSet.Ansi, ExactSpelling = true, SetLastError = true)] 43 | public static extern nint GetProcAddress(nint hModule, string lpProcName); 44 | 45 | [DllImport(Kernel32, SetLastError = true)] 46 | public static extern nint GetConsoleWindow(); 47 | 48 | [DllImport(Kernel32, SetLastError = true)] 49 | public static extern nint GetStdHandle(int nStdHandle); 50 | 51 | [DllImport(Kernel32, SetLastError = true)] 52 | public static extern int GetFileType(nint hFile); 53 | 54 | [DllImport(Shell32, CharSet = CharSet.Auto, SetLastError = true)] 55 | public static extern nint ExtractAssociatedIcon( 56 | nint hInst, 57 | string lpIconPath, 58 | out ushort lpiIcon 59 | ); 60 | } 61 | -------------------------------------------------------------------------------- /DotnetRuntimeBootstrapper.AppHost.Core/Native/NativeResource.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DotnetRuntimeBootstrapper.AppHost.Core.Native; 4 | 5 | internal abstract class NativeResource(nint handle) : IDisposable 6 | { 7 | public nint Handle { get; } = handle; 8 | 9 | ~NativeResource() => Dispose(false); 10 | 11 | protected abstract void Dispose(bool disposing); 12 | 13 | public void Dispose() 14 | { 15 | Dispose(true); 16 | GC.SuppressFinalize(this); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /DotnetRuntimeBootstrapper.AppHost.Core/Native/ProcessJob.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | using System.Diagnostics; 3 | using System.Runtime.InteropServices; 4 | 5 | namespace DotnetRuntimeBootstrapper.AppHost.Core.Native; 6 | 7 | internal partial class ProcessJob(nint handle) : NativeResource(handle) 8 | { 9 | public void Configure(JobObjectExtendedLimitInformation limitInfo) 10 | { 11 | var limitInfoSize = Marshal.SizeOf(typeof(JobObjectExtendedLimitInformation)); 12 | var limitInfoHandle = Marshal.AllocHGlobal(limitInfoSize); 13 | 14 | try 15 | { 16 | Marshal.StructureToPtr(limitInfo, limitInfoHandle, false); 17 | 18 | if ( 19 | !NativeMethods.SetInformationJobObject( 20 | Handle, 21 | JobObjectInfoType.ExtendedLimitInformation, 22 | limitInfoHandle, 23 | (uint)limitInfoSize 24 | ) 25 | ) 26 | { 27 | throw new Win32Exception(); 28 | } 29 | } 30 | finally 31 | { 32 | Marshal.FreeHGlobal(limitInfoHandle); 33 | } 34 | } 35 | 36 | public void AddProcess(nint processHandle) => 37 | NativeMethods.AssignProcessToJobObject(Handle, processHandle); 38 | 39 | public void AddProcess(Process process) => AddProcess(process.Handle); 40 | 41 | protected override void Dispose(bool disposing) => NativeMethods.CloseHandle(Handle); 42 | } 43 | 44 | internal partial class ProcessJob 45 | { 46 | public static ProcessJob Create() 47 | { 48 | var handle = NativeMethods.CreateJobObject(0, null); 49 | return handle != 0 ? new ProcessJob(handle) : throw new Win32Exception(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /DotnetRuntimeBootstrapper.AppHost.Core/Native/SystemInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace DotnetRuntimeBootstrapper.AppHost.Core.Native; 4 | 5 | [StructLayout(LayoutKind.Sequential)] 6 | internal readonly partial struct SystemInfo 7 | { 8 | public readonly ushort ProcessorArchitecture; 9 | public readonly ushort Reserved; 10 | public readonly uint PageSize; 11 | public readonly nint MinimumApplicationAddress; 12 | public readonly nint MaximumApplicationAddress; 13 | public readonly nuint ActiveProcessorMask; 14 | public readonly uint NumberOfProcessors; 15 | public readonly uint ProcessorType; 16 | public readonly uint AllocationGranularity; 17 | public readonly ushort ProcessorLevel; 18 | public readonly ushort ProcessorRevision; 19 | } 20 | 21 | internal partial struct SystemInfo 22 | { 23 | public static SystemInfo Instance { get; } = Resolve(); 24 | 25 | private static SystemInfo Resolve() 26 | { 27 | var systemInfo = default(SystemInfo); 28 | NativeMethods.GetNativeSystemInfo(ref systemInfo); 29 | 30 | return systemInfo; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /DotnetRuntimeBootstrapper.AppHost.Core/Native/SystemVersionInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace DotnetRuntimeBootstrapper.AppHost.Core.Native; 4 | 5 | [StructLayout(LayoutKind.Sequential)] 6 | internal readonly partial struct SystemVersionInfo 7 | { 8 | public readonly int OSVersionInfoSize; 9 | public readonly int MajorVersion; 10 | public readonly int MinorVersion; 11 | public readonly int BuildNumber; 12 | public readonly int PlatformId; 13 | 14 | [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] 15 | public readonly string CSDVersion; 16 | 17 | public readonly ushort ServicePackMajor; 18 | public readonly ushort ServicePackMinor; 19 | public readonly short SuiteMask; 20 | public readonly byte ProductType; 21 | public readonly byte Reserved; 22 | } 23 | 24 | internal partial struct SystemVersionInfo 25 | { 26 | public static SystemVersionInfo Instance { get; } = Resolve(); 27 | 28 | private static SystemVersionInfo Resolve() 29 | { 30 | var systemVersionInfo = default(SystemVersionInfo); 31 | NativeMethods.RtlGetVersion(ref systemVersionInfo); 32 | 33 | return systemVersionInfo; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /DotnetRuntimeBootstrapper.AppHost.Core/Platform/OperatingSystemEx.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Management; 4 | using DotnetRuntimeBootstrapper.AppHost.Core.Native; 5 | using DotnetRuntimeBootstrapper.AppHost.Core.Utils; 6 | 7 | namespace DotnetRuntimeBootstrapper.AppHost.Core.Platform; 8 | 9 | // The 'Ex' suffix here is just to disambiguate from the existing 'OperatingSystem' type in the BCL 10 | internal static class OperatingSystemEx 11 | { 12 | public static Version Version { get; } = 13 | new(SystemVersionInfo.Instance.MajorVersion, SystemVersionInfo.Instance.MinorVersion); 14 | 15 | public static ProcessorArchitecture ProcessorArchitecture => 16 | SystemInfo.Instance.ProcessorArchitecture switch 17 | { 18 | 0 => ProcessorArchitecture.X86, 19 | 9 => ProcessorArchitecture.X64, 20 | 5 => ProcessorArchitecture.Arm, 21 | 12 => ProcessorArchitecture.Arm64, 22 | _ => throw new InvalidOperationException("Unknown processor architecture."), 23 | }; 24 | 25 | public static IEnumerable GetInstalledUpdates() 26 | { 27 | using var search = new ManagementObjectSearcher( 28 | "SELECT HotFixID FROM Win32_QuickFixEngineering" 29 | ); 30 | 31 | using var results = search.Get(); 32 | 33 | foreach (var result in results) 34 | { 35 | var id = result["HotFixID"] as string; 36 | if (!string.IsNullOrEmpty(id)) 37 | yield return id; 38 | } 39 | } 40 | 41 | public static void Reboot() => CommandLine.Run("shutdown", ["/r", "/t", "0"]); 42 | } 43 | -------------------------------------------------------------------------------- /DotnetRuntimeBootstrapper.AppHost.Core/Platform/OperatingSystemVersion.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DotnetRuntimeBootstrapper.AppHost.Core.Platform; 4 | 5 | internal static class OperatingSystemVersion 6 | { 7 | public static Version Windows7 { get; } = new(6, 1); 8 | 9 | public static Version Windows8 { get; } = new(6, 2); 10 | 11 | // ReSharper disable once InconsistentNaming 12 | public static Version Windows8_1 { get; } = new(6, 3); 13 | 14 | public static Version Windows10 { get; } = new(10, 0); 15 | } 16 | -------------------------------------------------------------------------------- /DotnetRuntimeBootstrapper.AppHost.Core/Platform/ProcessorArchitecture.cs: -------------------------------------------------------------------------------- 1 | namespace DotnetRuntimeBootstrapper.AppHost.Core.Platform; 2 | 3 | internal enum ProcessorArchitecture 4 | { 5 | X86, 6 | X64, 7 | Arm, 8 | Arm64, 9 | } 10 | 11 | internal static class ProcessorArchitectureExtensions 12 | { 13 | public static bool Is64Bit(this ProcessorArchitecture arch) => 14 | arch is ProcessorArchitecture.X64 or ProcessorArchitecture.Arm64; 15 | 16 | public static string GetMoniker(this ProcessorArchitecture arch) => 17 | arch.ToString().ToLowerInvariant(); 18 | } 19 | -------------------------------------------------------------------------------- /DotnetRuntimeBootstrapper.AppHost.Core/Prerequisites/DotnetRuntimePrerequisite.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using DotnetRuntimeBootstrapper.AppHost.Core.Dotnet; 5 | using DotnetRuntimeBootstrapper.AppHost.Core.Platform; 6 | using DotnetRuntimeBootstrapper.AppHost.Core.Utils; 7 | using DotnetRuntimeBootstrapper.AppHost.Core.Utils.Extensions; 8 | using QuickJson; 9 | 10 | namespace DotnetRuntimeBootstrapper.AppHost.Core.Prerequisites; 11 | 12 | internal class DotnetRuntimePrerequisite(DotnetRuntime runtime) : IPrerequisite 13 | { 14 | public string DisplayName => 15 | runtime switch 16 | { 17 | { IsBase: true } => ".NET Runtime", 18 | { IsWindowsDesktop: true } => ".NET Desktop Runtime", 19 | { IsAspNet: true } => ".NET ASP.NET Runtime", 20 | _ => runtime.Name, 21 | } + $" v{runtime.Version}"; 22 | 23 | // We are looking for a runtime with the same name and the same major version. 24 | // Installed runtime may have higher minor version than the target runtime, but not lower. 25 | public bool IsInstalled() 26 | { 27 | try 28 | { 29 | return DotnetRuntime.GetAllInstalled().Any(runtime.IsSupersededBy); 30 | } 31 | catch (Exception ex) when (ex is DirectoryNotFoundException or FileNotFoundException) 32 | { 33 | // .NET is likely not installed altogether 34 | return false; 35 | } 36 | } 37 | 38 | private string GetInstallerDownloadUrl() 39 | { 40 | var manifest = Http.GetContentString( 41 | "https://dotnetcli.blob.core.windows.net/dotnet/release-metadata/" 42 | + $"{runtime.Version.ToString(2)}/releases.json" 43 | ); 44 | 45 | // Find the installer download URL applicable for the current system 46 | return Json 47 | // Find the list of files for the latest release 48 | .TryParse(manifest) 49 | ?.TryGetChild("releases") 50 | ?.TryGetChild(0) 51 | ?.TryGetChild( 52 | runtime switch 53 | { 54 | { IsWindowsDesktop: true } => "windowsdesktop", 55 | { IsAspNet: true } => "aspnetcore-runtime", 56 | _ => "runtime", 57 | } 58 | ) 59 | ?.TryGetChild("files") 60 | ?.EnumerateChildren() 61 | // Filter by processor architecture 62 | .Where(f => 63 | string.Equals( 64 | f.TryGetChild("rid")?.TryGetString(), 65 | "win-" + OperatingSystemEx.ProcessorArchitecture.GetMoniker(), 66 | StringComparison.OrdinalIgnoreCase 67 | ) 68 | ) 69 | // Filter by file type 70 | .Where(f => 71 | string.Equals( 72 | f.TryGetChild("name")?.TryGetString()?.Pipe(Path.GetExtension), 73 | ".exe", 74 | StringComparison.OrdinalIgnoreCase 75 | ) 76 | ) 77 | .Select(f => f.TryGetChild("url")?.TryGetString()) 78 | .FirstOrDefault() 79 | ?? throw new InvalidOperationException( 80 | "Failed to resolve the download URL for the required .NET runtime. " 81 | + $"Please try to download ${DisplayName} manually " 82 | + $"from https://dotnet.microsoft.com/download/dotnet/{runtime.Version.ToString(2)} or " 83 | + "from https://get.dot.net." 84 | ); 85 | } 86 | 87 | public IPrerequisiteInstaller DownloadInstaller(Action? handleProgress) 88 | { 89 | var downloadUrl = GetInstallerDownloadUrl(); 90 | var filePath = FileEx.GenerateTempFilePath( 91 | Url.TryExtractFileName(downloadUrl) ?? "installer.exe" 92 | ); 93 | 94 | Http.DownloadFile(downloadUrl, filePath, handleProgress); 95 | 96 | return new ExecutablePrerequisiteInstaller(this, filePath); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /DotnetRuntimeBootstrapper.AppHost.Core/Prerequisites/ExecutablePrerequisiteInstaller.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | using DotnetRuntimeBootstrapper.AppHost.Core.Utils; 3 | 4 | namespace DotnetRuntimeBootstrapper.AppHost.Core.Prerequisites; 5 | 6 | internal class ExecutablePrerequisiteInstaller(IPrerequisite prerequisite, string filePath) 7 | : IPrerequisiteInstaller 8 | { 9 | public IPrerequisite Prerequisite { get; } = prerequisite; 10 | 11 | public string FilePath { get; } = filePath; 12 | 13 | public PrerequisiteInstallerResult Run() 14 | { 15 | try 16 | { 17 | var exitCode = CommandLine.Run(FilePath, ["/install", "/quiet", "/norestart"], true); 18 | 19 | // https://github.com/Tyrrrz/DotnetRuntimeBootstrapper/issues/24#issuecomment-1021447102 20 | if (exitCode is 3010 or 3011 or 1641) 21 | return PrerequisiteInstallerResult.RebootRequired; 22 | 23 | if (exitCode != 0) 24 | { 25 | throw new BootstrapperException( 26 | $"Failed to install '{Prerequisite.DisplayName}'. " 27 | + $"Exit code: {exitCode}. " 28 | + $"Restart the application to try again, or install this component manually." 29 | ); 30 | } 31 | 32 | return PrerequisiteInstallerResult.Success; 33 | } 34 | // Installation was canceled before the process could start 35 | catch (Win32Exception ex) when (ex.NativeErrorCode == 1223) 36 | { 37 | throw new BootstrapperException( 38 | $"Failed to install '{Prerequisite.DisplayName}'. " 39 | + $"The operation was canceled. " 40 | + $"Restart the application to try again, or install this component manually.", 41 | ex 42 | ); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /DotnetRuntimeBootstrapper.AppHost.Core/Prerequisites/IPrerequisite.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DotnetRuntimeBootstrapper.AppHost.Core.Prerequisites; 4 | 5 | public interface IPrerequisite 6 | { 7 | string DisplayName { get; } 8 | 9 | bool IsInstalled(); 10 | 11 | IPrerequisiteInstaller DownloadInstaller(Action? handleProgress); 12 | } 13 | -------------------------------------------------------------------------------- /DotnetRuntimeBootstrapper.AppHost.Core/Prerequisites/IPrerequisiteInstaller.cs: -------------------------------------------------------------------------------- 1 | namespace DotnetRuntimeBootstrapper.AppHost.Core.Prerequisites; 2 | 3 | public interface IPrerequisiteInstaller 4 | { 5 | IPrerequisite Prerequisite { get; } 6 | 7 | string FilePath { get; } 8 | 9 | PrerequisiteInstallerResult Run(); 10 | } 11 | -------------------------------------------------------------------------------- /DotnetRuntimeBootstrapper.AppHost.Core/Prerequisites/PrerequisiteInstallerResult.cs: -------------------------------------------------------------------------------- 1 | namespace DotnetRuntimeBootstrapper.AppHost.Core.Prerequisites; 2 | 3 | public enum PrerequisiteInstallerResult 4 | { 5 | Success, 6 | RebootRequired, 7 | } 8 | -------------------------------------------------------------------------------- /DotnetRuntimeBootstrapper.AppHost.Core/Prerequisites/VisualCppPrerequisite.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using DotnetRuntimeBootstrapper.AppHost.Core.Platform; 3 | using DotnetRuntimeBootstrapper.AppHost.Core.Utils; 4 | using DotnetRuntimeBootstrapper.AppHost.Core.Utils.Extensions; 5 | using Microsoft.Win32; 6 | 7 | namespace DotnetRuntimeBootstrapper.AppHost.Core.Prerequisites; 8 | 9 | internal class VisualCppPrerequisite : IPrerequisite 10 | { 11 | public string DisplayName => "Visual C++ Redistributable 2015-2019"; 12 | 13 | public bool IsInstalled() => 14 | Registry.LocalMachine.ContainsSubKey( 15 | ( 16 | OperatingSystemEx.ProcessorArchitecture.Is64Bit() 17 | ? "SOFTWARE\\Wow6432Node\\" 18 | : "SOFTWARE\\" 19 | ) 20 | + "Microsoft\\VisualStudio\\14.0\\VC\\Runtimes\\" 21 | + OperatingSystemEx.ProcessorArchitecture.GetMoniker() 22 | ); 23 | 24 | public IPrerequisiteInstaller DownloadInstaller(Action? handleProgress) 25 | { 26 | var fileName = $"VC_redist.{OperatingSystemEx.ProcessorArchitecture.GetMoniker()}.exe"; 27 | var filePath = FileEx.GenerateTempFilePath(fileName); 28 | 29 | Http.DownloadFile($"https://aka.ms/vs/16/release/{fileName}", filePath, handleProgress); 30 | 31 | return new ExecutablePrerequisiteInstaller(this, filePath); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /DotnetRuntimeBootstrapper.AppHost.Core/Prerequisites/WindowsUpdate2999226Prerequisite.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using DotnetRuntimeBootstrapper.AppHost.Core.Platform; 4 | using DotnetRuntimeBootstrapper.AppHost.Core.Utils; 5 | 6 | namespace DotnetRuntimeBootstrapper.AppHost.Core.Prerequisites; 7 | 8 | // Universal C Runtime 9 | internal class WindowsUpdate2999226Prerequisite : IPrerequisite 10 | { 11 | private const string Id = "KB2999226"; 12 | 13 | public string DisplayName => $"Windows Update {Id}"; 14 | 15 | public bool IsInstalled() => 16 | OperatingSystemEx.Version >= OperatingSystemVersion.Windows10 17 | || OperatingSystemEx.GetInstalledUpdates().Contains(Id, StringComparer.OrdinalIgnoreCase); 18 | 19 | private string GetInstallerDownloadUrl() 20 | { 21 | if ( 22 | OperatingSystemEx.Version == OperatingSystemVersion.Windows7 23 | && OperatingSystemEx.ProcessorArchitecture == ProcessorArchitecture.X64 24 | ) 25 | { 26 | return "https://download.microsoft.com/download/1/1/5/11565A9A-EA09-4F0A-A57E-520D5D138140/Windows6.1-KB2999226-x64.msu"; 27 | } 28 | 29 | if ( 30 | OperatingSystemEx.Version == OperatingSystemVersion.Windows7 31 | && OperatingSystemEx.ProcessorArchitecture == ProcessorArchitecture.X86 32 | ) 33 | { 34 | return "https://download.microsoft.com/download/4/F/E/4FE73868-5EDD-4B47-8B33-CE1BB7B2B16A/Windows6.1-KB2999226-x86.msu"; 35 | } 36 | 37 | if ( 38 | OperatingSystemEx.Version == OperatingSystemVersion.Windows8 39 | && OperatingSystemEx.ProcessorArchitecture == ProcessorArchitecture.X64 40 | ) 41 | { 42 | return "https://download.microsoft.com/download/A/C/1/AC15393F-A6E6-469B-B222-C44B3BB6ECCC/Windows8-RT-KB2999226-x64.msu"; 43 | } 44 | 45 | if ( 46 | OperatingSystemEx.Version == OperatingSystemVersion.Windows8 47 | && OperatingSystemEx.ProcessorArchitecture == ProcessorArchitecture.X86 48 | ) 49 | { 50 | return "https://download.microsoft.com/download/1/E/8/1E8AFE90-5217-464D-9292-7D0B95A56CE4/Windows8-RT-KB2999226-x86.msu"; 51 | } 52 | 53 | if ( 54 | OperatingSystemEx.Version == OperatingSystemVersion.Windows8_1 55 | && OperatingSystemEx.ProcessorArchitecture == ProcessorArchitecture.X64 56 | ) 57 | { 58 | return "https://download.microsoft.com/download/9/6/F/96FD0525-3DDF-423D-8845-5F92F4A6883E/Windows8.1-KB2999226-x64.msu"; 59 | } 60 | 61 | if ( 62 | OperatingSystemEx.Version == OperatingSystemVersion.Windows8_1 63 | && OperatingSystemEx.ProcessorArchitecture == ProcessorArchitecture.X86 64 | ) 65 | { 66 | return "https://download.microsoft.com/download/E/4/6/E4694323-8290-4A08-82DB-81F2EB9452C2/Windows8.1-KB2999226-x86.msu"; 67 | } 68 | 69 | throw new InvalidOperationException("Unsupported operating system version."); 70 | } 71 | 72 | public IPrerequisiteInstaller DownloadInstaller(Action? handleProgress) 73 | { 74 | var filePath = FileEx.GenerateTempFilePath($"{Id}.msu"); 75 | 76 | Http.DownloadFile(GetInstallerDownloadUrl(), filePath, handleProgress); 77 | 78 | return new WindowsUpdatePrerequisiteInstaller(this, filePath); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /DotnetRuntimeBootstrapper.AppHost.Core/Prerequisites/WindowsUpdate3063858Prerequisite.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using DotnetRuntimeBootstrapper.AppHost.Core.Platform; 4 | using DotnetRuntimeBootstrapper.AppHost.Core.Utils; 5 | 6 | namespace DotnetRuntimeBootstrapper.AppHost.Core.Prerequisites; 7 | 8 | // Security update 9 | internal class WindowsUpdate3063858Prerequisite : IPrerequisite 10 | { 11 | private const string Id = "KB3063858"; 12 | 13 | public string DisplayName => $"Windows Update {Id}"; 14 | 15 | public bool IsInstalled() => 16 | OperatingSystemEx.Version >= OperatingSystemVersion.Windows8 17 | || OperatingSystemEx 18 | .GetInstalledUpdates() 19 | .Any(u => 20 | string.Equals(u, Id, StringComparison.OrdinalIgnoreCase) 21 | || 22 | // Supersession (https://github.com/Tyrrrz/LightBulb/issues/209) 23 | string.Equals(u, "KB3068708", StringComparison.OrdinalIgnoreCase) 24 | ); 25 | 26 | private string GetInstallerDownloadUrl() 27 | { 28 | if ( 29 | OperatingSystemEx.Version == OperatingSystemVersion.Windows7 30 | && OperatingSystemEx.ProcessorArchitecture == ProcessorArchitecture.X64 31 | ) 32 | { 33 | return "https://download.microsoft.com/download/0/8/E/08E0386B-F6AF-4651-8D1B-C0A95D2731F0/Windows6.1-KB3063858-x64.msu"; 34 | } 35 | 36 | if ( 37 | OperatingSystemEx.Version == OperatingSystemVersion.Windows7 38 | && OperatingSystemEx.ProcessorArchitecture == ProcessorArchitecture.X86 39 | ) 40 | { 41 | return "https://download.microsoft.com/download/C/9/6/C96CD606-3E05-4E1C-B201-51211AE80B1E/Windows6.1-KB3063858-x86.msu"; 42 | } 43 | 44 | throw new InvalidOperationException("Unsupported operating system version."); 45 | } 46 | 47 | public IPrerequisiteInstaller DownloadInstaller(Action? handleProgress) 48 | { 49 | var filePath = FileEx.GenerateTempFilePath($"{Id}.msu"); 50 | 51 | Http.DownloadFile(GetInstallerDownloadUrl(), filePath, handleProgress); 52 | 53 | return new WindowsUpdatePrerequisiteInstaller(this, filePath); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /DotnetRuntimeBootstrapper.AppHost.Core/Prerequisites/WindowsUpdatePrerequisiteInstaller.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | using DotnetRuntimeBootstrapper.AppHost.Core.Utils; 3 | 4 | namespace DotnetRuntimeBootstrapper.AppHost.Core.Prerequisites; 5 | 6 | internal class WindowsUpdatePrerequisiteInstaller(IPrerequisite prerequisite, string filePath) 7 | : IPrerequisiteInstaller 8 | { 9 | public IPrerequisite Prerequisite { get; } = prerequisite; 10 | 11 | public string FilePath { get; } = filePath; 12 | 13 | public PrerequisiteInstallerResult Run() 14 | { 15 | try 16 | { 17 | var exitCode = CommandLine.Run("wusa", [FilePath, "/quiet", "/norestart"], true); 18 | 19 | // https://github.com/Tyrrrz/DotnetRuntimeBootstrapper/issues/24#issuecomment-1021447102 20 | if (exitCode is 3010 or 3011 or 1641) 21 | return PrerequisiteInstallerResult.RebootRequired; 22 | 23 | if (exitCode != 0) 24 | { 25 | throw new BootstrapperException( 26 | $"Failed to install '{Prerequisite.DisplayName}'. " 27 | + $"Exit code: {exitCode}. " 28 | + $"Restart the application to try again, or install this component manually." 29 | ); 30 | } 31 | 32 | return PrerequisiteInstallerResult.Success; 33 | } 34 | // Installation was canceled before the process could start 35 | catch (Win32Exception ex) when (ex.NativeErrorCode == 1223) 36 | { 37 | throw new BootstrapperException( 38 | $"Failed to install '{Prerequisite.DisplayName}'. " 39 | + $"The operation was canceled. " 40 | + $"Restart the application to try again, or install this component manually.", 41 | ex 42 | ); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /DotnetRuntimeBootstrapper.AppHost.Core/TargetAssembly.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Diagnostics; 3 | using System.IO; 4 | using System.Linq; 5 | using DotnetRuntimeBootstrapper.AppHost.Core.Dotnet; 6 | using DotnetRuntimeBootstrapper.AppHost.Core.Prerequisites; 7 | using DotnetRuntimeBootstrapper.AppHost.Core.Utils.Extensions; 8 | 9 | namespace DotnetRuntimeBootstrapper.AppHost.Core; 10 | 11 | public partial class TargetAssembly(string filePath, string name) 12 | { 13 | public string FilePath { get; } = filePath; 14 | 15 | public string Name { get; } = name; 16 | 17 | private DotnetRuntime[] GetRuntimes() 18 | { 19 | var configFilePath = Path.ChangeExtension(FilePath, "runtimeconfig.json"); 20 | var runtimes = DotnetRuntime.GetAllTargets(configFilePath).ToList(); 21 | 22 | // Desktop runtimes already include the base runtimes, so we can filter out unnecessary targets 23 | // https://github.com/Tyrrrz/DotnetRuntimeBootstrapper/issues/30 24 | if (runtimes.Count > 1) 25 | { 26 | foreach (var desktopRuntime in runtimes.Where(r => r.IsWindowsDesktop).ToArray()) 27 | { 28 | // Only filter out compatible base runtimes! 29 | // If the app targets .NET 5 desktop and .NET 6 base, 30 | // we still need to keep the base. 31 | // Very unlikely that such a situation will happen though. 32 | runtimes.RemoveAll(r => 33 | r.IsBase 34 | && r.Version.Major == desktopRuntime.Version.Major 35 | && r.Version <= desktopRuntime.Version 36 | ); 37 | } 38 | } 39 | 40 | return runtimes.ToArray(); 41 | } 42 | 43 | public IPrerequisite[] GetMissingPrerequisites() 44 | { 45 | var prerequisites = new List 46 | { 47 | new WindowsUpdate2999226Prerequisite(), 48 | new WindowsUpdate3063858Prerequisite(), 49 | new VisualCppPrerequisite(), 50 | }; 51 | 52 | foreach (var runtime in GetRuntimes()) 53 | prerequisites.Add(new DotnetRuntimePrerequisite(runtime)); 54 | 55 | // Filter out prerequisites that are already installed 56 | prerequisites.RemoveAll(p => p.IsInstalled()); 57 | 58 | return prerequisites.ToArray(); 59 | } 60 | 61 | public int Run(string[] args) 62 | { 63 | using var host = DotnetHost.Load(); 64 | return host.Run(FilePath, args); 65 | } 66 | } 67 | 68 | public partial class TargetAssembly 69 | { 70 | public static TargetAssembly Resolve(string filePath) 71 | { 72 | try 73 | { 74 | var name = 75 | FileVersionInfo.GetVersionInfo(filePath).ProductName?.NullIfEmptyOrWhiteSpace() 76 | ?? Path.GetFileNameWithoutExtension(filePath); 77 | 78 | return new TargetAssembly(filePath, name); 79 | } 80 | catch (FileNotFoundException ex) 81 | { 82 | throw new FileNotFoundException( 83 | $"Failed to locate the target assembly at '{Path.GetFileName(filePath)}'.", 84 | ex 85 | ); 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /DotnetRuntimeBootstrapper.AppHost.Core/Utils/CommandLine.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Linq; 4 | using DotnetRuntimeBootstrapper.AppHost.Core.Native; 5 | 6 | namespace DotnetRuntimeBootstrapper.AppHost.Core.Utils; 7 | 8 | internal static class CommandLine 9 | { 10 | // Process job that will ensure that all child processes will be killed 11 | // when the parent process terminates. 12 | // Ensures we don't leave installers or other executables running if the 13 | // user decides to cancel or force exit. 14 | private static ProcessJob? ProcessJob { get; } = TryCreateProcessJob(); 15 | 16 | private static ProcessJob? TryCreateProcessJob() 17 | { 18 | try 19 | { 20 | var processJob = ProcessJob.Create(); 21 | 22 | processJob.Configure( 23 | new JobObjectExtendedLimitInformation 24 | { 25 | BasicLimitInformation = new JobObjectBasicLimitInformation 26 | { 27 | // JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE 28 | LimitFlags = 0x2000, 29 | }, 30 | } 31 | ); 32 | 33 | return processJob; 34 | } 35 | catch 36 | { 37 | // Not critical, ignore errors 38 | return null; 39 | } 40 | } 41 | 42 | private static string EscapeArgument(string argument) => 43 | '"' + argument.Replace("\"", "\\\"", StringComparison.Ordinal) + '"'; 44 | 45 | private static Process CreateProcess( 46 | string executableFilePath, 47 | string[] arguments, 48 | bool isElevated = false 49 | ) 50 | { 51 | var process = new Process 52 | { 53 | StartInfo = new ProcessStartInfo 54 | { 55 | FileName = executableFilePath, 56 | Arguments = string.Join(" ", arguments.Select(EscapeArgument).ToArray()), 57 | UseShellExecute = false, 58 | CreateNoWindow = true, 59 | }, 60 | }; 61 | 62 | if (isElevated) 63 | { 64 | process.StartInfo.UseShellExecute = true; 65 | process.StartInfo.Verb = "runas"; 66 | } 67 | 68 | return process; 69 | } 70 | 71 | public static int Run(string executableFilePath, string[] arguments, bool isElevated = false) 72 | { 73 | using var process = CreateProcess(executableFilePath, arguments, isElevated); 74 | 75 | process.Start(); 76 | ProcessJob?.AddProcess(process); 77 | 78 | process.WaitForExit(); 79 | 80 | return process.ExitCode; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /DotnetRuntimeBootstrapper.AppHost.Core/Utils/Disposable.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DotnetRuntimeBootstrapper.AppHost.Core.Utils; 4 | 5 | internal class Disposable(Action dispose) : IDisposable 6 | { 7 | public void Dispose() => dispose(); 8 | 9 | public static IDisposable Create(Action dispose) => new Disposable(dispose); 10 | } 11 | -------------------------------------------------------------------------------- /DotnetRuntimeBootstrapper.AppHost.Core/Utils/EnvironmentEx.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Linq; 4 | using System.Reflection; 5 | 6 | namespace DotnetRuntimeBootstrapper.AppHost.Core.Utils; 7 | 8 | internal static class EnvironmentEx 9 | { 10 | public static string ProcessPath { get; } = Assembly.GetExecutingAssembly().Location; 11 | 12 | public static void RefreshEnvironmentVariables() 13 | { 14 | var machineEnvironmentVariables = Environment 15 | .GetEnvironmentVariables(EnvironmentVariableTarget.Machine) 16 | .Cast(); 17 | 18 | foreach (var environmentVariable in machineEnvironmentVariables) 19 | { 20 | var key = (string)environmentVariable.Key; 21 | var value = (string?)environmentVariable.Value; 22 | 23 | Environment.SetEnvironmentVariable(key, value); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /DotnetRuntimeBootstrapper.AppHost.Core/Utils/Extensions/AssemblyExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Reflection; 3 | using System.Resources; 4 | using System.Text; 5 | 6 | namespace DotnetRuntimeBootstrapper.AppHost.Core.Utils.Extensions; 7 | 8 | internal static class AssemblyExtensions 9 | { 10 | public static string GetManifestResourceString(this Assembly assembly, string resourceName) 11 | { 12 | using var stream = 13 | assembly.GetManifestResourceStream(resourceName) 14 | ?? throw new MissingManifestResourceException( 15 | $"Failed to resolve resource '{resourceName}'." 16 | ); 17 | 18 | using var reader = new StreamReader(stream, Encoding.UTF8); 19 | 20 | return reader.ReadToEnd(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /DotnetRuntimeBootstrapper.AppHost.Core/Utils/Extensions/CollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace DotnetRuntimeBootstrapper.AppHost.Core.Utils.Extensions; 4 | 5 | internal static class CollectionExtensions 6 | { 7 | public static IEnumerable ToSingletonEnumerable(this T value) 8 | { 9 | yield return value; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /DotnetRuntimeBootstrapper.AppHost.Core/Utils/Extensions/GenericExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DotnetRuntimeBootstrapper.AppHost.Core.Utils.Extensions; 4 | 5 | internal static class GenericExtensions 6 | { 7 | public static TOut Pipe(this TIn input, Func transform) => 8 | transform(input); 9 | } 10 | -------------------------------------------------------------------------------- /DotnetRuntimeBootstrapper.AppHost.Core/Utils/Extensions/RegistryExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Win32; 2 | 3 | namespace DotnetRuntimeBootstrapper.AppHost.Core.Utils.Extensions; 4 | 5 | internal static class RegistryExtensions 6 | { 7 | public static bool ContainsSubKey(this RegistryKey key, string name) => 8 | key.OpenSubKey(name, false) is not null; 9 | } 10 | -------------------------------------------------------------------------------- /DotnetRuntimeBootstrapper.AppHost.Core/Utils/Extensions/StringExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace DotnetRuntimeBootstrapper.AppHost.Core.Utils.Extensions; 2 | 3 | internal static class StringExtensions 4 | { 5 | public static string? NullIfEmptyOrWhiteSpace(this string str) => 6 | !string.IsNullOrEmpty(str.Trim()) ? str : null; 7 | } 8 | -------------------------------------------------------------------------------- /DotnetRuntimeBootstrapper.AppHost.Core/Utils/FileEx.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using System.IO; 3 | using System.Text; 4 | 5 | namespace DotnetRuntimeBootstrapper.AppHost.Core.Utils; 6 | 7 | internal static class FileEx 8 | { 9 | private static string GenerateSalt() 10 | { 11 | var buffer = new StringBuilder(8); 12 | 13 | for (var i = 0; i < 8; i++) 14 | buffer.Append(RandomEx.Instance.Next(0, 10).ToString(CultureInfo.InvariantCulture)); 15 | 16 | return buffer.ToString(); 17 | } 18 | 19 | public static string GenerateTempFilePath(string fileNameBase) 20 | { 21 | var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(fileNameBase); 22 | var fileExtension = Path.GetExtension(fileNameBase); 23 | var salt = GenerateSalt(); 24 | 25 | return Path.Combine( 26 | Path.GetTempPath(), 27 | fileNameWithoutExtension + '.' + salt + fileExtension 28 | ); 29 | } 30 | 31 | public static void TryDelete(string filePath) 32 | { 33 | try 34 | { 35 | File.Delete(filePath); 36 | } 37 | catch 38 | { 39 | // Ignore 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /DotnetRuntimeBootstrapper.AppHost.Core/Utils/Http.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Net; 4 | 5 | namespace DotnetRuntimeBootstrapper.AppHost.Core.Utils; 6 | 7 | internal static class Http 8 | { 9 | private static readonly bool IsHttpsSupported; 10 | 11 | static Http() 12 | { 13 | try 14 | { 15 | // Disable certificate validation (valid certificate may fail on older operating systems) 16 | ServicePointManager.ServerCertificateValidationCallback = (_, _, _, _) => true; 17 | 18 | // Try to enable TLS1.2 if it's supported. 19 | // This is not required now, but may be in the future if one of the CDNs we rely on 20 | // decides to limit traffic to TLS1.2+ only. 21 | // Windows 7 doesn't have support for TLS1.2 out of the box, so this will always fail 22 | // unless the user has installed the corresponding Windows update (we can't install 23 | // it ourselves because it would require a reboot in the middle of installation). 24 | // On Windows 8 and higher this should succeed. 25 | ServicePointManager.SecurityProtocol = 26 | (SecurityProtocolType)0x00000C00 27 | | SecurityProtocolType.Tls 28 | | SecurityProtocolType.Ssl3; 29 | 30 | IsHttpsSupported = true; 31 | } 32 | catch 33 | { 34 | // This can fail if the protocol is not available 35 | } 36 | } 37 | 38 | private static HttpWebRequest CreateRequest(string url, string method = "GET") 39 | { 40 | var request = (HttpWebRequest) 41 | WebRequest.Create( 42 | // Certain older systems don't support HTTPS protocols required by most web servers. 43 | // If we're running on such a system, we have to downgrade to HTTP. 44 | IsHttpsSupported ? url : Url.ReplaceProtocol(url, "http") 45 | ); 46 | 47 | request.Method = method; 48 | 49 | return request; 50 | } 51 | 52 | private static Stream GetContentStream(string url, out long length) 53 | { 54 | try 55 | { 56 | var request = CreateRequest(url); 57 | var response = request.GetResponse(); 58 | 59 | length = response.ContentLength; 60 | return response.GetResponseStream() ?? Stream.Null; 61 | } 62 | catch (WebException ex) 63 | { 64 | throw new WebException($"Failed to send HTTP request to '{url}'.", ex); 65 | } 66 | } 67 | 68 | public static Stream GetContentStream(string url) => GetContentStream(url, out _); 69 | 70 | public static string GetContentString(string url) 71 | { 72 | using var stream = GetContentStream(url); 73 | using var reader = new StreamReader(stream); 74 | 75 | return reader.ReadToEnd(); 76 | } 77 | 78 | public static void DownloadFile( 79 | string url, 80 | string outputFilePath, 81 | Action? handleProgress = null 82 | ) 83 | { 84 | using var source = GetContentStream(url, out var contentLength); 85 | using var destination = File.Create(outputFilePath); 86 | 87 | var buffer = new byte[81920]; 88 | 89 | var totalBytesCopied = 0L; 90 | while (true) 91 | { 92 | var bytesCopied = source.Read(buffer, 0, buffer.Length); 93 | if (bytesCopied <= 0) 94 | break; 95 | 96 | destination.Write(buffer, 0, bytesCopied); 97 | 98 | // Report progress 99 | totalBytesCopied += bytesCopied; 100 | handleProgress?.Invoke(1.0 * totalBytesCopied / contentLength); 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /DotnetRuntimeBootstrapper.AppHost.Core/Utils/IconEx.cs: -------------------------------------------------------------------------------- 1 | using System.Drawing; 2 | using DotnetRuntimeBootstrapper.AppHost.Core.Native; 3 | 4 | namespace DotnetRuntimeBootstrapper.AppHost.Core.Utils; 5 | 6 | internal static class IconEx 7 | { 8 | // Built-in method in WinForms fails for network paths 9 | // https://github.com/Tyrrrz/DotnetRuntimeBootstrapper/issues/29 10 | public static Icon? TryExtractAssociatedIcon(string filePath) 11 | { 12 | var handle = NativeMethods.ExtractAssociatedIcon(0, filePath, out _); 13 | if (handle == 0) 14 | return null; 15 | 16 | try 17 | { 18 | return Icon.FromHandle(handle); 19 | } 20 | catch 21 | { 22 | return null; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /DotnetRuntimeBootstrapper.AppHost.Core/Utils/PathEx.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.Linq; 4 | 5 | namespace DotnetRuntimeBootstrapper.AppHost.Core.Utils; 6 | 7 | internal static class PathEx 8 | { 9 | public static string Combine(params IEnumerable paths) => paths.Aggregate(Path.Combine); 10 | } 11 | -------------------------------------------------------------------------------- /DotnetRuntimeBootstrapper.AppHost.Core/Utils/RandomEx.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DotnetRuntimeBootstrapper.AppHost.Core.Utils; 4 | 5 | internal static class RandomEx 6 | { 7 | public static Random Instance { get; } = new(); 8 | } 9 | -------------------------------------------------------------------------------- /DotnetRuntimeBootstrapper.AppHost.Core/Utils/Url.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.RegularExpressions; 3 | using DotnetRuntimeBootstrapper.AppHost.Core.Utils.Extensions; 4 | 5 | namespace DotnetRuntimeBootstrapper.AppHost.Core.Utils; 6 | 7 | internal static class Url 8 | { 9 | public static string ReplaceProtocol(string url, string protocol) 10 | { 11 | if (url.StartsWith(protocol + "://", StringComparison.Ordinal)) 12 | return url; 13 | 14 | var index = url.IndexOf("://", StringComparison.Ordinal); 15 | if (index < 0) 16 | return url; 17 | 18 | return protocol + url[index..]; 19 | } 20 | 21 | public static string? TryExtractFileName(string url) => 22 | Regex.Match(url, @".+/([^?]*)").Groups[1].Value.NullIfEmptyOrWhiteSpace(); 23 | } 24 | -------------------------------------------------------------------------------- /DotnetRuntimeBootstrapper.AppHost.Core/Utils/VersionEx.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.RegularExpressions; 3 | 4 | namespace DotnetRuntimeBootstrapper.AppHost.Core.Utils; 5 | 6 | internal static class VersionEx 7 | { 8 | public static Version? TryParse(string value) => 9 | Regex.IsMatch(value, @"^\d+\.\d+(?:\.\d+)?(?:\.\d+)?$") ? Parse(value) : null; 10 | 11 | public static Version Parse(string value) => new(value); 12 | } 13 | -------------------------------------------------------------------------------- /DotnetRuntimeBootstrapper.AppHost.Gui/Bootstrapper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Windows.Forms; 3 | using DotnetRuntimeBootstrapper.AppHost.Core; 4 | using DotnetRuntimeBootstrapper.AppHost.Core.Prerequisites; 5 | using DotnetRuntimeBootstrapper.AppHost.Gui.Utils; 6 | 7 | namespace DotnetRuntimeBootstrapper.AppHost.Gui; 8 | 9 | public class Bootstrapper : BootstrapperBase 10 | { 11 | protected override void ReportError(string message) => 12 | MessageBox.Show(message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); 13 | 14 | protected override bool Prompt( 15 | TargetAssembly targetAssembly, 16 | IPrerequisite[] missingPrerequisites 17 | ) 18 | { 19 | ApplicationEx.EnsureInitialized(); 20 | 21 | using var promptForm = new PromptForm(targetAssembly, missingPrerequisites); 22 | Application.Run(promptForm); 23 | 24 | return promptForm.IsSuccess; 25 | } 26 | 27 | protected override bool Install( 28 | TargetAssembly targetAssembly, 29 | IPrerequisite[] missingPrerequisites 30 | ) 31 | { 32 | ApplicationEx.EnsureInitialized(); 33 | 34 | using var installForm = new InstallForm(targetAssembly, missingPrerequisites); 35 | Application.Run(installForm); 36 | 37 | return installForm.IsSuccess; 38 | } 39 | 40 | [STAThread] 41 | public static int Main(string[] args) => new Bootstrapper().Run(args); 42 | } 43 | -------------------------------------------------------------------------------- /DotnetRuntimeBootstrapper.AppHost.Gui/DotnetRuntimeBootstrapper.AppHost.Gui.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | WinExe 5 | net35 6 | true 7 | ../favicon.ico 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /DotnetRuntimeBootstrapper.AppHost.Gui/InstallForm.Designer.cs: -------------------------------------------------------------------------------- 1 | namespace DotnetRuntimeBootstrapper.AppHost.Gui 2 | { 3 | partial class InstallForm 4 | { 5 | /// 6 | /// Required designer variable. 7 | /// 8 | private System.ComponentModel.IContainer components = null; 9 | 10 | /// 11 | /// Clean up any resources being used. 12 | /// 13 | /// true if managed resources should be disposed; otherwise, false. 14 | protected override void Dispose(bool disposing) 15 | { 16 | if (disposing && (components != null)) 17 | { 18 | components.Dispose(); 19 | } 20 | 21 | base.Dispose(disposing); 22 | } 23 | 24 | #region Windows Form Designer generated code 25 | 26 | /// 27 | /// Required method for Designer support - do not modify 28 | /// the contents of this method with the code editor. 29 | /// 30 | private void InitializeComponent() 31 | { 32 | this.TotalProgressBar = new System.Windows.Forms.ProgressBar(); 33 | this.StatusLabel = new System.Windows.Forms.Label(); 34 | this.CurrentProgressBar = new System.Windows.Forms.ProgressBar(); 35 | this.TotalProgressLabel = new System.Windows.Forms.Label(); 36 | this.SuspendLayout(); 37 | // 38 | // TotalProgressBar 39 | // 40 | this.TotalProgressBar.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); 41 | this.TotalProgressBar.Location = new System.Drawing.Point(12, 110); 42 | this.TotalProgressBar.Name = "TotalProgressBar"; 43 | this.TotalProgressBar.Size = new System.Drawing.Size(489, 30); 44 | this.TotalProgressBar.TabIndex = 0; 45 | // 46 | // StatusLabel 47 | // 48 | this.StatusLabel.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); 49 | this.StatusLabel.Location = new System.Drawing.Point(8, 10); 50 | this.StatusLabel.Name = "StatusLabel"; 51 | this.StatusLabel.Size = new System.Drawing.Size(490, 20); 52 | this.StatusLabel.TabIndex = 1; 53 | this.StatusLabel.Text = "Preparing..."; 54 | // 55 | // CurrentProgressBar 56 | // 57 | this.CurrentProgressBar.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); 58 | this.CurrentProgressBar.Location = new System.Drawing.Point(12, 40); 59 | this.CurrentProgressBar.Name = "CurrentProgressBar"; 60 | this.CurrentProgressBar.Size = new System.Drawing.Size(489, 30); 61 | this.CurrentProgressBar.TabIndex = 2; 62 | // 63 | // TotalProgressLabel 64 | // 65 | this.TotalProgressLabel.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); 66 | this.TotalProgressLabel.Location = new System.Drawing.Point(8, 80); 67 | this.TotalProgressLabel.Name = "TotalProgressLabel"; 68 | this.TotalProgressLabel.Size = new System.Drawing.Size(490, 20); 69 | this.TotalProgressLabel.TabIndex = 3; 70 | this.TotalProgressLabel.Text = "Total progress: ..."; 71 | // 72 | // InstallForm 73 | // 74 | this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 20F); 75 | this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; 76 | this.BackColor = System.Drawing.SystemColors.Window; 77 | this.ClientSize = new System.Drawing.Size(509, 161); 78 | this.Controls.Add(this.TotalProgressLabel); 79 | this.Controls.Add(this.CurrentProgressBar); 80 | this.Controls.Add(this.StatusLabel); 81 | this.Controls.Add(this.TotalProgressBar); 82 | this.Font = new System.Drawing.Font("Segoe UI", 11F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); 83 | this.Margin = new System.Windows.Forms.Padding(4, 5, 4, 5); 84 | this.MaximizeBox = false; 85 | this.MaximumSize = new System.Drawing.Size(525, 200); 86 | this.MinimumSize = new System.Drawing.Size(525, 200); 87 | this.Name = "InstallForm"; 88 | this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen; 89 | this.Text = ".NET Runtime Bootstrapper"; 90 | this.Load += new System.EventHandler(this.InstallationForm_Load); 91 | this.ResumeLayout(false); 92 | } 93 | 94 | #endregion 95 | 96 | private System.Windows.Forms.ProgressBar TotalProgressBar; 97 | private System.Windows.Forms.Label StatusLabel; 98 | private System.Windows.Forms.ProgressBar CurrentProgressBar; 99 | private System.Windows.Forms.Label TotalProgressLabel; 100 | } 101 | } -------------------------------------------------------------------------------- /DotnetRuntimeBootstrapper.AppHost.Gui/InstallForm.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading; 4 | using System.Windows.Forms; 5 | using DotnetRuntimeBootstrapper.AppHost.Core; 6 | using DotnetRuntimeBootstrapper.AppHost.Core.Platform; 7 | using DotnetRuntimeBootstrapper.AppHost.Core.Prerequisites; 8 | using DotnetRuntimeBootstrapper.AppHost.Core.Utils; 9 | 10 | namespace DotnetRuntimeBootstrapper.AppHost.Gui; 11 | 12 | public partial class InstallForm : Form 13 | { 14 | private readonly TargetAssembly _targetAssembly; 15 | private readonly IPrerequisite[] _missingPrerequisites; 16 | 17 | // Disable close button 18 | protected override CreateParams CreateParams 19 | { 20 | get 21 | { 22 | var result = base.CreateParams; 23 | result.ClassStyle |= 0x200; 24 | return result; 25 | } 26 | } 27 | 28 | public bool IsSuccess { get; private set; } 29 | 30 | public InstallForm(TargetAssembly targetAssembly, IPrerequisite[] missingPrerequisites) 31 | { 32 | _targetAssembly = targetAssembly; 33 | _missingPrerequisites = missingPrerequisites; 34 | 35 | InitializeComponent(); 36 | } 37 | 38 | private void InvokeOnUI(Action action) => Invoke(action); 39 | 40 | private void UpdateStatus(string status) => InvokeOnUI(() => StatusLabel.Text = status); 41 | 42 | private void UpdateCurrentProgress(double progress) => 43 | InvokeOnUI(() => 44 | { 45 | if (progress >= 0) 46 | { 47 | CurrentProgressBar.Style = ProgressBarStyle.Continuous; 48 | CurrentProgressBar.Value = (int)(progress * 100); 49 | } 50 | else 51 | { 52 | CurrentProgressBar.Style = ProgressBarStyle.Marquee; 53 | } 54 | }); 55 | 56 | private void UpdateTotalProgress(double totalProgress) => 57 | InvokeOnUI(() => 58 | { 59 | if (totalProgress >= 0) 60 | { 61 | TotalProgressBar.Style = ProgressBarStyle.Continuous; 62 | TotalProgressBar.Value = (int)(totalProgress * 100); 63 | TotalProgressLabel.Text = @$"Total progress: {totalProgress:P0}"; 64 | } 65 | else 66 | { 67 | TotalProgressBar.Style = ProgressBarStyle.Marquee; 68 | } 69 | }); 70 | 71 | private void Execute() 72 | { 73 | var currentStep = 1; 74 | var totalSteps = _missingPrerequisites.Length * 2; 75 | 76 | // Download 77 | var installers = new List(); 78 | foreach (var prerequisite in _missingPrerequisites) 79 | { 80 | UpdateStatus( 81 | @$"[{currentStep}/{totalSteps}] Downloading {prerequisite.DisplayName}..." 82 | ); 83 | UpdateCurrentProgress(0); 84 | 85 | var installer = prerequisite.DownloadInstaller(p => 86 | { 87 | UpdateCurrentProgress(p); 88 | UpdateTotalProgress((installers.Count + p) / (2.0 * _missingPrerequisites.Length)); 89 | }); 90 | 91 | installers.Add(installer); 92 | 93 | currentStep++; 94 | } 95 | 96 | // Install 97 | var isRebootRequired = false; 98 | var installersFinishedCount = 0; 99 | foreach (var installer in installers) 100 | { 101 | UpdateStatus( 102 | @$"[{currentStep}/{totalSteps}] Installing {installer.Prerequisite.DisplayName}..." 103 | ); 104 | UpdateCurrentProgress(-1); 105 | 106 | var installationResult = installer.Run(); 107 | 108 | FileEx.TryDelete(installer.FilePath); 109 | 110 | if (installationResult == PrerequisiteInstallerResult.RebootRequired) 111 | isRebootRequired = true; 112 | 113 | UpdateTotalProgress(0.5 + ++installersFinishedCount / (2.0 * installers.Count)); 114 | currentStep++; 115 | } 116 | 117 | // Finalize 118 | if (isRebootRequired) 119 | { 120 | var isRebootAccepted = 121 | MessageBox.Show( 122 | @$"You need to restart Windows before you can run {_targetAssembly.Name}. " 123 | + @"Would you like to do it now?", 124 | @"Restart required", 125 | MessageBoxButtons.YesNo, 126 | MessageBoxIcon.Warning 127 | ) == DialogResult.Yes; 128 | 129 | if (isRebootAccepted) 130 | OperatingSystemEx.Reboot(); 131 | 132 | IsSuccess = false; 133 | } 134 | else 135 | { 136 | IsSuccess = true; 137 | } 138 | 139 | Close(); 140 | } 141 | 142 | private void InstallationForm_Load(object sender, EventArgs e) 143 | { 144 | Text = @$"{_targetAssembly.Name}: installing prerequisites"; 145 | Icon = IconEx.TryExtractAssociatedIcon(Application.ExecutablePath); 146 | 147 | UpdateStatus(@"Preparing installation"); 148 | 149 | new Thread(Execute) { Name = nameof(Execute), IsBackground = true }.Start(); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /DotnetRuntimeBootstrapper.AppHost.Gui/PromptForm.Designer.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | 3 | namespace DotnetRuntimeBootstrapper.AppHost.Gui 4 | { 5 | partial class PromptForm 6 | { 7 | /// 8 | /// Required designer variable. 9 | /// 10 | private IContainer components = null; 11 | 12 | /// 13 | /// Clean up any resources being used. 14 | /// 15 | /// true if managed resources should be disposed; otherwise, false. 16 | protected override void Dispose(bool disposing) 17 | { 18 | if (disposing && (components != null)) 19 | { 20 | components.Dispose(); 21 | } 22 | 23 | base.Dispose(disposing); 24 | } 25 | 26 | #region Windows Form Designer generated code 27 | 28 | /// 29 | /// Required method for Designer support - do not modify 30 | /// the contents of this method with the code editor. 31 | /// 32 | private void InitializeComponent() 33 | { 34 | this.components = new System.ComponentModel.Container(); 35 | this.ToolTip = new System.Windows.Forms.ToolTip(this.components); 36 | this.InstallButton = new System.Windows.Forms.Button(); 37 | this.ExitButton = new System.Windows.Forms.Button(); 38 | this.MainPanel = new System.Windows.Forms.Panel(); 39 | this.MissingComponentsLabel = new System.Windows.Forms.Label(); 40 | this.MissingPrerequisitesTextBox = new System.Windows.Forms.TextBox(); 41 | this.DescriptionLabel = new System.Windows.Forms.Label(); 42 | this.IconPictureBox = new System.Windows.Forms.PictureBox(); 43 | this.MainPanel.SuspendLayout(); 44 | ((System.ComponentModel.ISupportInitialize)(this.IconPictureBox)).BeginInit(); 45 | this.SuspendLayout(); 46 | // 47 | // InstallButton 48 | // 49 | this.InstallButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); 50 | this.InstallButton.AutoEllipsis = true; 51 | this.InstallButton.Font = new System.Drawing.Font("Segoe UI", 11F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(0))); 52 | this.InstallButton.Location = new System.Drawing.Point(307, 230); 53 | this.InstallButton.Name = "InstallButton"; 54 | this.InstallButton.Size = new System.Drawing.Size(90, 42); 55 | this.InstallButton.TabIndex = 8; 56 | this.InstallButton.Text = "Install"; 57 | this.ToolTip.SetToolTip(this.InstallButton, "Download and install the missing components"); 58 | this.InstallButton.UseVisualStyleBackColor = true; 59 | this.InstallButton.Click += new System.EventHandler(this.InstallButton_Click); 60 | // 61 | // ExitButton 62 | // 63 | this.ExitButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); 64 | this.ExitButton.AutoEllipsis = true; 65 | this.ExitButton.DialogResult = System.Windows.Forms.DialogResult.Cancel; 66 | this.ExitButton.Location = new System.Drawing.Point(402, 230); 67 | this.ExitButton.Name = "ExitButton"; 68 | this.ExitButton.Size = new System.Drawing.Size(90, 42); 69 | this.ExitButton.TabIndex = 9; 70 | this.ExitButton.Text = "Cancel"; 71 | this.ToolTip.SetToolTip(this.ExitButton, "Exit without running the application"); 72 | this.ExitButton.UseVisualStyleBackColor = true; 73 | this.ExitButton.Click += new System.EventHandler(this.ExitButton_Click); 74 | // 75 | // MainPanel 76 | // 77 | this.MainPanel.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); 78 | this.MainPanel.BackColor = System.Drawing.SystemColors.Window; 79 | this.MainPanel.Controls.Add(this.MissingComponentsLabel); 80 | this.MainPanel.Controls.Add(this.MissingPrerequisitesTextBox); 81 | this.MainPanel.Controls.Add(this.DescriptionLabel); 82 | this.MainPanel.Controls.Add(this.IconPictureBox); 83 | this.MainPanel.Location = new System.Drawing.Point(-1, -1); 84 | this.MainPanel.Name = "MainPanel"; 85 | this.MainPanel.Size = new System.Drawing.Size(505, 220); 86 | this.MainPanel.TabIndex = 11; 87 | // 88 | // MissingComponentsLabel 89 | // 90 | this.MissingComponentsLabel.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); 91 | this.MissingComponentsLabel.Location = new System.Drawing.Point(8, 75); 92 | this.MissingComponentsLabel.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); 93 | this.MissingComponentsLabel.Name = "MissingComponentsLabel"; 94 | this.MissingComponentsLabel.Size = new System.Drawing.Size(484, 20); 95 | this.MissingComponentsLabel.TabIndex = 8; 96 | this.MissingComponentsLabel.Text = "Missing components:"; 97 | // 98 | // MissingPrerequisitesTextBox 99 | // 100 | this.MissingPrerequisitesTextBox.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); 101 | this.MissingPrerequisitesTextBox.BackColor = System.Drawing.SystemColors.Window; 102 | this.MissingPrerequisitesTextBox.Location = new System.Drawing.Point(12, 100); 103 | this.MissingPrerequisitesTextBox.Multiline = true; 104 | this.MissingPrerequisitesTextBox.Name = "MissingPrerequisitesTextBox"; 105 | this.MissingPrerequisitesTextBox.ReadOnly = true; 106 | this.MissingPrerequisitesTextBox.ScrollBars = System.Windows.Forms.ScrollBars.Vertical; 107 | this.MissingPrerequisitesTextBox.Size = new System.Drawing.Size(480, 105); 108 | this.MissingPrerequisitesTextBox.TabIndex = 7; 109 | this.MissingPrerequisitesTextBox.TabStop = false; 110 | // 111 | // DescriptionLabel 112 | // 113 | this.DescriptionLabel.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); 114 | this.DescriptionLabel.Location = new System.Drawing.Point(56, 13); 115 | this.DescriptionLabel.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); 116 | this.DescriptionLabel.Name = "DescriptionLabel"; 117 | this.DescriptionLabel.Size = new System.Drawing.Size(436, 45); 118 | this.DescriptionLabel.TabIndex = 6; 119 | this.DescriptionLabel.Text = "Your system is missing runtime components required by this application. Would you" + " like to download and install them now?"; 120 | // 121 | // IconPictureBox 122 | // 123 | this.IconPictureBox.Image = System.Drawing.SystemIcons.Warning.ToBitmap(); 124 | this.IconPictureBox.Location = new System.Drawing.Point(16, 16); 125 | this.IconPictureBox.Margin = new System.Windows.Forms.Padding(4); 126 | this.IconPictureBox.Name = "IconPictureBox"; 127 | this.IconPictureBox.Size = new System.Drawing.Size(32, 32); 128 | this.IconPictureBox.SizeMode = System.Windows.Forms.PictureBoxSizeMode.Zoom; 129 | this.IconPictureBox.TabIndex = 5; 130 | this.IconPictureBox.TabStop = false; 131 | // 132 | // PromptForm 133 | // 134 | this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 20F); 135 | this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; 136 | this.ClientSize = new System.Drawing.Size(504, 281); 137 | this.Controls.Add(this.MainPanel); 138 | this.Controls.Add(this.InstallButton); 139 | this.Controls.Add(this.ExitButton); 140 | this.Font = new System.Drawing.Font("Segoe UI", 11F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); 141 | this.Margin = new System.Windows.Forms.Padding(4); 142 | this.MaximizeBox = false; 143 | this.MinimumSize = new System.Drawing.Size(520, 320); 144 | this.Name = "PromptForm"; 145 | this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen; 146 | this.Text = ".NET Runtime Bootstrapper"; 147 | this.Load += new System.EventHandler(this.InstallationPromptForm_Load); 148 | this.MainPanel.ResumeLayout(false); 149 | this.MainPanel.PerformLayout(); 150 | ((System.ComponentModel.ISupportInitialize)(this.IconPictureBox)).EndInit(); 151 | this.ResumeLayout(false); 152 | } 153 | 154 | #endregion 155 | 156 | private System.Windows.Forms.Button InstallButton; 157 | private System.Windows.Forms.Button ExitButton; 158 | private System.Windows.Forms.ToolTip ToolTip; 159 | private System.Windows.Forms.Panel MainPanel; 160 | private System.Windows.Forms.Label MissingComponentsLabel; 161 | private System.Windows.Forms.TextBox MissingPrerequisitesTextBox; 162 | private System.Windows.Forms.Label DescriptionLabel; 163 | private System.Windows.Forms.PictureBox IconPictureBox; 164 | } 165 | } -------------------------------------------------------------------------------- /DotnetRuntimeBootstrapper.AppHost.Gui/PromptForm.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Windows.Forms; 4 | using DotnetRuntimeBootstrapper.AppHost.Core; 5 | using DotnetRuntimeBootstrapper.AppHost.Core.Prerequisites; 6 | using DotnetRuntimeBootstrapper.AppHost.Core.Utils; 7 | 8 | namespace DotnetRuntimeBootstrapper.AppHost.Gui; 9 | 10 | public partial class PromptForm : Form 11 | { 12 | private readonly TargetAssembly _targetAssembly; 13 | private readonly IPrerequisite[] _missingPrerequisites; 14 | 15 | public bool IsSuccess { get; private set; } 16 | 17 | public PromptForm(TargetAssembly targetAssembly, IPrerequisite[] missingPrerequisites) 18 | { 19 | _targetAssembly = targetAssembly; 20 | _missingPrerequisites = missingPrerequisites; 21 | 22 | InitializeComponent(); 23 | } 24 | 25 | private void InstallationPromptForm_Load(object sender, EventArgs e) 26 | { 27 | Text = @$"{_targetAssembly.Name}: prerequisites missing"; 28 | Icon = IconEx.TryExtractAssociatedIcon(Application.ExecutablePath); 29 | MissingPrerequisitesTextBox.Lines = _missingPrerequisites 30 | .Select(c => $"• {c.DisplayName}") 31 | .ToArray(); 32 | } 33 | 34 | private void InstallButton_Click(object sender, EventArgs e) 35 | { 36 | IsSuccess = true; 37 | Close(); 38 | } 39 | 40 | private void ExitButton_Click(object sender, EventArgs e) 41 | { 42 | IsSuccess = false; 43 | Close(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /DotnetRuntimeBootstrapper.AppHost.Gui/Utils/ApplicationEx.cs: -------------------------------------------------------------------------------- 1 | using System.Windows.Forms; 2 | 3 | namespace DotnetRuntimeBootstrapper.AppHost.Gui.Utils; 4 | 5 | internal static class ApplicationEx 6 | { 7 | private static bool _isInitialized; 8 | 9 | public static void EnsureInitialized() 10 | { 11 | if (_isInitialized) 12 | return; 13 | 14 | _isInitialized = true; 15 | 16 | Application.EnableVisualStyles(); 17 | Application.SetCompatibleTextRenderingDefault(false); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /DotnetRuntimeBootstrapper.Demo.Cli/DotnetRuntimeBootstrapper.Demo.Cli.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Exe 10 | net9.0 11 | 1.2.3 12 | ../favicon.ico 13 | true 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /DotnetRuntimeBootstrapper.Demo.Cli/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | 4 | namespace DotnetRuntimeBootstrapper.Demo.Cli; 5 | 6 | public static class Program 7 | { 8 | public static void Main(string[] args) 9 | { 10 | // Show routed command line arguments 11 | if (args.Any()) 12 | { 13 | Console.WriteLine("Routed command line arguments:"); 14 | Console.WriteLine(string.Join(" ", args)); 15 | Console.WriteLine(); 16 | } 17 | 18 | Console.WriteLine("Hello world!"); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /DotnetRuntimeBootstrapper.Demo.Gui/DotnetRuntimeBootstrapper.Demo.Gui.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | WinExe 10 | net9.0-windows 11 | true 12 | 1.2.3 13 | ../favicon.ico 14 | Manifest.xml 15 | true 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /DotnetRuntimeBootstrapper.Demo.Gui/MainForm.Designer.cs: -------------------------------------------------------------------------------- 1 | namespace DotnetRuntimeBootstrapper.Demo.Gui 2 | { 3 | partial class MainForm 4 | { 5 | /// 6 | /// Required designer variable. 7 | /// 8 | private System.ComponentModel.IContainer components = null; 9 | 10 | /// 11 | /// Clean up any resources being used. 12 | /// 13 | /// true if managed resources should be disposed; otherwise, false. 14 | protected override void Dispose(bool disposing) 15 | { 16 | if (disposing && (components != null)) 17 | { 18 | components.Dispose(); 19 | } 20 | 21 | base.Dispose(disposing); 22 | } 23 | 24 | #region Windows Form Designer generated code 25 | 26 | /// 27 | /// Required method for Designer support - do not modify 28 | /// the contents of this method with the code editor. 29 | /// 30 | private void InitializeComponent() 31 | { 32 | this.MainLabel = new System.Windows.Forms.Label(); 33 | this.SuspendLayout(); 34 | // 35 | // MainLabel 36 | // 37 | this.MainLabel.Anchor = ((System.Windows.Forms.AnchorStyles) ((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); 38 | this.MainLabel.Font = new System.Drawing.Font("Microsoft Sans Serif", 32F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte) (0))); 39 | this.MainLabel.Location = new System.Drawing.Point(12, 9); 40 | this.MainLabel.Name = "MainLabel"; 41 | this.MainLabel.Size = new System.Drawing.Size(776, 432); 42 | this.MainLabel.TabIndex = 0; 43 | this.MainLabel.Text = "Hello world!"; 44 | this.MainLabel.TextAlign = System.Drawing.ContentAlignment.MiddleCenter; 45 | // 46 | // MainForm 47 | // 48 | this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); 49 | this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; 50 | this.ClientSize = new System.Drawing.Size(800, 450); 51 | this.Controls.Add(this.MainLabel); 52 | this.Name = "MainForm"; 53 | this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen; 54 | this.Text = "Hello world"; 55 | this.ResumeLayout(false); 56 | } 57 | 58 | private System.Windows.Forms.Label MainLabel; 59 | 60 | #endregion 61 | } 62 | } -------------------------------------------------------------------------------- /DotnetRuntimeBootstrapper.Demo.Gui/MainForm.cs: -------------------------------------------------------------------------------- 1 | using System.Windows.Forms; 2 | 3 | namespace DotnetRuntimeBootstrapper.Demo.Gui; 4 | 5 | public partial class MainForm : Form 6 | { 7 | public MainForm() 8 | { 9 | InitializeComponent(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /DotnetRuntimeBootstrapper.Demo.Gui/Manifest.xml: -------------------------------------------------------------------------------- 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 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /DotnetRuntimeBootstrapper.Demo.Gui/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Windows.Forms; 4 | 5 | namespace DotnetRuntimeBootstrapper.Demo.Gui; 6 | 7 | public static class Program 8 | { 9 | [STAThread] 10 | public static void Main(string[] args) 11 | { 12 | // Show routed command line arguments 13 | if (args.Any()) 14 | { 15 | MessageBox.Show(string.Join(" ", args), "Routed command line arguments"); 16 | } 17 | 18 | // Show form 19 | Application.SetHighDpiMode(HighDpiMode.SystemAware); 20 | Application.EnableVisualStyles(); 21 | Application.SetCompatibleTextRenderingDefault(false); 22 | Application.Run(new MainForm()); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /DotnetRuntimeBootstrapper.sln: -------------------------------------------------------------------------------- 1 | Microsoft Visual Studio Solution File, Format Version 12.00 2 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotnetRuntimeBootstrapper.AppHost.Gui", "DotnetRuntimeBootstrapper.AppHost.Gui\DotnetRuntimeBootstrapper.AppHost.Gui.csproj", "{C3E22118-1186-41FB-939F-7C48EA109D07}" 3 | EndProject 4 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotnetRuntimeBootstrapper", "DotnetRuntimeBootstrapper\DotnetRuntimeBootstrapper.csproj", "{40809F7D-623B-4CB1-8EF4-25A90352B487}" 5 | EndProject 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Misc", "Misc", "{ADC60863-6275-4C58-9223-C8CCDBB7D12D}" 7 | ProjectSection(SolutionItems) = preProject 8 | Directory.Build.props = Directory.Build.props 9 | License.txt = License.txt 10 | Readme.md = Readme.md 11 | EndProjectSection 12 | EndProject 13 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotnetRuntimeBootstrapper.Demo.Gui", "DotnetRuntimeBootstrapper.Demo.Gui\DotnetRuntimeBootstrapper.Demo.Gui.csproj", "{E3B1C334-C7BC-41C5-BE94-F15A4A77346D}" 14 | EndProject 15 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotnetRuntimeBootstrapper.AppHost.Core", "DotnetRuntimeBootstrapper.AppHost.Core\DotnetRuntimeBootstrapper.AppHost.Core.csproj", "{BBC765B2-054F-4F83-911E-52E1FA180AB8}" 16 | EndProject 17 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotnetRuntimeBootstrapper.Demo.Cli", "DotnetRuntimeBootstrapper.Demo.Cli\DotnetRuntimeBootstrapper.Demo.Cli.csproj", "{87960C84-72B2-4472-888A-D85E92CFEFC3}" 18 | EndProject 19 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotnetRuntimeBootstrapper.AppHost.Cli", "DotnetRuntimeBootstrapper.AppHost.Cli\DotnetRuntimeBootstrapper.AppHost.Cli.csproj", "{FF316DCE-0272-4BAB-9BA9-ED3BCF2BECC4}" 20 | EndProject 21 | Global 22 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 23 | Debug|Any CPU = Debug|Any CPU 24 | Release|Any CPU = Release|Any CPU 25 | EndGlobalSection 26 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 27 | {C3E22118-1186-41FB-939F-7C48EA109D07}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 28 | {C3E22118-1186-41FB-939F-7C48EA109D07}.Debug|Any CPU.Build.0 = Debug|Any CPU 29 | {C3E22118-1186-41FB-939F-7C48EA109D07}.Release|Any CPU.ActiveCfg = Release|Any CPU 30 | {C3E22118-1186-41FB-939F-7C48EA109D07}.Release|Any CPU.Build.0 = Release|Any CPU 31 | {40809F7D-623B-4CB1-8EF4-25A90352B487}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 32 | {40809F7D-623B-4CB1-8EF4-25A90352B487}.Debug|Any CPU.Build.0 = Debug|Any CPU 33 | {40809F7D-623B-4CB1-8EF4-25A90352B487}.Release|Any CPU.ActiveCfg = Release|Any CPU 34 | {40809F7D-623B-4CB1-8EF4-25A90352B487}.Release|Any CPU.Build.0 = Release|Any CPU 35 | {E3B1C334-C7BC-41C5-BE94-F15A4A77346D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 36 | {E3B1C334-C7BC-41C5-BE94-F15A4A77346D}.Debug|Any CPU.Build.0 = Debug|Any CPU 37 | {E3B1C334-C7BC-41C5-BE94-F15A4A77346D}.Release|Any CPU.ActiveCfg = Release|Any CPU 38 | {E3B1C334-C7BC-41C5-BE94-F15A4A77346D}.Release|Any CPU.Build.0 = Release|Any CPU 39 | {BBC765B2-054F-4F83-911E-52E1FA180AB8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 40 | {BBC765B2-054F-4F83-911E-52E1FA180AB8}.Debug|Any CPU.Build.0 = Debug|Any CPU 41 | {BBC765B2-054F-4F83-911E-52E1FA180AB8}.Release|Any CPU.ActiveCfg = Release|Any CPU 42 | {BBC765B2-054F-4F83-911E-52E1FA180AB8}.Release|Any CPU.Build.0 = Release|Any CPU 43 | {87960C84-72B2-4472-888A-D85E92CFEFC3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 44 | {87960C84-72B2-4472-888A-D85E92CFEFC3}.Debug|Any CPU.Build.0 = Debug|Any CPU 45 | {87960C84-72B2-4472-888A-D85E92CFEFC3}.Release|Any CPU.ActiveCfg = Release|Any CPU 46 | {87960C84-72B2-4472-888A-D85E92CFEFC3}.Release|Any CPU.Build.0 = Release|Any CPU 47 | {FF316DCE-0272-4BAB-9BA9-ED3BCF2BECC4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 48 | {FF316DCE-0272-4BAB-9BA9-ED3BCF2BECC4}.Debug|Any CPU.Build.0 = Debug|Any CPU 49 | {FF316DCE-0272-4BAB-9BA9-ED3BCF2BECC4}.Release|Any CPU.ActiveCfg = Release|Any CPU 50 | {FF316DCE-0272-4BAB-9BA9-ED3BCF2BECC4}.Release|Any CPU.Build.0 = Release|Any CPU 51 | EndGlobalSection 52 | EndGlobal 53 | -------------------------------------------------------------------------------- /DotnetRuntimeBootstrapper/BootstrapperTask.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Reflection; 4 | using System.Runtime.InteropServices; 5 | using System.Text; 6 | using DotnetRuntimeBootstrapper.Utils.Extensions; 7 | using Microsoft.Build.Framework; 8 | using Microsoft.Build.Utilities; 9 | using Mono.Cecil; 10 | using Ressy; 11 | using Ressy.HighLevel.Versions; 12 | 13 | namespace DotnetRuntimeBootstrapper; 14 | 15 | public class BootstrapperTask : Task 16 | { 17 | private Version Version { get; } = Assembly.GetExecutingAssembly().GetName().Version; 18 | 19 | public string? RuntimeIdentifier { get; init; } 20 | 21 | public bool IsWindowsTarget => 22 | string.IsNullOrWhiteSpace(RuntimeIdentifier) 23 | && RuntimeInformation.IsOSPlatform(OSPlatform.Windows) 24 | || RuntimeIdentifier?.StartsWith("win", StringComparison.OrdinalIgnoreCase) == true; 25 | 26 | [Required] 27 | public required string Variant { get; init; } 28 | 29 | [Required] 30 | public required bool IsPromptRequired { get; init; } 31 | 32 | [Required] 33 | public required string TargetFilePath { get; init; } 34 | 35 | public string TargetFileName => Path.GetFileName(TargetFilePath); 36 | 37 | public string AppHostFilePath => Path.ChangeExtension(TargetFilePath, "exe"); 38 | 39 | public string AppHostFileName => Path.GetFileName(AppHostFilePath); 40 | 41 | private void ExtractAppHost() 42 | { 43 | Log.LogMessage("Extracting apphost..."); 44 | 45 | var assembly = Assembly.GetExecutingAssembly(); 46 | var resourceName = 47 | GetType().Namespace 48 | + Variant.ToUpperInvariant() switch 49 | { 50 | "CLI" => ".AppHost.Cli.exe", 51 | "GUI" => ".AppHost.Gui.exe", 52 | _ => throw new InvalidOperationException( 53 | $"Unknown bootstrapper variant '{Variant}'." 54 | ), 55 | }; 56 | 57 | // Executable file 58 | assembly.ExtractManifestResource(resourceName, AppHostFilePath); 59 | 60 | Log.LogMessage("Extracted apphost to '{0}'.", AppHostFilePath); 61 | 62 | // Config file 63 | assembly.ExtractManifestResource(resourceName + ".config", AppHostFilePath + ".config"); 64 | 65 | Log.LogMessage("Extracted apphost config to '{0}'.", AppHostFilePath + ".config"); 66 | } 67 | 68 | private void InjectConfiguration() 69 | { 70 | Log.LogMessage("Injecting configuration..."); 71 | 72 | var configuration = $""" 73 | TargetFileName={TargetFileName} 74 | IsPromptRequired={IsPromptRequired} 75 | """; 76 | 77 | using var assembly = AssemblyDefinition.ReadAssembly( 78 | AppHostFilePath, 79 | new ReaderParameters { ReadWrite = true } 80 | ); 81 | 82 | assembly.MainModule.Resources.RemoveAll(r => 83 | string.Equals(r.Name, "BootstrapperConfiguration", StringComparison.OrdinalIgnoreCase) 84 | ); 85 | 86 | assembly.MainModule.Resources.Add( 87 | new EmbeddedResource( 88 | "BootstrapperConfiguration", 89 | ManifestResourceAttributes.Public, 90 | Encoding.UTF8.GetBytes(configuration) 91 | ) 92 | ); 93 | 94 | assembly.Write(); 95 | 96 | Log.LogMessage("Injected configuration into '{0}'.", AppHostFileName); 97 | } 98 | 99 | private void InjectResources() 100 | { 101 | Log.LogMessage("Injecting resources..."); 102 | 103 | var sourcePortableExecutable = new PortableExecutable(TargetFilePath); 104 | var targetPortableExecutable = new PortableExecutable(AppHostFilePath); 105 | 106 | targetPortableExecutable.ClearResources(); 107 | 108 | // Copy resources 109 | foreach (var identifier in sourcePortableExecutable.GetResourceIdentifiers()) 110 | { 111 | targetPortableExecutable.SetResource( 112 | identifier, 113 | sourcePortableExecutable.GetResource(identifier).Data 114 | ); 115 | } 116 | 117 | // Modify the version info resource 118 | targetPortableExecutable.SetVersionInfo(v => 119 | v.SetFileType(FileType.Application) 120 | .SetAttribute(VersionAttributeName.InternalName, AppHostFileName) 121 | .SetAttribute(VersionAttributeName.OriginalFilename, AppHostFileName) 122 | .SetAttribute( 123 | "AppHost", 124 | $".NET Runtime Bootstrapper v{Version.ToString(3)} ({Variant})" 125 | ) 126 | ); 127 | 128 | Log.LogMessage("Injected resources into '{0}'.", AppHostFileName); 129 | } 130 | 131 | public override bool Execute() 132 | { 133 | Log.LogMessage("Version: '{0}'.", Version); 134 | Log.LogMessage("Runtime identifier: '{0}'.", RuntimeIdentifier); 135 | Log.LogMessage("Variant: '{0}'.", Variant); 136 | Log.LogMessage("Prompt required: '{0}'.", IsPromptRequired); 137 | Log.LogMessage("Target: '{0}'.", TargetFilePath); 138 | 139 | // Currently only Windows is supported 140 | if (!IsWindowsTarget) 141 | { 142 | Log.LogMessage("Target platform is not Windows. Boostrapper will not be created."); 143 | return true; 144 | } 145 | 146 | ExtractAppHost(); 147 | InjectConfiguration(); 148 | InjectResources(); 149 | 150 | Log.LogMessage("Bootstrapper successfully created."); 151 | return true; 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /DotnetRuntimeBootstrapper/DotnetRuntimeBootstrapper.Local.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | $(MSBuildThisFileDirectory)/bin/$(Configuration)/netstandard2.0/DotnetRuntimeBootstrapper.dll 8 | $(MSBuildThisFileDirectory)/bin/$(Configuration)/net472/DotnetRuntimeBootstrapper.dll 9 | 10 | 11 | -------------------------------------------------------------------------------- /DotnetRuntimeBootstrapper/DotnetRuntimeBootstrapper.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0;net472 5 | true 6 | true 7 | true 8 | 9 | 10 | 11 | $(Company) 12 | .NET runtime bootstrapper for Windows applications 13 | windows desktop dotnet runtime install bootstrapper 14 | https://github.com/Tyrrrz/DotnetRuntimeBootstrapper 15 | https://github.com/Tyrrrz/DotnetRuntimeBootstrapper/releases 16 | favicon.png 17 | MIT 18 | true 19 | tasks 20 | true 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | TargetFramework 42 | true 43 | false 44 | false 45 | false 46 | 47 | 48 | TargetFramework 49 | true 50 | false 51 | false 52 | false 53 | 54 | 55 | 56 | 57 | 58 | 59 | AppHost.Cli.exe 60 | Never 61 | false 62 | 63 | 64 | AppHost.Cli.exe.config 65 | Never 66 | false 67 | 68 | 69 | AppHost.Gui.exe 70 | Never 71 | false 72 | 73 | 74 | AppHost.Gui.exe.config 75 | Never 76 | false 77 | 78 | 79 | 80 | 81 | 82 | 83 | $(TargetsForTfmSpecificBuildOutput);CopyProjectReferencesToPackage 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | $(GetTargetPathDependsOn);GetDependencyTargetPaths 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /DotnetRuntimeBootstrapper/DotnetRuntimeBootstrapper.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | false 5 | 6 | 7 | 8 | $(MSBuildThisFileDirectory)/../tasks/netstandard2.0/DotnetRuntimeBootstrapper.dll 9 | $(MSBuildThisFileDirectory)/../tasks/net472/DotnetRuntimeBootstrapper.dll 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /DotnetRuntimeBootstrapper/DotnetRuntimeBootstrapper.targets: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 8 | 9 | CLI 10 | GUI 11 | GUI 12 | true 13 | 14 | 15 | 20 | 21 | 22 | 23 | 26 | 27 | CLI 28 | GUI 29 | GUI 30 | true 31 | 32 | 33 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /DotnetRuntimeBootstrapper/Utils/Extensions/AssemblyExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Reflection; 3 | using System.Resources; 4 | 5 | namespace DotnetRuntimeBootstrapper.Utils.Extensions; 6 | 7 | internal static class AssemblyExtensions 8 | { 9 | public static void ExtractManifestResource( 10 | this Assembly assembly, 11 | string resourceName, 12 | string filePath 13 | ) 14 | { 15 | var resourceStream = 16 | assembly.GetManifestResourceStream(resourceName) 17 | ?? throw new MissingManifestResourceException( 18 | $"Failed to find resource '{resourceName}'." 19 | ); 20 | 21 | using var fileStream = File.Create(filePath); 22 | resourceStream.CopyTo(fileStream); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /DotnetRuntimeBootstrapper/Utils/Extensions/CollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace DotnetRuntimeBootstrapper.Utils.Extensions; 6 | 7 | internal static class CollectionExtensions 8 | { 9 | public static void RemoveAll(this ICollection source, Func predicate) 10 | { 11 | foreach (var i in source.ToArray()) 12 | { 13 | if (predicate(i)) 14 | source.Remove(i); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /License.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-2024 Oleksii Holub 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. -------------------------------------------------------------------------------- /NuGet.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # .NET Runtime Bootstrapper 2 | 3 | [![Status](https://img.shields.io/badge/status-maintenance-ffd700.svg)](https://github.com/Tyrrrz/.github/blob/master/docs/project-status.md) 4 | [![Made in Ukraine](https://img.shields.io/badge/made_in-ukraine-ffd700.svg?labelColor=0057b7)](https://tyrrrz.me/ukraine) 5 | [![Build](https://img.shields.io/github/actions/workflow/status/Tyrrrz/DotnetRuntimeBootstrapper/main.yml?branch=master)](https://github.com/Tyrrrz/DotnetRuntimeBootstrapper/actions) 6 | [![Version](https://img.shields.io/nuget/v/DotnetRuntimeBootstrapper.svg)](https://nuget.org/packages/DotnetRuntimeBootstrapper) 7 | [![Downloads](https://img.shields.io/nuget/dt/DotnetRuntimeBootstrapper.svg)](https://nuget.org/packages/DotnetRuntimeBootstrapper) 8 | [![Discord](https://img.shields.io/discord/869237470565392384?label=discord)](https://discord.gg/2SUWKFnHSm) 9 | [![Fuck Russia](https://img.shields.io/badge/fuck-russia-e4181c.svg?labelColor=000000)](https://twitter.com/tyrrrz/status/1495972128977571848) 10 | 11 | 12 | 13 | 14 | 15 |
Development of this project is entirely funded by the community. Consider donating to support!
16 | 17 |

18 | Icon 19 |

20 | 21 | **.NET Runtime Bootstrapper** is an MSBuild plugin that replaces the default application host `exe` file — generated for Windows executables during the build process — with a fully featured bootstrapper that can automatically download and install the .NET runtime and other missing components required by your application. 22 | 23 | ## Terms of use[[?]](https://github.com/Tyrrrz/.github/blob/master/docs/why-so-political.md) 24 | 25 | By using this project or its source code, for any purpose and in any shape or form, you grant your **implicit agreement** to all the following statements: 26 | 27 | - You **condemn Russia and its military aggression against Ukraine** 28 | - You **recognize that Russia is an occupant that unlawfully invaded a sovereign state** 29 | - You **support Ukraine's territorial integrity, including its claims over temporarily occupied territories of Crimea and Donbas** 30 | - You **reject false narratives perpetuated by Russian state propaganda** 31 | 32 | To learn more about the war and how you can help, [click here](https://tyrrrz.me/ukraine). Glory to Ukraine! 🇺🇦 33 | 34 | ## Install 35 | 36 | - 📦 [NuGet](https://nuget.org/packages/DotnetRuntimeBootstrapper): `dotnet add package DotnetRuntimeBootstrapper` 37 | 38 | ## Why? 39 | 40 | Currently, .NET offers two main ways of [distributing applications](https://docs.microsoft.com/en-us/dotnet/core/deploying): **framework-dependent** deployment and **self-contained** deployment. 41 | Both of them come with a set of obvious and somewhat less obvious drawbacks. 42 | 43 | - **Framework-dependent** deployment: 44 | - Requires the user to have the correct .NET runtime installed on their machine. Not only will many users inevitably miss or ignore this requirement, the task of installing the _correct_ .NET runtime can be very challenging for non-technical individuals. Depending on their machine and the specifics of your application, they will need to carefully examine the [download page](https://dotnet.microsoft.com/download/dotnet/8.0/runtime) to find the installer for the right version, framework (i.e. base, desktop, or aspnet), CPU architecture, and operating system. 45 | - Comes with an application host that is _not platform-agnostic_. Even though the application itself (the `dll` file) is portable in the sense that it can run on any platform where the target runtime is supported, the application host (the `exe` file) is a native executable built for a specific platform (by default, the same platform as the dev machine). This means that if the application was built on Windows x64, a user running on Windows x86 will not be able to launch the application through the `exe` file, even if they have the correct runtime installed (`dotnet myapp.dll` will still work, however). 46 | - **Self-contained** deployment: 47 | - While eliminating the need for installing the correct runtime, this method comes at a significant file size overhead. A very basic WinForms application, for example, starts at around 100 MB in size. This can be very cumbersome when doing auto-updates, but also seems quite wasteful if you consider that the user may end up with multiple .NET applications each bringing their own runtime. 48 | - Targets a specific platform, which means that you have to provide separate binaries for each operating system and processor architecture that you intend to support. Additionally, it can also create confusion among non-technical users, who may have a hard time figuring out which of the release binaries they need to download. 49 | - Snapshots a specific version of the runtime when it's produced. This means that your application won't be able to benefit from newer releases of the runtime — which may potentially contain performance or security improvements — unless you deploy a new version of the application. 50 | - Is, in fact, _not completely self-contained_. Depending on the user's machine, they might still need to install the Visual C++ runtime or certain Windows updates, neither of which are packaged with the application. Although this is only required for older operating systems, it may still affect a significant portion of your user base. 51 | 52 | **.NET Runtime Bootstrapper** seeks to solve all the above problems by providing an alternative, third deployment option — **bootstrapped** deployment. 53 | 54 | - **Bootstrapped** deployment: 55 | - Takes care of installing the target .NET runtime automatically. All the user has to do is accept the prompt and the bootstrapper will download and install the correct version of the runtime on its own. 56 | - Can also automatically install the Visual C++ runtime and missing Windows updates, when running on older operating systems. This means that users who are still using Windows 7 will have just as seamless experience as those running on Windows 11. 57 | - Does not impose any file size overhead as it does not package additional files. Missing prerequisites are downloaded on-demand. 58 | - Allows your application to benefit from newer releases of the runtime that the user might install in the future. When deploying your application, you are only tying it to a _minimum_ .NET version within the same major. 59 | - Is _truly portable_ because the provided application host is a platform-agnostic .NET Framework 3.5 executable that works out-of-the-box on all environments starting with Windows 7. This means that you only need to share a single distribution of your application, without worrying about different CPU architectures or other details. 60 | 61 | ## Features 62 | 63 | - Executes the target assembly in-process using a custom runtime host 64 | - Provides a GUI-based or a CLI-based host, depending on the application 65 | - Detects and installs missing dependencies: 66 | - Required version of the .NET runtime 67 | - Required Visual C++ binaries 68 | - Required Windows updates 69 | - Works out-of-the-box on Windows 7 and higher 70 | - Supports all platforms in a single executable 71 | - Integrates seamlessly inside the build process 72 | - Retains native resources, such as version info, manifest, and icons 73 | - Imposes no overhead in file size or performance 74 | 75 | ## Video 76 | 77 | https://user-images.githubusercontent.com/1935960/123711355-346ed380-d825-11eb-982f-6272a9e55ebd.mp4 78 | 79 | ## Usage 80 | 81 | To add **.NET Runtime Bootstrapper** to your project, simply install the corresponding [NuGet package](https://nuget.org/packages/DotnetRuntimeBootstrapper). 82 | MSBuild will automatically pick up the `props` and `targets` files provided by the package and integrate them inside the build process. 83 | After that, no further configuration is required. 84 | 85 | ### Publishing 86 | 87 | In order to create a sharable distribution of your application, run `dotnet publish` as you normally would. 88 | This should produce the following files in the output directory: 89 | 90 | ```txt 91 | MyApp.exe <-- bootstrapper's application host 92 | MyApp.exe.config <-- assembly config required by the application host 93 | MyApp.runtimeconfig.json <-- runtime config required by the application host 94 | MyApp.dll <-- main assembly of your application 95 | MyApp.pdb 96 | MyApp.deps.json 97 | ... other application dependencies ... 98 | ``` 99 | 100 | Make sure to include all highlighted files in your application distribution. 101 | 102 | > **Warning**: 103 | > Single-file deployment (`/p:PublishSingleFile=true`) is not supported by the bootstrapper. 104 | 105 | ### Application host 106 | 107 | The client-facing side of **.NET Runtime Bootstrapper** is implemented as a [custom .NET runtime host](https://docs.microsoft.com/en-us/dotnet/core/tutorials/netcore-hosting). 108 | It's generated during the build process by injecting project-specific instructions into a special pre-compiled executable provided by the package. 109 | Internally, the host executable is a managed .NET Framework v3.5 assembly, which allows it to run out-of-the-box on all platforms starting with Windows 7. 110 | 111 | When the user executes the application using the bootstrapper, it goes through the following steps: 112 | 113 | ```mermaid 114 | flowchart 115 | 1[Locate an existing .NET installation] --> 1a(Found?) 116 | 1a -- Yes --> 2 117 | 1a -- No --> 3 118 | 119 | 2[Run the app using latest hostfxr.dll] --> 2a(Successful?) 120 | 2a -- Yes --> 2b[Wait until exit] 121 | 2a -- No --> 3 122 | 123 | 3[Resolve target runtime from runtimeconfig.json] --> 124 | 4[Identify missing prerequisites] --> 125 | 5[Prompt the user to install them] --> 126 | 6[Download and install] --> 6a(Reboot required?) 127 | 6a -- Yes --> 6b[Prompt the user to reboot] --> 6c[Reboot] --> 1 128 | 6a -- No --> 1 129 | ``` 130 | 131 | ### Application resources 132 | 133 | When the bootstrapper is created, the build task copies all native resources from the target assembly into the application host. 134 | This includes: 135 | 136 | - Application manifest (resource type: `24`). Configured by the `` project property. 137 | - Application icon (resource types: `3` and `14`). Configured by the `` project property. 138 | - Version info (resource type: `16`). Contains values configured by ``, ``, ``, ``, and other similar project properties. 139 | 140 | Additionally, version info resource is further modified to contain the following attributes: 141 | 142 | - `InternalName` set to the application host's file name. 143 | - `OriginalName` set to the application host's file name. 144 | - `AppHost` set to `.NET Runtime Bootstrapper vX.Y.Z (VARIANT)` where `X.Y.Z` is the version of the bootstrapper and `VARIANT` is either `CLI` or `GUI`. 145 | 146 | ### Customizing behavior 147 | 148 | #### Generate bootstrapper on build 149 | 150 | By default, bootstrapper is only created when publishing the project (i.e. when running `dotnet publish`). 151 | If you want to also have it created on regular builds as well, set the `` project property to `true`: 152 | 153 | ```xml 154 | 155 | 156 | 157 | WinExe 158 | net8.0-windows 159 | 160 | 161 | 162 | true 163 | 164 | 165 | 166 | 167 | 168 | ``` 169 | 170 | > **Warning**: 171 | > Bootstrapper's application host does not support debugging. 172 | > In order to retain debugging capabilities of your application during local development, keep `` set to `false` (default). 173 | 174 | #### Override bootstrapper variant 175 | 176 | Depending on your application type (i.e. the value of the `` project property), the build process will generate either a CLI-based or a GUI-based bootstrapper. 177 | You can override the default behavior and specify the preferred variant explicitly using the `` project property: 178 | 179 | ```xml 180 | 181 | 182 | 183 | WinExe 184 | net8.0-windows 185 | 186 | 187 | 188 | GUI 189 | 190 | 191 | 192 | 193 | 194 | ``` 195 | 196 | #### Override runtime version 197 | 198 | **DotnetRuntimeBootstrapper** relies on the autogenerated `runtimeconfig.json` file to determine the version of the runtime required by your application. 199 | You can override the default value (which is inferred from the `` project property) by using the `` project property: 200 | 201 | ```xml 202 | 203 | 204 | 205 | WinExe 206 | net8.0-windows 207 | 208 | 209 | 210 | 8.0.1 211 | 212 | 213 | 214 | 215 | 216 | ``` 217 | 218 | #### Disable confirmation prompt 219 | 220 | By default, the bootstrapper will prompt the user to confirm the installation of missing prerequisites. 221 | You can disable this prompt by setting the `` project property to `false`: 222 | 223 | ```xml 224 | 225 | 226 | 227 | WinExe 228 | net8.0-windows 229 | 230 | 231 | 232 | false 233 | 234 | 235 | 236 | 237 | 238 | ``` 239 | 240 | ### Troubleshooting 241 | 242 | #### Build task logs 243 | 244 | If the build process does not seem to generate the bootstrapper correctly, you may be able to get more information by running the command with higher verbosity. 245 | For example, running `dotnet publish --verbosity normal` should produce an output that includes the following section: 246 | 247 | ```txt 248 | CreateBootstrapperAfterPublish: 249 | Bootstrapper target: 'f:\Projects\Softdev\DotnetRuntimeBootstrapper\DotnetRuntimeBootstrapper.Demo.Gui\bin\Debug\net6.0-windows\DotnetRuntimeBootstrapper.Demo.dll'. 250 | Bootstrapper variant: 'GUI'. 251 | Extracting apphost... 252 | Extracted apphost to 'f:\Projects\Softdev\DotnetRuntimeBootstrapper\DotnetRuntimeBootstrapper.Demo.Gui\bin\Debug\net6.0-windows\DotnetRuntimeBootstrapper.Demo.Gui.exe'. 253 | Extracted apphost config to 'f:\Projects\Softdev\DotnetRuntimeBootstrapper\DotnetRuntimeBootstrapper.Demo.Gui\bin\Debug\net6.0-windows\DotnetRuntimeBootstrapper.Demo.Gui.exe.config'. 254 | Injecting target binding... 255 | Injected target binding to 'DotnetRuntimeBootstrapper.Demo.Gui.exe'. 256 | Injecting manifest... 257 | Injected manifest to 'DotnetRuntimeBootstrapper.Demo.Gui.exe'. 258 | Injecting icon... 259 | Injected icon to 'DotnetRuntimeBootstrapper.Demo.Gui.exe'. 260 | Injecting version info... 261 | Injected version info to 'DotnetRuntimeBootstrapper.Demo.Gui.exe'. 262 | Bootstrapper created successfully. 263 | ``` 264 | 265 | #### Application host logs 266 | 267 | In the event of a fatal error, bootstrapper will produce an error dump (in addition to showing a message to the user). 268 | It can be found in the Windows event log under **Windows Logs** → **Application** with event ID `1023` and source `.NET Runtime`. 269 | The dump has the following format: 270 | 271 | ```txt 272 | Description: Bootstrapper for a .NET application has failed. 273 | Application: DotnetRuntimeBootstrapper.Demo.Gui.exe 274 | Path: F:\Projects\Softdev\DotnetRuntimeBootstrapper\DotnetRuntimeBootstrapper.Demo.Gui\bin\Debug\net6.0-windows\DotnetRuntimeBootstrapper.Demo.Gui.exe 275 | AppHost: .NET Runtime Bootstrapper v2.3.0 276 | Message: System.Exception: Test failure 277 | at DotnetRuntimeBootstrapper.AppHost.Core.ApplicationShellBase.Run(String[] args) 278 | at DotnetRuntimeBootstrapper.AppHost.Gui.Program.Main(String[] args) 279 | ``` 280 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tyrrrz/DotnetRuntimeBootstrapper/f1962f93f258a3f4597cabcb5d799bb1d22219ea/favicon.ico -------------------------------------------------------------------------------- /favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tyrrrz/DotnetRuntimeBootstrapper/f1962f93f258a3f4597cabcb5d799bb1d22219ea/favicon.png --------------------------------------------------------------------------------