1
0
mirror of https://github.com/bitwarden/browser synced 2026-01-31 08:43:54 +00:00
Files
browser/apps/desktop/scripts/appx-cross-build.ps1
Isaiah Inuwa 881afacded Enable cross-compilation and packaging of Windows Appx from macOS (#17976)
* Enable cross-compilation and packaging of Windows Appx from macOS

* Consolidate cargo build execution into a single function in native build script

* Install cargo-xwin when needed

* Install Appx tools when needed

* Consolidate command execution into a single function in native build script

* Only include the native node modules for the appropriate platform

electron-builder's globs interact strangely, so we can't
exclude all the .node files in the global config and then
include the platform-specific files in the platform
configuration.

* Always copy Rust binaries to dist folder

* Log source and destination when copying files

* Update copyright

* Match Electron version in Beta build
2026-01-09 20:18:17 +00:00

227 lines
8.1 KiB
PowerShell
Executable File

#!/usr/bin/env pwsh
<#
.SYNOPSIS
Script to build, package and sign the Bitwarden desktop client as a Windows Appx
package.
.DESCRIPTION
This script provides cross-platform support for packaging and signing the
Bitwarden desktop client as a Windows Appx package.
Currently, only macOS -> Windows Appx is supported, but Linux -> Windows Appx
could be added in the future by providing Linux binaries for the msix-packaging
project.
.NOTES
The reason this script exists is because electron-builder does not currently
support cross-platform Appx packaging without proprietary tools (Parallels
Windows VM). This script uses third-party tools (makemsix from msix-packaging
and osslsigncode) to package and sign the Appx.
The signing certificate must have the same subject as the publisher name. This
can be generated on the Windows target using PowerShell 5.1 and copied to the
host, or directly on the host with OpenSSL.
Using Windows PowerShell 5.1:
```powershell
$publisher = "CN=Bitwarden Inc., O=Bitwarden Inc., L=Santa Barbara, S=California, C=US, SERIALNUMBER=7654941, OID.2.5.4.15=Private Organization, OID.1.3.6.1.4.1.311.60.2.1.2=Delaware, OID.1.3.6.1.4.1.311.60.2.1.3=US"
$certificate = New-SelfSignedCertificate -Type Custom -KeyUsage DigitalSignature -CertStoreLocation "Cert:\CurrentUser\My" -TextExtension @("2.5.29.37={text}1.3.6.1.5.5.7.3.3", "2.5.29.19={text}") -Subject $publisher -FriendlyName "Bitwarden Developer Signing Certificate"
$password = Read-Host -AsSecureString
Export-PfxCertificate -cert "Cert:\CurrentUser\My\${$certificate.Thumbprint}" -FilePath "C:\path/to/pfx" -Password $password
```
Using OpenSSL:
```sh
subject="jurisdictionCountryName=US/jurisdictionStateOrProvinceName=Delaware/businessCategory=Private Organization/serialNumber=7654941, C=US, ST=California, L=Santa Barbara, O=Bitwarden Inc., CN=Bitwarden Inc."
keyfile="/tmp/mysigning.rsa.pem"
certfile="/tmp/mysigning.cert.pem"
p12file="/tmp/mysigning.p12"
openssl req -x509 -keyout "$keyfile" -out "$certfile" -subj "$subject" \
-newkey rsa:2048 -days 3650 -nodes \
-addext 'keyUsage=critical,digitalSignature' \
-addext 'extendedKeyUsage=critical,codeSigning' \
-addext 'basicConstraints=critical,CA:FALSE'
openssl pkcs12 -inkey "$keyfile" -in "$certfile" -export -out "$p12file"
rm $keyfile
```
.EXAMPLE
./scripts/cross-build.ps1 -Architecture arm64 -CertificatePath ~/Development/code-signing.pfx -CertificatePassword (Read-Host -AsSecureString) -Release -Beta
Reads the signing certificate password from user input, then builds, packages
and signs the Appx.
Alternatively, you can specify the CERTIFICATE_PASSWORD environment variable.
#>
param(
[Parameter(Mandatory=$true)]
[ValidateSet("X64", "ARM64")]$Architecture,
[string]
# Path to PKCS12 certificate file. If not specified, the Appx will not be signed.
$CertificatePath,
[SecureString]
# Password for PKCS12 certificate. Alternatively, may be specified in
# CERTIFICATE_PASSWORD environment variable. If not specified, the Appx will
# not be signed.
$CertificatePassword,
[Switch]
# Whether to build the Beta version of the app.
$Beta=$false,
[Switch]
# Whether to build in release mode.
$Release=$false
)
$ErrorActionPreference = "Stop"
$PSNativeCommandUseErrorActionPreference = $true
$startTime = Get-Date
$originalLocation = Get-Location
if (!(Get-Command makemsix -ErrorAction SilentlyContinue)) {
Write-Error "The `makemsix` tool from the msix-packaging project is required to construct Appx package."
Write-Error "On macOS, you can install with Homebrew:"
Write-Error " brew install iinuwa/msix-packaging-tap/msix-packaging"
Exit 1
}
if (!(Get-Command osslsigncode -ErrorAction SilentlyContinue)) {
Write-Error "The `osslsigncode` tool is required to sign the Appx package."
Write-Error "On macOS, you can install with Homebrew:"
Write-Error " brew install osslsigncode"
Exit 1
}
if (!(Get-Command cargo-xwin -ErrorAction SilentlyContinue)) {
Write-Error "The `cargo-xwin` tool is required to cross-compile Windows native code."
Write-Error "You can install with cargo:"
Write-Error " cargo install --version 0.20.2 --locked cargo-xwin"
Exit 1
}
try {
# Resolve certificate file before we change directories.
$CertificateFile = Get-Item $CertificatePath -ErrorAction SilentlyContinue
cd $PSScriptRoot/..
if ($Beta) {
$electronConfigFile = Get-Item "./electron-builder.beta.json"
}
else {
$electronConfigFile = Get-Item "./electron-builder.json"
}
$builderConfig = Get-Content $electronConfigFile | ConvertFrom-Json
$packageConfig = Get-Content package.json | ConvertFrom-Json
$manifestTemplate = Get-Content $builderConfig.appx.customManifestPath
$srcDir = Get-Location
$assetsDir = Get-Item $builderConfig.directories.buildResources
$buildDir = Get-Item $builderConfig.directories.app
$outDir = Join-Path (Get-Location) ($builderConfig.directories.output ?? "dist")
if ($Release) {
$buildConfiguration = "--release"
}
$arch = "$Architecture".ToLower()
$ext = "appx"
$version = Get-Date -Format "yyyy.M.d.1HHmm"
$productName = $builderConfig.productName
$artifactName = "${productName}-$($packageConfig.version)-${arch}.$ext"
Write-Host "Building native code"
$rustTarget = switch ($arch) {
x64 { "x86_64-pc-windows-msvc" }
arm64 { "aarch64-pc-windows-msvc" }
default {
Write-Error "Unsupported architecture: $Architecture. Supported architectures are x64 and arm64"
Exit(1)
}
}
npm run build-native -- cross-platform $buildConfiguration "--target=$rustTarget"
Write-Host "Building Javascript code"
if ($Release) {
npm run build
}
else {
npm run build:dev
}
Write-Host "Cleaning output folder"
Remove-Item -Recurse -Force $outDir -ErrorAction Ignore
Write-Host "Packaging Electron executable"
& npx electron-builder --config $electronConfigFile --publish never --dir --win --$arch
cd $outDir
New-Item -Type Directory (Join-Path $outDir "appx")
Write-Host "Building Appx directory structure"
$appxDir = (Join-Path $outDir appx/app)
if ($arch -eq "x64") {
Move-Item (Join-Path $outDir "win-unpacked") $appxDir
}
else {
Move-Item (Join-Path $outDir "win-${arch}-unpacked") $appxDir
}
Write-Host "Copying Assets"
New-Item -Type Directory (Join-Path $outDir appx/assets)
Copy-Item $srcDir/resources/appx/* $outDir/appx/assets/
Write-Host "Building Appx manifest"
$translationMap = @{
'arch' = $arch
'applicationId' = $builderConfig.appx.applicationId
'displayName' = $productName
'executable' = "app\${productName}.exe"
'publisher' = $builderConfig.appx.publisher
'publisherDisplayName' = $builderConfig.appx.publisherDisplayName
'version' = $version
}
$manifest = $manifestTemplate
$translationMap.Keys | ForEach-Object {
$manifest = $manifest.Replace("`${$_}", $translationMap[$_])
}
$manifest | Out-File appx/AppxManifest.xml
$unsignedArtifactpath = [System.IO.Path]::GetFileNameWithoutExtension($artifactName) + "-unsigned.$ext"
Write-Host "Creating unsigned Appx"
makemsix pack -d appx -p $unsignedArtifactpath
$outfile = Join-Path $outDir $unsignedArtifactPath
if ($null -eq $CertificatePath) {
Write-Warning "No Certificate specified. Not signing Appx."
}
elseif ($null -eq $CertificatePassword -and $null -eq $env:CERTIFICATE_PASSWORD) {
Write-Warning "No certificate password specified in CertificatePassword argument nor CERTIFICATE_PASSWORD environment variable. Not signing Appx."
}
else {
$cert = $CertificateFile
$pw = $null
if ($null -ne $CertificatePassword) {
$pw = ConvertFrom-SecureString -SecureString $CertificatePassword -AsPlainText
} else {
$pw = $env:CERTIFICATE_PASSWORD
}
$unsigned = $outfile
$outfile = (Join-Path $outDir $artifactName)
Write-Host "Signing $artifactName with $cert"
osslsigncode sign `
-pkcs12 "$cert" `
-pass "$pw" `
-in $unsigned `
-out $outfile
Remove-Item $unsigned
}
$endTime = Get-Date
$elapsed = $endTime - $startTime
Write-Host "Successfully packaged $(Get-Item $outfile)"
Write-Host ("Finished at $($endTime.ToString('HH:mm:ss')) in $($elapsed.ToString('mm')) minutes and $($elapsed.ToString('ss')).$($elapsed.ToString('fff')) seconds")
}
finally {
Set-Location -Path $originalLocation
}