Forge
powershelldeeb25a2
1# Generic dotnet publish to a fixed target path (no spaces-friendly).
2# Supports killing a running app that would lock the target.
3#
4# Usage examples:
5# .\scripts\deploy\publish-to-fixed-target.ps1 -Project .\CascadeIDE.csproj -Runtime win-x64 -Configuration Debug -Target D:\cascade-ide-debug -SelfContained
6# .\scripts\deploy\publish-to-fixed-target.ps1 -Project .\MyApp\MyApp.csproj -Runtime win-x64 -Configuration Release -Target D:\myapp -KillRunning
7#
8[CmdletBinding()]
9param(
10 [Parameter(Mandatory = $true)]
11 [string] $Project,
12
13 [string] $Runtime = "win-x64",
14 [ValidateSet("Debug", "Release")]
15 [string] $Configuration = "Debug",
16
17 [switch] $SelfContained,
18
19 [string] $OutDir,
20 [Parameter(Mandatory = $true)]
21 [string] $Target,
22
23 [string] $AppExeName,
24
25 [switch] $KillRunning,
26
27 [string[]] $MsbuildProps = @(),
28 [string[]] $AdditionalDotnetArgs = @()
29)
30
31$ErrorActionPreference = "Stop"
32
33function Resolve-FullPath([string] $p) {
34 return [System.IO.Path]::GetFullPath($p)
35}
36
37function Get-ProjectExeName([string] $projectPath) {
38 if (-not [string]::IsNullOrWhiteSpace($AppExeName)) { return $AppExeName }
39 return [System.IO.Path]::GetFileNameWithoutExtension($projectPath)
40}
41
42function Stop-AppIfRunning {
43 param(
44 [string] $ExpectedExePath,
45 [string] $ProcessName
46 )
47
48 $procs = Get-Process -Name $ProcessName -ErrorAction SilentlyContinue
49 if (-not $procs) { return }
50
51 foreach ($p in $procs) {
52 $exePath = $null
53 try { $exePath = $p.Path } catch { $exePath = $null }
54
55 if ([string]::IsNullOrWhiteSpace($exePath)) { continue }
56 if (-not ([string]::Equals($exePath, $ExpectedExePath, [System.StringComparison]::OrdinalIgnoreCase))) { continue }
57
58 if (-not $KillRunning) {
59 Write-Error "$ProcessName is running from target path and will lock publish output:`n $exePath`nClose it or re-run with -KillRunning."
60 exit 1
61 }
62
63 Write-Host "Stopping $ProcessName PID $($p.Id) from $exePath"
64 Stop-Process -Id $p.Id -Force -ErrorAction Stop
65 }
66}
67
68$projectPath = Resolve-FullPath $Project
69if (-not (Test-Path -LiteralPath $projectPath)) {
70 Write-Error "Project not found: $projectPath"
71 exit 1
72}
73
74$repoRoot = Resolve-FullPath (Split-Path -Parent $projectPath)
75if ([string]::IsNullOrWhiteSpace($OutDir)) {
76 $OutDir = Join-Path $repoRoot ("publish-" + $Configuration.ToLowerInvariant())
77}
78
79if (-not (Test-Path -LiteralPath $Target)) {
80 New-Item -ItemType Directory -Path $Target -Force | Out-Null
81}
82
83$exeName = Get-ProjectExeName $projectPath
84$targetExe = Join-Path $Target ($exeName + ".exe")
85
86Stop-AppIfRunning -ExpectedExePath $targetExe -ProcessName $exeName
87
88$publishArgs = @(
89 "publish", $projectPath,
90 "-c", $Configuration,
91 "-r", $Runtime,
92 "-o", $OutDir,
93 "-v", "minimal"
94)
95
96if ($SelfContained) {
97 $publishArgs += "--self-contained"
98 $publishArgs += "true"
99}
100
101foreach ($p in $MsbuildProps) {
102 if (-not [string]::IsNullOrWhiteSpace($p)) {
103 $publishArgs += $p
104 }
105}
106
107foreach ($a in $AdditionalDotnetArgs) {
108 if (-not [string]::IsNullOrWhiteSpace($a)) {
109 $publishArgs += $a
110 }
111}
112
113& dotnet @publishArgs
114if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
115
116robocopy $OutDir $Target /E /MIR /NFL /NDL /NJH /NJS | Out-Null
117$robocode = $LASTEXITCODE
118if ($robocode -ge 8) {
119 Write-Error "robocopy failed with exit code $robocode"
120 exit $robocode
121}
122
123if (-not (Test-Path -LiteralPath $targetExe)) {
124 Write-Error "Expected exe not found: $targetExe"
125 exit 1
126}
127
128# Avalonia: dotnet publish may copy a DLL without CompileAvaloniaXaml output (XamlLoadException at runtime).
129# Prefer the post-build assembly from bin/{Configuration}/{tfm}/{Runtime}/ when present.
130$tfm = "net10.0"
131try {
132 [xml]$projXml = Get-Content -LiteralPath $projectPath
133 $tfmNode = $projXml.Project.PropertyGroup.TargetFramework | Select-Object -First 1
134 if ($tfmNode -and -not [string]::IsNullOrWhiteSpace($tfmNode.'#text')) { $tfm = $tfmNode.'#text'.Trim() }
135 elseif ($projXml.Project.PropertyGroup.TargetFrameworks) {
136 $tfm = ($projXml.Project.PropertyGroup.TargetFrameworks | Select-Object -First 1).'#text'.Split(';')[0].Trim()
137 }
138} catch { }
139
140$binDll = Join-Path $repoRoot "bin\$Configuration\$tfm\$Runtime\$exeName.dll"
141$binPdb = Join-Path $repoRoot "bin\$Configuration\$tfm\$Runtime\$exeName.pdb"
142$targetDll = Join-Path $Target "$exeName.dll"
143$targetPdb = Join-Path $Target "$exeName.pdb"
144if (Test-Path -LiteralPath $binDll) {
145 $pubDll = Join-Path $OutDir "$exeName.dll"
146 $pubLen = (Test-Path -LiteralPath $pubDll) ? (Get-Item -LiteralPath $pubDll).Length : -1
147 $binLen = (Get-Item -LiteralPath $binDll).Length
148 if (-not (Test-Path -LiteralPath $pubDll) -or $pubLen -ne $binLen) {
149 Write-Host "Syncing $exeName.dll from bin ($binLen bytes) -> publish + target (Avalonia XAML IL)."
150 Copy-Item -LiteralPath $binDll -Destination $pubDll -Force
151 Copy-Item -LiteralPath $binDll -Destination $targetDll -Force
152 if (Test-Path -LiteralPath $binPdb) {
153 Copy-Item -LiteralPath $binPdb -Destination (Join-Path $OutDir "$exeName.pdb") -Force -ErrorAction SilentlyContinue
154 Copy-Item -LiteralPath $binPdb -Destination $targetPdb -Force -ErrorAction SilentlyContinue
155 }
156 }
157}
158
159$publishExe = Join-Path $OutDir ($exeName + ".exe")
160$publishTs = (Test-Path -LiteralPath $publishExe) ? (Get-Item -LiteralPath $publishExe).LastWriteTimeUtc : $null
161$targetTs = (Get-Item -LiteralPath $targetExe).LastWriteTimeUtc
162
163$ts = $targetTs.ToString("o")
164Write-Host ""
165Write-Host "OK: $targetExe (UTC $ts)"
166if ($publishTs) { Write-Host " publish: $($publishTs.ToString('o'))" }
167Write-Host " target: $($targetTs.ToString('o'))"
168Write-Host ""
169
170
View only · write via MCP/CIDE