Ajout du support de unattend

Ajout du script cmd à utiliser pour lancer DeployCenter
This commit is contained in:
benoit committed 2026-10-17 20:41:41 +02:00
1 parent 2019c565f5
commit e067721763
12 files changed
+663 -166

No files matched your search

+219 -125
View File
@@ -1,11 +1,51 @@
# DeployCenter - Partitionnement et installation d'une edition Windows
# Compatible Windows PowerShell 5.1 / WinPE.
Import-Module (Join-Path $PSScriptRoot 'Unattend.psm1') -Force
function Write-DeployCenterDeploymentLog {
param([AllowNull()][scriptblock]$LogAction, [string]$Message, [string]$Level = 'INFO')
if ($LogAction) { & $LogAction $Message $Level }
}
function Update-DeployCenterHostStorageCache {
param([AllowNull()][scriptblock]$LogAction = $null)
try {
Update-HostStorageCache -ErrorAction Stop
Write-DeployCenterDeploymentLog -LogAction $LogAction `
-Message 'Cache de stockage WinPE actualisé.' -Level 'INFO'
return $true
} catch {
Write-DeployCenterDeploymentLog -LogAction $LogAction `
-Message ("Actualisation du cache de stockage WinPE indisponible : {0}" -f $_.Exception.Message) `
-Level 'WARNING'
return $false
}
}
function Restart-DeployCenterWinPE {
[CmdletBinding()]
param([AllowNull()][scriptblock]$LogAction = $null)
$wpeutilPath = Join-Path $env:WINDIR 'System32\wpeutil.exe'
if (-not (Test-Path -LiteralPath $wpeutilPath -PathType Leaf)) {
throw 'wpeutil.exe est introuvable ; le redémarrage WinPE ne peut pas être demandé.'
}
Write-DeployCenterDeploymentLog -LogAction $LogAction -Message 'Redémarrage de WinPE demandé via wpeutil.' -Level 'INFO'
$output = @(& $wpeutilPath Reboot 2>&1)
$exitCode = $LASTEXITCODE
foreach ($line in $output) {
if (-not [string]::IsNullOrWhiteSpace([string]$line)) {
Write-DeployCenterDeploymentLog -LogAction $LogAction -Message ([string]$line) -Level 'INFO'
}
}
if ($null -ne $exitCode -and $exitCode -ne 0) {
throw "wpeutil Reboot a renvoyé le code $exitCode."
}
}
function ConvertTo-DeployCenterNativeArgument {
param([AllowNull()][string]$Value)
@@ -109,6 +149,7 @@ function Remove-DeployCenterDismSourceAccess {
} catch {
$releaseError = $_.Exception
}
[void](Update-DeployCenterHostStorageCache -LogAction $LogAction)
if (Test-Path -LiteralPath ([string]$Lease.ImagePath) -PathType Leaf) {
$details = if ($releaseError) { $releaseError.Message } else { 'Le fichier source reste accessible.' }
Write-DeployCenterDeploymentLog -LogAction $LogAction `
@@ -159,6 +200,7 @@ function Invoke-DeployCenterNativeCommand {
$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'
$isImageApply = $isDism -and @($Arguments | Where-Object { [string]$_ -ieq '/Apply-Image' }).Count -gt 0
if ($isDism) {
$Arguments += "/LogPath:$dismLogPath"
}
@@ -168,69 +210,54 @@ function Invoke-DeployCenterNativeCommand {
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 ($isImageApply) {
$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 (Test-Path -LiteralPath $stdoutPath -PathType Leaf) {
$nativePercent = Get-DeployCenterNativeProgressPercent -OutputPath $stdoutPath
if ($null -ne $nativePercent) { & $ProgressAction $null $nativePercent }
}
}
}
}
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
$process.WaitForExit()
$process.Refresh()
$exitCode = $process.ExitCode
$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 ($bootFilesCreated -or $bcdStoreExists) {
if ($ProgressAction -and (Test-Path -LiteralPath $stdoutPath -PathType Leaf)) {
$nativePercent = Get-DeployCenterNativeProgressPercent -OutputPath $stdoutPath
if ($null -ne $nativePercent) { & $ProgressAction $null $nativePercent }
}
if ($null -eq $exitCode -and @($nativeOutputLines | Where-Object { $_ -match '(?i)completed successfully|a r.ussi' }).Count -gt 0) {
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." `
-Message 'DISM confirme que Windows a été appliqué, mais le code de sortie est absent dans WinPE.' `
-Level 'WARNING'
return
}
} else {
$commandOutput = @(& $FilePath @Arguments 2>&1)
$exitCode = $LASTEXITCODE
foreach ($line in $commandOutput) {
if (-not [string]::IsNullOrWhiteSpace([string]$line)) {
[void]$nativeOutputLines.Add([string]$line)
Write-DeployCenterDeploymentLog -LogAction $LogAction -Message ([string]$line) -Level 'INFO'
}
}
}
if ($null -eq $exitCode -or [int]$exitCode -ne 0) {
$diagnostic = @()
@@ -255,6 +282,61 @@ function Invoke-DeployCenterNativeCommand {
}
}
function Get-DeployCenterOfflineWindowsBootGuid {
param(
[Parameter(Mandatory=$true)][string]$BcdStorePath,
[Parameter(Mandatory=$true)][string]$WindowsDriveLetter,
[AllowNull()][scriptblock]$LogAction = $null
)
if (-not (Test-Path -LiteralPath $BcdStorePath -PathType Leaf)) {
throw "Magasin BCD introuvable après la création des fichiers de démarrage : $BcdStorePath"
}
$bcdeditPath = Join-Path $env:WINDIR 'System32\bcdedit.exe'
if (-not (Test-Path -LiteralPath $bcdeditPath -PathType Leaf)) { $bcdeditPath = 'bcdedit.exe' }
$output = @(& $bcdeditPath /store $BcdStorePath /enum osloader /v 2>&1)
$exitCode = $LASTEXITCODE
foreach ($line in $output) {
if (-not [string]::IsNullOrWhiteSpace([string]$line)) {
Write-DeployCenterDeploymentLog -LogAction $LogAction -Message ([string]$line) -Level 'INFO'
}
}
if ($null -eq $exitCode -or [int]$exitCode -ne 0) {
throw "bcdedit n’a pas pu lire le magasin BCD $BcdStorePath (code $exitCode)."
}
$targetLetter = $WindowsDriveLetter.TrimEnd(':').ToUpperInvariant()
$entries = New-Object System.Collections.Generic.List[object]
$currentEntry = $null
foreach ($lineObject in $output) {
$line = [string]$lineObject
if ($line -match '^\s*(?:identifier|identificateur)\s+(?<guid>\{[0-9A-Fa-f-]{36}\})\s*$') {
if ($null -ne $currentEntry) { $entries.Add($currentEntry) }
$currentEntry = [pscustomobject]@{
Guid = [string]$Matches['guid']
Device = $null
OsDevice = $null
}
continue
}
if ($null -eq $currentEntry) { continue }
if ($line -match '^\s*device\s+partition=(?<letter>[A-Za-z]):\\?\s*$') {
$currentEntry.Device = $Matches['letter'].ToUpperInvariant()
} elseif ($line -match '^\s*osdevice\s+partition=(?<letter>[A-Za-z]):\\?\s*$') {
$currentEntry.OsDevice = $Matches['letter'].ToUpperInvariant()
}
}
if ($null -ne $currentEntry) { $entries.Add($currentEntry) }
$matchingEntries = @($entries | Where-Object {
$_.Device -eq $targetLetter -and $_.OsDevice -eq $targetLetter
})
if ($matchingEntries.Count -ne 1) {
throw "Impossible d’identifier une entrée BCD unique pour Windows sur ${targetLetter}: (entrées correspondantes : $($matchingEntries.Count))."
}
return [string]$matchingEntries[0].Guid
}
function Get-DeployCenterTemporaryDriveLetters {
param([Parameter(Mandatory=$true)][int]$DiskNumber, [Parameter(Mandatory=$true)]$Profile)
@@ -270,7 +352,6 @@ function Get-DeployCenterTemporaryDriveLetters {
if ($targetLetters -contains $letter) { continue }
$occupied[$letter] = $true
}
$lettersByRole = @{}
foreach ($partition in $Profile.Partitions) {
if ([string]$partition.Role -eq 'Reserved') { continue }
@@ -343,25 +424,6 @@ function New-DeployCenterPartitionPlan {
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 @"
<?xml version="1.0" encoding="utf-8"?>
<unattend xmlns="urn:schemas-microsoft-com:unattend">
<settings pass="specialize">
<component name="Microsoft-Windows-Shell-Setup" processorArchitecture="$processorArchitecture" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
<ComputerName>$HostName</ComputerName>
</component>
</settings>
</unattend>
"@
}
function Invoke-DeployCenterWindowsDeployment {
[CmdletBinding()]
param(
@@ -369,15 +431,18 @@ function Invoke-DeployCenterWindowsDeployment {
[Parameter(Mandatory=$true)][System.Collections.IDictionary]$Profile,
[AllowNull()]$SizingState,
[Parameter(Mandatory=$true)]$Edition,
[Parameter(Mandatory=$true)][string]$HostName,
[AllowNull()][string]$HostName = $null,
[AllowNull()][System.Collections.IDictionary]$UnattendProfile = $null,
[AllowNull()][System.Collections.IDictionary]$UnattendConfiguration = $null,
[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 ($null -ne $UnattendProfile) {
if ($null -eq $UnattendConfiguration) { throw 'La configuration DeployCenter.ini est absente pour le profil Unattend sélectionné.' }
Test-DeployCenterUnattendConfiguration -Profile $UnattendProfile -Configuration $UnattendConfiguration -HostName ([string]$HostName)
}
if ([string]$Firmware -ne [string]$Profile.Firmware) {
throw ("Le profil {0} exige le firmware {1}, détecté : {2}." -f $Profile.Name, $Profile.Firmware, $Firmware)
@@ -422,7 +487,6 @@ function Invoke-DeployCenterWindowsDeployment {
$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
@@ -444,27 +508,51 @@ function Invoke-DeployCenterWindowsDeployment {
if ([string]$definition.FileSystem) {
$scriptLines.Add("format fs=$($definition.FileSystem) quick label=$label")
}
if ($role -eq 'Recovery' -and $driveLetters.ContainsKey($role)) {
$scriptLines.Add("assign letter=$($driveLetters[$role])")
}
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])") }
if ($driveLetters.ContainsKey($role) -and $role -ne 'Recovery') {
$scriptLines.Add("assign letter=$($driveLetters[$role])")
}
}
$cleanLines = @("select disk $($disk.Number)", 'clean')
$cleanPath = Join-Path $TemporaryRoot ("clean_{0}.txt" -f ([guid]::NewGuid().ToString('N')))
$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)
[System.IO.File]::WriteAllLines($cleanPath, $cleanLines, [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', $cleanPath) `
-TemporaryRoot $TemporaryRoot -LogAction $LogAction -ProgressAction $ProgressAction
[void](Update-DeployCenterHostStorageCache -LogAction $LogAction)
[System.IO.File]::WriteAllLines($diskpartPath, $scriptLines.ToArray(), [System.Text.Encoding]::ASCII)
Invoke-DeployCenterNativeCommand -FilePath 'diskpart.exe' -Arguments @('/s', $diskpartPath) `
-TemporaryRoot $TemporaryRoot -LogAction $LogAction -ProgressAction $ProgressAction
[void](Update-DeployCenterHostStorageCache -LogAction $LogAction)
$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)
$recoveryPlanIndex = -1
for ($index = 0; $index -lt $partitionPlan.Count; $index++) {
if ([string]$partitionPlan[$index].Definition.Role -eq 'Recovery') {
$recoveryPlanIndex = $index
break
}
}
if ($recoveryPlanIndex -lt 0) { throw 'La partition Recovery est absente du plan exécuté.' }
$recoveryPartition = $orderedCreatedPartitions[$recoveryPlanIndex]
$recoveryLetter = [string]$driveLetters['Recovery']
$recoveryDriveRoot = '{0}:\' -f $recoveryLetter
if (-not (Test-Path -LiteralPath $recoveryDriveRoot -PathType Container)) {
throw "La lettre temporaire ${recoveryLetter}: de WinRE n’est pas accessible après le partitionnement."
}
$partitionsByLetter = @{}
for ($index = 0; $index -lt $partitionPlan.Count; $index++) {
$role = [string]$partitionPlan[$index].Definition.Role
@@ -483,7 +571,6 @@ function Invoke-DeployCenterWindowsDeployment {
$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) {
@@ -534,13 +621,18 @@ function Invoke-DeployCenterWindowsDeployment {
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 ($null -ne $UnattendProfile) {
if ($ProgressAction) { & $ProgressAction 'Préparation du fichier de réponses Windows…' }
$editionArchitecture = if ($Edition.Edition -and $Edition.Edition.Architecture) { [string]$Edition.Edition.Architecture } else { [string]$Edition.Architecture }
$unattendXml = New-DeployCenterUnattendXml -Profile $UnattendProfile `
-Configuration $UnattendConfiguration -HostName ([string]$HostName) -Architecture $editionArchitecture
[System.IO.File]::WriteAllText($unattendPath, $unattendXml, (New-Object System.Text.UTF8Encoding($false)))
$pantherUnattendDirectory = Join-Path $windowsRoot 'Panther\Unattend'
New-Item -Path $pantherUnattendDirectory -ItemType Directory -Force -ErrorAction Stop | Out-Null
Copy-Item -LiteralPath $unattendPath -Destination (Join-Path $pantherUnattendDirectory 'Unattend.xml') -Force -ErrorAction Stop
Write-DeployCenterDeploymentLog -LogAction $LogAction `
-Message ("Fichier de réponses Windows préparé avec le profil '{0}' (nom d’hôte : {1})." -f $UnattendProfile.Name, $HostName) -Level 'INFO'
}
if ($ProgressAction) { & $ProgressAction 'Configuration du démarrage UEFI…' }
Invoke-DeployCenterNativeCommand -FilePath $bcdbootPath -Arguments @(
@@ -557,52 +649,54 @@ function Invoke-DeployCenterWindowsDeployment {
if (-not (Test-Path -LiteralPath $reagentcPath -PathType Leaf)) {
throw 'REAgentC.exe est introuvable dans Windows appliqué et dans WinPE.'
}
$bcdStorePath = "${efiLetter}:\EFI\Microsoft\Boot\BCD"
$windowsBootGuid = Get-DeployCenterOfflineWindowsBootGuid -BcdStorePath $bcdStorePath `
-WindowsDriveLetter $windowsLetter -LogAction $LogAction
Invoke-DeployCenterNativeCommand -FilePath $reagentcPath -Arguments @(
'/SetReImage', '/Path', $recoveryPath, '/Target', $windowsRoot
) -TemporaryRoot $TemporaryRoot -LogAction $LogAction -ProgressAction $ProgressAction
Invoke-DeployCenterNativeCommand -FilePath $reagentcPath -Arguments @(
'/Enable', '/Target', $windowsRoot
'/Enable', '/OSGUID', $windowsBootGuid
) -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")
$lettersToRemove = @()
foreach ($roleToRemove in @('System', 'Recovery')) {
$letter = [string]$driveLetters[$roleToRemove]
if (-not [string]::IsNullOrWhiteSpace($letter)) { $lettersToRemove += $letter }
}
$lettersToRemove = @($lettersToRemove | Select-Object -Unique)
foreach ($letter in $lettersToRemove) {
$partitionToRemove = $partitionsByLetter[$letter]
$removeLines.Add("select disk $($disk.Number)")
$removeLines.Add("select partition $([int]$partitionToRemove.PartitionNumber)")
$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."
if ($removeLines.Count -gt 0) {
$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
[void](Update-DeployCenterHostStorageCache -LogAction $LogAction)
# Get-Partition peut conserver la lettre Recovery dans ses propriétés
# après que DiskPart l'a effectivement retirée sous WinPE.
$remainingPaths = @($lettersToRemove | Where-Object {
[System.IO.Directory]::Exists(('{0}:\' -f $_))
})
if ($remainingPaths.Count -gt 0) {
$remaining = (@($remainingPaths | ForEach-Object { ([string]$_).ToUpperInvariant() } | Select-Object -Unique)) -join ', '
throw "DiskPart n’a pas libéré toutes les lettres temporaires EFI/WinRE : $remaining."
}
} finally {
if (Test-Path -LiteralPath $removePath -PathType Leaf) { Remove-Item -LiteralPath $removePath -Force -ErrorAction SilentlyContinue }
}
$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'
$completionMode = if ($null -ne $UnattendProfile) { "profil Unattend '$($UnattendProfile.Name)'" } else { 'Avec OOBE' }
Write-DeployCenterDeploymentLog -LogAction $LogAction -Message ("Déploiement terminé : Windows {0}, disque {1}, mode {2}." -f $Edition.Name, $disk.Number, $completionMode) -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)) {
foreach ($temporaryFile in @($cleanPath, $diskpartPath, $unattendPath)) {
if (Test-Path -LiteralPath $temporaryFile -PathType Leaf) { Remove-Item -LiteralPath $temporaryFile -Force -ErrorAction SilentlyContinue }
}
}
@@ -611,4 +705,4 @@ function Invoke-DeployCenterWindowsDeployment {
}
}
Export-ModuleMember -Function Invoke-DeployCenterWindowsDeployment
Export-ModuleMember -Function Invoke-DeployCenterWindowsDeployment, Restart-DeployCenterWinPE