///|
priv struct WindowsSigningConfig {
certificate : String
password : String
timestamp : String?
}
///|
fn windows_app_path(spec : PackageSpec) -> String {
path_join(spec.output, executable_name(spec.product_name))
}
///|
fn windows_staging_path(spec : PackageSpec) -> String {
path_join(spec.output, "." + executable_name(spec.product_name) + ".staging")
}
///|
fn windows_zip_path(spec : PackageSpec) -> String {
path_join(spec.output, executable_name(spec.product_name) + ".zip")
}
///|
fn windows_signing_config() -> WindowsSigningConfig raise WindowsPackagingError {
let certificate = match @env.get_env_var("PROTON_WINDOWS_CERTIFICATE") {
Some(path) if path.trim().to_owned() != "" => path.trim().to_owned()
_ =>
raise WindowsPackagingError::InvalidConfiguration(
detail="signing requires PROTON_WINDOWS_CERTIFICATE",
)
}
let timestamp = match @env.get_env_var("PROTON_WINDOWS_TIMESTAMP_URL") {
Some(value) if value.trim().to_owned().to_lower() == "none" => None
Some(value) if value.trim().to_owned() != "" =>
Some(value.trim().to_owned())
_ => Some("http://timestamp.digicert.com")
}
WindowsSigningConfig::{
certificate,
password: @env.get_env_var("PROTON_WINDOWS_CERTIFICATE_PASSWORD").unwrap_or(
"",
),
timestamp,
}
}
///|
async fn run_signtool(args : Array[String], stage : String) -> Unit {
let (code, output) = @process.collect_output_merged(
"signtool",
args,
no_console_window=true,
) catch {
error =>
raise WindowsPackagingError::ToolFailure(
stage~,
detail=@debug.render(Repr(error)),
)
}
guard code == 0 else {
raise WindowsPackagingError::ToolFailure(
stage~,
detail="exit code " +
code.to_string() +
": " +
(output.text() catch { _ => "non-UTF-8 output" }),
)
}
}
///|
async fn sign_windows_executable(path : String) -> Unit {
let signing = windows_signing_config()
guard path_is_file(signing.certificate) else {
raise WindowsPackagingError::InvalidConfiguration(
detail="Windows signing certificate is missing: " + signing.certificate,
)
}
let args = ["sign", "/fd", "SHA256", "/f", signing.certificate]
if signing.password != "" {
args.push("/p")
args.push(signing.password)
}
match signing.timestamp {
Some(timestamp) => {
args.push("/tr")
args.push(timestamp)
args.push("/td")
args.push("SHA256")
}
None => ()
}
args.push(path)
run_signtool(args, "sign Windows executable")
verify_windows_executable(path)
}
///|
async fn verify_windows_executable(path : String) -> Unit {
guard path_is_file(path) else {
raise WindowsPackagingError::MissingTarget(path~)
}
run_signtool(
["verify", "/pa", "/all", "/v", path],
"verify Windows executable",
)
}
///|
async fn create_windows_zip(source : String, destination : String) -> Unit {
let staging = destination + ".staging"
remove_tree(staging)
errdefer (remove_tree(staging) catch { _ => () })
let script =
#|& {
#| $ErrorActionPreference = "Stop"
#| Compress-Archive -LiteralPath $env:PROTON_PACKAGE_ZIP_SOURCE -DestinationPath $env:PROTON_PACKAGE_ZIP_DESTINATION -Force
#|}
let (code, output) = @process.collect_output_merged(
"powershell",
["-NoProfile", "-NonInteractive", "-Command", script],
extra_env={
"PROTON_PACKAGE_ZIP_SOURCE": source,
"PROTON_PACKAGE_ZIP_DESTINATION": staging,
},
no_console_window=true,
) catch {
error =>
raise WindowsPackagingError::ToolFailure(
stage="create Windows zip",
detail=@debug.render(Repr(error)),
)
}
guard code == 0 else {
raise WindowsPackagingError::ToolFailure(
stage="create Windows zip",
detail="exit code " +
code.to_string() +
": " +
(output.text() catch { _ => "non-UTF-8 output" }),
)
}
promote(staging, destination)
}
///|
async fn package_windows(
spec : PackageSpec,
customization : PackageCustomization,
prepare : async (PackageLayout) -> Unit,
) -> Array[String] {
guard path_is_file(spec.executable) else {
raise PackagePlanError::MissingExecutable(path=spec.executable)
}
let staging = windows_staging_path(spec)
remove_tree(staging)
ensure_dir(staging)
errdefer (remove_tree(staging) catch { _ => () })
let name = executable_name(spec.product_name) + ".exe"
let staged_executable = path_join(staging, name)
copy_file(spec.executable, staged_executable)
copy_payloads(spec, path_join(staging, "Resources"), staging, staging)
stage_windows_icon(spec, staged_executable)
let layout = PackageLayout::{
root: staging,
executable: staged_executable,
resources: path_join(staging, "Resources"),
libraries: staging,
executables: staging,
}
prepare(layout)
if spec.sign {
let targets = [staged_executable]
for target in customization.windows_sign_targets {
let path = path_join(staging, target)
if !targets.contains(path) {
targets.push(path)
}
}
for target in targets {
sign_windows_executable(target)
}
for target in customization.windows_verify_targets {
verify_windows_executable(path_join(staging, target))
}
}
let outputs : Array[String] = []
let app = windows_app_path(spec)
promote(staging, app)
if spec.formats.contains(App) {
outputs.push(app)
}
if spec.formats.contains(Zip) {
let zip = windows_zip_path(spec)
create_windows_zip(app, zip)
outputs.push(zip)
}
if spec.formats.contains(Nsis) {
create_windows_installer(spec, app, false)
outputs.push(windows_installer_path(spec))
}
if !spec.formats.contains(App) {
remove_tree(app)
}
outputs
}