WinGet 进阶
基础篇解决的是「用 winget 装软件」。进阶篇解决的是这些:
- 把它塞进脚本跑(CI、计划任务、新机器初始化)
- 管住它的行为(源、作用域、默认版本、代理、实验功能)
- 出问题能自己查(日志、错误码、verbose)
- 仓库里搜不到你要的软件时,自己写一个 manifest 提交上去
目录
- 源管理
- 脚本化:四个必备参数与幂等
- 安装位置、作用域与安装器类型
- 版本锁定与机器迁移
- settings.json:把 winget 调成你想要的样子
- 代理与网络
- 日志与错误码
- 批量、CI 与 WinGet Configuration
- 自己写 manifest 并提交到 winget-pkgs
- 与本项目配合
- 排错清单
1. 源管理
winget source list
输出里的 Explicit 一列是重点:false 表示这个源会参与所有命令的搜索;true 表示只有显式 --source 指定它才会被搜索。winget-font 默认是 true,所以字体要单独装:
winget search --source winget-font --tag monospace
增删改查
winget source list # 看所有源
winget source list --name winget # 看某个源的详情
winget source update # 更新所有源的索引
winget source update --name winget # 只更新一个
winget source add --name mycorp `
--arg https://packages.mycorp.com/winget-source/ `
--type Microsoft.Rest `
--header "X-Api-Key: 你的密钥" `
--data-directory D:\winget-cache
winget source edit --name winget-font --explicit false # 改成参与全局搜索
winget source remove --name mycorp
winget source reset # 把默认源恢复出厂
参数说明
| 参数 | 作用 |
|---|---|
--arg | 源的地址(RST 源的 REST 端点) |
--type | 源类型:Microsoft.Rest(REST API)或 Microsoft.LocalFolder(本地文件夹) |
--header | 自定义 HTTP 头,私有源鉴权用 |
--data-directory | 本地缓存目录,留空则不缓存,每次联网查 |
--explicit | 是否显式源 |
私有源的三种做法
| 做法 | 适用 | 注意 |
|---|---|---|
Microsoft.LocalFolder + 本地目录 | 公司内网、离线环境、只用几个内部软件 | 目录结构必须和 winget-pkgs 一致 |
| 自建 REST 源 | 团队共享、要支持多客户端 | 得自己实现 REST 接口,工作量大 |
| 直接放一个本地 manifest | 只有一个软件要装 | 用 winget install --manifest,见第 9 节 |
本地目录源长这样(把 winget-pkgs 的某个子目录拷出来就能用):
winget source add --name internal --arg D:\winget-local --type Microsoft.LocalFolder
winget search --source internal foo
源出问题了怎么修
winget source update # 先试这个
winget source reset # 还不行就恢复默认源
0x8A15000A(索引损坏)、0x8A150009(索引版本比客户端新)这类错误,升级「应用安装程序」比删缓存更有效——前者是客户端太旧读不了新索引。
源的优先级(实验功能)
开启后,多源有同名包时,优先级高的源排在搜索结果前面、并优先被安装:
"experimentalFeatures": {
"sourcePriority": true
}
配合 winget source edit --name winget --priority 10。
这是实验性功能,行为可能随版本变化。别把它当成生产环境的依赖。
2. 脚本化:四个必备参数与幂等
在脚本和 CI 里跑 winget,默认是不安全的,因为它会停下来问你问题。一个无人值守的安装命令至少要加四个参数:
winget install --id Microsoft.VisualStudioCode -e `
--silent `
--accept-package-agreements `
--accept-source-agreements `
--disable-interactivity
| 参数 | 关掉了什么 |
|---|---|
--silent(-h) | 安装器弹出的所有 UI(进度条、结束提示) |
--accept-package-agreements | 「是否同意本软件许可协议」 |
--accept-source-agreements | 「是否同意本源的使用条款」 |
--disable-interactivity | winget 自己的一切提问(多结果时列清单、是否重启等) |
为什么 -e(--exact)不能省
不加 -e 时 winget 做的是子串匹配,命中多个就会列出来让你选。在交互式环境下这没问题(你能选),在 CI 里就是直接失败或装错东西。
用 --id X -e 把目标钉死,行为就确定了。
幂等:让脚本可以反复跑
# 已装了就跳过(哪怕版本旧)
winget install --id Python.Python.3.12 -e --no-upgrade `
--silent --accept-package-agreements --disable-interactivity
| 想要的语义 | 怎么写 |
|---|---|
| 没装就装,装了就跳过 | --no-upgrade |
| 一定要装最新版 | upgrade(配合 --force 可覆盖失败) |
| 强制重装同版本 | --allow-reinstall |
| 出错也继续 | 检查 $LASTEXITCODE,别让脚本被中断 |
| 顺带升所有 | winget upgrade --all --disable-interactivity |
退出码
winget 用退出码汇报结果,脚本里必须看:
| 退出码 | 含义 |
|---|---|
0 | 成功 |
0x8A150014 | 没找到匹配的包(ID 写错 / 源里没有) |
0x8A150016 | 匹配到多个包,无法确定目标 |
0x8A150010 | 没有适用于当前系统的安装器 |
0x8A150011 | 安装包哈希不匹配 |
0x8A150002 | 命令行参数不合法 |
0x8A150005 | 收到取消信号(Ctrl+C) |
注意退出码在 PowerShell 里是 16 进制十进制数,$LASTEXITCODE 拿到的是有符号十进制,0x8A150014 是负数。要判断就直接用具体的 exit 值比较,最稳的方式是:
winget install --id Foo.Bar -e --silent --disable-interactivity
switch ($LASTEXITCODE) {
0 { Write-Host 'OK' }
-1978335212 { Write-Warning '没找到这个包,检查 ID' } # 0x8A150014
-1978335210 { Write-Warning '匹配到多个包' } # 0x8A150016
default { Write-Error "winget 失败: $LASTEXITCODE" }
}
用 winget error <code> 查表比背数字靠谱得多:
winget error 0x8A150014
一个真正能用的批量安装脚本
#requires -Version 5.1
<#
.SYNOPSIS
用 winget 批量安装/补装软件,可重复执行。
.NOTES
跑法:powershell -ExecutionPolicy Bypass -File .\setup.ps1
#>
[CmdletBinding()]
param(
[switch]$Upgrade # 存在时改为「全部升级」
)
$ErrorActionPreference = 'Continue'
# 脚本里统一用这些参数
$wingetCommon = @(
'--exact'
'--silent'
'--accept-package-agreements'
'--accept-source-agreements'
'--disable-interactivity'
'--nowarn'
)
$packages = [ordered]@{
'Microsoft.VisualStudioCode' = '代码编辑器'
'Git.Git' = 'Git'
'GitHub.cli' = 'GitHub CLI'
'Microsoft.PowerShell' = 'PowerShell 7'
'Python.Python.3.12' = 'Python 3.12'
'NodeJS.LTS' = 'Node.js LTS'
'7zip.7zip' = '7-Zip'
'Microsoft.WindowsTerminal' = 'Windows Terminal'
'Notepad++.Notepad++' = 'Notepad++'
'Google.Chrome' = 'Chrome'
}
if ($Upgrade) {
Write-Host '==> 升级所有已安装软件' -ForegroundColor Cyan
winget upgrade --all @wingetCommon
exit $LASTEXITCODE
}
# 先同步一次源索引,避免装到过期列表
winget source update --accept-source-agreements
$failed = [System.Collections.Generic.List[string]]::new()
foreach ($id in $packages.Keys) {
$label = $packages[$id]
Write-Host "==> [$label] $id" -ForegroundColor Cyan
# --no-upgrade:已装则跳过,让脚本可以重复执行
winget install --id $id @wingetCommon --no-upgrade
if ($LASTEXITCODE -ne 0) {
$reason = switch ($LASTEXITCODE) {
-1978335212 { '源里没有这个包(检查 ID 拼写)' }
-1978335210 { '匹配到多个包,需要 -e 之外再限定源' }
-1978335216 { '没有适用于本机的安装器(架构不对?)' }
-1978335215 { '安装包哈希不匹配,先别用 --ignore-security-hash' }
default { "退出码 $LASTEXITCODE" }
}
Write-Warning " 失败:$label —— $reason"
$failed.Add("$id ($label)")
}
}
Write-Host ''
if ($failed.Count -gt 0) {
Write-Host "以下 ${($failed.Count)} 个包没装上:" -ForegroundColor Red
$failed | ForEach-Object { Write-Host " - $_" -ForegroundColor Red }
exit 1
}
Write-Host '全部安装完成。' -ForegroundColor Green
几个值得抄走的写法:
[ordered]@{}保存「ID → 中文名」,日志可读性大幅提升- 单个包失败不中断整批(不设
$ErrorActionPreference = 'Stop',并且逐个检查退出码) - 用
switch把退出码翻译成中文,最后汇总成一份「失败清单」而不是第一个错误就退出 - 先
source update再装,避免用过期索引解析出一个已经下线的版本
3. 安装位置、作用域与安装器类型
--scope:装给「我」还是「所有人」
winget install --id Microsoft.PowerToys -e --scope user # 只装当前用户
winget install --id Microsoft.PowerToys -e --scope machine # 全机器(要 UAC)
规则是硬性的:
manifest 里
Scope声明的安装器,不支持--location; manifest 里支持--location的安装器(通常portable/zip),只支持user作用域。
所以 --scope machine 和 --location 基本是互斥的。另外 user 作用域要求「以用户身份运行 winget」——在管理员 PowerShell 里 --scope user 可能直接失败。
--location 的前置条件
winget install --id Microsoft.PowerToys -e -l "D:\Tools\PowerToys"
只有 manifest 里 InstallerSwitches: Custom: InstallLocation: true 的包才支持 -l。 想知道支不支持:
winget show --id Microsoft.PowerToys
输出里会明确写是否支持自定义安装位置。社区里大多数包都不支持,这也是 Scoop 更适合装「绿色工具」的原因。
--installer-type:手动指定安装器类型
同一个包在 manifest 里可能同时提供 msi、exe、zip 几种安装器,winget 会按优先级自动挑:
winget install --id Some.App -e --installer-type zip
可选值:appx、burn、exe、font、inno、msi、msix、msstore、nullsoft、portable、wix、zip。
它的主要用途是排错:如果默认选的安装器在你机器上有问题,换一个类型常常能解决。
--architecture
winget install --id Python.Python.3.12 -e -a x64
值:x86、x64、arm、arm64。不写就取当前系统架构。如果你要在一台 x64 机器上装 x86 版,必须显式指定,否则会装错。
4. 版本锁定与机器迁移
export / import:把「我装了什么」导出来
winget export -o D:\backup\apps.json
winget export -o D:\backup\apps.json --include-versions # 连版本号一起导出
生成的文件长这样(schema 在 winget-cli 仓库的 schemas/JSON/packages):
{
"Sources": [
{
"Source": "winget",
"Arg": "https://cdn.winget.microsoft.com/cache",
"Type": "Microsoft.Rest"
}
],
"Packages": [
{ "PackageIdentifier": "Git.Git" },
{ "PackageIdentifier": "Microsoft.VisualStudioCode", "Version": "1.95.0" }
]
}
导入到新机器:
winget import -i D:\backup\apps.json
import 的行为要点:
- 不加
--include-versions导出的文件,import时装的是最新版,不是导出时的版本 - 装了 ID 相同的更高版本时,默认会跳过并警告,不会降级
- 源里已经没有的包会跳过并警告——导出文件不保证 100% 可导入,尤其是在包被下架或改名之后
想「无条件装成指定版本」:
winget import -i D:\backup\apps.json --ignore-versions # 忽略锁定版本,全部用最新
--ignore-versions 和「降级」是两件事:它是不要版本号,不是「装旧版」。要降级只能用 install --version。
pin:把这个包钉住,不要随便升
winget pin add --id Microsoft.PowerToys -e # 轻度:挡住 upgrade --all
winget pin add --id Microsoft.PowerToys -e --blocking # 重度:连 upgrade <包> 也挡
winget pin add --id Microsoft.PowerToys -e --version 0.70.* # 门控:锁版本/版本区间
winget pin list
winget pin remove --id Microsoft.PowerToys -e
winget pin reset # 预览会被清掉的 pin
winget pin reset --force # 真的清掉
三种 pin 的区别(这是官方定义):
| 类型 | 怎么加 | 效果 |
|---|---|---|
| Pinning | 默认 | 被 upgrade --all 排除,但 winget upgrade <包> 仍可升;upgrade --all --include-pinned 可以带上它 |
| Blocking | --blocking | upgrade --all 和 upgrade <包> 都挡,必须先解除 pin(--force 可强破) |
| Gating | --version 1.2.* | 锁死版本或版本区间;1.2.* 表示 1.2.0 到 1.2.x 都算合法 |
pin 只约束 winget。软件自己的自动更新(VS Code、Chrome 都有)不受 pin 影响,仍会自己升上去。
什么时候该用哪个
| 场景 | 做法 |
|---|---|
| 团队要「统一环境」 | export --include-versions + 版本控制的 JSON,提交进仓库 |
| 某个库在旧版本才正常 | pin add --version <版本> 锁住,别用 --blocking(太狠) |
| 生产/测试机 | pin add --blocking,防止任何人误升 |
5. settings.json:把 winget 调成你想要的样子
winget settings
会直接用系统默认的 JSON 编辑器打开配置文件。
文件位置
%LOCALAPPDATA%\Packages\Microsoft.DesktopAppInstaller_8wekyb3d8bbwe\LocalState\settings.json
如果你用的是自己编译的非商店版 winget,路径是
%LOCALAPPDATA%\Microsoft\WinGet\Settings\settings.json。
一份可直接使用的配置
{
"$schema": "https://aka.ms/winget-settings.schema.json",
"source": {
"autoUpdateIntervalInMinutes": 60
},
"installBehavior": {
"preferences": {
"scope": "user",
"locale": ["zh-CN", "en-US"]
}
},
"visual": {
"progressBar": "rainbow"
},
"network": {
"downloader": "do"
},
"experimentalFeatures": {
"installPortability": true
},
"telemetry": {
"disable": true
}
}
第一行的 $schema 一定要留着——有它的话 VS Code 才会给你自动补全和类型检查。
常用配置项
| 路径 | 作用 | 说明 |
|---|---|---|
source.autoUpdateIntervalInMinutes | 源索引自动更新间隔 | 0 = 关闭;只在用到该源时才检查 |
installBehavior.preferences.scope | 默认作用域 | user / machine,省得每次敲 --scope |
installBehavior.preferences.locale | 偏好语言列表 | 装多语言安装器时优先选哪个 |
installBehavior.preferences.architecture | 偏好架构 | 同上 |
installBehavior.preferences.installerType | 偏好安装器类型 | 配合 --installer-type 使用 |
visual.progressBar | 进度条样式 | accent(默认)/ rainbow / retro / sixel / disabled |
visual.enableSixels | 六种颜色真彩色输出 | 终端支持时好看 |
network.downloader | 下载器实现 | default / wininet / do(Delivery Optimization) |
telemetry.disable | 关闭遥测 | 尊重系统隐私设置;也可在此显式关闭 |
experimentalFeatures | 实验功能开关 | 见下 |
实验性功能
winget features list # 看当前版本有哪些实验功能
# 逐个开关(需要管理员)
winget settings --enable LocalManifestFiles
winget settings --disable LocalManifestFiles
几个实用的:
| 功能 | 作用 |
|---|---|
LocalManifestFiles | 允许 winget install --manifest / show --manifest 装本地 manifest(第 9 节要用) |
directMSI | 直接处理 MSI,减少对 msiexec 的依赖 |
resume | 某些命令在重启后继续执行 |
fonts | winget font list 之类的字体相关能力 |
sourcePriority | 源优先级 |
installPortability | 便携版相关的安装行为 |
实验功能会随版本增删,不要把生产环境的可用性押在实验功能上。
downloader 选哪个
| 值 | 特点 |
|---|---|
default | 让 winget 自己判断 |
wininet | 走系统 WinINet,代理行为和浏览器一致,适合公司环境 |
do | 走 Delivery Optimization,局域网内多台机器能共享下载同一个安装包 |
内网批量装机用 do 能省不少带宽。
6. 代理与网络
winget install --id Foo.Bar -e --proxy http://127.0.0.1:7890
winget install --id Foo.Bar -e --no-proxy # 本次强制不走代理
--proxy 只影响这一次执行,不会写进配置。
三层代理,依次排查
# 1. winget 自己能不能出去
winget source update
# 2. 系统代理有没有生效
netsh winhttp show proxy
# 3. 域名能不能解析 / 连通
Test-NetConnection cdn.winget.microsoft.com -Port 443
netsh winhttp 显示「Direct access」而浏览器能上网,说明系统 WinINet 代理没配,此时 winget(默认走 WinINet)就上不去。解决方案:
- 在「设置 → 网络和 Internet → 代理」里配系统代理(会同步到 WinINet);
- 或者在 settings.json 里把
network.downloader改成do(不走 WinINet); - 或者干脆每次带
--proxy。
常见网络报错
| 现象 | 原因 |
|---|---|
0x801901a0 | HTTP 2.0 响应头不合法,典型是中间设备(代理 / 防火墙)篡改 |
0x80d03002 | 证书链验证失败:代理做了 SSL 中间人、证书没装全、或开了 __ 拦截 |
0x80070490 | 安装组件本身出问题(依赖 / 系统文件) |
这几个是 winget 官方 troubleshooting 文档点名的常见错误。
winget download 做离线
# 联网机器下好
winget download --id Microsoft.VisualStudioCode -d D:\offline
winget download --id Microsoft.VisualStudioCode -d D:\offline --architecture x64
# 内网机器离线装
winget install --id Microsoft.VisualStudioCode -e `
--manifest D:\offline\Microsoft.VisualStudioCode.yaml
离线安装走的是
--manifest:从download出来的包会带一个.yaml,里面已经填好了 SHA256,所以离线安装依然能通过哈希校验。这正是download存在的意义。
7. 日志与错误码
找日志
winget --logs # 打开日志目录
路径:
%LOCALAPPDATA%\Packages\Microsoft.DesktopAppInstaller_8wekyb3d8bbwe\LocalState\DiagOutputDir\
每次执行生成一个 时间戳-命令名-进程ID.log。报 bug 给别人时,把这个文件贴上去,里面什么都有。
两种日志级别
winget install --id Foo.Bar -e --verbose # 本次生成详细日志
winget install --id Foo.Bar -e -o D:\a.log # 本次写到指定文件
详细日志里会记录源返回的原始响应、哈希计算过程、安装器退出码——排查「为什么匹配到 0 个包」这类问题时非常关键。
错误码速查
用 winget error <hex> 可以随时查,下面是常用的一部分:
| 错误码 | 符号 | 含义 |
|---|---|---|
0x8A150001 | INTERNAL_ERROR | winget 内部错误,通常要升级客户端 |
0x8A150002 | INVALID_CLI_ARGUMENTS | 命令行参数不合法 |
0x8A150003 | COMMAND_FAILED | 命令执行失败 |
0x8A150004 | MANIFEST_FAILED | 打开/解析 manifest 失败 |
0x8A150005 | CTRL_SIGNAL_RECEIVED | 被 Ctrl+C 取消 |
0x8A150006 | SHELLEXEC_INSTALL_FAILED | 调用安装器失败 |
0x8A150007 | UNSUPPORTED_MANIFESTVERSION | manifest 版本比你的客户端新 → 升级「应用安装程序」 |
0x8A150008 | DOWNLOAD_FAILED | 下载安装包失败 |
0x8A150009 | CANNOT_WRITE_TO_UPLEVEL_INDEX | 索引 schema 比客户端新,同上:升级客户端 |
0x8A15000A | INDEX_INTEGRITY_COMPROMISED | 本地索引损坏 |
0x8A15000B | SOURCES_INVALID | 源配置损坏 |
0x8A15000C | SOURCE_NAME_ALREADY_EXISTS | 源名重复 |
0x8A15000D | INVALID_SOURCE_TYPE | 源类型不合法 |
0x8A15000F | SOURCE_DATA_MISSING | 源数据缺失 |
0x8A150010 | NO_APPLICABLE_INSTALLER | 没有适用于本机的安装器(架构/系统不匹配) |
0x8A150011 | INSTALLER_HASH_MISMATCH | 安装包哈希与 manifest 不符 |
0x8A150012 | SOURCE_NAME_DOES_NOT_EXIST | 源不存在 |
0x8A150013 | SOURCE_ARG_ALREADY_EXISTS | 同一个地址已被另一个名字占用 |
0x8A150014 | NO_APPLICATIONS_FOUND | 没找到匹配的包 |
0x8A150015 | NO_SOURCES_DEFINED | 没有任何源 |
0x8A150016 | MULTIPLE_APPLICATIONS_FOUND | 匹配到多个包 |
0x8A150017 | NO_MANIFEST_FOUND | 找不到 manifest |
0x8A15001C | MSSTORE_APP_BLOCKED_BY_POLICY | 商店被组策略挡了 |
0x8A15001D | EXPERIMENTAL_FEATURE_DISABLED | 实验功能没开 |
0x8A15C005 | CONFIG_ERROR_SET_APPLY_FAILED | winget configure 应用失败 |
0x8A15C006 | CONFIG_ERROR_DUPLICATE_IDENTIFIER | 配置文件里有重复 ID |
hash 不匹配(0x8A150011)要认真对待:它意味着「你下到的安装包和 manifest 里声明的那个不是同一个文件」。常见原因是软件官网换了安装包但没换版本号、下载被 CDN 截断、或者中间人攻击。
处理顺序:① 加 --verbose 看详细日志;② 换个网络/清 CDN 缓存重试;③ 确认软件官网是不是重新上传过安装包;④ 只有在人工确认过安装包来源可信时,才考虑 --ignore-security-hash,并且永远不要在生产/CI 里长期开着。
非 winget 的错误码
winget 也会转发 MSI 和 MSIX 的原始错误码,用 winget error 一样能查:
0x80070005:权限不足(需要管理员)0x80073D02:安装源被打开,程序在使用中 → 关掉那个软件再装0x80070490:组件未正确配置0x8A150109相关场景:需要重启才能继续
8. 批量、CI 与 WinGet Configuration
需求 → 工具
| 需求 | 用什么 |
|---|---|
| 一台机器装十来个软件 | winget install A B C(一次多 ID) |
| 迁移整机软件清单 | export / import |
| 团队统一开发环境(含系统设置) | winget configure |
| 定时升级所有软件 | 计划任务 + winget upgrade --all |
| 企业批量管控 | Intune / GPO 推 winget configure |
| 反复测试 manifest | Windows Sandbox + SandboxTest.ps1 |
计划任务:每天自动升级
# 每天凌晨 3 点升级全部已装软件
$action = New-ScheduledTaskAction -Execute 'winget.exe' `
-Argument 'upgrade --all --silent --accept-package-agreements --accept-source-agreements --disable-interactivity'
$trigger = New-ScheduledTaskTrigger -Daily -At 3am
$principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -RunLevel Highest
Register-ScheduledTask -TaskName 'winget-daily-upgrade' `
-Action $action -Trigger $trigger -Principal $principal `
-Description '每日自动升级所有 winget 管理的软件'
用 SYSTEM 跑是为了拿到最高权限(省掉 UAC),代价是装的是全机器范围。个人机器上更建议用 --scope user 的配置。
WinGet Configuration:winget configure
import 只能管「装哪些包」;configure 能把软件、版本、系统设置、注册表、服务一起声明,是真正的「声明式环境搭建」。需要 winget ≥ v1.6.2631。
文件长什么样
文件名约定用 .winget 扩展名;放在 Git 项目里推荐放 ./.config/configuration.winget。
# yaml-language-server: $schema=https://aka.ms/configuration-dsc-schema/0.2
properties:
assertions:
- resource: Microsoft.Windows.Developer/OsVersion
directives:
description: 校验系统版本下限
allowPrerelease: true
settings:
MinVersion: '10.0.22000'
resources:
- resource: Microsoft.Windows.Settings/WindowsSettings
directives:
description: 打开开发者模式
allowPrerelease: true
securityContext: elevated
settings:
DeveloperMode: true
- resource: Microsoft.WinGet.DSC/WinGetPackage
id: vsCode
directives:
description: 安装 VS Code
securityContext: elevated
settings:
id: Microsoft.VisualStudioCode
source: winget
- resource: Microsoft.WinGet.DSC/WinGetPackage
id: node
dependsOn:
- vsCode
directives:
description: 安装 Node.js LTS
settings:
id: NodeJS.LTS
source: winget
Pin: true
configurationVersion: 0.2.0
结构说明
| 节点 | 作用 |
|---|---|
properties.assertions | 前置断言。断言失败 → 依赖它的资源被跳过(不算整体失败) |
properties.resources | 要做的事,每条对应一个 PowerShell DSC 资源 |
resource | 格式固定为 模块名/DSC资源名,winget 会自动从 PowerShell Gallery 下载该模块 |
directives.description | 干什么用的(日志里能看到) |
directives.securityContext: elevated | 需要管理员。一次配置只会弹一次 UAC,winget 会起两个进程分别跑提权 / 非提权的部分 |
directives.allowPrerelease | 允许装预发布版模块 |
settings | 传给 DSC 资源的参数 |
id | 给这条资源起个名字,供 dependsOn 引用 |
dependsOn | 依赖。顺序不重要——DSC 是声明式的,会自动算执行顺序,部分任务还能并行 |
WinGetPackage 这个资源常用 settings:id、source、Pin、Scope、Type、Version、Architecture、SecurityHash、Dependencies。
运行与排查
winget configure show -f .config\configuration.winget # 看这个文件要干什么(★ 先看再跑)
winget configure validate -f .config\configuration.winget
winget configure list # 历史上应用过哪些配置
winget configure test -f .config\configuration.winget # 对比当前状态和目标状态
winget configure export -o out.winget # 把当前状态导出成配置文件
show 一定要在 configure 之前跑一次。configure 能执行任意 PowerShell DSC 资源(理论上能做任何事),微软官方明确警告过:不要运行你审阅过、且确认可信的配置文件。
${WinGetConfigRoot} 变量指向运行 configure 时的工作目录,配置里引用同目录的文件时用它,就不用写死绝对路径:
settings:
vsConfigFile: '${WinGetConfigRoot}\..\.vsconfig'
和 Scoop / Chocolatey 共存
没有理由只用一种:
| 工具 | 装什么 |
|---|---|
| winget | 桌面软件、官方安装器、MSIX 商店应用、字体 |
| Scoop | 命令行工具、便携版、免管理员、用户级隔离 |
| Chocolatey | 企业管控、需要商业功能(付费版)时的老牌方案 |
冲突的常见场景只有一个:同一个软件两个包管理器都装了。winget list 和 winget upgrade 看到的是「已经装了所以跳过」,真正的问题在卸载时(两边的卸载记录可能对不上)。做法是固定用一个工具管一个软件。
9. 自己写 manifest 并提交到 winget-pkgs
当 winget search 搜不到你要的软件时,正确的做法不是「放弃」,而是给它写一个 manifest 提交到社区仓库。这对全世界的用户都有用,也是 winget 社区最主要的贡献方式。
manifest 是什么
一个 YAML 文件,描述「这个软件是什么、下载地址、哈希是多少、怎么静默安装、怎么卸载」。
winget-pkgs 仓库里长这样:
manifests/
└── m/ ← 发布者首字母的小写
└── Microsoft/ ← 发布者
└── Edge/ ← 软件名
└── 130.0.2849.80/ ← 版本号
├── Microsoft.Edge.installer.yaml
├── Microsoft.Edge.locale.en-US.yaml (如有)
└── Microsoft.Edge.license.yaml (如有)
路径规则:
manifests/<发布者首字母小写>/<发布者>/<软件>/<版本>/一个 PR 只能提一个版本的一个包。 顺带改 README、拼写、工具脚本的,要单独开一个 PR。
一份真实的 manifest
这是 Edge 130 的 installer manifest(原文,未改动):
# Created using wingetcreate 1.6.5.0
# yaml-language-server: $schema=https://aka.ms/winget-manifest.installer.1.6.0.schema.json
PackageIdentifier: Microsoft.Edge
PackageVersion: 130.0.2849.80
InstallerLocale: en-US
Platform:
- Windows.Desktop
InstallerType: msi
Scope: machine
InstallModes:
- interactive
- silent
- silentWithProgress
UpgradeBehavior: install
Commands:
- msedge
Protocols:
- http
- https
FileExtensions:
- crx
- htm
- html
- pdf
- url
AppsAndFeaturesEntries:
- UpgradeCode: '{883C2625-37F7-357F-A0F4-DFAF391B2B9C}'
Installers:
- Architecture: x86
InstallerUrl: https://msedge.sf.dl.delivery.mp.microsoft.com/filestreamingservice/files/5eafcbf7-.../MicrosoftEdgeEnterpriseX86.msi
InstallerSha256: 6027D2AAD7BDBB0515CAF68230C942FFF732A433271AEBBD7F6414560331A320
ProductCode: '{5077DDBB-F9B5-32C6-92DB-F1FFDAF82CE5}'
- Architecture: x64
InstallerUrl: https://.../MicrosoftEdgeEnterpriseX64.msi
InstallerSha256: 84BA997B57BB7B4EE4222816D2202D090F8002874A3FBA6EF54F11A74CFF1BD4
ProductCode: '{414F64AB-22EF-39CD-A991-5BA905E30C30}'
ManifestType: installer
ManifestVersion: 1.6.0
顶层字段
| 字段 | 必填 | 说明 |
|---|---|---|
PackageIdentifier | ✅ | 就是 ID,格式 发布者.软件名,全局唯一 |
PackageVersion | ✅ | 版本号,必须和下载文件里的实际版本一致 |
Publisher | 发布者官网 | |
PublisherUrl | 发布者主页 | |
InstallerType | ✅ | 安装器类型,见下表 |
Scope | user / machine,决定能不能用 --location | |
Platform | ✅ | Windows.Desktop / Windows.Universal |
InstallerLocale | 只在需要区分语言包时写(多语言安装器) | |
InstallModes | interactive / silent / silentWithProgress | |
InstallerSwitches | 静默参数怎么传 | |
UpgradeBehavior | install(直接覆盖装)/ uninstallPrevious / installThenUninstall | |
Commands | 装完能敲的命令名,决定 winget search --command 能不能搜到 | |
FileExtensions / Protocols | 注册的文件类型 / 协议 | |
AppsAndFeaturesEntries | 卸载、检测版本、升级全靠它 | |
Dependencies | 依赖的 Windows 版本 / 功能 | |
ManifestType / ManifestVersion | ✅ | 固定 installer + 当前 schema 版本 |
InstallerType 与静默参数对照
| InstallerType | 装什么 | 静默参数写法(InstallerSwitches) |
|---|---|---|
msi | MSI 包 | Silent: /quiet,SilentWithProgress: /quiet /norestart |
wix | WiX 打包的 exe | /quiet、/quiet /norestart |
burn | 引导程序(bootstrapper) | /quiet、/quiet /norestart |
inno | Inno Setup 打的 exe | /SP- /VERYSILENT /SUPPRESSMSGBOXES /NORESTART |
nullsoft | NSIS 打的 exe | /S |
exe | 其他 exe | 必须自己写死参数;Silent / SilentWithProgress 两套 |
msix / appx | 商店格式 | 通常无需参数 |
msstore | 商店源包 | 传 Store 产品 ID |
zip | 压缩包 | 需要 Archive.NestedInstallerFiles 指定里面哪个文件 |
portable | 绿色版 | 同 zip,加 Archive.ArchiveBinaries 生成 shim |
font | 字体 | — |
inno 和 nullsoft 是最有价值的两条:因为 winget 能「认识」这两种安装器,你甚至不用写任何静默参数,winget 也会自动加上正确的那一套。这就是为什么同一个软件,「有人写了 manifest」和「没写」体验差距巨大。
Archive 相关(用于 zip / portable):
Archive:
ArchiveBinaries: rg.exe # 生成一个 shim,让 `rg` 命令到处都能用
NestedInstallerFiles:
- RelativeFilePath: rg.exe # 装完后主程序位置
AppsAndFeaturesEntries:别偷懒
这个字段决定了三件事:能不能被 winget list 认出来、能不能干净地卸载、升级时怎么判断版本。
常见组合:
AppsAndFeaturesEntries:
- DisplayName: Some App 1.0
DisplayVersion: 1.0.0
ProductCode: '{GUID-OR-MSI-CODE}' # MSI 类必填
UpgradeCode: '{GUID-OR-MSI-CODE}' # 决定「这是同一个软件的新版本」
InstallerType: exe
DisplayIcon: SomeApp.exe
Publisher: Some Publisher
UninstallString: '"C:\Program Files\SomeApp\unins000.exe" /SILENT'
QuietUninstallString: '"C:\Program Files\SomeApp\unins000.exe" /VERYSILENT'
UpgradeCode 写错是 PR 被退回的高频原因——它必须和注册表里真实的一致。
怎么拿到这些值
| 需要的信息 | 怎么查 |
|---|---|
| 安装包 SHA256 | winget hash --file .\setup.exe |
| 静默参数 | 查该安装器类型的官方文档(Inno / NSIS / WiX 官网) |
ProductCode / UpgradeCode | 装完之后从「程序和功能」→ 右键属性 / 或用 Get-ItemProperty 读卸载注册表项 |
| 版本号 | 文件属性 → 详细信息,或安装后从注册表读 DisplayVersion |
工具:WinGetCreate
手写 YAML 很累,官方有工具:
winget install wingetcreate
WinGetCreate 的常用命令:
wingetcreate sources # 装它的依赖(winget-cli、YamlCreate 等)
wingetcreate new C:\Downloads\setup.exe # 扫安装包,生成 manifest 骨架
wingetcreate update Publisher AppName # 为已有包生成「新版本」的 manifest
wingetcreate show Publisher AppName # 导出仓库里现有的 manifest
wingetcreate export C:\Downloads\app.zip # 导出成你写的格式
wingetcreate submit .\manifests\... # 直接帮你开 PR(需要 GitHub 认证)
社区还有个非常好用的 GUI 工具 Komac,可以点点选选生成 manifest,也能直接提 PR。
建议流程:wingetcreate new 生成骨架 → 手工补 AppsAndFeaturesEntries 和静默参数 → 本地验证 → 提交。
本地验证
前置条件:本地 manifest 功能默认是关闭的,需要在管理员 PowerShell 里开一次:
winget settings --enable LocalManifestFiles
# 1. 语法 + schema 校验
winget validate --manifest .\Microsoft.SomeApp.installer.yaml
# 2. 看 winget 自己怎么理解这个 manifest
winget show --manifest .\Microsoft.SomeApp.installer.yaml
# 3. 真装一次
winget install --manifest .\Microsoft.SomeApp.installer.yaml
强烈建议开详细日志:
winget install --manifest .\manifest.yaml --verbose
validate 只检查格式和schema,它不检查你的静默参数对不对、不检查 URL 还能不能下载。所以第 3 步的实装测试才是真正的验证。
在 Windows Sandbox 里测
仓库自带一个测试脚本,在一次性的干净环境里跑安装,不污染你的机器:
git clone https://github.com/microsoft/winget-pkgs
cd winget-pkgs
.\Tools\SandboxTest.ps1 <manifest 路径>
它会开一个 Windows Sandbox、装 winget、校验 manifest、尝试安装、再把结果贴回来。提交 PR 之前跑一次,能挡掉绝大多数「在我机器上好好的」。
提交 PR
- Fork
microsoft/winget-pkgs - 按目录结构放文件:
manifests/<首字母小写>/<发布者>/<软件>/<版本>/ - 一个 PR 只放一个版本的一个包(其他改动另开 PR)
- 提交前确认:仓库里还没有这个软件的 manifest、也没有人在开着的 PR
- 开 PR,描述里写清来源、下载地址、静默参数、测试情况
校验流程
GitHub 上的机器人(BOT)和核心团队会打 label 来标记进度。校验失败时 BOT 会指出问题并把 PR 退回给你。流程里有两个时间点值得注意:
5 天没有任何活动 → 打上
No-Recent-Activity标签;再过 3 天仍无活动 → PR 被自动关闭。
常见被退回的原因
| 问题 | 说明 |
|---|---|
| 同一个 PR 提了多个版本 | 规则硬性要求一 PR 一版本 |
| 顺带改了非 manifest 文件 | README / 工具脚本的改动必须单独提 PR |
InstallerSha256 算错 | 用 winget hash 重算,别手抄 |
| 静默参数没验证 | 必须实装测试过(--silent 真的不弹窗) |
UpgradeCode / ProductCode 不对 | 与注册表实际值不一致 |
Scope 与 --location 兼容性问题 | 见第 3 节的规则 |
| 软件没有官网 / 域名可疑 | 仓库有政策限制 |
| 安装包是「每次下载都不一样」 | 有些站的安装器是动态生成的,需要先下载下来再上传或换源 |
| 复用他人 manifest | 同一个包的 manifest 只能有一个来源 |
有价值的可选字段(如 Publisher、Tags、License、ReleaseNotes、图标等)受仓库政策限制,其中一部分需要**「已验证开发者」流程才能提交。所以新 PR 老实写必填字段就行**,别为了好看去硬填。
提新版本
软件更新后,给同一个包加一个新版本目录:
manifests/m/Microsoft/Edge/131.0.0.0/Microsoft.Edge.installer.yaml
同样一个 PR 一个版本。老版本目录不要删——pin --version 和 --version 装旧版本时还要靠它。
10. 与本项目配合
本站的自动化脚本(DrissionPage / ruyipage 等)常和 winget 配合用在两处:
1. 测试环境初始化
自动化脚本要跑的机器上,浏览器、驱动、Python 依赖都得先在。用 winget 装,比在文档里写一堆「请先下载并安装……」可靠得多:
# 一台干净的 CI/测试机
winget install -e --id Python.Python.3.12 --silent --accept-package-agreements
winget install -e --id Google.Chrome --silent --accept-package-agreements
winget install -e --id Google.ChromeDriver --silent --accept-package-agreements
pip install DrissionPage ruyipage
2. 驱动版本对不上的兜底
DrissionPage 这类库依赖特定版本的 Chrome/Edge。跑之前可以用 winget 校验浏览器版本:
$edge = winget list --id Microsoft.Edge -e
$edge.Version # 和脚本里期望的版本比对
if ($edge.Version -notlike '13*.0*') {
Write-Warning "Edge 版本不符合预期:$($edge.Version)"
winget upgrade --id Microsoft.Edge -e --silent --accept-package-agreements
}
3. 与油猴脚本配合
本站的油猴文档 讲的是「在浏览器里注入代码」。如果你在自动化流程里需要加载某个用户脚本,可以:
- 用 winget 把脚本依赖的环境装好(Node、浏览器);
- 脚本本身存成
.user.js文件,由自动化脚本读取后注入页面(相当于@grant none的无沙箱模式,能用的GM_*API 有限)。
分工原则:一次性、可编程的逻辑放 Python 侧;长期复用、人机共用的页面增强放油猴脚本;环境依赖和版本校验交给 winget。
11. 排错清单
| 症状 | 根因 | 处理 |
|---|---|---|
0x8A150007 / 0x8A150009 | 索引或 manifest 版本比客户端新 | 更新「应用安装程序」 |
0x8A15000A 索引损坏 | 本地索引坏了 | winget source reset;仍不行就更新客户端 |
0x8A150011 哈希不匹配 | 安装包与 manifest 不符 | --verbose 看详情;确认软件是否重传过安装包;不要随手 --ignore-security-hash |
0x8A150010 无适用安装器 | 架构/系统不匹配 | -a x64;或换 --installer-type |
0x8A150014 找不到包 | ID 写错 / 包被下架 | winget search 核对 ID;看仓库有没有删除该包 |
0x8A150016 多个匹配 | 多源重名 | --id X -e -s winget |
0x8A15001D 实验功能未开 | 用了需要开关的命令 | 管理员 winget settings --enable <功能> |
0x80070005 权限不足 | 需要管理员 | 用管理员 PowerShell,或 --scope user |
0x80073D02 源被占用 | 目标软件正在运行 | 关掉那个软件再装 |
0x801901a0 / 0x80d03002 | 代理/防火墙篡改或 SSL 中间人 | 配好系统代理;装好根证书;--no-proxy 试一下 |
winget list 有、uninstall 找不到 | 多源同名 | 加 -s 限定源 |
| 装了但 PATH 里没有 | 安装器没改环境变量 | 新开一个终端再看 |
| 静默安装仍弹窗 | manifest 没写静默参数 / 安装器不支持 | 用 --interactive 装一次确认行为;这是 manifest 的质量问题,提 PR 补上 |
--scope user 失败 | 在管理员会话里跑的 | 换成普通用户 PowerShell |
-l 指定目录无效 | 包不支持自定义位置 | winget show 确认;这类包改用 Scoop |
| 装完还是提示可升级 | 预览版 / 索引旧 / 源不一致 | source update;winget show 看版本后缀;pin add --version 锁住 |
| 升级后功能变了 | 上游改了默认行为 | pin add --version <已知良好版本> |
import 报一堆警告 | 源里已无对应包 | 用 export --include-versions 并把 JSON 提交进版本库,定期(比如每月)重新导出 |
configure 部分资源没跑 | 断言失败导致依赖被跳过 | winget configure show 看断言;configure test 看差异 |
排错的通用顺序:
winget --version # 1. 客户端是不是太旧(最常见根因)
winget --info # 2. 源对不对、有没有被组策略改过
winget source update # 3. 索引刷新
winget error <错误码> # 4. 读懂报错
<原命令> --verbose # 5. 看详细日志
90% 的 winget「玄学问题」最后都落在第 1 步和第 3 步。 先更新客户端,再刷新索引,然后才开始怀疑别的。
参考
- 官方文档:Use WinGet to install and manage applications
- 错误码全集:Return Codes
- manifest 规范:winget-pkgs 文档
- 客户端源码 / 提 issue:microsoft/winget-cli
- 常见问题:Troubleshooting