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.
386 lines
19 KiB
PowerShell
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
|