65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 Windows Powershell 构建 WinPE

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.75/5 (3投票s)

2014年4月27日

CPOL

6分钟阅读

viewsIcon

22064

使用 PowerShell 从 AIK/ADK 构建 WinPE

引言

我最初是在 2010 年末使用 Vista 的 AIK 编写的。此代码为 PXE、光盘和 USB 构建 WinPE,允许添加文件、驱动程序和(一个)startnet 命令。最近,我更新了代码,也使用了 Windows 8 的 ADK 8.1。(我曾认为在 PE 中拥有 .NET 和 PowerShell 会很好,但我还没有用过……我仍然使用 cmd 文件将文本文件馈送给 diskpart 并运行 imagex。)

我经常使用 WinPE 部署(和重新部署)Windows 映像。我也将其用于 XP 映像,但这需要一些额外的 bootsect.exe 命令,此处未涵盖,因为 XP 已不再受支持。通常,每个 WinPE 都针对特定品牌和型号的计算机。只要引入了需要新驱动程序的新型号,我就可以在几分钟内准备好 PE 映像。

背景

使用此脚本构建和自定义 WinPE 映像需要 Windows 自动安装工具包或 Windows 评估和部署工具包。

DISM 仅在 PE 版本大于 2.1 时才有效。

对于 ADK,需要 8.1 和 .Net 4.5。

Using the Code

*代码描述在代码之后。

注意:此脚本可用于格式化 USB 驱动器。作者对数据丢失不承担任何责任。它仅在存在一个 USB 设备的情况下进行了测试。(它将格式化任何选定的驱动器,因此请小心选择正确的驱动器。)

DISM 需要管理员权限,因此必须以管理员身份运行脚本。我使用注册表编辑,以便我可以右键单击脚本并选择“运行为”。这不是必需的。

Windows 注册表编辑器版本 5.00

[HKEY_CLASSES_ROOT\Microsoft.PowerShellScript.1\Shell\runas]

[HKEY_CLASSES_ROOT\Microsoft.PowerShellScript.1\Shell\runas\command]
@="\"c:\\windows\\system32\\windowspowershell\\v1.0\\powershell.exe\" -file \"%1\""

尽管代码(如发布)带有命令行参数,但我建议使用无参数选项,因为它将显示一个窗体。参数最初适用于 AIK 版本,但仍然有效,但仅适用于 ADK——因为 ADK 现在被设置为构建 WinPE 映像的默认设置。右键单击脚本,选择“以管理员身份运行”,然后加载一个窗体——这是我首选的方式。

参数

param(
    [string]$PeDirectory,
    [string]$PeEnvironment = "amd64",
    $PeDrivers,
    $PeExtras,
    [string]$PeOutPut = "NONE",
    $PeDiskId)

.Synopsis
为 ISO、可启动 USB 或 PXE 创建 WinPE。
.Parameter PeDirectory
将 WinPE 复制到的目录。省略此参数将显示 GUI。
.Parameter PeEnvironment
WinPE 环境:amd64 | x86 | i64。默认值为 amd64
.Parameter PeDrivers
要添加到 WinPE 的驱动程序的目录或目录。
.Parameter PeExtras
要添加到 System32 目录的其他文件——例如任何其他脚本文件。
.Parameter PeOutPut
最终 WinPE 的格式:ISO | USB | PXE。默认值为 NONE。
.Parameter PeDiskId
当 OutPut 为 USB 时,DiskId 用于 diskpart 准备。默认 USB 是 Disk 1。
.Description
创建 WinPE 映像。注意:需要管理员角色——Powershell 必须以管理员身份运行。

一些要求

