Skip to content

Commit

Permalink
refactor(bundler): switch to notarytool, closes #4300 (#7616)
Browse files Browse the repository at this point in the history
  • Loading branch information
lucasfernog authored Aug 16, 2023
1 parent a7777ff commit 964d81f
Show file tree
Hide file tree
Showing 5 changed files with 100 additions and 106 deletions.
3 changes: 2 additions & 1 deletion .changes/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
"pref": "Performance Improvements",
"changes": "What's Changed",
"sec": "Security fixes",
"deps": "Dependencies"
"deps": "Dependencies",
"breaking": "Breaking Changes"
},
"defaultChangeTag": "changes",
"pkgManagers": {
Expand Down
5 changes: 5 additions & 0 deletions .changes/notarytool.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"tauri-bundler": minor:breaking
---

The macOS notarization now uses `notarytool` as `altool` will be discontinued on November 2023. When authenticating with an API key, the key `.p8` file path must be provided in the `APPLE_API_KEY_PATH` environment variable.
8 changes: 4 additions & 4 deletions tooling/bundler/src/bundle/macos/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
use super::{
super::common,
icon::create_icns_file,
sign::{notarize, notarize_auth_args, sign},
sign::{notarize, notarize_auth, sign},
};
use crate::Settings;

Expand Down Expand Up @@ -87,9 +87,9 @@ pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<PathBuf>> {
// sign application
sign(app_bundle_path.clone(), identity, settings, true)?;
// notarization is required for distribution
match notarize_auth_args() {
Ok(args) => {
notarize(app_bundle_path.clone(), args, settings)?;
match notarize_auth() {
Ok(auth) => {
notarize(app_bundle_path.clone(), auth, settings)?;
}
Err(e) => {
warn!("skipping app notarization, {}", e.to_string());
Expand Down
189 changes: 88 additions & 101 deletions tooling/bundler/src/bundle/macos/sign.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,19 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT

use std::ffi::OsString;
use std::{fs::File, io::prelude::*, path::PathBuf, process::Command};
use std::{
env::{var, var_os},
ffi::OsString,
fs::File,
io::prelude::*,
path::PathBuf,
process::Command,
};

use crate::{bundle::common::CommandExt, Settings};
use anyhow::Context;
use log::info;
use regex::Regex;
use serde::Deserialize;

const KEYCHAIN_ID: &str = "tauri-build.keychain";
const KEYCHAIN_PWD: &str = "tauri-build";
Expand Down Expand Up @@ -147,8 +153,8 @@ pub fn sign(
info!(action = "Signing"; "{} with identity \"{}\"", path_to_sign.display(), identity);

let setup_keychain = if let (Some(certificate_encoded), Some(certificate_password)) = (
std::env::var_os("APPLE_CERTIFICATE"),
std::env::var_os("APPLE_CERTIFICATE_PASSWORD"),
var_os("APPLE_CERTIFICATE"),
var_os("APPLE_CERTIFICATE_PASSWORD"),
) {
// setup keychain allow you to import your certificate
// for CI build
Expand Down Expand Up @@ -212,13 +218,18 @@ fn try_sign(
Ok(())
}

#[derive(Deserialize)]
struct NotarytoolSubmitOutput {
id: String,
status: String,
message: String,
}

pub fn notarize(
app_bundle_path: PathBuf,
auth_args: Vec<String>,
auth: NotarizeAuth,
settings: &Settings,
) -> crate::Result<()> {
let identifier = settings.bundle_identifier();

let bundle_stem = app_bundle_path
.file_stem()
.expect("failed to get bundle filename");
Expand Down Expand Up @@ -252,55 +263,47 @@ pub fn notarize(
sign(zip_path.clone(), identity, settings, false)?;
};

let mut notarize_args = vec![
"altool",
"--notarize-app",
"-f",
let notarize_args = vec![
"notarytool",
"submit",
zip_path
.to_str()
.expect("failed to convert zip_path to string"),
"--primary-bundle-id",
identifier,
"--wait",
"--output-format",
"json",
];

if let Some(provider_short_name) = &settings.macos().provider_short_name {
notarize_args.push("--asc-provider");
notarize_args.push(provider_short_name);
}

info!(action = "Notarizing"; "{}", app_bundle_path.display());

let output = Command::new("xcrun")
.args(notarize_args)
.args(auth_args.clone())
.notarytool_args(&auth)
.output_ok()
.context("failed to upload app to Apple's notarization servers.")?;

// combine both stdout and stderr to support macOS below 10.15
let mut notarize_response = std::str::from_utf8(&output.stdout)?.to_string();
notarize_response.push('\n');
notarize_response.push_str(std::str::from_utf8(&output.stderr)?);
notarize_response.push('\n');
if let Some(uuid) = Regex::new(r"\nRequestUUID = (.+?)\n")?
.captures_iter(&notarize_response)
.next()
{
info!("notarization started; waiting for Apple response...");

let uuid = uuid[1].to_string();
get_notarization_status(uuid, auth_args)?;
staple_app(app_bundle_path.clone())?;
if !output.status.success() {
return Err(anyhow::anyhow!("failed to notarize app").into());
}

let output_str = String::from_utf8_lossy(&output.stdout);
if let Ok(submit_output) = serde_json::from_str::<NotarytoolSubmitOutput>(&output_str) {
let log_message = format!(
"Finished with status {} for id {} ({})",
submit_output.status, submit_output.id, submit_output.message
);
if submit_output.status == "Accepted" {
log::info!(action = "Notarizing"; "{}", log_message);
staple_app(app_bundle_path)?;
Ok(())
} else {
Err(anyhow::anyhow!("{log_message}").into())
}
} else {
return Err(
anyhow::anyhow!(
"failed to parse RequestUUID from upload output. {}",
notarize_response
)
.into(),
anyhow::anyhow!("failed to parse notarytool output as JSON: `{output_str}`").into(),
);
}

Ok(())
}

fn staple_app(mut app_bundle_path: PathBuf) -> crate::Result<()> {
Expand All @@ -322,82 +325,66 @@ fn staple_app(mut app_bundle_path: PathBuf) -> crate::Result<()> {
Ok(())
}

fn get_notarization_status(uuid: String, auth_args: Vec<String>) -> crate::Result<()> {
std::thread::sleep(std::time::Duration::from_secs(10));
let result = Command::new("xcrun")
.args(vec!["altool", "--notarization-info", &uuid])
.args(auth_args.clone())
.output_ok();
pub enum NotarizeAuth {
AppleId {
apple_id: String,
password: String,
},
ApiKey {
key: String,
key_path: PathBuf,
issuer: String,
},
}

if let Ok(output) = result {
// combine both stdout and stderr to support macOS below 10.15
let mut notarize_status = std::str::from_utf8(&output.stdout)?.to_string();
notarize_status.push('\n');
notarize_status.push_str(std::str::from_utf8(&output.stderr)?);
notarize_status.push('\n');
if let Some(status) = Regex::new(r"\n *Status: (.+?)\n")?
.captures_iter(&notarize_status)
.next()
{
let status = status[1].to_string();
if status == "in progress" {
get_notarization_status(uuid, auth_args)
} else if status == "invalid" {
Err(
anyhow::anyhow!(format!(
"Apple failed to notarize your app. {}",
notarize_status
))
.into(),
)
} else if status != "success" {
Err(
anyhow::anyhow!(format!(
"Unknown notarize status {}. {}",
status, notarize_status
))
.into(),
)
} else {
Ok(())
}
} else {
get_notarization_status(uuid, auth_args)
pub trait NotarytoolCmdExt {
fn notarytool_args(&mut self, auth: &NotarizeAuth) -> &mut Self;
}

impl NotarytoolCmdExt for Command {
fn notarytool_args(&mut self, auth: &NotarizeAuth) -> &mut Self {
match auth {
NotarizeAuth::AppleId { apple_id, password } => self
.arg("--apple-id")
.arg(apple_id)
.arg("--password")
.arg(password),
NotarizeAuth::ApiKey {
key,
key_path,
issuer,
} => self
.arg("--key-id")
.arg(key)
.arg("--key")
.arg(key_path)
.arg("--issuer")
.arg(issuer),
}
} else {
get_notarization_status(uuid, auth_args)
}
}

pub fn notarize_auth_args() -> crate::Result<Vec<String>> {
match (
std::env::var_os("APPLE_ID"),
std::env::var_os("APPLE_PASSWORD"),
) {
pub fn notarize_auth() -> crate::Result<NotarizeAuth> {
match (var_os("APPLE_ID"), var_os("APPLE_PASSWORD")) {
(Some(apple_id), Some(apple_password)) => {
let apple_id = apple_id
.to_str()
.expect("failed to convert APPLE_ID to string")
.to_string();
let apple_password = apple_password
let password = apple_password
.to_str()
.expect("failed to convert APPLE_PASSWORD to string")
.to_string();
Ok(vec![
"-u".to_string(),
apple_id,
"-p".to_string(),
apple_password,
])
Ok(NotarizeAuth::AppleId { apple_id, password })
}
_ => {
match (std::env::var_os("APPLE_API_KEY"), std::env::var_os("APPLE_API_ISSUER")) {
(Some(api_key), Some(api_issuer)) => {
let api_key = api_key.to_str().expect("failed to convert APPLE_API_KEY to string").to_string();
let api_issuer = api_issuer.to_str().expect("failed to convert APPLE_API_ISSUER to string").to_string();
Ok(vec!["--apiKey".to_string(), api_key, "--apiIssuer".to_string(), api_issuer])
match (var_os("APPLE_API_KEY"), var_os("APPLE_API_ISSUER"), var("APPLE_API_KEY_PATH")) {
(Some(api_key), Some(api_issuer), Ok(key_path)) => {
let key = api_key.to_str().expect("failed to convert APPLE_API_KEY to string").to_string();
let issuer = api_issuer.to_str().expect("failed to convert APPLE_API_ISSUER to string").to_string();
Ok(NotarizeAuth::ApiKey { key, key_path: key_path.into(), issuer })
},
_ => Err(anyhow::anyhow!("no APPLE_ID & APPLE_PASSWORD or APPLE_API_KEY & APPLE_API_ISSUER environment variables found").into())
_ => Err(anyhow::anyhow!("no APPLE_ID & APPLE_PASSWORD or APPLE_API_KEY & APPLE_API_ISSUER & APPLE_API_KEY_PATH environment variables found").into())
}
}
}
Expand Down
1 change: 1 addition & 0 deletions tooling/cli/ENVIRONMENT_VARIABLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ These environment variables are inputs to the CLI which may have an equivalent C
- This option will search the following directories in sequence for a private key file with the name of 'AuthKey_<api_key>.p8': './private_keys', '~/private_keys', '~/.private_keys', and '~/.appstoreconnect/private_keys'. Additionally, you can set environment variable $API_PRIVATE_KEYS_DIR or a user default API_PRIVATE_KEYS_DIR to specify the directory where your AuthKey file is located.
- See [creating API keys](https://developer.apple.com/documentation/appstoreconnectapi/creating_api_keys_for_app_store_connect_api) for more information.
- `APPLE_API_ISSUER` — Issuer ID. Required if `APPLE_API_KEY` is specified.
- `APPLE_API_KEY_PATH` - path to the API key `.p8` file.
- `APPLE_SIGNING_IDENTITY` — The identity used to code sign. Overwrites `tauri.conf.json > tauri > bundle > macOS > signingIdentity`.
- `APPLE_PROVIDER_SHORT_NAME` — If your Apple ID is connected to multiple teams, you have to specify the provider short name of the team you want to use to notarize your app. Overwrites `tauri.conf.json > tauri > bundle > macOS > providerShortName`.
- `CI` — If set, the CLI will run in CI mode and won't require any user interaction.
Expand Down

0 comments on commit 964d81f

Please sign in to comment.