# DeployCenter - Partitionnement et installation d'une edition Windows # Compatible Windows PowerShell 5.1 / WinPE. function Write-DeployCenterDeploymentLog { param([AllowNull()][scriptblock]$LogAction, [string]$Message, [string]$Level = 'INFO') if ($LogAction) { & $LogAction $Message $Level } } function ConvertTo-DeployCenterNativeArgument { param([AllowNull()][string]$Value) if ($null -eq $Value) { $Value = '' } if ($Value.Length -gt 0 -and $Value -notmatch '[\s"]') { return $Value } $builder = New-Object System.Text.StringBuilder [void]$builder.Append('"') $backslashCount = 0 foreach ($character in $Value.ToCharArray()) { if ($character -eq [char]92) { $backslashCount++ continue } if ($character -eq [char]34) { if ($backslashCount -gt 0) { [void]$builder.Append(('\' * (2 * $backslashCount))) } [void]$builder.Append('\"') $backslashCount = 0 continue } if ($backslashCount -gt 0) { [void]$builder.Append(('\' * $backslashCount)) } [void]$builder.Append($character) $backslashCount = 0 } if ($backslashCount -gt 0) { [void]$builder.Append(('\' * (2 * $backslashCount))) } [void]$builder.Append('"') return $builder.ToString() } function New-DeployCenterDismSourceAccess { param( [Parameter(Mandatory=$true)][string]$ImagePath, [Parameter(Mandatory=$true)][string]$TemporaryRoot, [AllowNull()][scriptblock]$LogAction = $null ) $lease = [pscustomobject]@{ ImagePath = $ImagePath; DriveLetter = $null; VolumeRoot = $null } if (-not $ImagePath.StartsWith('\\?\Volume{', [StringComparison]::OrdinalIgnoreCase)) { return $lease } $volumeEnd = $ImagePath.IndexOf('}\', [StringComparison]::OrdinalIgnoreCase) if ($volumeEnd -lt 0) { throw "Chemin de volume GUID invalide pour l’image : $ImagePath" } $volumeRoot = $ImagePath.Substring(0, $volumeEnd + 2) $relativePath = $ImagePath.Substring($volumeEnd + 2) $usedLetters = @{} foreach ($volume in @(Get-Volume -ErrorAction SilentlyContinue)) { if ($volume.DriveLetter) { $usedLetters[([string]$volume.DriveLetter).ToUpperInvariant()] = $true } } foreach ($drive in @(Get-PSDrive -PSProvider FileSystem -ErrorAction SilentlyContinue)) { $usedLetters[([string]$drive.Name).ToUpperInvariant()] = $true } $letter = $null foreach ($candidate in @('Z','Y','X','W','V','U','T','S','R','Q','P','O','N','M','L','K','J','I','H','G','F','E')) { if ($candidate -notin @('C','D') -and -not $usedLetters.ContainsKey($candidate)) { $letter = $candidate break } } if (-not $letter) { throw 'Aucune lettre temporaire disponible pour accéder à la source Windows.' } $mountVolPath = Join-Path $env:WINDIR 'System32\mountvol.exe' if (-not (Test-Path -LiteralPath $mountVolPath -PathType Leaf)) { $mountVolPath = 'mountvol.exe' } $letterPath = ("{0}:\{1}" -f $letter, $relativePath) $mountError = $null try { Invoke-DeployCenterNativeCommand -FilePath $mountVolPath -Arguments @(("{0}:\" -f $letter), $volumeRoot) ` -TemporaryRoot $TemporaryRoot -LogAction $LogAction } catch { $mountError = $_.Exception } if (-not (Test-Path -LiteralPath $letterPath -PathType Leaf)) { try { Invoke-DeployCenterNativeCommand -FilePath $mountVolPath -Arguments @(("{0}:\" -f $letter), '/D') ` -TemporaryRoot $TemporaryRoot -LogAction $LogAction } catch { } throw "La source Windows n’est pas accessible après l’affectation temporaire de ${letter}:." } if ($mountError) { Write-DeployCenterDeploymentLog -LogAction $LogAction ` -Message ("mountvol n’a pas fourni de code de sortie exploitable, mais la source est accessible via {0}:. L’affectation est confirmée." -f $letter) ` -Level 'WARNING' } $lease.ImagePath = $letterPath $lease.DriveLetter = $letter $lease.VolumeRoot = $volumeRoot return $lease } function Remove-DeployCenterDismSourceAccess { param( [AllowNull()]$Lease, [Parameter(Mandatory=$true)][string]$TemporaryRoot, [AllowNull()][scriptblock]$LogAction = $null ) if ($null -eq $Lease -or -not $Lease.DriveLetter) { return } $mountVolPath = Join-Path $env:WINDIR 'System32\mountvol.exe' if (-not (Test-Path -LiteralPath $mountVolPath -PathType Leaf)) { $mountVolPath = 'mountvol.exe' } $releaseError = $null try { Invoke-DeployCenterNativeCommand -FilePath $mountVolPath -Arguments @(("{0}:\" -f $Lease.DriveLetter), '/D') ` -TemporaryRoot $TemporaryRoot -LogAction $LogAction } catch { $releaseError = $_.Exception } if (Test-Path -LiteralPath ([string]$Lease.ImagePath) -PathType Leaf) { $details = if ($releaseError) { $releaseError.Message } else { 'Le fichier source reste accessible.' } Write-DeployCenterDeploymentLog -LogAction $LogAction ` -Message "Impossible de confirmer la libération de la lettre temporaire $($Lease.DriveLetter): $details" -Level 'WARNING' } elseif ($releaseError) { Write-DeployCenterDeploymentLog -LogAction $LogAction ` -Message ("mountvol n’a pas fourni de code de sortie exploitable, mais la source n’est plus accessible via {0}:. La libération est confirmée." -f $Lease.DriveLetter) ` -Level 'WARNING' } } function Get-DeployCenterNativeProgressPercent { param([Parameter(Mandatory=$true)][string]$OutputPath) $stream = $null $reader = $null try { $stream = [System.IO.File]::Open($OutputPath, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::ReadWrite) $reader = New-Object System.IO.StreamReader($stream, [System.Text.Encoding]::Default, $true) $content = $reader.ReadToEnd() } catch { return $null } finally { if ($reader) { $reader.Dispose() } elseif ($stream) { $stream.Dispose() } } $progressMatches = [regex]::Matches($content, '(?\d{1,3}(?:\.\d+)?)\s*%') if ($progressMatches.Count -eq 0) { return $null } $value = [double]$progressMatches[$progressMatches.Count - 1].Groups['percent'].Value return [int][math]::Max(0, [math]::Min(100, [math]::Floor($value))) } function Invoke-DeployCenterNativeCommand { param( [Parameter(Mandatory=$true)][string]$FilePath, [Parameter(Mandatory=$true)][string[]]$Arguments, [Parameter(Mandatory=$true)][string]$TemporaryRoot, [AllowNull()][scriptblock]$LogAction = $null, [AllowNull()][scriptblock]$ProgressAction = $null ) if (-not (Test-Path -LiteralPath $TemporaryRoot -PathType Container)) { New-Item -Path $TemporaryRoot -ItemType Directory -Force -ErrorAction Stop | Out-Null } $id = [guid]::NewGuid().ToString('N') $stdoutPath = Join-Path $TemporaryRoot ("native_{0}.out" -f $id) $stderrPath = Join-Path $TemporaryRoot ("native_{0}.err" -f $id) $dismLogPath = Join-Path $TemporaryRoot ("dism_{0}.log" -f $id) $isDism = [IO.Path]::GetFileName($FilePath) -ieq 'dism.exe' if ($isDism) { $Arguments += "/LogPath:$dismLogPath" } $argumentLine = ($Arguments | ForEach-Object { ConvertTo-DeployCenterNativeArgument -Value ([string]$_) }) -join ' ' try { Write-DeployCenterDeploymentLog -LogAction $LogAction -Message ("Commande native : {0} {1}" -f $FilePath, $argumentLine) $process = Start-Process -FilePath $FilePath -ArgumentList $argumentLine ` -PassThru -WindowStyle Hidden -RedirectStandardOutput $stdoutPath ` -RedirectStandardError $stderrPath -ErrorAction Stop while (-not $process.WaitForExit(250)) { if ($ProgressAction) { [System.Windows.Forms.Application]::DoEvents() if ($isDism -and (Test-Path -LiteralPath $stdoutPath -PathType Leaf)) { $nativePercent = Get-DeployCenterNativeProgressPercent -OutputPath $stdoutPath if ($null -ne $nativePercent) { & $ProgressAction $null $nativePercent } } } } $process.WaitForExit() $process.Refresh() $exitCode = $process.ExitCode $nativeOutputLines = New-Object System.Collections.Generic.List[string] $nativeOutputEncoding = [System.Console]::OutputEncoding if ($null -eq $nativeOutputEncoding) { $nativeOutputEncoding = [System.Text.Encoding]::Default } foreach ($outputPath in @($stdoutPath, $stderrPath)) { if (Test-Path -LiteralPath $outputPath -PathType Leaf) { foreach ($line in [System.IO.File]::ReadAllLines($outputPath, $nativeOutputEncoding)) { if (-not [string]::IsNullOrWhiteSpace($line)) { [void]$nativeOutputLines.Add([string]$line) Write-DeployCenterDeploymentLog -LogAction $LogAction -Message ([string]$line) -Level 'INFO' } } } } if ($ProgressAction -and $isDism) { $nativePercent = Get-DeployCenterNativeProgressPercent -OutputPath $stdoutPath if ($null -ne $nativePercent) { & $ProgressAction $null $nativePercent } } if ($null -eq $exitCode -and $isDism -and @($nativeOutputLines | Where-Object { $_ -match '(?i)completed successfully|a r.ussi' }).Count -gt 0) { Write-DeployCenterDeploymentLog -LogAction $LogAction ` -Message 'DISM a confirmé la réussite de l’opération, mais WinPE n’a pas fourni de code de sortie exploitable.' ` -Level 'WARNING' return } if ($null -eq $exitCode -and [IO.Path]::GetFileName($FilePath) -ieq 'diskpart.exe') { # Dans certains WinPE, Start-Process ne renseigne pas ExitCode même # lorsque DiskPart a terminé. L’appelant vérifie ensuite l’état réel # des partitions et des lettres avant de poursuivre. Write-DeployCenterDeploymentLog -LogAction $LogAction ` -Message "DiskPart n'a pas fourni de code de sortie exploitable ; le résultat sera confirmé par vérification du disque." ` -Level 'WARNING' return } if ($null -eq $exitCode -and [IO.Path]::GetFileName($FilePath) -ieq 'bcdboot.exe') { $bootOutput = ($nativeOutputLines -join "`n").Normalize([System.Text.NormalizationForm]::FormD) -replace '\p{Mn}', '' $bootFilesCreated = $bootOutput -match '(?i)boot files successfully created|fichiers de d\s*marrage.*bien.*cr' $systemArgumentIndex = [array]::IndexOf($Arguments, '/s') $bcdStoreExists = $false if ($systemArgumentIndex -ge 0 -and $systemArgumentIndex + 1 -lt $Arguments.Count) { $systemRoot = ([string]$Arguments[$systemArgumentIndex + 1]).TrimEnd('\') $bcdStorePath = "${systemRoot}\EFI\Microsoft\Boot\BCD" $bcdStoreExists = Test-Path -LiteralPath $bcdStorePath -PathType Leaf } if ($bootFilesCreated -or $bcdStoreExists) { Write-DeployCenterDeploymentLog -LogAction $LogAction ` -Message "bcdboot a créé les fichiers de démarrage, mais WinPE n’a pas fourni de code de sortie exploitable. La création est confirmée." ` -Level 'WARNING' return } } if ($null -eq $exitCode -or [int]$exitCode -ne 0) { $diagnostic = @() if ($isDism -and (Test-Path -LiteralPath $dismLogPath -PathType Leaf)) { $diagnostic = @(Get-Content -LiteralPath $dismLogPath -Tail 30 -ErrorAction SilentlyContinue | ForEach-Object { [string]$_ }) foreach ($line in $diagnostic) { Write-DeployCenterDeploymentLog -LogAction $LogAction -Message ("DISM : {0}" -f $line) -Level 'ERROR' } } $diagnosticMessage = $diagnostic -join ' | ' throw ("La commande {0} s'est terminée avec le code {1}. {2}" -f $FilePath, $exitCode, $diagnosticMessage) } } finally { foreach ($outputPath in @($stdoutPath, $stderrPath)) { if (Test-Path -LiteralPath $outputPath -PathType Leaf) { Remove-Item -LiteralPath $outputPath -Force -ErrorAction SilentlyContinue } } if (Test-Path -LiteralPath $dismLogPath -PathType Leaf) { Remove-Item -LiteralPath $dismLogPath -Force -ErrorAction SilentlyContinue } } } function Get-DeployCenterTemporaryDriveLetters { param([Parameter(Mandatory=$true)][int]$DiskNumber, [Parameter(Mandatory=$true)]$Profile) $targetLetters = @() try { $targetLetters = @(Get-Partition -DiskNumber $DiskNumber -ErrorAction SilentlyContinue | Where-Object { $_.DriveLetter } | ForEach-Object { ([string]$_.DriveLetter).ToUpperInvariant() }) } catch { } $occupied = @{} foreach ($volume in @(Get-Volume -ErrorAction SilentlyContinue)) { if (-not $volume.DriveLetter) { continue } $letter = ([string]$volume.DriveLetter).ToUpperInvariant() if ($targetLetters -contains $letter) { continue } $occupied[$letter] = $true } $lettersByRole = @{} foreach ($partition in $Profile.Partitions) { if ([string]$partition.Role -eq 'Reserved') { continue } $preferred = [string]$partition.DriveLetter if ([string]$partition.Role -eq 'System') { $preferred = 'S' } if ([string]$partition.Role -eq 'Recovery') { $preferred = 'R' } if ([string]::IsNullOrWhiteSpace($preferred)) { $preferred = 'W' } $preferred = $preferred.ToUpperInvariant() if (-not $occupied.ContainsKey($preferred)) { $selected = $preferred } else { $selected = $null foreach ($candidate in @('W','V','U','T','S','R','Q','P','O','N','M','L','K','J','I','H','G','F','E','D','C')) { if (-not $occupied.ContainsKey($candidate) -and $candidate -notin $lettersByRole.Values) { $selected = $candidate break } } if (-not $selected) { throw "Aucune lettre de lecteur libre pour la partition '$($partition.Name)'." } } if ($selected -in $lettersByRole.Values) { throw "La lettre ${selected}: est affectée à plusieurs partitions du profil." } $lettersByRole[[string]$partition.Role] = $selected $occupied[$selected] = $true } return $lettersByRole } function New-DeployCenterPartitionPlan { param( [Parameter(Mandatory=$true)]$DiskInfo, [Parameter(Mandatory=$true)][System.Collections.IDictionary]$Profile, [AllowNull()]$SizingState ) if ([string]$Profile.PartitionStyle -ne 'GPT' -or [string]$Profile.Firmware -ne 'UEFI') { throw "Le profil '$($Profile.Name)' n’est pas pris en charge par le moteur actuel (UEFI/GPT requis)." } $totalMB = [long][math]::Floor(([double]$DiskInfo.SizeBytes / 1MB)) $availableMB = $totalMB - 2 $usedMB = 0L $plan = New-Object System.Collections.Generic.List[object] foreach ($partition in $Profile.Partitions) { $policy = [string]$partition.Size.Policy $sizeMB = $null switch ($policy) { 'FixedMB' { $sizeMB = [long]$partition.Size.Value } 'ConfiguredMB' { $sizeMB = [long]$partition.Size.Value } 'UserInputGB' { if ($null -eq $SizingState -or [string]$SizingState.ProfileId -ne [string]$Profile.Id) { throw "Le profil '$($Profile.Name)' nécessite une taille utilisateur, qui n’a pas été validée." } $role = [string]$partition.Role if (-not $SizingState.Inputs.Contains($role)) { throw "La taille de la partition '$role' est absente." } $sizeMB = [long][math]::Round(([double]$SizingState.Inputs[$role] * 1024), 0) if ($sizeMB -lt ([long]$partition.Size.MinimumGB * 1024)) { throw "La partition '$role' est sous sa taille minimale." } } { $_ -in @('Remaining','RemainderAfterInputs') } { $sizeMB = $availableMB - $usedMB if ($sizeMB -lt 1) { throw "Le disque ne dispose plus d’espace pour la partition '$($partition.Name)'." } if ($policy -eq 'RemainderAfterInputs' -and $sizeMB -lt ([long]$partition.Size.MinimumGB * 1024)) { throw "L’espace restant pour '$($partition.Name)' est inférieur au minimum du profil." } } default { throw "Politique de taille non prise en charge lors du partitionnement : $policy" } } if ($null -ne $sizeMB) { $usedMB += [long]$sizeMB } $plan.Add([pscustomobject]@{ Definition = $partition; SizeMB = $sizeMB }) } if ($usedMB -gt $availableMB) { throw "La taille demandée dépasse la capacité du disque sélectionné." } return $plan.ToArray() } function ConvertTo-DeployCenterUnattendXml { param([Parameter(Mandatory=$true)][string]$HostName, [Parameter(Mandatory=$true)][string]$Architecture) $processorArchitecture = switch -Regex ($Architecture.ToLowerInvariant()) { 'arm64' { 'arm64'; break } 'x86|32' { 'x86'; break } default { 'amd64' } } return @" $HostName "@ } function Invoke-DeployCenterWindowsDeployment { [CmdletBinding()] param( [Parameter(Mandatory=$true)]$DiskInfo, [Parameter(Mandatory=$true)][System.Collections.IDictionary]$Profile, [AllowNull()]$SizingState, [Parameter(Mandatory=$true)]$Edition, [Parameter(Mandatory=$true)][string]$HostName, [Parameter(Mandatory=$true)][string]$Firmware, [Parameter(Mandatory=$true)][string]$TemporaryRoot, [AllowNull()][scriptblock]$LogAction = $null, [AllowNull()][scriptblock]$ProgressAction = $null ) if ($HostName -notmatch '^(?=.*[A-Za-z])[A-Za-z0-9](?:[A-Za-z0-9-]{0,13}[A-Za-z0-9])?$') { throw 'Le nom de machine doit respecter les règles NetBIOS et ne pas dépasser 15 caractères.' } if ([string]$Firmware -ne [string]$Profile.Firmware) { throw ("Le profil {0} exige le firmware {1}, détecté : {2}." -f $Profile.Name, $Profile.Firmware, $Firmware) } foreach ($requiredRole in @('System','Reserved','Recovery','Windows')) { $roleCount = @($Profile.Partitions | Where-Object { [string]$_.Role -eq $requiredRole }).Count if ($roleCount -ne 1) { throw "Le profil doit définir exactement une partition de rôle '$requiredRole'. Le disque n’a pas été modifié." } } $recoveryDefinitions = @($Profile.Partitions | Where-Object { [string]$_.Role -eq 'Recovery' }) if ($recoveryDefinitions.Count -ne 1 -or [string]$recoveryDefinitions[0].Size.Policy -ne 'ConfiguredMB') { throw "Le profil doit définir une taille configurée pour la partition Recovery. Le disque n’a pas été modifié." } $winrePartitionSizeMB = [long]$recoveryDefinitions[0].Size.Value if ($winrePartitionSizeMB -le 0) { throw "La taille configurée de la partition WinRE doit être strictement positive. Le disque n’a pas été modifié." } if (-not $Edition.ImagePath -or -not (Test-Path -LiteralPath ([string]$Edition.ImagePath) -PathType Leaf)) { throw "Le chemin de l’image Windows sélectionnée est introuvable." } if (-not $Edition.Index -or [int]$Edition.Index -lt 1) { throw "L’index de l’édition Windows est invalide." } $disk = Get-Disk -Number ([int]$DiskInfo.Index) -ErrorAction Stop if ($disk.IsBoot -or $disk.IsSystem) { throw 'Le disque sélectionné héberge le système WinPE et ne peut pas être effacé.' } if ($disk.IsOffline) { [void](Set-Disk -Number $disk.Number -IsOffline $false -ErrorAction Stop) } if ($disk.IsReadOnly) { [void](Set-Disk -Number $disk.Number -IsReadOnly $false -ErrorAction Stop) } if (-not (Test-Path -LiteralPath $TemporaryRoot -PathType Container)) { New-Item -Path $TemporaryRoot -ItemType Directory -Force -ErrorAction Stop | Out-Null } $sourceLease = New-DeployCenterDismSourceAccess -ImagePath ([string]$Edition.ImagePath) ` -TemporaryRoot $TemporaryRoot -LogAction $LogAction try { $deploymentImagePath = [string]$sourceLease.ImagePath $partitionSizingDisk = [pscustomobject]@{ Index = [int]$disk.Number; SizeBytes = [long]$disk.Size } $partitionPlan = @(New-DeployCenterPartitionPlan -DiskInfo $partitionSizingDisk -Profile $Profile -SizingState $SizingState) $driveLetters = Get-DeployCenterTemporaryDriveLetters -DiskNumber ([int]$disk.Number) -Profile $Profile $dismPath = Join-Path $env:WINDIR 'System32\dism.exe' if (-not (Test-Path -LiteralPath $dismPath -PathType Leaf)) { $dismPath = 'dism.exe' } $bcdbootPath = Join-Path $env:WINDIR 'System32\bcdboot.exe' if (-not (Test-Path -LiteralPath $bcdbootPath -PathType Leaf)) { $bcdbootPath = 'bcdboot.exe' } $scriptLines = New-Object System.Collections.Generic.List[string] $scriptLines.Add("select disk $($disk.Number)") $scriptLines.Add('clean') $scriptLines.Add('convert gpt') foreach ($item in $partitionPlan) { $definition = $item.Definition $role = [string]$definition.Role switch ($role) { 'System' { $scriptLines.Add("create partition efi size=$($item.SizeMB)") } 'Reserved' { $scriptLines.Add("create partition msr size=$($item.SizeMB)") } default { if ($null -eq $item.SizeMB -or [long]$item.SizeMB -ge ([long][math]::Floor(([double]$disk.Size / 1MB)) - 2)) { $scriptLines.Add('create partition primary') } else { $scriptLines.Add("create partition primary size=$($item.SizeMB)") } } } if ($role -eq 'Reserved') { continue } $label = [string]$definition.Name if ($label -notmatch '^[A-Za-z0-9_-]{1,32}$') { throw "Nom de partition invalide dans le profil : $label" } if ([string]$definition.FileSystem) { $scriptLines.Add("format fs=$($definition.FileSystem) quick label=$label") } if ($definition.GptTypeId) { $scriptLines.Add("set id=$($definition.GptTypeId)") } if ($definition.GptAttributes) { $scriptLines.Add("gpt attributes=$($definition.GptAttributes)") } if ($driveLetters.ContainsKey($role)) { $scriptLines.Add("assign letter=$($driveLetters[$role])") } } $diskpartPath = Join-Path $TemporaryRoot ("partition_{0}.txt" -f ([guid]::NewGuid().ToString('N'))) $unattendPath = Join-Path $TemporaryRoot ("unattend_{0}.xml" -f ([guid]::NewGuid().ToString('N'))) $partitionCreated = $false $lettersReleased = $false try { [System.IO.File]::WriteAllLines($diskpartPath, $scriptLines.ToArray(), [System.Text.Encoding]::ASCII) if ($ProgressAction) { & $ProgressAction 'Effacement et partitionnement du disque…' } Write-DeployCenterDeploymentLog -LogAction $LogAction -Message ("Partitionnement du disque {0} avec le profil '{1}' ; partition WinRE configurée : {2} Mo." -f $disk.Number, $Profile.Name, $winrePartitionSizeMB) $partitionCreated = $true Invoke-DeployCenterNativeCommand -FilePath 'diskpart.exe' -Arguments @('/s', $diskpartPath) ` -TemporaryRoot $TemporaryRoot -LogAction $LogAction -ProgressAction $ProgressAction $createdPartitions = @(Get-Partition -DiskNumber ([int]$disk.Number) -ErrorAction Stop) if ($createdPartitions.Count -ne $partitionPlan.Count) { throw "DiskPart a créé $($createdPartitions.Count) partition(s), alors que le profil en prévoit $($partitionPlan.Count). L’installation est interrompue." } $orderedCreatedPartitions = @($createdPartitions | Sort-Object -Property PartitionNumber) $partitionsByLetter = @{} for ($index = 0; $index -lt $partitionPlan.Count; $index++) { $role = [string]$partitionPlan[$index].Definition.Role if ($role -eq 'Reserved') { continue } $letter = [string]$driveLetters[$role] $driveRoot = '{0}:\' -f $letter if (-not (Test-Path -LiteralPath $driveRoot -PathType Container)) { throw "La lettre temporaire ${letter}: n’est pas accessible après le partitionnement. L’installation est interrompue." } # WinPE peut ne pas exposer les partitions EFI/Recovery dans les # requêtes Get-Partition par lettre. Leur ordre sur le disque suit # directement celui des définitions exécutées par DiskPart. $partitionsByLetter[$letter] = $orderedCreatedPartitions[$index] } $windowsLetter = [string]$driveLetters['Windows'] $windowsPath = "${windowsLetter}:\" $windowsRoot = Join-Path $windowsPath 'Windows' $recoveryLetter = [string]$driveLetters['Recovery'] $recoveryPath = "${recoveryLetter}:\Recovery\WindowsRE" $efiLetter = [string]$driveLetters['System'] if (-not $windowsLetter -or -not $recoveryLetter -or -not $efiLetter) { throw "Le profil doit définir les rôles System, Windows et Recovery pour permettre l’installation UEFI avec WinRE." } if ($ProgressAction) { & $ProgressAction "Application de Windows sur ${windowsLetter}:…" } Invoke-DeployCenterNativeCommand -FilePath $dismPath -Arguments @( '/Apply-Image', "/ImageFile:$deploymentImagePath", "/Index:$([int]$Edition.Index)", "/ApplyDir:$windowsPath", '/CheckIntegrity' ) -TemporaryRoot $TemporaryRoot -LogAction $LogAction -ProgressAction $ProgressAction if ($ProgressAction) { & $ProgressAction 'Vérification de la capacité de la partition WinRE…' } $sourceWinre = Join-Path $windowsRoot 'System32\Recovery\winre.wim' if (-not (Test-Path -LiteralPath $sourceWinre -PathType Leaf)) { throw "winre.wim est absent de l’image Windows appliquée ; impossible de configurer WinRE." } $winreFile = Get-Item -LiteralPath $sourceWinre -ErrorAction Stop $recoveryPartition = $partitionsByLetter[$recoveryLetter] $recoveryDriveInfo = New-Object System.IO.DriveInfo -ArgumentList "${recoveryLetter}:\" if (-not $recoveryDriveInfo.IsReady) { throw "Le volume de récupération ${recoveryLetter}: n’est pas prêt pour vérifier l’espace WinRE." } $minimumPartitionBytes = [long](990 * 1MB) $minimumFreeAfterCopyBytes = [long](250 * 1MB) $recoveryFreeBytes = [long]$recoveryDriveInfo.AvailableFreeSpace $expectedFreeBytes = $recoveryFreeBytes - [long]$winreFile.Length $winreSizeForLog = [math]::Ceiling(([double]$winreFile.Length / 1MB)) $availableFreeMB = [math]::Floor(([double]$recoveryFreeBytes / 1MB)) $availableAfterCopyMB = [math]::Floor(([double]$expectedFreeBytes / 1MB)) if ($recoveryFreeBytes -lt [long]$winreFile.Length) { $message = "Espace insuffisant sur la partition WinRE pour copier winre.wim : fichier de $winreSizeForLog Mo, espace disponible $availableFreeMB Mo. Le déploiement ne peut pas configurer WinRE." Write-DeployCenterDeploymentLog -LogAction $LogAction -Message $message -Level 'ERROR' throw $message } $recommendationWarnings = New-Object System.Collections.Generic.List[string] if ([long]$recoveryPartition.Size -lt $minimumPartitionBytes) { $actualPartitionMB = [math]::Floor(([double]$recoveryPartition.Size / 1MB)) [void]$recommendationWarnings.Add("taille de partition : $actualPartitionMB Mo, minimum recommandé : 990 Mo") } if ($expectedFreeBytes -lt $minimumFreeAfterCopyBytes) { [void]$recommendationWarnings.Add("espace libre après copie : $availableAfterCopyMB Mo, minimum recommandé : 250 Mo") } if ($recommendationWarnings.Count -gt 0) { $warningDetails = $recommendationWarnings -join '; ' $message = "Avertissement WinRE : les recommandations Microsoft ne sont pas toutes respectées ($warningDetails). La copie de winre.wim reste possible ; le déploiement continue." Write-DeployCenterDeploymentLog -LogAction $LogAction -Message $message -Level 'WARNING' } else { Write-DeployCenterDeploymentLog -LogAction $LogAction -Message ("Vérification WinRE réussie : partition {0} Mo, winre.wim {1} Mo, espace restant estimé après copie {2} Mo." -f $winrePartitionSizeMB, $winreSizeForLog, $availableAfterCopyMB) -Level 'SUCCESS' } if ($ProgressAction) { & $ProgressAction "Configuration du nom d’hôte…" } $editionArchitecture = if ($Edition.Edition -and $Edition.Edition.Architecture) { [string]$Edition.Edition.Architecture } else { [string]$Edition.Architecture } $unattendXml = ConvertTo-DeployCenterUnattendXml -HostName $HostName -Architecture $editionArchitecture [System.IO.File]::WriteAllText($unattendPath, $unattendXml, (New-Object System.Text.UTF8Encoding($false))) Invoke-DeployCenterNativeCommand -FilePath $dismPath -Arguments @( "/Image:$windowsPath", "/Apply-Unattend:$unattendPath" ) -TemporaryRoot $TemporaryRoot -LogAction $LogAction -ProgressAction $ProgressAction if ($ProgressAction) { & $ProgressAction 'Configuration du démarrage UEFI…' } Invoke-DeployCenterNativeCommand -FilePath $bcdbootPath -Arguments @( ("{0}:\Windows" -f $windowsLetter), '/s', ("{0}:" -f $efiLetter), '/f', 'UEFI' ) -TemporaryRoot $TemporaryRoot -LogAction $LogAction -ProgressAction $ProgressAction if ($ProgressAction) { & $ProgressAction 'Configuration de la partition de récupération WinRE…' } New-Item -Path $recoveryPath -ItemType Directory -Force -ErrorAction Stop | Out-Null Copy-Item -LiteralPath $sourceWinre -Destination (Join-Path $recoveryPath 'winre.wim') -Force -ErrorAction Stop $reagentcPath = Join-Path $windowsRoot 'System32\reagentc.exe' if (-not (Test-Path -LiteralPath $reagentcPath -PathType Leaf)) { $reagentcPath = Join-Path $env:WINDIR 'System32\reagentc.exe' } if (-not (Test-Path -LiteralPath $reagentcPath -PathType Leaf)) { throw 'REAgentC.exe est introuvable dans Windows appliqué et dans WinPE.' } Invoke-DeployCenterNativeCommand -FilePath $reagentcPath -Arguments @( '/SetReImage', '/Path', $recoveryPath, '/Target', $windowsRoot ) -TemporaryRoot $TemporaryRoot -LogAction $LogAction -ProgressAction $ProgressAction Invoke-DeployCenterNativeCommand -FilePath $reagentcPath -Arguments @( '/Enable', '/Target', $windowsRoot ) -TemporaryRoot $TemporaryRoot -LogAction $LogAction -ProgressAction $ProgressAction if ($ProgressAction) { & $ProgressAction 'Finalisation des partitions…' } $removeLines = New-Object System.Collections.Generic.List[string] foreach ($letter in @($driveLetters.Values | Select-Object -Unique)) { $removeLines.Add("select volume $letter") $removeLines.Add("remove letter=$letter") } $removePath = Join-Path $TemporaryRoot ("cleanup_{0}.txt" -f ([guid]::NewGuid().ToString('N'))) try { [System.IO.File]::WriteAllLines($removePath, $removeLines.ToArray(), [System.Text.Encoding]::ASCII) Invoke-DeployCenterNativeCommand -FilePath 'diskpart.exe' -Arguments @('/s', $removePath) ` -TemporaryRoot $TemporaryRoot -LogAction $LogAction -ProgressAction $ProgressAction $remainingLetters = @( Get-Partition -DiskNumber ([int]$disk.Number) -ErrorAction Stop | Where-Object { $_.DriveLetter -and ([string]$_.DriveLetter).ToUpperInvariant() -in @($driveLetters.Values | ForEach-Object { ([string]$_).ToUpperInvariant() }) } ) if ($remainingLetters.Count -gt 0) { $remaining = @($remainingLetters | ForEach-Object { ([string]$_.DriveLetter).ToUpperInvariant() } | Select-Object -Unique) -join ', ' throw "DiskPart n’a pas libéré toutes les lettres temporaires du disque : $remaining." } $lettersReleased = $true } finally { if (Test-Path -LiteralPath $removePath -PathType Leaf) { Remove-Item -LiteralPath $removePath -Force -ErrorAction SilentlyContinue } } Write-DeployCenterDeploymentLog -LogAction $LogAction -Message ("Déploiement terminé : Windows {0}, disque {1}, hôte {2}." -f $Edition.Name, $disk.Number, $HostName) -Level 'SUCCESS' } finally { if ($partitionCreated -and -not $lettersReleased) { # En cas d’échec après création, libérer les lettres temporaires même si # l’image n’a pas pu être entièrement déployée. foreach ($letter in @($driveLetters.Values | Select-Object -Unique)) { try { $cleanup = Join-Path $TemporaryRoot ("release_{0}.txt" -f ([guid]::NewGuid().ToString('N'))) [System.IO.File]::WriteAllLines($cleanup, @("select volume $letter", "remove letter=$letter"), [System.Text.Encoding]::ASCII) Invoke-DeployCenterNativeCommand -FilePath 'diskpart.exe' -Arguments @('/s', $cleanup) ` -TemporaryRoot $TemporaryRoot -LogAction $LogAction -ProgressAction $ProgressAction Remove-Item -LiteralPath $cleanup -Force -ErrorAction SilentlyContinue } catch { } } } foreach ($temporaryFile in @($diskpartPath, $unattendPath)) { if (Test-Path -LiteralPath $temporaryFile -PathType Leaf) { Remove-Item -LiteralPath $temporaryFile -Force -ErrorAction SilentlyContinue } } } } finally { Remove-DeployCenterDismSourceAccess -Lease $sourceLease -TemporaryRoot $TemporaryRoot -LogAction $LogAction } } Export-ModuleMember -Function Invoke-DeployCenterWindowsDeployment