Netscoot.Shared/Dotnet/Projects.ps1
|
$script:ManagedProjectExtensions = @('.csproj', '.fsproj', '.vbproj') $script:NativeProjectExtensions = @('.vcxproj') function Test-IsNativeProject { # C++/native (.vcxproj). dotnet CLI lists these in solutions but can't reconcile # their link model (AdditionalLibraryDirectories/Dependencies, .props imports). [CmdletBinding()] param([Parameter(Mandatory)][string]$Path) return ([System.IO.Path]::GetExtension($Path) -in $script:NativeProjectExtensions) } function Find-ProjectFiles { # MSBuild project files beneath a root. Managed by default; -IncludeNative also returns .vcxproj. [CmdletBinding()] param( [Parameter(Mandatory)][string]$Root, [switch]$IncludeNative ) $exts = $script:ManagedProjectExtensions if ($IncludeNative) { $exts = $exts + $script:NativeProjectExtensions } $nested = Get-NestedWorktreePath -Root $Root # linked worktrees hold duplicate copies Get-ChildItem -LiteralPath $Root -Recurse -File -ErrorAction SilentlyContinue | Where-Object { $_.Extension -in $exts -and $_.FullName -notmatch '[\\/](bin|obj|\.vs|\.git)[\\/]' -and -not (Test-PathUnderAny -Path $_.FullName -Dirs $nested) } } function Read-ProjectXml { # Read text (File.ReadAllText auto-detects BOM/encoding on both editions), strip any # residual BOM, then LoadXml from the string. Avoids both the 5.1 [xml]-cast BOM failure # and Load(path) URI quirks. [CmdletBinding()] param([Parameter(Mandatory)][string]$Path) $full = Resolve-FullPath $Path if (-not (Test-Path -LiteralPath $full -PathType Leaf)) { throw "Project file not found for XML read: $full" } $text = [System.IO.File]::ReadAllText($full).TrimStart([char]0xFEFF) $xml = New-Object System.Xml.XmlDocument # Harden against XXE: a null resolver stops external DTD/entity resolution (local file read / SSRF) # when parsing an untrusted project file. Windows PowerShell 5.1's XmlDocument resolves entities by # default; .NET Core does not, but set it on both for safety. $xml.XmlResolver = $null $xml.LoadXml($text) return $xml } function Get-ProjectReferencePaths { # Every <ProjectReference Include=...> in a project file, classified. A reference is literal # only when its Include is a plain relative path; an Include built from an MSBuild property # ($(...)), an item list (@(...)), or a wildcard (* ?) cannot be resolved to one file, and a # reference (or its enclosing ItemGroup) carrying a Condition may not always apply. Non-literal # references get FullPath = $null, since there is no single path to reconcile. [CmdletBinding()] param([Parameter(Mandatory)][string]$ProjectFile) $projDir = Split-Path -Parent (Resolve-FullPath $ProjectFile) $xml = Read-ProjectXml -Path $ProjectFile $refs = @() foreach ($node in $xml.SelectNodes('//*[local-name()="ProjectReference"]')) { $include = $node.GetAttribute('Include') if ([string]::IsNullOrWhiteSpace($include)) { continue } $isLiteral = -not ($include -match '\$\(|@\(|[*?]') $hasCondition = -not [string]::IsNullOrWhiteSpace($node.GetAttribute('Condition')) -or ($null -ne $node.ParentNode -and -not [string]::IsNullOrWhiteSpace($node.ParentNode.GetAttribute('Condition'))) $abs = if ($isLiteral) { [System.IO.Path]::GetFullPath((Join-Path $projDir $include)) } else { $null } $refs += [pscustomobject]@{ Raw = $include; FullPath = $abs; IsLiteral = $isLiteral; HasCondition = $hasCondition } } return $refs } function Get-UnreconcilableReferences { # ProjectReferences the dotnet CLI cannot safely reconcile on a move: a non-literal Include # (MSBuild property / item list / wildcard) or a conditional reference. Reported, not rewritten. [CmdletBinding()] param([Parameter(Mandatory)][string]$ProjectFile) return @(Get-ProjectReferencePaths -ProjectFile $ProjectFile | Where-Object { -not $_.IsLiteral -or $_.HasCondition }) } function Write-UnreconcilableReferenceWarning { # Warn about references that a move cannot auto-fix: the moved project's own non-literal / # conditional references, and any other repository project that has such references (which may point # at the moved project through a variable/glob and so was never detected as a consumer). [CmdletBinding()] param( [Parameter(Mandatory)][string]$MovedProject, [Parameter(Mandatory)][AllowEmptyCollection()][object[]]$AllProjects, [Parameter(Mandatory)][AllowEmptyCollection()][string[]]$LiteralConsumers ) $movedFull = Resolve-FullPath $MovedProject foreach ($r in (Get-UnreconcilableReferences -ProjectFile $movedFull)) { $why = if (-not $r.IsLiteral) { 'non-literal path' } else { 'conditional' } Write-Warning ("$(Split-Path -Leaf $movedFull) has an unreconcilable ProjectReference '$($r.Raw)' ($why); verify it by hand after the move.") } foreach ($proj in $AllProjects) { $pf = Resolve-FullPath $proj.FullName if ((Test-PathEqual $pf $movedFull) -or (Test-PathInList $pf $LiteralConsumers)) { continue } if (@(Get-UnreconcilableReferences -ProjectFile $pf).Count -gt 0) { Write-Warning ("$(Split-Path -Leaf $pf) has non-literal/conditional ProjectReference(s); if any point at $(Split-Path -Leaf $movedFull), they were not reconciled - verify by hand.") } } } function Get-ConsumingProjects { # Project files (from $Candidates) that have a ProjectReference to $ProjectFile. [CmdletBinding()] param( [Parameter(Mandatory)][string]$ProjectFile, [Parameter(Mandatory)][AllowEmptyCollection()][object[]]$Candidates ) $target = Resolve-FullPath $ProjectFile $hits = @() foreach ($proj in $Candidates) { if (Test-PathEqual (Resolve-FullPath $proj.FullName) $target) { continue } foreach ($ref in (Get-ProjectReferencePaths -ProjectFile $proj.FullName)) { if (-not $ref.IsLiteral) { continue } # non-literal Include resolves to no single path if (Test-PathEqual $ref.FullPath $target) { $hits += $proj.FullName; break } } } return $hits } function Test-DirectoryBuildInheritance { # Warn if moving from $OldDir to $NewDir changes which inherited MSBuild file applies. Covers # Directory.Build.props/.targets (SDK auto-imports) and Directory.Packages.props (Central # Package Management). MSBuild and CPM each import only the NEAREST ancestor file of a given # name (the project's own directory counts), so inheritance changes when that nearest file # changes - comparing by full path, not leaf name, since every level uses the same filename. [CmdletBinding()] param( [Parameter(Mandatory)][string]$OldDir, [Parameter(Mandatory)][string]$NewDir, [Parameter(Mandatory)][string]$RepositoryRoot ) $rootFull = (Resolve-FullPath $RepositoryRoot) function _nearest([string]$start, [string]$name) { $d = [System.IO.DirectoryInfo]::new((Resolve-FullPath $start)) while ($null -ne $d) { $p = Join-Path $d.FullName $name if (Test-Path -LiteralPath $p) { return (Resolve-FullPath $p) } if (Test-PathEqual (Resolve-FullPath $d.FullName) $rootFull) { break } $d = $d.Parent } return $null } $changes = foreach ($name in 'Directory.Build.props', 'Directory.Build.targets', 'Directory.Packages.props') { $b = _nearest $OldDir $name $a = _nearest $NewDir $name $same = ($b -and $a -and (Test-PathEqual $b $a)) -or (-not $b -and -not $a) if (-not $same) { [pscustomobject]@{ Name = $name; Before = $b; After = $a } } } if ($changes) { Write-Warning "Directory.Build.* / Directory.Packages.props inheritance changes with this move:" foreach ($c in $changes) { $b = if ($c.Before) { $c.Before } else { '(none)' } $a = if ($c.After) { $c.After } else { '(none)' } Write-Warning " $($c.Name): $b -> $a" } } } |