scripts/PwX-Build.ps1
|
#Requires -Version 5.1 <# .SYNOPSIS PwX Build Engine - Standalone .ps1 -> .exe converter (CLI). .DESCRIPTION Parses source script syntax WITHOUT EXECUTING it during compilation. PS7 : src\PwX.BuildHost.PS7 (PowerShell SDK host) -> dotnet publish PS51 : src\PwX.BuildHost.PS51 (C# launcher) -> .NET Framework csc.exe .EXAMPLE .\scripts\PwX-Build.ps1 -Source .\tests\samples\hello.ps1 -Output .\out\hello.exe -Target PS7 -SingleFile .EXAMPLE .\scripts\PwX-Build.ps1 -Source .\app.ps1 -Target PS51 -Mode Gui -Icon .\app.ico -ProductName "MyApp" #> [CmdletBinding()] param( [Parameter(Mandatory)][string]$Source, [string]$Output, [ValidateSet('PS51', 'PS7')][string]$Target = 'PS7', [ValidateSet('Console', 'Gui')][string]$Mode = 'Console', [ValidateSet('SelfContained', 'FrameworkDependent')][string]$RuntimeMode = 'SelfContained', [ValidateSet('win-x86', 'win-x64', 'win-arm64')][string]$Architecture = 'win-x64', [switch]$SingleFile, [string]$Icon, [string]$ProductName, [string]$CompanyName, [string]$Description, [string]$FileVersion = '1.0.0.0', [string]$ProductVersion, [string]$PsSdkVersion = '7.4.6', # Roadmap 4.4: pinned tested version [string]$Framework = 'net8.0', # PS SDK 7.4.x -> net8.0 [string]$LogPath, [switch]$Force, [switch]$KeepTemp, [switch]$EnableTrim, [switch]$EnableUpx, [switch]$AutoInstallPrereqs, # PS7: auto-install missing .NET SDK 8+ / repair NuGet source [bool]$Admin = $false, [bool]$Utf8 = $true, [bool]$Unicode = $false, [bool]$Dpi = $false, [bool]$Config = $false, [bool]$SupportOS = $false, [bool]$LongPaths = $false, [bool]$NoOutput = $false ) Set-StrictMode -Version 2.0 $ErrorActionPreference = 'Stop' $script:Root = Split-Path -Parent $PSScriptRoot $script:Diags = New-Object System.Collections.Generic.List[object] $script:LogFile = $null $script:EnvSaved = $null $script:Utf8NoBom = New-Object System.Text.UTF8Encoding($false) $script:Utf8Bom = New-Object System.Text.UTF8Encoding($true) # ---------------------------------------------------------------- Helpers function Write-Log { param([string]$Msg, [ValidateSet('Info', 'Warning', 'Error', 'Debug')][string]$Level = 'Info') $line = '[{0:HH:mm:ss}] [{1}] {2}' -f (Get-Date), $Level.ToUpperInvariant(), $Msg if ($script:LogFile) { [IO.File]::AppendAllText($script:LogFile, $line + [Environment]::NewLine, $script:Utf8NoBom) } switch ($Level) { 'Info' { Write-Host $line } 'Warning' { Write-Host $line -ForegroundColor Yellow } 'Error' { Write-Host $line -ForegroundColor Red } 'Debug' { } # Log file only } } function Add-Diag { param([string]$Severity, [string]$Code, [string]$Message, [int]$Line = 0, [int]$Column = 0) $script:Diags.Add([pscustomobject]@{ Severity = $Severity; Code = $Code; Message = $Message; SourcePath = $script:SrcPath; Line = $Line; Column = $Column }) $loc = if ($Line -gt 0) { " (line $Line, col $Column)" } else { '' } Write-Log "$Code $Message$loc" $Severity } function Stop-Build { param([string]$Code, [string]$Message, [int]$Line = 0, [int]$Column = 0) Add-Diag 'Error' $Code $Message $Line $Column throw 'PWX_BUILD_FAILED' } function Invoke-Native { param([string]$File, [string[]]$Arguments, [string]$WorkDir) $old = $ErrorActionPreference; $ErrorActionPreference = 'Continue' Push-Location -LiteralPath $WorkDir try { & $File @Arguments 2>&1 | ForEach-Object { Write-Log ([string]$_) 'Debug' } $code = $LASTEXITCODE } finally { Pop-Location; $ErrorActionPreference = $old } return $code } function Get-ScriptText { param([string]$Path) $bytes = [IO.File]::ReadAllBytes($Path) $start = 0 if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 } try { $strict = New-Object System.Text.UTF8Encoding($false, $true) return $strict.GetString($bytes, $start, $bytes.Length - $start) } catch { Add-Diag 'Warning' 'PWX2001' 'Source is not valid UTF-8; reading as fallback encoding. Saving source with UTF-8 BOM is recommended.' try { $enc = [Text.Encoding]::GetEncoding(1252) } catch { $enc = [Text.Encoding]::Default } return $enc.GetString($bytes) } } function ConvertTo-CsLiteral { param([string]$s) if ($null -eq $s) { $s = '' }; ($s -replace '\\', '\\' -replace '"', '\"' -replace "[\r\n]+", ' ') } function ConvertTo-XmlText { param([string]$s) [Security.SecurityElement]::Escape($s) } function New-AppManifest { param([string]$Path, [bool]$RequireAdmin, [bool]$DpiAware, [bool]$OsSupport, [bool]$LongPathAware) $execution = if ($RequireAdmin) { 'requireAdministrator' } else { 'asInvoker' } $windows = if ($DpiAware -or $LongPathAware) { $dpiNode = if ($DpiAware) { '<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware><dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>' } else { '' } $longNode = if ($LongPathAware) { '<longPathAware xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">true</longPathAware>' } else { '' } '<application xmlns="urn:schemas-microsoft-com:asm.v3"><windowsSettings>' + $dpiNode + $longNode + '</windowsSettings></application>' } else { '' } $compat = if ($OsSupport) { '<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1"><application><supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/><supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/></application></compatibility>' } else { '' } $xml = '<?xml version="1.0" encoding="utf-8" standalone="yes"?><assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0"><assemblyIdentity version="1.0.0.0" processorArchitecture="*" name="PwXApp" type="win32"/><trustInfo xmlns="urn:schemas-microsoft-com:asm.v3"><security><requestedPrivileges><requestedExecutionLevel level="{0}" uiAccess="false" /></requestedPrivileges></security></trustInfo>{1}{2}</assembly>' -f $execution, $windows, $compat [IO.File]::WriteAllText($Path, $xml, $script:Utf8NoBom) } function New-AppConfig { param([string]$Path) $xml = '<?xml version="1.0" encoding="utf-8"?><configuration><startup useLegacyV2RuntimeActivationPolicy="true"><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0"/></startup></configuration>' [IO.File]::WriteAllText($Path, $xml, $script:Utf8NoBom) } # ---------------------------------------------------------------- PS7 prerequisites function Invoke-NativeLines { param([string]$File, [string[]]$Arguments) $old = $ErrorActionPreference; $ErrorActionPreference = 'Continue' try { $out = @(& $File @Arguments 2>&1 | ForEach-Object { [string]$_ }) $code = $LASTEXITCODE } finally { $ErrorActionPreference = $old } [pscustomobject]@{ Code = $code; Lines = $out } } function Get-DotnetSdkInfo { $cands = New-Object System.Collections.Generic.List[string] $cmd = Get-Command dotnet -ErrorAction SilentlyContinue if ($cmd) { $cands.Add($cmd.Source) } $roots = @($env:ProgramFiles, ${env:ProgramFiles(x86)}) if ($env:LOCALAPPDATA) { $roots += (Join-Path $env:LOCALAPPDATA 'Microsoft') } foreach ($root in $roots) { if ($root) { $p = Join-Path $root 'dotnet\dotnet.exe' if (Test-Path -LiteralPath $p) { $cands.Add($p) } } } foreach ($c in ($cands | Select-Object -Unique)) { $r = Invoke-NativeLines $c @('--list-sdks') $vers = @($r.Lines | ForEach-Object { ($_ -split ' ')[0] } | Where-Object { $_ -match '^(\d+)\.' -and [int]$Matches[1] -ge 8 }) if ($vers.Count -gt 0) { return [pscustomobject]@{ Path = $c; Version = $vers[$vers.Count - 1] } } } return $null } function Install-DotnetSdk8 { $winget = Get-Command winget -ErrorAction SilentlyContinue if ($winget) { Write-Log 'Installing .NET SDK 8 via winget (may require elevation, may take a few minutes)...' $r = Invoke-NativeLines $winget.Source @('install', '--id', 'Microsoft.DotNet.SDK.8', '--exact', '--silent', '--accept-package-agreements', '--accept-source-agreements') foreach ($l in $r.Lines) { Write-Log $l 'Debug' } $info = Get-DotnetSdkInfo if ($info) { return $info } Write-Log "winget installation could not be verified (exit code $($r.Code)); trying fallback method." 'Warning' } else { Write-Log 'winget not found; using fallback method (dotnet-install.ps1).' 'Warning' } try { $dir = Join-Path $env:LOCALAPPDATA 'Microsoft\dotnet' $tmp = Join-Path ([IO.Path]::GetTempPath()) 'dotnet-install.ps1' try { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 } catch { Write-Log "Could not set TLS 1.2: $($_.Exception.Message)" 'Debug' } Write-Log "Installing .NET SDK 8 via official dotnet-install.ps1 to user directory: $dir" Invoke-WebRequest -Uri 'https://dot.net/v1/dotnet-install.ps1' -OutFile $tmp -UseBasicParsing $psExe = (Get-Command powershell.exe -ErrorAction Stop).Source $r = Invoke-NativeLines $psExe @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $tmp, '-Channel', '8.0', '-InstallDir', $dir) foreach ($l in $r.Lines) { Write-Log $l 'Debug' } } catch { Write-Log "Fallback installation failed: $($_.Exception.Message)" 'Warning' } return (Get-DotnetSdkInfo) } function Get-NuGetSources { param([string]$DotnetExe) $r = Invoke-NativeLines $DotnetExe @('nuget', 'list', 'source') $list = New-Object System.Collections.Generic.List[object] foreach ($l in $r.Lines) { if ($l -match '^\s*\d+\.\s+(.+?)\s+\[(Enabled|Disabled)\]\s*$') { $list.Add([pscustomobject]@{ Name = $Matches[1]; Enabled = ($Matches[2] -eq 'Enabled'); Url = '' }) } elseif ($list.Count -gt 0 -and $l -match '^\s+(\S.*?)\s*$') { $last = $list[$list.Count - 1] if (-not $last.Url) { $last.Url = $Matches[1] } } } [pscustomobject]@{ Sources = $list.ToArray(); NoneFound = (($r.Lines -join "`n") -match 'No sources found') } } function Test-NuGetReady { param([string]$DotnetExe) $s = Get-NuGetSources $DotnetExe $enabled = @($s.Sources | Where-Object { $_.Enabled }) if ($enabled.Count -gt 0) { return $true } if ($s.NoneFound -or @($s.Sources).Count -gt 0) { return $false } return $true } function Repair-NuGetSources { param([string]$DotnetExe) $s = Get-NuGetSources $DotnetExe $org = @($s.Sources | Where-Object { $_.Url -match 'api\.nuget\.org' }) | Select-Object -First 1 if ($org) { $r = Invoke-NativeLines $DotnetExe @('nuget', 'enable', 'source', $org.Name) } else { $r = Invoke-NativeLines $DotnetExe @('nuget', 'add', 'source', 'https://api.nuget.org/v3/index.json', '--name', 'nuget.org') } foreach ($l in $r.Lines) { Write-Log $l 'Debug' } return (Test-NuGetReady $DotnetExe) } # ---------------------------------------------------------------- Main build pipeline $sw = [Diagnostics.Stopwatch]::StartNew() $workDir = $null $result = [ordered]@{ Success = $false; OutputPath = $null; Target = $Target; Duration = $null; OutputSize = 0; LogPath = $null; Diagnostics = $script:Diags } $script:SrcPath = $Source try { # Logging setup if (-not $LogPath) { $LogPath = Join-Path $script:Root ('artifacts\logs\build-{0:yyyyMMdd-HHmmss}.log' -f (Get-Date)) } New-Item -ItemType Directory -Force -Path (Split-Path -Parent $LogPath) | Out-Null $script:LogFile = $LogPath $result.LogPath = $LogPath Write-Log "PwX Build Engine starting. Target=$Target Mode=$Mode Runtime=$RuntimeMode" # 1) Source validation if (-not (Test-Path -LiteralPath $Source -PathType Leaf)) { Stop-Build 'PWX1001' "Source file not found: $Source" } $src = (Resolve-Path -LiteralPath $Source).ProviderPath $script:SrcPath = $src if ([IO.Path]::GetExtension($src) -ne '.ps1') { Add-Diag 'Warning' 'PWX1002' 'Source file extension is not .ps1.' } # 2) Output path resolution if (-not $Output) { $Output = [IO.Path]::ChangeExtension($src, '.exe') } if (-not [IO.Path]::GetExtension($Output)) { $Output += '.exe' } $Output = [IO.Path]::GetFullPath($(if ([IO.Path]::IsPathRooted($Output)) { $Output } else { Join-Path (Get-Location).ProviderPath $Output })) if ($Output -ieq $src) { Stop-Build 'PWX1004' 'Output path cannot be identical to source file.' } $outDir = Split-Path -Parent $Output try { New-Item -ItemType Directory -Force -Path $outDir | Out-Null $probe = Join-Path $outDir ('.pwx-probe-' + [guid]::NewGuid().ToString('N')) [IO.File]::WriteAllText($probe, 'x') Remove-Item -LiteralPath $probe -Force } catch { Stop-Build 'PWX1005' "Output directory is not writable: $outDir" } $baseName = [IO.Path]::GetFileNameWithoutExtension($Output) # 3) Metadata validation if ($FileVersion -notmatch '^\d+(\.\d+){1,3}$') { Stop-Build 'PWX1007' "FileVersion is invalid: $FileVersion (e.g. 1.2.3.4)" } if (-not $ProductVersion) { $ProductVersion = $FileVersion } if (-not $ProductName) { $ProductName = $baseName } $iconFull = $null if ($Icon) { if (-not (Test-Path -LiteralPath $Icon -PathType Leaf)) { Stop-Build 'PWX1008' "Icon file not found: $Icon" } $iconFull = (Resolve-Path -LiteralPath $Icon).ProviderPath $hdr = [IO.File]::ReadAllBytes($iconFull) if ($hdr.Length -lt 6 -or $hdr[0] -ne 0 -or $hdr[1] -ne 0 -or $hdr[2] -ne 1 -or $hdr[3] -ne 0) { Stop-Build 'PWX1009' 'Icon file is not a valid .ico file.' } } # 4) Syntax parsing (WITHOUT EXECUTING) $tokens = $null; $errs = $null [void][System.Management.Automation.Language.Parser]::ParseFile($src, [ref]$tokens, [ref]$errs) if ($errs -and $errs.Count -gt 0) { foreach ($e in $errs) { Add-Diag 'Error' 'PWX1003' $e.Message $e.Extent.StartLineNumber $e.Extent.StartColumnNumber } throw 'PWX_BUILD_FAILED' } Write-Log 'Syntax check passed.' # 5) Destination safety checks $finalExe = $Output $distDir = $null if ($Target -eq 'PS7' -and -not $SingleFile) { $distDir = Join-Path $outDir $baseName $finalExe = Join-Path $distDir ($baseName + '.exe') if ((Test-Path -LiteralPath $distDir) -and -not $Force) { Stop-Build 'PWX1006' "Target folder already exists: $distDir (use -Force to overwrite)" } if ((Test-Path -LiteralPath $distDir) -and -not (Test-Path -LiteralPath (Join-Path $distDir '.pwx-build'))) { Stop-Build 'PWX1006' "Target folder was not created by PwX, refusing to delete: $distDir" } } elseif ((Test-Path -LiteralPath $Output) -and -not $Force) { Stop-Build 'PWX1006' "Output file already exists: $Output (use -Force to overwrite)" } # 6) Unique temporary workspace $workDir = Join-Path ([IO.Path]::GetTempPath()) ('pwx-build-' + [guid]::NewGuid().ToString('N')) New-Item -ItemType Directory -Path $workDir | Out-Null Write-Log "Workspace: $workDir" 'Debug' $manifest = $null if ($Admin -or $Dpi -or $SupportOS -or $LongPaths) { $manifest = Join-Path $workDir 'app.manifest' New-AppManifest $manifest $Admin $Dpi $SupportOS $LongPaths Write-Log "Windows application manifest generated (Admin=$Admin, DPI=$Dpi, SupportOS=$SupportOS, LongPaths=$LongPaths)." 'Debug' } # 7) Payload: normalize with UTF-8 BOM $payload = Join-Path $workDir 'payload.ps1' [IO.File]::WriteAllText($payload, (Get-ScriptText $src), $script:Utf8Bom) $gui = ($Mode -eq 'Gui') if ($Target -eq 'PS7') { # ------------------------------------------------ PowerShell 7 backend if ($EnableTrim -and $RuntimeMode -ne 'SelfContained') { Stop-Build 'PWX3007' '-EnableTrim can only be used with -RuntimeMode SelfContained. Note: Trimming is experimental for PowerShell SDK.' } if ($RuntimeMode -eq 'FrameworkDependent') { $needs = if ($Mode -eq 'Gui') { '.NET 8 Desktop Runtime (Microsoft.WindowsDesktop.App 8.x)' } else { '.NET 8 Runtime (Microsoft.NETCore.App 8.x)' } Write-Log "WARNING: FrameworkDependent output requires $needs on target machine. pwsh is not required; PowerShell engine is included." 'Warning' } $script:EnvSaved = @{ DOTNET_CLI_UI_LANGUAGE = $env:DOTNET_CLI_UI_LANGUAGE; VSLANG = $env:VSLANG } $env:DOTNET_CLI_UI_LANGUAGE = 'en'; $env:VSLANG = '1033' $sdkInfo = Get-DotnetSdkInfo if (-not $sdkInfo) { if (-not $AutoInstallPrereqs) { Stop-Build 'PWX3005' '.NET SDK 8+ is not installed. Use -AutoInstallPrereqs or install manually: winget install Microsoft.DotNet.SDK.8' } Write-Log '.NET SDK 8+ not found; starting automatic installation...' 'Warning' $sdkInfo = Install-DotnetSdk8 if (-not $sdkInfo) { Stop-Build 'PWX3005' '.NET SDK 8 could not be installed automatically. Install manually: winget install Microsoft.DotNet.SDK.8' } Write-Log ".NET SDK $($sdkInfo.Version) installed." } $dotnet = [pscustomobject]@{ Source = $sdkInfo.Path } Write-Log ".NET SDK $($sdkInfo.Version): $($sdkInfo.Path)" 'Debug' if (-not (Test-NuGetReady $sdkInfo.Path)) { if (-not $AutoInstallPrereqs) { Stop-Build 'PWX3006' 'No active NuGet package source; cannot restore PowerShell SDK. Use -AutoInstallPrereqs or run: dotnet nuget add source https://api.nuget.org/v3/index.json -n nuget.org' } Write-Log 'No active NuGet source found; adding/enabling nuget.org...' 'Warning' if (-not (Repair-NuGetSources $sdkInfo.Path)) { Stop-Build 'PWX3006' 'Could not repair NuGet source. Run: dotnet nuget add source https://api.nuget.org/v3/index.json -n nuget.org' } } $hostSrc = Join-Path $script:Root 'src\PwX.BuildHost.PS7' if (-not (Test-Path -LiteralPath $hostSrc)) { Stop-Build 'PWX3002' "Host project not found: $hostSrc" } $hostDir = Join-Path $workDir 'host' Copy-Item -LiteralPath $hostSrc -Destination $hostDir -Recurse Get-ChildItem -LiteralPath $hostDir -Include bin, obj -Recurse -Directory -ErrorAction SilentlyContinue | Remove-Item -Recurse -Force $lines = New-Object System.Collections.Generic.List[string] $lines.Add('<Project><PropertyGroup>') $lines.Add('<PwxPayload>' + (ConvertTo-XmlText $payload) + '</PwxPayload>') $lines.Add('<PwxGui>' + $gui.ToString().ToLowerInvariant() + '</PwxGui>') $lines.Add('<PwxUtf8>' + (($Utf8 -or $Unicode).ToString().ToLowerInvariant()) + '</PwxUtf8>') $lines.Add('<PwxNoOutput>' + $NoOutput.ToString().ToLowerInvariant() + '</PwxNoOutput>') $lines.Add('<PwxSelfContained>' + ($RuntimeMode -eq 'SelfContained').ToString().ToLowerInvariant() + '</PwxSelfContained>') $lines.Add('<PwxFramework>' + (ConvertTo-XmlText $Framework) + '</PwxFramework>') $lines.Add('<PwxPsSdkVersion>' + (ConvertTo-XmlText $PsSdkVersion) + '</PwxPsSdkVersion>') if ($iconFull) { $lines.Add('<PwxIcon>' + (ConvertTo-XmlText $iconFull) + '</PwxIcon>') } $lines.Add('<Product>' + (ConvertTo-XmlText $ProductName) + '</Product>') if ($CompanyName) { $lines.Add('<Company>' + (ConvertTo-XmlText $CompanyName) + '</Company>') } if ($Description) { $lines.Add('<Description>' + (ConvertTo-XmlText $Description) + '</Description>') } if ($manifest) { $lines.Add('<PwxManifest>' + (ConvertTo-XmlText $manifest) + '</PwxManifest>') } $lines.Add('<AssemblyVersion>' + $FileVersion + '</AssemblyVersion>') $lines.Add('<FileVersion>' + $FileVersion + '</FileVersion>') $lines.Add('<InformationalVersion>' + (ConvertTo-XmlText $ProductVersion) + '</InformationalVersion>') $lines.Add('</PropertyGroup></Project>') [IO.File]::WriteAllText((Join-Path $hostDir 'pwx.props'), ($lines -join "`r`n"), $script:Utf8Bom) $pub = Join-Path $workDir 'publish' $pa = @('publish', (Join-Path $hostDir 'PwX.BuildHost.PS7.csproj'), '-c', 'Release', '-r', $Architecture, '-o', $pub, '--nologo', '-v', 'minimal', '-p:DebugType=none', '-p:DebugSymbols=false') $pa += $(if ($RuntimeMode -eq 'SelfContained') { @('--self-contained', 'true') } else { @('--self-contained', 'false') }) if ($SingleFile) { $pa += @('-p:PublishSingleFile=true', '-p:IncludeNativeLibrariesForSelfExtract=true', '-p:IncludeAllContentForSelfExtract=true') } if ($EnableTrim) { Write-Log 'WARNING: -EnableTrim is experimental; verify output before distribution.' 'Warning' $pa += @('-p:PublishTrimmed=true') } Write-Log "dotnet publish running (PowerShell SDK packages will be downloaded on first run)..." $code = Invoke-Native $dotnet.Source $pa $workDir if ($code -ne 0) { $why = '' $logText = '' try { $logText = [IO.File]::ReadAllText($script:LogFile) } catch { Write-Log "Could not read log file: $($_.Exception.Message)" 'Debug' } if ($logText -match 'NU1301') { $why = ' Unable to reach NuGet server (check internet connection or proxy).' } elseif ($logText -match 'NU110[01]') { $why = ' Could not resolve NuGet packages; verify source: dotnet nuget list source' } Stop-Build 'PWX3003' "dotnet publish failed (exit code $code).$why See log for details: $LogPath" } $built = Join-Path $pub 'PwXApp.exe' if (-not (Test-Path -LiteralPath $built)) { Stop-Build 'PWX3004' 'PwXApp.exe not found in publish output.' } # Deploy built-in PowerShell modules ($PSHOME\Modules) to output $runtimesMod = Join-Path $pub "runtimes\win\lib\$Framework\Modules" if (-not (Test-Path -LiteralPath $runtimesMod)) { $runtimesMod = Join-Path $pub "runtimes\win\lib\net8.0\Modules" } $targetMod = Join-Path $pub 'Modules' if ((Test-Path -LiteralPath $runtimesMod) -and -not (Test-Path -LiteralPath $targetMod)) { Copy-Item -LiteralPath $runtimesMod -Destination $targetMod -Recurse -Force } if ($SingleFile) { Copy-Item -LiteralPath $built -Destination $Output -Force } else { if (Test-Path -LiteralPath $distDir) { Remove-Item -LiteralPath $distDir -Recurse -Force } New-Item -ItemType Directory -Path $distDir | Out-Null Copy-Item -Path (Join-Path $pub '*') -Destination $distDir -Recurse -Force Rename-Item -LiteralPath (Join-Path $distDir 'PwXApp.exe') -NewName ($baseName + '.exe') [IO.File]::WriteAllText((Join-Path $distDir '.pwx-build'), "PwX Build Engine`r`n", $script:Utf8NoBom) } if ($Config) { $configPath = if ($SingleFile) { "$Output.config" } else { Join-Path $distDir ($baseName + '.exe.config') } New-AppConfig $configPath } # Optional: UPX compression if ($EnableUpx) { $upx = Get-Command upx -ErrorAction SilentlyContinue if (-not $upx) { Write-Log 'UPX not found; compression skipped.' 'Warning' } elseif ($SingleFile) { Write-Log 'UPX skipped: single-file packages may become corrupted with UPX.' 'Warning' } else { $exeToCompress = Join-Path $distDir ($baseName + '.exe') Write-Log "Applying UPX compression: $exeToCompress" $r = Invoke-NativeLines $upx.Source @('--best', '--lzma', $exeToCompress) foreach ($l in $r.Lines) { Write-Log $l 'Debug' } if ($r.Code -ne 0) { Write-Log "UPX failed (exit code $($r.Code)); check output." 'Warning' } else { Write-Log 'UPX compression completed.' } } } } else { # ------------------------------------------------ Windows PowerShell 5.1 backend $csc = @("$env:windir\Microsoft.NET\Framework64\v4.0.30319\csc.exe", "$env:windir\Microsoft.NET\Framework\v4.0.30319\csc.exe") | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1 if (-not $csc) { Stop-Build 'PWX3101' '.NET Framework csc.exe not found.' } $stubSrc = Join-Path $script:Root 'src\PwX.BuildHost.PS51\Stub.cs' if (-not (Test-Path -LiteralPath $stubSrc)) { Stop-Build 'PWX3102' "Stub source not found: $stubSrc" } Copy-Item -LiteralPath $stubSrc -Destination (Join-Path $workDir 'Stub.cs') $asmInfo = @" using System.Reflection; [assembly: AssemblyTitle("$(ConvertTo-CsLiteral $(if ($Description) { $Description } else { $ProductName }))")] [assembly: AssemblyCompany("$(ConvertTo-CsLiteral $CompanyName)")] [assembly: AssemblyProduct("$(ConvertTo-CsLiteral $ProductName)")] [assembly: AssemblyVersion("$FileVersion")] [assembly: AssemblyFileVersion("$FileVersion")] [assembly: AssemblyInformationalVersion("$(ConvertTo-CsLiteral $ProductVersion)")] "@ [IO.File]::WriteAllText((Join-Path $workDir 'AssemblyInfo.cs'), $asmInfo, $script:Utf8Bom) $rsp = New-Object System.Collections.Generic.List[string] $rsp.Add('/nologo'); $rsp.Add($(if ($gui) { '/target:winexe' } else { '/target:exe' })) $platform = if ($Architecture -eq 'win-x86') { 'x86' } elseif ($Architecture -eq 'win-arm64') { 'anycpu' } else { 'x64' } $rsp.Add('/platform:' + $platform); $rsp.Add('/optimize+'); $rsp.Add('/debug-') if ($Utf8 -or $Unicode) { $rsp.Add('/utf8output'); $rsp.Add('/define:PWX_UTF8_OUTPUT') } if ($NoOutput) { $rsp.Add('/define:PWX_NO_OUTPUT') } if ($gui) { $rsp.Add('/define:PWX_GUI') } $rsp.Add('/out:out.exe') $rsp.Add('/resource:payload.ps1,PwX.payload.ps1') if ($iconFull) { Copy-Item -LiteralPath $iconFull -Destination (Join-Path $workDir 'icon.ico'); $rsp.Add('/win32icon:icon.ico') } if ($manifest) { $rsp.Add('/win32manifest:app.manifest') } $rsp.Add('Stub.cs'); $rsp.Add('AssemblyInfo.cs') [IO.File]::WriteAllText((Join-Path $workDir 'build.rsp'), ($rsp -join "`r`n"), $script:Utf8Bom) Write-Log 'csc.exe compiling...' $code = Invoke-Native $csc @('@build.rsp') $workDir if ($code -ne 0 -or -not (Test-Path -LiteralPath (Join-Path $workDir 'out.exe'))) { Stop-Build 'PWX3103' "csc compilation failed (exit code $code). Details: $LogPath" } Copy-Item -LiteralPath (Join-Path $workDir 'out.exe') -Destination $Output -Force if ($Config) { New-AppConfig "$Output.config" } } # 8) Verify executable output if (-not (Test-Path -LiteralPath $finalExe)) { Stop-Build 'PWX4001' 'Output executable was not created.' } $fi = Get-Item -LiteralPath $finalExe $mz = [IO.File]::ReadAllBytes($finalExe)[0..1] if ($fi.Length -le 0 -or $mz[0] -ne 0x4D -or $mz[1] -ne 0x5A) { Stop-Build 'PWX4002' 'Output is not a valid Windows executable.' } $result.Success = $true $result.OutputPath = $finalExe $result.OutputSize = $fi.Length Write-Log ('SUCCESS: {0} ({1:N1} MB)' -f $finalExe, ($fi.Length / 1MB)) } catch { if ($_.Exception.Message -ne 'PWX_BUILD_FAILED') { Add-Diag 'Error' 'PWX9000' ("Unexpected error: " + $_.Exception.Message) } Write-Log 'BUILD FAILED.' 'Error' } finally { if ($script:EnvSaved) { $env:DOTNET_CLI_UI_LANGUAGE = $script:EnvSaved.DOTNET_CLI_UI_LANGUAGE $env:VSLANG = $script:EnvSaved.VSLANG } if ($workDir -and (Test-Path -LiteralPath $workDir) -and -not $KeepTemp) { try { Remove-Item -LiteralPath $workDir -Recurse -Force -ErrorAction Stop } catch { Write-Log "Could not remove temp folder: $workDir" 'Warning' } } $sw.Stop() $result.Duration = $sw.Elapsed Write-Log ('Duration: {0:N1} s | Log: {1}' -f $sw.Elapsed.TotalSeconds, $LogPath) } [pscustomobject]$result if (-not $result.Success) { exit 1 } |