Renommage du profil unattend pour mieux correspondre à ce qu'il fait. Ajout d'un warning si WinRE n'est pas assez grands au regard des préconisations Microsoft et du redémarrage automatique si l'installation réussi.
752 lines
41 KiB
PowerShell
752 lines
41 KiB
PowerShell
# 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)
|
||
|
||
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
|
||
}
|
||
[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 `
|
||
-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.])(?<percent>\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'
|
||
$isImageApply = $isDism -and @($Arguments | Where-Object { [string]$_ -ieq '/Apply-Image' }).Count -gt 0
|
||
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)
|
||
$nativeOutputLines = New-Object System.Collections.Generic.List[string]
|
||
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 }
|
||
}
|
||
}
|
||
}
|
||
$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 ($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 '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 = @()
|
||
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-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)
|
||
|
||
$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 Invoke-DeployCenterWindowsDeployment {
|
||
[CmdletBinding()]
|
||
param(
|
||
[Parameter(Mandatory=$true)]$DiskInfo,
|
||
[Parameter(Mandatory=$true)][System.Collections.IDictionary]$Profile,
|
||
[AllowNull()]$SizingState,
|
||
[Parameter(Mandatory=$true)]$Edition,
|
||
[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 ($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)
|
||
}
|
||
$winreRecommendationWarnings = @()
|
||
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." }
|
||
|
||
$wallpaperSourcePath = $null
|
||
if ($null -ne $UnattendProfile -and $null -ne $UnattendConfiguration) {
|
||
$configuredWallpaperPath = [string]$UnattendConfiguration.UnattendWallpaperPath
|
||
if (-not [string]::IsNullOrWhiteSpace($configuredWallpaperPath)) {
|
||
$deployRoot = Split-Path -Path $PSScriptRoot -Parent
|
||
if ([System.IO.Path]::IsPathRooted($configuredWallpaperPath)) {
|
||
$wallpaperSourcePath = [System.IO.Path]::GetFullPath($configuredWallpaperPath)
|
||
} else {
|
||
$wallpaperSourcePath = [System.IO.Path]::GetFullPath((Join-Path $deployRoot $configuredWallpaperPath))
|
||
}
|
||
if ([System.IO.Path]::GetExtension($wallpaperSourcePath) -notin @('.png', '.jpg', '.bmp')) {
|
||
throw 'WallpaperPath doit désigner un fichier .png, .jpg ou .bmp.'
|
||
}
|
||
if (-not (Test-Path -LiteralPath $wallpaperSourcePath -PathType Leaf)) {
|
||
throw "Image de fond introuvable : $wallpaperSourcePath"
|
||
}
|
||
}
|
||
}
|
||
|
||
$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('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 ($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) -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')))
|
||
try {
|
||
[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)
|
||
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
|
||
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'
|
||
$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."
|
||
}
|
||
|
||
$wallpaperTargetPath = $null
|
||
$wallpaperDestination = $null
|
||
if ($wallpaperSourcePath) {
|
||
$wallpaperName = [System.IO.Path]::GetFileName($wallpaperSourcePath)
|
||
$wallpaperDirectory = Join-Path $windowsRoot 'Web\Wallpaper\DeployCenter'
|
||
$wallpaperDestination = Join-Path $wallpaperDirectory $wallpaperName
|
||
$wallpaperTargetPath = '%WINDIR%\Web\Wallpaper\DeployCenter\' + $wallpaperName
|
||
if ($wallpaperDestination.Length -gt 259) { throw "Le chemin configuré du fond d’écran dépasse 259 caractères dans Windows." }
|
||
}
|
||
|
||
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 ($wallpaperSourcePath) {
|
||
New-Item -Path $wallpaperDirectory -ItemType Directory -Force -ErrorAction Stop | Out-Null
|
||
Copy-Item -LiteralPath $wallpaperSourcePath -Destination $wallpaperDestination -Force -ErrorAction Stop
|
||
Write-DeployCenterDeploymentLog -LogAction $LogAction `
|
||
-Message ("Image de fond copiée dans Windows : {0}." -f $wallpaperDestination) -Level 'INFO'
|
||
}
|
||
|
||
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 '; '
|
||
$winreRecommendationWarnings = @($recommendationWarnings.ToArray())
|
||
$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 ($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 -WallpaperPath ([string]$wallpaperTargetPath)
|
||
[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 @(
|
||
("{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.'
|
||
}
|
||
$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', '/OSGUID', $windowsBootGuid
|
||
) -TemporaryRoot $TemporaryRoot -LogAction $LogAction -ProgressAction $ProgressAction
|
||
|
||
if ($ProgressAction) { & $ProgressAction 'Finalisation des partitions…' }
|
||
$removeLines = New-Object System.Collections.Generic.List[string]
|
||
$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")
|
||
}
|
||
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 }
|
||
}
|
||
}
|
||
$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 {
|
||
foreach ($temporaryFile in @($cleanPath, $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
|
||
}
|
||
return [pscustomobject]@{
|
||
WinRERecommendationWarnings = @($winreRecommendationWarnings)
|
||
WinRERecommendationsMet = ($winreRecommendationWarnings.Count -eq 0)
|
||
}
|
||
}
|
||
|
||
Export-ModuleMember -Function Invoke-DeployCenterWindowsDeployment, Restart-DeployCenterWinPE
|