$IsSTA = ([System.Threading.Thread]::CurrentThread.GetApartmentState() -eq 'STA')
$psVersionMajor = [int]($PSVersionTable.PSVersion).Major
if((Get-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Environment")."Processor_Architecture".Contains("64")) {
    $defDir = [String]::Format("{0}\\Windows Kits\8.1\Assessment and Deployment Kit", ${env:ProgramFiles(x86)})
}
else {
    $defDir = [String]::Format("{0}\\Windows Kits\8.1\Assessment and Deployment Kit", $env:ProgramFiles)
}
#$defDir = "the actual install directory" # required for running with parameters.

使用了 FolderBrowserDialog ;因此,必须知道当前线程的 Apartment。

要使用 ADK 的 Powershell 导入,需要版本 4 和 .NET 4.5。

必须定义 AIK/ADK 目录;默认行为是将其设置为预期位置。(我没有将两者都安装在默认位置。)此检查仅适用于窗体版本。另请注意,在使用窗体时可以更改此设置。

注意:如果安装目录不是默认的,则脚本需要在 Powershell 窗口中使用参数运行,并设置一个默认值。(我也将其设置为这样,这样每次运行时都不必浏览到安装目录。)

将 WinPE 复制到工作目录

function Copy-PE {
    param([Parameter(Position=0)][string]$ProcessorArchitecture, 
    [Parameter(Position=1)][string]$Destination, $installDirectory, [switch]$Aik)
    # Create Directories
    if(Test-Path "$Destination") {
        if((Get-Item "$Destination").GetDirectories().Length -gt 0) {
            Write-Warning "$Destination already exists."
            return
        }
    }
    else {
        Write-Verbose -Message "Creating $Destination"
        New-Item -Path "$Destination" -Type directory | Out-Null
    }

原始版本并未对现有 WinPE 映像执行更新。使用此代码可以轻松满足任何更新映像的需求。在此修订版中允许映像更新引入了一个新的危险:交叉映像操作,这效果不太好。我没有添加任何检查程序来防止这种情况发生。此外,在更新映像时,可以将多个命令添加到 startnet(这也是需要注意的……)。

    # DISM issue
    New-Item -Path "$Destination\scratch" -Type directory | Out-Null
    # well known SID: S-1-1-0 (everyone)
    $identity = New-Object System.Security.Principal.NTAccount("Everyone")
    $fsr = [System.Security.AccessControl.FileSystemRights]::FullControl
    $act = [System.Security.AccessControl.AccessControlType]::Allow
    $inherit = [System.Security.AccessControl.InheritanceFlags]::ContainerInherit -bxor [System.Security.AccessControl.InheritanceFlags]::ObjectInherit
    $prop = [System.Security.AccessControl.PropagationFlags]::None
    $acr = New-Object System.Security.AccessControl.FileSystemAccessRule($identity, $fsr, $inherit, $prop, $act)
    $acl = Get-Acl "$Destination\scratch"
    $acl.AddAccessRule($acr)
    (Get-Item "$Destination\scratch").SetAccessControl($acl)

我最初在使用 ADK 时遇到了问题,并找到了对此类修复的多种建议。唉,它不起作用。它之所以在此处,只是为了以防万一其他人遇到类似问题(在这种情况下,ADK 命令将必须包含 scratch 参数)。如后面所见,我的问题是 DISM 环境变量——我选择在执行期间更改到 DISM 目录。此代码可以省略。

    # Default ADK
    $bootFiles = "$installDirectory\Windows Preinstallation Environment\$ProcessorArchitecture\Media"
    $isoFiles = "$installDirectory\Deployment Tools\$ProcessorArchitecture\Oscdimg"
    $winPeFile = "$installDirectory\Windows Preinstallation Environment\$ProcessorArchitecture\en-us"
    
    if($Aik) {
        $bootFiles = "$installDirectory\Tools\PETools\$ProcessorArchitecture"
        $isoFiles = "$installDirectory\Tools\PETools\$ProcessorArchitecture\boot"
        $winPeFile = "$installDirectory\Tools\PETools\$ProcessorArchitecture"
    }

仅设置所需目录。

    Write-Verbose -Message "Copying efisys.bin"
    Copy-Item -Path "$isoFiles\efisys.bin" -Destination "$Destination"
    Write-Verbose -Message "Copying efisys_noprompt.bin"
    Copy-Item -Path "$isoFiles\efisys_noprompt.bin" -Destination "$Destination"
    Write-Verbose -Message "Copying etfsboot.com"
    Copy-Item -Path "$isoFiles\etfsboot.com" -Destination "$Destination"
        
    Write-Verbose -Message "Copying winpe.wim"
    Copy-Item -Path "$winPeFile\winpe.wim" -Destination "$Destination"

复制 winpe.wim 和其他 ISO 支持文件。

    # Mount
    Write-Verbose -Message "Creating $Destination\mount"
    New-Item -Path "$Destination\mount" -Type directory | Out-Null
    
    # ISO
    Write-Verbose -Message "Creating $Destination\ISO"
    New-Item -Path "$Destination\ISO" -Type directory | Out-Null
    
    # ISO\sources
    Write-Verbose -Message "Creating $Destination\ISO\sources"
    New-Item -Path "$Destination\ISO\sources" -Type directory | Out-Null
    
    Write-Verbose -Message "Copying boot.wim"
    Copy-Item -Path "$winPeFile\winpe.wim" -Destination "$Destination\ISO\sources"
    Rename-Item -Path "$Destination\ISO\sources\winpe.wim" -NewName "boot.wim"

您可能会注意到我保留了 AIK 目录样式——如果它没坏,就不要修理它。

    Write-Verbose -Message "Copying bootmgr"
    Copy-Item -Path "$bootFiles\bootmgr" -Destination "$Destination\ISO"
    Write-Verbose -Message "Copying bootmgr.efi"
    Copy-Item -Path "$bootFiles\bootmgr.efi" -Destination "$Destination\ISO"
    
    # Boot/EFI directories
    
    Write-Verbose -Message "Copying $sourceDir\boot"
    Copy-Item -Path "$bootFiles\boot" -Destination "$Destination\ISO" -Recurse
    Write-Verbose -Message "Copying $sourceDir\efi"
    Copy-Item -Path "$bootFiles\EFI" -Destination "$Destination\ISO" -Recurse
}

最后需要的是启动文件。

创建可启动 ISO

function Create-ISO {
    param(
    [Parameter(Position=0)][string]$PeDir, 
    [Parameter(Position=1)][string]$FileName,
    [Parameter(Position=2)][string]$OscdimgPath)
    
    Write-OutPut "Creating $PeDir\$FileName"
    
    [System.Diagnostics.ProcessStartInfo] $sInfo = New-Object System.Diagnostics.ProcessStartInfo -ArgumentList "$OscdimgPath\oscdimg.exe"
    $sInfo.Arguments = [String]::Format("-n `-b`"{0}`" `"{1}`" `"{2}`"", "$PeDir\etfsboot.com", "$PeDir\ISO", "$PeDir\$FileName")
    $sInfo.CreateNoWindow = $true
    $sInfo.UseShellExecute = $false
    
    [System.Diagnostics.Process] $proc = New-Object System.Diagnostics.Process
    $proc.StartInfo = $sInfo
    [void]$proc.Start()
    $proc.WaitForExit()
    
    if(Test-Path "$PeDir\$FileName") {
        Write-OutPut "ISO created successfully."
    }
    else {
        Write-OutPut "ISO was not created."
    }
}

需要注意的是,oscdimg.exe 必须与执行环境(x86 或 amd64)相同。

创建可启动 USB

function Create-USB
{
    param(
    [Parameter(Position=0)][string]$IsoDir,
    [string]$UsbDisk = "1")
    
    if((gwmi Win32_Volume -Filter 'DriveType = 2') -eq $null) {
        Write-Error "No USB device detected."
        return
    }
    
    $temp = ${env:TEMP}
    Out-File -FilePath "$temp\partdisk.txt" -InputObject "Select Disk $UsbDisk" -Encoding ASCII
    Out-File -FilePath "$temp\partdisk.txt" -InputObject "clean" -Append -Encoding ASCII
    Out-File -FilePath "$temp\partdisk.txt" -InputObject "Create Partition Primary" -Append -Encoding ASCII
    Out-File -FilePath "$temp\partdisk.txt" -InputObject "Select Part 1" -Append -Encoding ASCII
    Out-File -FilePath "$temp\partdisk.txt" -InputObject "Active" -Append -Encoding ASCII
    Out-File -FilePath "$temp\partdisk.txt" -InputObject "Format fs=ntfs quick" -Append -Encoding ASCII
    $letter = "Z"
    $assigned = "Assign letter=$letter"
    [byte]$s = 68
    while($s -lt 91) {
        $letter = [System.Convert]::ToChar($s)
        $s++
        
        $filter = [String]::Format("DriveLetter = `"{0}:`"", $letter)
        
        if((gwmi Win32_Volume -Filter $filter) -eq $null) {
            $assigned = "Assign letter=$letter"
            break
        }
    }
    Out-File -FilePath "$temp\partdisk.txt" -InputObject $assigned -Append -Encoding ASCII
    Out-File -FilePath "$temp\partdisk.txt" -InputObject "Exit" -Append -Encoding ASCII
    
    Write-OutPut "Formating USB--all data will be lost."
    $procOutput = diskpart /s "$temp\partdisk.txt" 2>&1
    Remove-Item -Path "$temp\partdisk.txt"
    
    Write-OutPut "Copying files."
    
    # Copy Files
    if(Test-Path "$letter`:\") {
        Copy-Item -Path "$IsoDir\bootmgr" -Destination "$letter`:\"
        Copy-Item -Path "$IsoDir\bootmgr.efi" -Destination "$letter`:\"
        
        Copy-Item -Path "$IsoDir\boot" -Destination "$letter`:\" -Recurse
        Copy-Item -Path "$IsoDir\EFI" -Destination "$letter`:\" -Recurse
        Copy-Item -Path "$IsoDir\sources" -Destination "$letter`:\" -Recurse
        
        Write-OutPut "USB ISO created successfully."
    }
    else {
        Write-OutPut "USB was not created."
    }    
}

本节使用 diskpart 格式化和分区 USB 驱动器。

(它仅编写为将一个 WinPE 映像复制到 USB 设备。但是,通过使用 bcdedit 进行修改,可以在单个 USB 上使用多个 WinPE 映像。)

安装 AIK 包

function Install-AikPackage {
    param([Parameter(Position=0)][string]$pePackage,
    [Parameter(Position=1)]$installedPackages,
    [Parameter(Position=2)][string]$peDir,
    [Parameter(Position=3)][string]$WinPE_FPs)
    # check for package
    [System.Text.RegularExpressions.Regex]$regx = New-Object System.Text.RegularExpressions.Regex -argumentlist "$pePackage", IgnoreCase
    $matches = $regx.Matches($installedPackages)
    
    if($matches -ne $null) {
        if($matches.count -lt 2) {
            # check for language pack
            $b = Select-String -InputObject $packages -Pattern "$pePackage(.*?)en-us~"
            if($b -eq $null) {                
                # install language pack
                Write-OutPut "Installing $pePackage language package."
                dism /image:"$peDir\mount" /Add-Package /PackagePath:"$WinPE_FPs\en-us\$pePackage`_en-us.cab"
            }
            else {
                # has language pack
                Write-OutPut "Installing $pePackage package."
                dism /image:"$peDir\mount" /Add-Package /PackagePath:"$WinPE_FPs\$pePackage.cab"
            }
        }
        else {
            Write-OutPut "Skipping $pePackage Packages."
        }
    }
    else {
        Write-OutPut "Installing $pePackage Packages."
        dism /image:"$peDir\mount" /Add-Package /PackagePath:"$WinPE_FPs\$pePackage.cab"
        dism /image:"$peDir\mount" /Add-Package /PackagePath:"$WinPE_FPs\en-us\$pePackage`_en-us.cab"
    }
}

安装 AIK 特定包。

安装 ADK 包

function Install-AdkPackage {
    param([Parameter(Position=0)][string]$pePackage,
    [Parameter(Position=1)]$installedPackages,
    [Parameter(Position=2)][string]$peDir,
    [Parameter(Position=3)][string]$WinPE_FPs)
    $found = $false
    foreach($p in $installedPackages) {
    
        # doesn't check for language packs (install both regardless)
        [System.Text.RegularExpressions.Regex]$regx = New-Object System.Text.RegularExpressions.Regex -argumentlist "$pePackage", IgnoreCase
        $m = $regx.Matches($p.FeatureName)
        if($m -ne $null -and $m.Count -gt 0) {
            $found = $true
        }        
    }
    if($found) {
        Write-Host "Skipping $pePackage"
        return
    }    
    Add-WindowsPackage -Path "$peDir\mount" -PackagePath "$WinPE_FPs\$pePackage.cab" | Out-Null
    Add-WindowsPackage -Path "$peDir\mount" -PackagePath "$WinPE_FPs\en-us\$pePackage`_en-us.cab" | Out-Null
}

安装 ADK 特定包。

检查管理员权限

function Has-Role {
    param([Security.Principal.WindowsBuiltInRole]$Role = [Security.Principal.WindowsBuiltInRole]::Administrator)
    
    $identity = [Security.Principal.WindowsIdentity]::GetCurrent()
    $principal = New-Object Security.Principal.WindowsPrincipal $identity
    return $principal.IsInRole($Role)
}

DISM 需要提升权限,否则 DISM 将出错。

文件夹选择

function Get-Directory {
    param([Parameter(Position=0)][string] $WindowCaption, $StartDirectory, [switch] $NoNewFolderButton)
    if($IsSTA) {
        [System.Windows.Forms.FolderBrowserDialog] $browserDialog = New-Object System.Windows.Forms.FolderBrowserDialog
        $browserDialog.Description = $WindowCaption
        if($StartDirectory -ne $null) {
            $browserDialog.SelectedPath = $StartDirectory
        }
        if($NoNewFolderButton) {
            $browserDialog.ShowNewFolderButton = $false
        }
        if ($browserDialog.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) {
            return $browserDialog.SelectedPath
        }
    }
    else {
        $shell = New-Object -COM "Shell.Application"
        $f = $shell.BrowseForFolder(0, $WindowCaption, 0)
        if($f -ne $null) {
            return $f.Self.Path
        }
    }
    # otherwise
    return [String]::Empty
}

选择目录需要 STA 检查。

消息框

# MessageBox -- MUST Have Assemblies Loaded
function Show-Message
{
Param(
[Parameter(Position=0)][string]$message,
[Parameter(Position=1)][string]$caption)
    if ($caption -eq $null -or $caption -eq [String]::Empty) {
        return [System.Windows.Forms.MessageBox]::Show($message)
    }
    else {
        return [System.Windows.Forms.MessageBox]::Show($message,$caption)
    }
}

制作 WinPE 映像

function Main
{
param(
    [Parameter(Position=0, Mandatory=$true)][string]$WinPEDirectory,
    [string]$Environment = "amd64",
    $DriverLocation,
    $ExtrasPath,
    [string]$OutPut = "NONE",
    $DiskId,
    [string]$startnet,
    $WinKitDirectory,
    [switch]$UseAik,
    [switch]$useExisting)
  • WinPEDirectory:WinPE 将在此处构建
  • Environment:WinPE 环境
  • DriverLocation:驱动程序目录或驱动程序 inf 文件的 string[]
  • ExtrasPath:包含要添加的其他文件的目录或文件的 string[]
  • OutPut:输出类型是 PXE、USB、Disc 或 None
  • DiskId:USB 的磁盘 ID
  • startnet:要添加到 startnet 的命令,例如要运行的批处理文件
  • WinKitDirectory:AIK 或 ADK 安装目录
  • UseAik:默认为 ADK,此处设置为使用 AIK
  • useExisting:更新现有的 WinPE 映像
    # Begin Process................................................
    Write-OutPut "------------------------------------------------"
    $msg = [DateTime]::Now.ToShortTimeString()
    Write-OutPut "Beginning Make-WinPE $msg"
    Write-OutPut "------------------------------------------------"
    $dismArchitecture = "amd64"
    if(!([Environment]::Is64BitProcess)){
        $dismArchitecture = "x86"
    }

这里最重要的是获取正确版本的 DISM。

    # Copy Files
    if($UseAik) {
        # AIK
        if(!($useExisting)) {
            Copy-PE $Environment $WinPEDirectory -installDirectory $WinKitDirectory -Aik
        }
    }
    else {
        
        # import dism for ADK (Set-Location for dism dependencies)
        Set-Location "$WinKitDirectory\Deployment Tools\$dismArchitecture\DISM"
        import-module "$WinKitDirectory\Deployment Tools\$dismArchitecture\DISM"
           
        if(!($useExisting)) {
            Copy-PE $Environment $WinPEDirectory -installDirectory $WinKitDirectory
        }
    }

复制新构建的文件。(还设置了 ADK 环境要求。)

    if((Test-Path "$WinPEDirectory\ISO\sources\boot.wim") -ne $true) {
        Write-Output "boot.wim is missing from $WinPEDirectory\ISO\sources"
        Write-Output "Copying new boot.wim"
        
        if(Test-Path "$WinPEDirectory\winpe.wim") {
            Copy-Item -Path "$WinPEDirectory\winpe.wim" -Destination "$WinPEDirectory\ISO\sources"
            Rename-Item -Path "$WinPEDirectory\ISO\sources\winpe.wim" -NewName "boot.wim"
        }
        else {
            Write-Error "$WinPEDirectory\winpe.wim is missing. 
            The WinPE directory appears to be corrupted. Select an alternate location for WinPE output."
            return
        }
    }

额外的 boot.wim 检查

    # Mount...
    if($UseAik) {
        dism /Mount-Wim /WimFile:"$WinPEDirectory\ISO\sources\boot.wim" 
        /index:1 /MountDir:"$WinPEDirectory\mount"
    }
    else {
        Mount-WindowsImage -Path "$WinPEDirectory\mount" 
        -ImagePath "$WinPEDirectory\ISO\sources\boot.wim" -Index 1 | Out-Null
    }

挂载映像。

    if($OutPut.ToUpper() -eq "PXE") {
    # extract PXE boot
        if(Test-Path "$WinPEDirectory\mount\Windows\Boot\PXE") {
            # Copy
            if((Test-Path "$WinPEDirectory\PXE") -ne $true) {
                Write-OutPut "Extracting PXE boot files to $WinPEDirectory\PXE"
                Copy-Item -Path "$WinPEDirectory\mount\Windows\Boot\PXE" 
                -Destination "$WinPEDirectory" -Recurse
            }
        }
        else {
            # Possible Error Mounting
            if($UseAik) {
                $stdOut = dism /Unmount-Wim /MountDir:"$WinPEDirectory\mount" /discard 2>&1
            }
            else {
                Dismount-WindowsImage -Path "$WinPEDirectory\mount" -Discard | Out-Null
            }
            Write-Error "Unable to find mounted files."
            return
        }
    }

如果构建 PXE,则复制 PXE 文件。

    $packages = New-Object System.Collections.ArrayList
    # AIK & ADK
    [void] $packages.Add("winpe-wmi")
    [void] $packages.Add("winpe-scripting")
    [void] $packages.Add("winpe-hta")

    $imagexFile = ""
    $packageFiles = ""
    $osCd = ""
    if($UseAik) {
        $imagexFile = "$WinKitDirectory\Tools\$Environment\imagex.exe"
        $packageFiles = "$WinKitDirectory\Tools\PETools\$Environment\WinPE_FPs"
        $osCd = "$WinKitDirectory\Tools\$dismArchitecture"
    }
    else {
        $imagexFile = "$WinKitDirectory\Deployment Tools\$Environment\DISM\imagex.exe"
        $packageFiles = "$WinKitDirectory\Windows Preinstallation Environment\$Environment\WinPE_OCs"
        $osCd = "$WinKitDirectory\Deployment Tools\$dismArchitecture\Oscdimg"
        [void] $packages.Add("WinPE-NetFX")
        [void] $packages.Add("WinPE-PowerShell")
        [void] $packages.Add("WinPE-DismCmdlets")
        [void] $packages.Add("WinPE-StorageWMI")
        [void] $packages.Add("WinPE-EnhancedStorage")
    }

移除任何不需要的包,否则映像会非常大。

    # imagex
    if((Test-Path "$WinPEDirectory\mount\Windows\System32\imagex.exe") -ne $true) {
        Write-OutPut "Adding imagex.exe"
        Copy-Item -Path $imagexFile -Destination "$WinPEDirectory\mount\Windows\System32"
    }
    
    if($startnet -ne [String]::Empty) {
        Out-File -FilePath "$WinPEDirectory\mount\Windows\System32\startnet.cmd" 
        -InputObject "$startnet" -Append -Encoding ASCII
    }
    # Currently Installed Packages
    if($UseAik) {
        $installedPackages = dism /image:"$WinPEDirectory\mount" /Get-Packages 2>&1
    }
    else {
        # instead of Get-WindowsPackage
        $installedPackages = Get-WindowsOptionalFeature -Path "$WinPEDirectory\mount"
        
    }
    # Packages
    $packages | % {
        if($UseAik) {
            Install-AikPackage $_ $installedPackages "$WinPEDirectory" "$packageFiles"
        }
        else {
            Install-AdkPackage $_ $installedPackages "$WinPEDirectory" "$packageFiles"
        }
    }

安装包、更新 startnet 和复制 imagex。我使用了 Get-WindowsOptionalFeature 而不是 Get-WindowsPackage,根据文档,它返回的结果更广泛。

    # Drivers
    if($DriverLocation -ne $null)
    {
        Write-OutPut "Installing Drivers"
        
        foreach($s in $DriverLocation) {
            if($s -ne [String]::Empty) {
                if((Get-Item "$s").PSIsContainer) {                
                    $files = [System.IO.Directory]::GetFiles("$s")                    
                    if($files -ne $null) {
                        foreach($file in $files) {
                            if($file.EndsWith("inf")) {
                                if($UseAik) {
                                    dism /image:"$WinPEDirectory\mount" /Add-Driver /driver:"$file"
                                }
                                else {
                                    Add-WindowsDriver -Path "$WinPEDirectory\mount" -Driver "$file" | Out-Null
                                }
                            }
                        }
                    }
                }
                else {
                    # install
                    if($UseAik) {
                        dism /image:"$WinPEDirectory\mount" /Add-Driver /driver:"$s"
                    }
                    else {
                        Add-WindowsDriver -Path "$WinPEDirectory\mount" -Driver "$s" | Out-Null
                    }
                }
            }
        }
    }

驱动程序安装

    # Extras
    if($ExtrasPath -ne $null) {
        Write-OutPut "Copying additional files"
        
        foreach($f in $ExtrasPath) {
            if($f -ne [String]::Empty) {                
                if((Get-Item "$f").PSIsContainer) {                
                    $files = [System.IO.Directory]::GetFiles("$f")                    
                    if($files -ne $null) {
                        foreach($file in $files) {
                            Copy-Item -Path "$file" -Destination "$WinPEDirectory\mount\Windows\System32"
                        }
                    }
                }
                else {
                    Copy-Item -Path "$f" -Destination "$WinPEDirectory\mount\Windows\System32"
                }
            }
        }
    }

额外文件

    # Save changes to wim
    Write-OutPut "Applying Changes to WinPE"
    # Unmount
     if($UseAik) {
        dism /Unmount-Wim /MountDir:"$WinPEDirectory\mount" /commit
     }
     else {
        Dismount-WindowsImage -Path "$WinPEDirectory\mount" -Save | Out-Null
     }
    switch ($OutPut.ToUpper()) {
        "ISO" {
            if(Test-Path "$WinPEDirectory\WinPE_$Environment.iso") {
            Remove-Item "$WinPEDirectory\WinPE_$Environment.iso"
            }
        
            Create-ISO "$WinPEDirectory" "WinPE_$Environment.iso" "$osCd"
            break
        }
        "USB" {
            Create-USB "$WinPEDirectory\ISO"
            break
        }
        "PXE" {
            if(Test-Path "$WinPEDirectory\PXE") {
                Write-OutPut "Copying files for PXE"
                Copy-Item -Path "$WinPEDirectory\ISO" 
                -Destination "$WinPEDirectory\PXE" -Recurse
                
                [int]$bfSize = ((Get-Item "$WinPEDirectory\PXE\pxeboot.n12").Length / 512)
                $h = [String]::Format("0x{0}", $bfSize.ToString("X"))
                
                # Save dhcp pxe settings to file
                Out-File -FilePath "$WinPEDirectory\PXE\DHCP_PXE_SETTINGS.txt" 
                -InputObject "Option 13 $h"
                Out-File -FilePath "$WinPEDirectory\PXE\DHCP_PXE_SETTINGS.txt" 
                -InputObject "Option 67 pxeboot.n12" -Append
            }
            else {
                Write-OutPut "PXE files could not be copied."
            }
            break
        }
        default { break }
    }
    Write-OutPut "------------------------------------------------"
    $msg = [DateTime]::Now.ToShortTimeString()
    Write-OutPut "Ended Make-WinPE $msg"
    Write-OutPut "------------------------------------------------"
}

完成:创建所需的输出。对于 PXE 输出,我还包含了一个额外的 dhcp 设置文件,以提醒如何设置 PXE。

使用窗体

这部分比其他部分长,涵盖了窗体控件和事件。这是使用 VS 创建的,然后使用 Notepad 和 find-replace 转换为 Powershell。

function Use-Form
{
    Add-Type -AssemblyName 'System.Drawing'
    Add-Type -AssemblyName 'System.Windows.Forms'
    
    # Start Form
    $form1 = New-Object System.Windows.Forms.Form
    
    $label1 = New-Object System.Windows.Forms.Label
    $label2 = New-Object System.Windows.Forms.Label
    $label3 = New-Object System.Windows.Forms.Label
    $label4 = New-Object System.Windows.Forms.Label
    $label5 = New-Object System.Windows.Forms.Label
    $label6 = New-Object System.Windows.Forms.Label
    $label7 = New-Object System.Windows.Forms.Label
    
    $btnOutput = New-Object System.Windows.Forms.Button
    $btnDrivers = New-Object System.Windows.Forms.Button
    $btnFiles = New-Object System.Windows.Forms.Button
    $btnCancel = New-Object System.Windows.Forms.Button
    $btnOK = New-Object System.Windows.Forms.Button
    $btnRemoveDriver = New-Object System.Windows.Forms.Button
    $btnRemoveFile = New-Object System.Windows.Forms.Button
    
    $txtOutPut = New-Object System.Windows.Forms.TextBox
    $txtStartnet = New-Object System.Windows.Forms.TextBox
    $cmbOutPut = New-Object System.Windows.Forms.ComboBox
    [System.Windows.Forms.ListBox]$lstDrivers = New-Object System.Windows.Forms.ListBox
    $groupBox1 = New-Object System.Windows.Forms.GroupBox
    $cmbDisk = New-Object System.Windows.Forms.ComboBox
    [System.Windows.Forms.ListBox]$lstFiles = New-Object System.Windows.Forms.ListBox
    $cmbEnv = New-Object System.Windows.Forms.ComboBox
    
    # Added for ADK
    $gb = New-Object System.Windows.Forms.GroupBox
    $rdoAik = New-Object System.Windows.Forms.RadioButton
    $rdoAdk = New-Object System.Windows.Forms.RadioButton
    $lblInsRoot = New-Object System.Windows.Forms.Label
    $txtInsRoot = New-Object System.Windows.Forms.TextBox
    $btnInsRoot = New-Object System.Windows.Forms.Button
    $chkPrevious = New-Object System.Windows.Forms.CheckBox

这里最重要的是 Add-Type 来加载窗体和对话框所需的程序集。

    # Update
    $yOffset = 120
    $label1.AutoSize = $true
    $label1.Location = New-Object System.Drawing.Point(12, (9 + $yOffset))
    $label1.Size = New-Object System.Drawing.Size(84, 13)
    $label1.Text = "Output Directory"
    
    $label2.AutoSize = $true
    $label2.Location = New-Object System.Drawing.Point(12, (40 + $yOffset))
    $label2.Size = New-Object System.Drawing.Size(66, 13)
    $label2.Text = "Output Type"
    
    $label3.AutoSize = $true
    $label3.Location = New-Object System.Drawing.Point(12, (147 + $yOffset))
    $label3.Size = New-Object System.Drawing.Size(40, 13)
    $label3.Text = "Drivers"
    
    $label4.AutoSize = $true
    $label4.Location = New-Object System.Drawing.Point(7, 20) # (20 + $yOffset))
    $label4.Size = New-Object System.Drawing.Size(61, 13)
    $label4.Text = "Disk Select"
    
    $label5.AutoSize = $true
    $label5.Location = New-Object System.Drawing.Point(15, (258 + $yOffset))
    $label5.Size = New-Object System.Drawing.Size(77, 13)
    $label5.Text = "Additional Files"
    
    $label6.AutoSize = $true
    $label6.Location = New-Object System.Drawing.Point(307, (40 + $yOffset))
    $label6.Size = New-Object System.Drawing.Size(66, 13)
    $label6.Text = "Environment"
    
    $label7.AutoSize = $true
    $label7.Location = New-Object System.Drawing.Point(18, (360 + $yOffset))
    $label7.Size = New-Object System.Drawing.Size(94, 13)
    $label7.Text = "Startnet Command"

引入 ADK 使用时,一些控件需要移动。

    # Disks
    $cmbDisk.FormattingEnabled = $true
    $cmbDisk.Location = New-Object System.Drawing.Point(85, 20) #(20 + $yOffset))
    $cmbDisk.Size = New-Object System.Drawing.Size(325, 21)
    $cmbDisk.TabIndex = 1
    # $cmbDisk.Text = "Disk 1"
    
    $groupBox1.Controls.Add($cmbDisk)
    $groupBox1.Controls.Add($label4)
    $groupBox1.Location = New-Object System.Drawing.Point(103, (67 + $yOffset))
    $groupBox1.Size = New-Object System.Drawing.Size(418, 59)
    $groupBox1.TabStop = $false
    $groupBox1.Text = "DISKPART"
    $groupBox1.Visible = $false
    
    $txtOutPut.Location = New-Object System.Drawing.Point(110, (9 + $yOffset))
    $txtOutPut.Size = New-Object System.Drawing.Size(410, 20)
    $txtOutPut.TabIndex = 1
    
    $txtStartnet.Location = New-Object System.Drawing.Point(130, (360 + $yOffset))
    $txtStartnet.Size = New-Object System.Drawing.Size(390, 20)
    $txtStartnet.TabIndex = 19
    $lstDrivers.FormattingEnabled = $true
    $lstDrivers.Location = New-Object System.Drawing.Point(110, (147 + $yOffset))
    $lstDrivers.Name = "lstDrivers"
    $lstDrivers.SelectionMode = [System.Windows.Forms.SelectionMode]::MultiSimple
    $lstDrivers.Size = New-Object System.Drawing.Size(410, 95)
    $lstDrivers.TabIndex = 6
    $lstFiles.FormattingEnabled = $true
    $lstFiles.Location = New-Object System.Drawing.Point(110, (258 + $yOffset))
    $lstFiles.Name = "lstFiles"
    $lstFiles.SelectionMode = [System.Windows.Forms.SelectionMode]::MultiSimple
    $lstFiles.Size = New-Object System.Drawing.Size(410, 95)
    $lstFiles.TabIndex = 10
    $btnOutput.Location = New-Object System.Drawing.Point(528, (9 + $yOffset))
    $btnOutput.Name = "btnOutput"
    $btnOutput.Size = New-Object System.Drawing.Size(85, 23)
    $btnOutput.TabIndex = 2
    $btnOutput.Text = "Browse"
    $btnOutput.UseVisualStyleBackColor = $true
    $btnOutput.Add_Click(
    {
        $txtOutPut.Text = Get-Directory "WinPE Output Directory"
    })
    $btnDrivers.Location = New-Object System.Drawing.Point(528, (147 + $yOffset))
    $btnDrivers.Name = "btnDrivers"
    $btnDrivers.Size = New-Object System.Drawing.Size(85, 23)
    $btnDrivers.TabIndex = 7
    $btnDrivers.Text = "Add Drivers"
    $btnDrivers.UseVisualStyleBackColor = $true
    $btnDrivers.Add_Click(
    {
        [System.Windows.Forms.OpenFileDialog] $ofd = New-Object System.Windows.Forms.OpenFileDialog
        if ($IsSTA -ne $true) {
            $ofd.AutoUpgradeEnabled = $true
            $ofd.ShowHelp = $true # Needed if not using ISE
        }
        $ofd.Filter = "Setup File (*.inf)|*.inf"
        $ofd.Multiselect = $true
        if ($ofd.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) {
            foreach ($s in $ofd.FileNames) {
                $lstDrivers.Items.Add($s)
            }
        }
    })

使用 Powershell 中的对话框——特别是 FolderBrowserDialog

    $btnFiles.Location = New-Object System.Drawing.Point(528, (258 + $yOffset))
    $btnFiles.Name = "btnFiles"
    $btnFiles.Size = New-Object System.Drawing.Size(85, 23)
    $btnFiles.TabIndex = 11
    $btnFiles.Text = "Add Files"
    $btnFiles.UseVisualStyleBackColor = $true
    $btnFiles.Add_Click(
    {
        [System.Windows.Forms.OpenFileDialog] $ofd = New-Object System.Windows.Forms.OpenFileDialog
        if ($IsSTA -ne $true) {
            $ofd.AutoUpgradeEnabled = $true
            $ofd.ShowHelp = $true # Needed if not using ISE
        }
        $ofd.Filter = "All Files (*.*)|*.*"
        $ofd.Multiselect = $true
        if ($ofd.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) {
            foreach ($s in $ofd.FileNames) {
                $lstFiles.Items.Add($s);
            }
        }
    })

第二个(也是最后一个)FolderBrowserDialog

    $btnCancel.DialogResult = [System.Windows.Forms.DialogResult]::Cancel
    $btnCancel.Location = New-Object System.Drawing.Point(527, (395 + $yOffset))
    $btnCancel.Name = "btnCancel"
    $btnCancel.Size = New-Object System.Drawing.Size(85, 23)
    $btnCancel.TabIndex = 12
    $btnCancel.Text = "Cancel"
    $btnCancel.UseVisualStyleBackColor = $true
    $btnOK.Location = New-Object System.Drawing.Point(440, (395 + $yOffset))
    $btnOK.Name = "btnOK"
    $btnOK.Size = New-Object System.Drawing.Size(85, 23)
    $btnOK.TabIndex = 13
    $btnOK.Text = "OK"
    $btnOK.UseVisualStyleBackColor = $true
    $btnOK.Add_Click(
    {
        $form1.DialogResult = [System.Windows.Forms.DialogResult]::OK
    })
    $btnRemoveDriver.Location = New-Object System.Drawing.Point(528, (177 + $yOffset))
    $btnRemoveDriver.Name = "btnRemoveDriver"
    $btnRemoveDriver.Size = New-Object System.Drawing.Size(85, 23)
    $btnRemoveDriver.TabIndex = 14
    $btnRemoveDriver.Text = "Remove"
    $btnRemoveDriver.UseVisualStyleBackColor = $true
    $btnRemoveDriver.Add_Click(
    {
        $items = $lstDrivers.SelectedItems
        
        while($items -ne $null) {
            $lstDrivers.Items.Remove($items[0])
            $items = $lstDrivers.SelectedItems
        }
    })
    $btnRemoveFile.Location = New-Object System.Drawing.Point(527, (287 + $yOffset))
    $btnRemoveFile.Name = "btnRemoveFile"
    $btnRemoveFile.Size = New-Object System.Drawing.Size(85, 23)
    $btnRemoveFile.TabIndex = 15
    $btnRemoveFile.Text = "Remove"
    $btnRemoveFile.UseVisualStyleBackColor = $true
    $btnRemoveFile.Add_Click(
    {
        $items = $lstFiles.SelectedItems
        
        while($items -ne $null) {
            $lstFiles.Items.Remove($items[0])
            $items = $lstFiles.SelectedItems
        }
    })
    $cmbOutPut.FormattingEnabled = $true
    $cmbOutPut.Items.AddRange(("ISO","USB","PXE"))
    $cmbOutPut.Location = New-Object System.Drawing.Point(110, (40 + $yOffset))
    $cmbOutPut.Size = New-Object System.Drawing.Size(121, 21)
    $cmbOutPut.TabIndex = 4
    $cmbOutPut.Text = "NONE"
    $cmbOutPut.Add_SelectedIndexChanged(
    {
        if ($cmbOutPut.Text.ToUpper() -eq "USB") {
            if($groupBox1.Visible -eq $false) {
                # First Show: Get Disks
                Show-Message "Please wait while the disks are listed. This may take a while." | Out-Null
                $w32dd = gwmi -List | Where-Object -FilterScript {$_.Name -eq "Win32_DiskDrive"}
                $disks = $w32dd.GetInstances()
                $disks | % {
                    $size = [Math]::Round(($_.Size / [Math]::Pow(2, 20)))
                    if($size -gt 1024) {
                        $size = ([Math]::Round(($_.Size / [Math]::Pow(2, 30)))).ToString() + " GB"
                    }
                    else {
                        $size = $size.ToString() + " MB"
                    }
                    $cmbDisk.Items.Add([String]::Format("{0}-{1} - {2}", $_.Index, $size, $_.Caption))
                }
                
                if($cmbDisk.Items.Count -lt 2)
                {
                    Show-Message "No USB drive detected." | Out-Null
                }
                else
                {
                    $cmbDisk.SelectedIndex = 1
                }
            }
            
            $groupBox1.Visible = $true
            
        }
        else {
            $groupBox1.Visible = $false
        }
    })

这可能需要一些解释。大小只是 2 的指数:KB = 2^10 (1024),MB = 2^20 (1024 * 1024),依此类推。使用 Pow 只是确定 MB/GB 值的一种捷径。

    $cmbEnv.FormattingEnabled = $true
    $cmbEnv.Items.AddRange(("x86","i64"))
    $cmbEnv.Location = New-Object System.Drawing.Point(400, (40 + $yOffset))
    $cmbEnv.Size = New-Object System.Drawing.Size(121, 21)
    $cmbEnv.TabIndex = 4
    $cmbEnv.Text = "amd64"
    
    $toolTip = New-Object System.Windows.Forms.ToolTip
    $toolTip.SetToolTip($txtStartnet, "Command executed after wpeinit. Example: wscript script.vbs")
    $toolTip.SetToolTip($cmbEnv, "Processor architecture")
    $toolTip.SetToolTip($cmbOutPut, "Output format")
    $toolTip.SetToolTip($lstFiles, "Scripts and executables to be added to WinPE. (Imagex is added by default)")
    $toolTip.SetToolTip($lstDrivers, "Drivers to add to WinPE. Example: network drivers")
    $toolTip.SetToolTip($txtOutPut, "WinPE output directory")
    $toolTip.SetToolTip($cmbDisk, "USB disk")
    
    
    # Finish Form
    $form1.AutoScaleDimensions = New-Object System.Drawing.SizeF(6, 13) #6, 13
    $form1.AutoScaleMode = [System.Windows.Forms.AutoScaleMode]::Font
    $form1.CancelButton = $btnCancel
    $form1.ClientSize = New-Object System.Drawing.Size(626, 550) #430
    
    #update
    $gb.Size = New-Object System.Drawing.Size(606, 102)
    $gb.Location = New-Object System.Drawing.Point(13, 13)
    $gb.Text = "WinPE Source"
    $rdoAik.AutoSize = $true
    $rdoAdk.AutoSize = $true
    $lblInsRoot.AutoSize = $true
    $chkPrevious.AutoSize = $true
    if($psVersionMajor -lt 4) {
        $rdoAik.Checked = $true
        $rdoAdk.Enabled = $false
    }
    else {
        # Must have powershell 4 to use dism import (otherwise default is wrong dism version)
        $rdoAdk.Checked = $true
    }
    $rdoAdk.Text = "ADK"
    $rdoAik.Text = "AIK"
    $lblInsRoot.Text = "Root (Assessment and Deployment Kit for ADK or Windows AIK for AIK)"
    $btnInsRoot.Text = "Browse..."
    $chkPrevious.Text = "Update an existing image"
    $txtInsRoot.Text = $defDir
    $rdoAdk.UseVisualStyleBackColor = $true
    $rdoAik.UseVisualStyleBackColor = $true
    $chkPrevious.UseVisualStyleBackColor = $true
    $btnInsRoot.UseVisualStyleBackColor = $true
    $rdoAik.Location = New-Object System.Drawing.Point(6, 69)
    $rdoAdk.Location = New-Object System.Drawing.Point(62, 69)
    $lblInsRoot.Location = New-Object System.Drawing.Point(6, 21)
    $txtInsRoot.Location = New-Object System.Drawing.Point(6, 41)
    $btnInsRoot.Location = New-Object System.Drawing.Point(513, 41)
    $chkPrevious.Location = New-Object System.Drawing.Point(141, 69)
    $rdoAdk.Size = New-Object System.Drawing.Size(57, 21)
    $rdoAik.Size = New-Object System.Drawing.Size(50, 21)
    $lblInsRoot.Size = New-Object System.Drawing.Size(259, 17)
    $txtInsRoot.Size = New-Object System.Drawing.Size(500, 22)
    $btnInsRoot.Size = New-Object System.Drawing.Size(75, 30)
    $chkPrevious.Size = New-Object System.Drawing.Size(189, 21)
    $btnInsRoot.Add_Click(
    {
        $insPath = Get-Directory -WindowCaption "Kit Installation Root" # -StartDirectory {$env:ProgramFiles}
        $txtInsRoot.Text = $insPath
    })
    
    $gb.Controls.Add($rdoAik)
    $gb.Controls.Add($rdoAdk)
    $gb.Controls.Add($lblInsRoot)
    $gb.Controls.Add($txtInsRoot)
    $gb.Controls.Add($btnInsRoot)
    $gb.Controls.Add($chkPrevious)

    $form1.Controls.Add($gb)
    $form1.Controls.Add($btnRemoveFile)
    $form1.Controls.Add($btnRemoveDriver)
    $form1.Controls.Add($btnOK)
    $form1.Controls.Add($btnCancel)
    $form1.Controls.Add($btnOutput)
    $form1.Controls.Add($btnFiles)
    $form1.Controls.Add($btnDrivers)
    $form1.Controls.Add($lstFiles)
    $form1.Controls.Add($groupBox1)
    $form1.Controls.Add($lstDrivers)
    $form1.Controls.Add($cmbOutPut)
    $form1.Controls.Add($txtOutPut)
    $form1.Controls.Add($txtStartnet)
    $form1.Controls.Add($label1)
    $form1.Controls.Add($label2)
    $form1.Controls.Add($label3)
    $form1.Controls.Add($label5)
    $form1.Controls.Add($label6)
    $form1.Controls.Add($label7)
    $form1.Controls.Add($cmbEnv)
    $form1.Name = "Form1"
    $form1.Text = "WinPE"
    # Display
    $form1.Add_Shown({$form1.Activate})
    $result = $form1.ShowDialog()
    
    if($result -eq [System.Windows.Forms.DialogResult]::OK) {
        if($cmbDisk.Text -ne [String]::Empty) {
            $selDisk = $cmbDisk.Text.Substring(0, 1).Trim()
            if($selDisk -eq "0") {
                Show-Message "Invalid Disk Selection." | Out-Null
                return
            }
            $cmbDisk.Text = $selDisk
        }
        if($rdoAik.Checked) {
            if($chkPrevious.Checked) {
                Main -WinPEDirectory $txtOutPut.Text -Environment $cmbEnv.Text 
                -DriverLocation $lstDrivers.Items -ExtrasPath $lstFiles.Items 
                -OutPut $cmbOutPut.Text -DiskId $cmbDisk.Text -startnet $txtStartnet.Text 
                -WinKitDirectory $txtInsRoot.Text -UseAik -useExisting
            }
            else {
                Main -WinPEDirectory $txtOutPut.Text -Environment $cmbEnv.Text 
                -DriverLocation $lstDrivers.Items -ExtrasPath $lstFiles.Items 
                -OutPut $cmbOutPut.Text -DiskId $cmbDisk.Text -startnet $txtStartnet.Text 
                -WinKitDirectory $txtInsRoot.Text -UseAik
            }
        }
        else {
            if($chkPrevious.Checked) {
                Main -WinPEDirectory $txtOutPut.Text -Environment $cmbEnv.Text 
                -DriverLocation $lstDrivers.Items -ExtrasPath $lstFiles.Items 
                -OutPut $cmbOutPut.Text -DiskId $cmbDisk.Text 
                -startnet $txtStartnet.Text -WinKitDirectory $txtInsRoot.Text -useExisting
            }
            else {
                Main -WinPEDirectory $txtOutPut.Text -Environment $cmbEnv.Text 
                -DriverLocation $lstDrivers.Items -ExtrasPath $lstFiles.Items 
                -OutPut $cmbOutPut.Text -DiskId $cmbDisk.Text 
                -startnet $txtStartnet.Text -WinKitDirectory $txtInsRoot.Text
            }
        }
    }
}

显示窗体并执行 Main 函数。

    if((Has-Role) -ne $true) {
        Write-OutPut "Elevated permissions are required to run DISM."
        Write-OutPut "Use an elevated command to complete these tasks."
        return
    }

if($PeDirectory -ne $null -and $PeDirectory -ne [String]::Empty)
{
    Main -WinPEDirectory $PeDirectory -Environment $PeEnvironment 
    -DriverLocation $PeDrivers -ExtrasPath $PeExtras -OutPut $PeOutPut 
    -DiskId $PeDiskId -WinKitDirectory $defDir
}
else
{
    # Display Form
    Use-Form
}

脚本执行:如果 PeDirectory 为空,则显示窗体;否则,假定已传递参数。请注意,defDir 用于 WinKitDirectory,我将再次强调从命令行运行时设置它的重要性。

可以从本文复制代码以生成功能齐全的脚本。(附加的脚本不完全相同,此处发布的代码已编辑内容。)

历史

  • 2014 年 4 月 27 日 - 最初发布
© . All rights reserved.