Compare commits

...

3 Commits

Author SHA1 Message Date
25301bd60e change all cn to en to avoid annoying errors 2026-05-20 20:43:42 -04:00
39c0420835 fix manifest temp msi locked 2026-05-20 20:25:29 -04:00
e9aa6024ff support force post install 2026-05-16 23:01:12 -04:00
7 changed files with 138 additions and 81 deletions

View File

@@ -1,15 +1,15 @@
# 设置严格模式,遇到错误停止,防止错误的命令雪崩 # Stop on errors to avoid cascading failures.
$ErrorActionPreference = "Stop" $ErrorActionPreference = "Stop"
#Set-StrictMode -Version Latest #Set-StrictMode -Version Latest
# === 1. 加载依赖库 === # === 1. Check dependencies ===
# 确保 lib 目录存在 # Make sure the lib directory exists.
if (-not (Test-Path "$PSScriptRoot\..\lib")) { if (-not (Test-Path "$PSScriptRoot\..\lib")) {
Write-Error "Cannot find .\lib, some files missing." Write-Error "Cannot find .\lib, some files missing."
exit 1 exit 1
} }
# === 2. 读取配置 === # === 2. Read configuration ===
$jsonPath = "$PSScriptRoot\apps.json" $jsonPath = "$PSScriptRoot\apps.json"
if (-not (Test-Path $jsonPath)) { if (-not (Test-Path $jsonPath)) {
Write-Error "Cannot find: $jsonPath" Write-Error "Cannot find: $jsonPath"
@@ -17,35 +17,82 @@ if (-not (Test-Path $jsonPath)) {
} }
Write-Host "Reading configurations..." -ForegroundColor Cyan Write-Host "Reading configurations..." -ForegroundColor Cyan
# 使用 UTF8 读取防止中文乱码 # Read as UTF-8.
$apps = Get-Content $jsonPath -Encoding UTF8 | ConvertFrom-Json $apps = Get-Content $jsonPath -Encoding UTF8 | ConvertFrom-Json
$forcePostInstall = $false
# === 2.1 加载外部开关配置 (selection.ini) === function Unblock-WinGetCache {
# 尝试查找根目录下的 selection.ini (位于 bin 的上一级) param(
[Parameter(Mandatory=$true)] [string]$PackageId
)
$wingetCacheRoot = Join-Path $env:TEMP "WinGet"
if (-not (Test-Path $wingetCacheRoot)) {
return
}
$cacheItems = Get-ChildItem -Path $wingetCacheRoot -Recurse -File -ErrorAction SilentlyContinue |
Where-Object { $_.FullName -like "*\$PackageId.*" }
foreach ($item in $cacheItems) {
try {
Unblock-File -LiteralPath $item.FullName -ErrorAction Stop
Write-Host "-> Unblocked cached installer: $($item.FullName)" -ForegroundColor DarkGray
} catch {
Write-Warning "Failed to unblock cached installer: $($item.FullName), $_"
}
}
}
# === 2.1 Load selection.ini overrides ===
# selection.ini is located in the project root, one level above bin.
$selectionPath = Join-Path (Split-Path $PSScriptRoot -Parent) "selection.ini" $selectionPath = Join-Path (Split-Path $PSScriptRoot -Parent) "selection.ini"
if (Test-Path $selectionPath) { if (Test-Path $selectionPath) {
Write-Host "Loading selection overrides from selection.ini..." -ForegroundColor Cyan Write-Host "Loading selection overrides from selection.ini..." -ForegroundColor Cyan
try { try {
# 读取所有行,过滤掉注释和空行 # Store app switches and global options separately.
$iniLines = Get-Content $selectionPath -Encoding UTF8 | Where-Object { $_ -match "=" -and $_ -notmatch "^;" -and $_ -notmatch "^\[" }
# 创建一个哈希表来存储 INI 配置
$selectionConfig = @{} $selectionConfig = @{}
foreach ($line in $iniLines) { $optionsConfig = @{}
# 按第一个等号分割,限制分割次数为 2 $currentSection = ""
foreach ($line in (Get-Content $selectionPath -Encoding UTF8)) {
$line = $line.Trim()
if ([string]::IsNullOrWhiteSpace($line) -or $line.StartsWith(";") -or $line.StartsWith("#")) {
continue
}
if ($line -match '^\[(.+)\]$') {
$currentSection = $matches[1].Trim()
continue
}
if ($line -notmatch "=") {
continue
}
# Split on the first equals sign only.
$parts = $line -split '=', 2 $parts = $line -split '=', 2
if ($parts.Count -eq 2) { if ($parts.Count -eq 2) {
$key = $parts[0].Trim() $key = $parts[0].Trim()
$value = $parts[1].Trim() $value = $parts[1].Trim()
$selectionConfig[$key] = $value
if ($currentSection -eq "Options") {
$optionsConfig[$key] = $value
} else {
$selectionConfig[$key] = $value
}
} }
} }
# 应用配置 if ($optionsConfig.ContainsKey("ForcePostInstall")) {
$forcePostInstall = $optionsConfig["ForcePostInstall"] -eq "1"
}
# Apply app enabled overrides.
foreach ($app in $apps) { foreach ($app in $apps) {
if ($selectionConfig.ContainsKey($app.Name)) { if ($selectionConfig.ContainsKey($app.Name)) {
# 转换为整数 1 0 # Convert to integer 1 or 0.
$app.Enabled = [int]$selectionConfig[$app.Name] $app.Enabled = [int]$selectionConfig[$app.Name]
} }
} }
@@ -54,16 +101,16 @@ if (Test-Path $selectionPath) {
} }
} }
# === 2.2 启用本地 Manifest 安装 === # === 2.2 Enable local manifest installation ===
# winget install --manifest 需要先启用 LocalManifestFiles,重复执行不会影响后续安装。 # winget install --manifest requires LocalManifestFiles. Re-running this is harmless.
Write-Host "Enabling winget local manifest support..." -ForegroundColor Cyan Write-Host "Enabling winget local manifest support..." -ForegroundColor Cyan
winget settings --enable LocalManifestFiles winget settings --enable LocalManifestFiles
# === 3. 主循环 === # === 3. Main loop ===
foreach ($app in $apps) { foreach ($app in $apps) {
if ($app.Enabled -ne 1) { if ($app.Enabled -ne 1) {
Write-Host "[Skipping] $($app.Name)" -ForegroundColor DarkGray Write-Host "[Skipping] $($app.Name)" -ForegroundColor DarkGray
continue # 立即结束本次循环,进入下一个软件 continue # Skip this app.
} }
Write-Host "`n==========================================" -ForegroundColor Cyan Write-Host "`n==========================================" -ForegroundColor Cyan
@@ -71,10 +118,10 @@ foreach ($app in $apps) {
Write-Host "==========================================" Write-Host "=========================================="
if ($app.Id -eq "System.Config") { if ($app.Id -eq "System.Config") {
# 如果是纯配置项,直接打印跳过信息 # This is a configuration-only item.
Write-Host "[System Config] Skipping install..." -ForegroundColor Magenta Write-Host "[System Config] Skipping install..." -ForegroundColor Magenta
} else { } else {
# --- 步骤 A: Winget 安装 --- # --- Step A: Winget install ---
$manifestPath = $null $manifestPath = $null
$manifestTempDir = $null $manifestTempDir = $null
@@ -126,8 +173,7 @@ foreach ($app in $apps) {
# "--scope", "machine" # "--scope", "machine"
) )
# [版本检查逻辑] # Add Version only when configured.
# 检查 Version 是否存在且不为空字符串
if (-not [string]::IsNullOrWhiteSpace($app.Version)) { if (-not [string]::IsNullOrWhiteSpace($app.Version)) {
Write-Host "-> Version: $($app.Version)" Write-Host "-> Version: $($app.Version)"
$wingetArgs += "-v" $wingetArgs += "-v"
@@ -139,36 +185,48 @@ foreach ($app in $apps) {
Write-Host "-> Installing via winget..." Write-Host "-> Installing via winget..."
# 执行安装 # Run install command.
try { try {
& winget @wingetArgs & winget @wingetArgs
$exitCode = $LASTEXITCODE $exitCode = $LASTEXITCODE
if ($exitCode -eq -1978335231 -and -not [string]::IsNullOrWhiteSpace($app.Manifest)) {
Write-Warning "winget failed after download. Unblocking cached installer and retrying once..."
Unblock-WinGetCache -PackageId $app.Id
& winget @wingetArgs
$exitCode = $LASTEXITCODE
}
} finally { } finally {
if ($manifestTempDir -and (Test-Path $manifestTempDir)) { if ($manifestTempDir -and (Test-Path $manifestTempDir)) {
Remove-Item -LiteralPath $manifestTempDir -Recurse -Force Remove-Item -LiteralPath $manifestTempDir -Recurse -Force
} }
} }
# 检查安装结果 (0=成功, -1978335189=已安装) # Check install result (0=success, -1978335189=already installed).
if ($exitCode -eq 0) { if ($exitCode -eq 0) {
Write-Host "[Success]" -ForegroundColor Green Write-Host "[Success]" -ForegroundColor Green
} elseif ($exitCode -eq -1978335189) { } elseif ($exitCode -eq -1978335189) {
Write-Host "[Skip] Already installed" -ForegroundColor Yellow if ($forcePostInstall) {
continue # 已经安装的为了避免覆盖配置,也就不配置了 Write-Host "[Skip] Already installed, running PostInstall..." -ForegroundColor Yellow
} else {
Write-Host "[Skip] Already installed" -ForegroundColor Yellow
continue # Skip PostInstall when already installed.
}
} else { } else {
Write-Error "[Fail] Error code: $exitCode" Write-Error "[Fail] Error code: $exitCode"
continue # 安装失败则跳过后续配置 continue # Skip PostInstall on install failure.
} }
} }
# --- 步骤 B: PostInstall 配置 --- # --- Step B: PostInstall configuration ---
if ($app.PostInstall -and $app.PostInstall.Count -gt 0) { if ($app.PostInstall -and $app.PostInstall.Count -gt 0) {
Write-Host "`n-> Configuring..." -ForegroundColor Cyan Write-Host "`n-> Configuring..." -ForegroundColor Cyan
$stepIndex = 0 $stepIndex = 0
foreach ($action in $app.PostInstall) { foreach ($action in $app.PostInstall) {
# [间隔逻辑] 如果这不是第一步,先等待 2 秒 # Wait between PostInstall steps.
if ($stepIndex -gt 0) { if ($stepIndex -gt 0) {
Write-Host "(Waiting 2 seconds...)" -ForegroundColor DarkGray Write-Host "(Waiting 2 seconds...)" -ForegroundColor DarkGray
Start-Sleep -Seconds 2 Start-Sleep -Seconds 2
@@ -177,20 +235,20 @@ foreach ($app in $apps) {
try { try {
switch ($action.Type) { switch ($action.Type) {
# 1. 复制文件 # 1. Copy file.
"FileCopy" { "FileCopy" {
& "$PSScriptRoot\..\lib\invoke-filecopy.ps1" ` & "$PSScriptRoot\..\lib\invoke-filecopy.ps1" `
-Source $action.Source ` -Source $action.Source `
-Destination $action.Destination -Destination $action.Destination
} }
# 2. 导入注册表 # 2. Import registry file.
"RegImport" { "RegImport" {
& "$PSScriptRoot\..\lib\invoke-regimport.ps1" ` & "$PSScriptRoot\..\lib\invoke-regimport.ps1" `
-Path $action.Path -Path $action.Path
} }
# 3. 执行 CMD # 3. Run command.
"Command" { "Command" {
& "$PSScriptRoot\..\lib\invoke-cmdexec.ps1" ` & "$PSScriptRoot\..\lib\invoke-cmdexec.ps1" `
-Command $action.Command ` -Command $action.Command `

View File

@@ -1,26 +1,25 @@
@echo off @echo off
:: ========================================== rem ==========================================
:: 自动化装机工具启动器 rem Winit Helper launcher
:: ========================================== rem ==========================================
:: 1. 强制切换到当前批处理文件所在的目录 rem Always run from the project directory.
:: 这一步至关重要,防止以管理员身份运行时路径变成了 C:\Windows\System32
cd /d "%~dp0" cd /d "%~dp0"
:: 2. 检查管理员权限 rem Relaunch as administrator when needed.
if not "%1" == "am_admin" ( if not "%1" == "am_admin" (
rem you'd better keep the following line as it is. rem you'd better keep the following line as it is.
powershell start -verb runas '%0' am_admin & exit /b powershell.exe -NoProfile -Command "Start-Process -FilePath '%~f0' -ArgumentList 'am_admin' -Verb RunAs"
exit /b
) )
:: ========================================== rem ==========================================
:: 3. 核心执行逻辑 rem Run main script
:: ========================================== rem ==========================================
echo Calling main.ps1... echo Calling main.ps1...
:: -NoProfile: 不加载用户配置文件,加快启动速度 rem Remove Zone.Identifier from copied or downloaded project files.
:: -ExecutionPolicy Bypass: 绕过默认的脚本执行策略限制 powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "Get-ChildItem -LiteralPath '%~dp0' -Recurse -File -Include *.ps1,*.psm1,*.bat,*.cmd,*.json,*.yaml,*.yml,*.reg | Unblock-File -ErrorAction SilentlyContinue"
:: -File: 指定要运行的脚本文件
powershell.exe -NoExit -NoProfile -ExecutionPolicy Bypass -File ".\bin\main.ps1" powershell.exe -NoExit -NoProfile -ExecutionPolicy Bypass -File ".\bin\main.ps1"

View File

@@ -3,36 +3,36 @@ param(
[string]$WorkDir = $null [string]$WorkDir = $null
) )
# 1. 处理工作目录 # 1. Resolve work directory.
if ([string]::IsNullOrEmpty($WorkDir)) { if ([string]::IsNullOrEmpty($WorkDir)) {
$WorkDir = $PSScriptRoot $WorkDir = $PSScriptRoot
} else { } else {
# 如果 WorkDir 里包含变量(如 $env:APPDATA),展开它 # Expand variables in WorkDir, such as $env:APPDATA.
$WorkDir = $ExecutionContext.InvokeCommand.ExpandString($WorkDir) $WorkDir = $ExecutionContext.InvokeCommand.ExpandString($WorkDir)
} }
# 确保目录存在,否则命令会报错 # Make sure the directory exists.
if (-not (Test-Path $WorkDir)) { if (-not (Test-Path $WorkDir)) {
Write-Warning "`n[CMD] WARNING: The work dir does not exist, using ($WorkDir)" Write-Warning "`n[CMD] WARNING: The work dir does not exist, using ($WorkDir)"
$WorkDir = $PSScriptRoot $WorkDir = $PSScriptRoot
} }
# 2. 处理命令中的环境变量 # 2. Expand variables in the command.
$ProjectRoot = Split-Path $PSScriptRoot -Parent $ProjectRoot = Split-Path $PSScriptRoot -Parent
# === 替换 $PSScriptRoot 为实际的绝对路径 === # Replace $PSScriptRoot with the project root path.
# 注意:要处理反斜杠转义问题,直接用字符串替换最安全 # Direct string replacement is the safest option for backslashes.
if ($Command.Contains('$PSScriptRoot')) { if ($Command.Contains('$PSScriptRoot')) {
$Command = $Command.Replace('$PSScriptRoot', $ProjectRoot) $Command = $Command.Replace('$PSScriptRoot', $ProjectRoot)
} }
# 这一步很关键,让你可以写 "echo $env:USERNAME" # This allows commands such as "echo $env:USERNAME".
$Command = $ExecutionContext.InvokeCommand.ExpandString($Command) $Command = $ExecutionContext.InvokeCommand.ExpandString($Command)
Write-Host "`n[CMD] Execute: $Command" -ForegroundColor Gray Write-Host "`n[CMD] Execute: $Command" -ForegroundColor Gray
# 3. 启动进程 # 3. Start cmd.exe.
# /c 表示执行完命令后关闭 cmd 窗口 # /c closes cmd after the command completes.
# /s 开启参数的一般处理(忽略第一个和最后一个引号,为了兼容复杂引号情况) # /s enables cmd quote handling for complex command strings.
$processOptions = @{ $processOptions = @{
FilePath = "cmd.exe" FilePath = "cmd.exe"
ArgumentList = "/s", "/c", "`"$Command`"" ArgumentList = "/s", "/c", "`"$Command`""
@@ -45,10 +45,10 @@ $processOptions = @{
try { try {
$proc = Start-Process @processOptions $proc = Start-Process @processOptions
# 4. 检查退出代码 (ExitCode) # 4. Check exit code.
if ($proc.ExitCode -ne 0) { if ($proc.ExitCode -ne 0) {
Write-Error "`n[CMD] Failed to execute, exit code: $($proc.ExitCode)" Write-Error "`n[CMD] Failed to execute, exit code: $($proc.ExitCode)"
} }
} catch { } catch {
Write-Error "`n[CMD] Failed to start process: $_" Write-Error "`n[CMD] Failed to start process: $_"
} }

View File

@@ -7,35 +7,33 @@ param(
) )
try { try {
# 1. 路径预处理:展开环境变量 (例如把 $env:APPDATA 变成 C:\Users\...\AppData\Roaming) # 1. Expand variables in paths.
$ResolvedSource = $ExecutionContext.InvokeCommand.ExpandString($Source) $ResolvedSource = $ExecutionContext.InvokeCommand.ExpandString($Source)
$ResolvedDest = $ExecutionContext.InvokeCommand.ExpandString($Destination) $ResolvedDest = $ExecutionContext.InvokeCommand.ExpandString($Destination)
# 2. 处理相对路径 (针对源文件) # 2. Resolve relative source paths from the project root.
# 如果源路径是相对路径 (./assets/...), 尝试将其转换为绝对路径
# 假设脚本是从项目根目录调用的
if (-not (Test-Path $ResolvedSource) -and (Test-Path "$PWD\$ResolvedSource")) { if (-not (Test-Path $ResolvedSource) -and (Test-Path "$PWD\$ResolvedSource")) {
$ResolvedSource = Join-Path $PWD $ResolvedSource $ResolvedSource = Join-Path $PWD $ResolvedSource
} }
# 3. 再次检查源文件是否存在 # 3. Make sure the source file exists.
if (-not (Test-Path $ResolvedSource -PathType Leaf)) { if (-not (Test-Path $ResolvedSource -PathType Leaf)) {
Write-Warning "`n[FileCopy] [Skip] File not found: $ResolvedSource" Write-Warning "`n[FileCopy] [Skip] File not found: $ResolvedSource"
return # 退出脚本 return
} }
# 4. 处理目标目录 (自动创建不存在的文件夹) # 4. Create destination directory when needed.
$DestDir = Split-Path -Path $ResolvedDest -Parent $DestDir = Split-Path -Path $ResolvedDest -Parent
if (-not (Test-Path $DestDir)) { if (-not (Test-Path $DestDir)) {
Write-Host "`n[FileCopy] Create dir: $DestDir" -ForegroundColor DarkGray Write-Host "`n[FileCopy] Create dir: $DestDir" -ForegroundColor DarkGray
New-Item -Path $DestDir -ItemType Directory -Force | Out-Null New-Item -Path $DestDir -ItemType Directory -Force | Out-Null
} }
# 5. 执行复制 (Force = 覆盖) # 5. Copy file. Force overwrites existing files.
Copy-Item -Path $ResolvedSource -Destination $ResolvedDest -Force -ErrorAction Stop Copy-Item -Path $ResolvedSource -Destination $ResolvedDest -Force -ErrorAction Stop
Write-Host "`n[FileCopy] Success: $ResolvedDest" -ForegroundColor Green Write-Host "`n[FileCopy] Success: $ResolvedDest" -ForegroundColor Green
} catch { } catch {
Write-Error "`n[FileCopy] Fail: $_" Write-Error "`n[FileCopy] Fail: $_"
} }

View File

@@ -4,17 +4,15 @@ param(
) )
try { try {
# 1. 路径预处理 # 1. Expand variables in the path.
# 展开环境变量 (虽然 .reg 路径通常是固定的,但支持一下没坏处)
$ResolvedPath = $ExecutionContext.InvokeCommand.ExpandString($Path) $ResolvedPath = $ExecutionContext.InvokeCommand.ExpandString($Path)
# 2. 处理相对路径 # 2. Resolve relative paths from the project root.
# 如果路径是相对的 (./assets/...), 转换为绝对路径
if (-not (Test-Path $ResolvedPath) -and (Test-Path "$PWD\$ResolvedPath")) { if (-not (Test-Path $ResolvedPath) -and (Test-Path "$PWD\$ResolvedPath")) {
$ResolvedPath = Join-Path $PWD $ResolvedPath $ResolvedPath = Join-Path $PWD $ResolvedPath
} }
# 3. 检查文件是否存在 # 3. Make sure the registry file exists.
if (-not (Test-Path $ResolvedPath)) { if (-not (Test-Path $ResolvedPath)) {
Write-Warning "`n[RegImport] [Skip] Cannot find: $ResolvedPath" Write-Warning "`n[RegImport] [Skip] Cannot find: $ResolvedPath"
return return
@@ -22,8 +20,7 @@ try {
Write-Host "`n[RegImport] Importing: $(Split-Path $ResolvedPath -Leaf)" -ForegroundColor Gray Write-Host "`n[RegImport] Importing: $(Split-Path $ResolvedPath -Leaf)" -ForegroundColor Gray
# 4. 调用 reg.exe 导入 # 4. Import with reg.exe and capture the exit code.
# 使用 Start-Process 以获取退出代码
$proc = Start-Process -FilePath "reg.exe" -ArgumentList "import", "`"$ResolvedPath`"" -Wait -PassThru -NoNewWindow $proc = Start-Process -FilePath "reg.exe" -ArgumentList "import", "`"$ResolvedPath`"" -Wait -PassThru -NoNewWindow
if ($proc.ExitCode -eq 0) { if ($proc.ExitCode -eq 0) {
@@ -34,4 +31,4 @@ try {
} catch { } catch {
Write-Error "`n[RegImport] Failed to start process: $_" Write-Error "`n[RegImport] Failed to start process: $_"
} }

View File

@@ -1,7 +1,7 @@
# yaml-language-server: $schema=https://aka.ms/winget-manifest.singleton.1.12.0.schema.json # yaml-language-server: $schema=https://aka.ms/winget-manifest.singleton.1.12.0.schema.json
PackageIdentifier: Google.Chrome PackageIdentifier: Google.Chrome
PackageVersion: 146.0.7680.178 PackageVersion: 148.0.7778.168
PackageLocale: zh-CN PackageLocale: zh-CN
Publisher: Google LLC Publisher: Google LLC
PackageName: Google Chrome PackageName: Google Chrome
@@ -29,11 +29,11 @@ FileExtensions:
ElevationRequirement: elevatesSelf ElevationRequirement: elevatesSelf
Installers: Installers:
- Architecture: x64 - Architecture: x64
InstallerUrl: https://dl.volan.top/winget-repo/manifests/g/Google/Chrome/146.0.7680.178/googlechromestandaloneenterprise64.msi InstallerUrl: https://dl.volan.top/winget-repo/manifests/g/Google/Chrome/148.0.7778.168/googlechromestandaloneenterprise64.msi
InstallerSha256: DB9C5E0519C83FBECFE5E5AC1FAC0FE44C15B3943C9611D52759FE474436D31E InstallerSha256: A42AEA4FBC79A1B2C5872FAA39414567AB9D79473626D2B4C58ED6A4FFA60386
ProductCode: '{6939CB9C-515D-372C-AF4A-BA8D6A40CC4B}' ProductCode: '{AB53BB27-D54C-3E8F-8EFF-C3B1068B5BA5}'
AppsAndFeaturesEntries: AppsAndFeaturesEntries:
- ProductCode: '{6939CB9C-515D-372C-AF4A-BA8D6A40CC4B}' - ProductCode: '{042C29DE-2EAE-34E1-A978-C2BE5AB65557}'
UpgradeCode: '{C1DFDF69-5945-32F2-A35E-EE94C99C7CF4}' UpgradeCode: '{C1DFDF69-5945-32F2-A35E-EE94C99C7CF4}'
ManifestType: singleton ManifestType: singleton
ManifestVersion: 1.12.0 ManifestVersion: 1.12.0

View File

@@ -1,3 +1,8 @@
[Options]
# 设为 0 表示已安装的软件不会进行设置
# 设为 1 表示已安装的软件也会进行设置
ForcePostInstall=0
[Apps] [Apps]
7-Zip=1 7-Zip=1
Google Chrome=1 Google Chrome=1