param( [string]$TargetFramework = "net10.0-windows10.0.19041.0", [string]$RuntimeIdentifier = "win-x64", [string]$Configuration = "Release", [switch]$SkipInnoSetup, [switch]$SkipVersionBump, [string]$ArtifactsRoot = (Join-Path $PSScriptRoot ("artifacts\windows\{0}" -f $RuntimeIdentifier)) ) $ErrorActionPreference = "Stop" $ScriptPath = $PSScriptRoot $ProjectDir = Join-Path $ScriptPath "src\Hua.Todo.Maui" $ProjectFile = Join-Path $ProjectDir "Hua.Todo.Maui.csproj" $SetupScript = Join-Path $ProjectDir "setup.iss" $ProjectBaseName = [System.IO.Path]::GetFileNameWithoutExtension($ProjectFile) function Read-ProjectVersion { param( [Parameter(Mandatory = $true)] [string]$ProjectFile ) $currentVersion = "0.0.0" [xml]$csproj = Get-Content $ProjectFile -Raw $versionNode = $csproj.SelectSingleNode("//Version") if ($null -ne $versionNode -and -not [string]::IsNullOrWhiteSpace($versionNode.InnerText)) { return $versionNode.InnerText.Trim() } $versionNode = $csproj.SelectSingleNode("//ApplicationDisplayVersion") if ($null -ne $versionNode -and -not [string]::IsNullOrWhiteSpace($versionNode.InnerText)) { return $versionNode.InnerText.Trim() } return $currentVersion } function Read-InnoSetupAppName { param( [Parameter(Mandatory = $true)] [string]$SetupScript ) if (!(Test-Path $SetupScript)) { return $null } $content = Get-Content $SetupScript -Raw $match = [regex]::Match($content, '#define\s+MyAppName\s+"([^"]+)"') if ($match.Success) { return $match.Groups[1].Value.Trim() } return $null } # Read OutputBaseFilename from setup.iss so we can determine the exact installer file path after ISCC runs. # The value may contain Inno preprocessor macros like {#MyAppName}/{#MyAppVersion}. function Read-InnoSetupOutputBaseFilename { param( [Parameter(Mandatory = $true)] [string]$SetupScript ) if (!(Test-Path $SetupScript)) { return $null } $content = Get-Content $SetupScript -Raw $match = [regex]::Match($content, '^\s*OutputBaseFilename\s*=\s*(.+?)\s*$', [System.Text.RegularExpressions.RegexOptions]::Multiline) if ($match.Success) { return $match.Groups[1].Value.Trim().Trim('"') } return $null } # Resolve the subset of Inno preprocessor macros used by this repo to a concrete file base name. function Resolve-InnoSetupOutputBaseFilename { param( [Parameter(Mandatory = $true)] [string]$Template, [string]$AppName, [string]$Version ) if ([string]::IsNullOrWhiteSpace($Template)) { return $null } $resolved = $Template if (![string]::IsNullOrWhiteSpace($AppName)) { $resolved = $resolved.Replace("{#MyAppName}", $AppName) } if (![string]::IsNullOrWhiteSpace($Version)) { $resolved = $resolved.Replace("{#MyAppVersion}", $Version) } return $resolved.Trim() } function Update-InnoSetupVersion { param( [Parameter(Mandatory = $true)] [string]$SetupScript, [Parameter(Mandatory = $true)] [string]$Version ) if (!(Test-Path $SetupScript)) { return } $issContent = Get-Content $SetupScript $versionFound = $false for ($i = 0; $i -lt $issContent.Count; $i++) { if ($issContent[$i] -like '#define MyAppVersion *') { $issContent[$i] = '#define MyAppVersion "' + $Version + '"' $versionFound = $true break } } if ($versionFound) { Set-Content $SetupScript -Value $issContent -Encoding UTF8 } } function Copy-Directory { param( [Parameter(Mandatory = $true)] [string]$Source, [Parameter(Mandatory = $true)] [string]$Destination ) if (Test-Path $Destination) { Remove-Item -Recurse -Force $Destination } New-Item -ItemType Directory -Path $Destination | Out-Null Copy-Item -Path (Join-Path $Source "*") -Destination $Destination -Recurse -Force } $currentVersion = Read-ProjectVersion -ProjectFile $ProjectFile Update-InnoSetupVersion -SetupScript $SetupScript -Version $currentVersion Write-Host "Publishing Hua.Todo.Maui (TFM=$TargetFramework, RID=$RuntimeIdentifier, Config=$Configuration)..." -ForegroundColor Cyan # UseMonoRuntime 在 csproj 内按 TargetFramework 做了条件配置:仅 Android 启用,其它目标关闭。 dotnet publish $ProjectFile -f $TargetFramework -c $Configuration -r $RuntimeIdentifier --self-contained false if ($LASTEXITCODE -ne 0) { Write-Error "MAUI build failed" exit 1 } $publishDir = Join-Path $ProjectDir ("bin\{0}\{1}\{2}\publish" -f $Configuration, $TargetFramework, $RuntimeIdentifier) if (!(Test-Path $publishDir)) { $publishDir = (Get-ChildItem -Path (Join-Path $ProjectDir "bin\$Configuration") -Recurse -Directory -Filter publish -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty FullName) } if (!(Test-Path $publishDir)) { Write-Error "Publish directory not found" exit 1 } $publishAppSettings = Join-Path $publishDir "appsettings.json" if (Test-Path $publishAppSettings) { $appSettingsObj = Get-Content $publishAppSettings -Raw | ConvertFrom-Json if ($null -eq $appSettingsObj.WebServer) { $appSettingsObj | Add-Member -MemberType NoteProperty -Name "WebServer" -Value ([pscustomobject]@{}) } $appSettingsObj.WebServer.IsUsingStatic = $true $appSettingsObj | ConvertTo-Json -Depth 32 | Set-Content -Path $publishAppSettings -Encoding UTF8 } else { Write-Error "Publish appsettings.json not found: $publishAppSettings" exit 1 } $desiredExeName = "$ProjectBaseName.exe" $desiredExePath = Join-Path $publishDir $desiredExeName if (!(Test-Path $desiredExePath)) { $candidateExe = $null $preferredCandidateNames = @( "Hua.Todo.Maui.exe", "Hua.Todo.exe" ) foreach ($name in $preferredCandidateNames) { $candidatePath = Join-Path $publishDir $name if (Test-Path $candidatePath) { $candidateExe = Get-Item -Path $candidatePath break } } if ($null -eq $candidateExe) { $exeFiles = Get-ChildItem -Path $publishDir -Filter "*.exe" -File -ErrorAction SilentlyContinue | Where-Object { $_.Name -notlike "*Setup*" } $candidateExe = $exeFiles | Where-Object { Test-Path (Join-Path $publishDir ($_.BaseName + ".dll")) } | Select-Object -First 1 if ($null -eq $candidateExe) { $candidateExe = $exeFiles | Sort-Object Length -Descending | Select-Object -First 1 } } if ($null -ne $candidateExe) { Rename-Item -Path $candidateExe.FullName -NewName $desiredExeName -Force } } if (!(Test-Path $desiredExePath)) { Write-Error "Publish main executable not found: $desiredExeName" exit 1 } $mauiWwwrootDir = Join-Path $ProjectDir "wwwroot" $mauiIndexPath = Join-Path $mauiWwwrootDir "index.html" if (!(Test-Path $mauiIndexPath)) { Write-Error "MAUI wwwroot not found: $mauiIndexPath" exit 1 } $publishWwwroot = Join-Path $publishDir "wwwroot" if (Test-Path $publishWwwroot) { Remove-Item -Recurse -Force $publishWwwroot } New-Item -ItemType Directory -Path $publishWwwroot | Out-Null Copy-Item -Path (Join-Path $mauiWwwrootDir "*") -Destination $publishWwwroot -Recurse -Force $installerPath = $null if (!$SkipInnoSetup.IsPresent) { $ISCC = "${env:ProgramFiles(x86)}\Inno Setup 6\ISCC.exe" if (Test-Path $ISCC) { $innoAppName = Read-InnoSetupAppName -SetupScript $SetupScript $outputDir = Join-Path $ProjectDir "Output" $outputBaseTemplate = Read-InnoSetupOutputBaseFilename -SetupScript $SetupScript $resolvedOutputBase = Resolve-InnoSetupOutputBaseFilename -Template $outputBaseTemplate -AppName $innoAppName -Version $currentVersion $expectedInstallerPath = $null if (![string]::IsNullOrWhiteSpace($resolvedOutputBase)) { $expectedInstallerPath = Join-Path $outputDir ("{0}.exe" -f $resolvedOutputBase) } if (![string]::IsNullOrWhiteSpace($innoAppName)) { # Prevent stale unversioned installer (Output\Hua.Todo.exe) from confusing local release artifacts. $oldInstallerPath = Join-Path $outputDir ("{0}.exe" -f $innoAppName) if (Test-Path $oldInstallerPath) { Remove-Item -Path $oldInstallerPath -Force } } # ISCC sometimes fails with a transient file sharing violation (e.g. antivirus/indexer holding the script/output briefly). $maxAttempts = 3 for ($attempt = 1; $attempt -le $maxAttempts; $attempt++) { $isccOutput = (& $ISCC $SetupScript 2>&1 | Out-String) $exitCode = $LASTEXITCODE if ($exitCode -eq 0) { break } Write-Host $isccOutput if ($attempt -lt $maxAttempts -and ($isccOutput -match 'Compile aborted|being used by another process|Sharing violation|process cannot access')) { Start-Sleep -Seconds 2 continue } Write-Error "Packaging failed" exit 1 } if ($null -ne $expectedInstallerPath -and (Test-Path $expectedInstallerPath)) { $installerPath = $expectedInstallerPath } if ($null -eq $installerPath) { if (Test-Path $outputDir) { $installerPath = (Get-ChildItem -Path $outputDir -Filter "*.exe" -File -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending | Select-Object -First 1 -ExpandProperty FullName) } } Write-Host "Setup package created successfully!" -ForegroundColor Green if ($null -ne $installerPath -and (Test-Path $installerPath)) { Write-Host ("Output: {0}" -f $installerPath) -ForegroundColor Green } } else { Write-Error "Inno Setup compiler not found" exit 1 } } if (![string]::IsNullOrWhiteSpace($ArtifactsRoot)) { $publishArtifactDir = Join-Path $ArtifactsRoot "publish" $installerArtifactDir = Join-Path $ArtifactsRoot "installer" Copy-Directory -Source $publishDir -Destination $publishArtifactDir if ($null -ne $installerPath -and (Test-Path $installerPath)) { if (Test-Path $installerArtifactDir) { Remove-Item -Recurse -Force $installerArtifactDir } New-Item -ItemType Directory -Path $installerArtifactDir | Out-Null $installerName = "hua.todo-$currentVersion-$RuntimeIdentifier-setup.exe" Copy-Item -Path $installerPath -Destination (Join-Path $installerArtifactDir $installerName) -Force } } if (!$SkipVersionBump.IsPresent) { $versionMatch = [regex]::Match($currentVersion, '^(?\d+)\.(?\d+)\.(?\d+)$') if ($versionMatch.Success) { $newVersion = "{0}.{1}.{2}" -f $versionMatch.Groups["major"].Value, $versionMatch.Groups["minor"].Value, ([int]$versionMatch.Groups["patch"].Value + 1) $content = Get-Content $ProjectFile -Raw if ($content -match "[^<]*") { $content = $content -replace "[^<]*", "$newVersion" Set-Content $ProjectFile -Value $content -Encoding UTF8 } else { Write-Host "Skip version bump: node not found in csproj." -ForegroundColor Yellow } } else { Write-Host "Skip version bump: version is not MAJOR.MINOR.PATCH -> $currentVersion" -ForegroundColor Yellow } }