Skip to content

Commit 964d81f

Browse files
authored
refactor(bundler): switch to notarytool, closes #4300 (#7616)
1 parent a7777ff commit 964d81f

File tree

5 files changed

+100
-106
lines changed

5 files changed

+100
-106
lines changed

.changes/config.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
"pref": "Performance Improvements",
99
"changes": "What's Changed",
1010
"sec": "Security fixes",
11-
"deps": "Dependencies"
11+
"deps": "Dependencies",
12+
"breaking": "Breaking Changes"
1213
},
1314
"defaultChangeTag": "changes",
1415
"pkgManagers": {

.changes/notarytool.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"tauri-bundler": minor:breaking
3+
---
4+
5+
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.

tooling/bundler/src/bundle/macos/app.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
use super::{
2626
super::common,
2727
icon::create_icns_file,
28-
sign::{notarize, notarize_auth_args, sign},
28+
sign::{notarize, notarize_auth, sign},
2929
};
3030
use crate::Settings;
3131

@@ -87,9 +87,9 @@ pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<PathBuf>> {
8787
// sign application
8888
sign(app_bundle_path.clone(), identity, settings, true)?;
8989
// notarization is required for distribution
90-
match notarize_auth_args() {
91-
Ok(args) => {
92-
notarize(app_bundle_path.clone(), args, settings)?;
90+
match notarize_auth() {
91+
Ok(auth) => {
92+
notarize(app_bundle_path.clone(), auth, settings)?;
9393
}
9494
Err(e) => {
9595
warn!("skipping app notarization, {}", e.to_string());

tooling/bundler/src/bundle/macos/sign.rs

Lines changed: 88 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,19 @@
33
// SPDX-License-Identifier: Apache-2.0
44
// SPDX-License-Identifier: MIT
55

6-
use std::ffi::OsString;
7-
use std::{fs::File, io::prelude::*, path::PathBuf, process::Command};
6+
use std::{
7+
env::{var, var_os},
8+
ffi::OsString,
9+
fs::File,
10+
io::prelude::*,
11+
path::PathBuf,
12+
process::Command,
13+
};
814

915
use crate::{bundle::common::CommandExt, Settings};
1016
use anyhow::Context;
1117
use log::info;
12-
use regex::Regex;
18+
use serde::Deserialize;
1319

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

149155
let setup_keychain = if let (Some(certificate_encoded), Some(certificate_password)) = (
150-
std::env::var_os("APPLE_CERTIFICATE"),
151-
std::env::var_os("APPLE_CERTIFICATE_PASSWORD"),
156+
var_os("APPLE_CERTIFICATE"),
157+
var_os("APPLE_CERTIFICATE_PASSWORD"),
152158
) {
153159
// setup keychain allow you to import your certificate
154160
// for CI build
@@ -212,13 +218,18 @@ fn try_sign(
212218
Ok(())
213219
}
214220

221+
#[derive(Deserialize)]
222+
struct NotarytoolSubmitOutput {
223+
id: String,
224+
status: String,
225+
message: String,
226+
}
227+
215228
pub fn notarize(
216229
app_bundle_path: PathBuf,
217-
auth_args: Vec<String>,
230+
auth: NotarizeAuth,
218231
settings: &Settings,
219232
) -> crate::Result<()> {
220-
let identifier = settings.bundle_identifier();
221-
222233
let bundle_stem = app_bundle_path
223234
.file_stem()
224235
.expect("failed to get bundle filename");
@@ -252,55 +263,47 @@ pub fn notarize(
252263
sign(zip_path.clone(), identity, settings, false)?;
253264
};
254265

255-
let mut notarize_args = vec![
256-
"altool",
257-
"--notarize-app",
258-
"-f",
266+
let notarize_args = vec![
267+
"notarytool",
268+
"submit",
259269
zip_path
260270
.to_str()
261271
.expect("failed to convert zip_path to string"),
262-
"--primary-bundle-id",
263-
identifier,
272+
"--wait",
273+
"--output-format",
274+
"json",
264275
];
265276

266-
if let Some(provider_short_name) = &settings.macos().provider_short_name {
267-
notarize_args.push("--asc-provider");
268-
notarize_args.push(provider_short_name);
269-
}
270-
271277
info!(action = "Notarizing"; "{}", app_bundle_path.display());
272278

273279
let output = Command::new("xcrun")
274280
.args(notarize_args)
275-
.args(auth_args.clone())
281+
.notarytool_args(&auth)
276282
.output_ok()
277283
.context("failed to upload app to Apple's notarization servers.")?;
278284

279-
// combine both stdout and stderr to support macOS below 10.15
280-
let mut notarize_response = std::str::from_utf8(&output.stdout)?.to_string();
281-
notarize_response.push('\n');
282-
notarize_response.push_str(std::str::from_utf8(&output.stderr)?);
283-
notarize_response.push('\n');
284-
if let Some(uuid) = Regex::new(r"\nRequestUUID = (.+?)\n")?
285-
.captures_iter(&notarize_response)
286-
.next()
287-
{
288-
info!("notarization started; waiting for Apple response...");
289-
290-
let uuid = uuid[1].to_string();
291-
get_notarization_status(uuid, auth_args)?;
292-
staple_app(app_bundle_path.clone())?;
285+
if !output.status.success() {
286+
return Err(anyhow::anyhow!("failed to notarize app").into());
287+
}
288+
289+
let output_str = String::from_utf8_lossy(&output.stdout);
290+
if let Ok(submit_output) = serde_json::from_str::<NotarytoolSubmitOutput>(&output_str) {
291+
let log_message = format!(
292+
"Finished with status {} for id {} ({})",
293+
submit_output.status, submit_output.id, submit_output.message
294+
);
295+
if submit_output.status == "Accepted" {
296+
log::info!(action = "Notarizing"; "{}", log_message);
297+
staple_app(app_bundle_path)?;
298+
Ok(())
299+
} else {
300+
Err(anyhow::anyhow!("{log_message}").into())
301+
}
293302
} else {
294303
return Err(
295-
anyhow::anyhow!(
296-
"failed to parse RequestUUID from upload output. {}",
297-
notarize_response
298-
)
299-
.into(),
304+
anyhow::anyhow!("failed to parse notarytool output as JSON: `{output_str}`").into(),
300305
);
301306
}
302-
303-
Ok(())
304307
}
305308

306309
fn staple_app(mut app_bundle_path: PathBuf) -> crate::Result<()> {
@@ -322,82 +325,66 @@ fn staple_app(mut app_bundle_path: PathBuf) -> crate::Result<()> {
322325
Ok(())
323326
}
324327

325-
fn get_notarization_status(uuid: String, auth_args: Vec<String>) -> crate::Result<()> {
326-
std::thread::sleep(std::time::Duration::from_secs(10));
327-
let result = Command::new("xcrun")
328-
.args(vec!["altool", "--notarization-info", &uuid])
329-
.args(auth_args.clone())
330-
.output_ok();
328+
pub enum NotarizeAuth {
329+
AppleId {
330+
apple_id: String,
331+
password: String,
332+
},
333+
ApiKey {
334+
key: String,
335+
key_path: PathBuf,
336+
issuer: String,
337+
},
338+
}
331339

332-
if let Ok(output) = result {
333-
// combine both stdout and stderr to support macOS below 10.15
334-
let mut notarize_status = std::str::from_utf8(&output.stdout)?.to_string();
335-
notarize_status.push('\n');
336-
notarize_status.push_str(std::str::from_utf8(&output.stderr)?);
337-
notarize_status.push('\n');
338-
if let Some(status) = Regex::new(r"\n *Status: (.+?)\n")?
339-
.captures_iter(&notarize_status)
340-
.next()
341-
{
342-
let status = status[1].to_string();
343-
if status == "in progress" {
344-
get_notarization_status(uuid, auth_args)
345-
} else if status == "invalid" {
346-
Err(
347-
anyhow::anyhow!(format!(
348-
"Apple failed to notarize your app. {}",
349-
notarize_status
350-
))
351-
.into(),
352-
)
353-
} else if status != "success" {
354-
Err(
355-
anyhow::anyhow!(format!(
356-
"Unknown notarize status {}. {}",
357-
status, notarize_status
358-
))
359-
.into(),
360-
)
361-
} else {
362-
Ok(())
363-
}
364-
} else {
365-
get_notarization_status(uuid, auth_args)
340+
pub trait NotarytoolCmdExt {
341+
fn notarytool_args(&mut self, auth: &NotarizeAuth) -> &mut Self;
342+
}
343+
344+
impl NotarytoolCmdExt for Command {
345+
fn notarytool_args(&mut self, auth: &NotarizeAuth) -> &mut Self {
346+
match auth {
347+
NotarizeAuth::AppleId { apple_id, password } => self
348+
.arg("--apple-id")
349+
.arg(apple_id)
350+
.arg("--password")
351+
.arg(password),
352+
NotarizeAuth::ApiKey {
353+
key,
354+
key_path,
355+
issuer,
356+
} => self
357+
.arg("--key-id")
358+
.arg(key)
359+
.arg("--key")
360+
.arg(key_path)
361+
.arg("--issuer")
362+
.arg(issuer),
366363
}
367-
} else {
368-
get_notarization_status(uuid, auth_args)
369364
}
370365
}
371366

372-
pub fn notarize_auth_args() -> crate::Result<Vec<String>> {
373-
match (
374-
std::env::var_os("APPLE_ID"),
375-
std::env::var_os("APPLE_PASSWORD"),
376-
) {
367+
pub fn notarize_auth() -> crate::Result<NotarizeAuth> {
368+
match (var_os("APPLE_ID"), var_os("APPLE_PASSWORD")) {
377369
(Some(apple_id), Some(apple_password)) => {
378370
let apple_id = apple_id
379371
.to_str()
380372
.expect("failed to convert APPLE_ID to string")
381373
.to_string();
382-
let apple_password = apple_password
374+
let password = apple_password
383375
.to_str()
384376
.expect("failed to convert APPLE_PASSWORD to string")
385377
.to_string();
386-
Ok(vec![
387-
"-u".to_string(),
388-
apple_id,
389-
"-p".to_string(),
390-
apple_password,
391-
])
378+
Ok(NotarizeAuth::AppleId { apple_id, password })
392379
}
393380
_ => {
394-
match (std::env::var_os("APPLE_API_KEY"), std::env::var_os("APPLE_API_ISSUER")) {
395-
(Some(api_key), Some(api_issuer)) => {
396-
let api_key = api_key.to_str().expect("failed to convert APPLE_API_KEY to string").to_string();
397-
let api_issuer = api_issuer.to_str().expect("failed to convert APPLE_API_ISSUER to string").to_string();
398-
Ok(vec!["--apiKey".to_string(), api_key, "--apiIssuer".to_string(), api_issuer])
381+
match (var_os("APPLE_API_KEY"), var_os("APPLE_API_ISSUER"), var("APPLE_API_KEY_PATH")) {
382+
(Some(api_key), Some(api_issuer), Ok(key_path)) => {
383+
let key = api_key.to_str().expect("failed to convert APPLE_API_KEY to string").to_string();
384+
let issuer = api_issuer.to_str().expect("failed to convert APPLE_API_ISSUER to string").to_string();
385+
Ok(NotarizeAuth::ApiKey { key, key_path: key_path.into(), issuer })
399386
},
400-
_ => Err(anyhow::anyhow!("no APPLE_ID & APPLE_PASSWORD or APPLE_API_KEY & APPLE_API_ISSUER environment variables found").into())
387+
_ => Err(anyhow::anyhow!("no APPLE_ID & APPLE_PASSWORD or APPLE_API_KEY & APPLE_API_ISSUER & APPLE_API_KEY_PATH environment variables found").into())
401388
}
402389
}
403390
}

tooling/cli/ENVIRONMENT_VARIABLES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ These environment variables are inputs to the CLI which may have an equivalent C
2929
- 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.
3030
- See [creating API keys](https://developer.apple.com/documentation/appstoreconnectapi/creating_api_keys_for_app_store_connect_api) for more information.
3131
- `APPLE_API_ISSUER` — Issuer ID. Required if `APPLE_API_KEY` is specified.
32+
- `APPLE_API_KEY_PATH` - path to the API key `.p8` file.
3233
- `APPLE_SIGNING_IDENTITY` — The identity used to code sign. Overwrites `tauri.conf.json > tauri > bundle > macOS > signingIdentity`.
3334
- `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`.
3435
- `CI` — If set, the CLI will run in CI mode and won't require any user interaction.

0 commit comments

Comments
 (0)