Netscoot.Native/Public/Move-NativeProject.ps1
|
function Move-NativeProject { <# .SYNOPSIS Move a native or C++/CLI project (.vcxproj), update the solutions and projects that reference it, and report the native path-bearing settings it does not rewrite so they are never silently broken. Windows-only. .DESCRIPTION Native projects link through MSBuild settings that a move can break: AdditionalIncludeDirectories / AdditionalLibraryDirectories / AdditionalDependencies, `<Import>` of shared .props/.targets, $(SolutionDir)-relative OutDir, and the paired vcxproj.filters file. C++/CLI is Windows-only, so this cmdlet refuses to run elsewhere. It will: move the folder (git mv when tracked) with its paired .vcxproj.filters; rewrite the project's path in each .sln/.slnx entry, in every ProjectReference to it (native or managed consumers) and in its own ProjectReferences, keeping GUIDs, platform mappings and solution folders as they are; and report native settings for a human to verify: every relative or SolutionDir-relative setting in the moved project (returned as UnreconciledSettings), and every parent-relative (..) setting in another project that points into its folder (written as warnings). It does not rewrite those MSBuild settings. The dotnet CLI is not used: it cannot load a .vcxproj outside Visual Studio's MSBuild. .PARAMETER Project Path to the .vcxproj. Accepts a path string or a Get-ChildItem/Get-Item item from the pipeline, and rejects other object types. .PARAMETER Destination Where to move the project folder, following `git mv` rules: An existing directory means move into it, keeping the name. Any other path is the new folder path. .PARAMETER RepositoryRoot Root to scan for solutions. Defaults to the enclosing git repository root. .PARAMETER Force When git is not installed, move with a plain PowerShell `Move-Item` without asking first. Without -Force it asks before falling back. The plain move does not preserve git history. .PARAMETER NoJournal Skip recording this move in the undo journal for this call, even when journaling is enabled (Undo-Netscoot will not see this move). .OUTPUTS Netscoot.NativeMoveResult .EXAMPLE # Preview, including the native path settings it cannot reconcile (verify by hand after) Move-NativeProject -Project ./Aleppo/Aleppo.vcxproj -Destination ./native/Aleppo -WhatIf # Move it (also moves the paired .vcxproj.filters) Move-NativeProject -Project ./Aleppo/Aleppo.vcxproj -Destination ./native/Aleppo # Move into an existing folder (lands at ./native/Aleppo) Move-NativeProject -Project ./Aleppo/Aleppo.vcxproj -Destination ./native #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')] [OutputType('Netscoot.NativeMoveResult')] param( [Parameter(Mandatory, Position = 0, ValueFromPipeline)] [Netscoot.PathInputTransform()] [ValidateNotNullOrEmpty()] [string]$Project, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$Destination, [string]$RepositoryRoot, [switch]$Force, [switch]$NoJournal ) process { if (-not (Test-IsWindowsHost)) { $PSCmdlet.WriteError([System.Management.Automation.ErrorRecord]::new( [System.PlatformNotSupportedException]::new("Native/C++ projects are Windows-only; Move-NativeProject cannot run on this OS."), 'WindowsOnly', [System.Management.Automation.ErrorCategory]::NotImplemented, $Project)) return } $projFull = Resolve-FullPath $Project if (-not (Test-Path -LiteralPath $projFull)) { $PSCmdlet.WriteError([System.Management.Automation.ErrorRecord]::new( [System.IO.FileNotFoundException]::new("Project not found: $Project"), 'ProjectNotFound', [System.Management.Automation.ErrorCategory]::ObjectNotFound, $Project)) return } if ([System.IO.Path]::GetExtension($projFull).ToLowerInvariant() -eq '.vcproj') { $PSCmdlet.WriteError([System.Management.Automation.ErrorRecord]::new( [System.NotSupportedException]::new("'$Project' is a legacy Visual C++ project (.vcproj, pre-VS2010), which predates MSBuild and is not supported. Convert it to .vcxproj (open it in Visual Studio 2010 or later), then retry."), 'LegacyVcprojNotSupported', [System.Management.Automation.ErrorCategory]::NotImplemented, $Project)) return } if (-not (Test-IsNativeProject $projFull)) { $PSCmdlet.WriteError([System.Management.Automation.ErrorRecord]::new( [System.ArgumentException]::new("Not a native project (.vcxproj): $Project. Use Move-DotnetProject for managed projects."), 'NotANativeProject', [System.Management.Automation.ErrorCategory]::InvalidArgument, $Project)) return } $oldDir = Split-Path -Parent $projFull $projFile = Split-Path -Leaf $projFull if (-not $RepositoryRoot) { $RepositoryRoot = Get-RepositoryRoot -StartPath $oldDir } $repoFull = Resolve-FullPath $RepositoryRoot # git mv semantics: an existing destination directory means "move the project folder into # it"; otherwise Destination is the project's new folder path. $newDir = Resolve-MoveTarget -Source $oldDir -Destination $Destination $newProj = Join-Path $newDir $projFile if (Test-Path -LiteralPath $newDir) { $PSCmdlet.WriteError([System.Management.Automation.ErrorRecord]::new( [System.IO.IOException]::new("Destination already exists: $newDir"), 'DestinationExists', [System.Management.Automation.ErrorCategory]::ResourceExists, $newDir)) return } if (Test-PathOverlap $newDir $oldDir) { $PSCmdlet.WriteError([System.Management.Automation.ErrorRecord]::new( [System.InvalidOperationException]::new("Destination '$newDir' overlaps the source '$oldDir'; a project folder cannot be moved into itself or its own subtree."), 'PathOverlap', [System.Management.Automation.ErrorCategory]::InvalidArgument, $Destination)) return } # One repository parse for this invocation; solutions-referencing reuses the embedded parse. $workspace = Get-Workspace -RepositoryRoot $repoFull $allSolutions = @(Get-WorkspaceSolutions -Workspace $workspace) $solutions = @(Get-SolutionsReferencing -ProjectFile $projFull -Candidates $allSolutions) $nativeSettings = @(Get-NativePathSettings -ProjectFile $projFull) $filters = "$projFull.filters" $hasFilters = Test-Path -LiteralPath $filters # The dotnet CLI cannot evaluate a .vcxproj outside Visual Studio's MSBuild and would drop the # entry's platform mapping and GUID, so every path is rewritten in place instead. $pointAt = { param($Ref, $Target) [void]$Ref.PointAt($Target) } $followFile = { param($Ref, $NewFile) [void]$Ref.FollowFile($NewFile) } $incoming = @() foreach ($s in $solutions) { $incoming += @(Get-SolutionProjectEntries -SolutionFile $s.FullName | Where-Object { Test-PathEqual $_.Target $projFull }) } $consumers = @(Get-WorkspaceConsumingProjects -Workspace $workspace -ProjectFile $projFull) foreach ($c in $consumers) { foreach ($r in @(Get-WorkspaceProjectRefs -Workspace $workspace -ProjectFile $c | Where-Object { $_.IsLiteral -and (Test-PathEqual $_.FullPath $projFull) })) { $incoming += [Netscoot.StoredPath]::InAttribute($c, 'Include', $r.Raw, $projFull) } } $outgoing = @(Get-WorkspaceProjectRefs -Workspace $workspace -ProjectFile $projFull | Where-Object { $_.IsLiteral } | ForEach-Object { [Netscoot.StoredPath]::InAttribute($projFull, 'Include', $_.Raw, $_.FullPath) } | Where-Object { $_.RawFollowing($newProj) -ne $_.Raw }) $items = @() foreach ($ref in $incoming) { $items += New-MoveItem -Description "$(Split-Path -Leaf $ref.File): $($ref.Raw) -> $($ref.RawPointingAt($newProj))" ` -Reattach $pointAt -ReattachArgs @($ref, $newProj) } foreach ($ref in $outgoing) { $items += New-MoveItem -Description "own reference: $($ref.Raw) -> $($ref.RawFollowing($newProj))" ` -Reattach $followFile -ReattachArgs @($ref, $newProj) } Write-UnreconcilableReferenceWarning -MovedProject $projFull -AllProjects @(Get-WorkspaceProjectFiles -Workspace $workspace -IncludeNative) ` -LiteralConsumers $consumers -Workspace $workspace # Other projects' native settings that resolve into the moved folder break too; report them. $pointingIn = @() foreach ($other in @(Get-WorkspaceProjectFiles -Workspace $workspace -IncludeNative | Where-Object { $_.Extension -eq '.vcxproj' })) { if (Test-PathEqual $other.Abs $projFull) { continue } $otherDir = Split-Path -Parent $other.Abs foreach ($setting in @(Get-NativePathSettings -ProjectFile $other.Abs)) { foreach ($entry in ($setting.Value -split ';')) { $candidate = $entry.Trim() -replace '^\$\((ProjectDir|MSBuildThisFileDirectory)\)', '' if (-not $candidate -or $candidate -match '[$%]\(') { continue } $abs = [System.IO.Path]::GetFullPath((Join-Path $otherDir $candidate.Replace('\', [System.IO.Path]::DirectorySeparatorChar))) if ((Test-PathEqual $abs $oldDir) -or (Test-PathUnder $abs $oldDir)) { $pointingIn += "$($other.Name): [$($setting.Kind)] $($entry.Trim())" } } } } $slnNames = @(); foreach ($s in $solutions) { $slnNames += $s.Name } $settingLines = @($nativeSettings | ForEach-Object { "[$($_.Kind)] $($_.Value)" }) Write-MovePlan -Cmdlet $PSCmdlet -Caption "Move-NativeProject $projFile $oldDir -> $newDir" -Items ([ordered]@{ 'solutions to update' = $slnNames 'consuming projects to update' = @($consumers | ForEach-Object { Split-Path -Leaf $_ }) 'paired .filters file moving' = $hasFilters 'unreconciled native settings (manual review)' = $settingLines 'other projects pointing into the folder' = $pointingIn }) $performed = $false if ($PSCmdlet.ShouldProcess("$projFile : $oldDir -> $newDir", 'Move native project and update solution entries and project references (native paths reported only)')) { $ctx = Resolve-MoveContext -Cmdlet $PSCmdlet -Force:$Force -TargetForError $projFull if (-not $ctx) { return } $move = { param($UseGit, $Src, $Dst, $Repository) Move-PathTracked -UseGit $UseGit -Source $Src -Destination $Dst -RepositoryRoot $Repository } $backup = @($incoming | ForEach-Object { $_.File }) + @($projFull) Invoke-MovePlan -Caption "Move native $projFile" -Items $items -Move $move ` -MoveArgs @($ctx.UseGit, $oldDir, $newDir, $repoFull) ` -BackupPath $backup -Rollback $move -RollbackArgs @($ctx.UseGit, $newDir, $oldDir, $repoFull) ` -RepositoryRoot $repoFull -Command 'Move-NativeProject' -Engine 'native' -Source $projFull -Destination $newProj ` -UndoParams @{ Project = $newProj; Destination = $oldDir; Force = [bool]$Force } -NoJournal:$NoJournal $performed = $true } if ($nativeSettings.Count -gt 0) { Write-Warning "$($nativeSettings.Count) native path setting(s) in $projFile are not auto-reconciled - verify by hand or with a native engine:" foreach ($s in $nativeSettings) { Write-Warning " [$($s.Kind)] $($s.Value)" } } if ($hasFilters) { Write-Warning "Paired .filters for $projFile moved with the folder; its entries are project-relative and usually survive, but confirm no parent-relative entries broke." } foreach ($line in $pointingIn) { Write-Warning "Points into the moved folder, not auto-reconciled - verify by hand: $line" } New-MoveResult -TypeName 'Netscoot.NativeMoveResult' -Engine 'native' -Source $projFull -Destination $newProj ` -Performed $performed -Extra ([ordered]@{ Solutions = $slnNames UnreconciledSettings = $nativeSettings HadFilters = $hasFilters }) } } |