Files
DeployCenter/Modules/WindowsSources.psm1
benoit 2212e25d31 Ajout de la langue anglaise
Ajout d'un README.md avec le descriptif et les instructions
L'ancien README.md s'appelle désormais CHANGELOG.md
Activation du pavé numérique par défaut pour les installations sans assistance
Nouveau design pour la fenêtre de déploiement.
Ajout de paramètre pour pouvoir procéder à une installation totalement automatisée
Ajout de paramètre pour copier au choix sources, drivers ou les deux sur C:\DeployVenter avant leur usage par DISM.
2026-10-18 04:14:17 +02:00

386 lines
19 KiB
PowerShell

# DeployCenter - Find and read Windows sources
# Compatible with Windows PowerShell 5.1
function Find-WindowsSources {
param(
[Parameter(Mandatory=$true)][string]$RootPath,
[AllowNull()][scriptblock]$LogAction = $null
)
$results = New-Object System.Collections.Generic.List[object]
$roots = @($RootPath -split ';' | ForEach-Object {
$candidate = $_.Trim().Trim('"')
if ($candidate -notmatch '^[A-Za-z]:\\$') { $candidate = $candidate.TrimEnd('\') }
$candidate
} | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique)
if ($roots.Count -eq 0) {
throw 'Specify at least one local path and/or SMB share, separated by semicolons.'
}
foreach ($root in $roots) {
if (-not (Test-Path -LiteralPath $root -PathType Container)) {
if ($LogAction) { & $LogAction "Source inaccessible, skipped: $root" 'WARNING' }
continue
}
if ($LogAction) { & $LogAction "Searching for Windows sources in: $root" 'INFO' }
# Read-only scan. Errors accessing subfolders are ignored.
# Show ISO files and standalone WIM/ESD files.
# Exception: hide boot.wim only when it is part of an
# extracted Windows medium whose Sources folder also contains install.wim/esd.
$allFiles = @(Get-ChildItem -LiteralPath $root -Recurse -File -ErrorAction SilentlyContinue)
$files = New-Object System.Collections.Generic.List[object]
foreach ($file in $allFiles) {
if ($file.Extension -ieq '.iso') {
[void]$files.Add($file)
continue
}
if ($file.Extension -ine '.wim' -and $file.Extension -ine '.esd') {
continue
}
$isBootWimInExtractedMedia = $false
if ($file.Name -ieq 'boot.wim' -and $file.Directory.Name -ieq 'Sources') {
$installWim = Join-Path $file.Directory.FullName 'install.wim'
$installEsd = Join-Path $file.Directory.FullName 'install.esd'
if ((Test-Path -LiteralPath $installWim -PathType Leaf) -or
(Test-Path -LiteralPath $installEsd -PathType Leaf)) {
$isBootWimInExtractedMedia = $true
}
}
if (-not $isBootWimInExtractedMedia) {
[void]$files.Add($file)
}
}
foreach ($file in $files) {
$kind = switch ($file.Extension.ToLowerInvariant()) {
'.iso' { 'Image ISO' }
'.wim' { 'Image WIM Windows' }
'.esd' { 'Image ESD Windows' }
}
$detail = if ($file.Name -ieq 'install.wim' -or $file.Name -ieq 'install.esd') {
"Installation image on extracted media"
} elseif ($file.Extension -ieq '.iso') {
"ISO installation media"
} else {
"Image WIM/ESD autonome"
}
$results.Add([pscustomobject]@{
Type = $kind; Name = $file.Name; Path = $file.FullName
Size = ('{0:N2} GB' -f ($file.Length / 1GB)); Detail = $detail
})
}
}
if (-not ($roots | Where-Object { Test-Path -LiteralPath $_ -PathType Container })) {
throw "None of the paths is accessible: $($roots -join '; ')`nCheck that the local drive and SMB share are available."
}
if ($LogAction) { & $LogAction ("Detection complete: {0} element(s) trouve(s)." -f $results.Count) 'SUCCESS' }
return @($results | Sort-Object Type, Name, Path -Unique)
}
function Get-WindowsImageEditions {
param(
[Parameter(Mandatory=$true)][string]$ImagePath,
[AllowNull()][scriptblock]$LogAction = $null
)
if (-not (Test-Path -LiteralPath $ImagePath -PathType Leaf)) {
throw "Image file not found: $ImagePath"
}
$normalized = @()
$getWindowsImage = Get-Command Get-WindowsImage -ErrorAction SilentlyContinue
if ($getWindowsImage) {
try {
$images = @(Get-WindowsImage -ImagePath $ImagePath -ErrorAction Stop)
foreach ($image in $images) {
$indexValue = $null
foreach ($propertyName in @('ImageIndex', 'Index')) {
if ($image.PSObject.Properties[$propertyName] -and $image.$propertyName) {
$indexValue = [int]$image.$propertyName
break
}
}
if ($null -eq $indexValue) { continue }
$nameValue = $null
foreach ($propertyName in @('ImageName', 'Name', 'ImageDescription')) {
if ($image.PSObject.Properties[$propertyName] -and $image.$propertyName) {
$nameValue = [string]$image.$propertyName
break
}
}
$archValue = $null
foreach ($propertyName in @('Architecture', 'ImageArchitecture')) {
if ($image.PSObject.Properties[$propertyName] -and $image.$propertyName) {
$archValue = $image.$propertyName
break
}
}
$versionValue = $null
foreach ($propertyName in @('Version', 'ImageVersion')) {
if ($image.PSObject.Properties[$propertyName] -and $image.$propertyName) {
$versionValue = [string]$image.$propertyName
break
}
}
$normalized += [pscustomobject]@{
Index = $indexValue
Name = $nameValue
Description = if ($image.PSObject.Properties['ImageDescription']) { $image.ImageDescription } else { $null }
Architecture = $archValue
Version = $versionValue
Edition = if ($image.PSObject.Properties['EditionId']) { $image.EditionId } else { $null }
}
}
} catch {
if ($LogAction) { & $LogAction "Get-WindowsImage unavailable for $ImagePath : $($_.Exception.Message)" 'WARNING' }
}
}
$dism = Join-Path $env:WINDIR 'System32\dism.exe'
if (-not (Test-Path -LiteralPath $dism)) { $dism = 'dism.exe' }
# If the PowerShell module did not return indexes/names, retrieve them with DISM.
if ($normalized.Count -eq 0) {
$output = & $dism /English /Get-WimInfo "/WimFile:$ImagePath" 2>&1
if ($LASTEXITCODE -ne 0) {
throw "DISM could not read the image: $ImagePath`n$($output -join "`n")"
}
$current = $null
foreach ($line in $output) {
$lineText = [string]$line
if ($lineText -match '^\s*Index\s*:\s*(\d+)\s*$') {
if ($null -ne $current) { $normalized += [pscustomobject]$current }
$current = @{ Index = [int]$Matches[1]; Name = ''; Description = ''; Architecture = $null; Version = $null; Edition = $null }
} elseif ($null -ne $current -and $lineText -match '^\s*Name\s*:\s*(.*?)\s*$') {
$current.Name = $Matches[1]
} elseif ($null -ne $current -and $lineText -match '^\s*Description\s*:\s*(.*?)\s*$') {
$current.Description = $Matches[1]
}
}
if ($null -ne $current) { $normalized += [pscustomobject]$current }
}
if ($normalized.Count -eq 0) {
throw "DISM returned no usable editions for: $ImagePath"
}
# Query DISM for every index even if Get-WindowsImage returned the names:
# Architecture/Version properties may be missing in some module versions.
foreach ($edition in $normalized) {
if ($edition.Architecture -and $edition.Version) { continue }
$detailOutput = & $dism /English /Get-WimInfo "/WimFile:$ImagePath" "/Index:$($edition.Index)" 2>&1
if ($LASTEXITCODE -ne 0) {
if ($LogAction) { & $LogAction "DISM could not read metadata for index $($edition.Index) : $($detailOutput -join ' ')" 'WARNING' }
continue
}
$insideImageDetails = $false
$imageVersion = $null
$servicePackBuild = $null
foreach ($detailLine in $detailOutput) {
$detailText = [string]$detailLine
if ($detailText -match '^\s*Details for image\s*:') {
$insideImageDetails = $true
continue
}
# Ignore the DISM version printed at the start of its output.
if (-not $insideImageDetails) { continue }
if (-not $edition.Architecture -and $detailText -match '^\s*Architecture\s*:\s*(.*?)\s*$') {
$edition.Architecture = $Matches[1]
} elseif ($detailText -match '^\s*Version\s*:\s*(.*?)\s*$') {
$imageVersion = $Matches[1]
} elseif ($detailText -match '^\s*ServicePack Build\s*:\s*(.*?)\s*$') {
$servicePackBuild = $Matches[1]
} elseif (-not $edition.Name -and $detailText -match '^\s*Name\s*:\s*(.*?)\s*$') {
$edition.Name = $Matches[1]
} elseif (-not $edition.Edition -and $detailText -match '^\s*Edition\s*:\s*(.*?)\s*$') {
$edition.Edition = $Matches[1]
}
}
if ($imageVersion) {
$edition.Version = if ($servicePackBuild -and $servicePackBuild -ne '0') {
"$imageVersion.$servicePackBuild"
} else {
$imageVersion
}
}
}
# Overall image version: show it only if the index metadata
# reports one consistent version. Do not repeat it on every row.
$versionValues = @($normalized | ForEach-Object {
if (-not [string]::IsNullOrWhiteSpace([string]$_.Version)) { [string]$_.Version }
} | Select-Object -Unique)
$globalVersion = $null
if ($versionValues.Count -eq 1) {
$globalVersion = $versionValues[0]
}
foreach ($edition in $normalized) {
if ($edition.Architecture -is [int] -or [string]$edition.Architecture -match '^\d+$') {
switch ([string]$edition.Architecture) {
'0' { $edition.Architecture = 'x86' }
'5' { $edition.Architecture = 'ARM' }
'9' { $edition.Architecture = 'x64' }
'12' { $edition.Architecture = 'ARM64' }
}
}
}
return [pscustomobject]@{
Editions = @($normalized)
GlobalVersion = $globalVersion
}
}
function Resolve-InstallImagePath {
param(
[Parameter(Mandatory=$true)][string]$SelectedPath,
[AllowNull()][object]$ExistingImage = $null
)
if ([IO.Path]::GetExtension($SelectedPath) -in @('.wim', '.esd')) {
return [pscustomobject]@{ SourcePath = $SelectedPath; ImagePath = $SelectedPath; MountedImage = $null }
}
if (Test-Path -LiteralPath $SelectedPath -PathType Container) {
foreach ($relative in @('sources\install.wim', 'sources\install.esd', 'Sources\install.wim', 'Sources\install.esd')) {
$candidate = Join-Path $SelectedPath $relative
if (Test-Path -LiteralPath $candidate -PathType Leaf) {
return [pscustomobject]@{ SourcePath = $SelectedPath; ImagePath = $candidate; MountedImage = $null }
}
}
throw "No Sources\install.wim or Sources\install.esd file in: $SelectedPath"
}
if ([IO.Path]::GetExtension($SelectedPath) -ieq '.iso') {
if ($ExistingImage -and $ExistingImage.SourcePath -and
[string]::Equals([IO.Path]::GetFullPath([string]$ExistingImage.SourcePath), [IO.Path]::GetFullPath($SelectedPath), [StringComparison]::OrdinalIgnoreCase) -and
$ExistingImage.MountedImage -and $ExistingImage.ImagePath) {
return $ExistingImage
}
if (-not (Get-Command Mount-DiskImage -ErrorAction SilentlyContinue)) {
throw "ISO mounting is unavailable in this environment (Mount-DiskImage command not found). Use an extracted folder or an install.wim/esd file to analyze this source."
}
# Do not let Windows assign an arbitrary drive letter (often D:) to the ISO.
$diskImage = Mount-DiskImage -ImagePath $SelectedPath -Access ReadOnly -NoDriveLetter -PassThru -ErrorAction Stop
$mountInfo = [pscustomobject]@{
SourcePath = $SelectedPath
ImagePath = $null
MountedImage = $diskImage
MountedPartition = $null
DriveLetter = $null
}
try {
# Start with the volumes explicitly associated with this image to avoid
# confusing the virtual CD-ROM drive with a physical disk in WinPE.
$volumes = @(Get-Volume -DiskImage $diskImage -ErrorAction Stop)
if ($volumes.Count -eq 0) { throw 'Windows did not expose any ISO volumes.' }
$usedLetters = @(
Get-Volume -ErrorAction SilentlyContinue | Where-Object { $_.DriveLetter } | ForEach-Object { ([string]$_.DriveLetter).ToUpperInvariant() }
Get-PSDrive -PSProvider FileSystem -ErrorAction SilentlyContinue | ForEach-Object { ([string]$_.Name).ToUpperInvariant() }
)
$availableLetter = $null
foreach ($candidateLetter in @('Z','Y','X','W','V','U','T','S','R','Q','P','O','N','M','L','K','J','I','H','G','F','E')) {
if ($candidateLetter -notin @('C','D') -and $candidateLetter -notin $usedLetters) {
$availableLetter = $candidateLetter
break
}
}
if (-not $availableLetter) { throw 'No temporary drive letter is available to mount the ISO.' }
$checkedPaths = @()
foreach ($volume in $volumes) {
$volumeRoot = [string]$volume.Path
if (-not $volumeRoot.StartsWith('\\?\Volume{', [StringComparison]::OrdinalIgnoreCase) -and $volume.DriveLetter) {
$mountVolCommand = Join-Path $env:WINDIR 'System32\mountvol.exe'
if (-not (Test-Path -LiteralPath $mountVolCommand -PathType Leaf)) { $mountVolCommand = 'mountvol.exe' }
$volumeMountPath = '{0}:\' -f [string]$volume.DriveLetter
$volumeNameOutput = & $mountVolCommand $volumeMountPath '/L' 2>&1
$volumeGuidPath = @($volumeNameOutput | ForEach-Object { [string]$_ } | Where-Object { $_.StartsWith('\\?\Volume{', [StringComparison]::OrdinalIgnoreCase) }) | Select-Object -First 1
if ($volumeGuidPath) { $volumeRoot = [string]$volumeGuidPath }
}
if ($volumeRoot -and $volumeRoot.StartsWith('\\?\Volume{', [StringComparison]::OrdinalIgnoreCase)) {
if (-not $volumeRoot.EndsWith('\')) { $volumeRoot += '\' }
foreach ($relative in @('sources\install.wim', 'sources\install.esd')) {
$candidate = $volumeRoot + $relative
$checkedPaths += $candidate
if (Test-Path -LiteralPath $candidate -PathType Leaf) {
if ([string]$volume.DriveLetter -eq 'D') {
$mountVolCommand = Join-Path $env:WINDIR 'System32\mountvol.exe'
if (-not (Test-Path -LiteralPath $mountVolCommand -PathType Leaf)) { $mountVolCommand = 'mountvol.exe' }
$removeOutput = & $mountVolCommand 'D:\' '/D' 2>&1
if ($LASTEXITCODE -ne 0) { throw "Could not release D: for the ISO volume: $($removeOutput -join ' ')" }
}
$mountInfo.ImagePath = $candidate
return $mountInfo
}
}
}
$partition = Get-Partition -Volume $volume -ErrorAction Stop
if ($null -eq $partition) { continue }
$existingDrivePaths = @($partition.AccessPaths | Where-Object { [string]$_ -match '^[A-Za-z]:\\$' })
$reusableDrivePath = $existingDrivePaths | Where-Object { [string]$_ -notmatch '^[CDcd]:\\$' } | Select-Object -First 1
if ($reusableDrivePath) {
$driveAccessPath = [string]$reusableDrivePath
$driveLetter = $driveAccessPath.Substring(0, 1).ToUpperInvariant()
} else {
foreach ($existingDrivePath in $existingDrivePaths) {
Remove-PartitionAccessPath -InputObject $partition -AccessPath ([string]$existingDrivePath) -ErrorAction Stop | Out-Null
}
$driveLetter = $availableLetter
$driveAccessPath = '{0}:\' -f $driveLetter
Add-PartitionAccessPath -InputObject $partition -AccessPath $driveAccessPath -ErrorAction Stop
}
$mountInfo.MountedPartition = $partition
$mountInfo.DriveLetter = $driveLetter
foreach ($relative in @('sources\install.wim', 'sources\install.esd')) {
$candidate = "${driveLetter}:\$relative"
$checkedPaths += $candidate
if (Test-Path -LiteralPath $candidate -PathType Leaf) {
$mountInfo.ImagePath = $candidate
return $mountInfo
}
}
if ($driveLetter -eq $availableLetter) {
Remove-PartitionAccessPath -InputObject $partition -AccessPath $driveAccessPath -ErrorAction SilentlyContinue | Out-Null
}
$mountInfo.MountedPartition = $null
$mountInfo.DriveLetter = $null
}
throw "No Sources\install.wim or Sources\install.esd file was found in the ISO. Checked paths: $($checkedPaths -join ', ')."
} catch {
try { [void](Dismount-DeployCenterInstallImage -Image $mountInfo -ErrorAction SilentlyContinue) } catch { }
throw
}
}
throw "Unsupported source type for analysis: $SelectedPath"
}
function Dismount-DeployCenterInstallImage {
param([AllowNull()][object]$Image)
if ($null -eq $Image -or $null -eq $Image.MountedImage) { return }
try {
if ($Image.MountedPartition -and $Image.DriveLetter) {
$driveAccessPath = '{0}:\' -f [string]$Image.DriveLetter
Remove-PartitionAccessPath -InputObject $Image.MountedPartition -AccessPath $driveAccessPath -ErrorAction Stop | Out-Null
}
} finally {
Dismount-DiskImage -InputObject $Image.MountedImage -ErrorAction Stop | Out-Null
}
}
Export-ModuleMember -Function Find-WindowsSources, Get-WindowsImageEditions, Resolve-InstallImagePath, Dismount-DeployCenterInstallImage