diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 5e731e4..1ef9297 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -2,6 +2,8 @@ # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net name: .NET +permissions: + contents: read on: push: diff --git a/.gitignore b/.gitignore index fab1644..89dd7af 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,420 @@ -bin/ -inno_setup/ -obj/ -Properties/ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo *.user -.* -build +*.userosscache +*.sln.docstates +build/ +build-installer/ +scripts/*.iss + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +[Aa][Rr][Mm]64[Ee][Cc]/ +bld/ +[Oo]bj/ +[Oo]ut/ +[Ll]og/ +[Ll]ogs/ + +# Build results on 'Bin' directories +**/[Bb]in/* +# Uncomment if you have tasks that rely on *.refresh files to move binaries +# (https://github.com/github/gitignore/pull/3736) +#!**/[Bb]in/*.refresh + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* +*.trx + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Approval Tests result files +*.received.* + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.idb +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +# but not Directory.Build.rsp, as it configures directory-level build defaults +!Directory.Build.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +**/.paket/paket.exe +paket-files/ + +# FAKE - F# Make +**/.fake/ + +# CodeRush personal settings +**/.cr/personal + +# Python Tools for Visual Studio (PTVS) +**/__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +#tools/** +#!tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog +MSBuild_Logs/ + +# AWS SAM Build and Temporary Artifacts folder +.aws-sam + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +**/.mfractor/ + +# Local History for Visual Studio +**/.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +**/.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp diff --git a/ConsoleApp/ConsoleApp.csproj b/ConsoleApp/ConsoleApp.csproj index 0bea368..da2b86d 100644 --- a/ConsoleApp/ConsoleApp.csproj +++ b/ConsoleApp/ConsoleApp.csproj @@ -3,7 +3,7 @@ wcit Exe - net9.0-windows + net10.0-windows enable x64 true @@ -12,7 +12,7 @@ none latest-recommended ConsoleApp.Program - 0.0.6.3 + 0.0.7.0 true app.manifest diff --git a/ConsoleApp/src/ArgumentParser.cs b/ConsoleApp/src/ArgumentParser.cs index 68f5c51..7deb9bb 100644 --- a/ConsoleApp/src/ArgumentParser.cs +++ b/ConsoleApp/src/ArgumentParser.cs @@ -58,12 +58,18 @@ internal static void ParseArgs(ref Parameters parameters, string[] args) case "/imagefilepath": parameters.ImageFilePath = args[Array.IndexOf(args, arg) + 1].ToLowerInvariant(); continue; - case "/installextradrivers": - parameters.InstallExtraDrivers = true; + case "/additionaldriversdrive": + parameters.AdditionalDriversDrive = args[Array.IndexOf(args, arg) + 1].ToLowerInvariant(); continue; case "/firmwaretype": parameters.FirmwareType = args[Array.IndexOf(args, arg) + 1].ToUpperInvariant(); continue; + default: + if (arg.StartsWith("/", StringComparison.CurrentCulture) || !string.IsNullOrWhiteSpace(arg)) + { + throw new ArgumentException($"Unknown argument: {arg}"); + } + break; } } #if DEBUG @@ -74,7 +80,7 @@ internal static void ParseArgs(ref Parameters parameters, string[] args) Console.WriteLine($" Source Drive: {parameters.SourceDrive}"); Console.WriteLine($" Image Index: {parameters.ImageIndex}"); Console.WriteLine($" Image File Path: {parameters.ImageFilePath}"); - Console.WriteLine($" Install Extra Drivers: {parameters.InstallExtraDrivers}"); + Console.WriteLine($" Additional Drivers Drive: {parameters.AdditionalDriversDrive}"); Console.WriteLine($" Firmware Type: {parameters.FirmwareType}"); Console.WriteLine("Press any key to continue..."); Console.ReadKey(); diff --git a/README.md b/README.md index a8db7a2..e895e92 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,58 @@ -# wcit +# wcit – Windows CLI Installer Tool -## What is this? -Windows CLI Installer Tool is a program which deploys Windows onto any storage device within an existing Windows installation eliminating the need to reboot to install the OS. By running multiple instances of this tool it is possible to install Windows several times at once targeting as many devices as instances there are. +## 📌 What is this? +**Windows CLI Installer Tool** is a C# program that deploys Windows onto any storage device **from within an existing Windows installation**, eliminating the need to reboot into a separate installer. -## Getting started -Download the latest release from `Releases` at the right side. +By running multiple instances, you can install Windows on several devices at once — as many as you have instances running. This can be useful for: +- Rapid OS deployment in IT environments +- Preparing multiple drives for testing or distribution +- Automating repetitive installation tasks + +--- + +## 🚀 Getting Started + +### Download Prebuilt Release +If you just want to use the tool, grab the latest release from the **[Releases](../../releases)** section on the right-hand side of this page. + +--- + +## 🛠 Building from Source + +### Prerequisites +- **Windows 10/11** +- **.NET SDK** (version 8.0 or later — replace with your actual target) +- **Visual Studio** (Community Edition or higher) with `.NET desktop development` workload +- *(Optional)* [Inno Setup](https://jrsoftware.org/isinfo.php) if you want to compile the installer + +--- + +### Build Scripts Overview + +All build scripts are in the root folder. Run them from **Command Prompt** or **PowerShell**. + +| Script | What it does | +|--------|--------------| +| `build.bat` | Builds the project in Release mode | +| `build-clean.bat` | Cleans previous build artifacts, then builds | +| `publish.bat` | Publishes the project (self-contained build) | +| `publish-cleanup.bat` | Cleans, then publishes the project | +| `compile-installer.bat` | Compiles without publishing | +| `compile-installer-cleanup.bat` | Cleans, then compiles without publishing | +| `cleanup.bat` | Removes build artifacts | +| `patch-installer-script.ps1` | Patches the installer script before compiling | +| `download-installer-script.ps1` | Helper script for downloading the installer script | + +--- + +### Example Build Commands +> **Note**: These commands must be ran at the root directory of the repository. + +- To build and cleanup: +```powershell +.\scripts\build-clean.bat +``` +- To compile the installer: +```powershell +.\scripts\publish-cleanup.bat +``` diff --git a/WindowsInstallerLib/WindowsInstallerLib.csproj b/WindowsInstallerLib/WindowsInstallerLib.csproj index 0bfcb28..3969d30 100644 --- a/WindowsInstallerLib/WindowsInstallerLib.csproj +++ b/WindowsInstallerLib/WindowsInstallerLib.csproj @@ -1,14 +1,14 @@  - net9.0-windows + net10.0-windows enable x64 True none $(AssemblyName) latest - 1.1.2.2 + 1.1.4.0 x64 true @@ -27,8 +27,8 @@ - - + + diff --git a/WindowsInstallerLib/src/DeployManager.cs b/WindowsInstallerLib/src/DeployManager.cs index 684f09e..1b9e272 100644 --- a/WindowsInstallerLib/src/DeployManager.cs +++ b/WindowsInstallerLib/src/DeployManager.cs @@ -1,64 +1,163 @@ +using Microsoft.Dism; using System; using System.IO; using System.Runtime.Versioning; -using Microsoft.Dism; namespace WindowsInstallerLib { /// - /// Manages the deployment of Windows to a new drive. + /// Provides methods for managing the deployment of Windows images, including adding drivers, applying images, + /// retrieving image information, and installing the bootloader. /// + /// This class is designed to work with Windows Deployment Image Servicing and Management (DISM) + /// APIs and requires administrative privileges for most operations. It is supported only on Windows + /// platforms. [SupportedOSPlatform("windows")] internal static class DeployManager { /// - /// Adds drivers to the Windows image. + /// Installs a driver to the specified offline Windows image. /// - /// - /// - /// - /// - internal static void AddDrivers(ref Parameters parameters, string DriversSource) + /// This method initializes the DISM API, opens an offline session for the specified + /// destination drive, and adds the provided drivers. If is an array, all + /// drivers in the array are added recursively. Otherwise, a single driver is added. The DISM API is properly + /// shut down after the operation completes, even if an exception occurs. + /// A reference to a object containing details about the image file path and + /// destination drive. The and + /// properties must not be null, empty, or whitespace. + /// The source path of the driver or drivers to be added. This can be a single driver file path or an array of + /// driver paths. The value must not be null, empty, or whitespace. + /// Thrown if the directory specified in does not exist. + /// Thrown if the current process does not have administrative privileges required to initialize the DISM API. + internal static void AddDriver(ref Parameters parameters, + string DriverSource, + bool ForceUnsigned = false) { ArgumentException.ThrowIfNullOrWhiteSpace(parameters.ImageFilePath, nameof(parameters.ImageFilePath)); - ArgumentException.ThrowIfNullOrWhiteSpace(DriversSource, nameof(DriversSource)); + ArgumentException.ThrowIfNullOrWhiteSpace(DriverSource, nameof(DriverSource)); ArgumentException.ThrowIfNullOrWhiteSpace(parameters.DestinationDrive, nameof(parameters.DestinationDrive)); + if (!PrivilegesManager.IsAdmin()) + { + throw new UnauthorizedAccessException("You do not have enough privileges to initialize the DISM API."); + } + if (!Directory.Exists(parameters.DestinationDrive)) { throw new DirectoryNotFoundException($"Could not find the directory: {parameters.DestinationDrive}"); } + DismSession? session = null; + + try + { + DismApi.InitializeEx(DismLogLevel.LogErrorsWarnings); + } + catch (DismException) + { + throw; + } + catch (Exception) + { + throw; + } + + try + { + session = DismApi.OpenOfflineSession(parameters.DestinationDrive); + DismApi.AddDriver(session, DriverSource, ForceUnsigned); + } + catch (DismRebootRequiredException) + { + throw; + } + catch (DismException) + { + throw; + } + finally + { + session?.Close(); + DismApi.Shutdown(); + } + } + + /// + /// Installs multiple drivers at once to the specified offline Windows image. + /// + /// This method initializes the DISM API, opens an offline session for the specified + /// destination drive, and adds the provided drivers. If is an array, all + /// drivers in the array are added recursively. Otherwise, a single driver is added. The DISM API is properly + /// shut down after the operation completes, even if an exception occurs. + /// A reference to a object containing details about the image file path and + /// destination drive. The and + /// properties must not be null, empty, or whitespace. + /// The source path of the driver or drivers to be added. This can be a single driver file path or an array of + /// driver paths. The value must not be null, empty, or whitespace. + /// Thrown if the directory specified in does not exist. + /// Thrown if the current process does not have administrative privileges required to initialize the DISM API. + internal static void AddDrivers(ref Parameters parameters, + string DriversSource, + bool ForceUnsigned = false, + bool Recursive = false) + { + ArgumentException.ThrowIfNullOrWhiteSpace(parameters.ImageFilePath, nameof(parameters.ImageFilePath)); + ArgumentException.ThrowIfNullOrWhiteSpace(DriversSource, nameof(DriversSource)); + ArgumentException.ThrowIfNullOrWhiteSpace(parameters.DestinationDrive, nameof(parameters.DestinationDrive)); + if (!PrivilegesManager.IsAdmin()) { throw new UnauthorizedAccessException("You do not have enough privileges to initialize the DISM API."); } + if (!Directory.Exists(parameters.DestinationDrive)) + { + throw new DirectoryNotFoundException($"Could not find the directory: {parameters.DestinationDrive}"); + } + + DismSession? session = null; + try { - DismApi.Initialize(DismLogLevel.LogErrorsWarningsInfo); - DismSession session = DismApi.OpenOfflineSession(parameters.DestinationDrive); + DismApi.InitializeEx(DismLogLevel.LogErrorsWarnings); + } + catch (DismException ex) + { + Console.Write($"An error occured: {ex}"); + } + catch (Exception) + { + throw; + } - if (DriversSource.GetType().IsArray) - { - DismApi.AddDriversEx(session, DriversSource, forceUnsigned: false, recursive: true); - } - else - { - DismApi.AddDriver(session, DriversSource, forceUnsigned: false); - } + try + { + session = DismApi.OpenOfflineSession(parameters.DestinationDrive); + DismApi.AddDriversEx(session, DriversSource, ForceUnsigned, Recursive); + } + catch (DirectoryNotFoundException) + { + throw; } finally { + session?.Close(); DismApi.Shutdown(); } } /// - /// Deploys an image of Windows to the specified . - /// What gets installed is specified by and the . + /// Applies a Windows image to the specified destination drive. /// - /// + /// This method uses the Deployment Image Servicing and Management (DISM) tool to apply + /// the specified image. Ensure that the destination drive does not already contain a Windows deployment, as the + /// method will not overwrite an existing installation. Administrative privileges are required to execute this + /// operation. + /// A object containing the necessary details for the operation, including the + /// destination drive, image file path, disk number, and image index. + /// An integer representing the exit code of the operation. Returns 1 if the destination drive already + /// contains a Windows deployment. Otherwise, returns the exit code of the underlying process. + /// Thrown if the current user does not have administrative privileges required to perform the operation. internal static int ApplyImage(ref Parameters parameters) { ArgumentException.ThrowIfNullOrWhiteSpace(parameters.DestinationDrive, nameof(parameters.DestinationDrive)); @@ -83,10 +182,15 @@ internal static int ApplyImage(ref Parameters parameters) } /// - /// Gets the image file from the source drive. + /// Determines the path to a valid image file (either "install.esd" or "install.wim") located in the "sources" + /// directory of the specified source drive. /// - /// - /// + /// This method checks for the presence of "install.esd" and "install.wim" files in the + /// "sources" directory of the drive specified by . If both files are + /// present, "install.esd" is returned. + /// A reference to a object containing the source drive and image file path. The property must specify the root directory of the source drive. + /// The full path to the image file ("install.esd" or "install.wim") if found. internal static string GetImageFile(ref Parameters parameters) { try @@ -94,20 +198,24 @@ internal static string GetImageFile(ref Parameters parameters) ArgumentException.ThrowIfNullOrWhiteSpace(nameof(parameters.SourceDrive)); ArgumentException.ThrowIfNullOrWhiteSpace(nameof(parameters.ImageFilePath)); - if (File.Exists(@$"{parameters.SourceDrive}\sources\install.esd")) + string IMAGE_FILE_ESD = Path.Join(parameters.SourceDrive, @"\sources\install.esd"); + string IMAGE_FILE_WIM = Path.Join(parameters.SourceDrive, @"\sources\install.wim"); + + bool IS_IMAGE_FILE_ESD = File.Exists(IMAGE_FILE_ESD); + bool IS_IMAGE_FILE_WIM = File.Exists(IMAGE_FILE_WIM); + + if (IS_IMAGE_FILE_ESD) { - parameters.ImageFilePath = @$"{parameters.SourceDrive}\sources\install.esd"; + return IMAGE_FILE_ESD; } - else if (File.Exists(@$"{parameters.SourceDrive}\sources\install.wim")) + else if (IS_IMAGE_FILE_WIM) { - parameters.ImageFilePath = @$"{parameters.SourceDrive}\sources\install.wim"; + return IMAGE_FILE_WIM; } else { throw new FileNotFoundException($"Could not find a valid image file at {parameters.SourceDrive}."); } - - return parameters.ImageFilePath; } catch (Exception) { @@ -116,9 +224,14 @@ internal static string GetImageFile(ref Parameters parameters) } /// - /// Gets all Windows editions available using DISM, if any. + /// Retrieves and displays information about the images contained in the specified image file. /// - /// + /// This method initializes the DISM API, retrieves image information from the specified + /// file, and outputs details about each image to the console. The method requires administrative privileges + /// to execute and will throw an exception if the caller lacks sufficient privileges. + /// A reference to a object containing the path to the image file. The property must not be null or empty. + /// Thrown if the caller does not have administrative privileges. internal static void GetImageInfo(ref Parameters parameters) { ArgumentException.ThrowIfNullOrEmpty(parameters.ImageFilePath, nameof(parameters)); @@ -170,31 +283,103 @@ internal static void GetImageInfo(ref Parameters parameters) } /// - /// Gets all Windows editions available using DISM, if any. + /// Retrieves information about the images contained in the specified image file. /// - /// - /// + /// This method initializes the DISM API to retrieve image information and ensures proper + /// shutdown of the API after the operation is complete. Ensure that the caller has sufficient privileges to + /// execute this method. + /// A reference to a object that contains the path to the image file. The property must not be null, empty, or consist only of whitespace. + /// A containing details about the images in the specified file. + /// Thrown if the is null, empty, or consists only of whitespace. + /// Thrown if the current user does not have administrative privileges required to initialize the DISM API. internal static DismImageInfoCollection GetImageInfoT(ref Parameters parameters) { + if (string.IsNullOrEmpty(parameters.ImageFilePath) || + string.IsNullOrWhiteSpace(parameters.ImageFilePath)) + { + throw new FileNotFoundException("No image file was specified.", parameters.ImageFilePath); + } + + switch (PrivilegesManager.IsAdmin()) + { + case true: + DismApi.Initialize(DismLogLevel.LogErrorsWarnings); + break; + case false: + throw new UnauthorizedAccessException("You do not have enough privileges to initialize the DISM API."); + } + try { - if (string.IsNullOrEmpty(parameters.ImageFilePath)) - { - throw new FileNotFoundException("No image file was specified.", parameters.ImageFilePath); - } + DismImageInfoCollection images = DismApi.GetImageInfo(parameters.ImageFilePath); + + return images; + } + catch (DismException) + { + throw; + } + catch (Exception) + { + throw; + } + finally + { + DismApi.Shutdown(); + } + } + /// + /// Installs additional drivers to the specified offline Windows image. + /// + /// + /// + internal static void InstallAdditionalDrivers(ref Parameters parameters) + { + DismSession? session = null; + + ArgumentException.ThrowIfNullOrWhiteSpace(parameters.AdditionalDriversDrive, nameof(parameters)); + + if (!PrivilegesManager.IsAdmin()) + { + throw new UnauthorizedAccessException("You do not have enough privileges to install additional drivers."); + } + + try + { switch (PrivilegesManager.IsAdmin()) { case true: - DismApi.Initialize(DismLogLevel.LogErrorsWarnings); + try + { + DismApi.Initialize(DismLogLevel.LogErrorsWarnings); + } + catch (DismException) + { + throw; + } + catch (Exception) + { + throw; + } break; case false: throw new UnauthorizedAccessException("You do not have enough privileges to initialize the DISM API."); } - DismImageInfoCollection images = DismApi.GetImageInfo(parameters.ImageFilePath); - - return images; + try + { + session ??= DismApi.OpenOfflineSession(parameters.DestinationDrive); + } + catch (DismException) + { + throw; + } + catch (Exception) + { + throw; + } } catch (DismException) { @@ -204,23 +389,45 @@ internal static DismImageInfoCollection GetImageInfoT(ref Parameters parameters) { throw; } + + try + { + DismApi.AddDriversEx(session, parameters.AdditionalDriversDrive, false, true); + } finally { + session?.Close(); DismApi.Shutdown(); } } /// - /// Installs the bootloader to the EFI drive of a new Windows installation. + /// Installs the bootloader to the specified EFI system partition. /// - /// - /// + /// This method requires administrative privileges to execute. Ensure that the calling + /// process has sufficient permissions. The method validates the existence of required directories and checks + /// for conflicts before proceeding with the installation. + /// A reference to a object containing the configuration for the bootloader + /// installation. The property specifies the drive containing the + /// Windows installation. The property specifies the EFI system partition + /// where the bootloader will be installed. The property specifies the + /// firmware type (e.g., "UEFI" or "BIOS"). + /// The exit code of the bootloader installation process. A value of 0 typically indicates success. + /// Thrown if the current process does not have administrative privileges required to perform the installation. internal static int InstallBootloader(ref Parameters parameters) { ArgumentException.ThrowIfNullOrWhiteSpace(parameters.DestinationDrive, nameof(parameters.DestinationDrive)); ArgumentException.ThrowIfNullOrWhiteSpace(parameters.EfiDrive, nameof(parameters.EfiDrive)); ArgumentException.ThrowIfNullOrWhiteSpace(parameters.FirmwareType, nameof(parameters.FirmwareType)); + string EFI_BOOT_PATH = Path.Join(parameters.EfiDrive, @"\EFI\Boot"); + string EFI_MICROSOFT_PATH = Path.Join(parameters.EfiDrive, @"\EFI\Microsoft"); + string WINDIR_PATH = Path.Join(parameters.DestinationDrive, @"\windows"); + + bool EFI_BOOT_EXISTS = Directory.Exists(EFI_BOOT_PATH); + bool EFI_MICROSOFT_EXISTS = Directory.Exists(EFI_MICROSOFT_PATH); + bool WINDIR_EXISTS = Directory.Exists(WINDIR_PATH); + if (!PrivilegesManager.IsAdmin()) { throw new UnauthorizedAccessException($"You do not have enough privileges to install the bootloader to {parameters.EfiDrive}"); @@ -228,21 +435,19 @@ internal static int InstallBootloader(ref Parameters parameters) try { - if (Directory.Exists(@$"{parameters.EfiDrive}\EFI\Boot") || Directory.Exists($@"{parameters.EfiDrive}\EFI\Microsoft")) + if (EFI_BOOT_EXISTS || EFI_MICROSOFT_EXISTS) { throw new IOException($"The drive letter {parameters.EfiDrive} is already in use."); } - else + + if (!WINDIR_EXISTS) { - if (!Directory.Exists(@$"{parameters.DestinationDrive}windows")) - { - throw new DirectoryNotFoundException(@$"The directory {parameters.DestinationDrive}windows does not exist!"); - } - - Console.WriteLine($"Firmware type is set to: {parameters.FirmwareType}"); - Console.WriteLine($"\n==> Installing bootloader to drive {parameters.EfiDrive} in disk {parameters.DiskNumber}"); - ProcessManager.StartCmdProcess("bcdboot", @$"{parameters.DestinationDrive}\windows /s {parameters.EfiDrive} /f {parameters.FirmwareType}"); + throw new DirectoryNotFoundException(@$"The directory {WINDIR_PATH} does not exist!"); } + + Console.WriteLine($"Firmware type is set to: {parameters.FirmwareType}"); + Console.WriteLine($"\n==> Installing bootloader to drive {parameters.EfiDrive} in disk {parameters.DiskNumber}"); + ProcessManager.StartCmdProcess("bcdboot", @$"{WINDIR_PATH} /s {parameters.EfiDrive} /f {parameters.FirmwareType}"); } catch (IOException) { @@ -252,7 +457,7 @@ internal static int InstallBootloader(ref Parameters parameters) { throw; } - + return ProcessManager.ExitCode; } } diff --git a/WindowsInstallerLib/src/DiskManager.cs b/WindowsInstallerLib/src/DiskManager.cs index 1ee4ac2..7f4780f 100644 --- a/WindowsInstallerLib/src/DiskManager.cs +++ b/WindowsInstallerLib/src/DiskManager.cs @@ -7,8 +7,11 @@ namespace WindowsInstallerLib { /// - /// Manages the disks on the system. + /// Provides functionality for managing and interacting with system disks on Windows platforms. /// + /// This class includes methods for formatting disks, listing available disks, and retrieving + /// disk information. It requires administrative privileges for certain operations, such as formatting a + /// disk. [SupportedOSPlatform("windows")] internal class DiskManager { @@ -41,8 +44,13 @@ internal static int FormatDisk(ref Parameters parameters) } /// - /// Lists all the disks on the system. + /// Lists all disk drives on the system and displays their details, including disk number, model, and device ID. /// + /// This method queries the system's disk drives using WMI (Windows Management + /// Instrumentation) and outputs the retrieved information to the console. The details include the disk number, + /// model, and device ID for each disk drive found. Note that this method is intended for internal use + /// and writes directly to the console. It does not return the retrieved data or provide a way to + /// programmatically access it. internal static void ListAll() { try @@ -64,9 +72,9 @@ internal static void ListAll() } /// - /// Lists all disk on the system using DriveInfo. + /// Retrieves an array of all available drive information on the system. /// - /// + /// An array of objects representing the drives available on the system. internal static DriveInfo[] GetDisksT() { try diff --git a/WindowsInstallerLib/src/InstallerManager.cs b/WindowsInstallerLib/src/InstallerManager.cs index 623d673..5107214 100644 --- a/WindowsInstallerLib/src/InstallerManager.cs +++ b/WindowsInstallerLib/src/InstallerManager.cs @@ -6,16 +6,19 @@ namespace WindowsInstallerLib { /// - /// Contains the parameters required for installing Windows. + /// Represents the parameters required for configuring and executing a disk imaging operation. /// - /// - /// - /// - /// - /// - /// - /// - /// + /// This structure encapsulates all the necessary information for performing a disk imaging + /// process, including source and destination drives, image file details, and additional configuration + /// options. + /// Gets or sets the destination drive where the image will be applied. + /// Gets or sets the EFI (Extensible Firmware Interface) drive used during the imaging process. + /// Gets or sets the disk number associated with the operation. + /// Gets or sets the source drive containing the data to be imaged. + /// Gets or sets the index of the image within the image file to be applied. + /// Gets or sets the file path of the image file to be used in the operation. + /// Gets or sets a value indicating whether additional drivers should be installed during the imaging process. + /// Gets or sets the firmware type of the system being imaged. [SupportedOSPlatform("windows")] public struct Parameters(string DestinationDrive, string EfiDrive, @@ -23,7 +26,7 @@ public struct Parameters(string DestinationDrive, string SourceDrive, int ImageIndex, string ImageFilePath, - bool InstallExtraDrivers, + string AdditionalDriversDrive, string FirmwareType) { public string DestinationDrive { get; set; } = DestinationDrive; @@ -32,22 +35,30 @@ public struct Parameters(string DestinationDrive, public string SourceDrive { get; set; } = SourceDrive; public int ImageIndex { get; set; } = ImageIndex; public string ImageFilePath { get; set; } = ImageFilePath; - public bool InstallExtraDrivers { get; set; } = InstallExtraDrivers; + public string AdditionalDriversDrive { get; set; } = AdditionalDriversDrive; public string FirmwareType { get; set; } = FirmwareType; } /// - /// Manages the installation of Windows. + /// Provides functionality to configure and install Windows on a specified disk. /// + /// This class is designed for use on Windows platforms and provides methods to set up the + /// environment and deploy Windows installations. It requires valid parameters to be provided for successful + /// operation. [SupportedOSPlatform("windows")] public sealed class InstallerManager { /// - /// Sets up the environment correctly for deploying Windows. + /// Configures the specified object by prompting the user for missing values. /// - /// - /// - /// + /// This method ensures that all required properties of the + /// object are populated. If any property is missing or invalid, the user is prompted to provide the necessary + /// input. The method validates user input and throws exceptions for invalid or incomplete data. + /// A reference to the object to configure. This object must not be null, and its + /// properties will be updated based on user input. + /// Thrown if the user provides invalid input, such as an improperly formatted drive letter or missing required + /// values. + /// Thrown if the firmware type cannot be determined or is invalid. public static void Configure(ref Parameters parameters) { #region DestinationDrive @@ -80,13 +91,16 @@ public static void Configure(ref Parameters parameters) ArgumentException.ThrowIfNullOrWhiteSpace(p_DestinationDrive); - if (p_DestinationDrive.StartsWith(':')) + if (p_DestinationDrive.Length != 2 || + p_DestinationDrive.Length > 2) { - throw new ArgumentException(@$"Invalid source drive {p_DestinationDrive}, it must have a colon at the end not at the beginning. For example: 'Z:'."); + throw new ArgumentException(@$"Invalid source drive {p_DestinationDrive}. Too many characters."); } - else if (!p_DestinationDrive.EndsWith(':')) + + if (p_DestinationDrive.StartsWith(':') || + !p_DestinationDrive.EndsWith(':')) { - throw new ArgumentException($"Invalid source drive {p_DestinationDrive}, it must have a colon. For example: 'Z:'."); + throw new InvalidDataException(@$"Invalid source drive {p_DestinationDrive}. A valid drive is for example: 'Z:'."); } parameters.DestinationDrive = p_DestinationDrive; @@ -269,53 +283,114 @@ public static void Configure(ref Parameters parameters) Console.WriteLine($"\nThe installer has set the firmware type to {parameters.FirmwareType}.", ConsoleColor.Yellow); break; default: - throw new InvalidDataException(nameof(parameters.FirmwareType)); + throw new InvalidDataException($"Invalid firmware type: {parameters.FirmwareType}"); } } #endregion - } - /// - /// Installs Windows on the specified disk. - /// - /// - [SupportedOSPlatform("windows")] - public static void InstallWindows(ref Parameters parameters) - { - try + #region AdditionalDriversList + if (string.IsNullOrWhiteSpace(parameters.AdditionalDriversDrive)) { - if (parameters.DiskNumber.Equals(-1)) + Console.Write("\n=> Do you want to add additional drivers to your installation?: [Y/N]: "); + string? UserWantsExtraDrivers = Console.ReadLine()?.ToLower(CultureInfo.CurrentCulture); + + if (string.IsNullOrEmpty(UserWantsExtraDrivers) || + string.IsNullOrWhiteSpace(UserWantsExtraDrivers)) { - throw new InvalidDataException("No disk number was specified, required to know where to install Windows at."); + UserWantsExtraDrivers = "no"; } - if (string.IsNullOrWhiteSpace(parameters.EfiDrive)) + switch (UserWantsExtraDrivers.ToLower()) { - throw new InvalidDataException("No EFI drive was specified, required for the bootloader installation."); - } + case "yes": + case "y": + Console.Write("\n==> Specify the directory where the drivers are located. (e.g. X:\\Drivers): "); + string? driversPath = Console.ReadLine(); + if (string.IsNullOrEmpty(driversPath) || + string.IsNullOrWhiteSpace(driversPath)) + { + throw new ArgumentException("No drivers path was specified."); + } - ArgumentException.ThrowIfNullOrWhiteSpace(parameters.DestinationDrive); - ArgumentException.ThrowIfNullOrWhiteSpace(parameters.ImageFilePath); - ArgumentOutOfRangeException.ThrowIfEqual(parameters.ImageIndex, -1); - ArgumentException.ThrowIfNullOrWhiteSpace(parameters.FirmwareType); + if (!Directory.Exists(driversPath)) + { + throw new FileNotFoundException($"The directory {driversPath} does not exist"); + } - switch (parameters.FirmwareType) - { - case "UEFI": - break; - case "BIOS": + parameters.AdditionalDriversDrive = driversPath; break; default: - throw new InvalidDataException($"Invalid firmware type: {parameters.FirmwareType}"); + return; } + } + #endregion + } + + /// + /// Installs Windows on the specified disk and configures the bootloader. + /// + /// This method performs the following steps: 1. Formats the specified disk. 2. Applies + /// the Windows image to the destination drive. 3. Installs the bootloader on the EFI drive. Each step relies + /// on the values provided in the object. Ensure all required properties are + /// correctly set before calling this method. If any step fails, the corresponding exception will propagate to + /// the caller. + /// A reference to a object containing the necessary configuration for the + /// installation. This includes the disk number, EFI drive, destination drive, image file path, image index, and + /// firmware type. + /// Thrown if the object contains invalid or missing values, such as: - Disk + /// number is -1. - EFI drive is null, empty, or whitespace. + [SupportedOSPlatform("windows")] + public static void InstallWindows(ref Parameters parameters) + { + if (parameters.DiskNumber.Equals(-1)) + { + throw new InvalidDataException("No disk number was specified, required to know where to install Windows at."); + } + if (string.IsNullOrWhiteSpace(parameters.EfiDrive)) + { + throw new InvalidDataException("No EFI drive was specified, required for the bootloader installation."); + } + + ArgumentException.ThrowIfNullOrWhiteSpace(parameters.DestinationDrive); + ArgumentException.ThrowIfNullOrWhiteSpace(parameters.ImageFilePath); + ArgumentOutOfRangeException.ThrowIfEqual(parameters.ImageIndex, -1); + ArgumentException.ThrowIfNullOrWhiteSpace(parameters.FirmwareType); + + try + { DiskManager.FormatDisk(ref parameters); + } + catch (Exception ex) + { + Console.WriteLine($"An error occurred while formatting the disk: {ex}", ConsoleColor.Red); + } + + try + { DeployManager.ApplyImage(ref parameters); + } + catch (Exception ex) + { + Console.WriteLine($"An error occurred while applying the image: {ex}", ConsoleColor.Red); + } + + try + { + DeployManager.InstallAdditionalDrivers(ref parameters); + } + catch (Exception ex) + { + Console.WriteLine($"An error occurred when installing additional drivers: {ex}", ConsoleColor.Yellow); + } + + try + { DeployManager.InstallBootloader(ref parameters); } - catch (Exception) + catch (Exception ex) { - throw; + Console.WriteLine($"An error occurred while installing the bootloader: {ex}", ConsoleColor.Red); } } } diff --git a/WindowsInstallerLib/src/PrivilegesManager.cs b/WindowsInstallerLib/src/PrivilegesManager.cs index 5d1e025..a206d8f 100644 --- a/WindowsInstallerLib/src/PrivilegesManager.cs +++ b/WindowsInstallerLib/src/PrivilegesManager.cs @@ -6,14 +6,21 @@ namespace WindowsInstallerLib { /// - /// Manages the privileges of the current user. + /// Provides utility methods for managing and checking user privileges. /// + /// This class includes methods to determine the current user's privilege level, such as whether + /// the user has administrative rights. It is designed for use on Windows platforms and may throw exceptions if + /// security or identity-related issues occur. internal static class PrivilegesManager { /// - /// Checks if the current user is an administrator. + /// Determines whether the current user has administrative privileges on a Windows platform. /// - /// + /// This method is only supported on Windows platforms. It uses the and classes to check if the current user belongs to + /// the Administrators group. + /// if the current user is a member of the Administrators group; otherwise, . [SupportedOSPlatform("windows")] internal static bool IsAdmin() { diff --git a/WindowsInstallerLib/src/ProcessManager.cs b/WindowsInstallerLib/src/ProcessManager.cs index 9f93231..f4859ce 100644 --- a/WindowsInstallerLib/src/ProcessManager.cs +++ b/WindowsInstallerLib/src/ProcessManager.cs @@ -5,12 +5,30 @@ namespace WindowsInstallerLib { /// - /// Handles the creation and management of processes. + /// Provides utility methods for managing and executing system processes. /// + /// The class includes methods for starting and managing various + /// types of processes, such as command-line tools, disk partitioning utilities, and deployment tools. It also + /// tracks the exit code of the last executed process. This class is intended for internal use and is not + /// thread-safe. internal sealed class ProcessManager { + /// + /// Gets the exit code that represents the result of the process's execution. + /// internal static int ExitCode { get; private set; } + /// + /// Starts a command-line process with the specified file name and arguments, waits for it to complete, and + /// returns its exit code. + /// + /// The method uses a non-shell execution mode ( is set to ). The caller + /// is responsible for ensuring that the specified file exists and is executable. + /// The name or path of the executable file to start. This cannot be null or empty. + /// The command-line arguments to pass to the process. This can be null or an empty string if no arguments are + /// required. + /// The exit code of the process after it has completed execution. internal static int StartCmdProcess(string fileName, string args) { try @@ -40,6 +58,24 @@ internal static int StartCmdProcess(string fileName, string args) return ExitCode; } + /// + /// Executes a DiskPart process to format and partition a specified disk. + /// + /// This method uses the DiskPart utility to perform the following operations on the + /// specified disk: Selects the specified disk. + /// Cleans the disk. Converts the disk to GPT + /// format. Creates an MSR partition. + /// Creates and formats an EFI partition with the FAT32 file system. + /// Assigns the specified drive letter to the EFI partition. + /// Creates and formats a primary partition with the NTFS file system. + /// Assigns the specified drive letter to the primary partition. + /// The method writes commands to the DiskPart process via standard input and waits for the process to + /// complete. + /// The number of the disk to be formatted and partitioned. + /// The drive letter to assign to the EFI partition. + /// The drive letter to assign to the primary partition. + /// The exit code of the DiskPart process. A value of 0 indicates success, while a non-zero value indicates + /// failure. internal static int StartDiskPartProcess(int DiskNumber, string EfiDrive, string DestinationDrive) { Process process = new(); @@ -57,8 +93,8 @@ internal static int StartDiskPartProcess(int DiskNumber, string EfiDrive, string process.StandardInput.WriteLine($"select disk {DiskNumber}"); process.StandardInput.WriteLine("clean"); process.StandardInput.WriteLine("convert gpt"); - process.StandardInput.WriteLine("create partition msr size=16"); process.StandardInput.WriteLine("create partition efi size=100"); + process.StandardInput.WriteLine("create partition msr size=16"); process.StandardInput.WriteLine("format fs=fat32 quick"); process.StandardInput.WriteLine($"assign letter {EfiDrive}"); process.StandardInput.WriteLine("create partition primary"); @@ -108,6 +144,17 @@ internal static int StartDiskPartProcess(int DiskNumber, string EfiDrive, string return ExitCode; } + /// + /// Starts a new process to execute the Deployment Image Servicing and Management (DISM) tool with the specified + /// arguments. + /// + /// This method starts the DISM tool as a separate process and waits for it to complete + /// execution. The caller is responsible for ensuring that the provided arguments are valid and appropriate for + /// the DISM tool. + /// The command-line arguments to pass to the DISM tool. This must be a valid string of arguments supported by + /// DISM. + /// The exit code returned by the DISM process. A value of 0 typically indicates success, while non-zero values + /// indicate an error or failure. internal static int StartDismProcess(string args) { Process process = new(); @@ -141,6 +188,17 @@ internal static int StartDismProcess(string args) return ExitCode; } + /// + /// Starts a process with the specified executable file and arguments, waits for it to exit, and returns the + /// process's exit code. + /// + /// The method uses a non-shell execution model ( is set to ). The caller + /// is responsible for ensuring that the specified executable file exists and is accessible. + /// The path to the executable file to start. This cannot be null or empty. + /// The command-line arguments to pass to the executable. This can be null or empty if no arguments are + /// required. + /// The exit code of the process after it has completed execution. internal static int StartProcess(string filename, string args) { Process process = new(); diff --git a/WindowsInstallerLib/src/SystemInfoManager.cs b/WindowsInstallerLib/src/SystemInfoManager.cs index 90e7401..18c2f4c 100644 --- a/WindowsInstallerLib/src/SystemInfoManager.cs +++ b/WindowsInstallerLib/src/SystemInfoManager.cs @@ -5,25 +5,35 @@ namespace WindowsInstallerLib { + /// + /// Provides methods for retrieving system firmware information and determining the firmware type. + /// + /// This class includes functionality to check if the system is using EFI (Extensible Firmware + /// Interface) firmware. It is supported only on Windows platforms. [SupportedOSPlatform("windows")] internal static partial class SystemInfoManager { /// - /// Retrieves the firmware type of the system. + /// Retrieves the firmware type of the system by querying a firmware environment variable. /// - /// - /// - /// - /// - + /// This method is a platform invocation (P/Invoke) wrapper for the native Windows API + /// function GetFirmwareEnvironmentVariableA. It allows managed code to interact with firmware + /// environment variables. + /// The name of the firmware environment variable to query. + /// The globally unique identifier (GUID) of the firmware environment variable namespace. + /// A pointer to a buffer that receives the value of the firmware environment variable. + /// The size, in bytes, of the buffer pointed to by . + /// The size of the data retrieved, in bytes, if the call succeeds; otherwise, 0 if the call fails. [LibraryImport("kernel32.dll", EntryPoint = "GetFirmwareEnvironmentVariableA", SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] [UnmanagedCallConv(CallConvs = [typeof(CallConvStdcall)])] private static partial uint GetFirmwareType(string lpName, string lpGUID, IntPtr pBuffer, uint size); /// - /// Checks if the system is using EFI firmware. + /// Determines whether the system supports EFI (Extensible Firmware Interface). /// - /// + /// This method checks the system's firmware type by invoking a low-level function and + /// analyzing the result. It relies on the last Win32 error code to determine EFI support. + /// if the system supports EFI; otherwise, . internal static bool IsEFI() { // Call the function with a dummy variable name and a dummy variable namespace (function will fail because these don't exist.) diff --git a/scripts/build-clean.bat b/scripts/build-clean.bat index d60b3d7..ae4752e 100644 --- a/scripts/build-clean.bat +++ b/scripts/build-clean.bat @@ -1,5 +1,5 @@ @ECHO off -START "WCIT CLEANUP SCRIPT" /B scripts/cleanup.bat +CALL %~dp0/cleanup.bat -dotnet build "Windows Installer.sln" --nologo --self-contained --property:OutputPath=..\build\ --configuration "Debug" +dotnet build "Windows Installer.sln" --nologo --self-contained --property:OutputPath=%~dp0..\build\ --configuration "Debug" diff --git a/scripts/build.bat b/scripts/build.bat index 2a85041..6b8be71 100644 --- a/scripts/build.bat +++ b/scripts/build.bat @@ -1,3 +1,3 @@ @ECHO off -dotnet build "Windows Installer.sln" --nologo --self-contained --property:OutputPath=..\build\ --configuration "Debug" +dotnet build "Windows Installer.sln" --nologo --self-contained --property:OutputPath=%~dp0..\build\ --configuration "Debug" diff --git a/scripts/cleanup.bat b/scripts/cleanup.bat index 4ade831..22180da 100644 --- a/scripts/cleanup.bat +++ b/scripts/cleanup.bat @@ -1,19 +1,26 @@ -@ECHO off +@ECHO OFF -IF EXIST "BUILD" ( - DEL /S /Q "BUILD" -) -IF EXIST "CONSOLEAPP\BIN" ( - DEL /S /Q "CONSOLEAPP\BIN" -) - -IF EXIST "CONSOLEAPP\OBJ" ( - DEL /S /Q "CONSOLEAPP\OBJ" -) +CALL :REMOVEDIR %~dp0..\.vs +CALL :REMOVEDIR %~dp0..\build +CALL :REMOVEDIR %~dp0..\build-installer +CALL :REMOVEDIR %~dp0..\ConsoleApp\bin +CALL :REMOVEDIR %~dp0..\ConsoleApp\obj +CALL :REMOVEDIR %~dp0..\WindowsInstallerLib\bin +CALL :REMOVEDIR %~dp0..\WindowsInstallerLib\obj +:EXITWITHERROR IF %ERRORLEVEL%==0 ( - EXIT + EXIT /B ) ELSE ( - ECHO.PROGRAM EXITED WITH CODE %ERRORLEVEL% + ECHO PROGRAM EXITED WITH CODE %ERRORLEVEL% EXIT /B %ERRORLEVEL% ) + +:REMOVEDIR + IF EXIST %* ( + ECHO REMOVING DIRECTORY: %*... + RMDIR /S /Q %* + CALL :EXITWITHERROR + ) ELSE ( + ECHO [!] DIRECTORY NOT FOUND: %* + ) diff --git a/scripts/compile-installer-cleanup.bat b/scripts/compile-installer-cleanup.bat new file mode 100644 index 0000000..791189e --- /dev/null +++ b/scripts/compile-installer-cleanup.bat @@ -0,0 +1,55 @@ +@ECHO off + +CALL %~dp0publish-cleanup.bat + +SET COMPILER="%LOCALAPPDATA%\Programs\Inno Setup 6\ISCC.exe" /Q +FOR /F "tokens=2 delims=\\" %%A IN ('whoami') DO SET USERNAME="%%A" +SET LICENSE="%~dp0..\LICENSE" +SET REPOSITORYDIR="%~dp0.." +SET OUTPUTDIR="%~dp0..\build-installer" +SET SETUPSCRIPT="%~dp0..\build-installer\wcit-setup.iss" +SET USERNAME="felgmar" +SET VERSION="1.0.1.0" + +IF NOT EXIST %LICENSE% ( + ECHO.ERROR: LICENSE FILE NOT FOUND + EXIT /B 1 +) + +IF NOT EXIST %OUTPUTDIR% ( + MKDIR %OUTPUTDIR% +) + +IF EXIST %SETUPSCRIPT% ( + DEL /Q %SETUPSCRIPT% +) + +IF NOT EXIST %SETUPSCRIPT% ( + CALL powershell.exe -ExecutionPolicy Bypass -Command "& {%~dp0download-installer-script.ps1 -OutputPath %SETUPSCRIPT%}" + CALL icacls.exe "%SETUPSCRIPT%" /grant %USERNAME%:F +) + +FOR %%F IN ("%OUTPUTDIR%\*.exe") DO ( + ECHO REMOVING FILE %%F FROM %OUTPUTDIR%... + DEL /Q "%OUTPUTDIR%\%%F" +) + +IF "%1"=="" IF NOT EXIST %SETUPSCRIPT% ( + ECHO.ERROR: NO INNO SETUP SCRIPT WAS PROVIDED + EXIT /B 1 +) + +ECHO PATCHING INNO SETUP SCRIPT... +CALL powershell.exe -ExecutionPolicy Bypass -Command "& {%~dp0patch-installer-script.ps1 -OutputPath %SETUPSCRIPT% -Define RepositoryDir -Value %REPOSITORYDIR%}" +CALL powershell.exe -ExecutionPolicy Bypass -Command "& {%~dp0patch-installer-script.ps1 -OutputPath %SETUPSCRIPT% -Define AppOutputDir -Value %OUTPUTDIR%}" +CALL powershell.exe -ExecutionPolicy Bypass -Command "& {%~dp0patch-installer-script.ps1 -OutputPath %SETUPSCRIPT% -Define UserName -Value %USERNAME%}" +CALL powershell.exe -ExecutionPolicy Bypass -Command "& {%~dp0patch-installer-script.ps1 -OutputPath %SETUPSCRIPT% -Define AppLicense -Value %LICENSE%}" +CALL powershell.exe -ExecutionPolicy Bypass -Command "& {%~dp0patch-installer-script.ps1 -OutputPath %SETUPSCRIPT% -Define AppVersion -Value %VERSION%}" + +ECHO COMPILING INSTALLER... +CALL %COMPILER% %SETUPSCRIPT% + +IF %ERRORLEVEL% NEQ 0 ( + ECHO.COMPILATION FAILED WITH CODE %ERRORLEVEL% + EXIT /B %ERRORLEVEL% +) diff --git a/scripts/compile-installer.bat b/scripts/compile-installer.bat new file mode 100644 index 0000000..3391bf9 --- /dev/null +++ b/scripts/compile-installer.bat @@ -0,0 +1,49 @@ +@ECHO off + +SET COMPILER="%LOCALAPPDATA%\Programs\Inno Setup 6\ISCC.exe" /Q +FOR /F "tokens=2 delims=\\" %%A IN ('whoami') DO SET USERNAME="%%A" +SET LICENSE="%~dp0..\LICENSE" +SET REPOSITORYDIR="%~dp0.." +SET OUTPUTDIR="%~dp0..\build-installer" +SET SETUPSCRIPT="%~dp0..\build-installer\wcit-setup.iss" +SET USERNAME="felgmar" +SET VERSION="1.0.1.0" + +IF NOT EXIST %LICENSE% ( + ECHO.ERROR: LICENSE FILE NOT FOUND + EXIT /B 1 +) + +IF NOT EXIST %OUTPUTDIR% ( + MKDIR %OUTPUTDIR% +) + +IF NOT EXIST %SETUPSCRIPT% ( + CALL powershell.exe -ExecutionPolicy Bypass -Command "& {%~dp0download-installer-script.ps1 -OutputPath %SETUPSCRIPT%}" + CALL icacls.exe "%SETUPSCRIPT%" /grant %USERNAME%:F +) + +FOR %%F IN ("%OUTPUTDIR%\*.exe") DO ( + ECHO REMOVING FILE %%F FROM %OUTPUTDIR%... + DEL /Q "%OUTPUTDIR%\%%F" +) + +IF "%1"=="" IF NOT EXIST %SETUPSCRIPT% ( + ECHO.ERROR: NO INNO SETUP SCRIPT WAS PROVIDED + EXIT /B 1 +) + +ECHO PATCHING INNO SETUP SCRIPT... +CALL powershell.exe -ExecutionPolicy Bypass -Command "& {%~dp0patch-installer-script.ps1 -OutputPath %SETUPSCRIPT% -Define RepositoryDir -Value %REPOSITORYDIR%}" +CALL powershell.exe -ExecutionPolicy Bypass -Command "& {%~dp0patch-installer-script.ps1 -OutputPath %SETUPSCRIPT% -Define AppOutputDir -Value %OUTPUTDIR%}" +CALL powershell.exe -ExecutionPolicy Bypass -Command "& {%~dp0patch-installer-script.ps1 -OutputPath %SETUPSCRIPT% -Define UserName -Value %USERNAME%}" +CALL powershell.exe -ExecutionPolicy Bypass -Command "& {%~dp0patch-installer-script.ps1 -OutputPath %SETUPSCRIPT% -Define AppLicense -Value %LICENSE%}" +CALL powershell.exe -ExecutionPolicy Bypass -Command "& {%~dp0patch-installer-script.ps1 -OutputPath %SETUPSCRIPT% -Define AppVersion -Value %VERSION%}" + +ECHO COMPILING INSTALLER... +CALL %COMPILER% %SETUPSCRIPT% + +IF %ERRORLEVEL% NEQ 0 ( + ECHO.COMPILATION FAILED WITH CODE %ERRORLEVEL% + EXIT /B %ERRORLEVEL% +) diff --git a/scripts/download-installer-script.ps1 b/scripts/download-installer-script.ps1 new file mode 100644 index 0000000..f96b634 --- /dev/null +++ b/scripts/download-installer-script.ps1 @@ -0,0 +1,16 @@ +[CMDLetBinding()] +param( + [Parameter(Mandatory=$false)] + [String]$OutputPath +) + +process { + [String]$Uri = "https://raw.githubusercontent.com/felgmar/isscripts/refs/heads/main/wcit/wcit-setup.iss" + + try { + Invoke-WebRequest -Uri $Uri -OutFile "$OutputPath" -UseBasicParsing -WarningAction Ignore -ErrorAction Stop + } + catch { + throw $_.Exception.Message + } +} diff --git a/scripts/patch-installer-script.ps1 b/scripts/patch-installer-script.ps1 new file mode 100644 index 0000000..3e42950 --- /dev/null +++ b/scripts/patch-installer-script.ps1 @@ -0,0 +1,28 @@ +[CmdletBinding()] +param ( + [Parameter(Mandatory = $true)] + [string]$OutputPath, + [Parameter(Mandatory = $true)] + [ValidateSet('AppOutputDir', 'UserName', 'AppLicense', 'RepositoryDir', 'AppVersion')] + [String]$Define, + [Parameter(Mandatory = $true)] + [String]$Value +) + +process { + try { + foreach ($Field in $Define) { + $ScriptFile = Get-Content -Path $OutputPath -Raw + [String]$Pattern = "(#define\s$Field\s+).*" + [String]$Replacement = "`$1`"$Value`"" + $Patch = $ScriptFile -replace $Pattern, $Replacement + Set-Content -Path $OutputPath -Value $Patch -Force + } + } + catch { + throw $_.Exception.Message + } + finally { + Write-Host "Updated $Define in $OutputPath to '$Value'" + } +} diff --git a/scripts/publish-cleanup.bat b/scripts/publish-cleanup.bat index f56b957..1b3d04b 100644 --- a/scripts/publish-cleanup.bat +++ b/scripts/publish-cleanup.bat @@ -1,5 +1,9 @@ @ECHO off -START "WCIT CLEANUP SCRIPT" /B scripts/cleanup.bat +CALL %~dp0cleanup.bat -dotnet publish "Windows Installer.sln" --nologo --self-contained --property:OutputPath=..\build\ --configuration "Release" +dotnet publish "Windows Installer.sln" --nologo --self-contained --property:OutputPath=%~dp0..\build\ --configuration "Release" + +MOVE /Y %~dp0..\build\publish %~dp0..\ +RMDIR /Q /S %~dp0..\build +MOVE /Y %~dp0..\publish %~dp0..\build diff --git a/scripts/publish.bat b/scripts/publish.bat index 06b6fd3..3cefdf3 100644 --- a/scripts/publish.bat +++ b/scripts/publish.bat @@ -1,3 +1,3 @@ @ECHO off -dotnet publish "Windows Installer.sln" --nologo --self-contained --property:OutputPath=..\build\ --configuration "Release" +dotnet publish "Windows Installer.sln" --nologo --self-contained --property:OutputPath=%~dp0..\build\ --configuration "Release"