#!/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"
    'identityName' = $builderConfig.appx.identityName
    '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
}
