上周帮人重装一台笔记本,系统装完、等驱动的二十分钟里我发现,最麻烦的一步其实是想不起来原来装过什么。后来想起 winget 能导出软件清单,就先在自己这台 Win11 上跑了一遍,结果把原来的计划全改了。
导出来的清单只有三十条
这台机器上 winget list 跑了 8.57 秒,刷出 136 行条目。紧接着 winget export -o list.json,耗时 11.7 秒,文件 2319 字节,里面躺着 29 个 PackageIdentifier。
剩下的呢?命令行一边扫一边往下刷:
- 无法从任何源获得已安装的程序包: Windows 记事本
- 无法从任何源获得已安装的程序包: Windows 计算器
- 无法从任何源获得已安装的程序包: Microsoft 照片
- 无法从任何源获得已安装的程序包: WinRAR
- 无法从任何源获得已安装的程序包: 001pdfmenuex
这样的提示一共 103 条。
注意第四条那个 WinRAR。我确实装了 WinRAR,而且它也在能导出的 29 个里。之所以同时出现,是因为机器上有两个同名东西:一个是商店装的右键菜单扩展(MSIX\WinRAR.ShellExtension_1.0.0.2_x64__d9ma7nkbkv4rp),一个是真正的 RARLab.WinRAR 安装包。前者在 winget 源里对不上任何清单,就被丢掉了。
所以这份 json 的定位得先摆正:它不是备份。它只覆盖「winget 源里能找到 manifest 的桌面程序」。商店应用、MSIX 包、绿色版、还有自己下 exe 装的国产软件,全都不在里面。
带不带版本号,结果差很多
winget export 有两套写法,差别只在一个字段:
winget export -o list.json --accept-source-agreements→ 2319 字节,每个包只有 PackageIdentifier- 加
--include-versions→ 3205 字节,多一个"Version": "7.20.2",条目数还是 29
看着带版本更保险,实际上更坑。我拿 WinRAR 单独试了一条,版本号就填它当前装的 7.20.2:
找到已安装的现有包。正在尝试升级已安装的包...
找不到匹配的版本: 7.20.2
搜索失败: RARLab.WinRAR
退出码 0x8A150067。版本号明明是对的,还是过不去。原因是源里 RARLab.WinRAR 的可用版本已经走到 7.23.0,7.20.2 那份清单被挤下去了,import 按精确版本去要,要不到就整条放弃。
去掉版本,或者补一个 --ignore-versions,同一份清单就顺了:
已安装包版本。已取消安装。
已安装程序包: RARLab.WinRAR
两个包 2.3 秒,退出码 0。
我的结论是:导出时别加 --include-versions,导入时把 --ignore-versions 一起带上。旧版本号在新系统上本来也装不回来,pin 住它只会让整条清单更容易断。
装回还是全量升级,由 --no-upgrade 决定
这条我原本没注意。import 遇到已经装着的包,默认动作不是跳过,是去升级——输出的原话就是「找到已安装的现有包。正在尝试升级已安装的包...」。也就是说,重装完系统你跑一遍 import,等于顺手把所有能识别的软件全刷到最新版,不管你想不想。
要它就停在那儿,加 --no-upgrade,输出变成「已安装包版本。已取消安装。」,两个包 2.3 秒结束,退出码 0。
这事儿在重装当天挺重要。我这台机器上 winget list 的「可用」列里,WinRAR 是 7.20.2 对 7.23.0,微信是 3.9.9.43 对 3.9.12.57。刚装完系统、驱动还没配齐的时候把微信从 3.9.9 推到 3.9.12,出了问题你都分不清是新系统的事还是新版的事。所以我的做法是第一次导入带 --no-upgrade,先原样装回去,等系统稳定几天再单独 winget upgrade。
手写清单会撞 schema
想干净一点的人(比如我)会动心思:干脆手写一份自己的常用软件清单,比导出靠谱。这条路能走,但有三处必须对。
省掉 $schema 直接报:
JSON 文件未指定可识别的架构。
Sources 下面少了 SourceDetails:
Schema validation failed.
Error context: <root>[Sources][0] Description: Missing required property 'SourceDetails'.
SourceDetails 里的 Argument 写成 null 也不行:
Description: Value type not permitted by 'type' constraint.
export 自己生成的是个 URL 字符串,照抄就不会错。这两种 schema 错误的退出码都是 0x8A150036。
能用的最小写法:
{
"$schema": "https://aka.ms/winget-packages.schema.2.0.json",
"Sources": [
{
"SourceDetails": {
"Argument": "https://cdn.winget.microsoft.com/cache",
"Identifier": "Microsoft.Winget.Source_8wekyb3d8bbwe",
"Name": "winget",
"Type": "Microsoft.PreIndexed.Package"
},
"Packages": [
{ "PackageIdentifier": "RARLab.WinRAR" },
{ "PackageIdentifier": "Tencent.WeChat" }
]
}
]
}
清单里没有的只能手动补
重装完那些商店应用怎么办?记事本、计算器、照片这类,Win11 装完其实大都在,缺的自己点一下商店就行。别指望 winget 按名字搜回来:我试了 winget search --source msstore 记事本,前八条是「记事本本」「记事本笔记」「记事本日记」「记事本便签」「记事本手记」「记事本子」「记事本儿」「备忘录记事本」,全是第三方。要装商店里的正主,认商店页面链接末尾那串 ID,比按名字搜稳得多。
另外两条小坑,都是我自己踩的:
winget export -o不会建目录。指到一个不存在的文件夹,它会先老老实实扫完 2.8 秒,最后才甩一句0x80070003 : The system cannot find the path specified.- 同名文件直接覆盖,不打任何招呼。我第一次的测试清单写进 d.json,第二次 export 又指到 d.json,回头一看内容已经换成完整的 29 条,白测一轮。
还有一条关于 --ignore-unavailable:清单里混进一个源里查不到的包,比如某个已经下架的,import 会在那一条上打出「未找到程序包」,然后继续处理后面几条,不会中断,但整体退出码是非 0;加上 --ignore-unavailable,同样的清单退出码回到 0。写批处理脚本的话这个区别很要命,判 0 才往下走。
我现在重装当天的顺序
- 旧系统上跑
winget export -o D:\list.json --accept-source-agreements,不加--include-versions - list.json 丢网盘,顺手把浏览器书签、微信文件目录这些清单外的也单独存一份
- 新系统先确认
winget --version有输出。没输出说明 App Installer 没装或太老,先从商店补 winget import -i D:\list.json --ignore-unavailable --ignore-versions --no-upgrade --accept-package-agreements --accept-source-agreements,先原样装回,别顺手升级- 对着旧系统截图,把手动装的那几个补上
这套流程跑下来,能自动装回去的永远只是那三十条左右,剩下全靠 1 到 2 步骤里那份截图和记忆。所以我现在导出之后不删清单,每装一个新软件就顺手看一眼它在不在导出结果里——在的就算进了安全区,不在的自己记一笔。
你们重装系统之后,是靠什么把软件装回来的?