diff --git a/.gitignore b/.gitignore
index 2078eb4..23b5a58 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,5 @@
/build
+/cache
/output
/win11-builder-amd64
/win11-builder-arm64
-
diff --git a/configs/autounattend-bootstrap-amd64-20260524.xml b/configs/autounattend-bootstrap-amd64-20260524.xml
new file mode 100644
index 0000000..bd3a1ce
--- /dev/null
+++ b/configs/autounattend-bootstrap-amd64-20260524.xml
@@ -0,0 +1,1224 @@
+
+
+
+
+
+
+
+ en-US
+
+ de-DE
+ en-US
+ en-US
+ en-US
+ en-US
+
+
+ true
+
+ OnError
+ true
+
+ 0
+ true
+
+
+ 1
+ EFI
+ 300
+
+
+ 2
+ MSR
+ 100
+
+
+ 3
+ Primary
+ true
+
+
+
+
+ 1
+ 3
+
+ C
+ NTFS
+
+
+
+
+
+ true
+
+
+ VK7JG-NPHTM-C97JM-9MPGT-3V66T
+ Never
+
+
+
+ false
+ OnError
+
+
+ false
+ OnError
+
+
+
+ true
+ false
+
+ 0
+ 3
+
+ OnError
+
+
+
+
+
+
+ %configsetroot%\drivers
+
+
+
+
+
+
+ 1
+
+
+
+
+ nano11
+
+
+ true
+
+
+ 0
+
+
+
+
+ 1
+ powershell.exe -WindowStyle "Normal" -NoProfile -Command "$xml = [xml]::new(); $xml.Load('C:\Windows\Panther\unattend.xml'); $sb = [scriptblock]::Create( $xml.unattend.Extensions.ExtractScript ); Invoke-Command -ScriptBlock $sb -ArgumentList $xml;"
+
+
+ 2
+ powershell.exe -WindowStyle "Normal" -ExecutionPolicy "Unrestricted" -NoProfile -File "C:\Windows\Setup\Scripts\Specialize.ps1"
+
+
+ 3
+ reg.exe load "HKU\DefaultUser" "C:\Users\Default\NTUSER.DAT"
+
+
+ 4
+ powershell.exe -WindowStyle "Normal" -ExecutionPolicy "Unrestricted" -NoProfile -File "C:\Windows\Setup\Scripts\DefaultUser.ps1"
+
+
+ 5
+ reg.exe unload "HKU\DefaultUser"
+
+
+
+
+
+
+
+
+ de-DE
+ en-US
+ en-US
+ en-US
+
+
+ true
+
+
+ false
+
+
+
+ Admin
+
+ Administrators
+
+ secret
+ true
+
+
+
+ User
+
+ Users
+
+ user
+ true
+
+
+
+
+
+ Admin
+ true
+
+ secret
+ true
+
+
+
+ 3
+ true
+ true
+ true
+ true
+
+
+
+ 1
+ powershell.exe -WindowStyle "Normal" -ExecutionPolicy "Unrestricted" -NoProfile -File "C:\Windows\Setup\Scripts\FirstLogon.ps1"
+
+
+
+
+
+
+
+ 1cbe01daa2a4b8df5548c4a5eb1cce7f28699acd
+ https://github.com/cschneegans/unattend-generator/commit/1cbe01daa2a4b8df5548c4a5eb1cce7f28699acd
+
+
+
+param(
+ [xml] $Document
+);
+
+foreach( $file in $Document.unattend.Extensions.File ) {
+ $path = [System.Environment]::ExpandEnvironmentVariables( $file.GetAttribute( 'path' ) );
+ mkdir -Path( $path | Split-Path -Parent ) -ErrorAction 'SilentlyContinue';
+ $encoding = switch( [System.IO.Path]::GetExtension( $path ) ) {
+ { $_ -in '.ps1', '.xml' } { [System.Text.Encoding]::UTF8; }
+ { $_ -in '.reg', '.vbs', '.js' } { [System.Text.UnicodeEncoding]::new( $false, $true ); }
+ default { [System.Text.Encoding]::Default; }
+ };
+ $bytes = $encoding.GetPreamble() + $encoding.GetBytes( $file.InnerText.Trim() );
+ [System.IO.File]::WriteAllBytes( $path, $bytes );
+}
+
+
+$selectors = @(
+ 'Microsoft.Microsoft3DViewer';
+ 'Microsoft.BingSearch';
+ 'Microsoft.WindowsCamera';
+ 'Clipchamp.Clipchamp';
+ 'Microsoft.WindowsAlarms';
+ 'Microsoft.Copilot';
+ 'Microsoft.549981C3F5F10';
+ 'Microsoft.Windows.DevHome';
+ 'MicrosoftCorporationII.MicrosoftFamily';
+ 'Microsoft.WindowsFeedbackHub';
+ 'Microsoft.Edge.GameAssist';
+ 'Microsoft.GetHelp';
+ 'Microsoft.Getstarted';
+ 'microsoft.windowscommunicationsapps';
+ 'Microsoft.WindowsMaps';
+ 'Microsoft.MixedReality.Portal';
+ 'Microsoft.BingNews';
+ 'Microsoft.MicrosoftOfficeHub';
+ 'Microsoft.Office.OneNote';
+ 'Microsoft.OutlookForWindows';
+ 'Microsoft.Paint';
+ 'Microsoft.MSPaint';
+ 'Microsoft.People';
+ 'Microsoft.Windows.Photos';
+ 'Microsoft.PowerAutomateDesktop';
+ 'MicrosoftCorporationII.QuickAssist';
+ 'Microsoft.SkypeApp';
+ 'Microsoft.ScreenSketch';
+ 'Microsoft.MicrosoftSolitaireCollection';
+ 'Microsoft.MicrosoftStickyNotes';
+ 'Microsoft.WindowsStore';
+ 'Microsoft.StorePurchaseApp';
+ 'MicrosoftTeams';
+ 'MSTeams';
+ 'Microsoft.Todos';
+ 'Microsoft.WindowsSoundRecorder';
+ 'Microsoft.Wallet';
+ 'Microsoft.BingWeather';
+ 'Microsoft.Xbox.TCUI';
+ 'Microsoft.XboxApp';
+ 'Microsoft.XboxGameOverlay';
+ 'Microsoft.XboxGamingOverlay';
+ 'Microsoft.XboxIdentityProvider';
+ 'Microsoft.XboxSpeechToTextOverlay';
+ 'Microsoft.GamingApp';
+ 'Microsoft.YourPhone';
+ 'Microsoft.ZuneMusic';
+ 'Microsoft.ZuneVideo';
+);
+$getCommand = {
+ Get-AppxProvisionedPackage -Online;
+};
+$filterCommand = {
+ $_.DisplayName -eq $selector;
+};
+$removeCommand = {
+ [CmdletBinding()]
+ param(
+ [Parameter( Mandatory, ValueFromPipeline )]
+ $InputObject
+ );
+ process {
+ $InputObject | Remove-AppxProvisionedPackage -AllUsers -Online -ErrorAction 'Continue';
+ }
+};
+$type = 'Package';
+$logfile = 'C:\Windows\Setup\Scripts\RemovePackages.log';
+& {
+ $installed = & $getCommand;
+ foreach( $selector in $selectors ) {
+ $result = [ordered] @{
+ Selector = $selector;
+ };
+ $found = $installed | Where-Object -FilterScript $filterCommand;
+ if( $found ) {
+ $result.Output = $found | & $removeCommand;
+ if( $? ) {
+ $result.Message = "$type removed.";
+ } else {
+ $result.Message = "$type not removed.";
+ $result.Error = $Error[0];
+ }
+ } else {
+ $result.Message = "$type not installed.";
+ }
+ $result | ConvertTo-Json -Depth 3 -Compress;
+ }
+} *>&1 | Out-String -Width 1KB -Stream >> $logfile;
+
+
+$selectors = @(
+ 'Print.Fax.Scan';
+ 'Language.Handwriting';
+ 'Browser.InternetExplorer';
+ 'MathRecognizer';
+ 'OneCoreUAP.OneSync';
+ 'Microsoft.Windows.MSPaint';
+ 'App.Support.QuickAssist';
+ 'Microsoft.Windows.SnippingTool';
+ 'Language.Speech';
+ 'Language.TextToSpeech';
+ 'App.StepsRecorder';
+ 'Hello.Face.18967';
+ 'Hello.Face.Migration.18967';
+ 'Hello.Face.20134';
+ 'Media.WindowsMediaPlayer';
+ 'Microsoft.Windows.WordPad';
+);
+$getCommand = {
+ Get-WindowsCapability -Online | Where-Object -Property 'State' -NotIn -Value @(
+ 'NotPresent';
+ 'Removed';
+ );
+};
+$filterCommand = {
+ ($_.Name -split '~')[0] -eq $selector;
+};
+$removeCommand = {
+ [CmdletBinding()]
+ param(
+ [Parameter( Mandatory, ValueFromPipeline )]
+ $InputObject
+ );
+ process {
+ $InputObject | Remove-WindowsCapability -Online -ErrorAction 'Continue';
+ }
+};
+$type = 'Capability';
+$logfile = 'C:\Windows\Setup\Scripts\RemoveCapabilities.log';
+& {
+ $installed = & $getCommand;
+ foreach( $selector in $selectors ) {
+ $result = [ordered] @{
+ Selector = $selector;
+ };
+ $found = $installed | Where-Object -FilterScript $filterCommand;
+ if( $found ) {
+ $result.Output = $found | & $removeCommand;
+ if( $? ) {
+ $result.Message = "$type removed.";
+ } else {
+ $result.Message = "$type not removed.";
+ $result.Error = $Error[0];
+ }
+ } else {
+ $result.Message = "$type not installed.";
+ }
+ $result | ConvertTo-Json -Depth 3 -Compress;
+ }
+} *>&1 | Out-String -Width 1KB -Stream >> $logfile;
+
+
+$selectors = @(
+ 'MediaPlayback';
+ 'Microsoft-RemoteDesktopConnection';
+ 'Recall';
+ 'Microsoft-SnippingTool';
+);
+$getCommand = {
+ Get-WindowsOptionalFeature -Online | Where-Object -Property 'State' -NotIn -Value @(
+ 'Disabled';
+ 'DisabledWithPayloadRemoved';
+ );
+};
+$filterCommand = {
+ $_.FeatureName -eq $selector;
+};
+$removeCommand = {
+ [CmdletBinding()]
+ param(
+ [Parameter( Mandatory, ValueFromPipeline )]
+ $InputObject
+ );
+ process {
+ $InputObject | Disable-WindowsOptionalFeature -Online -Remove -NoRestart -ErrorAction 'Continue';
+ }
+};
+$type = 'Feature';
+$logfile = 'C:\Windows\Setup\Scripts\RemoveFeatures.log';
+& {
+ $installed = & $getCommand;
+ foreach( $selector in $selectors ) {
+ $result = [ordered] @{
+ Selector = $selector;
+ };
+ $found = $installed | Where-Object -FilterScript $filterCommand;
+ if( $found ) {
+ $result.Output = $found | & $removeCommand;
+ if( $? ) {
+ $result.Message = "$type removed.";
+ } else {
+ $result.Message = "$type not removed.";
+ $result.Error = $Error[0];
+ }
+ } else {
+ $result.Message = "$type not installed.";
+ }
+ $result | ConvertTo-Json -Depth 3 -Compress;
+ }
+} *>&1 | Out-String -Width 1KB -Stream >> $logfile;
+
+
+<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
+ <Triggers>
+ <BootTrigger>
+ <Repetition>
+ <Interval>P1D</Interval>
+ <StopAtDurationEnd>false</StopAtDurationEnd>
+ </Repetition>
+ <Enabled>true</Enabled>
+ </BootTrigger>
+ </Triggers>
+ <Principals>
+ <Principal id="Author">
+ <UserId>S-1-5-19</UserId>
+ <RunLevel>LeastPrivilege</RunLevel>
+ </Principal>
+ </Principals>
+ <Settings>
+ <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
+ <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
+ <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
+ <AllowHardTerminate>true</AllowHardTerminate>
+ <StartWhenAvailable>false</StartWhenAvailable>
+ <RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
+ <IdleSettings>
+ <StopOnIdleEnd>true</StopOnIdleEnd>
+ <RestartOnIdle>false</RestartOnIdle>
+ </IdleSettings>
+ <AllowStartOnDemand>true</AllowStartOnDemand>
+ <Enabled>true</Enabled>
+ <Hidden>false</Hidden>
+ <RunOnlyIfIdle>false</RunOnlyIfIdle>
+ <WakeToRun>false</WakeToRun>
+ <ExecutionTimeLimit>PT72H</ExecutionTimeLimit>
+ <Priority>7</Priority>
+ </Settings>
+ <Actions Context="Author">
+ <Exec>
+ <Command>C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe</Command>
+ <Arguments>-WindowStyle Hidden -NoProfile -NonInteractive -Command "$format = 'yyyy-MM-ddTHH\:mm\:ssK'; $now = [datetime]::UtcNow; $start = $now.ToString($format); $end = $now.AddDays(7).ToString($format); $params = @{ LiteralPath = 'Registry::HKLM\Software\Microsoft\WindowsUpdate\UX\Settings'; Type = 'String'; Force = $true; Verbose = $true; }; 'PauseFeatureUpdatesStartTime', 'PauseQualityUpdatesStartTime', 'PauseUpdatesStartTime' | foreach { Set-ItemProperty @params -Name $_ -Value $start; }; 'PauseFeatureUpdatesEndTime', 'PauseQualityUpdatesEndTime', 'PauseUpdatesExpiryTime' | foreach { Set-ItemProperty @params -Name $_ -Value $end; };"</Arguments>
+ </Exec>
+ </Actions>
+</Task>
+
+
+<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
+ <Triggers>
+ <BootTrigger>
+ <Repetition>
+ <Interval>PT4H</Interval>
+ <StopAtDurationEnd>false</StopAtDurationEnd>
+ </Repetition>
+ <Enabled>true</Enabled>
+ </BootTrigger>
+ <RegistrationTrigger>
+ <Repetition>
+ <Interval>PT4H</Interval>
+ <StopAtDurationEnd>false</StopAtDurationEnd>
+ </Repetition>
+ <Enabled>true</Enabled>
+ </RegistrationTrigger>
+ </Triggers>
+ <Principals>
+ <Principal id="Author">
+ <UserId>S-1-5-19</UserId>
+ <RunLevel>LeastPrivilege</RunLevel>
+ </Principal>
+ </Principals>
+ <Settings>
+ <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
+ <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
+ <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
+ <AllowHardTerminate>true</AllowHardTerminate>
+ <StartWhenAvailable>false</StartWhenAvailable>
+ <RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
+ <IdleSettings>
+ <StopOnIdleEnd>true</StopOnIdleEnd>
+ <RestartOnIdle>false</RestartOnIdle>
+ </IdleSettings>
+ <AllowStartOnDemand>true</AllowStartOnDemand>
+ <Enabled>true</Enabled>
+ <Hidden>false</Hidden>
+ <RunOnlyIfIdle>false</RunOnlyIfIdle>
+ <WakeToRun>false</WakeToRun>
+ <ExecutionTimeLimit>PT72H</ExecutionTimeLimit>
+ <Priority>7</Priority>
+ </Settings>
+ <Actions Context="Author">
+ <Exec>
+ <Command>%windir%\System32\conhost.exe</Command>
+ <Arguments>--headless %windir%\System32\WindowsPowerShell\v1.0\powershell.exe -WindowStyle Hidden -NoProfile -NonInteractive -Command "$p = @{ LiteralPath = 'Registry::HKLM\Software\Microsoft\WindowsUpdate\UX\Settings'; Type = 'DWord'; }; $h = [datetime]::Now.Hour; Set-ItemProperty @p -Name 'ActiveHoursStart' -Value (($h + 23) % 24); Set-ItemProperty @p -Name 'ActiveHoursEnd' -Value (($h + 11) % 24); Set-ItemProperty @p -Name 'SmartActiveHoursState' -Value 0;"</Arguments>
+ </Exec>
+ </Actions>
+</Task>
+
+
+$excludes = Get-ChildItem -LiteralPath 'Registry::HKU\DefaultUser\AppEvents\EventLabels' |
+ Where-Object -FilterScript { ($_ | Get-ItemProperty).ExcludeFromCPL -eq 1; } |
+ Select-Object -ExpandProperty 'PSChildName';
+Get-ChildItem -Path 'Registry::HKU\DefaultUser\AppEvents\Schemes\Apps\*\*' |
+ Where-Object -Property 'PSChildName' -NotIn $excludes |
+ Get-ChildItem -Include '.Current' | Set-ItemProperty -Name '(Default)' -Value '';
+
+
+$json = '{"pinnedList":[]}';
+if( [System.Environment]::OSVersion.Version.Build -lt 20000 ) {
+ return;
+}
+$key = 'Registry::HKLM\SOFTWARE\Microsoft\PolicyManager\current\device\Start';
+New-Item -Path $key -ItemType 'Directory' -ErrorAction 'SilentlyContinue';
+Set-ItemProperty -LiteralPath $key -Name 'ConfigureStartPins' -Value $json -Type 'String';
+
+
+<LayoutModificationTemplate Version="1" xmlns="http://schemas.microsoft.com/Start/2014/LayoutModification">
+ <LayoutOptions StartTileGroupCellWidth="6" />
+ <DefaultLayoutOverride>
+ <StartLayoutCollection>
+ <StartLayout GroupCellWidth="6" xmlns="http://schemas.microsoft.com/Start/2014/FullDefaultLayout" />
+ </StartLayoutCollection>
+ </DefaultLayoutOverride>
+</LayoutModificationTemplate>
+
+
+Add-Type -TypeDefinition '
+ using System.Drawing;
+ using System.Runtime.InteropServices;
+
+ public static class WallpaperSetter {
+ [DllImport("user32.dll")]
+ private static extern bool SetSysColors(
+ int cElements,
+ int[] lpaElements,
+ int[] lpaRgbValues
+ );
+
+ [DllImport("user32.dll")]
+ private static extern bool SystemParametersInfo(
+ uint uiAction,
+ uint uiParam,
+ string pvParam,
+ uint fWinIni
+ );
+
+ public static void SetDesktopBackground(Color color) {
+ SystemParametersInfo(20, 0, "", 0);
+ SetSysColors(1, new int[] { 1 }, new int[] { ColorTranslator.ToWin32(color) });
+ }
+
+ public static void SetDesktopImage(string file) {
+ SystemParametersInfo(20, 0, file, 0);
+ }
+ }
+' -ReferencedAssemblies 'System.Drawing';
+
+function Set-WallpaperColor {
+ param(
+ [string]
+ $HtmlColor
+ );
+
+ $color = [System.Drawing.ColorTranslator]::FromHtml( $HtmlColor );
+ [WallpaperSetter]::SetDesktopBackground( $color );
+ Set-ItemProperty -Path 'Registry::HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Wallpapers' -Name 'BackgroundType' -Type 'DWord' -Value 1 -Force;
+ Set-ItemProperty -Path 'Registry::HKCU\Control Panel\Desktop' -Name 'WallPaper' -Type 'String' -Value '' -Force;
+ Set-ItemProperty -Path 'Registry::HKCU\Control Panel\Colors' -Name 'Background' -Type 'String' -Value "$($color.R) $($color.G) $($color.B)" -Force;
+}
+
+function Set-WallpaperImage {
+ param(
+ [string]
+ $LiteralPath
+ );
+
+ if( $LiteralPath | Test-Path ) {
+ [WallpaperSetter]::SetDesktopImage( $LiteralPath );
+ Set-ItemProperty -Path 'Registry::HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Wallpapers' -Name 'BackgroundType' -Type 'DWord' -Value 0 -Force;
+ Set-ItemProperty -Path 'Registry::HKCU\Control Panel\Desktop' -Name 'WallPaper' -Type 'String' -Value $LiteralPath -Force;
+ } else {
+ "Cannot use '$LiteralPath' as a desktop wallpaper because that file does not exist.";
+ }
+}
+Set-WallpaperColor -HtmlColor '#008080';
+
+
+$scripts = @(
+ {
+ reg.exe add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\OOBE" /v BypassNRO /t REG_DWORD /d 1 /f;
+ };
+ {
+ Remove-Item -LiteralPath 'Registry::HKLM\Software\Microsoft\WindowsUpdate\Orchestrator\UScheduler_Oobe\DevHomeUpdate' -Force -ErrorAction 'SilentlyContinue';
+ };
+ {
+ Remove-Item -LiteralPath 'C:\Users\Default\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\OneDrive.lnk', 'C:\Windows\System32\OneDriveSetup.exe', 'C:\Windows\SysWOW64\OneDriveSetup.exe' -ErrorAction 'Continue';
+ };
+ {
+ Remove-Item -LiteralPath 'Registry::HKLM\Software\Microsoft\WindowsUpdate\Orchestrator\UScheduler_Oobe\OutlookUpdate' -Force -ErrorAction 'SilentlyContinue';
+ };
+ {
+ reg.exe add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Communications" /v ConfigureChatAutoInstall /t REG_DWORD /d 0 /f;
+ };
+ {
+ & 'C:\Windows\Setup\Scripts\RemovePackages.ps1';
+ };
+ {
+ & 'C:\Windows\Setup\Scripts\RemoveCapabilities.ps1';
+ };
+ {
+ & 'C:\Windows\Setup\Scripts\RemoveFeatures.ps1';
+ };
+ {
+ net.exe accounts /lockoutthreshold:0;
+ };
+ {
+ net.exe accounts /maxpwage:UNLIMITED;
+ };
+ {
+ Register-ScheduledTask -TaskName 'PauseWindowsUpdate' -Xml $( Get-Content -LiteralPath 'C:\Windows\Setup\Scripts\PauseWindowsUpdate.xml' -Raw );
+ };
+ {
+ reg.exe add "HKLM\SOFTWARE\Policies\Microsoft\Windows Defender Security Center\Notifications" /v DisableNotifications /t REG_DWORD /d 1 /f;
+ };
+ {
+ reg.exe add "HKLM\SYSTEM\CurrentControlSet\Control\CI\Policy" /v VerifiedAndReputablePolicyState /t REG_DWORD /d 0 /f;
+ };
+ {
+ reg.exe add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer" /v SmartScreenEnabled /t REG_SZ /d "Off" /f;
+ reg.exe add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\WTDS\Components" /v ServiceEnabled /t REG_DWORD /d 0 /f;
+ reg.exe add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\WTDS\Components" /v NotifyMalicious /t REG_DWORD /d 0 /f;
+ reg.exe add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\WTDS\Components" /v NotifyPasswordReuse /t REG_DWORD /d 0 /f;
+ reg.exe add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\WTDS\Components" /v NotifyUnsafeApp /t REG_DWORD /d 0 /f;
+ reg.exe add "HKLM\SOFTWARE\Policies\Microsoft\Windows Defender Security Center\Systray" /v HideSystray /t REG_DWORD /d 1 /f;
+ };
+ {
+ reg.exe add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" /v EnableLUA /t REG_DWORD /d 0 /f
+ };
+ {
+ reg.exe add "HKLM\SYSTEM\CurrentControlSet\Control\FileSystem" /v LongPathsEnabled /t REG_DWORD /d 1 /f
+ };
+ {
+ Set-ExecutionPolicy -Scope 'LocalMachine' -ExecutionPolicy 'RemoteSigned' -Force;
+ };
+ {
+ fsutil.exe behavior set disableLastAccess 1;
+ };
+ {
+ reg.exe add "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /v AUOptions /t REG_DWORD /d 4 /f;
+ reg.exe add "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /v NoAutoRebootWithLoggedOnUsers /t REG_DWORD /d 1 /f;
+ };
+ {
+ Register-ScheduledTask -TaskName 'MoveActiveHours' -Xml $( Get-Content -LiteralPath 'C:\Windows\Setup\Scripts\MoveActiveHours.xml' -Raw );
+ };
+ {
+ reg.exe add "HKLM\SOFTWARE\Policies\Microsoft\Dsh" /v AllowNewsAndInterests /t REG_DWORD /d 0 /f;
+ };
+ {
+ reg.exe add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\LogonUI\BootAnimation" /v DisableStartupSound /t REG_DWORD /d 1 /f;
+ reg.exe add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\EditionOverrides" /v UserSetting_DisableStartupSound /t REG_DWORD /d 1 /f;
+ };
+ {
+ reg.exe add "HKLM\Software\Policies\Microsoft\Windows\CloudContent" /v "DisableWindowsConsumerFeatures" /t REG_DWORD /d 1 /f;
+ };
+ {
+ reg.exe add "HKLM\SYSTEM\CurrentControlSet\Control\BitLocker" /v "PreventDeviceEncryption" /t REG_DWORD /d 1 /f;
+ };
+ {
+ reg.exe add "HKLM\Software\Policies\Microsoft\Edge" /v HideFirstRunExperience /t REG_DWORD /d 1 /f;
+ };
+ {
+ reg.exe add "HKLM\Software\Policies\Microsoft\Edge\Recommended" /v BackgroundModeEnabled /t REG_DWORD /d 0 /f;
+ reg.exe add "HKLM\Software\Policies\Microsoft\Edge\Recommended" /v StartupBoostEnabled /t REG_DWORD /d 0 /f;
+ };
+ {
+ & 'C:\Windows\Setup\Scripts\SetStartPins.ps1';
+ };
+ {
+ Set-ItemProperty -LiteralPath "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VisualEffects\ControlAnimations" -Name 'DefaultValue' -Value 0 -Type 'DWord' -Force;
+ Set-ItemProperty -LiteralPath "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VisualEffects\AnimateMinMax" -Name 'DefaultValue' -Value 0 -Type 'DWord' -Force;
+ Set-ItemProperty -LiteralPath "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VisualEffects\TaskbarAnimations" -Name 'DefaultValue' -Value 0 -Type 'DWord' -Force;
+ Set-ItemProperty -LiteralPath "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VisualEffects\DWMAeroPeekEnabled" -Name 'DefaultValue' -Value 0 -Type 'DWord' -Force;
+ Set-ItemProperty -LiteralPath "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VisualEffects\MenuAnimation" -Name 'DefaultValue' -Value 0 -Type 'DWord' -Force;
+ Set-ItemProperty -LiteralPath "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VisualEffects\TooltipAnimation" -Name 'DefaultValue' -Value 0 -Type 'DWord' -Force;
+ Set-ItemProperty -LiteralPath "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VisualEffects\SelectionFade" -Name 'DefaultValue' -Value 0 -Type 'DWord' -Force;
+ Set-ItemProperty -LiteralPath "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VisualEffects\DWMSaveThumbnailEnabled" -Name 'DefaultValue' -Value 0 -Type 'DWord' -Force;
+ Set-ItemProperty -LiteralPath "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VisualEffects\CursorShadow" -Name 'DefaultValue' -Value 0 -Type 'DWord' -Force;
+ Set-ItemProperty -LiteralPath "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VisualEffects\ListviewShadow" -Name 'DefaultValue' -Value 0 -Type 'DWord' -Force;
+ Set-ItemProperty -LiteralPath "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VisualEffects\ThumbnailsOrIcon" -Name 'DefaultValue' -Value 0 -Type 'DWord' -Force;
+ Set-ItemProperty -LiteralPath "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VisualEffects\ListviewAlphaSelect" -Name 'DefaultValue' -Value 0 -Type 'DWord' -Force;
+ Set-ItemProperty -LiteralPath "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VisualEffects\DragFullWindows" -Name 'DefaultValue' -Value 0 -Type 'DWord' -Force;
+ Set-ItemProperty -LiteralPath "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VisualEffects\ComboBoxAnimation" -Name 'DefaultValue' -Value 0 -Type 'DWord' -Force;
+ Set-ItemProperty -LiteralPath "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VisualEffects\FontSmoothing" -Name 'DefaultValue' -Value 0 -Type 'DWord' -Force;
+ Set-ItemProperty -LiteralPath "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VisualEffects\ListBoxSmoothScrolling" -Name 'DefaultValue' -Value 0 -Type 'DWord' -Force;
+ Set-ItemProperty -LiteralPath "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VisualEffects\DropShadow" -Name 'DefaultValue' -Value 0 -Type 'DWord' -Force;
+ };
+ {
+ reg.exe add "HKU\.DEFAULT\Control Panel\Accessibility\StickyKeys" /v Flags /t REG_SZ /d 10 /f;
+ };
+ {
+ reg.exe add "HKLM\Software\Microsoft\Windows\CurrentVersion\Policies\System" /v "DisableAutomaticRestartSignOn" /t REG_DWORD /d 1 /f;
+ };
+);
+
+& {
+ [float] $complete = 0;
+ [float] $increment = 100 / $scripts.Count;
+ foreach( $script in $scripts ) {
+ Write-Progress -Id 0 -Activity 'Running scripts to customize your Windows installation. Do not close this window.' -PercentComplete $complete;
+ '*** Will now execute command «{0}».' -f $(
+ $script.ToString().Trim() -replace '\s+', ' ' -replace '^(.{99})(.+)$', '$1…';
+ );
+ $start = [datetime]::Now;
+ & $script;
+ '*** Finished executing command after {0:0} ms.' -f [datetime]::Now.Subtract( $start ).TotalMilliseconds;
+ "`r`n" * 3;
+ $complete += $increment;
+ }
+} *>&1 | Out-String -Width 1KB -Stream >> "C:\Windows\Setup\Scripts\Specialize.log";
+
+
+$scripts = @(
+ {
+ Get-AppxPackage -Name 'Microsoft.Windows.Ai.Copilot.Provider' | Remove-AppxPackage;
+ };
+ {
+ @(
+ Get-ChildItem -LiteralPath $env:USERPROFILE -Force -Recurse -Depth 2;
+ ) | Where-Object -FilterScript {
+ $_.Attributes.HasFlag( [System.IO.FileAttributes]::ReparsePoint );
+ } | Remove-Item -Force -Recurse -Verbose;
+ };
+ {
+ Set-ItemProperty -LiteralPath 'Registry::HKCU\AppEvents\Schemes' -Name '(Default)' -Type 'String' -Value '.None';
+ };
+ {
+ Set-ItemProperty -LiteralPath 'Registry::HKCU\Software\Microsoft\Windows\CurrentVersion\Search' -Name 'SearchboxTaskbarMode' -Type 'DWord' -Value 0;
+ };
+ {
+ Set-ItemProperty -LiteralPath 'Registry::HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\VisualEffects' -Name 'VisualFXSetting' -Type 'DWord' -Value 2 -Force;
+ };
+ {
+ & 'C:\Windows\Setup\Scripts\SetWallpaper.ps1';
+ };
+ {
+ Get-Process -Name 'explorer' -ErrorAction 'SilentlyContinue' | Where-Object -FilterScript {
+ $_.SessionId -eq ( Get-Process -Id $PID ).SessionId;
+ } | Stop-Process -Force;
+ };
+);
+
+& {
+ [float] $complete = 0;
+ [float] $increment = 100 / $scripts.Count;
+ foreach( $script in $scripts ) {
+ Write-Progress -Id 0 -Activity 'Running scripts to configure this user account. Do not close this window.' -PercentComplete $complete;
+ '*** Will now execute command «{0}».' -f $(
+ $script.ToString().Trim() -replace '\s+', ' ' -replace '^(.{99})(.+)$', '$1…';
+ );
+ $start = [datetime]::Now;
+ & $script;
+ '*** Finished executing command after {0:0} ms.' -f [datetime]::Now.Subtract( $start ).TotalMilliseconds;
+ "`r`n" * 3;
+ $complete += $increment;
+ }
+} *>&1 | Out-String -Width 1KB -Stream >> "$env:TEMP\UserOnce.log";
+
+
+$scripts = @(
+ {
+ reg.exe add "HKU\DefaultUser\Software\Policies\Microsoft\Windows\WindowsCopilot" /v TurnOffWindowsCopilot /t REG_DWORD /d 1 /f;
+ };
+ {
+ reg.exe add "HKU\DefaultUser\Software\Microsoft\Internet Explorer\LowRegistry\Audio\PolicyConfig\PropertyStore" /f;
+ };
+ {
+ Remove-ItemProperty -LiteralPath 'Registry::HKU\DefaultUser\Software\Microsoft\Windows\CurrentVersion\Run' -Name 'OneDriveSetup' -Force -ErrorAction 'Continue';
+ };
+ {
+ reg.exe add "HKU\DefaultUser\Software\Microsoft\Windows\CurrentVersion\GameDVR" /v AppCaptureEnabled /t REG_DWORD /d 0 /f;
+ };
+ {
+ reg.exe add "HKU\DefaultUser\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced" /v "HideFileExt" /t REG_DWORD /d 0 /f;
+ };
+ {
+ reg.exe add "HKU\DefaultUser\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced" /v "Hidden" /t REG_DWORD /d 1 /f;
+ };
+ {
+ reg.exe add "HKU\DefaultUser\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced" /v "ShowSuperHidden" /t REG_DWORD /d 0 /f;
+ };
+ {
+ reg.exe add "HKU\DefaultUser\Software\Microsoft\Edge\SmartScreenEnabled" /ve /t REG_DWORD /d 0 /f;
+ reg.exe add "HKU\DefaultUser\Software\Microsoft\Edge\SmartScreenPuaEnabled" /ve /t REG_DWORD /d 0 /f;
+ reg.exe add "HKU\DefaultUser\Software\Microsoft\Windows\CurrentVersion\AppHost" /v EnableWebContentEvaluation /t REG_DWORD /d 0 /f;
+ reg.exe add "HKU\DefaultUser\Software\Microsoft\Windows\CurrentVersion\AppHost" /v PreventOverride /t REG_DWORD /d 0 /f;
+ };
+ {
+ & 'C:\Windows\Setup\Scripts\TurnOffSystemSounds.ps1';
+ };
+ {
+ $names = @(
+ 'ContentDeliveryAllowed';
+ 'FeatureManagementEnabled';
+ 'OEMPreInstalledAppsEnabled';
+ 'PreInstalledAppsEnabled';
+ 'PreInstalledAppsEverEnabled';
+ 'SilentInstalledAppsEnabled';
+ 'SoftLandingEnabled';
+ 'SubscribedContentEnabled';
+ 'SubscribedContent-310093Enabled';
+ 'SubscribedContent-338387Enabled';
+ 'SubscribedContent-338388Enabled';
+ 'SubscribedContent-338389Enabled';
+ 'SubscribedContent-338393Enabled';
+ 'SubscribedContent-353694Enabled';
+ 'SubscribedContent-353696Enabled';
+ 'SubscribedContent-353698Enabled';
+ 'SystemPaneSuggestionsEnabled';
+ );
+
+ foreach( $name in $names ) {
+ reg.exe add "HKU\DefaultUser\Software\Microsoft\Windows\CurrentVersion\ContentDeliveryManager" /v $name /t REG_DWORD /d 0 /f;
+ }
+ };
+ {
+ reg.exe add "HKU\DefaultUser\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced" /v TaskbarAl /t REG_DWORD /d 0 /f;
+ };
+ {
+ reg.exe add "HKU\DefaultUser\Software\Policies\Microsoft\Windows\Explorer" /v DisableSearchBoxSuggestions /t REG_DWORD /d 1 /f;
+ };
+ {
+ reg.exe add "HKU\DefaultUser\Control Panel\Accessibility\StickyKeys" /v Flags /t REG_SZ /d 10 /f;
+ };
+ {
+ reg.exe add "HKU\DefaultUser\Software\Microsoft\Windows\CurrentVersion\RunOnce" /v "UnattendedSetup" /t REG_SZ /d "powershell.exe -WindowStyle \""Normal\"" -ExecutionPolicy \""Unrestricted\"" -NoProfile -File \""C:\Windows\Setup\Scripts\UserOnce.ps1\""" /f;
+ };
+);
+
+& {
+ [float] $complete = 0;
+ [float] $increment = 100 / $scripts.Count;
+ foreach( $script in $scripts ) {
+ Write-Progress -Id 0 -Activity 'Running scripts to modify the default user’’s registry hive. Do not close this window.' -PercentComplete $complete;
+ '*** Will now execute command «{0}».' -f $(
+ $script.ToString().Trim() -replace '\s+', ' ' -replace '^(.{99})(.+)$', '$1…';
+ );
+ $start = [datetime]::Now;
+ & $script;
+ '*** Finished executing command after {0:0} ms.' -f [datetime]::Now.Subtract( $start ).TotalMilliseconds;
+ "`r`n" * 3;
+ $complete += $increment;
+ }
+} *>&1 | Out-String -Width 1KB -Stream >> "C:\Windows\Setup\Scripts\DefaultUser.log";
+
+
+$scripts = @(
+ {
+ # Set-ItemProperty -LiteralPath 'Registry::HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon' -Name 'AutoLogonCount' -Type 'DWord' -Force -Value 0;
+ };
+ {
+ @(
+ Get-ChildItem -LiteralPath 'C:\' -Force;
+ Get-ChildItem -LiteralPath 'C:\Users' -Force;
+ Get-ChildItem -LiteralPath 'C:\Users\Default' -Force -Recurse -Depth 2;
+ Get-ChildItem -LiteralPath 'C:\Users\Public' -Force -Recurse -Depth 2;
+ Get-ChildItem -LiteralPath 'C:\ProgramData' -Force;
+ ) | Where-Object -FilterScript {
+ $_.Attributes.HasFlag( [System.IO.FileAttributes]::ReparsePoint );
+ } | Remove-Item -Force -Recurse -Verbose;
+ };
+ {
+ Disable-ComputerRestore -Drive 'C:\';
+ };
+ {
+ cmd.exe /c "rmdir C:\Windows.old";
+ };
+ {
+ Set-Service -Name WSearch -StartupType 'Disabled' -Status 'Stopped' `
+ -Force -ErrorAction 'SilentlyContinue';
+ };
+ {
+ & 'C:\Windows\Setup\Scripts\WinRM.ps1';
+ };
+ {
+ & 'C:\Windows\Setup\Scripts\OpenSSH.ps1';
+ };
+ {
+ # & 'C:\Windows\Setup\Scripts\InstallChocolatey.ps1';
+ };
+ {
+ # & 'C:\Windows\Setup\Scripts\InstallPython.ps1';
+ };
+ {
+ # & 'C:\Windows\Setup\Scripts\InstallNodeJS.ps1';
+ };
+ {
+ # & 'C:\Windows\Setup\Scripts\InstallGit.ps1';
+ };
+ {
+ # & 'C:\Windows\Setup\Scripts\InstallVisualStudio.ps1';
+ };
+ {
+ & 'C:\Windows\Setup\Scripts\WinFSP.ps1';
+ };
+ {
+ Remove-Item -LiteralPath @(
+ 'C:\Windows\Panther\unattend.xml';
+ 'C:\Windows\Panther\unattend-original.xml';
+ 'C:\Windows\Setup\Scripts\Wifi.xml';
+ ) -Force -ErrorAction 'SilentlyContinue' -Verbose;
+ };
+ {
+ Remove-Item -LiteralPath @(
+ Get-ChildItem -LiteralPath $(Join-Path -Path $env:WINDIR -ChildPath 'Temp') -Force;
+ Get-ChildItem -LiteralPath $(Join-Path -Path $env:LOCALAPPDATA -ChildPath 'Temp') -Force;
+ ) -Force -ErrorAction 'SilentlyContinue' -Verbose;
+ };
+ {
+ $keepList = @( 'autounattend.xml' );
+ Get-ChildItem -Path "${env:WINDIR}\ConfigSetRoot" | Where-Object { $_.Name -notin $keepList } | ForEach-Object {
+ Write-Output "Removing non-essential file/folder from ConfigSetRoot: $($_.Name)"
+ Remove-Item -Path $_.FullName -Recurse -Force
+ }
+ };
+ {
+ New-Item -Path "${env:USERPROFILE}\nano11builder" -Type Directory -Force;
+ (New-Object System.Net.WebClient).DownloadFile('https://git.bitplumber.de/stkn/win11-builder/raw/branch/main/bootstrap/nano11builder-headless.ps1', `
+ "${env:USERPROFILE}\nano11builder\nano11builder.ps1");
+ };
+ {
+ cmd.exe /c "shutdown /r /f /t 3"
+ };
+);
+
+& {
+ [float] $complete = 0;
+ [float] $increment = 100 / $scripts.Count;
+ foreach( $script in $scripts ) {
+ Write-Progress -Id 0 -Activity 'Running scripts to finalize your Windows installation. Do not close this window.' -PercentComplete $complete;
+ '*** Will now execute command «{0}».' -f $(
+ $script.ToString().Trim() -replace '\s+', ' ' -replace '^(.{99})(.+)$', '$1…';
+ );
+ $start = [datetime]::Now;
+ & $script;
+ '*** Finished executing command after {0:0} ms.' -f [datetime]::Now.Subtract( $start ).TotalMilliseconds;
+ "`r`n" * 3;
+ $complete += $increment;
+ }
+} *>&1 | Out-String -Width 1KB -Stream >> "C:\Windows\Setup\Scripts\FirstLogon.log";
+
+
+
+
+Set-ExecutionPolicy Unrestricted -Scope LocalMachine -Force -ErrorAction Ignore
+
+Write-Output "Running WinRM quickconfig setup..."
+cmd.exe /c winrm quickconfig -q -force
+
+Write-Output "Disabling WinRM over HTTP..."
+# Scope: Public
+Disable-NetFirewallRule -Name "WINRM-HTTP-In-TCP"
+# Scope: Domain,Private
+Disable-NetFirewallRule -Name "WINRM-HTTP-In-TCP-NoScope"
+Get-ChildItem WSMan:\Localhost\listener -Force | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue
+
+Write-Output "Configuring WinRM for HTTPS..."
+Set-Item -Path WSMan:\LocalHost\MaxTimeoutms -Value '1800000' -Force
+Set-Item -Path WSMan:\LocalHost\Shell\MaxMemoryPerShellMB -Value '1024' -Force
+Set-Item -Path WSMan:\LocalHost\Service\AllowUnencrypted -Value 'false' -Force
+Set-Item -Path WSMan:\LocalHost\Service\Auth\Basic -Value 'true' -Force
+Set-Item -Path WSMan:\LocalHost\Service\Auth\CredSSP -Value 'true' -Force
+
+New-NetFirewallRule -Name "WINRM-HTTPS-In-TCP" `
+ -DisplayName "Windows Remote Management (HTTPS-In)" `
+ -Description "Inbound rule for Windows Remote Management via WS-Management. [TCP 5986]" `
+ -Group "Windows Remote Management" `
+ -Program "System" `
+ -Protocol TCP `
+ -LocalPort "5986" `
+ -Action Allow `
+ -Profile Domain,Private
+
+New-NetFirewallRule -Name "WINRM-HTTPS-In-TCP-PUBLIC" `
+ -DisplayName "Windows Remote Management (HTTPS-In)" `
+ -Description "Inbound rule for Windows Remote Management via WS-Management. [TCP 5986]" `
+ -Group "Windows Remote Management" `
+ -Program "System" `
+ -Protocol TCP `
+ -LocalPort "5986" `
+ -Action Allow `
+ -Profile Public
+
+$Hostname = [System.Net.Dns]::GetHostByName((hostname)).HostName.ToUpper()
+$Cert = New-SelfSignedCertificate -CertstoreLocation Cert:\LocalMachine\My -DnsName $Hostname
+
+New-Item -Path WSMan:\LocalHost\Listener -Address * -Transport HTTPS -Hostname $Hostname -CertificateThumbPrint $Cert.Thumbprint -Port "5986" -force
+
+Write-Output "Configuring WinRM service for automatic start..."
+Set-Service -Name WinRM -StartupType Automatic
+
+Write-Output "Restarting WinRM Service..."
+Restart-Service -Name WinRM -Force
+
+
+
+Write-Output "Installing OpenSSH 10.0.0.0p2 manually...";
+(New-Object System.Net.WebClient).DownloadFile('https://github.com/PowerShell/Win32-OpenSSH/releases/download/10.0.0.0p2-Preview/OpenSSH-AMD64-v10.0.0.0.msi', "${env:TEMP}\openssh-amd64.msi");
+Start-Process "msiexec.exe" -ArgumentList "/i `"${env:TEMP}\openssh-amd64.msi`" /log `"C:\Windows\Setup\Scripts\openssh-amd64.log`" /passive /norestart ALLUSERS=1" -Wait -PassThru;
+Remove-Item -LiteralPath "${env:TEMP}\openssh-amd64.msi" -Force -ErrorAction 'SilentlyContinue';
+
+Write-Output "Installing OpenSSH Server..."
+& "$env:ProgramFiles\OpenSSH-Win64\install-sshd.ps1"
+
+Write-Output "Enabling OpenSSH Server..."
+Set-Service -Name sshd -StartupType Automatic
+
+New-NetFirewallRule -Name "OpenSSH-SSH-In-TCP" `
+ -DisplayName "OpenSSH Server (SSH-In)" `
+ -Description "Inbound rule for OpenSSH Server connections. [TCP 22]" `
+ -Group "Windows Remote Management" `
+ -Protocol TCP `
+ -LocalPort "22" `
+ -Action Allow `
+ -Profile Domain,Private
+
+New-NetFirewallRule -Name "OpenSSH-SSH-In-TCP-PUBLIC" `
+ -DisplayName "OpenSSH Server (SSH-In)" `
+ -Description "Inbound rule for OpenSSH Server connections. [TCP 22]" `
+ -Group "Windows Remote Management" `
+ -Protocol TCP `
+ -LocalPort "22" `
+ -Action Allow `
+ -Profile Public
+
+
+
+Write-Output "Installing Chocolatey..."
+Set-ExecutionPolicy Bypass -Scope Process -Force
+[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072
+iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))
+
+
+
+
+#
+# Common defines
+#
+$cpu_arch = $env:PROCESSOR_ARCHITECTURE.ToLower();
+
+#
+# Git
+#
+$git_version = "2.54.0";
+$git_url = & {
+ if ($cpu_arch -eq "amd64") {
+ "https://github.com/git-for-windows/git/releases/download/v${git_version}.windows.1/Git-${git_version}-64-bit.exe";
+ } else {
+ "https://github.com/git-for-windows/git/releases/download/v${git_version}.windows.1/Git-${git_version}-${cpu_arch}.exe";
+ }
+};
+
+Write-Output "Installing Git ${git_version} manually...";
+(New-Object System.Net.WebClient).DownloadFile($git_url, "${env:TEMP}\git-${cpu_arch}.exe");
+Start-Process -FilePath "${env:TEMP}\git-${cpu_arch}.exe" -ArgumentList `
+ "/ALLUSERS /VERYSILENT /NORESTART /NOCANCEL /SP- /CLOSEAPPLICATIONS /RESTARTAPPLICATIONS /COMPONENTS=`"icons,assoc,assoc_sh,windowsterminal`"", `
+ "/o:EditorOption=Nano", `
+ "/o:CurlOption=WinSSL", `
+ "/o:PathOption=CmdTools" `
+ -Wait -PassThru;
+Remove-Item -LiteralPath "${env:TEMP}\git-${cpu_arch}.exe" -Force -ErrorAction 'SilentlyContinue';
+
+
+
+#
+# Common defines
+#
+$cpu_arch = $env:PROCESSOR_ARCHITECTURE.ToLower();
+
+#
+# Python
+#
+$python_version = "3.14.5";
+$python_url = "https://www.python.org/ftp/python/${python_version}/python-${python_version}-${cpu_arch}.exe";
+
+Write-Output "Installing Python ${python_version} manually...";
+(New-Object System.Net.WebClient).DownloadFile($python_url, "${env:TEMP}\python-${cpu_arch}.exe");
+Start-Process -FilePath "${env:TEMP}\python-${cpu_arch}.exe" -ArgumentList "/quiet PrependPath=1 InstallAllUsers=1" -Wait -PassThru;
+Remove-Item -LiteralPath "${env:TEMP}\python-${cpu_arch}.exe" -Force -ErrorAction 'SilentlyContinue';
+
+
+
+#
+# Common defines
+#
+$cpu_arch = $env:PROCESSOR_ARCHITECTURE.ToLower();
+
+#
+# NodeJS (LTS)
+#
+$node_version = "24.15.0";
+$node_url = & {
+ if ($cpu_arch -eq "amd64") {
+ "https://nodejs.org/dist/v${node_version}/node-v${node_version}-x64.msi";
+ } else {
+ "https://nodejs.org/dist/v${node_version}/node-v${node_version}-${cpu_arch}.msi";
+ }
+};
+
+Write-Output "Installing NodeJS ${node_version} manually...";
+(New-Object System.Net.WebClient).DownloadFile($node_url, "${env:TEMP}\node-${cpu_arch}.msi");
+Start-Process "msiexec.exe" -ArgumentList "/i `"${env:TEMP}\node-${cpu_arch}.msi`" /log `"${env:WINDIR}\Setup\Scripts\node-${cpu_arch}.log`" /passive /norestart ALLUSERS=1" -Wait -PassThru;
+Remove-Item -LiteralPath "${env:TEMP}\node-${cpu_arch}.msi" -Force -ErrorAction 'SilentlyContinue';
+
+
+
+
+#
+# VSWhere (AMD64 only)
+#
+$vswhere_version = "3.1.7";
+$vswhere_url = "https://github.com/microsoft/vswhere/releases/download/${vswhere_version}/vswhere.exe";
+
+Write-Output "Installing VSWhere ${vswhere_version} manually..."
+(New-Object System.Net.WebClient).DownloadFile($vswhere_url, "${env:WINDIR}\vswhere.exe");
+
+#
+# VS2022 Buildtools
+#
+# Write-Output "Installing VisualStudio 2022 Buildtools manually..."
+# (New-Object System.Net.WebClient).DownloadFile('https://aka.ms/vs/17/release/vs_buildtools.exe', "${env:TEMP}\vs_buildtools.exe");
+# Start-Process -FilePath "${env:TEMP}\vs_buildtools.exe" -ArgumentList `
+# "--passive --wait --norestart --nocache", `
+# "--add Microsoft.VisualStudio.Workload.VCTools", `
+# "--add Microsoft.VisualStudio.Component.VC.ATLMFC", `
+# "--add Microsoft.VisualStudio.Component.VC.ATL.ARM64", `
+# "--add Microsoft.VisualStudio.Component.VC.MFC.ARM64" `
+# -Wait -PassThru;
+
+#
+# VS2026 Buildtools
+#
+Write-Output "Installing VisualStudio 2026 Buildtools manually..."
+(New-Object System.Net.WebClient).DownloadFile('https://aka.ms/vs/18/Stable/vs_buildtools.exe', "${env:TEMP}\vs_buildtools.exe");
+Start-Process -FilePath "${env:TEMP}\vs_buildtools.exe" -ArgumentList `
+ "--passive --wait --norestart --nocache", `
+ "--add Microsoft.VisualStudio.Workload.VCTools", `
+ "--add Microsoft.VisualStudio.Component.VC.ATLMFC", `
+ "--add Microsoft.VisualStudio.Component.VC.ATL.ARM64", `
+ "--add Microsoft.VisualStudio.Component.VC.MFC.ARM64" `
+ -Wait -PassThru;
+
+#
+# VS2022 Community
+#
+# Write-Output "Installing VisualStudio 2022 Community manually..."
+# (New-Object System.Net.WebClient).DownloadFile('https://aka.ms/vs/17/release/vs_community.exe', "${env:TEMP}\vs_community.exe");
+# Start-Process -FilePath "${env:TEMP}\vs_community.exe" -ArgumentList `
+# "--passive --wait --norestart --nocache", `
+# "--add Microsoft.VisualStudio.Workload.NativeDesktop", `
+# "--add Microsoft.VisualStudio.Component.VC.ATLMFC", `
+# "--add Microsoft.VisualStudio.Component.VC.ATL.ARM64", `
+# "--add Microsoft.VisualStudio.Component.VC.MFC.ARM64", `
+# "--includeRecommended" `
+# -Wait -PassThru;
+
+# Remove VS bootstrapper
+Remove-Item -LiteralPath @(
+ "${env:TEMP}\vs_buildtools.exe";
+ "${env:TEMP}\vs_community.exe";
+) -Force -ErrorAction 'SilentlyContinue';
+
+
+
+# Download and install WinFSP
+$winfsp_version = "2026 Beta 1";
+#$winfsp_url = "https://github.com/winfsp/winfsp/releases/download/v2.2B1/winfsp-2.2.26112.msi";
+$winfsp_url = "https://github.com/winfsp/winfsp/releases/download/v2.1/winfsp-2.1.25156.msi";
+
+Write-Output "Installing WinFSP ${winfsp_version} manually...";
+(New-Object System.Net.WebClient).DownloadFile($winfsp_url, "${env:TEMP}\winfsp.msi");
+Start-Process "msiexec.exe" `
+ -ArgumentList "/i `"${env:TEMP}\winfsp.msi`" /log `"${env:WINDIR}\Setup\Scripts\winfsp.log`" /passive /norestart ALLUSERS=1" `
+ -Wait -PassThru;
+Remove-Item -LiteralPath "${env:TEMP}\winfsp.msi" -Force -ErrorAction 'SilentlyContinue';
+
+# Load viofs driver
+Start-Process "PnpUtil.exe" -ArgumentList "/add-driver `"${env:SystemDrive}\Drivers\viofs\viofs.inf`" /install" `
+ -Wait -PassThru;
+
+# Install VirtioFS service
+New-Service -Name VirtioFsSvc -DisplayName 'Virtio FS Service' `
+ -BinaryPathName "${env:SystemDrive}\Drivers\viofs\virtiofs.exe" `
+ -StartupType 'Automatic' `
+ -DependsOn 'WinFsp.Launcher';
+
+Start-Service -Name VirtioFsSvc -PassThru;
+
+
+
+
diff --git a/configs/autounattend-builder-amd64-20260527.xml b/configs/autounattend-builder-amd64-20260527.xml
new file mode 100644
index 0000000..3321726
--- /dev/null
+++ b/configs/autounattend-builder-amd64-20260527.xml
@@ -0,0 +1,1801 @@
+
+
+
+
+
+
+
+ en-US
+
+ de-DE
+ en-US
+ en-US
+ en-US
+ en-US
+
+
+ true
+
+ OnError
+ true
+
+ 0
+ true
+
+
+ 1
+ EFI
+ 300
+
+
+ 2
+ MSR
+ 100
+
+
+ 3
+ Primary
+ true
+
+
+
+
+ 1
+ 3
+
+ C
+ NTFS
+
+
+
+
+
+ true
+
+
+ VK7JG-NPHTM-C97JM-9MPGT-3V66T
+ Never
+
+
+
+ false
+ OnError
+
+
+ false
+ OnError
+
+
+
+ true
+ false
+
+ 0
+ 3
+
+ OnError
+
+
+
+
+
+
+ %configsetroot%\drivers
+
+
+
+
+
+
+ 1
+
+
+
+
+ nano11
+
+
+ true
+
+
+ 0
+
+
+
+
+ 1
+ powershell.exe -WindowStyle "Normal" -NoProfile -Command "$xml = [xml]::new(); $xml.Load('C:\Windows\Panther\unattend.xml'); $sb = [scriptblock]::Create( $xml.unattend.Extensions.ExtractScript ); Invoke-Command -ScriptBlock $sb -ArgumentList $xml;"
+
+
+ 2
+ powershell.exe -WindowStyle "Normal" -ExecutionPolicy "Unrestricted" -NoProfile -File "C:\Windows\Setup\Scripts\Specialize.ps1"
+
+
+ 3
+ reg.exe load "HKU\DefaultUser" "C:\Users\Default\NTUSER.DAT"
+
+
+ 4
+ powershell.exe -WindowStyle "Normal" -ExecutionPolicy "Unrestricted" -NoProfile -File "C:\Windows\Setup\Scripts\DefaultUser.ps1"
+
+
+ 5
+ reg.exe unload "HKU\DefaultUser"
+
+
+
+
+
+
+
+
+ de-DE
+ en-US
+ en-US
+ en-US
+
+
+ true
+
+
+ false
+
+
+
+ Admin
+
+ Administrators
+
+ secret
+ true
+
+
+
+ User
+
+ Users
+
+ user
+ true
+
+
+
+
+
+ Admin
+ true
+
+ secret
+ true
+
+
+
+ 3
+ true
+ true
+ true
+ true
+ Work
+ true
+ true
+
+
+
+ 1
+ powershell.exe -WindowStyle "Normal" -ExecutionPolicy "Unrestricted" -NoProfile -File "C:\Windows\Setup\Scripts\FirstLogon.ps1"
+
+
+
+
+
+
+
+ 1cbe01daa2a4b8df5548c4a5eb1cce7f28699acd
+ https://github.com/cschneegans/unattend-generator/commit/1cbe01daa2a4b8df5548c4a5eb1cce7f28699acd
+
+
+
+param(
+ [xml] $Document
+);
+
+foreach( $file in $Document.unattend.Extensions.File ) {
+ $path = [System.Environment]::ExpandEnvironmentVariables( $file.GetAttribute( 'path' ) );
+ mkdir -Path( $path | Split-Path -Parent ) -ErrorAction 'SilentlyContinue';
+ $encoding = switch( [System.IO.Path]::GetExtension( $path ) ) {
+ { $_ -in '.ps1', '.xml' } { [System.Text.Encoding]::UTF8; }
+ { $_ -in '.reg', '.vbs', '.js' } { [System.Text.UnicodeEncoding]::new( $false, $true ); }
+ default { [System.Text.Encoding]::Default; }
+ };
+ $bytes = $encoding.GetPreamble() + $encoding.GetBytes( $file.InnerText.Trim() );
+ [System.IO.File]::WriteAllBytes( $path, $bytes );
+}
+
+
+$selectors = @(
+ 'Microsoft.Microsoft3DViewer';
+ 'Microsoft.BingSearch';
+ 'Microsoft.WindowsCamera';
+ 'Clipchamp.Clipchamp';
+ 'Microsoft.WindowsAlarms';
+ 'Microsoft.Copilot';
+ 'Microsoft.549981C3F5F10';
+ 'Microsoft.Windows.DevHome';
+ 'MicrosoftCorporationII.MicrosoftFamily';
+ 'Microsoft.WindowsFeedbackHub';
+ 'Microsoft.Edge.GameAssist';
+ 'Microsoft.GetHelp';
+ 'Microsoft.Getstarted';
+ 'microsoft.windowscommunicationsapps';
+ 'Microsoft.WindowsMaps';
+ 'Microsoft.MixedReality.Portal';
+ 'Microsoft.BingNews';
+ 'Microsoft.MicrosoftOfficeHub';
+ 'Microsoft.Office.OneNote';
+ 'Microsoft.OutlookForWindows';
+ 'Microsoft.Paint';
+ 'Microsoft.MSPaint';
+ 'Microsoft.People';
+ 'Microsoft.Windows.Photos';
+ 'Microsoft.PowerAutomateDesktop';
+ 'MicrosoftCorporationII.QuickAssist';
+ 'Microsoft.SkypeApp';
+ 'Microsoft.ScreenSketch';
+ 'Microsoft.MicrosoftSolitaireCollection';
+ 'Microsoft.MicrosoftStickyNotes';
+ 'Microsoft.WindowsStore';
+ 'Microsoft.StorePurchaseApp';
+ 'MicrosoftTeams';
+ 'MSTeams';
+ 'Microsoft.Todos';
+ 'Microsoft.WindowsSoundRecorder';
+ 'Microsoft.Wallet';
+ 'Microsoft.BingWeather';
+ 'Microsoft.Xbox.TCUI';
+ 'Microsoft.XboxApp';
+ 'Microsoft.XboxGameOverlay';
+ 'Microsoft.XboxGamingOverlay';
+ 'Microsoft.XboxIdentityProvider';
+ 'Microsoft.XboxSpeechToTextOverlay';
+ 'Microsoft.GamingApp';
+ 'Microsoft.YourPhone';
+ 'Microsoft.ZuneMusic';
+ 'Microsoft.ZuneVideo';
+);
+$getCommand = {
+ Get-AppxProvisionedPackage -Online;
+};
+$filterCommand = {
+ $_.DisplayName -eq $selector;
+};
+$removeCommand = {
+ [CmdletBinding()]
+ param(
+ [Parameter( Mandatory, ValueFromPipeline )]
+ $InputObject
+ );
+ process {
+ $InputObject | Remove-AppxProvisionedPackage -AllUsers -Online -ErrorAction 'Continue';
+ }
+};
+$type = 'Package';
+$logfile = 'C:\Windows\Setup\Scripts\RemovePackages.log';
+& {
+ $installed = & $getCommand;
+ foreach( $selector in $selectors ) {
+ $result = [ordered] @{
+ Selector = $selector;
+ };
+ $found = $installed | Where-Object -FilterScript $filterCommand;
+ if( $found ) {
+ $result.Output = $found | & $removeCommand;
+ if( $? ) {
+ $result.Message = "$type removed.";
+ } else {
+ $result.Message = "$type not removed.";
+ $result.Error = $Error[0];
+ }
+ } else {
+ $result.Message = "$type not installed.";
+ }
+ $result | ConvertTo-Json -Depth 3 -Compress;
+ }
+} *>&1 | Out-String -Width 1KB -Stream >> $logfile;
+
+
+$selectors = @(
+ 'Print.Fax.Scan';
+ 'Language.Handwriting';
+ 'Browser.InternetExplorer';
+ 'MathRecognizer';
+ 'OneCoreUAP.OneSync';
+ 'Microsoft.Windows.MSPaint';
+ 'App.Support.QuickAssist';
+ 'Microsoft.Windows.SnippingTool';
+ 'Language.Speech';
+ 'Language.TextToSpeech';
+ 'App.StepsRecorder';
+ 'Hello.Face.18967';
+ 'Hello.Face.Migration.18967';
+ 'Hello.Face.20134';
+ 'Media.WindowsMediaPlayer';
+ 'Microsoft.Windows.WordPad';
+);
+$getCommand = {
+ Get-WindowsCapability -Online | Where-Object -Property 'State' -NotIn -Value @(
+ 'NotPresent';
+ 'Removed';
+ );
+};
+$filterCommand = {
+ ($_.Name -split '~')[0] -eq $selector;
+};
+$removeCommand = {
+ [CmdletBinding()]
+ param(
+ [Parameter( Mandatory, ValueFromPipeline )]
+ $InputObject
+ );
+ process {
+ $InputObject | Remove-WindowsCapability -Online -ErrorAction 'Continue';
+ }
+};
+$type = 'Capability';
+$logfile = 'C:\Windows\Setup\Scripts\RemoveCapabilities.log';
+& {
+ $installed = & $getCommand;
+ foreach( $selector in $selectors ) {
+ $result = [ordered] @{
+ Selector = $selector;
+ };
+ $found = $installed | Where-Object -FilterScript $filterCommand;
+ if( $found ) {
+ $result.Output = $found | & $removeCommand;
+ if( $? ) {
+ $result.Message = "$type removed.";
+ } else {
+ $result.Message = "$type not removed.";
+ $result.Error = $Error[0];
+ }
+ } else {
+ $result.Message = "$type not installed.";
+ }
+ $result | ConvertTo-Json -Depth 3 -Compress;
+ }
+} *>&1 | Out-String -Width 1KB -Stream >> $logfile;
+
+
+$selectors = @(
+ 'MediaPlayback';
+ 'Microsoft-RemoteDesktopConnection';
+ 'Recall';
+ 'Microsoft-SnippingTool';
+);
+$getCommand = {
+ Get-WindowsOptionalFeature -Online | Where-Object -Property 'State' -NotIn -Value @(
+ 'Disabled';
+ 'DisabledWithPayloadRemoved';
+ );
+};
+$filterCommand = {
+ $_.FeatureName -eq $selector;
+};
+$removeCommand = {
+ [CmdletBinding()]
+ param(
+ [Parameter( Mandatory, ValueFromPipeline )]
+ $InputObject
+ );
+ process {
+ $InputObject | Disable-WindowsOptionalFeature -Online -Remove -NoRestart -ErrorAction 'Continue';
+ }
+};
+$type = 'Feature';
+$logfile = 'C:\Windows\Setup\Scripts\RemoveFeatures.log';
+& {
+ $installed = & $getCommand;
+ foreach( $selector in $selectors ) {
+ $result = [ordered] @{
+ Selector = $selector;
+ };
+ $found = $installed | Where-Object -FilterScript $filterCommand;
+ if( $found ) {
+ $result.Output = $found | & $removeCommand;
+ if( $? ) {
+ $result.Message = "$type removed.";
+ } else {
+ $result.Message = "$type not removed.";
+ $result.Error = $Error[0];
+ }
+ } else {
+ $result.Message = "$type not installed.";
+ }
+ $result | ConvertTo-Json -Depth 3 -Compress;
+ }
+} *>&1 | Out-String -Width 1KB -Stream >> $logfile;
+
+
+<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
+ <Triggers>
+ <BootTrigger>
+ <Repetition>
+ <Interval>P1D</Interval>
+ <StopAtDurationEnd>false</StopAtDurationEnd>
+ </Repetition>
+ <Enabled>true</Enabled>
+ </BootTrigger>
+ </Triggers>
+ <Principals>
+ <Principal id="Author">
+ <UserId>S-1-5-19</UserId>
+ <RunLevel>LeastPrivilege</RunLevel>
+ </Principal>
+ </Principals>
+ <Settings>
+ <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
+ <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
+ <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
+ <AllowHardTerminate>true</AllowHardTerminate>
+ <StartWhenAvailable>false</StartWhenAvailable>
+ <RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
+ <IdleSettings>
+ <StopOnIdleEnd>true</StopOnIdleEnd>
+ <RestartOnIdle>false</RestartOnIdle>
+ </IdleSettings>
+ <AllowStartOnDemand>true</AllowStartOnDemand>
+ <Enabled>true</Enabled>
+ <Hidden>false</Hidden>
+ <RunOnlyIfIdle>false</RunOnlyIfIdle>
+ <WakeToRun>false</WakeToRun>
+ <ExecutionTimeLimit>PT72H</ExecutionTimeLimit>
+ <Priority>7</Priority>
+ </Settings>
+ <Actions Context="Author">
+ <Exec>
+ <Command>C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe</Command>
+ <Arguments>-WindowStyle Hidden -NoProfile -NonInteractive -Command "$format = 'yyyy-MM-ddTHH\:mm\:ssK'; $now = [datetime]::UtcNow; $start = $now.ToString($format); $end = $now.AddDays(7).ToString($format); $params = @{ LiteralPath = 'Registry::HKLM\Software\Microsoft\WindowsUpdate\UX\Settings'; Type = 'String'; Force = $true; Verbose = $true; }; 'PauseFeatureUpdatesStartTime', 'PauseQualityUpdatesStartTime', 'PauseUpdatesStartTime' | foreach { Set-ItemProperty @params -Name $_ -Value $start; }; 'PauseFeatureUpdatesEndTime', 'PauseQualityUpdatesEndTime', 'PauseUpdatesExpiryTime' | foreach { Set-ItemProperty @params -Name $_ -Value $end; };"</Arguments>
+ </Exec>
+ </Actions>
+</Task>
+
+
+<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
+ <Triggers>
+ <BootTrigger>
+ <Repetition>
+ <Interval>PT4H</Interval>
+ <StopAtDurationEnd>false</StopAtDurationEnd>
+ </Repetition>
+ <Enabled>true</Enabled>
+ </BootTrigger>
+ <RegistrationTrigger>
+ <Repetition>
+ <Interval>PT4H</Interval>
+ <StopAtDurationEnd>false</StopAtDurationEnd>
+ </Repetition>
+ <Enabled>true</Enabled>
+ </RegistrationTrigger>
+ </Triggers>
+ <Principals>
+ <Principal id="Author">
+ <UserId>S-1-5-19</UserId>
+ <RunLevel>LeastPrivilege</RunLevel>
+ </Principal>
+ </Principals>
+ <Settings>
+ <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
+ <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
+ <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
+ <AllowHardTerminate>true</AllowHardTerminate>
+ <StartWhenAvailable>false</StartWhenAvailable>
+ <RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
+ <IdleSettings>
+ <StopOnIdleEnd>true</StopOnIdleEnd>
+ <RestartOnIdle>false</RestartOnIdle>
+ </IdleSettings>
+ <AllowStartOnDemand>true</AllowStartOnDemand>
+ <Enabled>true</Enabled>
+ <Hidden>false</Hidden>
+ <RunOnlyIfIdle>false</RunOnlyIfIdle>
+ <WakeToRun>false</WakeToRun>
+ <ExecutionTimeLimit>PT72H</ExecutionTimeLimit>
+ <Priority>7</Priority>
+ </Settings>
+ <Actions Context="Author">
+ <Exec>
+ <Command>%windir%\System32\conhost.exe</Command>
+ <Arguments>--headless %windir%\System32\WindowsPowerShell\v1.0\powershell.exe -WindowStyle Hidden -NoProfile -NonInteractive -Command "$p = @{ LiteralPath = 'Registry::HKLM\Software\Microsoft\WindowsUpdate\UX\Settings'; Type = 'DWord'; }; $h = [datetime]::Now.Hour; Set-ItemProperty @p -Name 'ActiveHoursStart' -Value (($h + 23) % 24); Set-ItemProperty @p -Name 'ActiveHoursEnd' -Value (($h + 11) % 24); Set-ItemProperty @p -Name 'SmartActiveHoursState' -Value 0;"</Arguments>
+ </Exec>
+ </Actions>
+</Task>
+
+
+$excludes = Get-ChildItem -LiteralPath 'Registry::HKU\DefaultUser\AppEvents\EventLabels' |
+ Where-Object -FilterScript { ($_ | Get-ItemProperty).ExcludeFromCPL -eq 1; } |
+ Select-Object -ExpandProperty 'PSChildName';
+Get-ChildItem -Path 'Registry::HKU\DefaultUser\AppEvents\Schemes\Apps\*\*' |
+ Where-Object -Property 'PSChildName' -NotIn $excludes |
+ Get-ChildItem -Include '.Current' | Set-ItemProperty -Name '(Default)' -Value '';
+
+
+$json = '{"pinnedList":[]}';
+if( [System.Environment]::OSVersion.Version.Build -lt 20000 ) {
+ return;
+}
+$key = 'Registry::HKLM\SOFTWARE\Microsoft\PolicyManager\current\device\Start';
+New-Item -Path $key -ItemType 'Directory' -ErrorAction 'SilentlyContinue';
+Set-ItemProperty -LiteralPath $key -Name 'ConfigureStartPins' -Value $json -Type 'String';
+
+
+<LayoutModificationTemplate Version="1" xmlns="http://schemas.microsoft.com/Start/2014/LayoutModification">
+ <LayoutOptions StartTileGroupCellWidth="6" />
+ <DefaultLayoutOverride>
+ <StartLayoutCollection>
+ <StartLayout GroupCellWidth="6" xmlns="http://schemas.microsoft.com/Start/2014/FullDefaultLayout" />
+ </StartLayoutCollection>
+ </DefaultLayoutOverride>
+</LayoutModificationTemplate>
+
+
+Add-Type -TypeDefinition '
+ using System.Drawing;
+ using System.Runtime.InteropServices;
+
+ public static class WallpaperSetter {
+ [DllImport("user32.dll")]
+ private static extern bool SetSysColors(
+ int cElements,
+ int[] lpaElements,
+ int[] lpaRgbValues
+ );
+
+ [DllImport("user32.dll")]
+ private static extern bool SystemParametersInfo(
+ uint uiAction,
+ uint uiParam,
+ string pvParam,
+ uint fWinIni
+ );
+
+ public static void SetDesktopBackground(Color color) {
+ SystemParametersInfo(20, 0, "", 0);
+ SetSysColors(1, new int[] { 1 }, new int[] { ColorTranslator.ToWin32(color) });
+ }
+
+ public static void SetDesktopImage(string file) {
+ SystemParametersInfo(20, 0, file, 0);
+ }
+ }
+' -ReferencedAssemblies 'System.Drawing';
+
+function Set-WallpaperColor {
+ param(
+ [string]
+ $HtmlColor
+ );
+
+ $color = [System.Drawing.ColorTranslator]::FromHtml( $HtmlColor );
+ [WallpaperSetter]::SetDesktopBackground( $color );
+ Set-ItemProperty -Path 'Registry::HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Wallpapers' -Name 'BackgroundType' -Type 'DWord' -Value 1 -Force;
+ Set-ItemProperty -Path 'Registry::HKCU\Control Panel\Desktop' -Name 'WallPaper' -Type 'String' -Value '' -Force;
+ Set-ItemProperty -Path 'Registry::HKCU\Control Panel\Colors' -Name 'Background' -Type 'String' -Value "$($color.R) $($color.G) $($color.B)" -Force;
+}
+
+function Set-WallpaperImage {
+ param(
+ [string]
+ $LiteralPath
+ );
+
+ if( $LiteralPath | Test-Path ) {
+ [WallpaperSetter]::SetDesktopImage( $LiteralPath );
+ Set-ItemProperty -Path 'Registry::HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Wallpapers' -Name 'BackgroundType' -Type 'DWord' -Value 0 -Force;
+ Set-ItemProperty -Path 'Registry::HKCU\Control Panel\Desktop' -Name 'WallPaper' -Type 'String' -Value $LiteralPath -Force;
+ } else {
+ "Cannot use '$LiteralPath' as a desktop wallpaper because that file does not exist.";
+ }
+}
+Set-WallpaperColor -HtmlColor '#008080';
+
+
+$scripts = @(
+ {
+ reg.exe add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\OOBE" /v BypassNRO /t REG_DWORD /d 1 /f;
+ };
+ {
+ Remove-Item -LiteralPath 'Registry::HKLM\Software\Microsoft\WindowsUpdate\Orchestrator\UScheduler_Oobe\DevHomeUpdate' -Force -ErrorAction 'SilentlyContinue';
+ };
+ {
+ Remove-Item -LiteralPath 'C:\Users\Default\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\OneDrive.lnk', 'C:\Windows\System32\OneDriveSetup.exe', 'C:\Windows\SysWOW64\OneDriveSetup.exe' -ErrorAction 'Continue';
+ };
+ {
+ Remove-Item -LiteralPath 'Registry::HKLM\Software\Microsoft\WindowsUpdate\Orchestrator\UScheduler_Oobe\OutlookUpdate' -Force -ErrorAction 'SilentlyContinue';
+ };
+ {
+ reg.exe add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Communications" /v ConfigureChatAutoInstall /t REG_DWORD /d 0 /f;
+ };
+ {
+ & 'C:\Windows\Setup\Scripts\RemovePackages.ps1';
+ };
+ {
+ & 'C:\Windows\Setup\Scripts\RemoveCapabilities.ps1';
+ };
+ {
+ & 'C:\Windows\Setup\Scripts\RemoveFeatures.ps1';
+ };
+ {
+ net.exe accounts /lockoutthreshold:0;
+ };
+ {
+ net.exe accounts /maxpwage:UNLIMITED;
+ };
+ {
+ Register-ScheduledTask -TaskName 'PauseWindowsUpdate' -Xml $( Get-Content -LiteralPath 'C:\Windows\Setup\Scripts\PauseWindowsUpdate.xml' -Raw );
+ };
+ {
+ reg.exe add "HKLM\SOFTWARE\Policies\Microsoft\Windows Defender Security Center\Notifications" /v DisableNotifications /t REG_DWORD /d 1 /f;
+ };
+ {
+ reg.exe add "HKLM\SYSTEM\CurrentControlSet\Control\CI\Policy" /v VerifiedAndReputablePolicyState /t REG_DWORD /d 0 /f;
+ };
+ {
+ reg.exe add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer" /v SmartScreenEnabled /t REG_SZ /d "Off" /f;
+ reg.exe add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\WTDS\Components" /v ServiceEnabled /t REG_DWORD /d 0 /f;
+ reg.exe add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\WTDS\Components" /v NotifyMalicious /t REG_DWORD /d 0 /f;
+ reg.exe add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\WTDS\Components" /v NotifyPasswordReuse /t REG_DWORD /d 0 /f;
+ reg.exe add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\WTDS\Components" /v NotifyUnsafeApp /t REG_DWORD /d 0 /f;
+ reg.exe add "HKLM\SOFTWARE\Policies\Microsoft\Windows Defender Security Center\Systray" /v HideSystray /t REG_DWORD /d 1 /f;
+ };
+ {
+ reg.exe add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" /v EnableLUA /t REG_DWORD /d 0 /f
+ };
+ {
+ reg.exe add "HKLM\SYSTEM\CurrentControlSet\Control\FileSystem" /v LongPathsEnabled /t REG_DWORD /d 1 /f
+ };
+ {
+ Set-ExecutionPolicy -Scope 'LocalMachine' -ExecutionPolicy 'RemoteSigned' -Force;
+ };
+ {
+ fsutil.exe behavior set disableLastAccess 1;
+ };
+ {
+ reg.exe add "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /v AUOptions /t REG_DWORD /d 4 /f;
+ reg.exe add "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /v NoAutoRebootWithLoggedOnUsers /t REG_DWORD /d 1 /f;
+ };
+ {
+ Register-ScheduledTask -TaskName 'MoveActiveHours' -Xml $( Get-Content -LiteralPath 'C:\Windows\Setup\Scripts\MoveActiveHours.xml' -Raw );
+ };
+ {
+ reg.exe add "HKLM\SOFTWARE\Policies\Microsoft\Dsh" /v AllowNewsAndInterests /t REG_DWORD /d 0 /f;
+ };
+ {
+ reg.exe add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\LogonUI\BootAnimation" /v DisableStartupSound /t REG_DWORD /d 1 /f;
+ reg.exe add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\EditionOverrides" /v UserSetting_DisableStartupSound /t REG_DWORD /d 1 /f;
+ };
+ {
+ reg.exe add "HKLM\Software\Policies\Microsoft\Windows\CloudContent" /v "DisableWindowsConsumerFeatures" /t REG_DWORD /d 1 /f;
+ };
+ {
+ reg.exe add "HKLM\SYSTEM\CurrentControlSet\Control\BitLocker" /v "PreventDeviceEncryption" /t REG_DWORD /d 1 /f;
+ };
+ {
+ reg.exe add "HKLM\Software\Policies\Microsoft\Edge" /v HideFirstRunExperience /t REG_DWORD /d 1 /f;
+ };
+ {
+ reg.exe add "HKLM\Software\Policies\Microsoft\Edge\Recommended" /v BackgroundModeEnabled /t REG_DWORD /d 0 /f;
+ reg.exe add "HKLM\Software\Policies\Microsoft\Edge\Recommended" /v StartupBoostEnabled /t REG_DWORD /d 0 /f;
+ };
+ {
+ & 'C:\Windows\Setup\Scripts\SetStartPins.ps1';
+ };
+ {
+ Set-ItemProperty -LiteralPath "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VisualEffects\ControlAnimations" -Name 'DefaultValue' -Value 0 -Type 'DWord' -Force;
+ Set-ItemProperty -LiteralPath "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VisualEffects\AnimateMinMax" -Name 'DefaultValue' -Value 0 -Type 'DWord' -Force;
+ Set-ItemProperty -LiteralPath "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VisualEffects\TaskbarAnimations" -Name 'DefaultValue' -Value 0 -Type 'DWord' -Force;
+ Set-ItemProperty -LiteralPath "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VisualEffects\DWMAeroPeekEnabled" -Name 'DefaultValue' -Value 0 -Type 'DWord' -Force;
+ Set-ItemProperty -LiteralPath "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VisualEffects\MenuAnimation" -Name 'DefaultValue' -Value 0 -Type 'DWord' -Force;
+ Set-ItemProperty -LiteralPath "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VisualEffects\TooltipAnimation" -Name 'DefaultValue' -Value 0 -Type 'DWord' -Force;
+ Set-ItemProperty -LiteralPath "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VisualEffects\SelectionFade" -Name 'DefaultValue' -Value 0 -Type 'DWord' -Force;
+ Set-ItemProperty -LiteralPath "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VisualEffects\DWMSaveThumbnailEnabled" -Name 'DefaultValue' -Value 0 -Type 'DWord' -Force;
+ Set-ItemProperty -LiteralPath "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VisualEffects\CursorShadow" -Name 'DefaultValue' -Value 0 -Type 'DWord' -Force;
+ Set-ItemProperty -LiteralPath "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VisualEffects\ListviewShadow" -Name 'DefaultValue' -Value 0 -Type 'DWord' -Force;
+ Set-ItemProperty -LiteralPath "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VisualEffects\ThumbnailsOrIcon" -Name 'DefaultValue' -Value 0 -Type 'DWord' -Force;
+ Set-ItemProperty -LiteralPath "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VisualEffects\ListviewAlphaSelect" -Name 'DefaultValue' -Value 0 -Type 'DWord' -Force;
+ Set-ItemProperty -LiteralPath "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VisualEffects\DragFullWindows" -Name 'DefaultValue' -Value 0 -Type 'DWord' -Force;
+ Set-ItemProperty -LiteralPath "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VisualEffects\ComboBoxAnimation" -Name 'DefaultValue' -Value 0 -Type 'DWord' -Force;
+ Set-ItemProperty -LiteralPath "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VisualEffects\FontSmoothing" -Name 'DefaultValue' -Value 0 -Type 'DWord' -Force;
+ Set-ItemProperty -LiteralPath "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VisualEffects\ListBoxSmoothScrolling" -Name 'DefaultValue' -Value 0 -Type 'DWord' -Force;
+ Set-ItemProperty -LiteralPath "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VisualEffects\DropShadow" -Name 'DefaultValue' -Value 0 -Type 'DWord' -Force;
+ };
+ {
+ reg.exe add "HKU\.DEFAULT\Control Panel\Accessibility\StickyKeys" /v Flags /t REG_SZ /d 10 /f;
+ };
+ {
+ reg.exe add "HKLM\Software\Microsoft\Windows\CurrentVersion\Policies\System" /v "DisableAutomaticRestartSignOn" /t REG_DWORD /d 1 /f;
+ };
+);
+
+& {
+ [float] $complete = 0;
+ [float] $increment = 100 / $scripts.Count;
+ foreach( $script in $scripts ) {
+ Write-Progress -Id 0 -Activity 'Running scripts to customize your Windows installation. Do not close this window.' -PercentComplete $complete;
+ '*** Will now execute command «{0}».' -f $(
+ $script.ToString().Trim() -replace '\s+', ' ' -replace '^(.{99})(.+)$', '$1…';
+ );
+ $start = [datetime]::Now;
+ & $script;
+ '*** Finished executing command after {0:0} ms.' -f [datetime]::Now.Subtract( $start ).TotalMilliseconds;
+ "`r`n" * 3;
+ $complete += $increment;
+ }
+} *>&1 | Out-String -Width 1KB -Stream >> "C:\Windows\Setup\Scripts\Specialize.log";
+
+
+$scripts = @(
+ {
+ Get-AppxPackage -Name 'Microsoft.Windows.Ai.Copilot.Provider' | Remove-AppxPackage;
+ };
+ {
+ @(
+ Get-ChildItem -LiteralPath $env:USERPROFILE -Force -Recurse -Depth 2;
+ ) | Where-Object -FilterScript {
+ $_.Attributes.HasFlag( [System.IO.FileAttributes]::ReparsePoint );
+ } | Remove-Item -Force -Recurse -Verbose;
+ };
+ {
+ Set-ItemProperty -LiteralPath 'Registry::HKCU\AppEvents\Schemes' -Name '(Default)' -Type 'String' -Value '.None';
+ };
+ {
+ Set-ItemProperty -LiteralPath 'Registry::HKCU\Software\Microsoft\Windows\CurrentVersion\Search' -Name 'SearchboxTaskbarMode' -Type 'DWord' -Value 0;
+ };
+ {
+ Set-ItemProperty -LiteralPath 'Registry::HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\VisualEffects' -Name 'VisualFXSetting' -Type 'DWord' -Value 2 -Force;
+ };
+ {
+ & 'C:\Windows\Setup\Scripts\SetWallpaper.ps1';
+ };
+ {
+ Get-Process -Name 'explorer' -ErrorAction 'SilentlyContinue' | Where-Object -FilterScript {
+ $_.SessionId -eq ( Get-Process -Id $PID ).SessionId;
+ } | Stop-Process -Force;
+ };
+ {
+ # Custom PS script to shut the system down after competing the user account setup
+ & 'C:\Windows\Setup\Scripts\FinishUserSetupAndShutdown.ps1';
+ };
+);
+
+& {
+ [float] $complete = 0;
+ [float] $increment = 100 / $scripts.Count;
+ foreach( $script in $scripts ) {
+ Write-Progress -Id 0 -Activity 'Running scripts to configure this user account. Do not close this window.' -PercentComplete $complete;
+ '*** Will now execute command «{0}».' -f $(
+ $script.ToString().Trim() -replace '\s+', ' ' -replace '^(.{99})(.+)$', '$1…';
+ );
+ $start = [datetime]::Now;
+ & $script;
+ '*** Finished executing command after {0:0} ms.' -f [datetime]::Now.Subtract( $start ).TotalMilliseconds;
+ "`r`n" * 3;
+ $complete += $increment;
+ }
+} *>&1 | Out-String -Width 1KB -Stream >> "$env:TEMP\UserOnce.log";
+
+
+$scripts = @(
+ {
+ reg.exe add "HKU\DefaultUser\Software\Policies\Microsoft\Windows\WindowsCopilot" /v TurnOffWindowsCopilot /t REG_DWORD /d 1 /f;
+ };
+ {
+ reg.exe add "HKU\DefaultUser\Software\Microsoft\Internet Explorer\LowRegistry\Audio\PolicyConfig\PropertyStore" /f;
+ };
+ {
+ Remove-ItemProperty -LiteralPath 'Registry::HKU\DefaultUser\Software\Microsoft\Windows\CurrentVersion\Run' -Name 'OneDriveSetup' -Force -ErrorAction 'Continue';
+ };
+ {
+ reg.exe add "HKU\DefaultUser\Software\Microsoft\Windows\CurrentVersion\GameDVR" /v AppCaptureEnabled /t REG_DWORD /d 0 /f;
+ };
+ {
+ reg.exe add "HKU\DefaultUser\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced" /v "HideFileExt" /t REG_DWORD /d 0 /f;
+ };
+ {
+ reg.exe add "HKU\DefaultUser\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced" /v "Hidden" /t REG_DWORD /d 1 /f;
+ };
+ {
+ reg.exe add "HKU\DefaultUser\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced" /v "ShowSuperHidden" /t REG_DWORD /d 0 /f;
+ };
+ {
+ reg.exe add "HKU\DefaultUser\Software\Microsoft\Edge\SmartScreenEnabled" /ve /t REG_DWORD /d 0 /f;
+ reg.exe add "HKU\DefaultUser\Software\Microsoft\Edge\SmartScreenPuaEnabled" /ve /t REG_DWORD /d 0 /f;
+ reg.exe add "HKU\DefaultUser\Software\Microsoft\Windows\CurrentVersion\AppHost" /v EnableWebContentEvaluation /t REG_DWORD /d 0 /f;
+ reg.exe add "HKU\DefaultUser\Software\Microsoft\Windows\CurrentVersion\AppHost" /v PreventOverride /t REG_DWORD /d 0 /f;
+ };
+ {
+ & 'C:\Windows\Setup\Scripts\TurnOffSystemSounds.ps1';
+ };
+ {
+ $names = @(
+ 'ContentDeliveryAllowed';
+ 'FeatureManagementEnabled';
+ 'OEMPreInstalledAppsEnabled';
+ 'PreInstalledAppsEnabled';
+ 'PreInstalledAppsEverEnabled';
+ 'SilentInstalledAppsEnabled';
+ 'SoftLandingEnabled';
+ 'SubscribedContentEnabled';
+ 'SubscribedContent-310093Enabled';
+ 'SubscribedContent-338387Enabled';
+ 'SubscribedContent-338388Enabled';
+ 'SubscribedContent-338389Enabled';
+ 'SubscribedContent-338393Enabled';
+ 'SubscribedContent-353694Enabled';
+ 'SubscribedContent-353696Enabled';
+ 'SubscribedContent-353698Enabled';
+ 'SystemPaneSuggestionsEnabled';
+ );
+
+ foreach( $name in $names ) {
+ reg.exe add "HKU\DefaultUser\Software\Microsoft\Windows\CurrentVersion\ContentDeliveryManager" /v $name /t REG_DWORD /d 0 /f;
+ }
+ };
+ {
+ reg.exe add "HKU\DefaultUser\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced" /v TaskbarAl /t REG_DWORD /d 0 /f;
+ };
+ {
+ reg.exe add "HKU\DefaultUser\Software\Policies\Microsoft\Windows\Explorer" /v DisableSearchBoxSuggestions /t REG_DWORD /d 1 /f;
+ };
+ {
+ reg.exe add "HKU\DefaultUser\Control Panel\Accessibility\StickyKeys" /v Flags /t REG_SZ /d 10 /f;
+ };
+ {
+ reg.exe add "HKU\DefaultUser\Software\Microsoft\Windows\CurrentVersion\RunOnce" /v "UnattendedSetup" /t REG_SZ /d "powershell.exe -WindowStyle \""Normal\"" -ExecutionPolicy \""Unrestricted\"" -NoProfile -File \""C:\Windows\Setup\Scripts\UserOnce.ps1\""" /f;
+ };
+);
+
+& {
+ [float] $complete = 0;
+ [float] $increment = 100 / $scripts.Count;
+ foreach( $script in $scripts ) {
+ Write-Progress -Id 0 -Activity 'Running scripts to modify the default user’’s registry hive. Do not close this window.' -PercentComplete $complete;
+ '*** Will now execute command «{0}».' -f $(
+ $script.ToString().Trim() -replace '\s+', ' ' -replace '^(.{99})(.+)$', '$1…';
+ );
+ $start = [datetime]::Now;
+ & $script;
+ '*** Finished executing command after {0:0} ms.' -f [datetime]::Now.Subtract( $start ).TotalMilliseconds;
+ "`r`n" * 3;
+ $complete += $increment;
+ }
+} *>&1 | Out-String -Width 1KB -Stream >> "C:\Windows\Setup\Scripts\DefaultUser.log";
+
+
+$scripts = @(
+ {
+ # Set-ItemProperty -LiteralPath 'Registry::HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon' -Name 'AutoLogonCount' -Type 'DWord' -Force -Value 0;
+ };
+ {
+ @(
+ Get-ChildItem -LiteralPath 'C:\' -Force;
+ Get-ChildItem -LiteralPath 'C:\Users' -Force;
+ Get-ChildItem -LiteralPath 'C:\Users\Default' -Force -Recurse -Depth 2;
+ Get-ChildItem -LiteralPath 'C:\Users\Public' -Force -Recurse -Depth 2;
+ Get-ChildItem -LiteralPath 'C:\ProgramData' -Force;
+ ) | Where-Object -FilterScript {
+ $_.Attributes.HasFlag( [System.IO.FileAttributes]::ReparsePoint );
+ } | Remove-Item -Force -Recurse -Verbose;
+ };
+ {
+ Disable-ComputerRestore -Drive 'C:\';
+ };
+ {
+ cmd.exe /c "rmdir C:\Windows.old";
+ };
+ {
+ Set-Service -Name WSearch -StartupType 'Disabled' -Status 'Stopped' `
+ -ErrorAction 'SilentlyContinue';
+ };
+ {
+ $keepList = @( 'autounattend.xml', 'drivers' );
+ Get-ChildItem -Path "${env:WINDIR}\ConfigSetRoot" | Where-Object { $_.Name -notin $keepList } | ForEach-Object {
+ Write-Output "Removing non-essential file/folder from ConfigSetRoot: $($_.Name)"
+ Remove-Item -Path $_.FullName -Recurse -Force
+ }
+ };
+ {
+ & "$env:WINDIR\Setup\Scripts\WinRM.ps1";
+ };
+ {
+ & "$env:WINDIR\Setup\Scripts\WinFSP.ps1";
+ };
+ {
+ & "$env:WINDIR\Setup\Scripts\OpenSSH.ps1";
+ };
+ {
+ # & "$env:WINDIR\Setup\Scripts\InstallChocolatey.ps1";
+ };
+ {
+ & "$env:WINDIR\Setup\Scripts\InstallPython.ps1";
+ };
+ {
+ & "$env:WINDIR\Setup\Scripts\InstallNodeJS.ps1";
+ };
+ {
+ & "$env:WINDIR\Setup\Scripts\InstallYarn1.ps1";
+ };
+ {
+ & "$env:WINDIR\Setup\Scripts\InstallGit.ps1";
+ };
+ {
+ & "$env:WINDIR\Setup\Scripts\InstallVisualStudio.ps1";
+ };
+ {
+ & "$env:WINDIR\Setup\Scripts\InstallWindowsSDK.ps1";
+ };
+ {
+ & "$env:WINDIR\Setup\Scripts\InstallRust.ps1";
+ };
+ {
+ & "$env:WINDIR\Setup\Scripts\InstallMesa3dOpenGL.ps1";
+ };
+ {
+ Remove-Item -LiteralPath @(
+ 'C:\Windows\Panther\unattend.xml';
+ 'C:\Windows\Panther\unattend-original.xml';
+ 'C:\Windows\Setup\Scripts\Wifi.xml';
+ ) -Force -ErrorAction 'SilentlyContinue' -Verbose;
+ };
+ {
+ Remove-Item -LiteralPath @(
+ Get-ChildItem -LiteralPath $(Join-Path -Path $env:WINDIR -ChildPath 'Temp') -Force;
+ Get-ChildItem -LiteralPath $(Join-Path -Path $env:LOCALAPPDATA -ChildPath 'Temp') -Force;
+ ) -Force -ErrorAction 'SilentlyContinue' -Verbose;
+ };
+ {
+ powercfg.exe /hibernate off;
+ powercfg.exe /change standby-timeout-dc 0;
+ powercfg.exe /change standby-timeout-ac 0;
+ powercfg.exe /change hibernate-timeout-dc 0;
+ powercfg.exe /change hibernate-timeout-ac 0;
+ };
+ {
+ & "$env:WINDIR\Setup\Scripts\FinishSystemSetupAndReboot.ps1";
+ };
+);
+
+& {
+ [float] $complete = 0;
+ [float] $increment = 100 / $scripts.Count;
+ foreach( $script in $scripts ) {
+ Write-Progress -Id 0 -Activity 'Running scripts to finalize your Windows installation. Do not close this window.' -PercentComplete $complete;
+ '*** Will now execute command «{0}».' -f $(
+ $script.ToString().Trim() -replace '\s+', ' ' -replace '^(.{99})(.+)$', '$1…';
+ );
+ $start = [datetime]::Now;
+ & $script;
+ '*** Finished executing command after {0:0} ms.' -f [datetime]::Now.Subtract( $start ).TotalMilliseconds;
+ "`r`n" * 3;
+ $complete += $increment;
+ }
+} *>&1 | Out-String -Width 1KB -Stream >> "C:\Windows\Setup\Scripts\FirstLogon.log";
+
+
+
+
+
+
+ # Prev: Unrestricted/LocalMachine
+ Set-ExecutionPolicy Unrestricted -Scope Process -Force -ErrorAction Ignore;
+
+ Write-Output "Running WinRM quickconfig setup...";
+ cmd.exe /c "winrm quickconfig -q -force";
+
+ Write-Output "Disabling WinRM over HTTP...";
+ # Scope: Public
+ Disable-NetFirewallRule -Name "WINRM-HTTP-In-TCP" | Out-Null;
+ # Scope: Domain,Private
+ Disable-NetFirewallRule -Name "WINRM-HTTP-In-TCP-NoScope" | Out-Null;
+ Get-ChildItem WSMan:\Localhost\listener -Force | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue | Out-Null;
+
+ Write-Output "Configuring WinRM for HTTPS...";
+ Set-Item -Path WSMan:\LocalHost\MaxTimeoutms -Value '1800000' -Force | Out-Null;
+ Set-Item -Path WSMan:\LocalHost\Shell\MaxMemoryPerShellMB -Value '1024' -Force | Out-Null;
+ Set-Item -Path WSMan:\LocalHost\Service\AllowUnencrypted -Value 'false' -Force | Out-Null;
+ Set-Item -Path WSMan:\LocalHost\Service\Auth\Basic -Value 'true' -Force | Out-Null;
+ Set-Item -Path WSMan:\LocalHost\Service\Auth\CredSSP -Value 'true' -Force | Out-Null;
+
+ New-NetFirewallRule -Name "WINRM-HTTPS-In-TCP" `
+ -DisplayName "Windows Remote Management (HTTPS-In)" `
+ -Description "Inbound rule for Windows Remote Management via WS-Management. [TCP 5986]" `
+ -Group "Windows Remote Management" `
+ -Program "System" `
+ -Protocol TCP `
+ -LocalPort "5986" `
+ -Action Allow `
+ -Profile Domain,Private | Out-Null;
+
+ New-NetFirewallRule -Name "WINRM-HTTPS-In-TCP-PUBLIC" `
+ -DisplayName "Windows Remote Management (HTTPS-In)" `
+ -Description "Inbound rule for Windows Remote Management via WS-Management. [TCP 5986]" `
+ -Group "Windows Remote Management" `
+ -Program "System" `
+ -Protocol TCP `
+ -LocalPort "5986" `
+ -Action Allow `
+ -Profile Public | Out-Null;
+
+ $Hostname = [System.Net.Dns]::GetHostByName((hostname)).HostName.ToUpper();
+ $Cert = New-SelfSignedCertificate -CertstoreLocation Cert:\LocalMachine\My -DnsName $Hostname;
+
+ New-Item -Path WSMan:\LocalHost\Listener -Address * -Transport HTTPS -Hostname $Hostname -CertificateThumbPrint $Cert.Thumbprint -Port "5986" -force | Out-Null;
+
+ Write-Output "Configuring WinRM service for automatic start...";
+ Set-Service -Name WinRM -StartupType Automatic | Out-Null;
+
+ Write-Output "Restarting WinRM Service...";
+ Restart-Service -Name WinRM -Force | Out-Null;
+
+
+
+
+ # Common defines
+ $cpu_arch = $env:PROCESSOR_ARCHITECTURE.ToLower();
+
+ #
+ # OpenSSH for Win32
+ #
+ $pkg_version = "10.0.0.0p2";
+ $pkg_file = "OpenSSH-${pkg_version}-${cpu_arch}.msi";
+ $pkg_url = & {
+ if ($cpu_arch -eq "amd64") {
+ "https://github.com/PowerShell/Win32-OpenSSH/releases/download/${pkg_version}-Preview/OpenSSH-Win64-v$($pkg_version -Replace 'p\d+$', '').msi";
+ } else {
+ "https://github.com/PowerShell/Win32-OpenSSH/releases/download/${pkg_version}-Preview/OpenSSH-ARM64-v$($pkg_version -Replace 'p\d+$', '').msi";
+ }
+ };
+
+ $pkg_config = (Get-Content -Raw "$env:WINDIR\Setup\Scripts\config.json" | ConvertFrom-JSON).Packages.OpenSSH;
+ if ($pkg_config.install -ne $true) {
+ Write-Output "Skipping OpenSSH ${pkg_version} installation";
+ exit;
+ }
+
+ if (Test-Path -Path "z:\${pkg_file}") {
+ Write-Output "Reading package '${pkg_file}' from cache...";
+ Copy-Item -Path "z:\${pkg_file}" -Destination "${env:TEMP}\${pkg_file}";
+ } else {
+ Write-Output "Fetching package from '${pkg_url}'...";
+ (New-Object System.Net.WebClient).DownloadFile($pkg_url, "${env:TEMP}\${pkg_file}");
+ if (Test-Path -Path "z:") { Copy-Item -Path "${env:TEMP}\${pkg_file}" -Destination "z:\${pkg_file}" -ErrorAction 'SilentlyContinue'; }
+ }
+
+ Write-Output "Installing OpenSSH ${pkg_version}...";
+ Start-Process "msiexec.exe" -ArgumentList "/i `"${env:TEMP}\${pkg_file}`" /log `"${env:WINDIR}\Setup\Scripts\OpenSSH.log`" /passive /norestart ALLUSERS=1" -Wait -PassThru | Out-Null;
+ Remove-Item -LiteralPath "${env:TEMP}\${pkg_file}" -Force -ErrorAction 'SilentlyContinue';
+
+ Write-Output "Installing OpenSSH Server...";
+ if (Test-Path "$env:ProgramFiles\OpenSSH\install-sshd.ps1") {
+ & "$env:ProgramFiles\OpenSSH\install-sshd.ps1";
+ } elseif (Test-Path "$env:ProgramFiles\OpenSSH-Win64\install-sshd.ps1") {
+ & "$env:ProgramFiles\OpenSSH-Win64\install-sshd.ps1";
+ } else {
+ Write-Output "No 'install-sshd.ps1' script found, skipping step";
+ }
+
+ Write-Output "Enabling OpenSSH Server...";
+ Set-Service -Name sshd -StartupType Automatic | Out-Null;
+
+ New-NetFirewallRule -Name "OpenSSH-SSH-In-TCP" `
+ -DisplayName "OpenSSH Server (SSH-In)" `
+ -Description "Inbound rule for OpenSSH Server connections. [TCP 22]" `
+ -Group "Windows Remote Management" `
+ -Protocol TCP `
+ -LocalPort "22" `
+ -Action Allow `
+ -Profile Domain,Private | Out-Null;
+
+ New-NetFirewallRule -Name "OpenSSH-SSH-In-TCP-PUBLIC" `
+ -DisplayName "OpenSSH Server (SSH-In)" `
+ -Description "Inbound rule for OpenSSH Server connections. [TCP 22]" `
+ -Group "Windows Remote Management" `
+ -Protocol TCP `
+ -LocalPort "22" `
+ -Action Allow `
+ -Profile Public | Out-Null;
+
+ try {
+ $userNames = & {
+ # Autoselection of first drive with autounattend.xml
+ $unattendXmlPath = @(Get-ChildItem function:[d-z]: -n | %{$_ + "\autounattend.xml"} | ?{Test-Path $_})[0];
+ $xpathSelector = "//ns:settings[@pass=`"oobeSystem`"]/ns:component[@name=`"Microsoft-Windows-Shell-Setup`"]/ns:UserAccounts/ns:LocalAccounts/*/ns:Group[text() = `"Users`"]/..";
+ Select-Xml -Path "$unattendXmlPath" -XPath "$xpathSelector/ns:Name" -Namespace @{ns='urn:schemas-microsoft-com:unattend'} | %{ $_.Node.InnerText };
+ };
+
+ $privileges = @(
+ 'SeBatchLogonRight',
+ 'SeRemoteShutdownPrivilege'
+ );
+
+ Write-Output "Granting provisioned users ($($usernames -join ', ')) additional privileges...";
+ secedit /export /areas user_rights /cfg "$env:TEMP\secpol.cfg" | Out-Null;
+
+ $userNames | ForEach-Object {
+ $account = New-Object -TypeName System.Security.Principal.NTAccount -ArgumentList "$_";
+ $accountSid = $account.Translate([System.Security.Principal.SecurityIdentifier]).Value;
+ if ([string]::IsNullOrEmpty($accountSid)) {
+ Write-Output "Failed to resolve SID for account '$_', skipping";
+ return;
+ }
+
+ $privileges | ForEach-Object {
+ (Get-Content "$env:TEMP\secpol.cfg") -replace "^$_ .+", "`$0,*$accountSid" | Out-File "$env:TEMP\secpol.cfg";
+ };
+ };
+
+ secedit /configure /db c:\windows\security\local.sdb /cfg "$env:TEMP\secpol.cfg" /areas user_rights /quiet;
+ Remove-Item -Path "$env:TEMP\secpol.cfg" -Force -Confirm:$false -ErrorAction SilentlyContinue;
+ } catch {
+ Write-Output "Failed to grant users shutdown permissions";
+ }
+
+
+
+
+ $pkg_config = (Get-Content -Raw "$env:WINDIR\Setup\Scripts\config.json" | ConvertFrom-JSON).Packages.Chocolatey;
+ if ($pkg_config.install -ne $true) {
+ Write-Output "Skipping Chocolatey installation";
+ exit;
+ }
+
+ Write-Output "Installing Chocolatey...";
+ Set-ExecutionPolicy Bypass -Scope Process -Force;
+ [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072;
+ iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'));
+
+
+
+
+ #
+ # Common defines
+ #
+ $cpu_arch = $env:PROCESSOR_ARCHITECTURE.ToLower();
+
+ #
+ # Git
+ #
+ $pkg_version = "26.1.1";
+ $pkg_file = "mesa3d-${pkg_version}-${cpu_arch}.7z";
+ $pkg_url = & {
+ if ($cpu_arch -eq "amd64") {
+ "https://github.com/pal1000/mesa-dist-win/releases/download/${pkg_version}/mesa3d-${pkg_version}-release-msvc.7z";
+ } else {
+ # TODO: Find source for this...
+ "https://github.com/pal1000/mesa-dist-win/releases/download/${pkg_version}/mesa3d-${pkg_version}-release-msvc.7z";
+ }
+ };
+
+ $pkg_config = (Get-Content -Raw "$env:WINDIR\Setup\Scripts\config.json" | ConvertFrom-JSON).Packages.Mesa3dOpenGL;
+ if ($pkg_config.install -ne $true) {
+ Write-Output "Skipping Mesa3d ${pkg_version} OpenGL installation";
+ exit;
+ }
+
+ if (Test-Path -Path "z:\${pkg_file}") {
+ Write-Output "Reading package '${pkg_file}' from cache...";
+ Copy-Item -Path "z:\${pkg_file}" -Destination "${env:TEMP}\${pkg_file}";
+ } else {
+ Write-Output "Fetching package from '${pkg_url}'...";
+ (New-Object System.Net.WebClient).DownloadFile($pkg_url, "${env:TEMP}\${pkg_file}");
+ if (Test-Path -Path "z:") { Copy-Item -Path "${env:TEMP}\${pkg_file}" -Destination "z:\${pkg_file}" -ErrorAction 'SilentlyContinue'; }
+ }
+
+ Write-Output "Unpacking Mesa3d ${pkg_version} OpenGL...";
+ New-Item -Path "${env:TEMP}\mesa3d" -Type 'Directory' -Force | Out-Null;
+ Start-Process "tar.exe" -ArgumentList @(
+ "-x -f `"${env:TEMP}\${pkg_file}`"",
+ "-C `"${env:TEMP}\mesa3d`""
+ ) -WindowStyle 'Hidden' -Wait -PassThru | Out-Null;
+ Remove-Item -LiteralPath "${env:TEMP}\${pkg_file}" -Force -ErrorAction 'SilentlyContinue';
+
+ Write-Output "Installing Mesa3d ${pkg_version} OpenGL...";
+ Start-Process -FilePath "${env:TEMP}\mesa3d\systemwidedeploy.cmd" -ArgumentList '1' -WindowStyle 'Hidden' -Wait -PassThru | Out-Null;
+ Remove-Item -LiteralPath "${env:TEMP}\mesa3d" -Force -Recurse -ErrorAction 'SilentlyContinue';
+
+
+
+
+ #
+ # Common defines
+ #
+ $cpu_arch = $env:PROCESSOR_ARCHITECTURE.ToLower();
+
+ #
+ # Git
+ #
+ $pkg_version = "2.54.0";
+ $pkg_file = "git-${pkg_version}-${cpu_arch}.exe";
+ $pkg_url = & {
+ if ($cpu_arch -eq "amd64") {
+ "https://github.com/git-for-windows/git/releases/download/v${pkg_version}.windows.1/Git-${pkg_version}-64-bit.exe";
+ } else {
+ "https://github.com/git-for-windows/git/releases/download/v${pkg_version}.windows.1/Git-${pkg_version}-${cpu_arch}.exe";
+ }
+ };
+
+ $pkg_config = (Get-Content -Raw "$env:WINDIR\Setup\Scripts\config.json" | ConvertFrom-JSON).Packages.Git;
+ if ($pkg_config.install -ne $true) {
+ Write-Output "Skipping Git ${pkg_version} installation";
+ exit;
+ }
+
+ if (Test-Path -Path "z:\${pkg_file}") {
+ Write-Output "Reading package '${pkg_file}' from cache...";
+ Copy-Item -Path "z:\${pkg_file}" -Destination "${env:TEMP}\${pkg_file}";
+ } else {
+ Write-Output "Fetching package from '${pkg_url}'...";
+ (New-Object System.Net.WebClient).DownloadFile($pkg_url, "${env:TEMP}\${pkg_file}");
+ if (Test-Path -Path "z:") { Copy-Item -Path "${env:TEMP}\${pkg_file}" -Destination "z:\${pkg_file}" -ErrorAction 'SilentlyContinue'; }
+ }
+
+ Write-Output "Installing Git ${pkg_version}...";
+ Start-Process -FilePath "${env:TEMP}\${pkg_file}" -ArgumentList `
+ "/ALLUSERS /VERYSILENT /NORESTART /NOCANCEL /SP- /CLOSEAPPLICATIONS /RESTARTAPPLICATIONS /COMPONENTS=`"icons,assoc,assoc_sh,windowsterminal`"", `
+ "/o:EditorOption=Nano", `
+ "/o:CurlOption=WinSSL", `
+ "/o:PathOption=CmdTools" `
+ -Wait -PassThru | Out-Null;
+
+ Remove-Item -LiteralPath "${env:TEMP}\${pkg_file}" -Force -ErrorAction 'SilentlyContinue';
+
+
+
+ # Common defines
+ $cpu_arch = $env:PROCESSOR_ARCHITECTURE.ToLower();
+
+ # Python
+ $pkg_version = "3.14.5";
+ $pkg_file = "python-${pkg_version}-${cpu_arch}.exe";
+ $pkg_url = "https://www.python.org/ftp/python/${pkg_version}/python-${pkg_version}-${cpu_arch}.exe";
+
+ $pkg_config = (Get-Content -Raw "$env:WINDIR\Setup\Scripts\config.json" | ConvertFrom-JSON).Packages.Python;
+ if ($pkg_config.install -ne $true) {
+ Write-Output "Skipping Python ${pkg_version} installation";
+ exit;
+ }
+
+ if (Test-Path -Path "z:\${pkg_file}") {
+ Write-Output "Reading package '${pkg_file}' from cache...";
+ Copy-Item -Path "z:\${pkg_file}" -Destination "${env:TEMP}\${pkg_file}";
+ } else {
+ Write-Output "Fetching package from '${pkg_url}'...";
+ (New-Object System.Net.WebClient).DownloadFile($pkg_url, "${env:TEMP}\${pkg_file}");
+ if (Test-Path -Path "z:") { Copy-Item -Path "${env:TEMP}\${pkg_file}" -Destination "z:\${pkg_file}" -ErrorAction 'SilentlyContinue'; }
+ }
+
+ Write-Output "Installing Python ${pkg_version}...";
+ Start-Process -FilePath "${env:TEMP}\${pkg_file}" -ArgumentList "/quiet PrependPath=1 InstallAllUsers=1" -Wait -PassThru | Out-Null;
+ Remove-Item -LiteralPath "${env:TEMP}\${pkg_file}" -Force -ErrorAction 'SilentlyContinue';
+
+
+
+ # Common defines
+ $cpu_arch = $env:PROCESSOR_ARCHITECTURE.ToLower();
+
+ # NodeJS (LTS)
+ $pkg_version = "24.15.0";
+ $pkg_file = "node-${pkg_version}-${cpu_arch}.msi";
+ $pkg_url = & {
+ if ($cpu_arch -eq "amd64") {
+ "https://nodejs.org/dist/v${pkg_version}/node-v${pkg_version}-x64.msi";
+ } else {
+ "https://nodejs.org/dist/v${pkg_version}/node-v${pkg_version}-${cpu_arch}.msi";
+ }
+ };
+
+ $pkg_config = (Get-Content -Raw "$env:WINDIR\Setup\Scripts\config.json" | ConvertFrom-JSON).Packages.NodeJS;
+ if ($pkg_config.install -ne $true) {
+ Write-Output "Skipping NodeJS ${pkg_version} installation";
+ exit;
+ }
+
+ if (Test-Path -Path "z:\${pkg_file}") {
+ Write-Output "Reading package '${pkg_file}' from cache...";
+ Copy-Item -Path "z:\${pkg_file}" -Destination "${env:TEMP}\${pkg_file}";
+ } else {
+ Write-Output "Fetching package from '${pkg_url}'...";
+ (New-Object System.Net.WebClient).DownloadFile($pkg_url, "${env:TEMP}\${pkg_file}");
+ if (Test-Path -Path "z:") { Copy-Item -Path "${env:TEMP}\${pkg_file}" -Destination "z:\${pkg_file}" -ErrorAction 'SilentlyContinue'; }
+ }
+
+ Write-Output "Installing NodeJS ${pkg_version}...";
+ Start-Process "msiexec.exe" -ArgumentList "/i `"${env:TEMP}\${pkg_file}`" /log `"${env:WINDIR}\Setup\Scripts\node.log`" /passive /norestart ALLUSERS=1" -Wait -PassThru | Out-Null;
+ Remove-Item -LiteralPath "${env:TEMP}\${pkg_file}" -Force -ErrorAction 'SilentlyContinue';
+
+
+
+ # Yarn Legacy
+ $pkg_version = "1.22.22";
+ $pkg_file = "yarn-${pkg_version}.msi";
+ $pkg_url = "https://github.com/yarnpkg/yarn/releases/download/v${pkg_version}/yarn-${pkg_version}.msi";
+
+ $pkg_config = (Get-Content -Raw "$env:WINDIR\Setup\Scripts\config.json" | ConvertFrom-JSON).Packages.Yarn;
+ if ($pkg_config.install -ne $true) {
+ Write-Output "Skipping Yarn Legacy ${pkg_version} installation";
+ exit;
+ }
+
+ if (Test-Path -Path "z:\${pkg_file}") {
+ Write-Output "Reading package '${pkg_file}' from cache...";
+ Copy-Item -Path "z:\${pkg_file}" -Destination "${env:TEMP}\${pkg_file}";
+ } else {
+ Write-Output "Fetching package from '${pkg_url}'...";
+ (New-Object System.Net.WebClient).DownloadFile($pkg_url, "${env:TEMP}\${pkg_file}");
+ if (Test-Path -Path "z:") { Copy-Item -Path "${env:TEMP}\${pkg_file}" -Destination "z:\${pkg_file}" -ErrorAction 'SilentlyContinue'; }
+ }
+
+ Write-Output "Installing Yarn Legacy ${pkg_version}...";
+ Start-Process "msiexec.exe" -ArgumentList "/i `"${env:TEMP}\${pkg_file}`" /log `"${env:WINDIR}\Setup\Scripts\yarn.log`" /passive /norestart ALLUSERS=1" -Wait -PassThru | Out-Null;
+ Remove-Item -LiteralPath "${env:TEMP}\${pkg_file}" -Force -ErrorAction 'SilentlyContinue';
+
+
+
+ # Common defines
+ $cpu_arch = $env:PROCESSOR_ARCHITECTURE.ToLower();
+
+ # Rust MSVC
+ $pkg_version = "1.96.0";
+ $pkg_file = "rust-${pkg_version}-msvc-${cpu_arch}.msi";
+ $pkg_url = & {
+ if ($cpu_arch -eq "amd64") {
+ "https://static.rust-lang.org/dist/rust-${pkg_version}-x86_64-pc-windows-msvc.msi";
+ } else {
+ "https://static.rust-lang.org/dist/rust-${pkg_version}-aarch64-pc-windows-msvc.msi";
+ }
+ };
+
+ $pkg_config = (Get-Content -Raw "$env:WINDIR\Setup\Scripts\config.json" | ConvertFrom-JSON).Packages.Rust;
+ if ($pkg_config.install -ne $true) {
+ Write-Output "Skipping Rust MSVC ${pkg_version} installation";
+ exit;
+ }
+
+ if (Test-Path -Path "z:\${pkg_file}") {
+ Write-Output "Reading package '${pkg_file}' from cache...";
+ Copy-Item -Path "z:\${pkg_file}" -Destination "${env:TEMP}\${pkg_file}";
+ } else {
+ Write-Output "Fetching package from '${pkg_url}'...";
+ (New-Object System.Net.WebClient).DownloadFile($pkg_url, "${env:TEMP}\${pkg_file}");
+ if (Test-Path -Path "z:") { Copy-Item -Path "${env:TEMP}\${pkg_file}" -Destination "z:\${pkg_file}" -ErrorAction 'SilentlyContinue'; }
+ }
+
+ Write-Output "Installing Rust MSVC ${pkg_version}...";
+ Start-Process "msiexec.exe" -ArgumentList "/i `"${env:TEMP}\${pkg_file}`" /log `"${env:WINDIR}\Setup\Scripts\rust.log`" /passive /norestart ALLUSERS=1" -Wait -PassThru | Out-Null;
+ Remove-Item -LiteralPath "${env:TEMP}\${pkg_file}" -Force -ErrorAction 'SilentlyContinue';
+
+
+
+
+ # Common
+ $cacheDrive = @(Get-ChildItem function:[d-z]: -n | ?{Test-Path $_\NvVars})[0];
+
+ #
+ # VSWhere (AMD64 only)
+ #
+ & {
+ $pkg_version = "3.1.7";
+ $pkg_file = "vswhere-${pkg_version}.exe";
+ $pkg_url = "https://github.com/microsoft/vswhere/releases/download/${pkg_version}/vswhere.exe";
+
+ $pkg_config = (Get-Content -Raw "$env:WINDIR\Setup\Scripts\config.json" | ConvertFrom-JSON).Packages.VSWhere;
+ if ($pkg_config.install -ne $true) {
+ Write-Output "Skipping VSWhere ${pkg_version} installation";
+ return;
+ }
+
+ if (Test-Path -Path "z:\${pkg_file}") {
+ Write-Output "Reading package '${pkg_file}' from cache...";
+ Copy-Item -Path "z:\${pkg_file}" -Destination "${env:TEMP}\${pkg_file}";
+ } else {
+ Write-Output "Fetching package from '${pkg_url}'...";
+ (New-Object System.Net.WebClient).DownloadFile($pkg_url, "${env:TEMP}\${pkg_file}");
+ if (Test-Path -Path "z:") { Copy-Item -Path "${env:TEMP}\${pkg_file}" -Destination "z:\${pkg_file}" -ErrorAction 'SilentlyContinue'; }
+ }
+
+ Write-Output "Installing VSWhere ${pkg_version}..."
+ Copy-Item -Path "${env:TEMP}\${pkg_file}" -Destination "${env:WINDIR}\vswhere.exe";
+ Remove-Item -LiteralPath "${env:TEMP}\${pkg_file}" -Force -ErrorAction 'SilentlyContinue';
+ };
+
+ #
+ # VisualStudio 2026 Community or Buildtools
+ #
+ & {
+ $pkg_config = (Get-Content -Raw "$env:WINDIR\Setup\Scripts\config.json" | ConvertFrom-JSON).Packages.VisualStudio;
+
+ # VS edition, one of: 'community' | 'buildtools'
+ $vs_edition = 'buildtools';
+ if (-not [string]::IsNullOrWhiteSpace($pkg_config.edition)) {
+ $vs_edition = $pkg_config.edition;
+ }
+
+ # VS version, only '18' supported
+ $vs_version = '18';
+ # if (-not [string]::IsNullOrWhiteSpace($pkg_config.version)) {
+ # $vs_version = $pkg_config.version;
+ # }
+
+ if (($pkg_config.install -ne $true) -or [string]::IsNullOrWhiteSpace($vs_edition) -or -not (@('community', 'buildtools') -contains $vs_edition)) {
+ Write-Output "Skipping VisualStudio ${vs_version} ${vs_edition} installation";
+ return;
+ }
+
+ #
+ # VS 2026 buildtools (default) or community edition
+ #
+ if ($vs_edition -eq "buildtools") {
+ #
+ # VS2026 Buildtools
+ #
+ $pkg_version = "${vs_version}";
+ $pkg_file = "vs_buildtools-${pkg_version}.exe";
+ $pkg_url = "https://aka.ms/vs/${pkg_version}/Stable/vs_buildtools.exe";
+ $pkg_layout = "z:\vs_buildtools-${pkg_version}";
+ $pkg_options = @(
+ '--add Microsoft.VisualStudio.Workload.VCTools',
+ '--add Microsoft.VisualStudio.Component.VC.ATLMFC',
+ '--add Microsoft.VisualStudio.Component.VC.Tools.ARM64',
+ '--add Microsoft.VisualStudio.Component.VC.ATL.ARM64',
+ '--add Microsoft.VisualStudio.Component.VC.MFC.ARM64',
+ '--includeRecommended'
+ );
+
+ if (Test-Path -Path "z:\${pkg_file}") {
+ Write-Output "Reading package '${pkg_file}' from cache...";
+ Copy-Item -Path "z:\${pkg_file}" -Destination "${env:TEMP}\${pkg_file}";
+ } else {
+ Write-Output "Fetching package from '${pkg_url}'...";
+ (New-Object System.Net.WebClient).DownloadFile($pkg_url, "${env:TEMP}\${pkg_file}");
+ if (Test-Path -Path "z:") { Copy-Item -Path "${env:TEMP}\${pkg_file}" -Destination "z:\${pkg_file}" -ErrorAction 'SilentlyContinue'; }
+ }
+
+ # Use cache device, if available
+ if (Test-Path -Path "z:") {
+ if (-not (Test-Path -Path "${pkg_layout}\${pkg_file}")) {
+ Write-Output "Generating VS 2026 Buildtools layout at '${pkg_layout}'...";
+ Remove-Item -Path "${pkg_layout}" -Recurse -Force -ErrorAction 'SilentlyContinue' | Out-Null;
+ Start-Process -FilePath "${env:TEMP}\${pkg_file}" -ArgumentList `
+ (@( "--layout `"${pkg_layout}`" --passive --wait --arch all --keepLayoutVersion --lang `"en-US de-DE`"" ) + $pkg_options) `
+ -Wait -PassThru | Out-Null;
+ }
+ }
+
+ if (Test-Path -Path "${pkg_layout}\${pkg_file}") {
+ Write-Output "Installing VS 2026 Buildtools using layout at '${pkg_layout}'...";
+ Start-Process -FilePath "${pkg_layout}\${pkg_file}" -ArgumentList `
+ (@( '--passive --wait --norestart --nocache' ) + $pkg_options) `
+ -Wait -PassThru | Out-Null;
+ } else {
+ Write-Output "Installing VisualStudio 2026 Buildtools...";
+ Start-Process -FilePath "${env:TEMP}\${pkg_file}" -ArgumentList `
+ (@( '--passive --wait --norestart --nocache' ) + $pkg_options) `
+ -Wait -PassThru | Out-Null;
+ }
+
+ Remove-Item -LiteralPath "${env:TEMP}\${pkg_file}" -Force -ErrorAction 'SilentlyContinue';
+
+ } elseif ($vs_edition -eq "community") {
+ #
+ # VS2026 Community
+ #
+ $pkg_version = "${vs_version}";
+ $pkg_file = "vs_community-${pkg_version}.exe";
+ $pkg_url = "https://aka.ms/vs/${pkg_version}/Stable/vs_community.exe";
+ $pkg_layout = "z:\vs_community-${pkg_version}";
+ $pkg_options = @(
+ '--add Microsoft.VisualStudio.Workload.NativeDesktop',
+ '--add Microsoft.VisualStudio.Component.VC.ATLMFC',
+ '--add Microsoft.VisualStudio.Component.VC.Tools.ARM64',
+ '--add Microsoft.VisualStudio.Component.VC.ATL.ARM64',
+ '--add Microsoft.VisualStudio.Component.VC.MFC.ARM64',
+ '--remove Component.VisualStudio.GitHub.Copilot',
+ '--remove ComponentGroup.Microsoft.NET.AppModernization',
+ '--remove Microsoft.VisualStudio.Component.IntelliCode',
+ '--remove Component.Microsoft.VisualStudio.LiveShare.2022',
+ '--remove Component.Incredibuild',
+ '--remove Component.IncredibuildMenu',
+ '--includeRecommended'
+ );
+
+ if (Test-Path -Path "z:\${pkg_file}") {
+ Write-Output "Reading package '${pkg_file}' from cache...";
+ Copy-Item -Path "z:\${pkg_file}" -Destination "${env:TEMP}\${pkg_file}";
+ } else {
+ Write-Output "Fetching package from '${pkg_url}'...";
+ (New-Object System.Net.WebClient).DownloadFile($pkg_url, "${env:TEMP}\${pkg_file}");
+ if (Test-Path -Path "z:") { Copy-Item -Path "${env:TEMP}\${pkg_file}" -Destination "z:\${pkg_file}" -ErrorAction 'SilentlyContinue'; }
+ }
+
+ # Use cache device, if available
+ if (Test-Path -Path "z:") {
+ if (-not (Test-Path -Path "${pkg_layout}\${pkg_file}")) {
+ Write-Output "Generating VS 2026 Community layout at '${pkg_layout}'...";
+ Remove-Item -Path "${pkg_layout}" -Recurse -Force -ErrorAction 'SilentlyContinue' | Out-Null;
+ Start-Process -FilePath "${env:TEMP}\${pkg_file}" -ArgumentList `
+ (@( "--layout `"${pkg_layout}`" --passive --wait --arch all --keepLayoutVersion --lang `"en-US de-DE`"" ) + $pkg_options) `
+ -Wait -PassThru | Out-Null;
+ }
+ }
+
+ if (Test-Path -Path "${pkg_layout}\${pkg_file}") {
+ Write-Output "Installing VS 2026 Community using layout at '${pkg_layout}'...";
+ Start-Process -FilePath "${pkg_layout}\${pkg_file}" -ArgumentList `
+ (@( '--passive --wait --norestart --nocache' ) + $pkg_options) `
+ -Wait -PassThru | Out-Null;
+ } else {
+ Write-Output "Installing VisualStudio 2026 Community...";
+ Start-Process -FilePath "${env:TEMP}\${pkg_file}" -ArgumentList `
+ (@( '--passive --wait --norestart --nocache' ) + $pkg_options) `
+ -Wait -PassThru | Out-Null;
+ }
+
+ Remove-Item -LiteralPath "${env:TEMP}\${pkg_file}" -Force -ErrorAction 'SilentlyContinue';
+ }
+ };
+
+
+
+
+ # Common
+ $cacheDrive = @(Get-ChildItem function:[d-z]: -n | ?{Test-Path $_\NvVars})[0];
+
+ $pkg_locations = @{
+ # Win11 28000
+ "10.0.28000.1839" = "https://go.microsoft.com/fwlink/?linkid=2361309";
+ # Win11 26100
+ "10.0.26100.8249" = "https://go.microsoft.com/fwlink/?linkid=2361308";
+ # Required for Chromium builds (as of 2026-05-30)
+ "10.0.26100.7705" = "https://go.microsoft.com/fwlink/?linkid=2349110";
+ };
+
+ $pkg_version = "10.0.26100.7705";
+ $pkg_file = "winsdksetup-${pkg_version}.exe";
+ $pkg_url = $pkg_locations[$pkg_version];
+ $pkg_features = @(
+ # Required for Chromium builds (minumum version: 10.0.26100.3323)
+ "OptionId.WindowsDesktopDebuggers",
+ # Useful tools
+ "OptionId.MSIInstallTools",
+ "OptionId.SigningTools",
+
+ # "OptionId.WindowsPerformanceToolkit",
+ # "OptionId.AvrfExternal",
+ # "OptionId.NetFxSoftwareDevelopmentKit",
+ # "OptionId.WindowsSoftwareLogoToolkit",
+ # "OptionId.IpOverUsb",
+ # "OptionId.UWPManaged",
+ # "OptionId.UWPCPP",
+ # "OptionId.UWPLocalized",
+ # "OptionId.DesktopCPPx86",
+ "" # sentinel
+ );
+
+ $pkg_config = (Get-Content -Raw "$env:WINDIR\Setup\Scripts\config.json" | ConvertFrom-JSON).Packages.WinSDK;
+ if ($pkg_config.install -ne $true) {
+ Write-Output "Skipping WinSDK installation";
+ exit;
+ }
+
+ if (Test-Path -Path "z:\${pkg_file}") {
+ Write-Output "Reading package '${pkg_file}' from cache...";
+ Copy-Item -Path "z:\${pkg_file}" -Destination "${env:TEMP}\${pkg_file}";
+ } else {
+ Write-Output "Fetching package from '${pkg_url}'...";
+ (New-Object System.Net.WebClient).DownloadFile($pkg_url, "${env:TEMP}\${pkg_file}");
+ if (Test-Path -Path "z:") { Copy-Item -Path "${env:TEMP}\${pkg_file}" -Destination "z:\${pkg_file}" -ErrorAction 'SilentlyContinue'; }
+ }
+
+ Write-Output "Installing Windows SDK ${pkg_version}...";
+
+ # additional options /layout [path] /list
+ $pkg_options = & {
+ $options = @();
+
+ $features = $pkg_features -Join " ";
+ if (-not ([string]::IsNullOrEmpty($features))) {
+ $options += "/features `"${features}`"";
+ }
+
+ $options;
+ };
+
+ Start-Process -FilePath "${env:TEMP}\${pkg_file}" -ArgumentList `
+ (@( '/quiet /norestart /ceip off', "/log `"${env:WINDIR}\Setup\Scripts\WinSDK.log`"" ) + $pkg_options) `
+ -Wait -PassThru | Out-Null;
+
+ Remove-Item -LiteralPath "${env:TEMP}\${pkg_file}" -Force -ErrorAction 'SilentlyContinue';
+
+
+
+
+ # Download and install WinFSP
+ $pkg_version = "2.2.26112";
+ $pkg_file = "winfsp-${pkg_version}.msi";
+ $pkg_url = "https://github.com/winfsp/winfsp/releases/download/v2.2B1/winfsp-${pkg_version}.msi";
+
+ Write-Output "Installing WinFSP ${pkg_version}...";
+ (New-Object System.Net.WebClient).DownloadFile($pkg_url, "${env:TEMP}\${pkg_file}");
+ Start-Process "msiexec.exe" `
+ -ArgumentList "/i `"${env:TEMP}\${pkg_file}`" /log `"${env:WINDIR}\Setup\Scripts\winfsp.log`" /passive /norestart ALLUSERS=1" `
+ -Wait -PassThru | Out-Null;
+ Remove-Item -LiteralPath "${env:TEMP}\${pkg_file}" -Force -ErrorAction 'SilentlyContinue';
+
+ # Load viofs driver
+ Start-Process "PnpUtil.exe" -ArgumentList "/add-driver `"${env:SystemDrive}\Drivers\viofs\viofs.inf`" /install" `
+ -Wait -PassThru;
+
+ # Install VirtioFS service
+ New-Service -Name VirtioFsSvc -DisplayName 'Virtio FS Service' `
+ -BinaryPathName "${env:SystemDrive}\Drivers\viofs\virtiofs.exe" `
+ -StartupType 'Automatic' `
+ -DependsOn 'WinFsp.Launcher';
+
+ Start-Service -Name VirtioFsSvc -PassThru;
+
+
+
+
+
+ # Autoselection of first drive with autounattend.xml
+ $unattendXmlPath = @(Get-ChildItem function:[d-z]: -n | %{$_ + "\autounattend.xml"} | ?{Test-Path $_})[0];
+
+ Write-Output "Resolving user account credentials from '$unattendXmlPath'...";
+ $xpathSelector = "//ns:settings[@pass=`"oobeSystem`"]/ns:component[@name=`"Microsoft-Windows-Shell-Setup`"]/ns:UserAccounts/ns:LocalAccounts/*/ns:Group[text() = `"Users`"][1]/..";
+ $userName = (Select-Xml -Path "$unattendXmlPath" -XPath "$xpathSelector/ns:Name" -Namespace @{ns='urn:schemas-microsoft-com:unattend'}).Node.InnerText;
+ $userPass = (Select-Xml -Path "$unattendXmlPath" -XPath "$xpathSelector/ns:Password/ns:Value" -Namespace @{ns='urn:schemas-microsoft-com:unattend'}).Node.InnerText;
+ Write-Output "Credentials for user account: '${userName}' / '$(''.PadRight($userPass.Length, '*'))'";
+
+ # Resolve SID from username
+ $userAccount = New-Object System.Security.Principal.NTAccount($userName);
+ $userSid = $userAccount.Translate([System.Security.Principal.SecurityIdentifier]).value;
+ Write-Output "Resolved username '${userName}' to SID '${userSid}'";
+
+ #
+ # Set-ExecutionPolicy -ExecutionPolicy 'RemoteSigned' -Scope 'LocalMachine' -ErrorAction 'SilentlyContinue';
+
+ # Switch auto-logon to builder user
+ Write-Output "Switching autologon to user account '$userName'...";
+ reg.exe add 'HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon' /v 'AutoAdminLogon' /t 'REG_SZ' /d '1' /f;
+ reg.exe add 'HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon' /v 'DefaultUserName' /t 'REG_SZ' /d "$userName" /f;
+ reg.exe add 'HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon' /v 'DefaultPassword' /t 'REG_SZ' /d "$userPass" /f;
+
+ # Reboot into user account
+ Write-Output "Scheduling system reboot to complete '$userName' setup...";
+ shutdown.exe /r /f /t 30;
+
+
+
+
+
+ # Autoselection of first drive with autounattend.xml
+ $unattendXmlPath = @(Get-ChildItem function:[d-z]: -n | %{$_ + "\autounattend.xml"} | ?{Test-Path $_})[0];
+
+ Write-Output "Resolving user account credentials from '$unattendXmlPath'...";
+ $xpathSelector = "//ns:settings[@pass=`"oobeSystem`"]/ns:component[@name=`"Microsoft-Windows-Shell-Setup`"]/ns:UserAccounts/ns:LocalAccounts/*/ns:Group[text() = `"Users`"][1]/..";
+ $userName = (Select-Xml -Path "$unattendXmlPath" -XPath "$xpathSelector/ns:Name" -Namespace @{ns='urn:schemas-microsoft-com:unattend'}).Node.InnerText;
+
+ if ($env:USERNAME -eq $userName) {
+ Write-Output "Scheduling system shutdown to finish setup...";
+ shutdown.exe /s /t 5;
+ }
+
+
+
+{
+ "packages": {
+ "Git": {
+ "install": true,
+ "version": "2.54.0"
+ },
+ "Mesa3dOpenGL": {
+ "install": true,
+ "version": "26.1.1"
+ },
+ "NodeJS": {
+ "install": true,
+ "version": "24.15.0"
+ },
+ "OpenSSH": {
+ "install": true,
+ "version": "10.0.0.0p2"
+ },
+ "Python": {
+ "install": true,
+ "version": "3.14.5"
+ },
+ "Rust": {
+ "install": true,
+ "version": "1.96.0"
+ },
+ "VisualStudio": {
+ "install": true,
+ "version": "18",
+ "edition": "buildtools"
+ },
+ "VSWhere": {
+ "install": false
+ },
+ "WinSDK": {
+ "install": true,
+ "version": "10.0.26100.7705"
+ },
+ "Yarn": {
+ "install": true,
+ "version": "1.22.22"
+ }
+ }
+}
+
+
+
diff --git a/isobuilder.sh b/isobuilder.sh
new file mode 100644
index 0000000..87d6888
--- /dev/null
+++ b/isobuilder.sh
@@ -0,0 +1,270 @@
+#!/usr/bin/env /bin/bash
+declare -r BASEDIR="`pwd`"
+
+declare -r mode="full" # one of 'full'|'unattend'
+
+declare -r ISO_BASEDIR="/mnt/machines/iso"
+declare -r CONFIG_DIR="${BASEDIR}/configs"
+
+declare -r UNATTEND_XML="${2:-"${CONFIG_DIR}/autounattend-builder-amd64-20260527.xml"}"
+
+#declare -r WINDOWS_ISO="${ISO_BASEDIR}/nano11-25H2-Pro-Eng-Arm64-20260513.iso"
+#declare -r WINDOWS_ISO="${ISO_BASEDIR}/tiny11-25H2-Pro-Eng-Arm64-20260514.iso"
+#declare -r WINDOWS_ISO="${ISO_BASEDIR}/nano11-25H2-English-Pro-2026-03-15.iso"
+#declare -r WINDOWS_ISO="${ISO_BASEDIR}/Tiny11-25H2-English-Pro-2026-05-20.iso"
+#declare -r WINDOWS_ISO="${ISO_BASEDIR}/Tiny11Core-25H2-English-Pro-2026-05-20.iso"
+#declare -r WINDOWS_ISO="${ISO_BASEDIR}/nano11-25H2-English-Pro-2026-05-20.iso"
+
+#declare -r WINDOWS_ISO="${ISO_BASEDIR}/nano11-base-25H2-Pro-Eng-amd64-20260526.iso"
+declare -r WINDOWS_ISO="${ISO_BASEDIR}/nano11-base-25H2-Pro-Eng-arm64-20260526.iso"
+
+declare -r VIRTIO_ISO="${ISO_BASEDIR}/virtio-win-0.1.285.iso"
+#declare -r VIRTIO_ISO="${BASEDIR}/virtio-win-0.1.285.iso"
+
+
+######################################################################################
+#
+#
+
+function detect_iso_arch {
+ local iso_file="${1}"
+ local fallback_arch="${2}"
+ case "${iso_file@L}" in
+ *-amd64-*|*-x64-*)
+ echo "amd64" ;;
+ *-arm64-*|*-aarch64-*)
+ echo "arm64" ;;
+ *) # Unknown use host arch as fallback
+ echo "NOTICE: Cannot detect architecture from ISO name, using fallback" >&2
+ echo "${fallback_arch}" ;;
+ esac
+}
+
+function detect_host_arch {
+ case "$(uname -m | tr '[:upper:]' '[:lower:]')" in
+ "x86_64") echo "amd64" ;;
+ "aarch64") echo "arm64" ;;
+ *) return 1 ;;
+ esac
+}
+
+######################################################################################
+# Detect host and guest architecture
+declare -r HOST_ARCH="$(detect_host_arch)"
+[[ -n "${HOST_ARCH}" ]] || {
+ echo "Failed to detect host arch" >&2;
+ exit 1;
+}
+
+declare -r TARGET_ARCH="$(detect_iso_arch "${WINDOWS_ISO}")"
+[[ -n "${TARGET_ARCH}" ]] || {
+ echo "Failed to detect target arch" >&2;
+ exit 1;
+}
+
+case "${mode}" in
+"full")
+ echo "Generating full ${TARGET_ARCH} installation ISO image..." >&2
+ declare -r DEFAULT_OUTPUT_FILE="install-${TARGET_ARCH}-$(date +%Y%m%d-%H%M00).iso"
+ ;;
+"unattend")
+ echo "Generating unattended ${TARGET_ARCH} ISO image..." >&2
+ declare -r DEFAULT_OUTPUT_FILE="unattend-${TARGET_ARCH}-$(date +%Y%m%d-%H%M00).iso"
+ ;;
+*)
+ echo "Invalid mode '${mode}' expected either 'full'|'unattend'" >&2
+ exit 1
+ ;;
+esac
+
+
+
+#
+# Output file
+#
+declare -r OUTPUT_FILE="${1:-${DEFAULT_OUTPUT_FILE}}"
+
+
+#
+# Validate input XML
+#
+xmllint -noout "${UNATTEND_XML}" || {
+ echo "$(basename "${UNATTEND_XML}") contains invalid XML, aborting" >&2
+ exit 1
+}
+
+
+mkdir -p "${BASEDIR}/build" || exit $?
+mkdir -p "${BASEDIR}/output" || exit $?
+
+# Convert architecture for virtio installation media
+case "${TARGET_ARCH}" in
+"arm64")
+ VIRTIO_ARCH="${TARGET_ARCH@U}" ;; # Virtio ISO uses "ARM64"
+*)
+ VIRTIO_ARCH="${TARGET_ARCH}" ;; # Virtio ISO uses "amd64"
+esac
+
+
+#
+# Generate builder script
+#
+cat -> "${BASEDIR}/build/genimage-${TARGET_ARCH}.sh" <<-EOF
+ #!/usr/bin/env /bin/ash
+
+ #
+ # Required packages
+ #
+ echo "Installing required packages..." >&2
+ apk update || exit \$?
+ apk add xorriso 7zip || exit \$?
+
+ #
+ # Prepare directories / mountpoints
+ #
+ echo "Preparing build directory and mountpoints..." >&2
+ mkdir -p /tmp/base || exit \$?
+ mkdir -p /tmp/virtio || exit \$?
+ mkdir -p /tmp/build || exit \$?
+
+ #
+ # Mounting (if possible) / unpacking of ISO images
+ #
+ if [[ "x${mode}" == "xfull" ]]; then
+ echo "Attempting to mount windows ISO image..." >&2
+ mount -t udf -o loop,ro /input/windows.iso /tmp/base/ 2>/dev/null || {
+ echo "Unpacking windows ISO image (fallback)..." >&2
+ 7z x -bb0 -o/tmp/base/ /input/windows.iso || exit \$?
+ }
+ else
+ echo "Building unattend ISO image, skipping windows mount" >&2
+ fi
+
+ echo "Attempting to mount virtio-win ISO image..." >&2
+ mount -o loop,ro /input/virtio.iso /tmp/virtio/ 2>/dev/null || {
+ echo "Unpacking virtio-win ISO image (fallback)..." >&2
+ 7z x -bb0 -o/tmp/virtio/ /input/virtio.iso || exit \$?
+ }
+
+ #
+ # Autounattend XML configuration file (replaces any existing one)
+ #
+ echo "Copying autounattend.xml..." >&2
+ sed -e 's|processorArchitecture="[^"]\+"|processorArchitecture="${TARGET_ARCH}"|' \
+ < /input/autounattend.xml \
+ > /tmp/build/autounattend.xml || exit \$?
+
+ #
+ # Required drivers for installation (into "$WinPEDriver$")
+ #
+ echo "Creating \\\$WinPEDriver\\\$..." >&2
+ mkdir -p "/tmp/build/\\\$WinPEDriver\\\$" || exit \$?
+ for x in NetKVM viostor vioscsi; do
+ cp -vfR /tmp/virtio/\${x}/w11/${VIRTIO_ARCH} "/tmp/build/\\\$WinPEDriver\\\$/\${x}" || exit \$?
+ done
+
+ #
+ # OEM files (drivers, packages, etc.)
+ #
+ echo "Copying OEM files..." >&2
+
+ mkdir -p \
+ "/tmp/build/\\\$OEM\\\$/\\\$\\\$" \
+ "/tmp/build/\\\$OEM\\\$/\\\$1" \
+ || exit \$?
+
+ # Viofs files for use in the final OS (virtiofsd)
+ mkdir -p "/tmp/build/\\\$OEM\\\$/\\\$1/drivers" || exit \$?
+ for x in /tmp/virtio/viofs/w11/${VIRTIO_ARCH}; do
+ drvname="\$(echo "\${x}" | cut -d'/' -f4)" || exit \$?
+ cp -vfR "\${x}" "/tmp/build/\\\$OEM\\\$/\\\$1/drivers/\${drvname}" || exit \$?
+ done
+
+ #
+ # Virtio drivers for auto-installation
+ #
+ echo "Copying virtio drivers for automatic installation..." >&2
+
+ mkdir -p "/tmp/build/drivers" || exit \$?
+ for x in /tmp/virtio/*/w11/${VIRTIO_ARCH}; do
+ drvname="\$(echo "\${x}" | cut -d'/' -f4)" || exit \$?
+ cp -vfR "\${x}" "/tmp/build/drivers/\${drvname}" || exit \$?
+ done
+
+ #
+ # ISO image generation
+ #
+ if [[ "x${mode}" == "xfull" ]]; then
+ echo "Generating ISO boot image..." >&2
+ mkisofs \\
+ -quiet \\
+ -b boot/etfsboot.com \\
+ -no-emul-boot \\
+ -boot-load-size 8 \\
+ -iso-level 3 -J -l -D -N -U -joliet-long -relaxed-filenames -rational-rock \\
+ -eltorito-alt-boot -e efi/microsoft/boot/efisys_noprompt.bin -no-emul-boot \\
+ -m "/tmp/base/autounattend.xml" \\
+ -V "Nano11-Builder" \\
+ -o "/output/$(basename "${OUTPUT_FILE}")" \\
+ "/tmp/base/" "/tmp/build/" || exit \$?
+ else
+ echo "Generating ISO unattend image..." >&2
+ mkisofs \\
+ -quiet \\
+ -iso-level 3 -J -l -D -N -U -joliet-long -relaxed-filenames -rational-rock \\
+ -V "Nano11-Builder-Unattend" \\
+ -o "/output/$(basename "${OUTPUT_FILE}")" \\
+ "/tmp/build/" || exit \$?
+ fi
+
+# genisoimage \\
+# -quiet \\
+# -b boot/etfsboot.com \\
+# -no-emul-boot \\
+# -boot-load-seg 1984 \\
+# -boot-load-size 8 \\
+# -iso-level 3 -J -l -D -N -U -joliet-long -allow-limited-size -relaxed-filenames -rock \\
+# -eltorito-alt-boot -e efi/microsoft/boot/efisys_noprompt.bin \\
+# -m "/tmp/base/autounattend.xml" \\
+# -V "Nano11-Builder" \\
+# -o "/build/output.iso" \\
+# "/tmp/base/" "/tmp/build/" || exit \$?
+
+ #
+ #
+ #
+ echo "Cleaning up..." >&2
+ umount /tmp/base/ 2>/dev/null
+ umount /tmp/virtio/ 2>/dev/null
+EOF
+
+chmod 0755 "${BASEDIR}/build/genimage-${TARGET_ARCH}.sh" || exit $?
+
+declare -r CONTAINER_IMAGE="alpine:3.23"
+declare -r CONTAINER_ARGS=(
+ "-it --rm --name isobuilder-${TARGET_ARCH}"
+ "-v ${VIRTIO_ISO}:/input/virtio.iso:ro,Z"
+ "-v ${WINDOWS_ISO}:/input/windows.iso:ro,Z"
+ "-v ${UNATTEND_XML}:/input/autounattend.xml:ro,Z"
+ "-v ${BASEDIR}/build:/build:ro,Z"
+ "-v ${BASEDIR}/output:/output:rw,Z"
+ "--privileged --network host"
+)
+
+#
+# Run generation script in alpine 3.23 container
+#
+if type -p docker &>/dev/null; then
+ echo "Running isobuilder in docker container" >&2
+ docker run ${CONTAINER_ARGS[@]} ${CONTAINER_IMAGE} \
+ /build/genimage-${TARGET_ARCH}.sh || exit $?
+elif type -p podman &>/dev/null; then
+ echo "Running isobuilder in podman container" >&2
+ podman run ${CONTAINER_ARGS[@]} ${CONTAINER_IMAGE} \
+ /build/genimage-${TARGET_ARCH}.sh || exit $?
+else
+ echo "Failed to start build container: no runtime available" >&2
+ exit 1
+fi
+
+(cd "${BASEDIR}/output" && ls -sh "$(basename "${OUTPUT_FILE}")") || exit $?
diff --git a/win11-builder-generic.sh b/win11-builder-generic.sh
new file mode 100755
index 0000000..7bcb89e
--- /dev/null
+++ b/win11-builder-generic.sh
@@ -0,0 +1,554 @@
+#!/usr/bin/env /bin/bash
+declare -r TOP_BASEDIR="`pwd`"
+
+#
+# ISO images
+#
+declare -r ISO_BASEDIR="/mnt/machines/iso"
+
+# Windows installation ISO
+#declare -r INSTALL_ISO="${ISO_BASEDIR}/Win11_25H2_English_x64_v2.iso"
+#declare -r INSTALL_ISO="${ISO_BASEDIR}/nano11-25H2-English-Pro-2026-03-20.iso"
+declare -r INSTALL_ISO="${TOP_BASEDIR}/output/install-builder-amd64-20260601.iso"
+
+# Unattended installation add-on ISO
+#declare -r UNATTEND_ISO="${TOP_BASEDIR}/output/unattend.iso"
+
+declare -r VIRTIO_ISO="${ISO_BASEDIR}/virtio-win-0.1.285.iso"
+
+#
+# Disk images
+#
+declare -r DISK_BASEDIR="/mnt/machines/qemu-machines"
+#declare -r DISK_IMG="${DISK_BASEDIR}/win11-builder-amd64.qcow2"
+declare -r DISK_SIZE="32G"
+
+#
+# x86-64-v3: Haswell w/o intel-specific instructions (TSX, IBRS)
+#
+# x86-64-v1: +cmov,+cx8,+fpu,+fxsr,+mmx,+osfxsr,+sce,+sse,+sse2 (aka "kvm64")
+# x86-64-v2: +cx16,+lahf-lm,+popcnt,+sse3,+sse4.1,+sse4.2,+ssse3
+# x86-64-v3: +avx,+avx2,+bmi1,+bmi2,+f16c,+fma,+movbe,+xsave,+aes
+# x86-64-v4: +avx512f,+avx512bw,+avx512cd,+avx512dq,+avx512vl
+#
+declare -r CPU_X64_V2="kvm64,+cx16,+lahf-lm,+popcnt,+sse3,+sse4.1,+sse4.2,+ssse3"
+declare -r CPU_X64_V3="${CPU_X64_V2},+avx,+avx2,+bmi1,+bmi2,+f16c,+fma,+movbe,+xsave,+aes"
+declare -r CPU_X64_V4="${CPU_X64_V3},+avx512f,+avx512bw,+avx512cd,+avx512dq,+avx512vl"
+
+# AMD64: Minimum requirement is X86_64-V3
+declare -r CPU_OPTIONS_AMD64="-cpu host -smp cpus=4,cores=4,sockets=1"
+#declare -r CPU_OPTIONS_AMD64="-cpu ${CPU_X64_V3} -smp cpus=4,cores=4,sockets=1"
+
+# ARM64: Minimum requirement for Windows 11 is Cortext A72?
+declare -r CPU_OPTIONS_ARM64="-cpu host -smp 4"
+#declare -r CPU_OPTIONS_ARM64="-cpu max,pauth-impdef=on -smp cpus=4,cores=4,sockets=1"
+#declare -r CPU_OPTIONS_ARM64="-cpu neoverse-n1 -smp cpus=4,cores=4,sockets=1"
+#declare -r CPU_OPTIONS_ARM64="-cpu cortext-a76 -smp cpus=4,cores=4,sockets=1"
+
+#
+# Memory config
+#
+declare -r MEM_SIZE="8G"
+declare -r MEM_OPTIONS="-m ${MEM_SIZE}"
+
+#
+#
+#
+declare -r ARM64_EFI_CODE_IMG="/usr/share/edk2/aarch64/QEMU_EFI.fd"
+declare -r ARM64_EFI_VARS_IMG=""
+# Optional: required EFI ROM size
+declare -r ARM64_EFI_CODE_SIZE="64M"
+
+#################################################################
+# No user-serviceable parts below this line
+#################################################################
+
+# Get builder command
+declare -r COMMAND="${1:-build}"
+shift
+
+function detect_iso_arch {
+ local iso_file="${1}"
+ local fallback_arch="${2}"
+ case "${iso_file@L}" in
+ *-amd64-*|*-x64-*)
+ echo "amd64" ;;
+ *-arm64-*|*-aarch64-*)
+ echo "arm64" ;;
+ *) # Unknown use host arch as fallback
+ echo "NOTICE: Cannot detect architecture from ISO name, using host" >&2
+ echo "${fallback_arch}" ;;
+ esac
+}
+
+function detect_host_arch {
+ case "$(uname -m | tr '[:upper:]' '[:lower:]')" in
+ "x86_64") echo "amd64" ;;
+ "aarch64") echo "arm64" ;;
+ *) return 1 ;;
+ esac
+}
+
+# Detect host and guest architecture
+declare -r HOST_ARCH="$(detect_host_arch)"
+[[ -n "${HOST_ARCH}" ]] || {
+ echo "Failed to detect host arch" >&2;
+ exit 1;
+}
+
+declare -r GUEST_ARCH="$(detect_iso_arch "${INSTALL_ISO}" "${HOST_ARCH}")"
+[[ -n "${GUEST_ARCH}" ]] || {
+ echo "Failed to detect guest arch" >&2;
+ exit 1;
+}
+
+# Setup locations
+declare -r BASEDIR="${TOP_BASEDIR}/win11-builder-${GUEST_ARCH}"
+declare -r CACHEDIR="${TOP_BASEDIR}/cache"
+
+mkdir -p "${BASEDIR}" || exit $?
+mkdir -p "${CACHEDIR}" || exit $?
+
+declare -r DISK_IMG="${DISK_BASEDIR:-${BASEDIR}}/win11-builder-${GUEST_ARCH}.qcow2"
+[[ -n "${DISK_IMG}" ]] || {
+ echo "Mandatory disk image configuration missing" >&2
+ exit 1
+}
+
+#
+# Common options used by all configurations
+#
+QEMU_DYN_OPTIONS=(
+ # Use host's /dev/urandom
+ "-object rng-random,filename=/dev/urandom,id=rng0"
+ "-device virtio-rng-pci,rng=rng0"
+
+ # virtio-fs
+ "-chardev socket,id=char0,path=${BASEDIR}/virtiofsd.sock"
+ "-device vhost-user-fs-pci,queue-size=1024,chardev=char0,tag=sharedfs"
+ "-object memory-backend-memfd,id=mem,size=${MEM_SIZE},share=on"
+ "-numa node,memdev=mem"
+)
+
+#
+# virtiofs
+#
+mkdir -p "${BASEDIR}/shared" || exit $?
+echo "This directory is shared between host and guest" \
+ > "${BASEDIR}/shared/README.md" || exit $?
+
+#
+# Randomized user credentials?
+#
+#declare -r USER_NAME="User" || exit $?
+#declare -r USER_PASSWORD="$(pwgen -cns 30 1)" || exit $?
+#QEMU_DYN_OPTIONS+=(
+# "-smbios type=11,value=AutoLogon\\${USER_NAME}\\${USER_PASSWORD}"
+#)
+
+function start_viofsd {
+ echo "Starting virtiofsd..." >&2
+ /usr/libexec/virtiofsd --sandbox=none --tag sharedfs \
+ --shared-dir "${CACHEDIR}" \
+ --socket-path "${BASEDIR}/virtiofsd.sock" \
+ --socket-group `id -gn` \
+ --translate-uid squash-guest:0:`id -u`:1000000 \
+ --translate-gid squash-guest:0:`id -g`:1000000 &
+ echo $! > "${BASEDIR}/virtiofsd.pid" || exit $?
+}
+
+function stop_viofsd {
+ local VIOFSD_PID="$(cat "${BASEDIR}/virtiofsd.pid")" || return $?
+ [[ -n "${VIOFSD_PID}" ]] || return 1
+ pgrep -p "${VIOFSD_PID}" || return 1
+ echo "Stopping virtiofsd..." >&2
+ kill -TERM "${VIOFSD_PID}"
+}
+
+trap "stop_viofsd" EXIT
+start_viofsd
+
+#
+# Qemu
+#
+
+function qemu_image_type_from_fileext {
+ local img_file="${1@L}"
+ case "${img_file##*.}" in
+ "qcow2") echo "qcow2" ;;
+ *) echo "raw" ;;
+ esac
+}
+
+function qemu_image_size {
+ local img_file="${1}"
+ local img_type="$(qemu_image_type_from_fileext "${1}")"
+ case "${img_type}" in
+ "qcow2")
+ echo "$(qemu-img info --output json "${img_file}" | jq '.["virtual-size"]')" || return $? ;;
+ "raw")
+ echo "$(stat -c '%s' "${img_file}")" || return $? ;;
+ *)
+ echo "Unknown image type '${img_type}', cannot detect size" >&2
+ return 1 ;;
+ esac
+}
+
+function fileext_from_qemu_image_type {
+ local img_type="${1@L}"
+ case "${img_type}" in
+ *) echo "${img_type}" ;;
+ esac
+}
+
+function qemu_prepare_disk {
+ local QEMU_DISK_FILE="${1}"
+ local QEMU_DISK_SIZE="${2:-32G}"
+
+ # Remove leftover temp files
+ rm -f "${QEMU_DISK_FILE}.tmp" 2>/dev/null
+
+ if [[ ! -f "${QEMU_DISK_FILE}" || "${FORCE_RESET}" -eq 1 ]]; then
+ echo "Creating QEMU disk image..." >&2
+ qemu-img create -f qcow2 "${QEMU_DISK_FILE}" "${QEMU_DISK_SIZE}" >/dev/null || return $?
+ elif [[ -f "${QEMU_DISK_FILE}" ]]; then
+ local QEMU_COW_FILE="${QEMU_DISK_FILE}.cow"
+ local QEMU_COW_SIZE="$(qemu_image_size "${QEMU_DISK_FILE}")" || return $?
+
+ echo "Creating QEMU CoW disk image for '$(basename "${QEMU_DISK_FILE}")' ..." >&2
+ qemu-img create -f qcow2 -B qcow2 -b "${QEMU_DISK_FILE}" "${QEMU_COW_FILE}" "${QEMU_COW_SIZE}" >/dev/null || return $?
+ fi
+
+ #
+ QEMU_DYN_OPTIONS+=(
+ "-drive if=none,format=qcow2,file=${QEMU_COW_FILE:-${QEMU_DISK_FILE}},discard=unmap,id=hd0"
+ "-device virtio-blk-pci,drive=hd0,bootindex=1"
+ )
+}
+
+function qemu_prepare_cdrom_amd64 {
+ local QEMU_BOOTINDEX=2 # First non-HD drive bootindex
+
+ # Only assign cdrom drives for build command (TODO: clean this up)
+ [[ "${COMMAND}" == "build" ]] || return 0;
+
+ [[ -n "${UNATTEND_ISO}" ]] && {
+ QEMU_DYN_OPTIONS+=(
+ "-drive if=none,file=${UNATTEND_ISO},media=cdrom,id=cd2"
+ "-device ide-cd,drive=cd2,bootindex=$(( QEMU_BOOTINDEX++ )),bus=ahci.2"
+ )
+ }
+
+ [[ -n "${INSTALL_ISO}" ]] && {
+ QEMU_DYN_OPTIONS+=(
+ "-drive if=none,file=${INSTALL_ISO},media=cdrom,id=cd0"
+ "-device ide-cd,drive=cd0,bootindex=$(( QEMU_BOOTINDEX++ )),bus=ahci.0"
+ )
+ }
+
+ [[ -n "${VIRTIO_ISO}" ]] && {
+ QEMU_DYN_OPTIONS+=(
+ "-drive if=none,file=${VIRTIO_ISO},media=cdrom,id=cd1"
+ "-device ide-cd,drive=cd1,bootindex=$(( QEMU_BOOTINDEX++ )),bus=ahci.1"
+ )
+ }
+}
+
+function qemu_prepare_cdrom_arm64 {
+ local QEMU_BOOTINDEX=2 # First non-HD drive bootindex
+
+ # Only assign cdrom drives for build command (TODO: clean this up)
+ [[ "${COMMAND}" == "build" ]] || return 0;
+
+ [[ -n "${UNATTEND_ISO}" ]] && {
+ QEMU_DYN_OPTIONS+=(
+ "-drive if=none,file=${UNATTEND_ISO},media=cdrom,id=cd2"
+ "-device usb-storage,drive=cd2,bootindex=$(( QEMU_BOOTINDEX++ ))"
+ )
+ }
+
+ [[ -n "${INSTALL_ISO}" ]] && {
+ QEMU_DYN_OPTIONS+=(
+ "-drive if=none,file=${INSTALL_ISO},media=cdrom,id=cd0"
+ "-device usb-storage,drive=cd0,bootindex=$(( QEMU_BOOTINDEX++ ))"
+ )
+ }
+
+ [[ -n "${VIRTIO_ISO}" ]] && {
+ QEMU_DYN_OPTIONS+=(
+ "-drive if=none,file=${VIRTIO_ISO},media=cdrom,id=cd1"
+ "-device usb-storage,drive=cd1,bootindex=$(( QEMU_BOOTINDEX++ ))"
+ )
+ }
+}
+
+function expand_size {
+ local value="${1}"
+ [[ "${value}" =~ ^[0-9]+$ ]] && {
+ echo "${value}"
+ return 0
+ }
+
+ local unit="${value:$(( ${#value} - 1 ))}"
+ case "${unit@U}" in
+ "B") echo "$(( ${value%${unit}} * 1 ))" ;;
+ "K") echo "$(( ${value%${unit}} * 2**10 ))" ;;
+ "M") echo "$(( ${value%${unit}} * 2**20 ))" ;;
+ "G") echo "$(( ${value%${unit}} * 2**30 ))" ;;
+ "T") echo "$(( ${value%${unit}} * 2**40 ))" ;;
+ *)
+ echo "Invalid unit suffix in value: '${value}'" >&2
+ return 1 ;;
+ esac
+}
+
+function resize_image {
+ local img_file="${1}"
+
+ local img_cur_size="$(qemu_image_size "${1}")"
+ local img_req_size="$(expand_size "${2:-${img_cur_size}}")"
+ [[ "${img_cur_size}" -eq "${img_req_size}" ]] && return 0;
+ [[ "${img_cur_size}" -lt "${img_req_size}" ]] || return 1;
+
+ local img_type="$(qemu_image_type_from_fileext "${img_file}")"
+ case "${img_type}" in
+ raw)
+ echo "Resizing raw image '$(basename "${img_file}")'" >&2
+ truncate --size="${img_req_size}" "${img_file}" || return $? ;;
+ *)
+ echo "Qcow2 image resizing is currently not implemented" >&2
+ return 1 ;;
+ esac
+}
+
+function qemu_prepare_efi_code_and_vars {
+ local SRC_EFI_CODE_IMG="${1}" # Required: EFI code image path
+ local SRC_EFI_VARS_IMG="${2}" # Optional: EFI vars image path
+ local SRC_EFI_CODE_SIZE="${3}" # Optional: EFI code image size
+
+ # Code image must exist
+ [[ -f "${SRC_EFI_CODE_IMG}" ]] || {
+ echo "EFI code ROM image '${SRC_EFI_CODE_IMG}' does not exist" >&2
+ return 1
+ }
+
+ local QEMU_EFI_CODE_SIZE="$(qemu_image_size "${SRC_EFI_CODE_IMG}")"
+ local QEMU_EFI_CODE_TYPE="$(qemu_image_type_from_fileext "${SRC_EFI_CODE_IMG}")"
+
+ local QEMU_EFI_CODE="${BASEDIR}/QEMU_EFI_CODE.$(fileext_from_qemu_image_type "${QEMU_EFI_CODE_TYPE}")"
+ if [[ ! -f "${QEMU_EFI_CODE}" || "${FORCE_RESET}" -eq 1 ]]; then
+ echo "Copying EFI code ROM image '$(basename "${SRC_EFI_CODE_IMG}")'..." >&2
+ cp -f "${SRC_EFI_CODE_IMG}" "${QEMU_EFI_CODE}" || return $?
+
+ resize_image "${QEMU_EFI_CODE}" "${SRC_EFI_CODE_SIZE:-}" || return $?
+ else
+ local src_checksum="$(sha256sum -b "${SRC_EFI_CODE_IMG}" | cut -d ' ' -f1)" || return $?
+ local dst_checksum="$(sha256sum -b "${QEMU_EFI_CODE}" | cut -d ' ' -f1)" || return $?
+ if [[ "${src_checksum}" != "${dst_checksum}" ]]; then
+ echo "Copying EFI code ROM image '$(basename "${SRC_EFI_CODE_IMG}")' (changed)..." >&2
+ cp -f "${SRC_EFI_CODE_IMG}" "${QEMU_EFI_CODE}" || return $?
+
+ resize_image "${QEMU_EFI_CODE}" "${SRC_EFI_CODE_SIZE:-}" || return $?
+ fi
+ fi
+
+ #
+ # UEFI variables flash rom
+ # Intialize a fresh new raw image
+ #
+ if [[ -n "${SRC_EFI_VARS_IMG}" ]]; then
+ # Vars image must exist
+ [[ -f "${SRC_EFI_VARS_IMG}" ]] || {
+ echo "EFI variables ROM image '${SRC_EFI_VARS_IMG}' does not exist" >&2
+ return 1
+ }
+
+ local QEMU_EFI_VARS_TYPE="$(qemu_image_type_from_fileext "${SRC_EFI_VARS_IMG}")" || return $?
+ local QEMU_EFI_VARS="${BASEDIR}/QEMU_EFI_VARS.$(fileext_from_qemu_image_type "${QEMU_EFI_VARS_TYPE}")"
+ local QEMU_EFI_VARS_SIZE="$(qemu_image_size "${QEMU_EFI_VARS}")"
+
+ if [[ ! -f "${QEMU_EFI_VARS}" || "${FORCE_RESET}" -eq 1 ]]; then
+ echo "Copying EFI variables ROM image '$(basename "${SRC_EFI_VARS_IMG}")'..." >&2
+ cp -f "${SRC_EFI_VARS_IMG}" "${QEMU_EFI_VARS}" || return $?
+ else
+ local src_checksum="$(sha256sum -b "${SRC_EFI_CODE_IMG}" | cut -d ' ' -f1)" || return $?
+ local dst_checksum="$(sha256sum -b "${QEMU_EFI_VARS}" | cut -d ' ' -f1)" || return $?
+ if [[ "${src_checksum}" != "${dst_checksum}" ]]; then
+ echo "Copying EFI variables ROM image '$(basename "${SRC_EFI_VARS_IMG}")' (changed)..." >&2
+ cp -f "${SRC_EFI_VARS_IMG}" "${QEMU_EFI_VARS}" || return $?
+ fi
+ fi
+ else
+ local QEMU_EFI_VARS="${BASEDIR}/QEMU_EFI_VARS.fd"
+ local QEMU_EFI_VARS_TYPE="$(qemu_image_type_from_fileext "${QEMU_EFI_VARS}")" || return $?
+ local QEMU_EFI_VARS_SIZE="$(qemu_image_size "${QEMU_EFI_CODE}")" # Match code image size
+
+ echo "Initializing UEFI variables flash ROM..." >&2
+ case "${QEMU_EFI_VARS_TYPE}" in
+ "qcow2") qemu-img create -t qcow2 "${QEMU_EFI_VARS}" "${QEMU_EFI_VARS_SIZE}" >/dev/null || return $? ;;
+ "raw") truncate --size=${QEMU_EFI_VARS_SIZE} "${QEMU_EFI_VARS}" || return $? ;;
+ *) echo "Unknown image type '${QEMU_EFI_VARS_TYPE}'" >&2; return 1 ;;
+ esac
+ fi
+
+ # Append architecture specific configuration
+ QEMU_DYN_OPTIONS+=(
+ "-drive if=pflash,format=${QEMU_EFI_CODE_TYPE},unit=0,file=${QEMU_EFI_CODE},readonly=on"
+ "-drive if=pflash,format=${QEMU_EFI_VARS_TYPE},unit=1,file=${QEMU_EFI_VARS}"
+ )
+}
+
+function qemu_prepare_amd64 {
+ # Source images for EFI code and variables
+ local AMD64_EFI_CODE_IMG="/usr/share/edk2/OvmfX64/OVMF_CODE_4M.qcow2"
+ local AMD64_EFI_VARS_IMG=""
+ qemu_prepare_efi_code_and_vars "${AMD64_EFI_CODE_IMG}" "${AMD64_EFI_VARS_IMG}" || return $?
+ qemu_prepare_cdrom_amd64 "${INSTALL_ISO}" "${VIRTIO_ISO}" "${UNATTEND_ISO}" || return $?
+}
+
+function qemu_prepare_arm64 {
+ # Source images for EFI code and variables
+ qemu_prepare_efi_code_and_vars "${ARM64_EFI_CODE_IMG}" "${ARM64_EFI_VARS_IMG}" "${ARM64_EFI_CODE_SIZE}" || return $?
+ qemu_prepare_cdrom_arm64 "${INSTALL_ISO}" "${VIRTIO_ISO}" "${UNATTEND_ISO}" || return $?
+}
+
+function qemu_run_amd64_native {
+ echo "Starting X64 QEMU KVM..." >&2
+ qemu-system-x86_64 \
+ -machine type=q35,usb=on,acpi=on,hpet=off -accel kvm -boot menu=off \
+ ${CPU_OPTIONS_AMD64} ${MEM_OPTIONS} \
+ -device virtio-gpu-pci,edid=on,xres=1280,yres=800 -vga virtio \
+ -device qemu-xhci -device usb-kbd -device usb-tablet \
+ -device ich9-ahci,id=ahci \
+ ${QEMU_DYN_OPTIONS[@]} \
+ -netdev user,id=net0,hostfwd=tcp::2222-:22 \
+ -device virtio-net-pci,netdev=net0,mac=2A:50:A7:4E:D9:C5 \
+ -display gtk,show-tabs=on,show-menubar=on,zoom-to-fit=off \
+ -monitor unix:${BASEDIR}/monitor.sock,server,nowait \
+ -vnc unix:${BASEDIR}/vnc.sock,password=on \
+ -nodefaults
+}
+
+function qemu_run_arm64_native {
+ # Return list of the big CPU cores on the system (TODO: Check for Cortex-A72+ before attempting to start)
+ local CPU_CONFIG="$(lscpu -J -b -e=CPU,MODELNAME)"
+ [[ -n "${CPU_CONFIG}" ]] || {
+ echo "Failed to retrieve CPU configuration" >&2
+ return 1
+ }
+
+ local CPU_MODELS="$(jq -r '.cpus | map(.modelname) | unique | map(select(. | test("A72|A76"))) | join("\n")' <<< "${CPU_CONFIG}")"
+ [[ -n "${CPU_MODELS}" ]] || {
+ echo "No supported CPU models found" >&2
+ return 1
+ }
+
+ local SELECTED_CPU_CORES="$(jq -r '.cpus | group_by(.modelname) | sort_by(.[].modelname) | last | map(.cpu) | join(",")' <<< "${CPU_CONFIG}")"
+ [[ -n "${SELECTED_CPU_CORES}" ]] || {
+ echo "No supported CPU cores found" >&2
+ return 1
+ }
+
+ echo "Starting ARM64 QEMU KVM (on cores ${SELECTED_CPU_CORES})..." >&2
+ taskset -c "${SELECTED_CPU_CORES}" \
+ qemu-system-aarch64 \
+ -machine type=virt,virtualization=off,acpi=on -accel kvm -boot menu=off \
+ ${CPU_OPTIONS_ARM64} ${MEM_OPTIONS} \
+ -device virtio-gpu-pci,edid=on,xres=1280,yres=800 -device ramfb \
+ -device qemu-xhci -device usb-kbd -device usb-tablet \
+ ${QEMU_DYN_OPTIONS[@]} \
+ -netdev user,id=net0,hostfwd=tcp::2222-:22 \
+ -device virtio-net-pci,netdev=net0,mac=2A:50:A7:4E:D9:C5 \
+ -display gtk,show-tabs=on,show-menubar=on,zoom-to-fit=off \
+ -monitor unix:${BASEDIR}/monitor.sock,server,nowait \
+ -vnc unix:${BASEDIR}/vnc.sock,password=on \
+ -nodefaults
+}
+
+function qemu_run_arm64_emulated {
+ echo "Starting ARM64 QEMU TCG..." >&2
+ qemu-system-aarch64 \
+ -machine type=virt,virtualization=off,acpi=on -accel tcg,thread=multi -boot menu=off \
+ ${CPU_OPTIONS_ARM64} ${MEM_OPTIONS} \
+ -device virtio-gpu-pci,edid=on,xres=1280,yres=800 -device ramfb \
+ -device qemu-xhci -device usb-kbd -device usb-tablet \
+ ${QEMU_DYN_OPTIONS[@]} \
+ -netdev user,id=net0,hostfwd=tcp::2222-:22 \
+ -device virtio-net-pci,netdev=net0,mac=2A:50:A7:4E:D9:C5 \
+ -display gtk,show-tabs=on,show-menubar=on,zoom-to-fit=off \
+ -monitor unix:${BASEDIR}/monitor.sock,server,nowait \
+ -vnc unix:${BASEDIR}/vnc.sock,password=on \
+ -nodefaults
+}
+
+function qemu_prepare_and_run {
+ #
+ # Prepare QEMU VM resources
+ #
+ case "${GUEST_ARCH}" in
+ "amd64")
+ qemu_prepare_amd64 || return $? ;;
+ "arm64")
+ qemu_prepare_arm64 || return $? ;;
+ *)
+ echo "Unsupported guest architecture '${GUEST_ARCH}'" >&2
+ return 1 ;;
+ esac
+
+ qemu_prepare_disk "${DISK_IMG}" "${DISK_SIZE}" || return $?
+
+ #
+ # Run QEMU VM
+ #
+ case "${HOST_ARCH}:${GUEST_ARCH}" in
+ "amd64:amd64") # Native: AMD64 on AMD64
+ qemu_run_amd64_native ;;
+ "amd64:arm64") # Emulated: ARM64 on AMD64
+ echo 'NOTICE: Running ARM64 emulated on AMD64, this will be slow' >&2
+ qemu_run_arm64_emulated ;;
+ "arm64:arm64") # Native: ARM64 on ARM64
+ qemu_run_arm64_native ;;
+ *)
+ echo "Unsupported host + guest architecture combination: '${GUEST_ARCH}' on '${HOST_ARCH}'" >&2
+ return 1 ;;
+ esac
+}
+
+function recompress_disk_image {
+ local IMG_FILE="${1}"
+ local IMG_SIZE="$(stat -c '%s' "${IMG_FILE}")" || return $?
+
+ [[ ${IMG_SIZE} -lt $(( 2 ** 30 )) ]] && {
+ echo 'Disk is smaller than 1 GiB, skipping recompression' >&2
+ return 0
+ }
+
+ echo "Optimizing QEMU disk image..." >&2
+ qemu-img convert -p -c -W -f qcow2 -O qcow2 "${IMG_FILE}" "${IMG_FILE}.tmp" || {
+ rm -f "${IMG_FILE}.tmp" 2>/dev/null
+ return 1
+ }
+
+ local IMG_SIZE_ORG="$(stat -c 'scale=2; %s / 2^30' "${IMG_FILE}" | bc)"
+ local IMG_SIZE_OPT="$(stat -c 'scale=2; %s / 2^30' "${IMG_FILE}.tmp" | bc)"
+ local PERCENT="$(bc <<< "scale=2; (${IMG_SIZE_OPT} * 100) / ${IMG_SIZE_ORG}")"
+ echo "'$(basename "${IMG_FILE}")': ${IMG_SIZE_ORG} GiB -> ${IMG_SIZE_OPT} GiB (${PERCENT} %)" >&2
+ mv -f "${IMG_FILE}.tmp" "${IMG_FILE}" || return $?
+}
+
+function handle_build_command {
+ qemu_prepare_and_run || return $?
+ recompress_disk_image "${DISK_IMG}" || return $?
+}
+
+function handle_run_command {
+ qemu_prepare_and_run || return $?
+}
+
+#
+# Handle user-supplied command
+#
+case "${COMMAND@L}" in
+"build") handle_build_command || exit $? ;;
+"run") handle_run_command || exit $? ;;
+esac