diff --git a/Elevenbuilder.ps1 b/Elevenbuilder.ps1 index 8bc822b..38cf9ed 100644 --- a/Elevenbuilder.ps1 +++ b/Elevenbuilder.ps1 @@ -1,163 +1,581 @@ -# Enable debugging if needed -# Set-PSDebug -Trace 1 +<# +.SYNOPSIS + Tiny11 Image Creator – Full Workflow (ISO Download, Mount, Customization, and ISO Creation) + +.DESCRIPTION + This script uses DISM and other tools to create a customized Windows 11 image. + It now supports obtaining the Windows 11 ISO from a user-supplied path or by auto‑downloading + (using massgrave.dev as the source) if no ISO is provided. The ISO is then mounted and assigned + a free drive letter. The script then copies installation files, processes install.wim and boot.wim, + applies registry tweaks and removals, and finally creates an ISO using oscdimg.exe. + +.PARAMETER ScratchDisk + A drive letter (e.g. "D") or path where the working files will be stored. + +.PARAMETER ISOPath + (Optional) Full path to a Windows 11 ISO file. If not provided, the script will prompt for + the desired language and auto‑download the ISO. + +.PARAMETER Language + (Optional) Desired language code for the ISO download (e.g. "en-US", "de-DE"). Only used if ISOPath is not provided. + +.NOTES + - This script requires administrative privileges. + - It assumes that your original workflow (registry tweaks, application removals, etc.) must remain intact. + - Some API endpoints (for downloading the ISO) are hypothetical and may need adjustment. +#> param ( [ValidatePattern('^[c-zC-Z]$')] - [string]$ScratchDisk + [string]$ScratchDisk, + [string]$ISOPath, # Full path to a Windows 11 ISO (optional) + [string]$Language # Desired language code (e.g., "en-US", "de-DE") ) -# Determine scratch disk location -if (-not $ScratchDisk) { - $ScratchDisk = $PSScriptRoot -replace '[\\]+$', '' -} else { - $ScratchDisk = "$ScratchDisk:" -} +#region Helper Functions (ISO download and mount) -Write-Output "Scratch disk set to $ScratchDisk" - -# Ensure script is running with proper execution policy -if ((Get-ExecutionPolicy) -eq 'Restricted') { - Write-Host "Your current PowerShell Execution Policy is 'Restricted'. This prevents scripts from running." - $response = Read-Host "Do you want to change it to 'RemoteSigned'? (yes/no)" - if ($response -eq 'yes') { - Set-ExecutionPolicy RemoteSigned -Scope CurrentUser -Confirm:$false - } else { - Write-Host "The script cannot proceed without changing the execution policy. Exiting..." - exit +function Get-Win11DownloadLink { + <# + .SYNOPSIS + Queries the API (via massgrave.dev) for the proper ISO download link. + .PARAMETER Language + The desired language code. + .OUTPUTS + The download URL as a string. + #> + param( + [Parameter(Mandatory = $true)] + [string]$Language + ) + # Adjust the endpoint and parameters as required. + $apiBase = "https://api.gravesoft.dev/msdl/" + $endpoint = "getDownloadLink" # Hypothetical endpoint. + $url = "$apiBase$endpoint?language=$Language" + + Write-Host "Querying download link for Windows 11 ISO for language: $Language" -ForegroundColor Cyan + try { + $response = Invoke-RestMethod -Uri $url -Method Get + if ($response -and $response.downloadUrl) { + Write-Host "Download URL obtained: $($response.downloadUrl)" -ForegroundColor Green + return $response.downloadUrl + } + else { + Write-Error "API did not return a valid download URL." + return $null + } + } + catch { + Write-Error "Error calling the download API: $_" + return $null } } -# Ensure script is running as administrator -$adminRole = [System.Security.Principal.WindowsBuiltInRole]::Administrator -$myWindowsPrincipal = New-Object System.Security.Principal.WindowsPrincipal([System.Security.Principal.WindowsIdentity]::GetCurrent()) +function Get-Windows11ISO { + <# + .SYNOPSIS + Returns the path to a Windows 11 ISO. If a valid ISOPath is provided, that file is used. + Otherwise, prompts (or uses the provided language) and downloads the ISO. + .PARAMETER ISOPath + User-supplied ISO path. + .PARAMETER Language + Desired language code. + .OUTPUTS + The full path to the Windows 11 ISO. + #> + param( + [string]$ISOPath, + [string]$Language + ) + # Use provided ISO if valid + if ($ISOPath -and (Test-Path $ISOPath -PathType Leaf)) { + Write-Host "Using provided ISO: $ISOPath" -ForegroundColor Green + return $ISOPath + } + + # If no ISO path, prompt for language (if not provided) + if (-not $Language) { + $Language = Read-Host "Enter your desired Windows 11 language (e.g., en-US, de-DE)" + } + + $downloadLink = Get-Win11DownloadLink -Language $Language + if (-not $downloadLink) { + Write-Error "Could not retrieve a valid download link. Exiting." + exit 1 + } + + $DownloadPath = "$env:TEMP\Windows11_$Language.iso" + Write-Host "Downloading Windows 11 ISO from $downloadLink ..." -ForegroundColor Cyan + try { + Invoke-WebRequest -Uri $downloadLink -OutFile $DownloadPath + Write-Host "Download complete: $DownloadPath" -ForegroundColor Green + return $DownloadPath + } + catch { + Write-Error "Failed to download the ISO: $_" + exit 1 + } +} -if (-not $myWindowsPrincipal.IsInRole($adminRole)) { - Write-Host "Restarting the script as administrator..." - $newProcess = New-Object System.Diagnostics.ProcessStartInfo "PowerShell" - $newProcess.Arguments = "-File `"$($MyInvocation.MyCommand.Definition)`"" - $newProcess.Verb = "runas" - [System.Diagnostics.Process]::Start($newProcess) +function Mount-ISOAndAssignDriveLetter { + <# + .SYNOPSIS + Mounts an ISO image and assigns a free drive letter if none is already assigned. + .PARAMETER ISOPath + The full path to the ISO file. + .OUTPUTS + The drive letter (e.g., "E:") where the ISO is mounted. + #> + param( + [Parameter(Mandatory = $true)] + [string]$ISOPath + ) + if (-not (Test-Path $ISOPath)) { + Write-Error "The ISO file '$ISOPath' does not exist." + return + } + + Write-Host "Mounting ISO image: $ISOPath" -ForegroundColor Cyan + $mountedImage = Mount-DiskImage -ImagePath $ISOPath -PassThru + if (-not $mountedImage) { + Write-Error "Failed to mount ISO image." + return + } + Start-Sleep -Seconds 3 # Wait for the volume to become available + + $diskImage = Get-DiskImage -ImagePath $ISOPath + if (-not $diskImage) { + Write-Error "Unable to retrieve disk image information." + return + } + $disk = $diskImage | Get-Disk + if (-not $disk) { + Write-Error "Unable to retrieve disk information for the mounted image." + return + } + $diskNumber = $disk.Number + + # Retrieve the first partition (most ISOs contain a single partition) + $partition = Get-Partition -DiskNumber $diskNumber | Select-Object -First 1 + if (-not $partition) { + Write-Error "No partition found on the mounted ISO." + return + } + + # If no drive letter is assigned, choose a free one (from C: to Z:) + if (-not $partition.DriveLetter) { + $freeLetters = [char[]](67..90) | ForEach-Object { [char]$_ } + $usedLetters = (Get-Volume | Where-Object { $_.DriveLetter } | Select-Object -ExpandProperty DriveLetter) + $availableLetters = $freeLetters | Where-Object { $usedLetters -notcontains $_ } + if ($availableLetters.Count -eq 0) { + Write-Error "No free drive letters available." + return + } + $freeLetter = $availableLetters | Select-Object -First 1 + Write-Host "Assigning drive letter '$freeLetter' to the mounted ISO." -ForegroundColor Yellow + Set-Partition -DiskNumber $diskNumber -PartitionNumber $partition.PartitionNumber -NewDriveLetter $freeLetter + $driveLetter = "$freeLetter`:" + } + else { + $driveLetter = "$($partition.DriveLetter):" + } + + Write-Host "ISO mounted at drive letter: $driveLetter" -ForegroundColor Green + return $driveLetter +} + +#endregion Helper Functions + +#region Environment Setup + +function Setup-Environment { + <# + .SYNOPSIS + Performs pre-flight checks, sets the scratch disk, adjusts execution policy, + ensures admin rights, starts logging, and creates necessary directories. + #> + # Set ScratchDisk (if not provided, use the script folder) + if (-not $ScratchDisk) { + $global:ScratchDisk = $PSScriptRoot.TrimEnd('\') + } else { + $global:ScratchDisk = "$ScratchDisk`:" # Append colon if needed. + } + Write-Output "Scratch disk set to $global:ScratchDisk" + + # Check and adjust execution policy + if ((Get-ExecutionPolicy) -eq 'Restricted') { + Write-Host "Your current PowerShell Execution Policy is 'Restricted'. Changing it to 'RemoteSigned'..." + Set-ExecutionPolicy RemoteSigned -Scope CurrentUser -Confirm:$false + } + + # Ensure script is running as administrator + $adminRole = [System.Security.Principal.WindowsBuiltInRole]::Administrator + $principal = New-Object System.Security.Principal.WindowsPrincipal([System.Security.Principal.WindowsIdentity]::GetCurrent()) + if (-not $principal.IsInRole($adminRole)) { + Write-Host "Restarting the script with elevated privileges..." + $arguments = "-NoProfile -ExecutionPolicy Bypass -File `"$PSCommandPath`"" + Start-Process powershell -Verb RunAs -ArgumentList $arguments + exit + } + + # Start logging and set window title + Start-Transcript -Path "$global:ScratchDisk\tiny11.log" + $Host.UI.RawUI.WindowTitle = "Tiny11 Image Creator" + Clear-Host + Write-Host "Welcome to the Tiny11 Image Creator! Release: 05-06-24" -ForegroundColor Cyan + + # Create required directories + $global:tiny11Folder = Join-Path $global:ScratchDisk "tiny11" + $global:sourcesFolder = Join-Path $global:tiny11Folder "sources" + $global:mountPath = Join-Path $global:ScratchDisk "scratchdir" + New-Item -ItemType Directory -Force -Path $global:sourcesFolder | Out-Null + New-Item -ItemType Directory -Force -Path $global:mountPath | Out-Null +} + +#endregion Environment Setup + +#region Obtain & Mount Installation Media + +function Get-InstallationMedia { + <# + .SYNOPSIS + Obtains the Windows 11 ISO (using a provided path or by downloading it) and mounts it. + .OUTPUTS + The drive letter where the installation media is mounted. + #> + # Get the ISO (download if necessary) + $global:win11ISO = Get-Windows11ISO -ISOPath $ISOPath -Language $Language + if (-not $global:win11ISO) { + Write-Error "Failed to obtain a Windows 11 ISO. Exiting." + exit + } + + # Mount the ISO and retrieve the drive letter + $mediaDrive = Mount-ISOAndAssignDriveLetter -ISOPath $global:win11ISO + if (-not $mediaDrive) { + Write-Error "Failed to mount the Windows 11 ISO. Exiting." + exit + } + Write-Output "Installation media mounted at: $mediaDrive" + return $mediaDrive +} + +#endregion Obtain & Mount Installation Media + +#region Process install.wim Image + +function Process-InstallImage { + <# + .SYNOPSIS + Processes the Windows installation image (install.wim). This includes: + - Validating that the necessary installation files exist (or converting install.esd) + - Copying installation files from the installation media to the working folder + - Mounting the install.wim, gathering image information, applying customizations, + removing apps, tweaking registries, and finally unmounting the image. + #> + param( + [string]$DriveLetter # Installation media drive letter + ) + + # Validate Windows installation files + if ((Test-Path "$DriveLetter\sources\boot.wim") -eq $false -or (Test-Path "$DriveLetter\sources\install.wim") -eq $false) { + if (Test-Path "$DriveLetter\sources\install.esd") { + Write-Host "Found install.esd, converting to install.wim..." + Get-WindowsImage -ImagePath "$DriveLetter\sources\install.esd" + $index = Read-Host "Please enter the image index to convert from install.esd" + Write-Host "Converting install.esd to install.wim. This may take a while..." + Export-WindowsImage -SourceImagePath "$DriveLetter\sources\install.esd" ` + -SourceIndex $index ` + -DestinationImagePath "$global:ScratchDisk\tiny11\sources\install.wim" ` + -CompressionType Maximum -CheckIntegrity + } else { + Write-Host "Cannot find Windows OS installation files on the installation media." + exit + } + } + else { + Write-Host "Copying Windows installation files from $DriveLetter..." + Copy-Item -Path "$DriveLetter\*" -Destination "$global:tiny11Folder" -Recurse -Force | Out-Null + # Remove install.esd if present + Set-ItemProperty -Path "$global:tiny11Folder\sources\install.esd" -Name IsReadOnly -Value $false -ErrorAction SilentlyContinue + Remove-Item "$global:tiny11Folder\sources\install.esd" -ErrorAction SilentlyContinue + Write-Host "Copy complete!" + } + + Start-Sleep -Seconds 2 + Clear-Host + Write-Host "Retrieving image information from install.wim..." + Get-WindowsImage -ImagePath (Join-Path $global:sourcesFolder "install.wim") + $index = Read-Host "Please enter the desired image index" + Write-Host "Mounting install.wim image. This may take a while..." + $global:wimFilePath = Join-Path $global:sourcesFolder "install.wim" + & takeown "/F" $global:wimFilePath + & icacls $global:wimFilePath "/grant" "$($adminGroup.Value):(F)" + try { + Set-ItemProperty -Path $global:wimFilePath -Name IsReadOnly -Value $false -ErrorAction Stop + } catch { + # Suppress errors + } + New-Item -ItemType Directory -Force -Path $global:mountPath > $null + Mount-WindowsImage -ImagePath $global:wimFilePath -Index $index -Path $global:mountPath + + # Retrieve system UI language from the mounted image + $imageIntl = & dism /English /Get-Intl "/Image:$global:mountPath" + $languageLine = $imageIntl -split '\n' | Where-Object { $_ -match 'Default system UI language : ([a-zA-Z]{2}-[a-zA-Z]{2})' } + if ($languageLine) { + $languageCode = $Matches[1] + Write-Host "Default system UI language code: $languageCode" + } else { + Write-Host "Default system UI language code not found." + } + + # Retrieve architecture information from the image + $imageInfo = & dism /English /Get-WimInfo "/wimFile:$global:sourcesFolder\install.wim" "/index:$index" + $lines = $imageInfo -split '\r?\n' + foreach ($line in $lines) { + if ($line -like '*Architecture : *') { + $architecture = $line -replace 'Architecture : ','' + if ($architecture -eq 'x64') { + $architecture = 'amd64' + } + Write-Host "Architecture: $architecture" + break + } + } + if (-not $architecture) { + Write-Host "Architecture information not found." + } + + Write-Host "Install image mounted. Proceeding with application removals and customizations..." + + # Remove unwanted applications (bloatware) via DISM + $packages = & dism /English "/image:$global:mountPath" '/Get-ProvisionedAppxPackages' | + ForEach-Object { + if ($_ -match 'PackageName : (.*)') { + $matches[1] + } + } + $packagePrefixes = 'Clipchamp.Clipchamp_', 'Microsoft.BingNews_', 'Microsoft.BingWeather_', 'Microsoft.GamingApp_', 'Microsoft.GetHelp_', 'Microsoft.Getstarted_', 'Microsoft.MicrosoftOfficeHub_', 'Microsoft.MicrosoftSolitaireCollection_', 'Microsoft.People_', 'Microsoft.PowerAutomateDesktop_', 'Microsoft.Todos_', 'Microsoft.WindowsAlarms_', 'microsoft.windowscommunicationsapps_', 'Microsoft.WindowsFeedbackHub_', 'Microsoft.WindowsMaps_', 'Microsoft.WindowsSoundRecorder_', 'Microsoft.Xbox.TCUI_', 'Microsoft.XboxGamingOverlay_', 'Microsoft.XboxGameOverlay_', 'Microsoft.XboxSpeechToTextOverlay_', 'Microsoft.YourPhone_', 'Microsoft.ZuneMusic_', 'Microsoft.ZuneVideo_', 'MicrosoftCorporationII.MicrosoftFamily_', 'MicrosoftCorporationII.QuickAssist_', 'MicrosoftTeams_', 'Microsoft.549981C3F5F10_' + $packagesToRemove = $packages | Where-Object { + $packageName = $_ + $packagePrefixes -contains ($packagePrefixes | Where-Object { $packageName -like "$_*" }) + } + foreach ($package in $packagesToRemove) { + & dism /English "/image:$global:mountPath" '/Remove-ProvisionedAppxPackage' "/PackageName:$package" + } + + # Remove Microsoft Edge and its components + Write-Host "Removing Microsoft Edge..." + Remove-Item -Path "$global:mountPath\Program Files (x86)\Microsoft\Edge" -Recurse -Force -ErrorAction SilentlyContinue + Remove-Item -Path "$global:mountPath\Program Files (x86)\Microsoft\EdgeUpdate" -Recurse -Force -ErrorAction SilentlyContinue + Remove-Item -Path "$global:mountPath\Program Files (x86)\Microsoft\EdgeCore" -Recurse -Force -ErrorAction SilentlyContinue + if ($architecture -eq 'amd64') { + $folderPath = Get-ChildItem -Path "$global:mountPath\Windows\WinSxS" -Filter "amd64_microsoft-edge-webview_31bf3856ad364e35*" -Directory | Select-Object -ExpandProperty FullName + if ($folderPath) { + & takeown '/f' $folderPath '/r' | Out-Null + & icacls $folderPath "/grant" "$($adminGroup.Value):(F)" '/T' '/C' | Out-Null + Remove-Item -Path $folderPath -Recurse -Force | Out-Null + } else { + Write-Host "Edge WebView folder not found." + } + } + elseif ($architecture -eq 'arm64') { + $folderPath = Get-ChildItem -Path "$global:mountPath\Windows\WinSxS" -Filter "arm64_microsoft-edge-webview_31bf3856ad364e35*" -Directory | Select-Object -ExpandProperty FullName + if ($folderPath) { + & takeown '/f' $folderPath '/r' | Out-Null + & icacls $folderPath "/grant" "$($adminGroup.Value):(F)" '/T' '/C' | Out-Null + Remove-Item -Path $folderPath -Recurse -Force | Out-Null + } else { + Write-Host "Edge WebView folder not found." + } + } + & takeown '/f' "$global:mountPath\Windows\System32\Microsoft-Edge-Webview" '/r' | Out-Null + & icacls "$global:mountPath\Windows\System32\Microsoft-Edge-Webview" '/grant' "$($adminGroup.Value):(F)" '/T' '/C' | Out-Null + Remove-Item -Path "$global:mountPath\Windows\System32\Microsoft-Edge-Webview" -Recurse -Force | Out-Null + + # Remove OneDrive + Write-Host "Removing OneDrive..." + & takeown '/f' "$global:mountPath\Windows\System32\OneDriveSetup.exe" | Out-Null + & icacls "$global:mountPath\Windows\System32\OneDriveSetup.exe" '/grant' "$($adminGroup.Value):(F)" '/T' '/C' | Out-Null + Remove-Item -Path "$global:mountPath\Windows\System32\OneDriveSetup.exe" -Force | Out-Null + Write-Host "Application removal complete!" + + Start-Sleep -Seconds 2 + Clear-Host + + # Load registry hives from the mounted image and apply tweaks + Write-Host "Loading registry hives from the mounted image..." + reg load HKLM\zCOMPONENTS "$global:mountPath\Windows\System32\config\COMPONENTS" | Out-Null + reg load HKLM\zDEFAULT "$global:mountPath\Windows\System32\config\default" | Out-Null + reg load HKLM\zNTUSER "$global:mountPath\Users\Default\ntuser.dat" | Out-Null + reg load HKLM\zSOFTWARE "$global:mountPath\Windows\System32\config\SOFTWARE" | Out-Null + reg load HKLM\zSYSTEM "$global:mountPath\Windows\System32\config\SYSTEM" | Out-Null + + Write-Host "Applying registry tweaks to bypass system requirements..." + & reg add 'HKLM\zDEFAULT\Control Panel\UnsupportedHardwareNotificationCache' '/v' 'SV1' '/t' 'REG_DWORD' '/d' '0' '/f' | Out-Null + & reg add 'HKLM\zDEFAULT\Control Panel\UnsupportedHardwareNotificationCache' '/v' 'SV2' '/t' 'REG_DWORD' '/d' '0' '/f' | Out-Null + & reg add 'HKLM\zNTUSER\Control Panel\UnsupportedHardwareNotificationCache' '/v' 'SV1' '/t' 'REG_DWORD' '/d' '0' '/f' | Out-Null + & reg add 'HKLM\zNTUSER\Control Panel\UnsupportedHardwareNotificationCache' '/v' 'SV2' '/t' 'REG_DWORD' '/d' '0' '/f' | Out-Null + & reg add 'HKLM\zSYSTEM\Setup\LabConfig' '/v' 'BypassCPUCheck' '/t' 'REG_DWORD' '/d' '1' '/f' | Out-Null + & reg add 'HKLM\zSYSTEM\Setup\LabConfig' '/v' 'BypassRAMCheck' '/t' 'REG_DWORD' '/d' '1' '/f' | Out-Null + & reg add 'HKLM\zSYSTEM\Setup\LabConfig' '/v' 'BypassSecureBootCheck' '/t' 'REG_DWORD' '/d' '1' '/f' | Out-Null + & reg add 'HKLM\zSYSTEM\Setup\LabConfig' '/v' 'BypassStorageCheck' '/t' 'REG_DWORD' '/d' '1' '/f' | Out-Null + & reg add 'HKLM\zSYSTEM\Setup\LabConfig' '/v' 'BypassTPMCheck' '/t' 'REG_DWORD' '/d' '1' '/f' | Out-Null + & reg add 'HKLM\zSYSTEM\Setup\MoSetup' '/v' 'AllowUpgradesWithUnsupportedTPMOrCPU' '/t' 'REG_DWORD' '/d' '1' '/f' | Out-Null + + Write-Host "Registry tweaks complete. Unloading registry hives..." + reg unload HKLM\zCOMPONENTS | Out-Null + reg unload HKLM\zDEFAULT | Out-Null + reg unload HKLM\zNTUSER | Out-Null + reg unload HKLM\zSOFTWARE | Out-Null + reg unload HKLM\zSYSTEM | Out-Null + + Write-Host "Performing component cleanup on the image..." + Repair-WindowsImage -Path $global:mountPath -StartComponentCleanup -ResetBase + Write-Host "Unmounting install.wim image (saving changes)..." + Dismount-WindowsImage -Path $global:mountPath -Save + Clear-Host + + Write-Host "Exporting updated install.wim..." + Export-WindowsImage -SourceImagePath (Join-Path $global:sourcesFolder "install.wim") -SourceIndex $index ` + -DestinationImagePath (Join-Path $global:sourcesFolder "install2.wim") -CompressionType Fast + Remove-Item -Path (Join-Path $global:sourcesFolder "install.wim") -Force | Out-Null + Rename-Item -Path (Join-Path $global:sourcesFolder "install2.wim") -NewName "install.wim" -Force | Out-Null + Write-Host "Install image processing complete. Proceeding with boot.wim..." +} + +#endregion Process install.wim Image + +#region Process boot.wim Image + +function Process-BootImage { + <# + .SYNOPSIS + Processes the boot image (boot.wim). This includes mounting the boot image, + applying necessary tweaks (if any), and then unmounting the image. + #> + Write-Host "Mounting boot.wim image..." + $global:wimFilePath = Join-Path $global:sourcesFolder "boot.wim" + & takeown "/F" $global:wimFilePath | Out-Null + & icacls $global:wimFilePath "/grant" "$($adminGroup.Value):(F)" | Out-Null + Set-ItemProperty -Path $global:wimFilePath -Name IsReadOnly -Value $false + Mount-WindowsImage -ImagePath $global:wimFilePath -Index 2 -Path $global:mountPath + Write-Host "Boot image mounted. Loading registry from boot image..." + reg load HKLM\zCOMPONENTS "$global:mountPath\Windows\System32\config\COMPONENTS" | Out-Null + reg load HKLM\zDEFAULT "$global:mountPath\Windows\System32\config\default" | Out-Null + reg load HKLM\zNTUSER "$global:mountPath\Users\Default\ntuser.dat" | Out-Null + reg load HKLM\zSOFTWARE "$global:mountPath\Windows\System32\config\SOFTWARE" | Out-Null + reg load HKLM\zSYSTEM "$global:mountPath\Windows\System32\config\SYSTEM" | Out-Null + + Write-Host "Applying tweaks to boot image registry..." + & reg add 'HKLM\zDEFAULT\Control Panel\UnsupportedHardwareNotificationCache' '/v' 'SV1' '/t' 'REG_DWORD' '/d' '0' '/f' | Out-Null + & reg add 'HKLM\zDEFAULT\Control Panel\UnsupportedHardwareNotificationCache' '/v' 'SV2' '/t' 'REG_DWORD' '/d' '0' '/f' | Out-Null + & reg add 'HKLM\zNTUSER\Control Panel\UnsupportedHardwareNotificationCache' '/v' 'SV1' '/t' 'REG_DWORD' '/d' '0' '/f' | Out-Null + & reg add 'HKLM\zNTUSER\Control Panel\UnsupportedHardwareNotificationCache' '/v' 'SV2' '/t' 'REG_DWORD' '/d' '0' '/f' | Out-Null + & reg add 'HKLM\zSYSTEM\Setup\LabConfig' '/v' 'BypassCPUCheck' '/t' 'REG_DWORD' '/d' '1' '/f' | Out-Null + & reg add 'HKLM\zSYSTEM\Setup\LabConfig' '/v' 'BypassRAMCheck' '/t' 'REG_DWORD' '/d' '1' '/f' | Out-Null + & reg add 'HKLM\zSYSTEM\Setup\LabConfig' '/v' 'BypassSecureBootCheck' '/t' 'REG_DWORD' '/d' '1' '/f' | Out-Null + & reg add 'HKLM\zSYSTEM\Setup\LabConfig' '/v' 'BypassStorageCheck' '/t' 'REG_DWORD' '/d' '1' '/f' | Out-Null + & reg add 'HKLM\zSYSTEM\Setup\LabConfig' '/v' 'BypassTPMCheck' '/t' 'REG_DWORD' '/d' '1' '/f' | Out-Null + & reg add 'HKLM\zSYSTEM\Setup\MoSetup' '/v' 'AllowUpgradesWithUnsupportedTPMOrCPU' '/t' 'REG_DWORD' '/d' '1' '/f' | Out-Null + + Write-Host "Tweaks for boot image applied. Unloading registry hives..." + reg unload HKLM\zCOMPONENTS | Out-Null + reg unload HKLM\zDEFAULT | Out-Null + reg unload HKLM\zNTUSER | Out-Null + reg unload HKLM\zSOFTWARE | Out-Null + reg unload HKLM\zSYSTEM | Out-Null + + Write-Host "Unmounting boot image (saving changes)..." + Dismount-WindowsImage -Path $global:mountPath -Save +} + +#endregion Process boot.wim Image + +#region Finalize ISO Creation + +function Finalize-ISO { + <# + .SYNOPSIS + Uses oscdimg.exe to create the final Tiny11 ISO from the customized image. + #> + Write-Host "Copying unattended file for bypassing MS account on OOBE..." + Copy-Item -Path "$PSScriptRoot\autounattend.xml" -Destination "$global:tiny11Folder\autounattend.xml" -Force | Out-Null + + Write-Host "Creating final ISO image..." + $ADKDepTools = "C:\Program Files (x86)\Windows Kits\10\Assessment and Deployment Kit\Deployment Tools\$hostarchitecture\Oscdimg" + $localOSCDIMGPath = "$PSScriptRoot\oscdimg.exe" + + if ([System.IO.Directory]::Exists($ADKDepTools)) { + Write-Host "Using oscdimg.exe from the system ADK." + $OSCDIMG = Join-Path $ADKDepTools "oscdimg.exe" + } else { + Write-Host "ADK folder not found. Using bundled oscdimg.exe." + if (-not (Test-Path -Path $localOSCDIMGPath)) { + Write-Host "Downloading oscdimg.exe..." + $url = "https://msdl.microsoft.com/download/symbols/oscdimg.exe/3D44737265000/oscdimg.exe" + Invoke-WebRequest -Uri $url -OutFile $localOSCDIMGPath + if (-not (Test-Path $localOSCDIMGPath)) { + Write-Error "Failed to download oscdimg.exe." + exit 1 + } + } else { + Write-Host "oscdimg.exe already exists locally." + } + $OSCDIMG = $localOSCDIMGPath + } + + # Define boot data (adjust paths if necessary) + $bootData = "2#p0,e,b$global:tiny11Folder\boot\etfsboot.com#pEF,e,b$global:tiny11Folder\efi\microsoft\boot\efisys.bin" + $isoOutput = Join-Path $PSScriptRoot "tiny11.iso" + & "$OSCDIMG" '-m' '-o' '-u2' '-udfver102' "-bootdata:$bootData" "$global:tiny11Folder" "$isoOutput" + Write-Host "ISO creation complete: $isoOutput" -ForegroundColor Green +} + +#endregion Finalize ISO Creation + +#region Cleanup + +function Cleanup-Environment { + <# + .SYNOPSIS + Cleans up temporary folders used during image processing. + #> + Write-Host "Performing cleanup..." + Remove-Item -Path "$global:tiny11Folder" -Recurse -Force -ErrorAction SilentlyContinue + Remove-Item -Path "$global:mountPath" -Recurse -Force -ErrorAction SilentlyContinue + Stop-Transcript + Write-Host "Cleanup complete." +} + +#endregion Cleanup + +#region Main Flow + +function Main { + # Step 1: Setup environment (parameters, admin check, logging, directories) + Setup-Environment + + # Step 2: Obtain and mount installation media (ISO) + $mediaDrive = Get-InstallationMedia + + # Step 3: Process the install.wim image (copy files, convert ESD if needed, apply tweaks) + Process-InstallImage -DriveLetter $mediaDrive + + # Step 4: Process the boot.wim image + Process-BootImage + + # Step 5: Finalize ISO creation using oscdimg.exe + Finalize-ISO + + # Step 6: Cleanup temporary folders and stop logging + Cleanup-Environment + + Write-Host "Tiny11 image creation completed. Press Enter to exit." + Read-Host exit } -# Start logging -Start-Transcript -Path "$ScratchDisk\tiny11.log" +# Start the main flow +Main -# Set window title -$Host.UI.RawUI.WindowTitle = "Tiny11 Image Creator" -Clear-Host -Write-Host "Welcome to Tiny11 Image Creator! Release: 05-06-24" - -# Ensure necessary directories exist -New-Item -ItemType Directory -Force -Path "$ScratchDisk\tiny11\sources" | Out-Null - -# Prompt for Windows 11 installation media drive letter -do { - $DriveLetter = Read-Host "Enter the drive letter of the Windows 11 installation media" - if ($DriveLetter -match '^[c-zC-Z]$') { - $DriveLetter = "$DriveLetter:" - Write-Output "Drive letter set to $DriveLetter" - } else { - Write-Output "Invalid drive letter. Enter a letter between C and Z." - } -} while ($DriveLetter -notmatch '^[c-zC-Z]:$') - -# Validate Windows installation files -if (-not (Test-Path "$DriveLetter\sources\boot.wim") -or -not (Test-Path "$DriveLetter\sources\install.wim")) { - if (Test-Path "$DriveLetter\sources\install.esd") { - Write-Host "install.esd found. Converting to install.wim..." - Get-WindowsImage -ImagePath "$DriveLetter\sources\install.esd" - $index = Read-Host "Enter the image index" - Write-Host "Converting install.esd to install.wim..." - Export-WindowsImage -SourceImagePath "$DriveLetter\sources\install.esd" ` - -SourceIndex $index ` - -DestinationImagePath "$ScratchDisk\tiny11\sources\install.wim" ` - -CompressionType Maximum -CheckIntegrity - } else { - Write-Host "Windows installation files not found. Please provide the correct drive letter." - exit - } -} - -# Copy Windows installation files -Write-Host "Copying Windows installation files..." -Copy-Item -Path "$DriveLetter\*" -Destination "$ScratchDisk\tiny11" -Recurse -Force | Out-Null -Remove-Item "$ScratchDisk\tiny11\sources\install.esd" -ErrorAction SilentlyContinue -Write-Host "Copy complete!" - -Start-Sleep -Seconds 2 -Clear-Host -Write-Host "Retrieving image information..." -Get-WindowsImage -ImagePath "$ScratchDisk\tiny11\sources\install.wim" - -# Prompt for image index -$index = Read-Host "Enter the image index" - -# Mount Windows image -Write-Host "Mounting Windows image... This may take a while." -New-Item -ItemType Directory -Force -Path "$ScratchDisk\scratchdir" | Out-Null -Mount-WindowsImage -ImagePath "$ScratchDisk\tiny11\sources\install.wim" -Index $index -Path "$ScratchDisk\scratchdir" - -# Retrieve system UI language -$imageIntl = & dism /English /Get-Intl "/Image:$($ScratchDisk)\scratchdir" -if ($imageIntl -match 'Default system UI language : ([a-zA-Z]{2}-[a-zA-Z]{2})') { - Write-Host "Default system UI language: $($matches[1])" -} else { - Write-Host "System UI language could not be determined." -} - -# Retrieve architecture -$imageInfo = & dism /English /Get-WimInfo "/wimFile:$($ScratchDisk)\tiny11\sources\install.wim" "/index:$index" -if ($imageInfo -match "Architecture : (\S+)") { - $architecture = $matches[1] -replace "x64", "amd64" - Write-Host "Architecture: $architecture" -} else { - Write-Host "Architecture information not found." -} - -# Remove bloatware applications -Write-Host "Removing preinstalled apps..." -$appsToRemove = @( - 'Clipchamp.Clipchamp_', 'Microsoft.BingNews_', 'Microsoft.BingWeather_', 'Microsoft.GamingApp_', - 'Microsoft.GetHelp_', 'Microsoft.Getstarted_', 'Microsoft.MicrosoftOfficeHub_', 'Microsoft.MicrosoftSolitaireCollection_', - 'Microsoft.People_', 'Microsoft.PowerAutomateDesktop_', 'Microsoft.Todos_', 'Microsoft.WindowsAlarms_', - 'microsoft.windowscommunicationsapps_', 'Microsoft.WindowsFeedbackHub_', 'Microsoft.WindowsMaps_', 'Microsoft.WindowsSoundRecorder_', - 'Microsoft.Xbox.TCUI_', 'Microsoft.XboxGamingOverlay_', 'Microsoft.XboxGameOverlay_', 'Microsoft.XboxSpeechToTextOverlay_', - 'Microsoft.YourPhone_', 'Microsoft.ZuneMusic_', 'Microsoft.ZuneVideo_', 'MicrosoftCorporationII.MicrosoftFamily_', - 'MicrosoftCorporationII.QuickAssist_', 'MicrosoftTeams_', 'Microsoft.549981C3F5F10_' -) - -$installedPackages = & dism /English "/image:$($ScratchDisk)\scratchdir" /Get-ProvisionedAppxPackages | - ForEach-Object { if ($_ -match 'PackageName : (.*)') { $matches[1] } } - -foreach ($package in $appsToRemove) { - if ($installedPackages -match "$package*") { - & dism /English "/image:$($ScratchDisk)\scratchdir" /Remove-ProvisionedAppxPackage "/PackageName:$matches[0]" - } -} - -# Remove Microsoft Edge -Write-Host "Removing Microsoft Edge..." -$edgeFolders = @( - "Program Files (x86)\Microsoft\Edge", - "Program Files (x86)\Microsoft\EdgeUpdate", - "Program Files (x86)\Microsoft\EdgeCore" -) - -foreach ($folder in $edgeFolders) { - Remove-Item -Path "$ScratchDisk\scratchdir\$folder" -Recurse -Force -ErrorAction SilentlyContinue -} - -# Remove OneDrive -Write-Host "Removing OneDrive..." -$oneDriveSetup = "$ScratchDisk\scratchdir\Windows\System32\OneDriveSetup.exe" -if (Test-Path $oneDriveSetup) { - & takeown /f $oneDriveSetup | Out-Null - & icacls $oneDriveSetup /grant "$($adminGroup.Value):(F)" /T /C | Out-Null - Remove-Item -Path $oneDriveSetup -Force | Out-Null -} - -Write-Host "Optimizations complete!" -Start-Sleep -Seconds 2 -Clear-Host -Write-Host "Tiny11 Image preparation finished!" +#endregion Main Flow