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 盘:

  1. 还原 NuGet.Config

打开 %APPDATA%\NuGet\NuGet.Config,删掉或注释掉这一行:

如果整个文件都是迁移时新建的,直接删除该文件即可(NuGet 会回退到默认配置)。

  1. 删除环境变量

    删除用户级环境变量

    [Environment]::SetEnvironmentVariable("NUGET_PACKAGES", $null, "User")
    [Environment]::SetEnvironmentVariable("NUGET_HTTP_CACHE_PATH", $null, "User")

或在「系统属性 → 环境变量」UI 里手动删除这两个变量。

  1. 重启电脑

让所有进程重新加载环境变量。重启后 VS 会自动恢复到 C 盘默认路径。

  1. 恢复 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

  1. 验证回滚成功

    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,生成一个现有项目,观察:

  1. 能否正常 restore(包能不能找到)
  2. 生成是否成功(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 体积增长,证明新包确实写进去了。


九、总结

迁移要点

  1. 两层配置缺一不可
    • 环境变量 NUGET_PACKAGES + NUGET_HTTP_CACHE_PATH(最高优先级,http-cache 只能靠它)
    • NuGet.Config 的 globalPackagesFolder(兜底,防止某些进程读不到环境变量)
  2. 必须重启/注销重登录
    • 环境变量写入注册表后,已启动的进程读不到新值
    • 重启电脑是最稳妥的方式
  3. 清理用「先重命名再删」
    • 不要直接 Remove-Item,先 Rename-Item 成 del 后缀
    • 观察一两天没问题再彻底删,避免误删导致工具报错
  4. 验证三层
    • 配置层: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

posted @ 2026-08-06 03:37  孤沉  阅读(49)  评论(0)    收藏  举报