mirror of
https://github.com/Grace-Solutions/Invoke-HTTPBootBiosConfiguration.git
synced 2026-09-12 05:49:05 +00:00
Merge pull request #1 from Grace-Solutions/development
Add HTTP(s) boot BIOS configuration with dynamic tool staging
This commit is contained in:
@@ -0,0 +1,486 @@
|
||||
# PowerShell Script Compilation Guidelines
|
||||
|
||||
## Overview
|
||||
This document outlines the standardized guidelines for creating enterprise-grade PowerShell scripts. These guidelines ensure consistency, reliability, and maintainability across all PowerShell automation projects.
|
||||
|
||||
## Core Principles
|
||||
|
||||
### 1. No Nested Functions Policy
|
||||
- **NEVER** use nested functions within scripts
|
||||
- Keep all code in the main scope with minimal function definitions
|
||||
- Use scriptblocks with `.Invoke()` pattern instead of nested functions
|
||||
- This prevents performance issues and maintains code clarity
|
||||
|
||||
### 2. File System Object Usage
|
||||
Always use proper .NET file system objects instead of string paths:
|
||||
|
||||
```powershell
|
||||
# \u2705 CORRECT - Use accelerators with Path.Combine
|
||||
$LogFile = [System.IO.FileInfo][System.IO.Path]::Combine("$($BaseDir)", 'Logs', 'Script.log')
|
||||
$WorkingDir = [System.IO.DirectoryInfo][System.IO.Path]::Combine("$($RootPath)", 'Data')
|
||||
|
||||
# \u274c INCORRECT - Don't use string concatenation
|
||||
$LogFile = "$BaseDir\Logs\Script.log"
|
||||
```
|
||||
|
||||
### 3. Variable Interpolation in Strings
|
||||
Use explicit variable syntax within quotes:
|
||||
|
||||
```powershell
|
||||
# \u2705 CORRECT
|
||||
Write-Output "Processing file: $($File.FullName)"
|
||||
$Path = [System.IO.Path]::Combine("$($BaseDirectory)", 'SubFolder')
|
||||
|
||||
# \u274c INCORRECT
|
||||
Write-Output "Processing file: $File.FullName"
|
||||
$Path = [System.IO.Path]::Combine($BaseDirectory, 'SubFolder')
|
||||
```
|
||||
|
||||
### 4. Code Formatting Standards
|
||||
Use extensive indentation and line breaks for easier code reading:
|
||||
|
||||
```powershell
|
||||
# \u2705 CORRECT - Extensive formatting
|
||||
Switch ($True)
|
||||
{
|
||||
{($Null -ieq $SourceDirectoryList) -or ($SourceDirectoryList.Count -eq 0)}
|
||||
{
|
||||
[System.IO.DirectoryInfo[]]$SourceDirectoryList = @("$($ContentDirectory.FullName)")
|
||||
}
|
||||
|
||||
{([String]::IsNullOrEmpty($DestinationDirectory) -eq $True) -or ([String]::IsNullOrWhiteSpace($DestinationDirectory) -eq $True)}
|
||||
{
|
||||
[System.IO.DirectoryInfo]$DestinationDirectory = "$($Env:SystemDrive)\Recovery\Customizations"
|
||||
}
|
||||
}
|
||||
|
||||
# \u274c INCORRECT - Compressed formatting
|
||||
Switch ($True) { {($Null -ieq $SourceDirectoryList) -or ($SourceDirectoryList.Count -eq 0)} { [System.IO.DirectoryInfo[]]$SourceDirectoryList = @("$($ContentDirectory.FullName)") } }
|
||||
```
|
||||
|
||||
## Logging Standards
|
||||
|
||||
### 1. Transcript Management
|
||||
```powershell
|
||||
# Script basename extraction
|
||||
$ScriptFileInfo = [System.IO.FileInfo]$MyInvocation.MyCommand.Path
|
||||
$ScriptBaseName = $ScriptFileInfo.BaseName
|
||||
|
||||
# Default log path with environment variables
|
||||
$LogPath = [System.IO.DirectoryInfo][System.IO.Path]::Combine("$($env:SystemRoot)", 'Logs', 'Software', "$($ScriptBaseName)")
|
||||
|
||||
# Transcript with rotation (keep last 3)
|
||||
$TranscriptFiles = $LogPath.GetFiles("$($ScriptBaseName)*.log") | Sort-Object CreationTime -Descending
|
||||
if ($TranscriptFiles.Count -gt 3) {
|
||||
for ($i = 3; $i -lt $TranscriptFiles.Count; $i++) {
|
||||
$Null = $TranscriptFiles[$i].Delete()
|
||||
}
|
||||
}
|
||||
|
||||
$TranscriptPath = [System.IO.FileInfo][System.IO.Path]::Combine($LogPath.FullName, "$($ScriptBaseName)_$(Get-Date -Format 'yyyyMMdd').log")
|
||||
$Null = Start-Transcript -Path $TranscriptPath.FullName -Force
|
||||
```
|
||||
|
||||
### 2. Logging Scriptblock Pattern
|
||||
```powershell
|
||||
# Logging scriptblock using PowerShell built-in cmdlets
|
||||
$LogMessage = {
|
||||
param(
|
||||
[string]$Message,
|
||||
[ValidateSet('INFO', 'WARN', 'ERROR', 'DEBUG')]
|
||||
[string]$Level = 'INFO'
|
||||
)
|
||||
|
||||
$Timestamp = [DateTime]::UtcNow.ToString('yyyy-MM-dd HH:mm:ss.fff')
|
||||
$LogEntry = "[$($Timestamp)] - [$($Level)] - $($Message)"
|
||||
|
||||
Switch ($Level)
|
||||
{
|
||||
'INFO'
|
||||
{
|
||||
Write-Verbose $LogEntry -Verbose
|
||||
}
|
||||
'WARN'
|
||||
{
|
||||
Write-Verbose $LogEntry
|
||||
}
|
||||
'ERROR'
|
||||
{
|
||||
Write-Warning $LogEntry
|
||||
}
|
||||
'DEBUG'
|
||||
{
|
||||
Write-Verbose $LogEntry
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Usage
|
||||
$LogMessage.Invoke('Script started successfully', 'INFO')
|
||||
```
|
||||
|
||||
### 3. Default Log Directory Structure
|
||||
```
|
||||
C:\Windows\Logs\Software\ScriptBaseName\
|
||||
\u251c\u2500\u2500 ScriptBaseName_20250924.log
|
||||
\u251c\u2500\u2500 ScriptBaseName_20250923.log
|
||||
\u2514\u2500\u2500 ScriptBaseName_20250922.log
|
||||
```
|
||||
|
||||
## Error Handling Standards
|
||||
|
||||
### 1. Central Error Handling Scriptblock
|
||||
```powershell
|
||||
$HandleError = {
|
||||
param(
|
||||
[System.Exception]$Exception,
|
||||
[string]$Context = 'Unknown'
|
||||
)
|
||||
|
||||
$ErrorMessage = "Error in $($Context): $($Exception.Message)"
|
||||
if ($Exception.InnerException) {
|
||||
$ErrorMessage += " Inner: $($Exception.InnerException.Message)"
|
||||
}
|
||||
$LogMessage.Invoke($ErrorMessage, 'ERROR')
|
||||
$LogMessage.Invoke("Stack Trace: $($Exception.StackTrace)", 'DEBUG')
|
||||
}
|
||||
|
||||
# Usage
|
||||
try {
|
||||
# Risky operation
|
||||
}
|
||||
catch {
|
||||
$HandleError.Invoke($_.Exception, 'Operation Name')
|
||||
throw
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Main Script Structure
|
||||
```powershell
|
||||
try {
|
||||
# Main script logic here
|
||||
$LogMessage.Invoke('Script execution started', 'INFO')
|
||||
|
||||
# Script operations...
|
||||
|
||||
$LogMessage.Invoke('Script execution completed successfully', 'INFO')
|
||||
}
|
||||
catch {
|
||||
$HandleError.Invoke($_.Exception, 'Main Script Execution')
|
||||
throw
|
||||
}
|
||||
finally {
|
||||
# Cleanup operations
|
||||
if ($SomeResource) {
|
||||
$SomeResource.Dispose()
|
||||
$LogMessage.Invoke('Resources cleaned up', 'DEBUG')
|
||||
}
|
||||
|
||||
$LogMessage.Invoke('Script execution finished', 'INFO')
|
||||
$Null = Stop-Transcript
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Optimization Guidelines
|
||||
|
||||
### 1. Collection Performance
|
||||
Use Generic Lists instead of arrays for better performance:
|
||||
|
||||
```powershell
|
||||
# \u2705 CORRECT - High performance collections
|
||||
$DeviceList = New-Object -TypeName 'System.Collections.Generic.List[PSObject]'
|
||||
$StringList = New-Object -TypeName 'System.Collections.Generic.List[String]'
|
||||
$FileList = New-Object -TypeName 'System.Collections.Generic.List[System.IO.FileInfo]'
|
||||
|
||||
# \u274c INCORRECT - Slower array operations
|
||||
$DeviceList = @()
|
||||
$StringList = @()
|
||||
$FileList = @()
|
||||
```
|
||||
|
||||
### 2. API Call Optimization
|
||||
- Pre-load large datasets once and use in-memory lookups
|
||||
- Use `Where-Object -ieq` for subsequent filtering instead of repeated API calls
|
||||
- Measure and log initial load times for performance tracking
|
||||
|
||||
### 2. Microsoft Graph API Best Practices
|
||||
- Use page size of 250 for optimal performance
|
||||
- Implement OR filters: `'(id eq 'groupId') or (displayName eq 'groupName')'`
|
||||
- Use `Get-MgGroupTransitiveMember` to get all member properties in `AdditionalProperties`
|
||||
- Reduce API calls from thousands to just a few strategic calls
|
||||
|
||||
### 3. MECM Integration Patterns
|
||||
- Implement thread safety with reduced concurrent jobs (5 max)
|
||||
- Add 1-second delays between operations to prevent overwhelming MECM
|
||||
- Include device removal functionality for direct membership rules
|
||||
- Provide percentage progress for long-running operations
|
||||
|
||||
## Code Style Standards
|
||||
|
||||
### 1. Parameter Declarations
|
||||
```powershell
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[System.IO.DirectoryInfo]$WorkingDirectory = [System.IO.Path]::Combine($env:TEMP, 'ScriptData'),
|
||||
[System.IO.DirectoryInfo]$LogPath = $null
|
||||
)
|
||||
```
|
||||
|
||||
### 2. PSObject Creation Pattern
|
||||
Use OrderedDictionary for consistent property ordering:
|
||||
|
||||
```powershell
|
||||
# \u2705 CORRECT - Structured PSObject creation
|
||||
$PSObjectProperties = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
|
||||
$PSObjectProperties.DeviceName = $Device.Name
|
||||
$PSObjectProperties.SerialNumber = $Device.SerialNumber
|
||||
$PSObjectProperties.Status = $Device.Status
|
||||
$PSObjectProperties.LastSeen = $Device.LastSeen
|
||||
|
||||
$DeviceObject = New-Object -TypeName 'System.Management.Automation.PSObject' -Property ($PSObjectProperties)
|
||||
|
||||
# Add to Generic List for performance
|
||||
$DeviceList.Add($DeviceObject)
|
||||
|
||||
# \u274c INCORRECT - Direct PSObject creation
|
||||
$DeviceObject = [PSCustomObject]@{
|
||||
DeviceName = $Device.Name
|
||||
SerialNumber = $Device.SerialNumber
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Switch Statement Usage
|
||||
Use Switch statements exclusively instead of if statements:
|
||||
|
||||
```powershell
|
||||
# \u2705 CORRECT - Single condition Switch
|
||||
Switch (Test-Path -Path $OperatingSystemInfoPath.FullName)
|
||||
{
|
||||
{($_ -eq $False)}
|
||||
{
|
||||
[Int]$Script:ErrorCode = $ErrorCodeRange.GetValue(2)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
# \u2705 CORRECT - Multiple conditions Switch
|
||||
Switch ($True)
|
||||
{
|
||||
{($Null -ieq $SourceDirectoryList) -or ($SourceDirectoryList.Count -eq 0)}
|
||||
{
|
||||
[System.IO.DirectoryInfo[]]$SourceDirectoryList = @("$($ContentDirectory.FullName)")
|
||||
}
|
||||
|
||||
{([String]::IsNullOrEmpty($DestinationDirectory) -eq $True) -or ([String]::IsNullOrWhiteSpace($DestinationDirectory) -eq $True)}
|
||||
{
|
||||
[System.IO.DirectoryInfo]$DestinationDirectory = "$($Env:SystemDrive)\Recovery\Customizations"
|
||||
}
|
||||
}
|
||||
|
||||
# \u274c INCORRECT - if/elseif statements
|
||||
if ($Condition1) {
|
||||
# Action 1
|
||||
} elseif ($Condition2) {
|
||||
# Action 2
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Flow Control with Switch
|
||||
- Use `break` to stop on specific conditions
|
||||
- Omit `break` to allow all conditions to evaluate
|
||||
- Use switch/loop labels for complex flow control
|
||||
|
||||
### 5. Null Assignment Pattern
|
||||
Use `$Null =` for commands that don't need output stored:
|
||||
```powershell
|
||||
$Null = Start-Transcript -Path $TranscriptPath.FullName -Force
|
||||
$Null = $Directory.Create()
|
||||
$Null = Add-AppxPackage -Path $PackagePath
|
||||
```
|
||||
|
||||
### 6. Progress Tracking for Long Operations
|
||||
```powershell
|
||||
$ProgressHandler = {
|
||||
param($WebSender, $ProgressEventArgs)
|
||||
$PercentComplete = [math]::Round(($ProgressEventArgs.BytesReceived / $ProgressEventArgs.TotalBytesToReceive) * 100, 2)
|
||||
$ReceivedMB = [math]::Round($ProgressEventArgs.BytesReceived / 1MB, 2)
|
||||
$TotalMB = [math]::Round($ProgressEventArgs.TotalBytesToReceive / 1MB, 2)
|
||||
|
||||
Write-Progress -Activity 'Operation in Progress' `
|
||||
-Status "Processed $($ReceivedMB) MB of $($TotalMB) MB" `
|
||||
-PercentComplete $PercentComplete `
|
||||
-CurrentOperation "Progress: $($PercentComplete)%"
|
||||
}
|
||||
```
|
||||
|
||||
### 7. Exit Code Management
|
||||
Always use `[System.Environment]::ExitCode` for proper exit code handling:
|
||||
|
||||
```powershell
|
||||
# Set default success code
|
||||
[System.Environment]::ExitCode = 0
|
||||
|
||||
# Set specific error codes for different scenarios
|
||||
Switch ($ErrorCondition)
|
||||
{
|
||||
'DownloadFailed'
|
||||
{
|
||||
[System.Environment]::ExitCode = 2
|
||||
}
|
||||
'InstallationFailed'
|
||||
{
|
||||
[System.Environment]::ExitCode = 3
|
||||
}
|
||||
'VerificationFailed'
|
||||
{
|
||||
[System.Environment]::ExitCode = 5
|
||||
}
|
||||
}
|
||||
|
||||
# Log exit code at end
|
||||
$LogMessage.Invoke("Script execution finished with exit code: $([System.Environment]::ExitCode)", 'INFO')
|
||||
```
|
||||
|
||||
## Script Requirements Template
|
||||
|
||||
### 1. Header Requirements
|
||||
```powershell
|
||||
#Requires -Version 5.1
|
||||
#Requires -RunAsAdministrator # If needed
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Brief description of script purpose
|
||||
.DESCRIPTION
|
||||
Detailed description of script functionality
|
||||
.PARAMETER ParameterName
|
||||
Description of parameter
|
||||
.EXAMPLE
|
||||
.\Script.ps1 -Parameter Value
|
||||
.NOTES
|
||||
Author: [Author Name]
|
||||
Version: [Version Number]
|
||||
Created: [Date]
|
||||
#>
|
||||
```
|
||||
|
||||
### 2. Package Management
|
||||
- Always use appropriate package managers instead of manual file editing
|
||||
- Use `npm install`, `pip install`, `cargo add`, etc.
|
||||
- Only edit package files directly for complex configuration changes
|
||||
|
||||
### 3. Testing Recommendations
|
||||
- Write unit tests for all major functions
|
||||
- Test error handling paths
|
||||
- Validate with different input scenarios
|
||||
- Run tests before deployment
|
||||
|
||||
## Directory Structure Best Practices
|
||||
|
||||
```
|
||||
ProjectRoot/
|
||||
\u251c\u2500\u2500 ScriptName/
|
||||
\u2502 \u251c\u2500\u2500 ScriptName.ps1
|
||||
\u2502 \u251c\u2500\u2500 README.md
|
||||
\u2502 \u251c\u2500\u2500 Tests/
|
||||
\u2502 \u2502 \u2514\u2500\u2500 ScriptName.Tests.ps1
|
||||
\u2502 \u2514\u2500\u2500 Logs/
|
||||
\u2502 \u2514\u2500\u2500 (auto-generated log files)
|
||||
\u251c\u2500\u2500 Common/
|
||||
\u2502 \u251c\u2500\u2500 CommonFunctions.ps1
|
||||
\u2502 \u2514\u2500\u2500 CommonVariables.ps1
|
||||
\u2514\u2500\u2500 Documentation/
|
||||
\u251c\u2500\u2500 PowerShell-Script-Compilation-Guidelines.md
|
||||
\u2514\u2500\u2500 ProjectSpecificDocs.md
|
||||
```
|
||||
|
||||
## Quality Checklist
|
||||
|
||||
Before finalizing any PowerShell script, ensure:
|
||||
|
||||
- [ ] No nested functions used
|
||||
- [ ] All paths use `System.IO.Path::Combine`
|
||||
- [ ] File system objects use proper accelerators
|
||||
- [ ] Variables in strings use `"$($Variable)"` syntax
|
||||
- [ ] Extensive indentation and line breaks for readability
|
||||
- [ ] Generic Lists used instead of arrays for performance
|
||||
- [ ] PSObjects created with OrderedDictionary pattern
|
||||
- [ ] Switch statements used exclusively (no if statements)
|
||||
- [ ] Proper flow control with break/labels as needed
|
||||
- [ ] Logging uses PowerShell built-in cmdlets
|
||||
- [ ] Central error handling implemented
|
||||
- [ ] Transcript management with rotation
|
||||
- [ ] Try-catch-finally structure used
|
||||
- [ ] Exit code management with [System.Environment]::ExitCode
|
||||
- [ ] Resources properly disposed in finally block
|
||||
- [ ] Progress tracking for long operations
|
||||
- [ ] Comprehensive parameter validation
|
||||
- [ ] Proper comment-based help
|
||||
- [ ] Performance considerations addressed
|
||||
|
||||
## Version Control Integration
|
||||
|
||||
### Commit Message Format
|
||||
```
|
||||
feat: Add Teams installation detection logic
|
||||
fix: Resolve file locking issue in logging
|
||||
docs: Update README with usage examples
|
||||
refactor: Implement central error handling pattern
|
||||
```
|
||||
|
||||
### Branch Naming
|
||||
- `feature/script-name-enhancement`
|
||||
- `bugfix/logging-file-lock-issue`
|
||||
- `docs/update-compilation-guidelines`
|
||||
|
||||
This document serves as the authoritative guide for PowerShell script development and should be referenced for all new script creation and existing script refactoring efforts.
|
||||
|
||||
Always use WebClient for downloads and not Invoke-WebRequest because it is way slower. This is applied only to downloading files.
|
||||
|
||||
For API calls using System.Net.WebRequest or Httprequest with status code translation and rate limit retry and paging until everything is collected.
|
||||
|
||||
Use Get-CIMInstance always and never Get-WMIObject
|
||||
|
||||
When doing powershell scripts
|
||||
No Write-Host
|
||||
No AI icons in log messages and strings
|
||||
Only Write-Verbose, Warning, Error, and Output as needed
|
||||
Logging function should be centralized and [TimestampUTC] - [Level] - Message
|
||||
Use $Var = New-Object -TypeName 'Type' and not [Type]::New() as much as possible
|
||||
Use $Var = System.Collections.Generic.List[T] for arrays and add with 2 indents then $Var.Add(Item)
|
||||
Use Splatting by doing $VarObjectProperties = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' then two indents and $Var.Property = $Value and after when running the command
|
||||
Set conditional properties when splatting by using a switch statement like Switch ($Property) or in multi case Switch ($True) {}
|
||||
Prefer switch statements over if statements
|
||||
Dont put { on the same line so that the code is more readable so we line break { and indent it one level then indent code one more level after that and then close with } at the same indent as the opening {. This applies to all blocks including Try/Catch/Finally, Switch, For, Function, etc. For example Try at column 0, { indented to column 4, code inside indented to column 8, } back at column 4. Nested blocks continue the pattern — each { is one level in from its parent statement, and the code inside is one more level past the {.
|
||||
Powershell scripts should be written in a PS7 compatible way so that Linux and Mac can be accomodated relatively easily although its not the main goal of each script
|
||||
[System.IO.FileInfo][System.IO.Path]::Combine(Path1, Path2, Leaf) and [System.IO.DirectoryInfo][System.IO.Path]::Combine(Path1, Path2) should be used for all paths so that directory separator logic is already accounted for.
|
||||
Use [System.IO.File]::Exists() for file existence checks and [System.IO.Directory]::Exists() for directory existence checks instead of Test-Path.
|
||||
Switch ($True) should only be used for unrelated conditions. For a single boolean variable use Switch ($VarName) with {($_ -eq $True)} and {($_ -eq $False)} as the cases.
|
||||
Parameter types should use [System.IO.FileInfo] for file paths and [System.IO.DirectoryInfo] for directory paths instead of [String]
|
||||
Use environment variable paths like $Env:ProgramFiles instead of hardcoded paths such as 'C:\Program Files'
|
||||
URLs should be System.URI with full scheme (e.g. https://)
|
||||
Use System.Net.WebClient with default credentials for downloads instead of Invoke-WebRequest
|
||||
Use WriteAllText or WriteAllLines with non BOM encoding for writing file content
|
||||
Environment variables should only be used for paths and non-sensitive configuration, never for secrets or API keys
|
||||
Here-strings should use PowerShell variable expansion with $($Var) syntax to bake in values at generation time rather than creating system environment variables and referencing them
|
||||
Splatting dictionary properties should be indented one additional level from the variable assignment
|
||||
Loops should generally be Long Loop Type For ($Counter Index etc) {} and have Counter of Count generally so that we can easily use Write-Progress when appropriate
|
||||
Main script should always have Try Catch Finally
|
||||
ScriptPath should be dynamically determined and used for the logpath and name using Start-Transcript (no -Append, no custom log appending functions). Transcript log name should be based on the BaseName of the script.
|
||||
Service names and display names should be generic to the product being installed, not specific to a single use case, since configurations can always be extended
|
||||
When doing Powershell modules, apply the following
|
||||
*no async/await
|
||||
*No updating progress bars from background threads because it does not work
|
||||
*No workarounds and simpler approaches without asking first
|
||||
*Code according to best practices
|
||||
*Code quality is that goal, not quick delivery
|
||||
*Think about what is needed before implementation so we can have reusable code across the code base, so BaseCmdlets, inherits, Classes, Models, Methods
|
||||
*Add write progress to cmdlets where approriate
|
||||
*The Module folder should not be gitignored
|
||||
*Build artifcacts should go into the Artifacts folder
|
||||
*All docs except the Readme should go into the docs folder
|
||||
*Any scripts should go into the scripts folder
|
||||
*A changelog should be kept
|
||||
*Constant printing of excessive information to the threads should not occur to keep the project concise
|
||||
*New threads speed up performance but should not forget the progress of the previous thread so we can pick up where we left off
|
||||
*Release format is yyyy.mm.dd.hhmm in all cases
|
||||
+5
-1
@@ -51,4 +51,8 @@ CodeCoverage/
|
||||
# NUnit
|
||||
*.VisualState.xml
|
||||
TestResult.xml
|
||||
nunit-*.xml
|
||||
nunit-*.xml
|
||||
|
||||
# Dynamically staged tool binaries (cannot be redistributed; staged at execution time by Invoke-ToolStaging)
|
||||
# The Toolkit/Tools/X86, X64, and ARM64 folders are already covered by the build-results directory rules above.
|
||||
Toolkit/Tools/All/7-Zip/
|
||||
@@ -0,0 +1,90 @@
|
||||
# Dynamic CCTK Acquisition and Tool Staging
|
||||
|
||||
The script acquires Dell Command | Configure (CCTK) at execution time without permanently installing anything onto the device. This document describes the mechanism, how to host your own portable archive, and how to update to a newer CCTK release.
|
||||
|
||||
## Tool staging
|
||||
|
||||
Because the binaries cannot be redistributed with this repository, the generic `Invoke-ToolStaging` toolkit function ([Toolkit/Functions/Invoke-ToolStaging.ps1](../Toolkit/Functions/Invoke-ToolStaging.ps1)) stages them dynamically on every execution - **regardless of the device manufacturer** - into the toolkit tools directory:
|
||||
|
||||
```
|
||||
Toolkit\Tools\All\7-Zip\7zr.exe Portable 7-Zip console executable
|
||||
Toolkit\Tools\X64\CCTK\ CCTK for x64 (from the package's X86_64 folder)
|
||||
Toolkit\Tools\ARM64\CCTK\ CCTK for ARM64 (when present within the source)
|
||||
Toolkit\Tools\X86\CCTK\ CCTK for x86 (when present within the source)
|
||||
```
|
||||
|
||||
Each tool defines completion test paths; when they already exist, the tool is skipped entirely (a cache hit takes well under a second). Because the tools directory lives inside the script folder, **the staged bits travel with it** - stage once on a deployment share and every subsequent device (including WinPE) uses the cached bits without downloading anything. These folders are git ignored so the bits can never be committed.
|
||||
|
||||
The toolkit resolves the device architecture (`X86`, `X64`, or `ARM64`) automatically, and the script locates `cctk.exe` in this order:
|
||||
|
||||
1. `Toolkit\Tools\<architecture>\CCTK\cctk.exe` (then `Toolkit\Tools\X64\CCTK\cctk.exe` as a fallback)
|
||||
2. `cctk.exe` resolvable from the process path (e.g. pre-staged within a boot image)
|
||||
3. `%ProgramFiles%\Dell\Command Configure\X86_64\cctk.exe` / `%ProgramFiles(x86)%\Dell\Command Configure\X86_64\cctk.exe`
|
||||
|
||||
## The Dell Update Package extraction chain (default)
|
||||
|
||||
Dell does **not** publish a portable (non-MSI) build of Dell Command | Configure for Windows - the DUP executable wrapping an MSI is the only official distribution. The portable folder you get after extraction is fully reusable, however, which is why the script caches it in the staging directory and supports self hosted portable archives (below) for an MSI-free path that can be reused every time.
|
||||
|
||||
The default `-CCTKDownloadURL` points at the Dell Update Package (DUP) for Dell Command | Configure. A DUP cannot simply be unzipped - it is a portable executable (DUPFramework.exe) with a 7-Zip archive embedded inside it, and that archive contains an MSI whose CAB stores files under mangled MSI File-table names. The validated chain is:
|
||||
|
||||
```
|
||||
DUP (.exe) Downloaded from dl.dell.com (requires a User-Agent header,
|
||||
| which the script always sends; anonymous requests are rejected)
|
||||
v
|
||||
Embedded 7z payload Carved out by scanning for the 7-Zip binary signature
|
||||
| 37 7A BC AF 27 1C followed by format version bytes 00 04.
|
||||
| (The signature alone produces false positives inside the
|
||||
| executable code; the version bytes disambiguate.)
|
||||
v
|
||||
CCTKPayload.7z Extracted with the portable 7-Zip console executable 7zr.exe
|
||||
| (~600 KB, downloaded from -SevenZipDownloadURL).
|
||||
| 7-Zip exit code 1 (warning) is accepted: the carved file
|
||||
| contains a couple of trailing bytes after the archive end.
|
||||
v
|
||||
Command_Configure.msi Expanded with an MSI administrative extraction:
|
||||
| msiexec /a Command_Configure.msi TARGETDIR=<working dir> /qn
|
||||
| This produces the proper directory tree WITHOUT installing.
|
||||
v
|
||||
Toolkit\Tools\X64\CCTK\ The X86_64 folder content, placed by the destination mapping
|
||||
Toolkit\Tools\ARM64\CCTK\ The ARM64 folder content, from the same package
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- The MSI administrative extraction fails with MSI error 1320 ("path too long") when the target path is deep, because the extracted tree itself is several levels deep. This is why the default staging directory is the short `%WINDIR%\Temp\HTTPBootBios`.
|
||||
- Dell Command | Configure 5.x no longer requires a separate HAPI driver installation. Dell's own WinPE integration script that ships inside the package (`X86_64\cctk_x86_64_winpe_11.bat`) simply copies the `X86_64` folder into the boot image; `cctk.exe` handles its BIOS interface at runtime. The portable extract therefore runs as-is in both a full OS and WinPE.
|
||||
- The extracted tree also contains an `ARM64` variant; the script deliberately selects the `X86_64` executable.
|
||||
|
||||
## Hosting a portable archive instead (`.zip` / `.7z`)
|
||||
|
||||
For WinPE boot images without the WinPE-MSI optional component, or to avoid depending on Dell/7-Zip servers, host your own archive and point `-CCTKDownloadURL` at it.
|
||||
|
||||
**Archive layout requirement:** the staging destination mappings locate folders literally named `X86_64` (and optionally `ARM64` / `X86`) anywhere within the archive - the natural layout of an installed or administratively extracted `Command Configure` tree. Do not zip the already renamed `Toolkit\Tools\X64\CCTK` folder directly, because it no longer contains a folder named `X86_64`.
|
||||
|
||||
1. Install Dell Command | Configure on any machine (or perform an MSI administrative extraction) so that you have a `Command Configure` tree containing `X86_64` and `ARM64`.
|
||||
2. Zip that tree:
|
||||
|
||||
```powershell
|
||||
Compress-Archive -Path "${Env:ProgramFiles(x86)}\Dell\Command Configure\*" -DestinationPath "CommandConfigurePortable.zip"
|
||||
```
|
||||
3. Host it on your content server and invoke the script with:
|
||||
|
||||
```
|
||||
-CCTKDownloadURL "https://contentserver.example.com/tools/CommandConfigurePortable.zip"
|
||||
```
|
||||
|
||||
`.zip` archives are extracted with the built-in `System.IO.Compression` classes; `.7z` archives are extracted with the dynamically staged `7zr.exe`. A bare `.msi` URL is also supported and is expanded with an MSI administrative extraction.
|
||||
|
||||
## Updating to a newer CCTK release
|
||||
|
||||
The default pins Dell Command | Configure **5.2.2 A00** (released 2026-03-31, driver ID `F2V9N`):
|
||||
|
||||
- URL: `https://dl.dell.com/FOLDER14333137M/1/Dell-Command-Configure-Application_F2V9N_WIN64_5.2.2.292_A00.EXE`
|
||||
|
||||
To move to a newer release:
|
||||
|
||||
1. Open the Dell Command | Configure landing page: <https://www.dell.com/support/kbdoc/en-us/000178000/dell-command-configure>
|
||||
2. Follow the link for the newest version to its driver details page and copy the direct `dl.dell.com` download URL.
|
||||
3. Either pass it as `-CCTKDownloadURL`, or update the default value within the "Set default parameter values" region of the script.
|
||||
|
||||
The carve-and-extract chain is version independent (it keys off the 7-Zip signature and `*.msi`), so newer DUPs are expected to work unchanged.
|
||||
@@ -0,0 +1,78 @@
|
||||
# HTTP Boot Profile Reference (Dell)
|
||||
|
||||
This document describes the HTTP boot profile document the script generates, the CCTK commands used to apply it, and the troubleshooting knowledge gathered while building this automation.
|
||||
|
||||
## Generated profile document
|
||||
|
||||
The script writes `<StagingDirectory>\HttpBootProfile.xml` (UTF-8, no BOM):
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<HttpBootProfile>
|
||||
<UrlInfo Type="https">
|
||||
<Url>https://prod.ipxe.example.com/2PXE/boot/x64/snponly_x64.efi</Url>
|
||||
<CertInfo Type="pem">
|
||||
<cert>-----BEGIN CERTIFICATE-----
|
||||
... PEM content of the CA root certificate (ISRG Root X1 by default) ...
|
||||
-----END CERTIFICATE-----
|
||||
</cert>
|
||||
</CertInfo>
|
||||
</UrlInfo>
|
||||
<IntegrityInfo>
|
||||
<Algorithm>sha256</Algorithm>
|
||||
<Digest />
|
||||
<SignValue />
|
||||
</IntegrityInfo>
|
||||
</HttpBootProfile>
|
||||
```
|
||||
|
||||
The document is built with an `XmlDocument` and written through an `XmlWriter` (two-space indentation, UTF-8 without BOM).
|
||||
|
||||
Element notes (informed by the `BiosConnectProfile.xsd` schema that ships inside Dell Command | Configure):
|
||||
|
||||
| Element | Notes |
|
||||
| --- | --- |
|
||||
| `UrlInfo@Type` | `https` or `http`, derived from the boot URL scheme |
|
||||
| `Url` | The full boot image URL |
|
||||
| `CertInfo` | Optional per the schema. Only emitted for `https` URLs. `Type="pem"` is the only supported certificate encoding |
|
||||
| `cert` | The PEM encoded CA **root** certificate of the chain that signs the boot server's TLS certificate. For Let's Encrypt this is **ISRG Root X1** - not the leaf and not the R10/R11 intermediates |
|
||||
| `IntegrityInfo` | **Required** by the schema (it cannot be omitted). `Algorithm` supports `sha1` and `sha256` |
|
||||
| `Digest` | Intentionally left empty (the schema allows an empty value). This script places configuration values only - it does not download or hash the boot image, so the boot image on the web server can change freely without invalidating the profile |
|
||||
| `SignValue` | Left empty (unsigned profile) |
|
||||
|
||||
## CCTK command sequence
|
||||
|
||||
```
|
||||
cctk --Version
|
||||
cctk --HttpsBoot=Enabled [--ValSetupPwd=<password>]
|
||||
cctk --HttpsBootMode=ManualMode [--ValSetupPwd=<password>]
|
||||
cctk HttpBootProfile --Delete [--ValSetupPwd=<password>] (skippable)
|
||||
cctk HttpBootProfile --Set=<path>\HttpBootProfile.xml [--ValSetupPwd=<password>]
|
||||
cctk HttpBootProfile --Get
|
||||
```
|
||||
|
||||
### Why delete before set
|
||||
|
||||
Updating only the URL inside an existing profile via `HttpBootProfile --Set` has been observed to **not always apply** - the BIOS keeps the old URL even though CCTK returns success. Deleting the profile first and applying the new one fresh avoids this. The script does this by default; `-SkipProfileDeletion` disables it if you want to test the in-place update behavior on a given BIOS release.
|
||||
|
||||
### Verification
|
||||
|
||||
After applying, the script runs `HttpBootProfile --Get` and asserts that the output contains the exact boot URL that was requested. If it does not, the script fails (unless `-ContinueOnError`), so a silently ignored profile surfaces as a task sequence error instead of a device that will not boot later.
|
||||
|
||||
## Relevant exit codes
|
||||
|
||||
CCTK exit codes are documented by Dell at <https://www.dell.com/support/kbdoc/en-us/000147084/dell-command-configure-error-codes>. The ones that matter here:
|
||||
|
||||
| Exit code | Meaning | Script handling |
|
||||
| --- | --- | --- |
|
||||
| 0 | Success | Accepted everywhere |
|
||||
| 150 | Profile Not Present | Accepted for `HttpBootProfile --Delete` and returned by `--Get` when nothing is configured (verified empirically on 5.2.2) |
|
||||
| 240-246 | Password related errors (wrong/required setup password) | Fails the script - check `-SetupPassword`. Note the asymmetry: a device WITH a setup password fails without the correct `-SetupPassword`, but supplying `-SetupPassword` on a device WITHOUT one is harmless - CCTK ignores `--ValSetupPwd` when no setup password is installed (verified on 5.2.2), so one fleet-wide command line works both ways |
|
||||
| 119 | Setting not supported on this platform | Fails the script - the model may not support HTTPS boot |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **Profile applies but device does not boot**: confirm the boot URL actually serves the boot image (e.g. `snponly_x64.efi`), and confirm the certificate in the profile is the root of the server's actual chain (`openssl s_client -connect server:443 -showcerts`).
|
||||
- **URL change did not take effect**: this is the delete-before-set issue above - re-run without `-SkipProfileDeletion`, or as a last resort clear it in the BIOS setup UI.
|
||||
- **`--Set` returns a schema/parse error**: inspect the generated `HttpBootProfile.xml` in the staging directory; the full document is also written into the script log.
|
||||
- **Certificate expiry**: the script logs the downloaded root certificate's subject, thumbprint, and expiration, and warns when it is expired. ISRG Root X1 is valid until 2035-06-04.
|
||||
@@ -1 +0,0 @@
|
||||
###
|
||||
@@ -0,0 +1,42 @@
|
||||
# Windows PE Guide
|
||||
|
||||
The script runs inside Windows PE (WinPE) so that the HTTP(s) boot BIOS configuration can be performed as a step within an MDT/SCCM task sequence. WinPE is detected automatically by the toolkit (`HKLM:\SYSTEM\ControlSet001\Control\MiniNT`), and logs are redirected to the standard MDT/SCCM task sequence log locations when a task sequence is running.
|
||||
|
||||
## Boot image requirements
|
||||
|
||||
| Component | Needed for | Notes |
|
||||
| --- | --- | --- |
|
||||
| WinPE-WMI | Manufacturer detection, CCTK | Included by default in MDT/SCCM boot images. Dell's own WinPE guidance starts the `winmgmt` service before running cctk |
|
||||
| WinPE-NetFx + WinPE-PowerShell | Running the script at all | Standard for script-driven task sequence steps |
|
||||
| WinPE-MSI | **Only** the dynamic Dell Update Package extraction path (`msiexec /a`) | Not needed when CCTK is pre-staged or downloaded as a portable archive |
|
||||
| Network + TLS | The dynamic downloads | The script enables TLS 1.2/1.3 on the ServicePointManager itself; no OS configuration needed |
|
||||
| Proxy (optional) | Environments that require a proxy for internet access | The script resolves the proxy automatically: user WinINET proxy first, then the machine WinHTTP proxy, then direct. In WinPE/SYSTEM contexts there is normally no user proxy, so configure the WinHTTP proxy when one is required: `netsh winhttp set proxy proxy.example.com:8080 "<local>;*.example.com"` (run it as a task sequence step before this script) |
|
||||
|
||||
Dell Command | Configure 5.x needs **no HAPI driver installation step** in WinPE. Dell's own boot image integration script that ships inside the package simply copies the `X86_64` folder into the image, so a copied/extracted `cctk.exe` folder works as-is (requires the WMI service, which WinPE task sequence environments already run).
|
||||
|
||||
## Four ways to provide CCTK in WinPE
|
||||
|
||||
Pick one, in order of preference:
|
||||
|
||||
1. **Stage once from a full Windows operating system (recommended).** Run the script once from a full OS out of the same script folder (for example the deployment share). The binaries are staged into `Toolkit\Tools` and travel with the folder, so every WinPE execution finds them already cached and downloads nothing.
|
||||
2. **Pre-stage cctk.exe in the boot image** (offline capable). Copy the extracted `Command Configure\X86_64` folder into the boot image (e.g. `X:\Command_Configure\X86_64`) as Dell's integration script does, or include it via an extra-files directory / task sequence package. The script finds it automatically when the folder is on the process `PATH`.
|
||||
3. **Self hosted portable archive** (dynamic, no WinPE-MSI needed). Host a `.zip`/`.7z` of an extracted `Command Configure` tree on your content server and pass `-CCTKDownloadURL`. See [CCTK-Acquisition.md](CCTK-Acquisition.md) for the required archive layout.
|
||||
4. **Direct Dell Update Package download** (fully dynamic, default). Works in WinPE **only when the boot image contains the WinPE-MSI optional component**, because expanding the MSI uses `msiexec /a`. Without it the script fails with a clear error that points at the options above.
|
||||
|
||||
ARM64 note: the portable `7zr.exe` is an x86 binary. A full ARM64 Windows installation runs it through x86 emulation, but ARM64 WinPE cannot, so options 3 (with a `.zip`, which needs no 7-Zip) and 4 are unavailable within ARM64 WinPE when a `.7z`/DUP must be extracted - use option 1 or 2 there. The staged `Toolkit\Tools\ARM64\CCTK` folder from an x64 staging run covers ARM64 devices via option 1.
|
||||
|
||||
## Task sequence step example
|
||||
|
||||
Run Command Line step (after a "Gather"/network is available, before the reboot that should HTTP boot):
|
||||
|
||||
```
|
||||
powershell.exe -ExecutionPolicy Bypass -NonInteractive -NoProfile -NoLogo -File "%DEPLOYROOT%\Scripts\Invoke-HTTPBootBiosConfiguration\Invoke-HTTPBootBiosConfiguration.ps1" -BootURL "https://prod.ipxe.example.com/2PXE/boot/x64/snponly_x64.efi" -CCTKDownloadURL "https://contentserver.example.com/tools/CommandConfigurePortable.zip" -SetupPassword "%BIOSPWD%"
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- The whole repository folder (script + `Toolkit\`) must travel together; the script dot-sources `Toolkit\Toolkit.ps1` relative to itself.
|
||||
- The script auto-elevates in a full OS; in WinPE everything already runs as SYSTEM, so it executes directly.
|
||||
- The staging directory defaults to `%WINDIR%\Temp\HTTPBootBios`, which resolves to the RAM disk (`X:`) in WinPE. The DUP download plus extraction needs roughly 350 MB of scratch space - increase the WinPE scratch space to 512 MB, or prefer the much smaller portable archive (option 2), which needs about 50 MB.
|
||||
- On non-Dell hardware the script logs a warning and exits successfully, so the same step can run unconditionally in a mixed fleet.
|
||||
- Failure behavior: any CCTK error, download failure, or a post-apply verification mismatch fails the step (non-zero exit code) unless `-ContinueOnError` is specified.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,188 +0,0 @@
|
||||
#Requires -Version 5
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
A short synopsis of of what the script does
|
||||
|
||||
.DESCRIPTION
|
||||
A meaningful description of what the script does
|
||||
|
||||
.PARAMETER TaskSequenceVariables
|
||||
One or more task sequence variable(s) to retrieve during task sequence execution.
|
||||
If this parameter is not specified, all task sequence variable(s) will be stored into the variable 'TSVariableTable'.
|
||||
Any task sequence variables that are new or have been updated will be saved back to the task sequence engine for futher usage.
|
||||
|
||||
$TSVariable.MyCustomVariableName = "MyCustomVariableValue"
|
||||
$TSVariable.Make = "MyDeviceModel"
|
||||
|
||||
.PARAMETER LogDir
|
||||
A valid folder path. If the folder does not exist, it will be created. This parameter can also be specified by the alias "LogPath".
|
||||
|
||||
.PARAMETER ContinueOnError
|
||||
Ignore failures.
|
||||
|
||||
.EXAMPLE
|
||||
Use this command to execute a VBSCript that will launch this powershell script automatically with the specified parameters. This is useful to avoid powershell execution complexities.
|
||||
|
||||
cscript.exe /nologo "%FolderPathContainingScript%\%ScriptName%.vbs" /SwitchParameter /ScriptParameter:"%ScriptParameterValue%" /ScriptParameterArray:"%ScriptParameterValue1%,%ScriptParameterValue2%"
|
||||
|
||||
wscript.exe /nologo "%FolderPathContainingScript%\%ScriptName%.vbs" /SwitchParameter /ScriptParameter:"%ScriptParameterValue%" /ScriptParameterArray:"%ScriptParameterValue1%,%ScriptParameterValue2%"
|
||||
|
||||
.EXAMPLE
|
||||
powershell.exe -ExecutionPolicy Bypass -NoProfile -NoLogo -File "%FolderPathContainingScript%\%ScriptName%.ps1" -SwitchParameter -ScriptParameter "%ScriptParameterValue%"
|
||||
|
||||
.EXAMPLE
|
||||
powershell.exe -ExecutionPolicy Bypass -NonInteractive -NoProfile -NoLogo -WindowStyle Hidden -Command "& '%FolderPathContainingScript%\%ScriptName%.ps1' -ScriptParameter1 '%ScriptParameter1Value%' -ScriptParameter2 %ScriptParameter2Value% -SwitchParameter"
|
||||
|
||||
.NOTES
|
||||
Any useful tidbits
|
||||
|
||||
.LINK
|
||||
A useful link
|
||||
#>
|
||||
|
||||
[CmdletBinding(SupportsShouldProcess=$True)]
|
||||
Param
|
||||
(
|
||||
[Parameter(Mandatory=$False)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[Alias('TSVars', 'TSVs')]
|
||||
[String[]]$TaskSequenceVariables,
|
||||
|
||||
[Parameter(Mandatory=$False)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[Alias('TSVD', 'TSVDL')]
|
||||
[String[]]$TSVariableDecodeList,
|
||||
|
||||
[Parameter(Mandatory=$False)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[Alias('LogDir', 'LogPath')]
|
||||
[System.IO.DirectoryInfo]$LogDirectory,
|
||||
|
||||
[Parameter(Mandatory=$False)]
|
||||
[Switch]$ContinueOnError
|
||||
)
|
||||
|
||||
Function Test-ProcessElevationStatus
|
||||
{
|
||||
$Identity = [System.Security.Principal.WindowsIdentity]::GetCurrent()
|
||||
$Principal = New-Object -TypeName 'System.Security.Principal.WindowsPrincipal' -ArgumentList ($Identity)
|
||||
$Result = $Principal.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator)
|
||||
|
||||
Write-Output -InputObject ($Result)
|
||||
}
|
||||
|
||||
Switch (Test-ProcessElevationStatus)
|
||||
{
|
||||
Default
|
||||
{
|
||||
Try
|
||||
{
|
||||
#region Define Default Action Preferences
|
||||
$Script:InformationPreference = 'Continue'
|
||||
$Script:DebugPreference = 'SilentlyContinue'
|
||||
$Script:ErrorActionPreference = 'Stop'
|
||||
$Script:VerbosePreference = 'SilentlyContinue'
|
||||
$Script:WarningPreference = 'Continue'
|
||||
$Script:ConfirmPreference = 'None'
|
||||
$Script:WhatIfPreference = $False
|
||||
#endregion
|
||||
|
||||
#region Set the default exit code for the script (By default, the script will exit with an exit code of 0)
|
||||
[System.Environment]::ExitCode = 0
|
||||
#endregion
|
||||
|
||||
#region Initialize Toolkit (This operation loads functions, modules, and variables into the current session, so if you do not see a variable defined below, it is because it is defined in the Toolkit)
|
||||
Try
|
||||
{
|
||||
[System.IO.FileInfo]$ToolkitScriptPath = "$([System.IO.Path]::GetDirectoryName($MyInvocation.MyCommand.Definition))\Toolkit\Toolkit.ps1"
|
||||
|
||||
. "$($ToolkitScriptPath.FullName)" -CallingScriptInvocationInfo ($MyInvocation) -CallingScriptParameterSetName ($PSCmdlet.ParameterSetName)
|
||||
}
|
||||
Catch
|
||||
{
|
||||
[System.Environment]::ExitCode = 6000
|
||||
|
||||
Throw
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Set default parameter values
|
||||
Switch ($True)
|
||||
{
|
||||
{([System.String]::IsNullOrEmpty($ExampleVariable) -eq $True) -or ([System.String]::IsNullOrWhiteSpace($ExampleVariable) -eq $True)}
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Perform Script Actions
|
||||
#Place custom code here
|
||||
#endregion
|
||||
}
|
||||
Catch
|
||||
{
|
||||
#region Perform error handling actions
|
||||
$ErrorHandlingDefinition.Invoke($Error[0], 2, $ContinueOnError.IsPresent)
|
||||
#endregion
|
||||
}
|
||||
Finally
|
||||
{
|
||||
#region Perform finalization actions
|
||||
$FinalizationActions.Invoke()
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
{($_ -eq $False)}
|
||||
{
|
||||
[System.IO.FileInfo]$ScriptPath = "$($MyInvocation.MyCommand.Definition)"
|
||||
|
||||
#$CurrentExecutionPolicy = Get-ExecutionPolicy -Scope Process
|
||||
|
||||
$CurrentExecutionPolicy = 'Bypass'
|
||||
|
||||
$ArgumentList = New-Object -TypeName 'System.Collections.Generic.List[String]'
|
||||
$ArgumentList.Add("-ExecutionPolicy $($CurrentExecutionPolicy)")
|
||||
$ArgumentList.Add('-NonInteractive')
|
||||
$ArgumentList.Add('-NoProfile')
|
||||
$ArgumentList.Add('-NoLogo')
|
||||
$ArgumentList.Add('-NoExit')
|
||||
$ArgumentList.Add('-Command')
|
||||
$ArgumentList.Add("`"`& {. `"$($ScriptPath.FullName)`"")
|
||||
|
||||
$MyInvocation.UnboundArguments.GetEnumerator() | ForEach-Object {$ArgumentList.Add("-$($_.Key) @($($_.Value | ForEach-Object {`"$($_)`"}))")}
|
||||
|
||||
$PSBoundParameters.GetEnumerator() | ForEach-Object {$ArgumentList.Add("-$($_.Key) @($($_.Value | ForEach-Object {`"$($_)`"}))")}
|
||||
|
||||
$ArgumentListTargetIndex = $ArgumentList.Count - 1
|
||||
|
||||
$ArgumentListIndexItem = $ArgumentList[$ArgumentListTargetIndex]
|
||||
|
||||
$Null = $ArgumentList.RemoveAt($ArgumentListTargetIndex)
|
||||
|
||||
$Null = $ArgumentList.Insert($ArgumentListTargetIndex, ($ArgumentListIndexItem + ';'))
|
||||
|
||||
$ArgumentList.Add("[System.Environment]::Exit((`$LASTEXITCODE -Bor [Int](-Not `$? -And -Not `$LASTEXITCODE)))}`"")
|
||||
|
||||
$ScriptInterpreterList = New-Object -TypeName 'System.Collections.Generic.List[System.String]'
|
||||
$ScriptInterpreterList.Add('powershell.exe')
|
||||
$ScriptInterpreterList.Add('pwsh.exe')
|
||||
|
||||
:ScriptInterpreterListLoop ForEach ($ScriptInterpreter In $ScriptInterpreterList)
|
||||
{
|
||||
$ScriptInterpreterObject = Try {Get-Command -Name ($ScriptInterpreter) -ErrorAction SilentlyContinue} Catch {$Null}
|
||||
|
||||
Switch ($Null -ine $ScriptInterpreterObject)
|
||||
{
|
||||
{($_ -eq $True)}
|
||||
{
|
||||
$Null = Start-Process -FilePath ($ScriptInterpreterObject.Path) -WorkingDirectory "$($Env:Temp.TrimEnd('\'))" -ArgumentList ($ArgumentList.ToArray()) -WindowStyle Normal -Verb RunAs -PassThru
|
||||
|
||||
Break ScriptInterpreterListLoop
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,100 @@
|
||||
# Powershell-Script-Template
|
||||
# Invoke-HTTPBootBiosConfiguration
|
||||
|
||||
Configures the UEFI HTTP(s) boot BIOS settings on supported devices so that they can network boot directly from a web server, such as a [2Pint DeployR](https://2pintsoftware.com) instance secured with a Let's Encrypt certificate.
|
||||
|
||||
The script is fully dynamic: everything it needs (the BIOS configuration utility, the certificate authority root certificate, and the boot image digest) is downloaded and generated at execution time. Nothing is permanently installed onto the device, and the script runs in both a full Windows operating system and Windows PE (WinPE), which makes it suitable for MDT/SCCM task sequences.
|
||||
|
||||
A manufacturer switch statement selects the configuration method per device vendor so that additional manufacturers can be added over time.
|
||||
|
||||
| Manufacturer | Status | Method |
|
||||
| --- | --- | --- |
|
||||
| Dell | Implemented | Dell Command \| Configure (CCTK) |
|
||||
| HP | Not implemented yet | - |
|
||||
| Lenovo | Not implemented yet | - |
|
||||
|
||||
## How it works
|
||||
|
||||
**Tool staging (all manufacturers).** Before anything vendor specific runs, the `Invoke-ToolStaging` toolkit function stages the required binaries into `Toolkit\Tools` (`All`, `X86`, `X64`, `ARM64`), because they cannot be redistributed with this repository: the portable 7-Zip console executable (`7zr.exe`) goes to `All\7-Zip`, and Dell Command | Configure is downloaded (the Dell Update Package by default - the embedded 7-Zip payload is carved out by binary signature, extracted with `7zr.exe`, and the resulting MSI is expanded with an MSI administrative extraction), then placed into `X64\CCTK` and `ARM64\CCTK`. Once staged, the binaries travel with the script folder (for example on a deployment share), so repeat executions on any device use the cached bits without downloading anything. See [Docs/CCTK-Acquisition.md](Docs/CCTK-Acquisition.md).
|
||||
|
||||
Then, for Dell devices:
|
||||
|
||||
1. **Locate CCTK** - the staged `Toolkit\Tools\<architecture>\CCTK` folder first (the toolkit resolves `X86`, `X64`, or `ARM64` automatically), then the process path, then the standard installation directories.
|
||||
2. **Download the root certificate** - the Let's Encrypt "ISRG Root X1" PEM by default, so the BIOS can validate the TLS certificate presented by the boot server.
|
||||
3. **Generate the HTTP boot profile XML** - built with an XmlDocument and written through an XmlWriter. The integrity digest is intentionally left empty: the script places configuration values only and does not download or hash the boot image. See [Docs/HttpBootProfile-Reference.md](Docs/HttpBootProfile-Reference.md) for the document format.
|
||||
4. **Apply the BIOS configuration** by executing the following CCTK commands in order:
|
||||
|
||||
| # | Command | Notes |
|
||||
| --- | --- | --- |
|
||||
| 1 | `cctk --Version` | Validates that the acquired executable runs |
|
||||
| 2 | `cctk --HttpsBoot=Enabled` | Enables the HTTPS boot BIOS feature |
|
||||
| 3 | `cctk --HttpsBootMode=ManualMode` | Sets the HTTPS boot mode to manual |
|
||||
| 4 | `cctk HttpBootProfile --Delete` | Deletes any existing profile first (exit code 150 "Profile Not Present" is accepted). Skippable with `-SkipProfileDeletion` |
|
||||
| 5 | `cctk HttpBootProfile --Set=<profile.xml>` | Applies the generated profile |
|
||||
| 6 | `cctk HttpBootProfile --Get` | Reads the profile back and verifies the configured URL |
|
||||
|
||||
The existing profile is deleted before the new one is applied because updating only the URL within an existing profile has been observed to not always apply. When a BIOS setup password is supplied with `-SetupPassword`, it is appended to each modification command as `--ValSetupPwd=<password>` and the command lines are obfuscated within the log.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
# Basic usage (elevates automatically when required)
|
||||
powershell.exe -ExecutionPolicy Bypass -NoProfile -NoLogo -File ".\Invoke-HTTPBootBiosConfiguration.ps1" -BootURL "https://prod.ipxe.example.com/2PXE/boot/x64/snponly_x64.efi"
|
||||
|
||||
# A URL that does not end with a file name gets "snponly_x64.efi" appended automatically (with or without a trailing slash)
|
||||
powershell.exe -ExecutionPolicy Bypass -NoProfile -NoLogo -File ".\Invoke-HTTPBootBiosConfiguration.ps1" -BootURL "https://prod.ipxe.example.com/2PXE/boot/x64" -SetupPassword "MyBiosPassword"
|
||||
|
||||
# Windows PE with a self hosted portable CCTK archive
|
||||
powershell.exe -ExecutionPolicy Bypass -NoProfile -NoLogo -File ".\Invoke-HTTPBootBiosConfiguration.ps1" -BootURL "https://prod.ipxe.example.com/2PXE/boot/x64/snponly_x64.efi" -CCTKDownloadURL "https://contentserver.example.com/tools/CommandConfigurePortable.zip"
|
||||
```
|
||||
|
||||
`Invoke-HTTPBootBiosConfiguration.exe` is the bootstrapper that launches the identically named PowerShell script, which avoids PowerShell execution policy complexities when invoked from deployment tooling.
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Default | Description |
|
||||
| --- | --- | --- |
|
||||
| `-BootURL` | (required) | Fully qualified HTTP(s) URL of the UEFI boot image, e.g. `https://prod.ipxe.example.com/2PXE/boot/x64/snponly_x64.efi`. When the URL does not end with a file name, `snponly_x64.efi` is appended automatically |
|
||||
| `-RootCertificateURL` | Let's Encrypt ISRG Root X1 (`https://letsencrypt.org/certs/isrgrootx1.pem`) | PEM encoded CA root certificate the BIOS uses to validate the boot server TLS certificate |
|
||||
| `-CCTKDownloadURL` | Dell Command \| Configure 5.2.2 DUP on `dl.dell.com` | Source for the dynamic CCTK acquisition. Supports `.exe` (DUP), `.zip`, and `.7z` |
|
||||
| `-SevenZipDownloadURL` | `https://www.7-zip.org/a/7zr.exe` | Portable 7-Zip console executable used for payload extraction |
|
||||
| `-StagingDirectory` | `%WINDIR%\Temp\HTTPBootBios` | Working directory for downloads and extraction. Keep it short (MSI extraction fails on deep paths) |
|
||||
| `-SetupPassword` | (none) | BIOS setup password, appended as `--ValSetupPwd=` on modification commands. Safe to supply fleet-wide: CCTK ignores the argument on devices with no setup password installed (verified on 5.2.2) |
|
||||
| `-SkipProfileDeletion` | off | Do not delete the existing HTTP boot profile before applying |
|
||||
| `-LogDirectory` | auto (toolkit) | Log folder override |
|
||||
| `-ContinueOnError` | off | Ignore failures |
|
||||
|
||||
## Windows PE
|
||||
|
||||
The script detects WinPE automatically (toolkit `$IsWindowsPE`). The only WinPE specific requirement is for the dynamic DUP extraction path, which needs `msiexec.exe` (the **WinPE-MSI** optional component). The simplest way around it: run the script once from a full Windows operating system - the staged binaries land in `Toolkit\Tools` and travel with the script folder, so WinPE executions find them already cached. Alternatively pre-stage `cctk.exe` or use a self hosted portable archive. Details, boot image requirements, and task sequence examples: [Docs/WindowsPE-Guide.md](Docs/WindowsPE-Guide.md).
|
||||
|
||||
## Proxy support
|
||||
|
||||
All downloads automatically honor the proxy configuration of the environment - no parameters needed:
|
||||
|
||||
1. **Current user proxy (WinINET)** - used when the user has a static proxy enabled or an automatic configuration script (PAC) set. PAC evaluation and per-protocol proxy lists are honored.
|
||||
2. **Machine WinHTTP proxy** - used when the user has none (typical when running as SYSTEM or in WinPE). This is the proxy set with `netsh winhttp set proxy`, including per-protocol lists (`https=` preferred) and the bypass list (`<local>` and wildcard entries are honored).
|
||||
3. **No proxy** - when neither is configured, the proxy is explicitly disabled, which also avoids WebClient's automatic proxy detection delay.
|
||||
|
||||
Default credentials are supplied to authenticating proxies. The resolved proxy source and address are written to the log.
|
||||
|
||||
## Repository layout
|
||||
|
||||
```
|
||||
Invoke-HTTPBootBiosConfiguration.ps1 The script
|
||||
Invoke-HTTPBootBiosConfiguration.exe Bootstrapper that launches the script
|
||||
Toolkit\ Shared toolkit (logging, error handling, process execution, tool staging)
|
||||
Toolkit\Tools\{All,X86,X64,ARM64}\ Staged tool binaries (populated at execution time; git ignored because the bits cannot be redistributed)
|
||||
Docs\ Detailed documentation
|
||||
Content\ Additional content placed here travels with the script
|
||||
.claude\rules\ PowerShell authoring guidelines for this repository
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
- [Docs/CCTK-Acquisition.md](Docs/CCTK-Acquisition.md) - how CCTK is acquired dynamically, how to host a portable archive, and how to update to a newer CCTK release
|
||||
- [Docs/HttpBootProfile-Reference.md](Docs/HttpBootProfile-Reference.md) - HTTP boot profile XML format, CCTK command and exit code reference, troubleshooting
|
||||
- [Docs/WindowsPE-Guide.md](Docs/WindowsPE-Guide.md) - running within Windows PE / task sequences
|
||||
|
||||
## Logging
|
||||
|
||||
A transcript is written to `%WINDIR%\Logs\Software\Invoke-HTTPBootBiosConfiguration` by default (task sequence aware: MDT/SCCM log paths are used automatically when a task sequence is running). All executed command lines, exit codes, and process output are logged. Command lines that contain the BIOS setup password are obfuscated.
|
||||
|
||||
@@ -0,0 +1,624 @@
|
||||
#region Invoke-ToolStaging
|
||||
Function Invoke-ToolStaging
|
||||
{
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Dynamically stages external tool binaries into the toolkit tools directory so that they travel with the script and repeat executions use the cached bits.
|
||||
|
||||
.DESCRIPTION
|
||||
Some vendor binaries cannot be redistributed with this repository, so they are downloaded and extracted at execution time instead. This function downloads each defined tool, extracts it when necessary, and places the binaries into the toolkit tools directory structure (All, X86, X64, ARM64). Once staged, the binaries travel with the script folder (for example on a deployment share), so subsequent executions on any device use the cached bits without downloading anything.
|
||||
|
||||
The following tool types are supported:
|
||||
|
||||
RawFile - The downloaded file is placed directly at the destination (no extraction).
|
||||
Archive - A .zip archive (extracted with the built in compression classes) or a .7z archive (extracted with the 7-Zip console executable).
|
||||
MSI - An MSI package expanded by using an MSI administrative extraction (msiexec /a), which produces the installed file tree without installing anything.
|
||||
DellUpdatePackage - A Dell Update Package (DUP) executable. The embedded 7-Zip payload is carved out of the package by binary signature, extracted with the 7-Zip console executable, and the resulting MSI is expanded by using an MSI administrative extraction.
|
||||
|
||||
.PARAMETER ToolList
|
||||
One or more tool definitions. Each definition is an ordered dictionary with the following properties:
|
||||
|
||||
Enabled - Boolean. Disabled definitions are skipped.
|
||||
Name - The tool name. Used for logging and for the working folder name.
|
||||
Type - RawFile, Archive, MSI, or DellUpdatePackage.
|
||||
DownloadURL - The URL the tool content is downloaded from.
|
||||
DestinationMappingTable - An ordered dictionary that maps content within the extracted tree to a destination relative to the tools directory. The key is a folder or file name to locate within the extracted tree (or "." for the downloaded file itself), and the value is the destination path relative to the tools directory. Mappings whose source cannot be located are logged and skipped, which allows a single definition to include architecture folders that are not present within every source.
|
||||
CompletionTestPathList - One or more paths relative to the tools directory. When ANY of them exists, the tool is considered already staged and is skipped (unless the Force parameter is specified).
|
||||
|
||||
.PARAMETER ToolsDirectory
|
||||
The root toolkit tools directory that the binaries are staged into (the folder containing the All, X86, X64, and ARM64 subdirectories).
|
||||
|
||||
.PARAMETER StagingDirectory
|
||||
The working directory used for downloads and extraction. Keep this path short, because MSI administrative extractions can fail with "path too long" errors when the working path is deep. The working content for each tool is removed after it has been staged successfully.
|
||||
|
||||
.PARAMETER SevenZipExecutablePath
|
||||
The path to the 7-Zip console executable (7zr.exe). Required to extract Dell Update Package payloads and .7z archives. Defaults to "All\7-Zip\7zr.exe" within the tools directory, so a RawFile definition that stages 7-Zip should be placed before any definition that requires it.
|
||||
|
||||
.PARAMETER WebProxy
|
||||
An optional web proxy that is applied to every download. When not specified, no proxy is used.
|
||||
|
||||
.PARAMETER Force
|
||||
Stages each tool even when its completion test paths indicate that it has already been staged.
|
||||
|
||||
.PARAMETER ContinueOnError
|
||||
Continue with the remaining tool definitions instead of throwing a terminating error when a tool cannot be staged.
|
||||
|
||||
.EXAMPLE
|
||||
$ToolStagingList = New-Object -TypeName 'System.Collections.Generic.List[System.Collections.IDictionary]'
|
||||
|
||||
$ToolDefinition = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
|
||||
$ToolDefinition.Enabled = $True
|
||||
$ToolDefinition.Name = 'SevenZip'
|
||||
$ToolDefinition.Type = 'RawFile'
|
||||
$ToolDefinition.DownloadURL = [System.URI]'https://www.7-zip.org/a/7zr.exe'
|
||||
$ToolDefinition.DestinationMappingTable = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
|
||||
$ToolDefinition.DestinationMappingTable['.'] = [System.IO.Path]::Combine('All', '7-Zip', '7zr.exe')
|
||||
$ToolDefinition.CompletionTestPathList = New-Object -TypeName 'System.Collections.Generic.List[String]'
|
||||
$ToolDefinition.CompletionTestPathList.Add([System.IO.Path]::Combine('All', '7-Zip', '7zr.exe'))
|
||||
$ToolStagingList.Add($ToolDefinition)
|
||||
|
||||
$InvokeToolStagingResult = Invoke-ToolStaging -ToolList ($ToolStagingList.ToArray()) -ToolsDirectory ($ToolsDirectory) -Verbose
|
||||
|
||||
.NOTES
|
||||
The download logic always sends a user agent header, because some download servers (such as dl.dell.com) reject anonymous requests.
|
||||
|
||||
.LINK
|
||||
https://www.dell.com/support/kbdoc/en-us/000178000/dell-command-configure
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
Param
|
||||
(
|
||||
[Parameter(Mandatory=$True)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[Alias('TL')]
|
||||
[System.Collections.IDictionary[]]$ToolList,
|
||||
|
||||
[Parameter(Mandatory=$True)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[Alias('TD')]
|
||||
[System.IO.DirectoryInfo]$ToolsDirectory,
|
||||
|
||||
[Parameter(Mandatory=$False)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[Alias('SD')]
|
||||
[System.IO.DirectoryInfo]$StagingDirectory,
|
||||
|
||||
[Parameter(Mandatory=$False)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[Alias('SZEP')]
|
||||
[System.IO.FileInfo]$SevenZipExecutablePath,
|
||||
|
||||
[Parameter(Mandatory=$False)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[Alias('WP')]
|
||||
[System.Net.IWebProxy]$WebProxy,
|
||||
|
||||
[Parameter(Mandatory=$False)]
|
||||
[Alias('F')]
|
||||
[Switch]$Force,
|
||||
|
||||
[Parameter(Mandatory=$False)]
|
||||
[Alias('COE')]
|
||||
[Switch]$ContinueOnError
|
||||
)
|
||||
|
||||
Try
|
||||
{
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
[String]$CmdletName = $MyInvocation.MyCommand.Name
|
||||
|
||||
$WriteLogMessage.Invoke(0, @("Function `'$($CmdletName)`' is beginning. Please Wait..."))
|
||||
|
||||
#region Set default parameter values
|
||||
Switch ($True)
|
||||
{
|
||||
{([System.String]::IsNullOrEmpty($StagingDirectory) -eq $True) -or ([System.String]::IsNullOrWhiteSpace($StagingDirectory) -eq $True)}
|
||||
{
|
||||
[System.IO.DirectoryInfo]$StagingDirectory = [System.IO.Path]::Combine("$($Env:Windir)", 'Temp', 'ToolStaging')
|
||||
}
|
||||
|
||||
{([System.String]::IsNullOrEmpty($SevenZipExecutablePath) -eq $True) -or ([System.String]::IsNullOrWhiteSpace($SevenZipExecutablePath) -eq $True)}
|
||||
{
|
||||
[System.IO.FileInfo]$SevenZipExecutablePath = [System.IO.Path]::Combine("$($ToolsDirectory.FullName)", 'All', '7-Zip', '7zr.exe')
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Define the internal download scriptblock
|
||||
[ScriptBlock]$DownloadToolFile = {
|
||||
Param
|
||||
(
|
||||
[System.URI]$SourceURL,
|
||||
[System.IO.FileInfo]$DestinationPath
|
||||
)
|
||||
|
||||
$WriteLogMessage.Invoke(0, @("Attempting to download `"$($SourceURL.AbsoluteUri)`" to `"$($DestinationPath.FullName)`". Please Wait..."))
|
||||
|
||||
Switch ([System.IO.Directory]::Exists($DestinationPath.Directory.FullName))
|
||||
{
|
||||
{($_ -eq $False)}
|
||||
{
|
||||
$Null = [System.IO.Directory]::CreateDirectory($DestinationPath.Directory.FullName)
|
||||
}
|
||||
}
|
||||
|
||||
$WebClient = New-Object -TypeName 'System.Net.WebClient'
|
||||
|
||||
Try
|
||||
{
|
||||
$WebClient.UseDefaultCredentials = $True
|
||||
|
||||
Switch ($Null -ine $WebProxy)
|
||||
{
|
||||
{($_ -eq $True)}
|
||||
{
|
||||
$WebClient.Proxy = $WebProxy
|
||||
}
|
||||
|
||||
Default
|
||||
{
|
||||
$WebClient.Proxy = $Null
|
||||
}
|
||||
}
|
||||
|
||||
$Null = $WebClient.Headers.Add('User-Agent', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) ToolStaging')
|
||||
|
||||
$Null = $WebClient.DownloadFile($SourceURL.AbsoluteUri, $DestinationPath.FullName)
|
||||
|
||||
$WriteLogMessage.Invoke(0, @("The download completed successfully. [Size: $([System.Math]::Round((Get-Item -Path $DestinationPath.FullName).Length / 1MB, 2)) MB]"))
|
||||
}
|
||||
Finally
|
||||
{
|
||||
$Null = $WebClient.Dispose()
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Define the MSI administrative extraction scriptblock
|
||||
[ScriptBlock]$ExpandMSIPackage = {
|
||||
Param
|
||||
(
|
||||
[System.IO.FileInfo]$MSIPackagePath,
|
||||
[System.IO.DirectoryInfo]$DestinationDirectory
|
||||
)
|
||||
|
||||
$MSIExecCommandObject = Try {Get-Command -Name 'msiexec.exe' -ErrorAction SilentlyContinue} Catch {$Null}
|
||||
|
||||
Switch ($Null -ieq $MSIExecCommandObject)
|
||||
{
|
||||
{($_ -eq $True)}
|
||||
{
|
||||
Throw 'The msiexec.exe executable could not be located, so the MSI cannot be expanded. Within Windows PE, either add the WinPE-MSI optional component to the boot image, or stage the tools by executing the script once from a full Windows operating system so that the staged binaries travel with the script folder.'
|
||||
}
|
||||
}
|
||||
|
||||
$WriteLogMessage.Invoke(0, @("Attempting to expand `"$($MSIPackagePath.FullName)`" into `"$($DestinationDirectory.FullName)`". Please Wait..."))
|
||||
|
||||
$StartProcessWithOutputParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
|
||||
$StartProcessWithOutputParameters.FilePath = "$($MSIExecCommandObject.Path)"
|
||||
$StartProcessWithOutputParameters.WorkingDirectory = "$($MSIPackagePath.Directory.FullName)"
|
||||
$StartProcessWithOutputParameters.ArgumentList = New-Object -TypeName 'System.Collections.Generic.List[String]'
|
||||
$StartProcessWithOutputParameters.ArgumentList.Add('/a')
|
||||
$StartProcessWithOutputParameters.ArgumentList.Add("`"$($MSIPackagePath.FullName)`"")
|
||||
$StartProcessWithOutputParameters.ArgumentList.Add("TARGETDIR=`"$($DestinationDirectory.FullName)`"")
|
||||
$StartProcessWithOutputParameters.ArgumentList.Add('/qn')
|
||||
$StartProcessWithOutputParameters.AcceptableExitCodeList = New-Object -TypeName 'System.Collections.Generic.List[String]'
|
||||
$StartProcessWithOutputParameters.AcceptableExitCodeList.Add('0')
|
||||
$StartProcessWithOutputParameters.CreateNoWindow = $True
|
||||
$StartProcessWithOutputParameters.ExecutionTimeout = [System.TimeSpan]::FromMinutes(10)
|
||||
$StartProcessWithOutputParameters.LogOutput = $True
|
||||
$StartProcessWithOutputParameters.Verbose = $True
|
||||
|
||||
$Null = Start-ProcessWithOutput @StartProcessWithOutputParameters
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Define the 7-Zip extraction scriptblock
|
||||
[ScriptBlock]$ExpandSevenZipArchive = {
|
||||
Param
|
||||
(
|
||||
[System.IO.FileInfo]$ArchivePath,
|
||||
[System.IO.DirectoryInfo]$DestinationDirectory
|
||||
)
|
||||
|
||||
Switch ([System.IO.File]::Exists($SevenZipExecutablePath.FullName))
|
||||
{
|
||||
{($_ -eq $False)}
|
||||
{
|
||||
Throw "The 7-Zip console executable does not exist at `"$($SevenZipExecutablePath.FullName)`". Ensure that a RawFile tool definition that stages 7-Zip is placed before any definition that requires it."
|
||||
}
|
||||
}
|
||||
|
||||
$WriteLogMessage.Invoke(0, @("Attempting to extract `"$($ArchivePath.FullName)`" into `"$($DestinationDirectory.FullName)`". Please Wait..."))
|
||||
|
||||
#An exit code of 1 is a 7-Zip warning (such as trailing bytes after the end of a carved archive) and is acceptable.
|
||||
$StartProcessWithOutputParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
|
||||
$StartProcessWithOutputParameters.FilePath = "$($SevenZipExecutablePath.FullName)"
|
||||
$StartProcessWithOutputParameters.WorkingDirectory = "$($ArchivePath.Directory.FullName)"
|
||||
$StartProcessWithOutputParameters.ArgumentList = New-Object -TypeName 'System.Collections.Generic.List[String]'
|
||||
$StartProcessWithOutputParameters.ArgumentList.Add('x')
|
||||
$StartProcessWithOutputParameters.ArgumentList.Add("`"$($ArchivePath.FullName)`"")
|
||||
$StartProcessWithOutputParameters.ArgumentList.Add("`"-o$($DestinationDirectory.FullName)`"")
|
||||
$StartProcessWithOutputParameters.ArgumentList.Add('-y')
|
||||
$StartProcessWithOutputParameters.AcceptableExitCodeList = New-Object -TypeName 'System.Collections.Generic.List[String]'
|
||||
$StartProcessWithOutputParameters.AcceptableExitCodeList.Add('0')
|
||||
$StartProcessWithOutputParameters.AcceptableExitCodeList.Add('1')
|
||||
$StartProcessWithOutputParameters.CreateNoWindow = $True
|
||||
$StartProcessWithOutputParameters.ExecutionTimeout = [System.TimeSpan]::FromMinutes(10)
|
||||
$StartProcessWithOutputParameters.LogOutput = $True
|
||||
$StartProcessWithOutputParameters.Verbose = $True
|
||||
|
||||
$Null = Start-ProcessWithOutput @StartProcessWithOutputParameters
|
||||
}
|
||||
#endregion
|
||||
|
||||
$OutputObjectList = New-Object -TypeName 'System.Collections.Generic.List[PSObject]'
|
||||
|
||||
[Int]$ToolStagingErrorCount = 0
|
||||
|
||||
:ToolListLoop For ($ToolListIndex = 0; $ToolListIndex -lt $ToolList.Count; $ToolListIndex++)
|
||||
{
|
||||
Try
|
||||
{
|
||||
$ToolDefinition = $ToolList[$ToolListIndex]
|
||||
|
||||
$OutputObjectProperties = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
|
||||
$OutputObjectProperties.Name = "$($ToolDefinition.Name)"
|
||||
$OutputObjectProperties.Staged = $False
|
||||
$OutputObjectProperties.FromCache = $False
|
||||
$OutputObjectProperties.DestinationPathList = New-Object -TypeName 'System.Collections.Generic.List[String]'
|
||||
|
||||
Switch ($ToolDefinition.Enabled)
|
||||
{
|
||||
{($_ -eq $False)}
|
||||
{
|
||||
$WriteLogMessage.Invoke(0, @("Tool $($ToolListIndex + 1) of $($ToolList.Count) `"$($ToolDefinition.Name)`" is disabled and will be skipped."))
|
||||
|
||||
$OutputObjectList.Add((New-Object -TypeName 'PSObject' -Property ($OutputObjectProperties)))
|
||||
|
||||
Continue ToolListLoop
|
||||
}
|
||||
}
|
||||
|
||||
$WriteLogMessage.Invoke(0, @("Tool $($ToolListIndex + 1) of $($ToolList.Count): Attempting to stage `"$($ToolDefinition.Name)`" [Type: $($ToolDefinition.Type)]. Please Wait..."))
|
||||
|
||||
#region Determine whether the tool has already been staged
|
||||
[Boolean]$ToolIsCached = $False
|
||||
|
||||
Switch (($Force.IsPresent -eq $False) -and ($Null -ine $ToolDefinition.CompletionTestPathList))
|
||||
{
|
||||
{($_ -eq $True)}
|
||||
{
|
||||
:CompletionTestPathLoop ForEach ($CompletionTestPath In $ToolDefinition.CompletionTestPathList)
|
||||
{
|
||||
[String]$CompletionTestFullPath = [System.IO.Path]::Combine("$($ToolsDirectory.FullName)", "$($CompletionTestPath)")
|
||||
|
||||
Switch (([System.IO.File]::Exists($CompletionTestFullPath) -eq $True) -or ([System.IO.Directory]::Exists($CompletionTestFullPath) -eq $True))
|
||||
{
|
||||
{($_ -eq $True)}
|
||||
{
|
||||
[Boolean]$ToolIsCached = $True
|
||||
|
||||
$WriteLogMessage.Invoke(0, @("Tool `"$($ToolDefinition.Name)`" has already been staged. The cached bits will be used. [Path: $($CompletionTestFullPath)]"))
|
||||
|
||||
Break CompletionTestPathLoop
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Switch ($ToolIsCached)
|
||||
{
|
||||
{($_ -eq $True)}
|
||||
{
|
||||
$OutputObjectProperties.Staged = $True
|
||||
$OutputObjectProperties.FromCache = $True
|
||||
|
||||
$OutputObjectList.Add((New-Object -TypeName 'PSObject' -Property ($OutputObjectProperties)))
|
||||
|
||||
Continue ToolListLoop
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Download the tool content
|
||||
[System.URI]$ToolDownloadURL = $ToolDefinition.DownloadURL
|
||||
|
||||
[System.IO.DirectoryInfo]$ToolWorkingDirectory = [System.IO.Path]::Combine("$($StagingDirectory.FullName)", "$($ToolDefinition.Name)")
|
||||
|
||||
[System.IO.FileInfo]$ToolDownloadPath = [System.IO.Path]::Combine("$($ToolWorkingDirectory.FullName)", [System.IO.Path]::GetFileName($ToolDownloadURL.LocalPath))
|
||||
|
||||
Switch ([System.IO.File]::Exists($ToolDownloadPath.FullName))
|
||||
{
|
||||
{($_ -eq $True)}
|
||||
{
|
||||
$WriteLogMessage.Invoke(0, @("The tool content has already been downloaded to `"$($ToolDownloadPath.FullName)`". The existing file will be reused."))
|
||||
}
|
||||
|
||||
Default
|
||||
{
|
||||
$Null = $DownloadToolFile.InvokeReturnAsIs($ToolDownloadURL, $ToolDownloadPath)
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Extract the tool content based on the tool type
|
||||
[System.IO.DirectoryInfo]$ToolExtractionDirectory = [System.IO.Path]::Combine("$($ToolWorkingDirectory.FullName)", 'Extracted')
|
||||
|
||||
Switch -Regex ("$($ToolDefinition.Type)")
|
||||
{
|
||||
'(^RawFile$)'
|
||||
{
|
||||
#No extraction is necessary. The destination mapping key "." refers to the downloaded file itself.
|
||||
}
|
||||
|
||||
'(^Archive$)'
|
||||
{
|
||||
Switch ([System.IO.Path]::GetExtension($ToolDownloadPath.FullName).ToLower())
|
||||
{
|
||||
{($_ -iin @('.zip'))}
|
||||
{
|
||||
$WriteLogMessage.Invoke(0, @("Attempting to extract the ZIP archive `"$($ToolDownloadPath.FullName)`" into `"$($ToolExtractionDirectory.FullName)`". Please Wait..."))
|
||||
|
||||
$Null = Add-Type -AssemblyName 'System.IO.Compression.FileSystem'
|
||||
|
||||
$Null = [System.IO.Compression.ZipFile]::ExtractToDirectory("$($ToolDownloadPath.FullName)", "$($ToolExtractionDirectory.FullName)")
|
||||
}
|
||||
|
||||
{($_ -iin @('.7z'))}
|
||||
{
|
||||
$Null = $ExpandSevenZipArchive.InvokeReturnAsIs($ToolDownloadPath, $ToolExtractionDirectory)
|
||||
}
|
||||
|
||||
Default
|
||||
{
|
||||
Throw "The archive extension of `"$($_)`" is not supported. Supported extensions: .zip, .7z"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
'(^MSI$)'
|
||||
{
|
||||
$Null = $ExpandMSIPackage.InvokeReturnAsIs($ToolDownloadPath, $ToolExtractionDirectory)
|
||||
}
|
||||
|
||||
'(^DellUpdatePackage$)'
|
||||
{
|
||||
#region Carve the embedded 7-Zip payload out of the Dell Update Package
|
||||
#A Dell Update Package is a portable executable with a 7-Zip archive embedded within it. The archive begins at the first occurrence of the 7-Zip binary signature (37 7A BC AF 27 1C) that is followed by the format version bytes (00 04). Candidate signatures without the version bytes are false positives within the executable code.
|
||||
$WriteLogMessage.Invoke(0, @("Attempting to locate the embedded 7-Zip payload within `"$($ToolDownloadPath.FullName)`". Please Wait..."))
|
||||
|
||||
[System.Byte[]]$ToolPackageByteList = [System.IO.File]::ReadAllBytes($ToolDownloadPath.FullName)
|
||||
|
||||
[Int64]$SevenZipArchiveSignatureOffset = -1
|
||||
|
||||
[Int64]$SevenZipArchiveSignatureCandidateIndex = [System.Array]::IndexOf($ToolPackageByteList, [System.Byte]0x37, 0)
|
||||
|
||||
:SevenZipArchiveSignatureScanLoop While ($SevenZipArchiveSignatureCandidateIndex -gt -1)
|
||||
{
|
||||
Switch (($SevenZipArchiveSignatureCandidateIndex + 8) -lt $ToolPackageByteList.Length)
|
||||
{
|
||||
{($_ -eq $True)}
|
||||
{
|
||||
Switch (($ToolPackageByteList[$SevenZipArchiveSignatureCandidateIndex + 1] -eq 0x7A) -and ($ToolPackageByteList[$SevenZipArchiveSignatureCandidateIndex + 2] -eq 0xBC) -and ($ToolPackageByteList[$SevenZipArchiveSignatureCandidateIndex + 3] -eq 0xAF) -and ($ToolPackageByteList[$SevenZipArchiveSignatureCandidateIndex + 4] -eq 0x27) -and ($ToolPackageByteList[$SevenZipArchiveSignatureCandidateIndex + 5] -eq 0x1C) -and ($ToolPackageByteList[$SevenZipArchiveSignatureCandidateIndex + 6] -eq 0x00) -and ($ToolPackageByteList[$SevenZipArchiveSignatureCandidateIndex + 7] -eq 0x04))
|
||||
{
|
||||
{($_ -eq $True)}
|
||||
{
|
||||
[Int64]$SevenZipArchiveSignatureOffset = $SevenZipArchiveSignatureCandidateIndex
|
||||
|
||||
Break SevenZipArchiveSignatureScanLoop
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Default
|
||||
{
|
||||
Break SevenZipArchiveSignatureScanLoop
|
||||
}
|
||||
}
|
||||
|
||||
[Int64]$SevenZipArchiveSignatureCandidateIndex = [System.Array]::IndexOf($ToolPackageByteList, [System.Byte]0x37, $SevenZipArchiveSignatureCandidateIndex + 1)
|
||||
}
|
||||
|
||||
Switch ($SevenZipArchiveSignatureOffset -gt -1)
|
||||
{
|
||||
{($_ -eq $True)}
|
||||
{
|
||||
$WriteLogMessage.Invoke(0, @("The embedded 7-Zip payload was located at byte offset $($SevenZipArchiveSignatureOffset)."))
|
||||
}
|
||||
|
||||
Default
|
||||
{
|
||||
Throw "An embedded 7-Zip payload could not be located within `"$($ToolDownloadPath.FullName)`". The downloaded file may not be a Dell Update Package."
|
||||
}
|
||||
}
|
||||
|
||||
[System.IO.FileInfo]$ToolPayloadArchivePath = [System.IO.Path]::Combine("$($ToolWorkingDirectory.FullName)", 'Payload.7z')
|
||||
|
||||
$ToolPayloadArchiveStream = New-Object -TypeName 'System.IO.FileStream' -ArgumentList @("$($ToolPayloadArchivePath.FullName)", [System.IO.FileMode]::Create)
|
||||
|
||||
Try
|
||||
{
|
||||
$Null = $ToolPayloadArchiveStream.Write($ToolPackageByteList, $SevenZipArchiveSignatureOffset, $ToolPackageByteList.Length - $SevenZipArchiveSignatureOffset)
|
||||
}
|
||||
Finally
|
||||
{
|
||||
$Null = $ToolPayloadArchiveStream.Dispose()
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Extract the carved payload and expand the MSI that it contains
|
||||
[System.IO.DirectoryInfo]$ToolPayloadDirectory = [System.IO.Path]::Combine("$($ToolWorkingDirectory.FullName)", 'Payload')
|
||||
|
||||
$Null = $ExpandSevenZipArchive.InvokeReturnAsIs($ToolPayloadArchivePath, $ToolPayloadDirectory)
|
||||
|
||||
$ToolPayloadMSIObject = Get-ChildItem -Path ($ToolPayloadDirectory.FullName) -Filter '*.msi' | Select-Object -First 1
|
||||
|
||||
Switch ($Null -ieq $ToolPayloadMSIObject)
|
||||
{
|
||||
{($_ -eq $True)}
|
||||
{
|
||||
Throw "An MSI could not be located within the extracted Dell Update Package payload at `"$($ToolPayloadDirectory.FullName)`"."
|
||||
}
|
||||
}
|
||||
|
||||
$Null = $ExpandMSIPackage.InvokeReturnAsIs(([System.IO.FileInfo]$ToolPayloadMSIObject.FullName), $ToolExtractionDirectory)
|
||||
#endregion
|
||||
}
|
||||
|
||||
Default
|
||||
{
|
||||
Throw "The tool type of `"$($ToolDefinition.Type)`" is not supported. Supported types: RawFile, Archive, MSI, DellUpdatePackage"
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Place the extracted content into the tools directory based on the destination mapping table
|
||||
ForEach ($DestinationMapping In $ToolDefinition.DestinationMappingTable.GetEnumerator())
|
||||
{
|
||||
[String]$DestinationMappingSource = "$($DestinationMapping.Key)"
|
||||
|
||||
[String]$DestinationMappingTarget = [System.IO.Path]::Combine("$($ToolsDirectory.FullName)", "$($DestinationMapping.Value)")
|
||||
|
||||
#region Resolve the source within the extracted tree
|
||||
$DestinationMappingSourceObject = $Null
|
||||
|
||||
Switch ($True)
|
||||
{
|
||||
{($DestinationMappingSource -ieq '.')}
|
||||
{
|
||||
$DestinationMappingSourceObject = Get-Item -Path ($ToolDownloadPath.FullName)
|
||||
}
|
||||
|
||||
{($DestinationMappingSource -ine '.') -and ([System.IO.Directory]::Exists([System.IO.Path]::Combine("$($ToolExtractionDirectory.FullName)", $DestinationMappingSource)) -eq $True)}
|
||||
{
|
||||
$DestinationMappingSourceObject = Get-Item -Path ([System.IO.Path]::Combine("$($ToolExtractionDirectory.FullName)", $DestinationMappingSource))
|
||||
}
|
||||
|
||||
{($DestinationMappingSource -ine '.') -and ($Null -ieq $DestinationMappingSourceObject)}
|
||||
{
|
||||
$DestinationMappingSourceObject = Get-ChildItem -Path ($ToolExtractionDirectory.FullName) -Recurse -Filter ($DestinationMappingSource) | Select-Object -First 1
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
Switch ($Null -ine $DestinationMappingSourceObject)
|
||||
{
|
||||
{($_ -eq $True)}
|
||||
{
|
||||
$WriteLogMessage.Invoke(0, @("Attempting to place `"$($DestinationMappingSourceObject.FullName)`" at `"$($DestinationMappingTarget)`". Please Wait..."))
|
||||
|
||||
Switch ($DestinationMappingSourceObject.PSIsContainer)
|
||||
{
|
||||
{($_ -eq $True)}
|
||||
{
|
||||
Switch ([System.IO.Directory]::Exists($DestinationMappingTarget))
|
||||
{
|
||||
{($_ -eq $True)}
|
||||
{
|
||||
$Null = [System.IO.Directory]::Delete($DestinationMappingTarget, $True)
|
||||
}
|
||||
}
|
||||
|
||||
$Null = [System.IO.Directory]::CreateDirectory($DestinationMappingTarget)
|
||||
|
||||
$Null = Copy-Item -Path "$($DestinationMappingSourceObject.FullName)\*" -Destination ($DestinationMappingTarget) -Recurse -Force
|
||||
}
|
||||
|
||||
Default
|
||||
{
|
||||
$Null = [System.IO.Directory]::CreateDirectory([System.IO.Path]::GetDirectoryName($DestinationMappingTarget))
|
||||
|
||||
$Null = Copy-Item -Path ($DestinationMappingSourceObject.FullName) -Destination ($DestinationMappingTarget) -Force
|
||||
}
|
||||
}
|
||||
|
||||
$OutputObjectProperties.DestinationPathList.Add($DestinationMappingTarget)
|
||||
}
|
||||
|
||||
Default
|
||||
{
|
||||
$WriteLogMessage.Invoke(2, @("The destination mapping source of `"$($DestinationMappingSource)`" could not be located within `"$($ToolExtractionDirectory.FullName)`". The mapping will be skipped. This is expected when the source content does not contain that architecture."))
|
||||
}
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Remove the tool working directory
|
||||
Switch ([System.IO.Directory]::Exists($ToolWorkingDirectory.FullName))
|
||||
{
|
||||
{($_ -eq $True)}
|
||||
{
|
||||
$WriteLogMessage.Invoke(0, @("Attempting to remove the tool working directory `"$($ToolWorkingDirectory.FullName)`". Please Wait..."))
|
||||
|
||||
$Null = Try {[System.IO.Directory]::Delete($ToolWorkingDirectory.FullName, $True)} Catch {$Null}
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
$OutputObjectProperties.Staged = ($OutputObjectProperties.DestinationPathList.Count -gt 0)
|
||||
|
||||
Switch ($OutputObjectProperties.Staged)
|
||||
{
|
||||
{($_ -eq $True)}
|
||||
{
|
||||
$WriteLogMessage.Invoke(0, @("Tool `"$($ToolDefinition.Name)`" was staged successfully. [Destination(s): $($OutputObjectProperties.DestinationPathList -Join '; ')]"))
|
||||
}
|
||||
|
||||
Default
|
||||
{
|
||||
Throw "None of the destination mappings for tool `"$($ToolDefinition.Name)`" could be staged."
|
||||
}
|
||||
}
|
||||
|
||||
$OutputObjectList.Add((New-Object -TypeName 'PSObject' -Property ($OutputObjectProperties)))
|
||||
}
|
||||
Catch
|
||||
{
|
||||
[Int]$ToolStagingErrorCount = $ToolStagingErrorCount + 1
|
||||
|
||||
$WriteLogMessage.Invoke(2, @("Tool `"$($ToolDefinition.Name)`" could not be staged.", "Message: $($_.Exception.Message)"))
|
||||
|
||||
$OutputObjectList.Add((New-Object -TypeName 'PSObject' -Property ($OutputObjectProperties)))
|
||||
|
||||
Switch ($ContinueOnError.IsPresent)
|
||||
{
|
||||
{($_ -eq $False)}
|
||||
{
|
||||
Throw
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Write-Output -InputObject ($OutputObjectList.ToArray())
|
||||
}
|
||||
Catch
|
||||
{
|
||||
$ExceptionPropertyDictionary = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
|
||||
$ExceptionPropertyDictionary.Add('Message', $_.Exception.Message)
|
||||
$ExceptionPropertyDictionary.Add('Category', $_.Exception.ErrorRecord.FullyQualifiedErrorID)
|
||||
$ExceptionPropertyDictionary.Add('LineNumber', $_.InvocationInfo.ScriptLineNumber)
|
||||
$ExceptionPropertyDictionary.Add('LinePosition', $_.InvocationInfo.OffsetInLine)
|
||||
$ExceptionPropertyDictionary.Add('Code', $_.InvocationInfo.Line.Trim())
|
||||
|
||||
$ExceptionMessageList = New-Object -TypeName 'System.Collections.Generic.List[String]'
|
||||
|
||||
ForEach ($ExceptionProperty In $ExceptionPropertyDictionary.GetEnumerator())
|
||||
{
|
||||
$ExceptionMessageList.Add("[$($ExceptionProperty.Key): $($ExceptionProperty.Value)]")
|
||||
}
|
||||
|
||||
$WriteLogMessage.Invoke(2, @("$($ExceptionMessageList -Join ' ')"))
|
||||
|
||||
Throw
|
||||
}
|
||||
Finally
|
||||
{
|
||||
$WriteLogMessage.Invoke(0, @("Function `'$($CmdletName)`' is completed."))
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
+22
-1
@@ -316,7 +316,28 @@ Try
|
||||
#endregion
|
||||
|
||||
#region Define Variables
|
||||
$OSArchitecture = $($OperatingSystem.OSArchitecture).Replace("-bit", "").Replace("32", "86").Insert(0,"x").ToUpper()
|
||||
#Determine the operating system architecture (X86, X64, or ARM64) for architecture specific tool selection
|
||||
:OSArchitectureSwitch Switch -Regex ("$($OperatingSystem.OSArchitecture)")
|
||||
{
|
||||
'(^.*ARM.*64.*$)'
|
||||
{
|
||||
$OSArchitecture = 'ARM64'
|
||||
|
||||
Break OSArchitectureSwitch
|
||||
}
|
||||
|
||||
'(^.*64.*$)'
|
||||
{
|
||||
$OSArchitecture = 'X64'
|
||||
|
||||
Break OSArchitectureSwitch
|
||||
}
|
||||
|
||||
Default
|
||||
{
|
||||
$OSArchitecture = 'X86'
|
||||
}
|
||||
}
|
||||
$ContentDirectory = [System.IO.DirectoryInfo][System.IO.Path]::Combine($ToolkitScriptDirectory.Parent.FullName, 'Content')
|
||||
$FunctionsDirectory = [System.IO.DirectoryInfo][System.IO.Path]::Combine($ToolkitScriptDirectory.FullName, 'Functions')
|
||||
$ModulesDirectory = [System.IO.DirectoryInfo][System.IO.Path]::Combine($ToolkitScriptDirectory.FullName, 'Modules')
|
||||
|
||||
Reference in New Issue
Block a user