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</PlainText> + </Password> + </LocalAccount> + <LocalAccount wcm:action="add"> + <Name>User</Name> + <DisplayName></DisplayName> + <Group>Users</Group> + <Password> + <Value>user</Value> + <PlainText>true</PlainText> + </Password> + </LocalAccount> + </LocalAccounts> + </UserAccounts> + <AutoLogon> + <Username>Admin</Username> + <Enabled>true</Enabled> + <Password> + <Value>secret</Value> + <PlainText>true</PlainText> + </Password> + </AutoLogon> + <OOBE> + <ProtectYourPC>3</ProtectYourPC> + <HideEULAPage>true</HideEULAPage> + <HideOEMRegistrationScreen>true</HideOEMRegistrationScreen> + <HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE> + <HideOnlineAccountScreens>true</HideOnlineAccountScreens> + </OOBE> + <FirstLogonCommands> + <SynchronousCommand wcm:action="add"> + <Order>1</Order> + <CommandLine>powershell.exe -WindowStyle "Normal" -ExecutionPolicy "Unrestricted" -NoProfile -File "C:\Windows\Setup\Scripts\FirstLogon.ps1"</CommandLine> + </SynchronousCommand> + </FirstLogonCommands> + </component> + </settings> + <Extensions xmlns="https://schneegans.de/windows/unattend-generator/"> + <Build> + <Commit> + <Hash>1cbe01daa2a4b8df5548c4a5eb1cce7f28699acd</Hash> + <GitHubUrl>https://github.com/cschneegans/unattend-generator/commit/1cbe01daa2a4b8df5548c4a5eb1cce7f28699acd</GitHubUrl> + </Commit> + </Build> + <ExtractScript> +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 ); +} + </ExtractScript> + <File path="C:\Windows\Setup\Scripts\RemovePackages.ps1"> +$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'; +&amp; { + $installed = &amp; $getCommand; + foreach( $selector in $selectors ) { + $result = [ordered] @{ + Selector = $selector; + }; + $found = $installed | Where-Object -FilterScript $filterCommand; + if( $found ) { + $result.Output = $found | &amp; $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; + } +} *&gt;&amp;1 | Out-String -Width 1KB -Stream &gt;&gt; $logfile; + </File> + <File path="C:\Windows\Setup\Scripts\RemoveCapabilities.ps1"> +$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'; +&amp; { + $installed = &amp; $getCommand; + foreach( $selector in $selectors ) { + $result = [ordered] @{ + Selector = $selector; + }; + $found = $installed | Where-Object -FilterScript $filterCommand; + if( $found ) { + $result.Output = $found | &amp; $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; + } +} *&gt;&amp;1 | Out-String -Width 1KB -Stream &gt;&gt; $logfile; + </File> + <File path="C:\Windows\Setup\Scripts\RemoveFeatures.ps1"> +$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'; +&amp; { + $installed = &amp; $getCommand; + foreach( $selector in $selectors ) { + $result = [ordered] @{ + Selector = $selector; + }; + $found = $installed | Where-Object -FilterScript $filterCommand; + if( $found ) { + $result.Output = $found | &amp; $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; + } +} *&gt;&amp;1 | Out-String -Width 1KB -Stream &gt;&gt; $logfile; + </File> + <File path="C:\Windows\Setup\Scripts\PauseWindowsUpdate.xml"> +&lt;Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task"&gt; + &lt;Triggers&gt; + &lt;BootTrigger&gt; + &lt;Repetition&gt; + &lt;Interval&gt;P1D&lt;/Interval&gt; + &lt;StopAtDurationEnd&gt;false&lt;/StopAtDurationEnd&gt; + &lt;/Repetition&gt; + &lt;Enabled&gt;true&lt;/Enabled&gt; + &lt;/BootTrigger&gt; + &lt;/Triggers&gt; + &lt;Principals&gt; + &lt;Principal id="Author"&gt; + &lt;UserId&gt;S-1-5-19&lt;/UserId&gt; + &lt;RunLevel&gt;LeastPrivilege&lt;/RunLevel&gt; + &lt;/Principal&gt; + &lt;/Principals&gt; + &lt;Settings&gt; + &lt;MultipleInstancesPolicy&gt;IgnoreNew&lt;/MultipleInstancesPolicy&gt; + &lt;DisallowStartIfOnBatteries&gt;false&lt;/DisallowStartIfOnBatteries&gt; + &lt;StopIfGoingOnBatteries&gt;false&lt;/StopIfGoingOnBatteries&gt; + &lt;AllowHardTerminate&gt;true&lt;/AllowHardTerminate&gt; + &lt;StartWhenAvailable&gt;false&lt;/StartWhenAvailable&gt; + &lt;RunOnlyIfNetworkAvailable&gt;false&lt;/RunOnlyIfNetworkAvailable&gt; + &lt;IdleSettings&gt; + &lt;StopOnIdleEnd&gt;true&lt;/StopOnIdleEnd&gt; + &lt;RestartOnIdle&gt;false&lt;/RestartOnIdle&gt; + &lt;/IdleSettings&gt; + &lt;AllowStartOnDemand&gt;true&lt;/AllowStartOnDemand&gt; + &lt;Enabled&gt;true&lt;/Enabled&gt; + &lt;Hidden&gt;false&lt;/Hidden&gt; + &lt;RunOnlyIfIdle&gt;false&lt;/RunOnlyIfIdle&gt; + &lt;WakeToRun&gt;false&lt;/WakeToRun&gt; + &lt;ExecutionTimeLimit&gt;PT72H&lt;/ExecutionTimeLimit&gt; + &lt;Priority&gt;7&lt;/Priority&gt; + &lt;/Settings&gt; + &lt;Actions Context="Author"&gt; + &lt;Exec&gt; + &lt;Command&gt;C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe&lt;/Command&gt; + &lt;Arguments&gt;-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; };"&lt;/Arguments&gt; + &lt;/Exec&gt; + &lt;/Actions&gt; +&lt;/Task&gt; + </File> + <File path="C:\Windows\Setup\Scripts\MoveActiveHours.xml"> +&lt;Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task"&gt; + &lt;Triggers&gt; + &lt;BootTrigger&gt; + &lt;Repetition&gt; + &lt;Interval&gt;PT4H&lt;/Interval&gt; + &lt;StopAtDurationEnd&gt;false&lt;/StopAtDurationEnd&gt; + &lt;/Repetition&gt; + &lt;Enabled&gt;true&lt;/Enabled&gt; + &lt;/BootTrigger&gt; + &lt;RegistrationTrigger&gt; + &lt;Repetition&gt; + &lt;Interval&gt;PT4H&lt;/Interval&gt; + &lt;StopAtDurationEnd&gt;false&lt;/StopAtDurationEnd&gt; + &lt;/Repetition&gt; + &lt;Enabled&gt;true&lt;/Enabled&gt; + &lt;/RegistrationTrigger&gt; + &lt;/Triggers&gt; + &lt;Principals&gt; + &lt;Principal id="Author"&gt; + &lt;UserId&gt;S-1-5-19&lt;/UserId&gt; + &lt;RunLevel&gt;LeastPrivilege&lt;/RunLevel&gt; + &lt;/Principal&gt; + &lt;/Principals&gt; + &lt;Settings&gt; + &lt;MultipleInstancesPolicy&gt;IgnoreNew&lt;/MultipleInstancesPolicy&gt; + &lt;DisallowStartIfOnBatteries&gt;false&lt;/DisallowStartIfOnBatteries&gt; + &lt;StopIfGoingOnBatteries&gt;false&lt;/StopIfGoingOnBatteries&gt; + &lt;AllowHardTerminate&gt;true&lt;/AllowHardTerminate&gt; + &lt;StartWhenAvailable&gt;false&lt;/StartWhenAvailable&gt; + &lt;RunOnlyIfNetworkAvailable&gt;false&lt;/RunOnlyIfNetworkAvailable&gt; + &lt;IdleSettings&gt; + &lt;StopOnIdleEnd&gt;true&lt;/StopOnIdleEnd&gt; + &lt;RestartOnIdle&gt;false&lt;/RestartOnIdle&gt; + &lt;/IdleSettings&gt; + &lt;AllowStartOnDemand&gt;true&lt;/AllowStartOnDemand&gt; + &lt;Enabled&gt;true&lt;/Enabled&gt; + &lt;Hidden&gt;false&lt;/Hidden&gt; + &lt;RunOnlyIfIdle&gt;false&lt;/RunOnlyIfIdle&gt; + &lt;WakeToRun&gt;false&lt;/WakeToRun&gt; + &lt;ExecutionTimeLimit&gt;PT72H&lt;/ExecutionTimeLimit&gt; + &lt;Priority&gt;7&lt;/Priority&gt; + &lt;/Settings&gt; + &lt;Actions Context="Author"&gt; + &lt;Exec&gt; + &lt;Command&gt;%windir%\System32\conhost.exe&lt;/Command&gt; + &lt;Arguments&gt;--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;"&lt;/Arguments&gt; + &lt;/Exec&gt; + &lt;/Actions&gt; +&lt;/Task&gt; + </File> + <File path="C:\Windows\Setup\Scripts\TurnOffSystemSounds.ps1"> +$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 ''; + </File> + <File path="C:\Windows\Setup\Scripts\SetStartPins.ps1"> +$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'; + </File> + <File path="C:\Users\Default\AppData\Local\Microsoft\Windows\Shell\LayoutModification.xml"> +&lt;LayoutModificationTemplate Version="1" xmlns="http://schemas.microsoft.com/Start/2014/LayoutModification"&gt; + &lt;LayoutOptions StartTileGroupCellWidth="6" /&gt; + &lt;DefaultLayoutOverride&gt; + &lt;StartLayoutCollection&gt; + &lt;StartLayout GroupCellWidth="6" xmlns="http://schemas.microsoft.com/Start/2014/FullDefaultLayout" /&gt; + &lt;/StartLayoutCollection&gt; + &lt;/DefaultLayoutOverride&gt; +&lt;/LayoutModificationTemplate&gt; + </File> + <File path="C:\Windows\Setup\Scripts\SetWallpaper.ps1"> +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'; + </File> + <File path="C:\Windows\Setup\Scripts\Specialize.ps1"> +$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; + }; + { + &amp; 'C:\Windows\Setup\Scripts\RemovePackages.ps1'; + }; + { + &amp; 'C:\Windows\Setup\Scripts\RemoveCapabilities.ps1'; + }; + { + &amp; '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; + }; + { + &amp; '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; + }; +); + +&amp; { + [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 &#xAB;{0}&#xBB;.' -f $( + $script.ToString().Trim() -replace '\s+', ' ' -replace '^(.{99})(.+)$', '$1&#x2026;'; + ); + $start = [datetime]::Now; + &amp; $script; + '*** Finished executing command after {0:0} ms.' -f [datetime]::Now.Subtract( $start ).TotalMilliseconds; + "`r`n" * 3; + $complete += $increment; + } +} *&gt;&amp;1 | Out-String -Width 1KB -Stream &gt;&gt; "C:\Windows\Setup\Scripts\Specialize.log"; + </File> + <File path="C:\Windows\Setup\Scripts\UserOnce.ps1"> +$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; + }; + { + &amp; '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; + }; +); + +&amp; { + [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 &#xAB;{0}&#xBB;.' -f $( + $script.ToString().Trim() -replace '\s+', ' ' -replace '^(.{99})(.+)$', '$1&#x2026;'; + ); + $start = [datetime]::Now; + &amp; $script; + '*** Finished executing command after {0:0} ms.' -f [datetime]::Now.Subtract( $start ).TotalMilliseconds; + "`r`n" * 3; + $complete += $increment; + } +} *&gt;&amp;1 | Out-String -Width 1KB -Stream &gt;&gt; "$env:TEMP\UserOnce.log"; + </File> + <File path="C:\Windows\Setup\Scripts\DefaultUser.ps1"> +$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; + }; + { + &amp; '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; + }; +); + +&amp; { + [float] $complete = 0; + [float] $increment = 100 / $scripts.Count; + foreach( $script in $scripts ) { + Write-Progress -Id 0 -Activity 'Running scripts to modify the default user&#x2019;&#x2019;s registry hive. Do not close this window.' -PercentComplete $complete; + '*** Will now execute command &#xAB;{0}&#xBB;.' -f $( + $script.ToString().Trim() -replace '\s+', ' ' -replace '^(.{99})(.+)$', '$1&#x2026;'; + ); + $start = [datetime]::Now; + &amp; $script; + '*** Finished executing command after {0:0} ms.' -f [datetime]::Now.Subtract( $start ).TotalMilliseconds; + "`r`n" * 3; + $complete += $increment; + } +} *&gt;&amp;1 | Out-String -Width 1KB -Stream &gt;&gt; "C:\Windows\Setup\Scripts\DefaultUser.log"; + </File> + <File path="C:\Windows\Setup\Scripts\FirstLogon.ps1"> +$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'; + }; + { + &amp; 'C:\Windows\Setup\Scripts\WinRM.ps1'; + }; + { + &amp; 'C:\Windows\Setup\Scripts\OpenSSH.ps1'; + }; + { + # &amp; 'C:\Windows\Setup\Scripts\InstallChocolatey.ps1'; + }; + { + # &amp; 'C:\Windows\Setup\Scripts\InstallPython.ps1'; + }; + { + # &amp; 'C:\Windows\Setup\Scripts\InstallNodeJS.ps1'; + }; + { + # &amp; 'C:\Windows\Setup\Scripts\InstallGit.ps1'; + }; + { + # &amp; 'C:\Windows\Setup\Scripts\InstallVisualStudio.ps1'; + }; + { + &amp; '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" + }; +); + +&amp; { + [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 &#xAB;{0}&#xBB;.' -f $( + $script.ToString().Trim() -replace '\s+', ' ' -replace '^(.{99})(.+)$', '$1&#x2026;'; + ); + $start = [datetime]::Now; + &amp; $script; + '*** Finished executing command after {0:0} ms.' -f [datetime]::Now.Subtract( $start ).TotalMilliseconds; + "`r`n" * 3; + $complete += $increment; + } +} *&gt;&amp;1 | Out-String -Width 1KB -Stream &gt;&gt; "C:\Windows\Setup\Scripts\FirstLogon.log"; + </File> + <!-- + ##### custom scripts start ##### + --> + <!-- Windows remote management interface setup script --> + <File path="C:\Windows\Setup\Scripts\WinRM.ps1"> +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 + </File> + <!-- OpenSSH server installation script --> + <File path="C:\Windows\Setup\Scripts\OpenSSH.ps1"> +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..." +&amp; "$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 + </File> + <!-- Chocolatey installation script (unused) --> + <File path="C:\Windows\Setup\Scripts\InstallChocolatey.ps1"> +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')) + </File> + + <!-- + Application / package installation script + + Previously based on chocolatey, but that does not support Arm64 natively, so... + --> + <File path="C:\Windows\Setup\Scripts\InstallGit.ps1"> +# +# Common defines +# +$cpu_arch = $env:PROCESSOR_ARCHITECTURE.ToLower(); + +# +# Git +# +$git_version = "2.54.0"; +$git_url = &amp; { + 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'; + </File> + + <File path="C:\Windows\Setup\Scripts\InstallPython.ps1"> +# +# 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'; + </File> + + <File path="C:\Windows\Setup\Scripts\InstallNodeJS.ps1"> +# +# Common defines +# +$cpu_arch = $env:PROCESSOR_ARCHITECTURE.ToLower(); + +# +# NodeJS (LTS) +# +$node_version = "24.15.0"; +$node_url = &amp; { + 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'; + </File> + + <!-- + VisualStudio 2022 / 2026 installation script + + NOTE: Installation fails on Arm64 + --> + <File path="C:\Windows\Setup\Scripts\InstallVisualStudio.ps1"> +# +# 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'; + </File> + <!-- + WinFSP and Virtio FS installation + --> + <File path="C:\Windows\Setup\Scripts\WinFSP.ps1"> +# 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; + </File> + <!-- + ##### custom scripts end ##### + --> + </Extensions> +</unattend> 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 @@ +<?xml version="1.0" encoding="utf-8"?> +<unattend xmlns="urn:schemas-microsoft-com:unattend" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State"> + <!--https://schneegans.de/windows/unattend-generator/?LanguageMode=Unattended&UILanguage=en-US&GeoLocation=94&Locale=de-DE&Keyboard=00000407&PEMode=Generated&DisableDefender=true&SkipIntegrityCheck=true&InstallFromMode=Name&InstallFromName=Windows+11+Pro&PartitionMode=Unattended&TargetDisk=0&PartitionLayout=GPT&EspSize=300&RecoveryMode=None&DiskAssertionMode=Skip&WindowsEditionMode=Generic&WindowsEdition=pro&ProcessorArchitecture=amd64&BypassRequirementsCheck=true&BypassNetworkCheck=true&UseConfigurationSet=true&ComputerNameMode=Random&TimeZoneMode=Implicit&UserAccountMode=Unattended&AccountName0=Admin&AccountDisplayName0=&AccountPassword0=secret&AccountGroup0=Administrators&AccountName1=User&AccountDisplayName1=&AccountPassword1=user&AccountGroup1=Users&AutoLogonMode=Own&PasswordExpirationMode=Unlimited&LockoutMode=Disabled&HideFiles=HiddenSystem&ShowFileExtensions=true&TaskbarSearch=Hide&TaskbarIconsMode=Default&DisableWidgets=true&LeftTaskbar=true&DisableBingResults=true&StartTilesMode=Empty&StartPinsMode=Empty&DisableWindowsUpdate=true&DisableUac=true&DisableSac=true&DisableSmartScreen=true&DisableSystemRestore=true&EnableLongPaths=true&DeleteJunctions=true&AllowPowerShellScripts=true&DisableLastAccess=true&PreventAutomaticReboot=true&TurnOffSystemSounds=true&DisableAppSuggestions=true&PreventDeviceEncryption=true&HideEdgeFre=true&DisableEdgeStartupBoost=true&DeleteWindowsOld=true&DisableAutomaticRestartSignOn=true&EffectsMode=Performance&DesktopIconsMode=Default&StartFoldersMode=Default&WifiMode=Skip&ExpressSettings=DisableAll&LockKeysMode=Skip&StickyKeysMode=Disabled&ColorMode=Default&WallpaperMode=Solid&WallpaperColor=%23008080&LockScreenMode=Default&Remove3DViewer=true&RemoveBingSearch=true&RemoveCamera=true&RemoveClipchamp=true&RemoveClock=true&RemoveCopilot=true&RemoveCortana=true&RemoveDevHome=true&RemoveWindowsHello=true&RemoveFamily=true&RemoveFeedbackHub=true&RemoveGameAssist=true&RemoveGetHelp=true&RemoveHandwriting=true&RemoveInternetExplorer=true&RemoveMailCalendar=true&RemoveMaps=true&RemoveMathInputPanel=true&RemoveMediaFeatures=true&RemoveStore=true&RemoveMixedReality=true&RemoveZuneVideo=true&RemoveNews=true&RemoveOffice365=true&RemoveOneDrive=true&RemoveOneNote=true&RemoveOneSync=true&RemoveOutlook=true&RemovePaint=true&RemovePaint3D=true&RemovePeople=true&RemovePhotos=true&RemovePowerAutomate=true&RemoveQuickAssist=true&RemoveRecall=true&RemoveRdpClient=true&RemoveSkype=true&RemoveSnippingTool=true&RemoveSolitaire=true&RemoveSpeech=true&RemoveStepsRecorder=true&RemoveStickyNotes=true&RemoveTeams=true&RemoveGetStarted=true&RemoveToDo=true&RemoveVoiceRecorder=true&RemoveWallet=true&RemoveWeather=true&RemoveFaxAndScan=true&RemoveWindowsMediaPlayer=true&RemoveZuneMusic=true&RemoveWordPad=true&RemoveXboxApps=true&RemoveYourPhone=true&WdacMode=Skip&AppLockerMode=Skip--> + <settings pass="offlineServicing"></settings> + <settings pass="windowsPE"> + <component name="Microsoft-Windows-International-Core-WinPE" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> + <SetupUILanguage> + <UILanguage>en-US</UILanguage> + </SetupUILanguage> + <InputLocale>de-DE</InputLocale> + <SystemLocale>en-US</SystemLocale> + <UILanguage>en-US</UILanguage> + <UserLocale>en-US</UserLocale> + <UILanguageFallback>en-US</UILanguageFallback> + </component> + <component name="Microsoft-Windows-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS"> + <UseConfigurationSet>true</UseConfigurationSet> + <DiskConfiguration> + <WillShowUI>OnError</WillShowUI> + <DisableEncryptedDiskProvisioning>true</DisableEncryptedDiskProvisioning> + <Disk wcm:action="add"> + <DiskID>0</DiskID> + <WillWipeDisk>true</WillWipeDisk> + <CreatePartitions> + <CreatePartition wcm:action="add"> + <Order>1</Order> + <Type>EFI</Type> + <Size>300</Size> + </CreatePartition> + <CreatePartition wcm:action="add"> + <Order>2</Order> + <Type>MSR</Type> + <Size>100</Size> + </CreatePartition> + <CreatePartition wcm:action="add"> + <Order>3</Order> + <Type>Primary</Type> + <Extend>true</Extend> + </CreatePartition> + </CreatePartitions> + <ModifyPartitions> + <ModifyPartition wcm:action="add"> + <Order>1</Order> + <PartitionID>3</PartitionID> + <Label>Windows</Label> + <Letter>C</Letter> + <Format>NTFS</Format> + </ModifyPartition> + </ModifyPartitions> + </Disk> + </DiskConfiguration> + <UserData> + <AcceptEula>true</AcceptEula> + <ProductKey> + <!-- generic windows 11 pro product key --> + <Key>VK7JG-NPHTM-C97JM-9MPGT-3V66T</Key> + <WillShowUI>Never</WillShowUI> + </ProductKey> + </UserData> + <UpgradeData> + <Upgrade>false</Upgrade> + <WillShowUI>OnError</WillShowUI> + </UpgradeData> + <DynamicUpdate> + <Enable>false</Enable> + <WillShowUI>OnError</WillShowUI> + </DynamicUpdate> + <ImageInstall> + <OSImage> + <Compact>true</Compact> + <InstallToAvailablePartition>false</InstallToAvailablePartition> + <InstallTo> + <DiskID>0</DiskID> + <PartitionID>3</PartitionID> + </InstallTo> + <WillShowUI>OnError</WillShowUI> + </OSImage> + </ImageInstall> + </component> + <component name="Microsoft-Windows-PnpCustomizationsWinPE" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS"> + <DriverPaths> + <PathAndCredentials wcm:action="add" wcm:keyValue="1"> + <Path>%configsetroot%\drivers</Path> + </PathAndCredentials> + </DriverPaths> + </component> + </settings> + <settings pass="generalize"> + <component name="Microsoft-Windows-Security-SPP" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS"> + <SkipRearm>1</SkipRearm> + </component> + </settings> + <settings pass="specialize"> + <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS"> + <ComputerName>nano11</ComputerName> + </component> + <component name="Microsoft-Windows-Security-SPP-UX" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS"> + <SkipAutoActivation>true</SkipAutoActivation> + </component> + <component name="Microsoft-Windows-SQMApi" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS"> + <CEIPEnabled>0</CEIPEnabled> + </component> + <component name="Microsoft-Windows-Deployment" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS"> + <RunSynchronous> + <RunSynchronousCommand wcm:action="add"> + <Order>1</Order> + <Path>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;"</Path> + </RunSynchronousCommand> + <RunSynchronousCommand wcm:action="add"> + <Order>2</Order> + <Path>powershell.exe -WindowStyle "Normal" -ExecutionPolicy "Unrestricted" -NoProfile -File "C:\Windows\Setup\Scripts\Specialize.ps1"</Path> + </RunSynchronousCommand> + <RunSynchronousCommand wcm:action="add"> + <Order>3</Order> + <Path>reg.exe load "HKU\DefaultUser" "C:\Users\Default\NTUSER.DAT"</Path> + </RunSynchronousCommand> + <RunSynchronousCommand wcm:action="add"> + <Order>4</Order> + <Path>powershell.exe -WindowStyle "Normal" -ExecutionPolicy "Unrestricted" -NoProfile -File "C:\Windows\Setup\Scripts\DefaultUser.ps1"</Path> + </RunSynchronousCommand> + <RunSynchronousCommand wcm:action="add"> + <Order>5</Order> + <Path>reg.exe unload "HKU\DefaultUser"</Path> + </RunSynchronousCommand> + </RunSynchronous> + </component> + </settings> + <settings pass="auditSystem"></settings> + <settings pass="auditUser"></settings> + <settings pass="oobeSystem"> + <component name="Microsoft-Windows-International-Core" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS"> + <InputLocale>de-DE</InputLocale> + <SystemLocale>en-US</SystemLocale> + <UILanguage>en-US</UILanguage> + <UserLocale>en-US</UserLocale> + </component> + <component name="Microsoft-Windows-SecureStartup-FilterDriver" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS"> + <PreventDeviceEncryption>true</PreventDeviceEncryption> + </component> + <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS"> + <ConfigureChatAutoInstall>false</ConfigureChatAutoInstall> + <UserAccounts> + <LocalAccounts> + <LocalAccount wcm:action="add"> + <Name>Admin</Name> + <DisplayName></DisplayName> + <Group>Administrators</Group> + <Password> + <Value>secret</Value> + <PlainText>true</PlainText> + </Password> + </LocalAccount> + <LocalAccount wcm:action="add"> + <Name>User</Name> + <DisplayName></DisplayName> + <Group>Users</Group> + <Password> + <Value>user</Value> + <PlainText>true</PlainText> + </Password> + </LocalAccount> + </LocalAccounts> + </UserAccounts> + <AutoLogon> + <Username>Admin</Username> + <Enabled>true</Enabled> + <Password> + <Value>secret</Value> + <PlainText>true</PlainText> + </Password> + </AutoLogon> + <OOBE> + <ProtectYourPC>3</ProtectYourPC> + <HideEULAPage>true</HideEULAPage> + <HideOEMRegistrationScreen>true</HideOEMRegistrationScreen> + <HideOnlineAccountScreens>true</HideOnlineAccountScreens> + <HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE> + <NetworkLocation>Work</NetworkLocation> + <SkipMachineOOBE>true</SkipMachineOOBE> + <SkipUserOOBE>true</SkipUserOOBE> + </OOBE> + <FirstLogonCommands> + <SynchronousCommand wcm:action="add"> + <Order>1</Order> + <CommandLine>powershell.exe -WindowStyle "Normal" -ExecutionPolicy "Unrestricted" -NoProfile -File "C:\Windows\Setup\Scripts\FirstLogon.ps1"</CommandLine> + </SynchronousCommand> + </FirstLogonCommands> + </component> + </settings> + <Extensions xmlns="https://schneegans.de/windows/unattend-generator/"> + <Build> + <Commit> + <Hash>1cbe01daa2a4b8df5548c4a5eb1cce7f28699acd</Hash> + <GitHubUrl>https://github.com/cschneegans/unattend-generator/commit/1cbe01daa2a4b8df5548c4a5eb1cce7f28699acd</GitHubUrl> + </Commit> + </Build> + <ExtractScript> +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 ); +} + </ExtractScript> + <File path="C:\Windows\Setup\Scripts\RemovePackages.ps1"> +$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'; +&amp; { + $installed = &amp; $getCommand; + foreach( $selector in $selectors ) { + $result = [ordered] @{ + Selector = $selector; + }; + $found = $installed | Where-Object -FilterScript $filterCommand; + if( $found ) { + $result.Output = $found | &amp; $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; + } +} *&gt;&amp;1 | Out-String -Width 1KB -Stream &gt;&gt; $logfile; + </File> + <File path="C:\Windows\Setup\Scripts\RemoveCapabilities.ps1"> +$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'; +&amp; { + $installed = &amp; $getCommand; + foreach( $selector in $selectors ) { + $result = [ordered] @{ + Selector = $selector; + }; + $found = $installed | Where-Object -FilterScript $filterCommand; + if( $found ) { + $result.Output = $found | &amp; $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; + } +} *&gt;&amp;1 | Out-String -Width 1KB -Stream &gt;&gt; $logfile; + </File> + <File path="C:\Windows\Setup\Scripts\RemoveFeatures.ps1"> +$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'; +&amp; { + $installed = &amp; $getCommand; + foreach( $selector in $selectors ) { + $result = [ordered] @{ + Selector = $selector; + }; + $found = $installed | Where-Object -FilterScript $filterCommand; + if( $found ) { + $result.Output = $found | &amp; $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; + } +} *&gt;&amp;1 | Out-String -Width 1KB -Stream &gt;&gt; $logfile; + </File> + <File path="C:\Windows\Setup\Scripts\PauseWindowsUpdate.xml"> +&lt;Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task"&gt; + &lt;Triggers&gt; + &lt;BootTrigger&gt; + &lt;Repetition&gt; + &lt;Interval&gt;P1D&lt;/Interval&gt; + &lt;StopAtDurationEnd&gt;false&lt;/StopAtDurationEnd&gt; + &lt;/Repetition&gt; + &lt;Enabled&gt;true&lt;/Enabled&gt; + &lt;/BootTrigger&gt; + &lt;/Triggers&gt; + &lt;Principals&gt; + &lt;Principal id="Author"&gt; + &lt;UserId&gt;S-1-5-19&lt;/UserId&gt; + &lt;RunLevel&gt;LeastPrivilege&lt;/RunLevel&gt; + &lt;/Principal&gt; + &lt;/Principals&gt; + &lt;Settings&gt; + &lt;MultipleInstancesPolicy&gt;IgnoreNew&lt;/MultipleInstancesPolicy&gt; + &lt;DisallowStartIfOnBatteries&gt;false&lt;/DisallowStartIfOnBatteries&gt; + &lt;StopIfGoingOnBatteries&gt;false&lt;/StopIfGoingOnBatteries&gt; + &lt;AllowHardTerminate&gt;true&lt;/AllowHardTerminate&gt; + &lt;StartWhenAvailable&gt;false&lt;/StartWhenAvailable&gt; + &lt;RunOnlyIfNetworkAvailable&gt;false&lt;/RunOnlyIfNetworkAvailable&gt; + &lt;IdleSettings&gt; + &lt;StopOnIdleEnd&gt;true&lt;/StopOnIdleEnd&gt; + &lt;RestartOnIdle&gt;false&lt;/RestartOnIdle&gt; + &lt;/IdleSettings&gt; + &lt;AllowStartOnDemand&gt;true&lt;/AllowStartOnDemand&gt; + &lt;Enabled&gt;true&lt;/Enabled&gt; + &lt;Hidden&gt;false&lt;/Hidden&gt; + &lt;RunOnlyIfIdle&gt;false&lt;/RunOnlyIfIdle&gt; + &lt;WakeToRun&gt;false&lt;/WakeToRun&gt; + &lt;ExecutionTimeLimit&gt;PT72H&lt;/ExecutionTimeLimit&gt; + &lt;Priority&gt;7&lt;/Priority&gt; + &lt;/Settings&gt; + &lt;Actions Context="Author"&gt; + &lt;Exec&gt; + &lt;Command&gt;C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe&lt;/Command&gt; + &lt;Arguments&gt;-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; };"&lt;/Arguments&gt; + &lt;/Exec&gt; + &lt;/Actions&gt; +&lt;/Task&gt; + </File> + <File path="C:\Windows\Setup\Scripts\MoveActiveHours.xml"> +&lt;Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task"&gt; + &lt;Triggers&gt; + &lt;BootTrigger&gt; + &lt;Repetition&gt; + &lt;Interval&gt;PT4H&lt;/Interval&gt; + &lt;StopAtDurationEnd&gt;false&lt;/StopAtDurationEnd&gt; + &lt;/Repetition&gt; + &lt;Enabled&gt;true&lt;/Enabled&gt; + &lt;/BootTrigger&gt; + &lt;RegistrationTrigger&gt; + &lt;Repetition&gt; + &lt;Interval&gt;PT4H&lt;/Interval&gt; + &lt;StopAtDurationEnd&gt;false&lt;/StopAtDurationEnd&gt; + &lt;/Repetition&gt; + &lt;Enabled&gt;true&lt;/Enabled&gt; + &lt;/RegistrationTrigger&gt; + &lt;/Triggers&gt; + &lt;Principals&gt; + &lt;Principal id="Author"&gt; + &lt;UserId&gt;S-1-5-19&lt;/UserId&gt; + &lt;RunLevel&gt;LeastPrivilege&lt;/RunLevel&gt; + &lt;/Principal&gt; + &lt;/Principals&gt; + &lt;Settings&gt; + &lt;MultipleInstancesPolicy&gt;IgnoreNew&lt;/MultipleInstancesPolicy&gt; + &lt;DisallowStartIfOnBatteries&gt;false&lt;/DisallowStartIfOnBatteries&gt; + &lt;StopIfGoingOnBatteries&gt;false&lt;/StopIfGoingOnBatteries&gt; + &lt;AllowHardTerminate&gt;true&lt;/AllowHardTerminate&gt; + &lt;StartWhenAvailable&gt;false&lt;/StartWhenAvailable&gt; + &lt;RunOnlyIfNetworkAvailable&gt;false&lt;/RunOnlyIfNetworkAvailable&gt; + &lt;IdleSettings&gt; + &lt;StopOnIdleEnd&gt;true&lt;/StopOnIdleEnd&gt; + &lt;RestartOnIdle&gt;false&lt;/RestartOnIdle&gt; + &lt;/IdleSettings&gt; + &lt;AllowStartOnDemand&gt;true&lt;/AllowStartOnDemand&gt; + &lt;Enabled&gt;true&lt;/Enabled&gt; + &lt;Hidden&gt;false&lt;/Hidden&gt; + &lt;RunOnlyIfIdle&gt;false&lt;/RunOnlyIfIdle&gt; + &lt;WakeToRun&gt;false&lt;/WakeToRun&gt; + &lt;ExecutionTimeLimit&gt;PT72H&lt;/ExecutionTimeLimit&gt; + &lt;Priority&gt;7&lt;/Priority&gt; + &lt;/Settings&gt; + &lt;Actions Context="Author"&gt; + &lt;Exec&gt; + &lt;Command&gt;%windir%\System32\conhost.exe&lt;/Command&gt; + &lt;Arguments&gt;--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;"&lt;/Arguments&gt; + &lt;/Exec&gt; + &lt;/Actions&gt; +&lt;/Task&gt; + </File> + <File path="C:\Windows\Setup\Scripts\TurnOffSystemSounds.ps1"> +$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 ''; + </File> + <File path="C:\Windows\Setup\Scripts\SetStartPins.ps1"> +$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'; + </File> + <File path="C:\Users\Default\AppData\Local\Microsoft\Windows\Shell\LayoutModification.xml"> +&lt;LayoutModificationTemplate Version="1" xmlns="http://schemas.microsoft.com/Start/2014/LayoutModification"&gt; + &lt;LayoutOptions StartTileGroupCellWidth="6" /&gt; + &lt;DefaultLayoutOverride&gt; + &lt;StartLayoutCollection&gt; + &lt;StartLayout GroupCellWidth="6" xmlns="http://schemas.microsoft.com/Start/2014/FullDefaultLayout" /&gt; + &lt;/StartLayoutCollection&gt; + &lt;/DefaultLayoutOverride&gt; +&lt;/LayoutModificationTemplate&gt; + </File> + <File path="C:\Windows\Setup\Scripts\SetWallpaper.ps1"> +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'; + </File> + <File path="C:\Windows\Setup\Scripts\Specialize.ps1"> +$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; + }; + { + &amp; 'C:\Windows\Setup\Scripts\RemovePackages.ps1'; + }; + { + &amp; 'C:\Windows\Setup\Scripts\RemoveCapabilities.ps1'; + }; + { + &amp; '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; + }; + { + &amp; '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; + }; +); + +&amp; { + [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 &#xAB;{0}&#xBB;.' -f $( + $script.ToString().Trim() -replace '\s+', ' ' -replace '^(.{99})(.+)$', '$1&#x2026;'; + ); + $start = [datetime]::Now; + &amp; $script; + '*** Finished executing command after {0:0} ms.' -f [datetime]::Now.Subtract( $start ).TotalMilliseconds; + "`r`n" * 3; + $complete += $increment; + } +} *&gt;&amp;1 | Out-String -Width 1KB -Stream &gt;&gt; "C:\Windows\Setup\Scripts\Specialize.log"; + </File> + <File path="C:\Windows\Setup\Scripts\UserOnce.ps1"> +$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; + }; + { + &amp; '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 + &amp; 'C:\Windows\Setup\Scripts\FinishUserSetupAndShutdown.ps1'; + }; +); + +&amp; { + [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 &#xAB;{0}&#xBB;.' -f $( + $script.ToString().Trim() -replace '\s+', ' ' -replace '^(.{99})(.+)$', '$1&#x2026;'; + ); + $start = [datetime]::Now; + &amp; $script; + '*** Finished executing command after {0:0} ms.' -f [datetime]::Now.Subtract( $start ).TotalMilliseconds; + "`r`n" * 3; + $complete += $increment; + } +} *&gt;&amp;1 | Out-String -Width 1KB -Stream &gt;&gt; "$env:TEMP\UserOnce.log"; + </File> + <File path="C:\Windows\Setup\Scripts\DefaultUser.ps1"> +$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; + }; + { + &amp; '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; + }; +); + +&amp; { + [float] $complete = 0; + [float] $increment = 100 / $scripts.Count; + foreach( $script in $scripts ) { + Write-Progress -Id 0 -Activity 'Running scripts to modify the default user&#x2019;&#x2019;s registry hive. Do not close this window.' -PercentComplete $complete; + '*** Will now execute command &#xAB;{0}&#xBB;.' -f $( + $script.ToString().Trim() -replace '\s+', ' ' -replace '^(.{99})(.+)$', '$1&#x2026;'; + ); + $start = [datetime]::Now; + &amp; $script; + '*** Finished executing command after {0:0} ms.' -f [datetime]::Now.Subtract( $start ).TotalMilliseconds; + "`r`n" * 3; + $complete += $increment; + } +} *&gt;&amp;1 | Out-String -Width 1KB -Stream &gt;&gt; "C:\Windows\Setup\Scripts\DefaultUser.log"; + </File> + <File path="C:\Windows\Setup\Scripts\FirstLogon.ps1"> +$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 + } + }; + { + &amp; "$env:WINDIR\Setup\Scripts\WinRM.ps1"; + }; + { + &amp; "$env:WINDIR\Setup\Scripts\WinFSP.ps1"; + }; + { + &amp; "$env:WINDIR\Setup\Scripts\OpenSSH.ps1"; + }; + { + # &amp; "$env:WINDIR\Setup\Scripts\InstallChocolatey.ps1"; + }; + { + &amp; "$env:WINDIR\Setup\Scripts\InstallPython.ps1"; + }; + { + &amp; "$env:WINDIR\Setup\Scripts\InstallNodeJS.ps1"; + }; + { + &amp; "$env:WINDIR\Setup\Scripts\InstallYarn1.ps1"; + }; + { + &amp; "$env:WINDIR\Setup\Scripts\InstallGit.ps1"; + }; + { + &amp; "$env:WINDIR\Setup\Scripts\InstallVisualStudio.ps1"; + }; + { + &amp; "$env:WINDIR\Setup\Scripts\InstallWindowsSDK.ps1"; + }; + { + &amp; "$env:WINDIR\Setup\Scripts\InstallRust.ps1"; + }; + { + &amp; "$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; + }; + { + &amp; "$env:WINDIR\Setup\Scripts\FinishSystemSetupAndReboot.ps1"; + }; +); + +&amp; { + [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 &#xAB;{0}&#xBB;.' -f $( + $script.ToString().Trim() -replace '\s+', ' ' -replace '^(.{99})(.+)$', '$1&#x2026;'; + ); + $start = [datetime]::Now; + &amp; $script; + '*** Finished executing command after {0:0} ms.' -f [datetime]::Now.Subtract( $start ).TotalMilliseconds; + "`r`n" * 3; + $complete += $increment; + } +} *&gt;&amp;1 | Out-String -Width 1KB -Stream &gt;&gt; "C:\Windows\Setup\Scripts\FirstLogon.log"; + </File> + + <!-- + ##### custom scripts start ##### + --> + + <!-- Windows remote management interface setup script --> + <File path="C:\Windows\Setup\Scripts\WinRM.ps1"> + # 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; + </File> + + <!-- OpenSSH server installation script --> + <File path="C:\Windows\Setup\Scripts\OpenSSH.ps1"> + # 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 = &amp; { + 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") { + &amp; "$env:ProgramFiles\OpenSSH\install-sshd.ps1"; + } elseif (Test-Path "$env:ProgramFiles\OpenSSH-Win64\install-sshd.ps1") { + &amp; "$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 = &amp; { + # 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"; + } + </File> + + <!-- Chocolatey installation script (unused) --> + <File path="C:\Windows\Setup\Scripts\InstallChocolatey.ps1"> + $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')); + </File> + + <!-- + Mesa3d OpenGL drivers + --> + <File path="C:\Windows\Setup\Scripts\InstallMesa3dOpenGL.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 = &amp; { + 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'; + </File> + + <!-- + Application / package installation script + + Previously based on chocolatey, but that does not support Arm64 natively, so... + --> + <File path="C:\Windows\Setup\Scripts\InstallGit.ps1"> + # + # Common defines + # + $cpu_arch = $env:PROCESSOR_ARCHITECTURE.ToLower(); + + # + # Git + # + $pkg_version = "2.54.0"; + $pkg_file = "git-${pkg_version}-${cpu_arch}.exe"; + $pkg_url = &amp; { + 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'; + </File> + + <File path="C:\Windows\Setup\Scripts\InstallPython.ps1"> + # 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'; + </File> + + <File path="C:\Windows\Setup\Scripts\InstallNodeJS.ps1"> + # 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 = &amp; { + 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'; + </File> + + <File path="C:\Windows\Setup\Scripts\InstallYarn1.ps1"> + # 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'; + </File> + + <File path="C:\Windows\Setup\Scripts\InstallRust.ps1"> + # 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 = &amp; { + 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'; + </File> + + <!-- + VisualStudio 2022 / 2026 installation script + + NOTE: Installation fails on Arm64 + --> + <File path="C:\Windows\Setup\Scripts\InstallVisualStudio.ps1"> + # Common + $cacheDrive = @(Get-ChildItem function:[d-z]: -n | ?{Test-Path $_\NvVars})[0]; + + # + # VSWhere (AMD64 only) + # + &amp; { + $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 + # + &amp; { + $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'; + } + }; + </File> + + <!-- + Windows SDK + --> + <File path="C:\Windows\Setup\Scripts\InstallWindowsSDK.ps1"> + # 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 = &amp; { + $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'; + </File> + + <!-- + WinFSP and Virtio FS installation + --> + <File path="C:\Windows\Setup\Scripts\WinFSP.ps1"> + # 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; + </File> + <!-- + ##### custom scripts end ##### + --> + + <!-- + Finish system and administrator account setup, switch autologin to user account + and reboot the system + --> + <File path="C:\Windows\Setup\Scripts\FinishSystemSetupAndReboot.ps1"> + # 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; + </File> + + <!-- + Failed attempts at scheduling a task for the non-admin user from the (admin) firstlogon scripts: + + # At-logon scheduled task (works) + # $shutdownTaskName = 'ShutdownAfterInstallation'; + # $shutdownTask = New-ScheduledTask -Action ( + # (New-ScheduledTaskAction -Execute 'powershell.exe' -Argument "-ExecutionPolicy 'Bypass' -Command `"Unregister-ScheduledTask -TaskName $shutdownTaskName -Confirm:`$false`""), + # (New-ScheduledTaskAction -Execute 'cmd.exe' -Argument "/c `"shutdown /s /t 150`"") + # ) -Trigger ( + # New-ScheduledTaskTrigger -AtLogon # -User "$userName" + # ) -Principal ( + # New-ScheduledTaskPrincipal -UserId "$userName" + # ); + + # $shutdownTask.Triggers[0].EndBoundary = [DateTime]::Now.AddSeconds(60).ToString("yyyy-MM-dd'T'HH:mm:ss"); + #$shutdownTask.Author = $userName; + #$shutdownTaskReg = $shutdownTask | Register-ScheduledTask -TaskName "$shutdownTaskName" -User "$userName" -Force; + + # Attempt to set once flag + #$shutdownTask = Get-ScheduledTask -TaskName "$shutdownTaskName"; + # $shutdownTask.Triggers[0].EndBoundary = [DateTime]::Now.AddSeconds(60).ToString("yyyy-MM-dd'T'HH:mm:ss"); + #$shutdownTask.Triggers[0].Once = $true; + #$shutdownTask | Set-ScheduledTask; + + # Grant target user delete permissions + #$taskScheduler = New-Object -ComObject "Schedule.Service"; + #$taskScheduler.Connect(); + #$taskHandle = $taskScheduler.GetFolder($shutdownTaskReg.TaskPath).GetTask($shutdownTaskReg.TaskName); + #$taskSecurityDesc = $taskHandle.GetSecurityDescriptor(0xF); + + #Write-Output "Before: $taskSecurityDesc"; + + #if ($taskSecurityDesc -match "A;;FR;;;$userSid") { + # $taskSecurityDesc = $taskSecurityDesc.Replace("A;;FR;;;$userSid", "A;;FA;;;$userSid"); + # Write-Output $taskHandle.SetSecurityDescriptor($taskSecurityDesc, 0xF); + #} else { + # $taskSecurityDesc = $taskSecurityDesc + "(A;;FA;;;$userSid)"; + # Write-Output $taskHandle.SetSecurityDescriptor($taskSecurityDesc, 0xF); + #} + + #Write-Output "After: $taskSecurityDesc"; + --> + + <File path="C:\Windows\Setup\Scripts\FinishUserSetupAndShutdown.ps1"> + # 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; + } + </File> + + <File path="C:\Windows\Setup\Scripts\config.json"> +{ + "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" + } + } +} + </File> + </Extensions> +</unattend> 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