Nuget包迁移指南
把 NuGet 包缓存从 C 盘迁移到 D 盘(VS2026 / .NET 实战指南)
适用于 Windows + Visual Studio 2022/2026 + .NET SDK 环境
迁移完成后:NuGet 包全部进 D 盘,C 盘零回写,VS/dotnet 命令行都遵循配置
一、为什么要迁移
NuGet 默认把全局包缓存放在 C 盘两个位置:
类型 默认路径 作用
global-packages C:\Users<用户>.nuget\packages\ restore 时下载的包本体(dll 等)
http-cache C:\Users<用户>\AppData\Local\NuGet\v3-cache\ 包索引的 HTTP 缓存
项目一多,这两个目录轻松突破 1 GB。C 盘是系统盘,空间宝贵,把缓存挪到 D 盘能:
- ✅ 释放 C 盘几个 GB 空间
- ✅ 重装系统时 NuGet 缓存不会丢(在 D 盘)
- ✅ 系统盘只放系统,数据盘放开发资源,分工清晰
二、迁移前的现状检查
先看看 C 盘 NuGet 缓存占多大:
\(pkgs = "\)env:USERPROFILE.nuget\packages"
\(http = "\)env:LOCALAPPDATA\NuGet\v3-cache"
foreach (\(p in @(\)pkgs, $http)) {
if (Test-Path $p) {
$size = (Get-ChildItem $p -Recurse -File | Measure-Object Length -Sum).Sum / 1MB
Write-Host ("{0,8:N2} MB {1}" -f $size, $p)
}
}
我机器上的结果:
916.47 MB C:\Users\liuxin.nuget\packages
172.46 MB C:\Users\liuxin\AppData\Local\NuGet\v3-cache
合计约 1.09 GB,确实值得搬。
再看当前 dotnet 认为这些缓存在哪:
dotnet nuget locals all --list
http-cache: C:\Users\liuxin\AppData\Local\NuGet\v3-cache
global-packages: C:\Users\liuxin.nuget\packages
temp: C:\Users\liuxin\AppData\Local\Temp\NuGetScratch
plugins-cache: C:\Users\liuxin\AppData\Local\NuGet\plugins-cache
确认都指向 C 盘,开始迁移。
三、配置方案:两层保险
NuGet 的全局包路径有两种配置方式,建议两层都设,互相兜底:
第 1 层:用户级环境变量(推荐,长期生效)
变量名 值
NUGET_PACKAGES D:\NuGet\packages
NUGET_HTTP_CACHE_PATH D:\NuGet\http-cache
环境变量是最高优先级的配置方式,对所有走 NuGet 链路的工具(dotnet CLI、VS、msbuild)都生效。
第 2 层:用户级 NuGet.Config(兜底)
文件路径:%APPDATA%\NuGet\NuGet.Config
⚠️ 关键点:http-cache 没有 NuGet.Config 配置项,只能通过环境变量 NUGET_HTTP_CACHE_PATH 改。这是微软官方文档明确写的。所以环境变量这一层不能省。
四、迁移步骤
步骤 1:创建 D 盘目标目录
New-Item -ItemType Directory -Path "D:\NuGet\packages" -Force
New-Item -ItemType Directory -Path "D:\NuGet\http-cache" -Force
步骤 2:用 robocopy 复制数据(保留时间戳,快速)
复制 packages(916 MB,几十秒搞定)
robocopy "$env:USERPROFILE.nuget\packages" "D:\NuGet\packages" /E /COPY:DAT /R:1 /W:1 /MT:8
复制 http-cache
robocopy "$env:LOCALAPPDATA\NuGet\v3-cache" "D:\NuGet\http-cache" /E /COPY:DAT /R:1 /W:1 /MT:8
参数说明:
- /E 复制所有子目录(含空目录)
- /COPY:DAT 复制数据、属性、时间戳
- /MT:8 8 线程并发
- /R:1 /W:1 失败重试 1 次,每次等 1 秒
复制后验证数据完整性:
foreach ($p in @("D:\NuGet\packages","D:\NuGet\http-cache")) {
$s = (Get-ChildItem $p -Recurse -File | Measure-Object Length -Sum).Sum / 1MB
$cnt = (Get-ChildItem $p -Recurse -File | Measure-Object).Count
Write-Host ("{0,8:N2} MB ({1} 文件) {2}" -f $s, $cnt, $p)
}
步骤 3:设置用户级环境变量
写入注册表 HKCU\Environment(长期生效,重启后仍有效)
[Environment]::SetEnvironmentVariable("NUGET_PACKAGES", "D:\NuGet\packages", "User")
[Environment]::SetEnvironmentVariable("NUGET_HTTP_CACHE_PATH", "D:\NuGet\http-cache", "User")
验证已写入
[Environment]::GetEnvironmentVariable("NUGET_PACKAGES", "User")
[Environment]::GetEnvironmentVariable("NUGET_HTTP_CACHE_PATH", "User")
注意:SetEnvironmentVariable 写入注册表后,已经启动的进程读不到新值——它们继承的是启动那一刻的环境快照。需要注销重登录或重启电脑才能让所有新进程读到。这是后面要重点验证的环节。
步骤 4:写入用户级 NuGet.Config(兜底)
\(configPath = "\)env:APPDATA\NuGet\NuGet.Config"
$configDir = Split-Path $configPath -Parent
if (-not (Test-Path $configDir)) { New-Item -ItemType Directory -Path $configDir -Force | Out-Null }
$config = @'
'@
# ⚠️ 上一行的 '@ 是 Here-String 结束符,必须顶格单独成行:
# - 行首不能有任何空格或缩进
# - 行尾也不能跟任何内容(包括注释),否则 PowerShell 会报语法错误
Set-Content -Path $configPath -Value $config -Encoding UTF8
Get-Content $configPath
⚠️ PowerShell Here-String 语法注意:@' 和 '@ 必须单独成行,结束符 '@ 必须顶格(行首不能有空格),后面也不能跟任何内容(包括注释,否则会报语法错误)。这是 Here-String 最容易踩的坑。
⚠️ 项目级 nuget.config 优先级更高:如果你的项目根目录下存在 nuget.config 文件,它的优先级高于用户级配置(%APPDATA%\NuGet\NuGet.Config)。如果迁移后个别项目仍往 C 盘写包,请检查该项目下是否独立指定了 globalPackagesFolder,如有需同步修改或删除该行。
🔙 回滚方案(如果迁移后 VS 编译失败)
如果重启后 VS 提示找不到包、编译报错,不要慌,按以下步骤快速回滚到 C 盘:
- 还原 NuGet.Config
打开 %APPDATA%\NuGet\NuGet.Config,删掉或注释掉这一行:
如果整个文件都是迁移时新建的,直接删除该文件即可(NuGet 会回退到默认配置)。
-
删除环境变量
删除用户级环境变量
[Environment]::SetEnvironmentVariable("NUGET_PACKAGES", $null, "User")
[Environment]::SetEnvironmentVariable("NUGET_HTTP_CACHE_PATH", $null, "User")
或在「系统属性 → 环境变量」UI 里手动删除这两个变量。
- 重启电脑
让所有进程重新加载环境变量。重启后 VS 会自动恢复到 C 盘默认路径。
- 恢复 C 盘原始数据
如果你已经执行了「重命名隔离」(步骤 1),C 盘原始数据还在,只是改了名字。把它改回来即可:
找到重命名后的目录( packages._del_yyyyMMdd )
Rename-Item "\(env:USERPROFILE\.nuget\packages._del_20260806" -NewName "packages" -Force Rename-Item "\)env:LOCALAPPDATA\NuGet\v3-cache._del_20260806" -NewName "v3-cache" -Force
-
验证回滚成功
dotnet nuget locals all --list
期望:global-packages 指回 C:\Users<用户>.nuget\packages
回滚完成后,D 盘的 D:\NuGet\packages 和 D:\NuGet\http-cache 可以安全删除(因为 C 盘原始数据已恢复)。
💡 小提示:只要你在「步骤 2:用 robocopy 复制数据」时用的是复制(不是移动),并且清理 C 盘时用的是「重命名」而非「直接删除」,回滚就永远是可能的——原始数据始终在 C 盘,只是换了个名字。
五、重启验证(关键一步)
环境变量写入注册表后,必须注销重登录或重启电脑,否则已打开的 VS、终端、Explorer 都读不到新变量。
重启后,新开 PowerShell 验证:
1. 环境变量是否被新进程继承
$env:NUGET_PACKAGES
$env:NUGET_HTTP_CACHE_PATH
2. dotnet 认为缓存在哪
dotnet nuget locals all --list
期望输出:
NUGET_PACKAGES = D:\NuGet\packages
NUGET_HTTP_CACHE_PATH = D:\NuGet\http-cache
http-cache: D:\NuGet\http-cache ✅
global-packages: D:\NuGet\packages ✅
两个核心路径都切到 D 盘了。
六、VS2026 编译验证
光配置好还不够,得用 VS 实际编译一个项目才算真通。打开 VS2026,生成一个现有项目,观察:
- 能否正常 restore(包能不能找到)
- 生成是否成功(dll 能不能产出)
实测结果(DataToys 项目,目标框架 net6.0):
1>------ 已启动生成: 项目: DataToys, 配置: Debug Any CPU ------
1> DataToys -> D:\TraeProject\DataToys\DataToys\bin\Debug\net6.0\DataToys.dll
========== 生成: 1 成功,0 失败,0 最新,0 已跳过 ==========
========== 生成 于 3:14 完成,耗时 10.006 秒 ==========
10 秒生成成功,dll 正常产出,证明 VS2026 完全从 D 盘读包。
铁证:查 project.assets.json
VS/dotnet restore 后会生成 obj/project.assets.json,里面记录了本次 restore 实际用的包路径:
在项目目录下
$json = Get-Content "obj\project.assets.json" -Raw | ConvertFrom-Json
$json.restore | Format-List
关键看 packagesPath 字段:
packagesPath : D:\NuGet\packages ✅ 铁证
这一行是 NuGet restore 写进去的,VS 和 msbuild 都从这里读包路径,所以 VS 一定用 D 盘。
七、清理 C 盘旧缓存(释放 1 GB)
确认 D 盘工作正常后,C 盘原缓存就是冗余了。用「先重命名再删」的保险方案:
步骤 1:重命名隔离(不直接删)
\(ts = Get-Date -Format 'yyyyMMdd' Rename-Item "\)env:USERPROFILE.nuget\packages" -NewName "packages.del\(ts" -Force Rename-Item "\)env:LOCALAPPDATA\NuGet\v3-cache" -NewName "v3-cache.del$ts" -Force
重命名后如果有任何工具还在引用旧路径,会立刻报错暴露,不会静默失败。观察一两天没问题再彻底删。
步骤 2:再次验证 dotnet 不受影响
dotnet nuget locals all --list
dotnet restore "<项目路径><项目>.csproj" --force
确认仍指向 D 盘、restore 仍成功(应该几十毫秒,因为 D 盘缓存已就位)。
步骤 3:彻底删除
Remove-Item "\(env:USERPROFILE\.nuget\packages._del_\)ts" -Recurse -Force
Remove-Item "\(env:LOCALAPPDATA\NuGet\v3-cache._del_\)ts" -Recurse -Force
释放约 1.09 GB。
八、最终稳定性验证(启动新项目)
光配置好、清理完还不够,必须用一个新启动的项目验证包确实往 D 盘写。这是最严格的测试。
启动一个之前没在这台机器上 restore 过的项目(比如从旧电脑拷来的项目),让它触发新包下载,然后检查:
检查 1:C 盘是否有包回写
检查 packages 目录下是否有文件(注意:.nuget 空文件夹本身可能依然存在,属于正常现象)
\(cPkgs = "\)env:USERPROFILE.nuget\packages"
\(cHttp = "\)env:LOCALAPPDATA\NuGet\v3-cache"
检查 packages:目录不存在 或 目录存在但里面没有文件,都算"无回写"
$pkgsFiles = if (Test-Path $cPkgs) { (Get-ChildItem $cPkgs -Recurse -File -ErrorAction SilentlyContinue | Measure-Object).Count } else { 0 }
Write-Host "C 盘 packages 文件数: $pkgsFiles \((if (\)pkgsFiles -eq 0) { '✅ 无回写' } else { '❌ 有回写' })"
检查 v3-cache:同上
$httpFiles = if (Test-Path $cHttp) { (Get-ChildItem $cHttp -Recurse -File -ErrorAction SilentlyContinue | Measure-Object).Count } else { 0 }
Write-Host "C 盘 v3-cache 文件数: $httpFiles \((if (\)httpFiles -eq 0) { '✅ 无回写' } else { '❌ 有回写' })"
💡 为什么不用 Test-Path? 即使你删除了 packages 子文件夹,外层的 .nuget 空目录本身可能依然存在(NuGet 或其他工具会保留这个空壳)。如果用 Test-Path "$env:USERPROFILE.nuget" 检查,会返回 True 让人误以为包又回来了。正确做法是检查目录下是否有文件,文件数为 0 即代表无回写。
检查 2:D 盘是否被实际写入(最近修改时间)
Get-Item "D:\NuGet\packages" | Select-Object LastWriteTime
# 期望:时间戳是几分钟前(新项目 restore 的时间)
检查 3:D 盘最近被修改的包(证明新项目在用 D 盘)
Get-ChildItem "D:\NuGet\packages" -Directory |
Where-Object { $_.LastWriteTime -gt (Get-Date).AddMinutes(-30) } |
Sort-Object LastWriteTime -Descending |
Select-Object Name, LastWriteTime -First 10
实测结果(启动一个 WPF + MVVM 项目后):
📁 handycontrol 3:23:18
📁 it.tangdao.core 3:23:17
📁 system.numerics.vectors 3:22:05
📁 microsoft.bcl.asyncinterfaces 3:22:05
📁 microsoft.extensions.dependencyinjection 3:22:05
📁 communitytoolkit.mvvm 3:22:05
📁 system.buffers 3:22:05
所有新包都下载到了 D:\NuGet\packages,C 盘一点没沾。
检查 4:D 盘数据增长情况
foreach ($p in @("D:\NuGet\packages","D:\NuGet\http-cache")) {
$s = (Get-ChildItem $p -Recurse -File | Measure-Object Length -Sum).Sum / 1MB
$cnt = (Get-ChildItem $p -Recurse -File | Measure-Object).Count
Write-Host ("{0,8:N2} MB ({1} 文件) {2}" -f $s, $cnt, $p)
}
指标 迁移时 启动新项目后 变化
packages 大小 916.47 MB 968.21 MB +51.74 MB
packages 文件数 3400 3836 +436
D 盘 packages 体积增长,证明新包确实写进去了。
九、总结
迁移要点
- 两层配置缺一不可
- 环境变量 NUGET_PACKAGES + NUGET_HTTP_CACHE_PATH(最高优先级,http-cache 只能靠它)
- NuGet.Config 的 globalPackagesFolder(兜底,防止某些进程读不到环境变量)
- 必须重启/注销重登录
- 环境变量写入注册表后,已启动的进程读不到新值
- 重启电脑是最稳妥的方式
- 清理用「先重命名再删」
- 不要直接 Remove-Item,先 Rename-Item 成 del 后缀
- 观察一两天没问题再彻底删,避免误删导致工具报错
- 验证三层
- 配置层:dotnet nuget locals all --list 指向 D 盘
- 项目层:project.assets.json 的 packagesPath 是 D 盘
- 行为层:启动新项目后 C 盘无回写、D 盘有新写入
最终配置一览
环境变量(HKCU\Environment):
NUGET_PACKAGES = D:\NuGet\packages
NUGET_HTTP_CACHE_PATH = D:\NuGet\http-cache
NuGet.Config (%APPDATA%\NuGet\NuGet.Config):
globalPackagesFolder = D:\NuGet\packages
D:\NuGet:
├── packages\ (包本体,约 970 MB)
└── http-cache\ (HTTP 索引缓存,约 183 MB)
C:\Users\liuxin.nuget:
└── (空目录,已清理)
释放空间
- C 盘释放:约 1.09 GB
- D 盘占用:约 1.15 GB(含后续新增的包)
十、踩坑提示
坑 1:只改 NuGet.Config 不够
很多人只改 NuGet.Config 加 globalPackagesFolder,以为搞定了。结果 http-cache 还在 C 盘——因为这个配置项只管 global-packages,不管 http-cache。http-cache 必须靠环境变量 NUGET_HTTP_CACHE_PATH。
坑 2:环境变量设了不重启就以为没生效
SetEnvironmentVariable 写入注册表后,当前 PowerShell 会话和已打开的 VS 都读不到新值。必须新开终端(最好注销重登录)才能验证。不要在原会话里 $env:NUGET_PACKAGES 一看是空就以为失败了。
坑 3:直接删 C 盘缓存可能误删正在用的包
如果某个 VS 实例正开着、还在用 C 盘的包,直接 Remove-Item 可能导致编译报错。先重命名成 del 隔离观察,确认无影响再彻底删。
坑 4:robocopy 复制大目录很慢
如果包特别多(几个 GB),Copy-Item 会非常慢甚至卡死。用 robocopy + /MT:8 多线程复制,速度快几倍。
坑 5:CI/CD 共享构建机的并发风险(高级场景)
本文方案适用于单用户开发机。如果你的环境是多人共享的构建机/CI Runner,把 NUGET_PACKAGES 指向同一个 D:\NuGet\packages 可能会出问题:
- 多个构建任务并发 restore 时,可能因同时写同一个包目录产生文件锁冲突
- 不同用户权限不同,可能因 ACL 权限导致他人无法读取/写入
- 某个任务清理缓存时会影响其他正在进行的构建
共享环境的推荐做法:让每个用户/构建任务用独立的子目录,避免互相干扰:
方案 A:按用户名隔离(适合多用户登录的构建机)
$pkgPath = "D:\NuGet\packages$env:USERNAME"
[Environment]::SetEnvironmentVariable("NUGET_PACKAGES", $pkgPath, "User")
方案 B:按构建任务隔离(适合 CI Runner,每个 pipeline 用独立工作区)
在 CI 脚本里临时设置(不写入注册表)
$env:NUGET_PACKAGES = "D:\NuGet\packages$env:CI_PIPELINE_ID"
💡 单用户开发机不用考虑这个,直接用 D:\NuGet\packages 即可。本条只针对团队共享构建机。
附录:一键自检命令
把下面这段保存成 Check-NuGet.ps1,随时跑一下确认配置健康:
Write-Host "=== NuGet 配置自检 ===" -ForegroundColor Cyan
1. 环境变量
Write-Host "`n[1] 环境变量:" -ForegroundColor Yellow
"NUGET_PACKAGES = [\(env:NUGET_PACKAGES]"
"NUGET_HTTP_CACHE_PATH = [\)env:NUGET_HTTP_CACHE_PATH]"
2. dotnet nuget locals
Write-Host "`n[2] dotnet nuget locals:" -ForegroundColor Yellow
dotnet nuget locals all --list 2>&1 | ForEach-Object { " $_" }
3. NuGet.Config
Write-Host "`n[3] NuGet.Config:" -ForegroundColor Yellow
\(cfg = "\)env:APPDATA\NuGet\NuGet.Config"
if (Test-Path $cfg) { Get-Content $cfg } else { " 不存在" }
4. D 盘数据
Write-Host "`n[4] D:\NuGet 数据:" -ForegroundColor Yellow
foreach ($p in @("D:\NuGet\packages","D:\NuGet\http-cache")) {
if (Test-Path $p) {
$s = (Get-ChildItem $p -Recurse -File | Measure-Object Length -Sum).Sum / 1MB
Write-Host (" {0,8:N2} MB {1}" -f $s, $p)
} else { Write-Host " ❌ 不存在: $p" -ForegroundColor Red }
}
5. C 盘是否有回写(检查文件数,而非目录是否存在——空目录属于正常现象)
Write-Host "`n[5] C 盘回写检查:" -ForegroundColor Yellow
\(cPkgs = "\)env:USERPROFILE.nuget\packages"
\(cHttp = "\)env:LOCALAPPDATA\NuGet\v3-cache"
foreach (\(p in @(\)cPkgs, $cHttp)) {
$files = if (Test-Path $p) { (Get-ChildItem \(p -Recurse -File -ErrorAction SilentlyContinue | Measure-Object).Count } else { 0 }
if (\)files -eq 0) {
Write-Host " ✅ $p (无回写)" -ForegroundColor Green
} else {
$s = (Get-ChildItem $p -Recurse -File -ErrorAction SilentlyContinue | Measure-Object Length -Sum).Sum / 1MB
Write-Host (" ❌ $p 又出现 {0} 个文件, {1:N2} MB" -f $files, $s) -ForegroundColor Red
}
}
迁移日期:2026-08-06
环境:Windows 11 + Visual Studio 2026 + .NET SDK 10.0.302
释放 C 盘空间:约 1.09 GB

浙公网安备 33010602011771号