diff --git a/build.bat b/build.bat index cfdc6c8..e6fabba 100644 --- a/build.bat +++ b/build.bat @@ -9,12 +9,15 @@ for %%A in (%*) do ( if "%ROOT:~-1%"=="\" set "ROOT=%ROOT:~0,-1%" cd /d "%ROOT%" +set "POWERSHELL_EXE=%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" +if not exist "%POWERSHELL_EXE%" set "POWERSHELL_EXE=powershell.exe" + echo ======================================== echo YMhut Box - WinUI 3 Build echo ======================================== echo. -powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%ROOT%\scripts\build-winui.ps1" %* +"%POWERSHELL_EXE%" -NoLogo -NoProfile -ExecutionPolicy Bypass -File "%ROOT%\scripts\build-winui.ps1" %* set "EXITCODE=%ERRORLEVEL%" if not "%EXITCODE%"=="0" ( echo. diff --git a/scripts/build-winui.ps1 b/scripts/build-winui.ps1 index fed5272..f902fb4 100644 --- a/scripts/build-winui.ps1 +++ b/scripts/build-winui.ps1 @@ -41,7 +41,9 @@ $ServerPublicRoot = Join-Path $Root 'server\update\public' $ServerDownloadRoot = Join-Path $ServerPublicRoot 'downloads' $ToolStateRoot = Join-Path $Root '.cache\tool_state' $NuGetRoot = Join-Path $Root '.cache\nuget' -$AppDataRoot = Join-Path $ToolStateRoot 'appdata' +$TauriHostRoot = Join-Path $Root 'src\YMhut.Box.PluginTauriHost\src-tauri' +$RepositoryCargoHome = Join-Path $Root '.cache\rust-toolchain\cargo' +$RepositoryRustupHome = Join-Path $Root '.cache\rust-toolchain\rustup' $PackageIdentityName = 'YMhut.Box' $PackagePublisher = 'CN=YMhut' @@ -50,6 +52,8 @@ $AppExecutable = 'YMhutBox.exe' $PfxPassword = 'ymhut-box-local' $PfxPath = Join-Path $OutputRoot 'certs\YMhutBox.pfx' $CerPath = Join-Path $OutputRoot 'YMhutBox.cer' +$CertificateScript = Join-Path $PSScriptRoot 'dev-certificate.ps1' +$WindowsPowerShell = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe' $DownloadBaseUri = if ($env:YMHUT_DOWNLOAD_BASE_URI) { $env:YMHUT_DOWNLOAD_BASE_URI.TrimEnd('/') + '/' } else { 'https://update.ymhut.cn/downloads/' } $UpdateBaseUri = if ($env:YMHUT_UPDATE_BASE_URI) { $env:YMHUT_UPDATE_BASE_URI.TrimEnd('/') + '/' } else { 'https://update.ymhut.cn/update-info/' } @@ -290,6 +294,60 @@ function Find-Executable([string] $Name, [string[]] $Candidates = @(), [string[] return $null } +function Find-Cargo { + $fromPath = Get-Command 'cargo.exe' -ErrorAction SilentlyContinue + if ($fromPath) { + return $fromPath.Source + } + + $repositoryCargo = Join-Path $RepositoryCargoHome 'bin\cargo.exe' + if ((Test-Path -LiteralPath $repositoryCargo) -and (Test-Path -LiteralPath $RepositoryRustupHome)) { + $env:CARGO_HOME = $RepositoryCargoHome + $env:RUSTUP_HOME = $RepositoryRustupHome + return $repositoryCargo + } + + return $null +} + +function Ensure-TauriPluginHost { + if (-not (Test-Path -LiteralPath (Join-Path $TauriHostRoot 'Cargo.toml'))) { + return $null + } + + $profile = if ($Configuration -ieq 'Release') { 'release' } else { 'debug' } + $executable = Join-Path $TauriHostRoot "target\$profile\ymhut-box-plugin-tauri-host.exe" + $cargo = Find-Cargo + if (-not $cargo) { + if (Test-Path -LiteralPath $executable) { + Write-Warning "Cargo was not found. Reusing the existing Tauri plugin host: $executable" + return $executable + } + + Write-Warning 'Cargo was not found, so the optional Tauri plugin host will not be included. Install the Rust stable MSVC toolchain to build ExternalRuntime support.' + return $null + } + + $cargoArguments = @('build', '--locked') + if ($profile -eq 'release') { + $cargoArguments += '--release' + } + + Push-Location $TauriHostRoot + try { + Invoke-Tool $cargo $cargoArguments 'Tauri plugin host build failed' + Write-Host " Tauri plugin host: $executable" + } finally { + Pop-Location + } + + if (-not (Test-Path -LiteralPath $executable)) { + throw "Cargo completed without producing the Tauri plugin host: $executable" + } + + return $executable +} + function Find-WindowsSdkTool([string] $Name) { $roots = @( (Join-Path $NuGetRoot 'microsoft.windows.sdk.buildtools'), @@ -448,45 +506,38 @@ function Ensure-MsixAssets { } function Ensure-LocalDeveloperCertificate { - New-Directory (Split-Path -Parent $PfxPath) - if ((Test-Path -LiteralPath $PfxPath) -and (Test-Path -LiteralPath $CerPath)) { - $secure = ConvertTo-SecureString -String $PfxPassword -AsPlainText -Force - $pfxCert = [Security.Cryptography.X509Certificates.X509Certificate2]::new($PfxPath, $secure) - $cerCert = [Security.Cryptography.X509Certificates.X509Certificate2]::new($CerPath) - if ($pfxCert.Thumbprint -ne $cerCert.Thumbprint) { - Export-Certificate -Cert $pfxCert -FilePath $CerPath -Force | Out-Null - } - - Import-Certificate -FilePath $CerPath -CertStoreLocation 'Cert:\CurrentUser\Root' | Out-Null - Import-Certificate -FilePath $CerPath -CertStoreLocation 'Cert:\CurrentUser\TrustedPeople' | Out-Null + if ($script:DeveloperCertificatePrepared) { return } - $secure = ConvertTo-SecureString -String $PfxPassword -AsPlainText -Force - $cert = New-SelfSignedCertificate ` - -Type CodeSigningCert ` - -Subject $PackagePublisher ` - -CertStoreLocation 'Cert:\CurrentUser\My' ` - -KeyExportPolicy Exportable ` - -KeyUsage DigitalSignature ` - -FriendlyName 'YMhut Box WinUI local signing' + if (-not (Test-Path -LiteralPath $WindowsPowerShell)) { + throw "Windows PowerShell was not found: $WindowsPowerShell" + } + if (-not (Test-Path -LiteralPath $CertificateScript)) { + throw "Developer certificate helper was not found: $CertificateScript" + } - Export-PfxCertificate -Cert $cert -FilePath $PfxPath -Password $secure | Out-Null - Export-Certificate -Cert $cert -FilePath $CerPath | Out-Null - Import-Certificate -FilePath $CerPath -CertStoreLocation 'Cert:\CurrentUser\Root' | Out-Null - Import-Certificate -FilePath $CerPath -CertStoreLocation 'Cert:\CurrentUser\TrustedPeople' | Out-Null + Invoke-ToolQuiet $WindowsPowerShell @( + '-NoLogo', '-NoProfile', '-ExecutionPolicy', 'Bypass', + '-File', $CertificateScript, + '-PfxPath', $PfxPath, + '-CerPath', $CerPath, + '-Password', $PfxPassword, + '-Subject', $PackagePublisher + ) 'Developer signing certificate preparation failed' + $script:DeveloperCertificatePrepared = $true } function Assert-ArtifactSignatureMatchesCertificate([string] $Path) { - $cerCert = [Security.Cryptography.X509Certificates.X509Certificate2]::new($CerPath) - $signature = Get-AuthenticodeSignature -FilePath $Path - if (-not $signature.SignerCertificate) { - throw "Signed artifact does not expose a signer certificate: $Path" - } - - if ($signature.SignerCertificate.Thumbprint -ne $cerCert.Thumbprint) { - throw "Signed artifact certificate thumbprint '$($signature.SignerCertificate.Thumbprint)' does not match exported certificate '$($cerCert.Thumbprint)'. Rebuild the package and keep the matching YMhutBox.cer next to it." - } + Invoke-ToolQuiet $WindowsPowerShell @( + '-NoLogo', '-NoProfile', '-ExecutionPolicy', 'Bypass', + '-File', $CertificateScript, + '-PfxPath', $PfxPath, + '-CerPath', $CerPath, + '-Password', $PfxPassword, + '-Subject', $PackagePublisher, + '-VerifyPath', $Path + ) "Signed artifact certificate validation failed for $Path" } function Get-WindowsAppRuntimePackageExtensions { @@ -1472,11 +1523,8 @@ pause Set-Content -LiteralPath $cmdPath -Value $cmd -Encoding ASCII } -New-Directory $AppDataRoot New-Directory $NuGetRoot New-Directory $OutputRoot -$env:APPDATA = $AppDataRoot -$env:LOCALAPPDATA = $AppDataRoot $env:DOTNET_CLI_HOME = Join-Path $ToolStateRoot 'dotnet' $env:NUGET_PACKAGES = $NuGetRoot $env:DOTNET_CLI_TELEMETRY_OPTOUT = '1' @@ -1491,6 +1539,7 @@ Write-Host " Target: $Target" Write-Host " Version: $($versionInfo.Version) build $($versionInfo.Build) ($($versionInfo.PackageVersion))" Ensure-MsixAssets +$tauriHostExecutable = Ensure-TauriPluginHost Invoke-DotNet @( 'restore', $Solution, @@ -1514,6 +1563,12 @@ foreach ($ridProject in @($Project, $InstallerBootstrapProject)) { } Publish-UnpackagedApp $versionInfo +if ($tauriHostExecutable) { + $publishedTauriHost = Join-Path $PublishRoot 'tauri-host\ymhut-box-plugin-tauri-host.exe' + if (-not (Test-Path -LiteralPath $publishedTauriHost)) { + throw "The Tauri plugin host was built but was not included in the publish payload: $publishedTauriHost" + } +} if (($Target -in @('exe', 'both')) -and -not $SkipExe) { $exeBuilt = Build-InnoInstaller $versionInfo $signTool -AllowMissingCompiler:($Target -eq 'both') diff --git a/scripts/dev-certificate.ps1 b/scripts/dev-certificate.ps1 new file mode 100644 index 0000000..3fe46dc --- /dev/null +++ b/scripts/dev-certificate.ps1 @@ -0,0 +1,76 @@ +param( + [Parameter(Mandatory = $true)] + [string] $PfxPath, + + [Parameter(Mandatory = $true)] + [string] $CerPath, + + [Parameter(Mandatory = $true)] + [string] $Password, + + [Parameter(Mandatory = $true)] + [string] $Subject, + + [string] $VerifyPath = '' +) + +$ErrorActionPreference = 'Stop' +$securityModule = Join-Path $PSHOME 'Modules\Microsoft.PowerShell.Security\Microsoft.PowerShell.Security.psd1' +$pkiModule = Join-Path $PSHOME 'Modules\PKI\PKI.psd1' +Import-Module $securityModule -ErrorAction Stop +Import-Module $pkiModule -ErrorAction Stop + +if ($VerifyPath) { + if (-not (Test-Path -LiteralPath $VerifyPath)) { + throw "Signed artifact was not found: $VerifyPath" + } + if (-not (Test-Path -LiteralPath $CerPath)) { + throw "Signing certificate was not found: $CerPath" + } + + $certificate = [Security.Cryptography.X509Certificates.X509Certificate2]::new($CerPath) + $signature = Get-AuthenticodeSignature -FilePath $VerifyPath + if (-not $signature.SignerCertificate) { + throw "Signed artifact does not expose a signer certificate: $VerifyPath" + } + if ($signature.SignerCertificate.Thumbprint -ne $certificate.Thumbprint) { + throw "Signed artifact certificate does not match the exported developer certificate: $VerifyPath" + } + exit 0 +} + +$parent = Split-Path -Parent $PfxPath +if ($parent) { + New-Item -ItemType Directory -Force -Path $parent | Out-Null +} + +$securePassword = ConvertTo-SecureString -String $Password -AsPlainText -Force +if (Test-Path -LiteralPath $PfxPath) { + $certificate = [Security.Cryptography.X509Certificates.X509Certificate2]::new($PfxPath, $securePassword) + $cerMatches = $false + if (Test-Path -LiteralPath $CerPath) { + $existingCer = [Security.Cryptography.X509Certificates.X509Certificate2]::new($CerPath) + $cerMatches = $existingCer.Thumbprint -eq $certificate.Thumbprint + } + if (-not $cerMatches) { + Export-Certificate -Cert $certificate -FilePath $CerPath -Force | Out-Null + } +} else { + $certificate = New-SelfSignedCertificate ` + -Type CodeSigningCert ` + -Subject $Subject ` + -CertStoreLocation 'Cert:\CurrentUser\My' ` + -KeyExportPolicy Exportable ` + -KeyUsage DigitalSignature ` + -FriendlyName 'YMhut Box WinUI local signing' + + Export-PfxCertificate -Cert $certificate -FilePath $PfxPath -Password $securePassword | Out-Null + Export-Certificate -Cert $certificate -FilePath $CerPath -Force | Out-Null +} + +foreach ($store in @('Cert:\CurrentUser\Root', 'Cert:\CurrentUser\TrustedPeople')) { + $present = Get-ChildItem -Path $store | Where-Object Thumbprint -eq $certificate.Thumbprint + if (-not $present) { + Import-Certificate -FilePath $CerPath -CertStoreLocation $store | Out-Null + } +}