update-packages.ps1 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567
  1. # update-packages.ps1
  2. # 检查并升级 NuGet 依赖包(智能查询框架兼容性)
  3. $ErrorActionPreference = 'Stop'
  4. $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
  5. $RootDir = Split-Path -Parent $ScriptDir
  6. function Write-ColorText {
  7. param([string]$Text, [string]$Color = 'White')
  8. Write-Host $Text -ForegroundColor $Color
  9. }
  10. # 读取 Y/N 确认,按Q立即退出
  11. function Read-Confirm {
  12. param([string]$Prompt, [bool]$DefaultYes = $false)
  13. Write-Host $Prompt -NoNewline
  14. while ($true) {
  15. $key = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown')
  16. if ($key.Character -eq 'q' -or $key.Character -eq 'Q') {
  17. Write-Host ''
  18. Write-ColorText '已退出' 'Yellow'
  19. exit 0
  20. }
  21. if ($key.Character -eq 'y' -or $key.Character -eq 'Y') {
  22. Write-Host 'Y'
  23. return $true
  24. }
  25. if ($key.Character -eq 'n' -or $key.Character -eq 'N') {
  26. Write-Host 'N'
  27. return $false
  28. }
  29. if ($key.VirtualKeyCode -eq 13) {
  30. if ($DefaultYes) {
  31. Write-Host 'Y'
  32. return $true
  33. } else {
  34. Write-Host 'N'
  35. return $false
  36. }
  37. }
  38. }
  39. }
  40. # 缓存已查询的包版本框架信息
  41. $script:FrameworkCache = @{}
  42. # 从 NuGet API 获取包版本支持的目标框架
  43. function Get-PackageFrameworks {
  44. param(
  45. [string]$PackageId,
  46. [string]$Version
  47. )
  48. $cacheKey = "$PackageId|$Version"
  49. if ($script:FrameworkCache.ContainsKey($cacheKey)) {
  50. return $script:FrameworkCache[$cacheKey]
  51. }
  52. try {
  53. # 使用 NuGet API 获取包元数据
  54. $lowerPackageId = $PackageId.ToLower()
  55. $lowerVersion = $Version.ToLower()
  56. $url = "https://api.nuget.org/v3-flatcontainer/$lowerPackageId/$lowerVersion/$lowerPackageId.nuspec"
  57. # 使用 Invoke-RestMethod 直接获取 XML
  58. [xml]$nuspec = Invoke-RestMethod -Uri $url -TimeoutSec 10
  59. # 提取支持的框架
  60. $frameworks = @()
  61. # 使用 XmlNamespaceManager 处理命名空间
  62. $nsMgr = New-Object System.Xml.XmlNamespaceManager($nuspec.NameTable)
  63. $nsMgr.AddNamespace("nuget", $nuspec.DocumentElement.NamespaceURI)
  64. # 从 dependencies 节点获取框架
  65. $depGroups = $nuspec.SelectNodes('//nuget:dependencies/nuget:group', $nsMgr)
  66. if ($depGroups -and $depGroups.Count -gt 0) {
  67. foreach ($group in $depGroups) {
  68. $tfm = $group.GetAttribute('targetFramework')
  69. if ($tfm) {
  70. $frameworks += $tfm
  71. }
  72. }
  73. }
  74. # 如果没有 group,可能是支持所有框架或者从 frameworkAssemblies 获取
  75. if ($frameworks.Count -eq 0) {
  76. # 尝试从 frameworkReferences 获取
  77. $fwRefs = $nuspec.SelectNodes('//nuget:frameworkReferences/nuget:group', $nsMgr)
  78. if ($fwRefs -and $fwRefs.Count -gt 0) {
  79. foreach ($group in $fwRefs) {
  80. $tfm = $group.GetAttribute('targetFramework')
  81. if ($tfm) {
  82. $frameworks += $tfm
  83. }
  84. }
  85. }
  86. }
  87. $script:FrameworkCache[$cacheKey] = $frameworks
  88. return $frameworks
  89. }
  90. catch {
  91. $script:FrameworkCache[$cacheKey] = @()
  92. return @()
  93. }
  94. }
  95. # 检查包版本是否支持指定的目标框架
  96. function Test-FrameworkCompatibility {
  97. param(
  98. [string]$PackageId,
  99. [string]$Version,
  100. [string]$TargetFramework
  101. )
  102. $frameworks = Get-PackageFrameworks -PackageId $PackageId -Version $Version
  103. # 如果没有获取到框架信息,假设兼容(可能是 netstandard 包)
  104. if ($frameworks.Count -eq 0) {
  105. return $true
  106. }
  107. # 解析目标框架版本号
  108. $targetVersion = 0
  109. if ($TargetFramework -match 'net(\d+)\.(\d+)') {
  110. $targetVersion = [int]$Matches[1] * 100 + [int]$Matches[2]
  111. }
  112. elseif ($TargetFramework -match 'net(\d+)$') {
  113. $targetVersion = [int]$Matches[1] * 100
  114. }
  115. foreach ($fw in $frameworks) {
  116. $fwLower = $fw.ToLower()
  117. # 检查 netstandard 兼容性
  118. if ($fwLower -match 'netstandard') {
  119. return $true
  120. }
  121. # 检查 .NET Core / .NET 5+ 兼容性
  122. if ($fwLower -match '\.netcoreapp(\d+)\.(\d+)' -or $fwLower -match 'netcoreapp(\d+)\.(\d+)') {
  123. $fwVersion = [int]$Matches[1] * 100 + [int]$Matches[2]
  124. if ($targetVersion -ge $fwVersion) {
  125. return $true
  126. }
  127. }
  128. # 检查 .NET 5+ 格式 (net5.0, net6.0, etc.)
  129. if ($fwLower -match '\.net(\d+)\.(\d+)' -or $fwLower -match 'net(\d+)\.(\d+)') {
  130. $fwVersion = [int]$Matches[1] * 100 + [int]$Matches[2]
  131. if ($targetVersion -ge $fwVersion) {
  132. return $true
  133. }
  134. }
  135. # 检查简化格式 (net6, net7, etc.)
  136. if ($fwLower -match 'net(\d+)$') {
  137. $fwVersion = [int]$Matches[1] * 100
  138. if ($targetVersion -ge $fwVersion) {
  139. return $true
  140. }
  141. }
  142. }
  143. return $false
  144. }
  145. # 获取包的所有可用版本
  146. function Get-PackageVersions {
  147. param([string]$PackageId)
  148. try {
  149. $lowerPackageId = $PackageId.ToLower()
  150. $url = "https://api.nuget.org/v3-flatcontainer/$lowerPackageId/index.json"
  151. # 使用 Invoke-RestMethod 直接获取 JSON
  152. $data = Invoke-RestMethod -Uri $url -TimeoutSec 10
  153. # 过滤掉预览版,返回稳定版本(降序排列)
  154. # 排除包含连字符的版本(预览版、alpha、beta、rc、dev、final 等)
  155. $stableVersions = $data.versions | Where-Object {
  156. $_ -notmatch '-' -and $_ -notmatch '^10\.'
  157. } | Sort-Object { [Version]($_ -replace '-.*$', '') } -Descending
  158. return $stableVersions
  159. }
  160. catch {
  161. return @()
  162. }
  163. }
  164. # 为指定框架找到最佳可用版本
  165. function Get-BestVersionForFramework {
  166. param(
  167. [string]$PackageId,
  168. [string]$TargetFramework,
  169. [string]$CurrentVersion
  170. )
  171. $versions = Get-PackageVersions -PackageId $PackageId
  172. if ($versions.Count -eq 0) {
  173. return $null
  174. }
  175. foreach ($version in $versions) {
  176. # 跳过当前版本及更低版本
  177. try {
  178. $vCurrent = [Version]($CurrentVersion -replace '-.*$', '')
  179. $vCheck = [Version]($version -replace '-.*$', '')
  180. if ($vCheck -le $vCurrent) {
  181. return $null # 没有更新的兼容版本
  182. }
  183. }
  184. catch {
  185. # 版本解析失败,继续检查
  186. }
  187. if (Test-FrameworkCompatibility -PackageId $PackageId -Version $version -TargetFramework $TargetFramework) {
  188. return $version
  189. }
  190. }
  191. return $null
  192. }
  193. Write-ColorText "`n========================================" 'Cyan'
  194. Write-ColorText ' Apq.Cfg 依赖包升级工具' 'Cyan'
  195. Write-ColorText ' (智能框架兼容性检查)' 'DarkCyan'
  196. Write-ColorText "========================================" 'Cyan'
  197. Write-ColorText ' 按 Q 随时退出' 'DarkGray'
  198. Write-ColorText "========================================`n" 'Cyan'
  199. # 检查过期包
  200. Write-ColorText '正在检查过期的依赖包...' 'Cyan'
  201. Write-Host ''
  202. $outdatedOutput = & dotnet list "$RootDir\Apq.Cfg.sln" package --outdated --format json 2>$null
  203. if ($LASTEXITCODE -ne 0) {
  204. Write-ColorText '错误: 无法检查过期包' 'Red'
  205. exit 1
  206. }
  207. $outdatedData = $outdatedOutput | ConvertFrom-Json
  208. # 收集需要升级的包(按项目+框架分组)
  209. $packagesToUpdate = @{}
  210. $frameworkCheckNeeded = @{}
  211. # 排除的包列表(这些包不应自动升级)
  212. $excludedPackages = @(
  213. 'Microsoft.CodeAnalysis.CSharp', # Roslyn - 需要保持低版本以兼容编译器
  214. 'Microsoft.CodeAnalysis.Analyzers' # Roslyn Analyzers
  215. )
  216. foreach ($project in $outdatedData.projects) {
  217. $projectName = Split-Path $project.path -Leaf
  218. $projectPath = $project.path
  219. foreach ($framework in $project.frameworks) {
  220. $tfm = $framework.framework
  221. foreach ($package in $framework.topLevelPackages) {
  222. $packageName = $package.id
  223. $currentVersion = $package.resolvedVersion
  224. $latestVersion = $package.latestVersion
  225. if ($currentVersion -ne $latestVersion) {
  226. # 跳过排除的包
  227. if ($excludedPackages -contains $packageName) {
  228. continue
  229. }
  230. # 检查最新版本是否为预览版
  231. if ($latestVersion -match '-(preview|alpha|beta|rc|dev)') {
  232. continue
  233. }
  234. # 检查是否为 .NET 10 预览版
  235. if ($latestVersion -match '^10\.') {
  236. continue
  237. }
  238. if (-not $packagesToUpdate.ContainsKey($packageName)) {
  239. $packagesToUpdate[$packageName] = @{
  240. Name = $packageName
  241. LatestVersion = $latestVersion
  242. ProjectFrameworks = @()
  243. }
  244. }
  245. $packagesToUpdate[$packageName].ProjectFrameworks += @{
  246. Project = $projectName
  247. ProjectPath = $projectPath
  248. Framework = $tfm
  249. CurrentVersion = $currentVersion
  250. BestVersion = $null
  251. }
  252. # 标记需要检查框架兼容性
  253. $frameworkCheckNeeded["$packageName|$tfm|$currentVersion"] = @{
  254. PackageName = $packageName
  255. Framework = $tfm
  256. CurrentVersion = $currentVersion
  257. LatestVersion = $latestVersion
  258. }
  259. }
  260. }
  261. }
  262. }
  263. if ($packagesToUpdate.Count -eq 0) {
  264. Write-ColorText '所有依赖包都是最新的!' 'Green'
  265. Write-Host ''
  266. exit 0
  267. }
  268. # 查询框架兼容性并确定最佳版本
  269. Write-ColorText '正在查询 NuGet 框架兼容性...' 'Cyan'
  270. $totalChecks = $frameworkCheckNeeded.Count
  271. $currentCheck = 0
  272. foreach ($key in $frameworkCheckNeeded.Keys) {
  273. $info = $frameworkCheckNeeded[$key]
  274. $currentCheck++
  275. Write-Host "`r 检查 $currentCheck/$totalChecks : $($info.PackageName) ($($info.Framework)) " -NoNewline
  276. $bestVersion = Get-BestVersionForFramework -PackageId $info.PackageName -TargetFramework $info.Framework -CurrentVersion $info.CurrentVersion
  277. # 更新 packagesToUpdate 中对应的 BestVersion
  278. foreach ($pf in $packagesToUpdate[$info.PackageName].ProjectFrameworks) {
  279. if ($pf.Framework -eq $info.Framework -and $pf.CurrentVersion -eq $info.CurrentVersion) {
  280. $pf.BestVersion = $bestVersion
  281. }
  282. }
  283. }
  284. Write-Host ''
  285. Write-Host ''
  286. # 整理升级计划
  287. $upgradeActions = @()
  288. foreach ($pkg in $packagesToUpdate.Values) {
  289. # 按项目路径分组 - 使用 ScriptBlock 访问哈希表属性
  290. $projectGroups = $pkg.ProjectFrameworks | Group-Object -Property { $_.ProjectPath }
  291. foreach ($group in $projectGroups) {
  292. $projectPath = $group.Name
  293. if (-not $projectPath) { continue }
  294. $projectName = Split-Path $projectPath -Leaf
  295. $frameworks = $group.Group
  296. # 检查该项目下所有框架的最佳版本
  297. $versionsToApply = @{}
  298. foreach ($fw in $frameworks) {
  299. if ($fw.BestVersion) {
  300. if (-not $versionsToApply.ContainsKey($fw.BestVersion)) {
  301. $versionsToApply[$fw.BestVersion] = @()
  302. }
  303. $versionsToApply[$fw.BestVersion] += $fw.Framework
  304. }
  305. }
  306. # 如果所有框架都能用同一个最新版本,直接升级
  307. # 否则需要按框架条件引用
  308. if ($versionsToApply.Count -eq 1) {
  309. $version = ($versionsToApply.Keys | Select-Object -First 1)
  310. $upgradeActions += @{
  311. Package = $pkg.Name
  312. Project = $projectName
  313. ProjectPath = $projectPath
  314. Type = 'Simple'
  315. Version = $version
  316. CurrentVersions = ($frameworks | ForEach-Object { $_.CurrentVersion } | Select-Object -Unique) -join ', '
  317. Frameworks = $versionsToApply[$version] -join ', '
  318. }
  319. }
  320. elseif ($versionsToApply.Count -gt 1) {
  321. # 需要按框架分别升级
  322. foreach ($version in $versionsToApply.Keys) {
  323. $fws = $versionsToApply[$version]
  324. $currentVer = ($frameworks | Where-Object { $fws -contains $_.Framework } | Select-Object -First 1).CurrentVersion
  325. $upgradeActions += @{
  326. Package = $pkg.Name
  327. Project = $projectName
  328. ProjectPath = $projectPath
  329. Type = 'PerFramework'
  330. Version = $version
  331. CurrentVersions = $currentVer
  332. Frameworks = $fws -join ', '
  333. }
  334. }
  335. }
  336. }
  337. }
  338. if ($upgradeActions.Count -eq 0) {
  339. Write-ColorText '没有可升级的包(所有包的最新版本都不兼容当前框架)' 'Yellow'
  340. Write-Host ''
  341. exit 0
  342. }
  343. # 显示升级计划
  344. Write-ColorText '升级计划:' 'Yellow'
  345. Write-Host ''
  346. $simpleUpgrades = $upgradeActions | Where-Object { $_.Type -eq 'Simple' }
  347. $perFrameworkUpgrades = $upgradeActions | Where-Object { $_.Type -eq 'PerFramework' }
  348. if ($simpleUpgrades.Count -gt 0) {
  349. Write-ColorText ' [统一升级] 所有框架使用相同版本:' 'Green'
  350. $index = 1
  351. foreach ($action in $simpleUpgrades) {
  352. Write-ColorText " $index. $($action.Package)" 'White'
  353. Write-ColorText " $($action.CurrentVersions) -> $($action.Version)" 'Gray'
  354. Write-ColorText " 项目: $($action.Project)" 'DarkGray'
  355. $index++
  356. }
  357. Write-Host ''
  358. }
  359. if ($perFrameworkUpgrades.Count -gt 0) {
  360. Write-ColorText ' [按框架升级] 不同框架使用不同版本:' 'Cyan'
  361. $grouped = $perFrameworkUpgrades | Group-Object -Property Package
  362. foreach ($group in $grouped) {
  363. Write-ColorText " $($group.Name):" 'White'
  364. foreach ($action in $group.Group) {
  365. Write-ColorText " [$($action.Frameworks)] $($action.CurrentVersions) -> $($action.Version)" 'Gray'
  366. Write-ColorText " 项目: $($action.Project)" 'DarkGray'
  367. }
  368. }
  369. Write-Host ''
  370. }
  371. if (-not (Read-Confirm '是否执行升级? (Y/n,默认为 Y): ' $true)) {
  372. Write-ColorText '已取消' 'Yellow'
  373. exit 0
  374. }
  375. Write-Host ''
  376. Write-ColorText '开始升级...' 'Cyan'
  377. Write-Host ''
  378. $successCount = 0
  379. $failCount = 0
  380. $skipCount = 0
  381. # 执行简单升级(所有框架用同一版本)
  382. foreach ($action in $simpleUpgrades) {
  383. Write-ColorText "升级 $($action.Package) -> $($action.Version)" 'White'
  384. Write-ColorText " $($action.Project) ..." 'Gray'
  385. $csprojFile = $action.ProjectPath
  386. if ($csprojFile -and (Test-Path $csprojFile)) {
  387. try {
  388. $output = & dotnet add $csprojFile package $action.Package --version $action.Version 2>&1
  389. if ($LASTEXITCODE -eq 0) {
  390. Write-ColorText " $($action.Project) 成功" 'Green'
  391. $successCount++
  392. } else {
  393. Write-ColorText " $($action.Project) 失败" 'Red'
  394. Write-ColorText " $output" 'DarkGray'
  395. $failCount++
  396. }
  397. } catch {
  398. Write-ColorText " $($action.Project) 失败: $($_.Exception.Message)" 'Red'
  399. $failCount++
  400. }
  401. } else {
  402. Write-ColorText " $($action.Project) 未找到项目文件: $csprojFile" 'Red'
  403. $failCount++
  404. }
  405. }
  406. # 执行按框架升级(需要修改 csproj 中的条件引用)
  407. if ($perFrameworkUpgrades.Count -gt 0) {
  408. Write-Host ''
  409. Write-ColorText '按框架升级:' 'Cyan'
  410. $grouped = $perFrameworkUpgrades | Group-Object -Property { "$($_.Package)|$($_.ProjectPath)" }
  411. foreach ($group in $grouped) {
  412. $actions = $group.Group
  413. $firstAction = $actions | Select-Object -First 1
  414. $packageName = $firstAction.Package
  415. $projectName = $firstAction.Project
  416. Write-ColorText " $packageName ($projectName):" 'White'
  417. $csprojFile = $firstAction.ProjectPath
  418. if ($csprojFile -and (Test-Path $csprojFile)) {
  419. # 读取项目文件
  420. $content = [System.IO.File]::ReadAllText($csprojFile, [System.Text.Encoding]::UTF8)
  421. $modified = $false
  422. foreach ($action in $actions) {
  423. $frameworks = $action.Frameworks -split ', '
  424. $newVersion = $action.Version
  425. foreach ($fw in $frameworks) {
  426. # 匹配带条件的 ItemGroup 中的 PackageReference
  427. # 格式: <ItemGroup Condition="'$(TargetFramework)' == 'net6.0'">
  428. # <PackageReference Include="PackageName" Version="x.x.x" />
  429. $pattern = "(<ItemGroup[^>]*Condition\s*=\s*[`"'][^`"']*\`$\(TargetFramework\)[^`"']*==\s*'$fw'[^`"']*[`"'][^>]*>[\s\S]*?<PackageReference\s+Include\s*=\s*[`"']$packageName[`"'][^>]*Version\s*=\s*[`"'])([^`"']+)([`"'])"
  430. if ($content -match $pattern) {
  431. $content = $content -replace $pattern, "`${1}$newVersion`${3}"
  432. $modified = $true
  433. Write-ColorText " [$fw] 已更新到 $newVersion" 'Green'
  434. }
  435. }
  436. }
  437. if ($modified) {
  438. # 检测原文件编码(是否有 BOM)
  439. $bytes = [System.IO.File]::ReadAllBytes($csprojFile)
  440. $hasBom = ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF)
  441. if ($hasBom) {
  442. $encoding = New-Object System.Text.UTF8Encoding($true)
  443. } else {
  444. $encoding = New-Object System.Text.UTF8Encoding($false)
  445. }
  446. [System.IO.File]::WriteAllText($csprojFile, $content, $encoding)
  447. $successCount++
  448. } else {
  449. # 如果没有找到条件引用,提示用户手动处理
  450. Write-ColorText " 未找到条件引用,请手动修改:" 'Yellow'
  451. foreach ($action in $actions) {
  452. Write-ColorText " [$($action.Frameworks)] -> $($action.Version)" 'Gray'
  453. }
  454. $skipCount++
  455. }
  456. } else {
  457. Write-ColorText " 未找到项目文件" 'Red'
  458. $failCount++
  459. }
  460. }
  461. }
  462. Write-Host ''
  463. Write-ColorText "========================================" 'Cyan'
  464. $resultColor = if ($failCount -eq 0 -and $skipCount -eq 0) { 'Green' } else { 'Yellow' }
  465. Write-ColorText "升级完成: 成功 $successCount, 失败 $failCount, 跳过 $skipCount" $resultColor
  466. Write-ColorText "========================================" 'Cyan'
  467. Write-Host ''
  468. # 验证构建
  469. if (Read-Confirm '是否验证构建? (Y/n,默认为 Y): ' $true) {
  470. Write-Host ''
  471. Write-ColorText '正在构建解决方案...' 'Cyan'
  472. Write-Host ''
  473. & dotnet build "$RootDir\Apq.Cfg.sln" -c Release --verbosity minimal
  474. if ($LASTEXITCODE -eq 0) {
  475. Write-Host ''
  476. Write-ColorText '构建成功!' 'Green'
  477. } else {
  478. Write-Host ''
  479. Write-ColorText '构建失败,请检查错误信息' 'Red'
  480. }
  481. }
  482. Write-Host ''