diff --git a/.changes/fix-macos-updater.md b/.changes/fix-macos-updater.md new file mode 100644 index 000000000..50fa6397c --- /dev/null +++ b/.changes/fix-macos-updater.md @@ -0,0 +1,5 @@ +--- +"tauri": patch +--- + +Fix macOS `EXC_BAD_ACCESS` panic when app is code-signed. diff --git a/.github/workflows/artifacts-updater.yml b/.github/workflows/artifacts-updater.yml index 7389202d4..2d370242d 100644 --- a/.github/workflows/artifacts-updater.yml +++ b/.github/workflows/artifacts-updater.yml @@ -101,7 +101,17 @@ jobs: yarn install node ../../tooling/cli.js/bin/tauri build env: - TAURI_PRIVATE_KEY: dW50cnVzdGVkIGNvbW1lbnQ6IHJzaWduIGVuY3J5cHRlZCBzZWNyZXQga2V5ClJXUlRZMEl5YTBGV3JiTy9lRDZVd3NkL0RoQ1htZmExNDd3RmJaNmRMT1ZGVjczWTBKZ0FBQkFBQUFBQUFBQUFBQUlBQUFBQWdMekUzVkE4K0tWQ1hjeGt1Vkx2QnRUR3pzQjVuV0ZpM2czWXNkRm9hVUxrVnB6TUN3K1NheHJMREhQbUVWVFZRK3NIL1VsMDBHNW5ET1EzQno0UStSb21nRW4vZlpTaXIwZFh5ZmRlL1lSN0dKcHdyOUVPclVvdzFhVkxDVnZrbHM2T1o4Tk1NWEU9Cg== + # Notarization (disabled) + # FIXME: enable only on `dev` push maybe? as it take some times... + # + # APPLE_ID: ${{ secrets.APPLE_ID }} + # APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} + + # Apple code signing testing + APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} + APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + # Updater signature + TAURI_PRIVATE_KEY: ${{ secrets.UPDATER_PRIVATE_KEY }} - uses: actions/upload-artifact@v2 if: matrix.platform == 'ubuntu-latest' with: @@ -118,4 +128,4 @@ jobs: if: matrix.platform == 'macos-latest' with: name: macos-updater-artifacts - path: ./target/release/bundle/macos/updater-example_*.app.tar.* + path: ./target/release/bundle/macos/updater-example.app.tar.* diff --git a/core/tauri/src/updater/core.rs b/core/tauri/src/updater/core.rs index 2f4d7a449..960272f5c 100644 --- a/core/tauri/src/updater/core.rs +++ b/core/tauri/src/updater/core.rs @@ -18,6 +18,8 @@ use std::{ time::{SystemTime, UNIX_EPOCH}, }; +#[cfg(target_os = "macos")] +use std::fs::rename; #[cfg(not(target_os = "macos"))] use std::process::Command; @@ -230,7 +232,7 @@ impl<'a> UpdateBuilder<'a> { } else { // we expect it to fail if we can't find the executable path // without this path we can't continue the update process. - env::current_exe().expect("Can't access current executable path.") + env::current_exe()? }; // Did the target is provided by the config? @@ -479,17 +481,16 @@ impl Update { // We should have an AppImage already installed to be able to copy and install // the extract_path is the current AppImage path // tmp_dir is where our new AppImage is found - #[cfg(target_os = "linux")] fn copy_files_and_run(tmp_dir: tempfile::TempDir, extract_path: PathBuf) -> Result { // we delete our current AppImage (we'll create a new one later) remove_file(&extract_path)?; // In our tempdir we expect 1 directory (should be the .app) - let paths = read_dir(&tmp_dir).unwrap(); + let paths = read_dir(&tmp_dir)?; for path in paths { - let found_path = path.expect("Unable to extract").path(); + let found_path = path?.path(); // make sure it's our .AppImage if found_path.extension() == Some(OsStr::new("AppImage")) { // Simply overwrite our AppImage (we use the command) @@ -522,16 +523,15 @@ fn copy_files_and_run(tmp_dir: tempfile::TempDir, extract_path: PathBuf) -> Resu // ## EXE // Update server can provide a custom EXE (installer) who can run any task. - #[cfg(target_os = "windows")] #[allow(clippy::unnecessary_wraps)] fn copy_files_and_run(tmp_dir: tempfile::TempDir, _extract_path: PathBuf) -> Result { - let paths = read_dir(&tmp_dir).unwrap(); + let paths = read_dir(&tmp_dir)?; // This consumes the TempDir without deleting directory on the filesystem, // meaning that the directory will no longer be automatically deleted. tmp_dir.into_path(); for path in paths { - let found_path = path.expect("Unable to extract").path(); + let found_path = path?.path(); // we support 2 type of files exe & msi for now // If it's an `exe` we expect an installer not a runtime. if found_path.extension() == Some(OsStr::new("exe")) { @@ -558,27 +558,57 @@ fn copy_files_and_run(tmp_dir: tempfile::TempDir, _extract_path: PathBuf) -> Res Ok(()) } -// MacOS +// Get the current app name in the path +// Example; `/Applications/updater-example.app/Contents/MacOS/updater-example` +// Should return; `updater-example.app` +#[cfg(target_os = "macos")] +fn macos_app_name_in_path(extract_path: &PathBuf) -> String { + let components = extract_path.components(); + let app_name = components.last().unwrap(); + let app_name = app_name.as_os_str().to_str().unwrap(); + app_name.to_string() +} +// MacOS // ### Expected structure: // ├── [AppName]_[version]_x64.app.tar.gz # GZ generated by tauri-bundler // │ └──[AppName].app # Main application // │ └── Contents # Application contents... // │ └── ... // └── ... - #[cfg(target_os = "macos")] fn copy_files_and_run(tmp_dir: tempfile::TempDir, extract_path: PathBuf) -> Result { // In our tempdir we expect 1 directory (should be the .app) - let paths = read_dir(&tmp_dir).unwrap(); + let paths = read_dir(&tmp_dir)?; + + // current app name in /Applications/.app + let app_name = macos_app_name_in_path(&extract_path); for path in paths { - let found_path = path.expect("Unable to extract").path(); + let mut found_path = path?.path(); // make sure it's our .app if found_path.extension() == Some(OsStr::new("app")) { - // Walk the temp dir and copy all files by replacing existing files only - // and creating directories if needed - Move::from_source(&found_path).walk_to_dest(&extract_path)?; + let found_app_name = macos_app_name_in_path(&found_path); + // make sure the app name in the archive matche the installed app name on path + if found_app_name != app_name { + // we need to replace the app name in the updater archive to match + // installed app name + let new_path = found_path.parent().unwrap().join(app_name); + rename(&found_path, &new_path)?; + + found_path = new_path; + } + + let sandbox_app_path = tempfile::Builder::new() + .prefix("tauri_current_app_sandbox") + .tempdir()?; + + // Replace the whole application to make sure the + // code signature is following + Move::from_source(&found_path) + .replace_using_temp(sandbox_app_path.path()) + .to_dest(&extract_path)?; + // early finish we have everything we need here return Ok(()); } @@ -619,7 +649,7 @@ pub fn extract_path_from_executable(executable_path: &Path) -> PathBuf { .expect("Can't determine extract path"); // MacOS example binary is in /Applications/TestApp.app/Contents/MacOS/myApp - // We need to get /Applications/TestApp.app + // We need to get /Applications/.app // todo(lemarier): Need a better way here // Maybe we could search for <*.app> to get the right path #[cfg(target_os = "macos")] @@ -691,22 +721,16 @@ pub fn verify_signature( let public_key = PublicKey::decode(pub_key_decoded)?; let signature_base64_decoded = base64_to_string(&release_signature)?; - let signature = - Signature::decode(&signature_base64_decoded).expect("Something wrong with the signature"); + let signature = Signature::decode(&signature_base64_decoded)?; // We need to open the file and extract the datas to make sure its not corrupted - let file_open = OpenOptions::new() - .read(true) - .open(&archive_path) - .expect("Can't open our archive to validate signature"); + let file_open = OpenOptions::new().read(true).open(&archive_path)?; let mut file_buff: BufReader = BufReader::new(file_open); // read all bytes since EOF in the buffer let mut data = vec![]; - file_buff - .read_to_end(&mut data) - .expect("Can't read buffer to validate signature"); + file_buff.read_to_end(&mut data)?; // Validate signature or bail out public_key.verify(&data, &signature)?; @@ -779,6 +803,17 @@ mod test { }"#.into() } + #[cfg(target_os = "macos")] + #[test] + fn test_app_name_in_path() { + let executable = extract_path_from_executable(Path::new( + "/Applications/updater-example.app/Contents/MacOS/updater-example", + )); + let app_name = macos_app_name_in_path(&executable); + assert!(executable.ends_with("updater-example.app")); + assert_eq!(app_name, "updater-example.app".to_string()); + } + #[test] fn simple_http_updater() { let _m = mockito::mock("GET", "/") diff --git a/examples/updater/entitlements.plist b/examples/updater/entitlements.plist new file mode 100644 index 000000000..39a04014a --- /dev/null +++ b/examples/updater/entitlements.plist @@ -0,0 +1,7 @@ + + + + + com.apple.security.automation.apple-events + + diff --git a/examples/updater/src-tauri/tauri.conf.json b/examples/updater/src-tauri/tauri.conf.json index 4e1a806be..03f74861d 100644 --- a/examples/updater/src-tauri/tauri.conf.json +++ b/examples/updater/src-tauri/tauri.conf.json @@ -28,6 +28,8 @@ "useBootstrapper": false }, "macOS": { + "signingIdentity": "Developer ID Application: David Lemarier (3KF8V3679C)", + "entitlements": "../entitlements.plist", "frameworks": [], "minimumSystemVersion": "", "useBootstrapper": false,