1
0
Files
Repository/Windows-CustoWimMUI/BuildMUI.ps1
2025-10-31 08:58:36 +01:00

741 lines
36 KiB
PowerShell

#requires -version 5.1
<#!
Title: Build Windows 11 Multi-Index (MUI) — GUI Pro (v2.3)
UI:
• Paths via TableLayoutPanel (DPI safe)
• Boutons flat + hover + coins arrondis
• Log en ListView: Time | Level | Message (menu Copy/Clear)
• Popup “build terminé” + son
#>
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
# --------------------------- Globals & Helpers ---------------------------
$global:Form = $null
$global:LogBox = $null # sera le ListView
$global:StatusLabel = $null
$ToolTip = New-Object System.Windows.Forms.ToolTip
function Write-Log {
param([string]$Message,[ValidateSet('INFO','WARN','ERROR')]$Level='INFO')
$ts = (Get-Date).ToString('yyyy-MM-dd HH:mm:ss')
$line = "[$ts] $Level $Message"
# console (utile si script lancé hors GUI)
switch ($Level) {
'ERROR' { Write-Host $line -ForegroundColor Red }
'WARN' { Write-Host $line -ForegroundColor Yellow }
default { Write-Host $line -ForegroundColor Gray }
}
# GUI
if ($global:LogBox -and -not $global:LogBox.IsDisposed) {
if ($global:LogBox -is [System.Windows.Forms.ListView]) {
$item = New-Object System.Windows.Forms.ListViewItem($ts)
[void]$item.SubItems.Add($Level)
[void]$item.SubItems.Add($Message)
switch($Level){
'ERROR' { $item.ForeColor = [Drawing.Color]::FromArgb(210,39,30) }
'WARN' { $item.ForeColor = [Drawing.Color]::FromArgb(185,120,0) }
default { $item.ForeColor = [Drawing.Color]::FromArgb(45,45,45) }
}
[void]$global:LogBox.Items.Add($item)
$global:LogBox.EnsureVisible($global:LogBox.Items.Count-1)
} else {
$global:LogBox.AppendText($line + [Environment]::NewLine)
$global:LogBox.SelectionStart = $global:LogBox.Text.Length
$global:LogBox.ScrollToCaret()
}
}
if ($global:StatusLabel -and !$global:StatusLabel.IsDisposed) {
$global:StatusLabel.Text = $Message
}
}
function Assert-Admin {
$id = [Security.Principal.WindowsIdentity]::GetCurrent()
$p = [Security.Principal.WindowsPrincipal]::new($id)
if (-not $p.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
[System.Windows.Forms.MessageBox]::Show('Run this script as Administrator.','Privilege required','OK','Error') | Out-Null
throw 'Not running as Administrator.'
}
}
function Ensure-ProjectSubfolders([string]$Root){
if ([string]::IsNullOrWhiteSpace($Root)) { throw 'Please set the project root folder.' }
foreach($s in 'ISO','WinPE_OCs','Export','SXS'){
$path = Join-Path $Root $s
if (-not (Test-Path $path)){
Write-Log "Create missing: $path"
New-Item -ItemType Directory -Force -Path $path | Out-Null
}
}
}
function Ensure-LocalSubfolders([string]$Root){
if ([string]::IsNullOrWhiteSpace($Root)) { throw 'Please set the local work root.' }
foreach($s in 'MountWinPE','MountWinRE','MountInstall','scratch','distribution'){
$path = Join-Path $Root $s
if (-not (Test-Path $path)){
Write-Log "Create missing local: $path"
New-Item -ItemType Directory -Force -Path $path | Out-Null
}
}
}
# --------------------------- Core functions ---------------------------
function CleanupSafe {
Param(
[parameter(Mandatory=$true)][string]$DossierMountWindows,
[parameter(Mandatory=$true)][string]$DossierMountWinPE,
[parameter(Mandatory=$true)][string]$DossierMountWinRE,
[parameter(Mandatory=$true)][string]$DossierMountScratch,
[parameter(Mandatory=$true)][string]$DossierTravail
)
Write-Log 'Cleanup (safe) started'
$oldEAP = $ErrorActionPreference
$ErrorActionPreference = 'SilentlyContinue'
try { & dism.exe /English /Cleanup-Wim 2>$null | Out-Null } catch {}
foreach ($p in @($DossierMountWinPE,$DossierMountWinRE,$DossierMountWindows)) {
try { Dismount-WindowsImage -Path $p -Discard -ErrorAction Stop | Out-Null; Write-Log "Dismounted: $p" } catch {}
}
try {
$mounted = & dism.exe /English /Get-MountedWimInfo 2>$null
if ($LASTEXITCODE -eq 0 -and $mounted) {
$orphans = foreach ($ln in $mounted) { if ($ln -like 'Mount Dir :*') { ($ln.Split(':',2)[1]).Trim() } }
foreach ($m in $orphans) { try { & dism.exe /Unmount-Wim /MountDir:"$m" /Discard 2>$null | Out-Null } catch {} }
}
} catch {}
foreach ($p in @($DossierMountWinPE,$DossierMountWindows,$DossierMountWinRE,$DossierMountScratch,$DossierTravail)) {
try { Remove-Item $p -Recurse -Force -ErrorAction SilentlyContinue } catch {}
}
$ErrorActionPreference = $oldEAP
Write-Log 'Cleanup (safe) finished'
}
function Prepare {
Param(
[parameter(Mandatory=$true)][String]$DossierMountWindows,
[parameter(Mandatory=$true)][String]$DossierMountWinPE,
[parameter(Mandatory=$true)][String]$DossierMountWinRE,
[parameter(Mandatory=$true)][String]$DossierMountScratch,
[parameter(Mandatory=$true)][String]$DossierTravail
)
Write-Log 'Prepare mount/scratch folders'
$ErrorActionPreference = 'SilentlyContinue'
New-Item -ItemType Directory -Force -Path $DossierMountWindows,$DossierMountWinPE,$DossierMountWinRE,$DossierMountScratch,$DossierTravail | Out-Null
$ErrorActionPreference = 'Continue'
}
Function CopierFichierInstallation {
Param(
[parameter(Mandatory=$true)][String]$LangueBase,
[parameter(Mandatory=$true)][String]$DossierExport,
[parameter(Mandatory=$true)][String]$DossierAvecISO
)
Write-Log 'Copy installation files'
Write-Log "Copy $LangueBase to $DossierExport"
$ListISO = Get-ChildItem -Path $DossierAvecISO | Where-Object {$_.Name -like '*.ISO*'}
foreach ($ISO in $ListISO) {
$PatchISO = $ISO.FullName
Write-Log $PatchISO
$vol = Mount-DiskImage -ImagePath $PatchISO -PassThru | Get-DiskImage | Get-Volume
$DossierLanguerecherche = $vol.DriveLetter+':\sources\'+$LangueBase
$IsoLangueRecherche = "$($vol.DriveLetter):\"
if(Test-Path $DossierLanguerecherche) {
Write-Log "Robocopy $IsoLangueRecherche $DossierExport"
Robocopy "$IsoLangueRecherche" "$DossierExport" /MIR | Out-Null
}
Dismount-DiskImage -ImagePath $PatchISO | Out-Null
}
}
Function CopierSXS {
Param(
[parameter(Mandatory=$true)][String]$DossierExportsxs,
[parameter(Mandatory=$true)][String]$DossierAvecISO
)
Write-Log 'Collect SXS language sources'
$LanguageCode = 'bg-bg','cs-cz','da-dk','de-de','el-gr','en-gb','en-us','es-es','es-mx','et-ee','fi-fi','fr-ca','fr-fr','hr-hr','hu-hu','it-it','ja-jp','ko-kr','lt-lt','lv-lv','nb-no','nl-nl','pl-pl','pt-br','pt-pt','ro-ro','ru-ru','sk-sk','sl-si','sv-se','tr-tr','uk-ua','zh-cn','zh-tw'
$ListISO = Get-ChildItem -Path $DossierAvecISO | Where-Object {$_.Name -like '*.ISO*'}
foreach ($ISO in $ListISO) {
$PatchISO = $ISO.FullName
$vol = Mount-DiskImage -ImagePath $PatchISO -PassThru | Get-DiskImage | Get-Volume
foreach ($Item in $LanguageCode) {
$TestLP = $vol.DriveLetter+':\sources\'+$Item
$Testsxs = $vol.DriveLetter+':\sources\sxs'
if((Test-Path -Path $TestLP) -and (Test-Path -Path $Testsxs)) {
Write-Log "copy : $DossierExportsxs\$Item"
Robocopy "$Testsxs" "$DossierExportsxs\$Item" /MIR | Out-Null
}
}
Dismount-DiskImage -ImagePath $PatchISO | Out-Null
}
}
Function MontageWim {
Param(
[parameter(Mandatory=$true)][String]$DossierMount,
[parameter(Mandatory=$true)][String]$ImageWim,
[parameter(Mandatory=$true)][String]$IndexWim
)
Write-Log 'Mount WIM'
Write-Log $ImageWim
Write-Log $DossierMount
Set-ItemProperty -Path $ImageWim -Name IsReadOnly -Value $false -ErrorAction SilentlyContinue
Write-Log "mount $ImageWim"
Mount-WindowsImage -ImagePath $ImageWim -Index $IndexWim -Path $DossierMount
}
Function SaveImageWim {
Param([parameter(Mandatory=$true)][String]$DossierMount)
Write-Log 'Save & Unmount WIM'
Write-Log "unmount $DossierMount"
Dismount-WindowsImage -Path $DossierMount -Save
}
# WinPE languages
Function AjoutLangueWinPE {
Param(
[parameter(Mandatory=$true)][string]$DossierMountWinPE,
[parameter(Mandatory=$true)][string]$DossierAvecISO,
[parameter(Mandatory=$true)][string]$OSclientOuServer, # 'Client' or 'Server'
[parameter(Mandatory=$true)][string]$DossierLangueWinPE,
[parameter(Mandatory=$true)][string]$DossierExport,
[parameter(Mandatory=$true)][string]$LangueDeBase
)
Write-Log 'Add WinPE languages'
if (-not (Test-Path -LiteralPath $DossierLangueWinPE)) { throw "WinPE_OCs folder not found: $DossierLangueWinPE" }
$LanguageCode = 'bg-bg','cs-cz','da-dk','de-de','el-gr','en-gb','en-us','es-es','es-mx','et-ee','fi-fi','fr-ca','fr-fr','hr-hr','hu-hu','it-it','ja-jp','ko-kr','lt-lt','lv-lv','nb-no','nl-nl','pl-pl','pt-br','pt-pt','ro-ro','sk-sk','sl-si','sv-se','tr-tr','uk-ua','zh-cn','zh-tw'
function Add-CabIfExists([string]$mount,[string]$cabPath){
if (Test-Path -LiteralPath $cabPath) {
Write-Log ("Add-WindowsPackage: {0}" -f ([IO.Path]::GetFileName($cabPath)))
Add-WindowsPackage -Path $mount -PackagePath $cabPath -ErrorAction Stop | Out-Null
return $true
} else { Write-Log "SKIP missing package: $cabPath"; return $false }
}
MontageWim -DossierMount $DossierMountWinPE -ImageWim (Join-Path $DossierExport 'sources/boot.wim') -IndexWim 2
$ListISO = Get-ChildItem -Path $DossierAvecISO | Where-Object { $_.Name -like '*.ISO*' }
foreach ($ISO in $ListISO) {
$PatchISO = $ISO.FullName
$vol = Mount-DiskImage -ImagePath $PatchISO -PassThru | Get-DiskImage | Get-Volume
foreach ($Item in $LanguageCode) {
$TestLP = $vol.DriveLetter+':\sources\'+$Item
if (Test-Path -LiteralPath $TestLP) {
if ($LangueDeBase -ne $Item) {
$lp = Join-Path (Join-Path $DossierLangueWinPE $Item) 'lp.cab'
[void](Add-CabIfExists -mount $DossierMountWinPE -cabPath $lp)
if ($OSclientOuServer -eq 'Client') { $cabNames = @("WinPE-Setup_${Item}.cab","WinPE-Setup-Client_${Item}.cab") }
else { $cabNames = @("WinPE-Setup_${Item}.cab","WinPE-Setup-Server_${Item}.cab") }
foreach ($cab in $cabNames) {
$rootPath = Join-Path $DossierLangueWinPE $cab
$altPath = Join-Path (Join-Path $DossierLangueWinPE $Item) $cab
if (-not (Add-CabIfExists -mount $DossierMountWinPE -cabPath $rootPath)) { [void](Add-CabIfExists -mount $DossierMountWinPE -cabPath $altPath) }
}
}
}
}
foreach ($ItemFont in 'ja-jp','ko-kr','zh-hk','zh-cn','zh-tw') {
$TestLP = $vol.DriveLetter+':\sources\'+$ItemFont
if (Test-Path -LiteralPath $TestLP) {
$fontCab = Join-Path $DossierLangueWinPE ("WinPE-FontSupport-{0}.cab" -f $ItemFont)
[void](Add-CabIfExists -mount $DossierMountWinPE -cabPath $fontCab)
}
}
$TestTH = $vol.DriveLetter+':\sources\th-TH'
if (Test-Path -LiteralPath $TestTH) { [void](Add-CabIfExists -mount $DossierMountWinPE -cabPath (Join-Path $DossierLangueWinPE 'WinPE-FontSupport-WinRE.cab')) }
Dismount-DiskImage -ImagePath $PatchISO | Out-Null
}
SaveImageWim -DossierMount $DossierMountWinPE
}
Function AjoutLangueDistribution {
Param(
[parameter(Mandatory=$true)][String]$DossierExport,
[parameter(Mandatory=$true)][String]$DossierAvecISO
)
Write-Log 'Copy distribution languages'
$LanguageCode = 'bg-bg','cs-cz','da-dk','de-de','el-gr','en-gb','en-us','es-es','es-mx','et-ee','fi-fi','fr-ca','fr-fr','hr-hr','hu-hu','it-it','ja-jp','ko-kr','lt-lt','lv-lv','nb-no','nl-nl','pl-pl','pt-br','pt-pt','ro-ro','ru-ru','sk-sk','sl-si','sv-se','tr-tr','uk-ua','zh-cn','zh-tw'
$ListISO = Get-ChildItem -Path $DossierAvecISO | Where-Object {$_.Name -like '*.ISO*'}
foreach ($ISO in $ListISO) {
$PatchISO = $ISO.FullName
$vol = Mount-DiskImage -ImagePath $PatchISO -PassThru | Get-DiskImage | Get-Volume
foreach ($Item in $LanguageCode) {
$TestLP = $vol.DriveLetter+':\sources\'+$Item
if(Test-Path -Path $TestLP){ Write-Log "copy : $TestLP"; Robocopy "$TestLP" "$DossierExport\Sources\$Item" /MIR | Out-Null }
}
Dismount-DiskImage -ImagePath $PatchISO | Out-Null
}
}
function ActivateNetFX3 {
param(
[parameter(Mandatory=$true)][string]$DossierMountWindows,
[parameter(Mandatory=$true)][string]$DossierExport,
[parameter(Mandatory=$true)][string]$DossierExportsxs,
[parameter(Mandatory=$true)][string]$IndexName
)
$listwim = Get-WindowsImage -ImagePath "$DossierExport\sources\install.wim"
foreach ($Version in $listwim) {
$NameWindowsVersion = $Version.ImageName
$IndexCheck = $Version.ImageIndex
Write-Log "$NameWindowsVersion VS $IndexName"
If (($NameWindowsVersion -eq $IndexName) -or ($IndexName -eq 'ALL')) {
Write-Log "Enable NetFx3 : $NameWindowsVersion"
MontageWim -DossierMount $DossierMountWindows -ImageWim "$DossierExport\sources\install.wim" -IndexWim $IndexCheck
$SourceSXS = Join-Path $DossierExportsxs $NameWindowsVersion
Write-Log "SourcesSXS=$SourceSXS"
Enable-WindowsOptionalFeature -Path $DossierMountWindows -FeatureName 'NetFx3' -All -Source $SourceSXS | Out-Null
SaveImageWim -DossierMount $DossierMountWindows
}
}
}
Function FakeLangIni {
Param(
[parameter(Mandatory=$true)][String]$DossierExport,
[parameter(Mandatory=$true)][String]$Languebase,
[parameter(Mandatory=$true)][String]$DossierAvecISO
)
Write-Log 'Generate lang.ini'
$LanguageCode = 'bg-bg','cs-cz','da-dk','de-de','el-gr','en-gb','en-us','es-es','es-mx','et-ee','fi-fi','fr-ca','fr-fr','hr-hr','hu-hu','it-it','ja-jp','ko-kr','lt-lt','lv-lv','nb-no','nl-nl','pl-pl','pt-pt','ro-ro','ru-ru','sk-sk','sl-si','sv-se','tr-tr','uk-ua','zh-cn','zh-tw'
Remove-item "$DossierExport\Sources\lang.ini" -Force -Recurse -ErrorAction SilentlyContinue
'' | Out-File "$DossierExport\Sources\lang.ini" -Append -Encoding UTF8 -Force
'[Available UI Languages]' | Out-File "$DossierExport\Sources\lang.ini" -Append -Encoding UTF8 -Force
"$Languebase = 3" | Out-File "$DossierExport\Sources\lang.ini" -Append -Encoding UTF8 -Force
$ListISO = Get-ChildItem -Path $DossierAvecISO | Where-Object {$_.Name -like '*.ISO*'}
foreach ($ISO in $ListISO) {
$PatchISO = $ISO.FullName
$vol = Mount-DiskImage -ImagePath $PatchISO -PassThru | Get-DiskImage | Get-Volume
foreach ($Item in $LanguageCode) {
$TestLP = $vol.DriveLetter+':\sources\'+$Item
if(Test-Path -Path $TestLP){ if ($Item -notlike "*$Languebase*") { "$Item = 2" | Out-File "$DossierExport\Sources\lang.ini" -Append -Encoding UTF8 -Force } }
}
Dismount-DiskImage -ImagePath $PatchISO | Out-Null
}
'' | Out-File "$DossierExport\Sources\lang.ini" -Append -Encoding UTF8 -Force
'[Fallback Languages]' | Out-File "$DossierExport\Sources\lang.ini" -Append -Encoding UTF8 -Force
"$Languebase = $Languebase" | Out-File "$DossierExport\Sources\lang.ini" -Append -Encoding UTF8 -Force
}
Function Wimindex {
Param(
[parameter(Mandatory=$true)][String]$DossierTravail,
[parameter(Mandatory=$true)][String]$DossierExport,
[parameter(Mandatory=$true)][String]$IndexName,
[parameter(Mandatory=$true)][String]$DossierAvecISO
)
Write-Log 'Build reduced install.wim (selected edition)'
$LanguageCode = 'bg-bg','cs-cz','da-dk','de-de','el-gr','en-gb','en-us','es-es','es-mx','et-ee','fi-fi','fr-ca','fr-fr','hr-hr','hu-hu','it-it','ja-jp','ko-kr','lt-lt','lv-lv','nb-no','nl-nl','pl-pl','pt-br','pt-pt','ro-ro','sk-sk','sl-si','sv-se','tr-tr','uk-ua','zh-cn','zh-tw'
if (Test-Path "$DossierExport\Sources\install.wim") { Remove-item "$DossierExport\Sources\install.wim" -Force -Recurse }
$ListISO = Get-ChildItem -Path $DossierAvecISO | Where-Object {$_.Name -like '*.ISO*'}
foreach ($ISO in $ListISO) {
$PatchISO = $ISO.FullName
$vol = Mount-DiskImage -ImagePath $PatchISO -PassThru | Get-DiskImage | Get-Volume
foreach ($Item in $LanguageCode) {
$TestLP = $vol.DriveLetter+':\sources\'+$Item
if(Test-Path -Path $TestLP){
$LettreISO = "$($vol.DriveLetter):\"
Write-Log "copy : $PatchISO"
Robocopy "$LettreISO" "$DossierTravail" /MIR | Out-Null
$listwim = Get-WindowsImage -ImagePath "$DossierTravail\sources\install.wim"
foreach ($Version in $listwim) {
$NameWindowsVersion = $Version.ImageName
$IndexCheck = $Version.ImageIndex
$NameWindowsVersion = $NameWindowsVersion -replace [char]160,' '
$NameWindowsVersion = $NameWindowsVersion -replace [char]201,'É'
$NameWindowsVersion = $NameWindowsVersion.Replace('Éducation','Education').Replace('Entreprise','Enterprise').Replace('Professionnel','Pro').Replace('pour les Stations de travail','for Workstations').Trim()
Write-Log "$NameWindowsVersion VS $IndexName"
If ($NameWindowsVersion -eq $IndexName) {
Write-Log "export : $DossierExport\Sources\install.wim => $NameWindowsVersion"
Dism /export-image /SourceImageFile:"$DossierTravail\sources\install.wim" /Sourceindex:$IndexCheck /DestinationImageFile:"$DossierExport\Sources\install.wim" /DestinationName:$Item | Out-Null
}
}
}
}
Dismount-DiskImage -ImagePath $PatchISO | Out-Null
}
}
# --------------------------- GUI (DPI-safe + Fancy buttons + ListView log) ---------------------------
[System.Windows.Forms.Application]::EnableVisualStyles()
Assert-Admin
$Form = New-Object System.Windows.Forms.Form
$Form.Text = 'Build Windows 11 MUI — MultiIndex (GUI Pro)'
$Form.Size = [System.Drawing.Size]::new(980,700)
$Form.MinimumSize = [System.Drawing.Size]::new(960,660)
$Form.StartPosition = 'CenterScreen'
$Form.Font = New-Object System.Drawing.Font('Segoe UI',9)
$Form.BackColor = [System.Drawing.Color]::FromArgb(250,250,251)
$Form.AutoScaleMode = 'Dpi'
$Form.KeyPreview = $true
# --- helpers boutons ---
function Set-RoundedRegion {
param([System.Windows.Forms.Button]$Button,[int]$Radius=6)
$gp = New-Object System.Drawing.Drawing2D.GraphicsPath
$rect = New-Object System.Drawing.Rectangle(0,0,$Button.Width,$Button.Height)
$d = $Radius * 2
$gp.AddArc(0,0,$d,$d,180,90) | Out-Null
$gp.AddArc($rect.Width-$d,0,$d,$d,270,90) | Out-Null
$gp.AddArc($rect.Width-$d,$rect.Height-$d,$d,$d,0,90) | Out-Null
$gp.AddArc(0,$rect.Height-$d,$d,$d,90,90) | Out-Null
$gp.CloseFigure()
$Button.Region = New-Object System.Drawing.Region($gp)
$gp.Dispose()
}
function New-FancyButton {
param([string]$Text,[ValidateSet('primary','secondary')][string]$Kind='secondary')
$b = New-Object System.Windows.Forms.Button
$b.Text = $Text; $b.Width = 140; $b.Height = 34
$b.FlatStyle = 'Flat'; $b.FlatAppearance.BorderSize = 0
$b.UseVisualStyleBackColor = $false; $b.Margin = '6,2,6,2'
if ($Kind -eq 'primary') {
$b.BackColor = [System.Drawing.Color]::FromArgb(40,132,244); $b.ForeColor = [System.Drawing.Color]::White
} else {
$b.BackColor = [System.Drawing.Color]::FromArgb(240,243,247); $b.ForeColor = [System.Drawing.Color]::FromArgb(32,32,32)
$b.FlatAppearance.BorderSize = 1; $b.FlatAppearance.BorderColor = [System.Drawing.Color]::FromArgb(208,213,220)
}
$b.Tag = $Kind
$b.Add_MouseEnter({ if ($this.Tag -eq 'primary') { $this.BackColor = [System.Drawing.Color]::FromArgb(30,120,235) } else { $this.BackColor = [System.Drawing.Color]::FromArgb(233,237,242) } })
$b.Add_MouseLeave({ if ($this.Tag -eq 'primary') { $this.BackColor = [System.Drawing.Color]::FromArgb(40,132,244) } else { $this.BackColor = [System.Drawing.Color]::FromArgb(240,243,247) } })
$b.Add_Resize({ param($s,$e) Set-RoundedRegion -Button $s -Radius 6 })
return $b
}
# --- Group: Paths (DPI safe) ---
$grpPaths = New-Object System.Windows.Forms.GroupBox -Property @{
Text='Paths'; Location=[System.Drawing.Point]::new(10,10); Size=[System.Drawing.Size]::new(950,120); Anchor='Top,Left,Right'
}
$tlpPaths = New-Object System.Windows.Forms.TableLayoutPanel -Property @{
ColumnCount = 2; RowCount = 2; Dock='Fill'; Padding='10,10,10,10'
}
$tlpPaths.ColumnStyles.Add([System.Windows.Forms.ColumnStyle]::new([System.Windows.Forms.SizeType]::AutoSize)) | Out-Null
$tlpPaths.ColumnStyles.Add([System.Windows.Forms.ColumnStyle]::new([System.Windows.Forms.SizeType]::Percent,100)) | Out-Null
$tlpPaths.RowStyles.Add([System.Windows.Forms.RowStyle]::new([System.Windows.Forms.SizeType]::AutoSize)) | Out-Null
$tlpPaths.RowStyles.Add([System.Windows.Forms.RowStyle]::new([System.Windows.Forms.SizeType]::AutoSize)) | Out-Null
$lblRoot = New-Object System.Windows.Forms.Label -Property @{ Text='Project root folder'; AutoSize=$true; Anchor='Left' }
$tbRoot = New-Object System.Windows.Forms.TextBox -Property @{ Dock='Fill' }
$lblLocal= New-Object System.Windows.Forms.Label -Property @{ Text='Local work root'; AutoSize=$true; Anchor='Left' }
$tbLocal = New-Object System.Windows.Forms.TextBox -Property @{ Dock='Fill' }
$tlpPaths.Controls.Add($lblRoot,0,0); $tlpPaths.Controls.Add($tbRoot,1,0)
$tlpPaths.Controls.Add($lblLocal,0,1); $tlpPaths.Controls.Add($tbLocal,1,1)
$grpPaths.Controls.Add($tlpPaths)
# --- Group: Options ---
$grpOpts = New-Object System.Windows.Forms.GroupBox -Property @{
Text='Options'; Location=[System.Drawing.Point]::new(10,$grpPaths.Bottom+10); Size=[System.Drawing.Size]::new(950,90); Anchor='Top,Left,Right'
}
$lblType = New-Object System.Windows.Forms.Label -Property @{ Text='Type'; Location=[System.Drawing.Point]::new(14,25); AutoSize=$true }
$cboType = New-Object System.Windows.Forms.ComboBox -Property @{ DropDownStyle='DropDownList'; Location=[System.Drawing.Point]::new(14,45); Width=120; Enabled=$false }
$cboType.Items.AddRange(@('Client','Server')) | Out-Null
$lblEdition = New-Object System.Windows.Forms.Label -Property @{ Text='Edition (IndexName)'; Location=[System.Drawing.Point]::new(150,25); AutoSize=$true }
$cboEdition = New-Object System.Windows.Forms.ComboBox -Property @{ Location=[System.Drawing.Point]::new(150,45); Width=260; Enabled=$false }
$cboEdition.Items.AddRange(@('Windows 11 Education','Windows 11 Enterprise','Windows 11 Pro','Windows 11 Pro Education','Windows 11 Pro for Workstations')) | Out-Null
$lblLang = New-Object System.Windows.Forms.Label -Property @{ Text='Base language'; Location=[System.Drawing.Point]::new(430,25); AutoSize=$true }
$cboLang = New-Object System.Windows.Forms.ComboBox -Property @{ Location=[System.Drawing.Point]::new(430,45); Width=180; DropDownStyle='DropDownList'; Enabled=$false }
$grpOpts.Controls.AddRange(@($lblType,$cboType,$lblEdition,$cboEdition,$lblLang,$cboLang))
# --- Group: Steps ---
$grpSteps = New-Object System.Windows.Forms.GroupBox -Property @{
Text='Steps'; Location=[System.Drawing.Point]::new(10,$grpOpts.Bottom+10); Size=[System.Drawing.Size]::new(950,70); Anchor='Top,Left,Right'
}
$chk1 = New-Object System.Windows.Forms.CheckBox -Property @{ Text='Step 1: Copy installation files'; Location=[System.Drawing.Point]::new(14,30); Checked=$true; AutoSize=$true }
$chk2 = New-Object System.Windows.Forms.CheckBox -Property @{ Text='Step 2: Add WinPE languages'; Location=[System.Drawing.Point]::new(260,30); Checked=$true; AutoSize=$true }
$chk3 = New-Object System.Windows.Forms.CheckBox -Property @{ Text='Step 3: Distribution languages'; Location=[System.Drawing.Point]::new(500,30); Checked=$true; AutoSize=$true }
$chk4 = New-Object System.Windows.Forms.CheckBox -Property @{ Text='Step 4: NetFX3 + lang.ini'; Location=[System.Drawing.Point]::new(750,30); Checked=$true; AutoSize=$true }
$grpSteps.Controls.AddRange(@($chk1,$chk2,$chk3,$chk4))
# --- Group: Actions ---
$grpActions = New-Object System.Windows.Forms.GroupBox -Property @{
Text='Actions'; Location=[System.Drawing.Point]::new(10,$grpSteps.Bottom+10); Size=[System.Drawing.Size]::new(950,70); Anchor='Top,Left,Right'
}
$flow = New-Object System.Windows.Forms.FlowLayoutPanel -Property @{
Dock='Fill'; FlowDirection='LeftToRight'; WrapContents=$false; AutoSize=$false; Padding='4,6,4,6'
}
$btnCheck = New-FancyButton 'Check ISO' 'secondary'
$btnBuild = New-FancyButton 'Run Build' 'primary'
$btnTest = New-FancyButton 'Test folders' 'secondary'
$btnSave = New-FancyButton 'Save Config' 'secondary'
$btnLoad = New-FancyButton 'Load Config' 'secondary'
$flow.Controls.AddRange(@($btnCheck,$btnBuild,$btnTest,$btnSave,$btnLoad))
$grpActions.Controls.Add($flow)
# --- Log ListView + status ---
$LogBox = New-Object System.Windows.Forms.ListView -Property @{
View='Details'; FullRowSelect=$true; GridLines=$false; HideSelection=$false
Location=[System.Drawing.Point]::new(10,$grpActions.Bottom+10)
Size=[System.Drawing.Size]::new(950,250); Anchor='Top,Left,Right'
HeaderStyle='Nonclickable'; Font=(New-Object System.Drawing.Font('Segoe UI',9))
}
$global:LogBox = $LogBox # <<< IMPORTANT pour Write-Log
# hauteur de ligne (imagelist)
$img = New-Object System.Windows.Forms.ImageList
$img.ImageSize = [Drawing.Size]::new(1,20)
$LogBox.SmallImageList = $img
[void]$LogBox.Columns.Add('Time',140)
[void]$LogBox.Columns.Add('Level',70)
[void]$LogBox.Columns.Add('Message',650)
$LogBox.Add_Resize({
$pad = 6
$msgWidth = $LogBox.ClientSize.Width - ($LogBox.Columns[0].Width + $LogBox.Columns[1].Width + $pad)
if ($msgWidth -lt 150) { $msgWidth = 150 }
$LogBox.Columns[2].Width = $msgWidth
})
# Menu contextuel: Copy / Clear
$cm = New-Object System.Windows.Forms.ContextMenuStrip
$miCopy = $cm.Items.Add('Copy selected')
$miClear = $cm.Items.Add('Clear')
$miCopy.Add_Click({
if ($LogBox.SelectedItems.Count -gt 0) {
$txt = $LogBox.SelectedItems | ForEach-Object { "{0} {1} {2}" -f $_.SubItems[0].Text,$_.SubItems[1].Text,$_.SubItems[2].Text } | Out-String
[System.Windows.Forms.Clipboard]::SetText($txt.TrimEnd())
}
})
$miClear.Add_Click({ $LogBox.Items.Clear() })
$LogBox.ContextMenuStrip = $cm
$status = New-Object System.Windows.Forms.StatusStrip -Property @{ SizingGrip=$false }
$global:StatusLabel = New-Object System.Windows.Forms.ToolStripStatusLabel -Property @{ Text='Ready'; Spring=$true }
[void]$status.Items.Add($global:StatusLabel)
$status.Dock = 'Bottom'
$status.BringToFront()
# add to form
$Form.Controls.AddRange(@($grpPaths,$grpOpts,$grpSteps,$grpActions,$LogBox,$status))
$Form.AcceptButton = $btnBuild
# ajuster la hauteur du log pour ne pas chevaucher la statusbar
function Update-LogArea {
$margin = 8
$newH = $Form.ClientSize.Height - $LogBox.Top - $status.Height - $margin
if ($newH -lt 80) { $newH = 80 }
$LogBox.Height = $newH
}
$Form.Add_Shown({ Update-LogArea })
$Form.Add_Resize({ Update-LogArea })
$Form.Add_Layout({ Update-LogArea })
# --------------------------- Defaults / default.json ---------------------------
$cboType.SelectedItem = 'Client'
$cboEdition.Text = 'Windows 11 Pro'
$tbRoot.Text = ''
$tbLocal.Text = ''
$DefaultJson = Join-Path $PSScriptRoot 'default.json'
if (Test-Path -LiteralPath $DefaultJson) {
try {
$cfg = Get-Content -LiteralPath $DefaultJson -Raw | ConvertFrom-Json
if ($cfg.ProjectRoot) { $tbRoot.Text = $cfg.ProjectRoot }
if ($cfg.LocalRoot) { $tbLocal.Text = $cfg.LocalRoot }
if ($cfg.ServerClient){ $cboType.Tag = $cfg.ServerClient } # applied after Check ISO
if ($cfg.OsName) { $cboEdition.Tag = $cfg.OsName } # applied after Check ISO
if ($cfg.BaseLangue) { $cboLang.Tag = $cfg.BaseLangue } # applied after Check ISO
if ($cfg.Step1_CopyInstall -ne $null) { $chk1.Checked = [bool]$cfg.Step1_CopyInstall }
if ($cfg.Step2_WinPELang -ne $null) { $chk2.Checked = [bool]$cfg.Step2_WinPELang }
if ($cfg.Step3_DistLang -ne $null) { $chk3.Checked = [bool]$cfg.Step3_DistLang }
if ($cfg.Step4_ModifyImg -ne $null) { $chk4.Checked = [bool]$cfg.Step4_ModifyImg }
Write-Log 'default.json loaded.'
} catch { Write-Log "Failed to read default.json: $_" 'WARN' }
}
function Get-Paths {
Ensure-ProjectSubfolders $tbRoot.Text
Ensure-LocalSubfolders $tbLocal.Text
return [ordered]@{
ISO = Join-Path $tbRoot.Text 'ISO'
WinPE_OCs = Join-Path $tbRoot.Text 'WinPE_OCs'
Export = Join-Path $tbRoot.Text 'Export'
SXS = Join-Path $tbRoot.Text 'SXS'
MountWinPE = Join-Path $tbLocal.Text 'MountWinPE'
MountWinRE = Join-Path $tbLocal.Text 'MountWinRE'
MountInstall = Join-Path $tbLocal.Text 'MountInstall'
scratch = Join-Path $tbLocal.Text 'scratch'
distribution = Join-Path $tbLocal.Text 'distribution'
}
}
function Get-Settings {
$serverClient = 'Client'
if ($cboType.Enabled -and $cboType.SelectedItem) { $serverClient = [string]$cboType.SelectedItem }
$osName = 'Windows 11 Pro'
if ($cboEdition.Enabled -and $cboEdition.Text) { $osName = [string]$cboEdition.Text }
$baseLang = ''
if ($cboLang.Enabled -and $cboLang.SelectedItem -and $cboLang.SelectedItem -ne 'Select') { $baseLang = [string]$cboLang.SelectedItem }
[ordered]@{
ProjectRoot = $tbRoot.Text
LocalRoot = $tbLocal.Text
ServerClient = $serverClient
OsName = $osName
BaseLangue = $baseLang
Step1_CopyInstall = $chk1.Checked
Step2_WinPELang = $chk2.Checked
Step3_DistLang = $chk3.Checked
Step4_ModifyImg = $chk4.Checked
}
}
# --------------------------- Actions ---------------------------
$btnTest.Add_Click({
try {
$p = Get-Paths
Write-Log "Project: $($tbRoot.Text)"
Write-Log "Local: $($tbLocal.Text)"
foreach($v in $p.GetEnumerator()){ Write-Log ("{0}: {1}" -f $v.Key, $v.Value) }
} catch { Write-Log "ERROR test: $_" 'ERROR' }
})
$btnSave.Add_Click({
try {
$json = (Get-Settings | ConvertTo-Json -Depth 5)
$sfd = New-Object System.Windows.Forms.SaveFileDialog
$sfd.Filter = 'JSON (*.json)|*.json'
$sfd.FileName = 'config.json'
if ($sfd.ShowDialog() -eq 'OK') { $json | Out-File -LiteralPath $sfd.FileName -Encoding UTF8; Write-Log "Config saved: $($sfd.FileName)" }
} catch { Write-Log "ERROR Save Config: $_" 'ERROR' }
})
$btnLoad.Add_Click({
try {
$ofd = New-Object System.Windows.Forms.OpenFileDialog
$ofd.Filter = 'JSON (*.json)|*.json'
if ($ofd.ShowDialog() -ne 'OK') { return }
$cfg = Get-Content -LiteralPath $ofd.FileName -Raw | ConvertFrom-Json
if ($cfg.ProjectRoot) { $tbRoot.Text = $cfg.ProjectRoot }
if ($cfg.LocalRoot) { $tbLocal.Text = $cfg.LocalRoot }
if ($cfg.ServerClient){ $cboType.Tag = $cfg.ServerClient }
if ($cfg.OsName) { $cboEdition.Tag = $cfg.OsName }
if ($cfg.BaseLangue) { $cboLang.Tag = $cfg.BaseLangue }
if ($cfg.Step1_CopyInstall -ne $null) { $chk1.Checked = [bool]$cfg.Step1_CopyInstall }
if ($cfg.Step2_WinPELang -ne $null) { $chk2.Checked = [bool]$cfg.Step2_WinPELang }
if ($cfg.Step3_DistLang -ne $null) { $chk3.Checked = [bool]$cfg.Step3_DistLang }
if ($cfg.Step4_ModifyImg -ne $null) { $chk4.Checked = [bool]$cfg.Step4_ModifyImg }
Write-Log "Config loaded: $($ofd.FileName)"
} catch { Write-Log "ERROR Load Config: $_" 'ERROR' }
})
# Check ISO: populate Base language and enable Options
$btnCheck.Add_Click({
try {
$p = Get-Paths
$isoDir = $p.ISO
if (-not (Test-Path -LiteralPath $isoDir)) { [System.Windows.Forms.MessageBox]::Show("ISO folder not found:`n$isoDir","ISO Languages","OK","Warning") | Out-Null; return }
$isos = Get-ChildItem -LiteralPath $isoDir -Filter *.iso -ErrorAction SilentlyContinue
if (-not $isos) { [System.Windows.Forms.MessageBox]::Show("No ISO found in:`n$isoDir","ISO Languages","OK","Information") | Out-Null; return }
$LanguageCode = 'bg-bg','cs-cz','da-dk','de-de','el-gr','en-gb','en-us','es-es','es-mx','et-ee','fi-fi','fr-ca','fr-fr','hr-hr','hu-hu','it-it','ja-jp','ko-kr','lt-lt','lv-lv','nb-no','nl-nl','pl-pl','pt-br','pt-pt','ro-ro','ru-ru','sk-sk','sl-si','sv-se','tr-tr','uk-ua','zh-cn','zh-tw'
$set = [System.Collections.Generic.HashSet[string]]::new()
foreach($iso in $isos){
try {
$vol = Mount-DiskImage -ImagePath $iso.FullName -PassThru | Get-DiskImage | Get-Volume | Select-Object -First 1
if (-not $vol) { continue }
foreach($lc in $LanguageCode){
$langPath = ('{0}:\\sources\\{1}' -f $vol.DriveLetter, $lc)
if (Test-Path -LiteralPath $langPath) { [void]$set.Add($lc) }
}
} finally { Dismount-DiskImage -ImagePath $iso.FullName -ErrorAction SilentlyContinue | Out-Null }
}
if ($set.Count -eq 0) { [System.Windows.Forms.MessageBox]::Show('No language folders were found in any ISO.','ISO Languages','OK','Information') | Out-Null; return }
$langs = @($set) | Sort-Object
Write-Log ("Languages found: {0}" -f ($langs -join ', '))
$cboLang.Items.Clear(); [void]$cboLang.Items.Add('Select')
foreach($l in $langs){ [void]$cboLang.Items.Add($l) }
$cboLang.Enabled = $true
$cboType.Enabled = $true
$cboEdition.Enabled = $true
if ($cboLang.Tag -and $cboLang.Items.Contains($cboLang.Tag)) { $cboLang.SelectedItem = $cboLang.Tag } else { $cboLang.SelectedIndex = 0 }
if ($cboType.Tag -and $cboType.Items.Contains($cboType.Tag)) { $cboType.SelectedItem = $cboType.Tag } else { if (-not $cboType.SelectedItem) { $cboType.SelectedItem = 'Client' } }
if ($cboEdition.Tag -and $cboEdition.Items.Contains($cboEdition.Tag)) { $cboEdition.SelectedItem = $cboEdition.Tag } else { if (-not $cboEdition.Text) { $cboEdition.Text = 'Windows 11 Pro' } }
} catch {
Write-Log "ERROR Check ISO: $_" 'ERROR'
[System.Windows.Forms.MessageBox]::Show($_.Exception.Message,'Error','OK','Error') | Out-Null
}
})
$btnBuild.Add_Click({
try {
if (-not $cboLang.Enabled -or -not $cboLang.SelectedItem -or $cboLang.SelectedItem -eq 'Select') {
[System.Windows.Forms.MessageBox]::Show('Please click "Check ISO" and choose a Base language before building.','Missing Base language','OK','Warning') | Out-Null
return
}
$p = Get-Paths
$ServerClient = 'Client'
if ($cboType.Enabled -and $cboType.SelectedItem) { $ServerClient = [string]$cboType.SelectedItem }
if ($ServerClient -eq 'Serveur') { $ServerClient = 'Server' }
$OsName = 'Windows 11 Pro'
if ($cboEdition.Enabled -and $cboEdition.Text) { $OsName = [string]$cboEdition.Text }
$BaseLangue = [string]$cboLang.SelectedItem
Write-Log '--- Build started (derived paths) ---'
CleanupSafe -DossierMountWindows $p.MountInstall -DossierMountWinPE $p.MountWinPE -DossierMountWinRE $p.MountWinRE -DossierMountScratch $p.scratch -DossierTravail $p.distribution
Prepare -DossierMountWindows $p.MountInstall -DossierMountWinPE $p.MountWinPE -DossierMountWinRE $p.MountWinRE -DossierMountScratch $p.scratch -DossierTravail $p.distribution
if ($chk1.Checked) { CopierFichierInstallation -LangueBase $BaseLangue -DossierExport $p.Export -DossierAvecISO $p.ISO }
if ($chk2.Checked) { AjoutLangueWinPE -DossierMountWinPE $p.MountWinPE -DossierAvecISO $p.ISO -OSclientOuServer $ServerClient -DossierLangueWinPE $p.WinPE_OCs -DossierExport $p.Export -LangueDeBase $BaseLangue }
if ($chk3.Checked) { AjoutLangueDistribution -DossierExport $p.Export -DossierAvecISO $p.ISO }
if ($chk4.Checked) {
Wimindex -DossierAvecISO $p.ISO -DossierExport $p.Export -DossierTravail $p.distribution -IndexName $OsName
CopierSXS -DossierExportsxs $p.SXS -DossierAvecISO $p.ISO
ActivateNetFX3 -DossierMountWindows $p.MountInstall -DossierExport $p.Export -DossierExportsxs $p.SXS -IndexName 'ALL'
FakeLangIni -DossierExport $p.Export -DossierAvecISO $p.ISO -Languebase $BaseLangue
}
CleanupSafe -DossierMountWindows $p.MountInstall -DossierMountWinPE $p.MountWinPE -DossierMountWinRE $p.MountWinRE -DossierMountScratch $p.scratch -DossierTravail $p.distribution
Write-Log '--- Build finished successfully ---'
# Popup de fin + son
[System.Media.SystemSounds]::Asterisk.Play()
[System.Windows.Forms.MessageBox]::Show(
$Form,
"Build finished successfully.`n`nExport folder:`n$($p.Export)",
'Build MUI — Completed',
[System.Windows.Forms.MessageBoxButtons]::OK,
[System.Windows.Forms.MessageBoxIcon]::Information
) | Out-Null
} catch {
Write-Log "ERROR build: $_" 'ERROR'
[System.Windows.Forms.MessageBox]::Show($_.Exception.Message,'Error','OK','Error') | Out-Null
}
})
# Show UI
[void]$Form.ShowDialog()