Anki/qt/bundle/mac/src/main.rs
Damien Elmes ba68764fcb Another attempt at fixing missing cacert.pem
A few reports like https://forums.ankiweb.net/t/error-report-check-database-did-not-work/25796
indicate that the previous solution has not helped, and has just made things
worse. My guess is that AppNap on macOS is preventing the timer from even
running before the file gets cleaned up.
2022-12-30 15:30:53 +10:00

227 lines
6.5 KiB
Rust

// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
#![cfg(unix)]
//! Munge the output of PyOxidizer into a macOS app bundle, and combine it
//! with our other runtime dependencies.
mod codesign;
mod dmg;
mod notarize;
use std::{env, fs, os::unix::prelude::PermissionsExt, process::Command};
use anyhow::{bail, Result};
use apple_bundles::MacOsApplicationBundleBuilder;
use camino::{Utf8Path, Utf8PathBuf};
use clap::{Parser, Subcommand, ValueEnum};
use codesign::{codesign_app, codesign_python_libs};
use dmg::{make_dmgs, BuildDmgsArgs};
use notarize::notarize_app;
use plist::Value;
use simple_file_manifest::FileEntry;
use walkdir::WalkDir;
#[derive(Clone, ValueEnum)]
enum DistKind {
Standard,
Alternate,
}
impl DistKind {
fn folder_name(&self) -> &'static str {
match self {
DistKind::Standard => "std",
DistKind::Alternate => "alt",
}
}
fn input_folder(&self) -> Utf8PathBuf {
Utf8Path::new("out/bundle").join(self.folder_name())
}
fn output_folder(&self) -> Utf8PathBuf {
Utf8Path::new("out/bundle/app")
.join(self.folder_name())
.join("Anki.app")
}
fn macos_min(&self) -> &str {
match self {
DistKind::Standard => {
if cfg!(target_arch = "aarch64") && env::var("MAC_X86").is_err() {
"11"
} else {
"10.14.4"
}
}
DistKind::Alternate => "10.13.4",
}
}
fn qt_repo(&self) -> &Utf8Path {
Utf8Path::new(match self {
DistKind::Standard => {
if cfg!(target_arch = "aarch64") && env::var("MAC_X86").is_err() {
"out/extracted/mac_arm_qt6"
} else {
"out/extracted/mac_amd_qt6"
}
}
DistKind::Alternate => "out/extracted/mac_amd_qt5",
})
}
}
#[derive(Parser)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
BuildApp {
version: String,
kind: DistKind,
stamp: Utf8PathBuf,
},
BuildDmgs(BuildDmgsArgs),
}
fn main() -> Result<()> {
match Cli::parse().command {
Commands::BuildApp {
version,
kind,
stamp,
} => {
let plist = get_plist(&version);
make_app(kind, plist, &stamp)
}
Commands::BuildDmgs(args) => make_dmgs(args),
}
}
fn make_app(kind: DistKind, mut plist: plist::Dictionary, stamp: &Utf8Path) -> Result<()> {
let input_folder = kind.input_folder();
let output_folder = kind.output_folder();
let output_variant = output_folder.parent().unwrap();
if output_variant.exists() {
fs::remove_dir_all(output_variant)?;
}
fs::create_dir_all(&output_folder)?;
let mut builder = MacOsApplicationBundleBuilder::new("Anki")?;
plist.insert(
"LSMinimumSystemVersion".into(),
Value::from(kind.macos_min()),
);
builder.set_info_plist_from_dictionary(plist)?;
builder.add_file_resources("Assets.car", &include_bytes!("../icon/Assets.car")[..])?;
for entry in WalkDir::new(&input_folder)
.into_iter()
.map(Result::unwrap)
.filter(|e| !e.file_type().is_dir())
{
let path = entry.path();
let entry = FileEntry::try_from(path)?;
let relative_path = path.strip_prefix(&input_folder)?;
let path_str = relative_path.to_str().unwrap();
if path_str.contains("libankihelper") {
builder.add_file_macos("libankihelper.dylib", entry)?;
} else if path_str.contains("aqt/data") || path_str.contains("certifi") {
builder.add_file_resources(relative_path.strip_prefix("lib").unwrap(), entry)?;
} else {
if path_str.contains("__pycache__") {
continue;
}
builder.add_file_macos(relative_path, entry)?;
}
}
builder.files().materialize_files(&output_folder)?;
fix_rpath(output_folder.join("Contents/MacOS/anki"))?;
codesign_python_libs(&output_folder)?;
copy_in_audio(&output_folder)?;
copy_in_qt(&output_folder, kind)?;
codesign_app(&output_folder)?;
fixup_perms(&output_folder)?;
notarize_app(&output_folder)?;
fs::write(stamp, b"")?;
Ok(())
}
/// The bundle builder writes some files without world read/execute perms,
/// which prevents them from being opened by a non-admin user.
fn fixup_perms(dir: &Utf8Path) -> Result<()> {
let status = Command::new("find")
.arg(dir)
.args(["-not", "-perm", "-a=r", "-exec", "chmod", "a+r", "{}", ";"])
.status()?;
if !status.success() {
bail!("error setting perms");
}
fs::set_permissions(
dir.join("Contents/MacOS/anki"),
PermissionsExt::from_mode(0o755),
)?;
Ok(())
}
/// Copy everything at the provided path into the Contents/ folder of our app.
fn extend_app_contents(source: &Utf8Path, target_dir: &Utf8Path) -> Result<()> {
let status = Command::new("rsync")
.arg("-a")
.arg(format!("{}/", source.as_str()))
.arg(target_dir)
.status()?;
if !status.success() {
bail!("error syncing {source:?}");
}
Ok(())
}
fn copy_in_audio(bundle_dir: &Utf8Path) -> Result<()> {
println!("Copying in audio...");
let src_folder = Utf8Path::new(
if cfg!(target_arch = "aarch64") && env::var("MAC_X86").is_err() {
"out/extracted/mac_arm_audio"
} else {
"out/extracted/mac_amd_audio"
},
);
extend_app_contents(src_folder, &bundle_dir.join("Contents/Resources"))
}
fn copy_in_qt(bundle_dir: &Utf8Path, kind: DistKind) -> Result<()> {
println!("Copying in Qt...");
extend_app_contents(kind.qt_repo(), &bundle_dir.join("Contents"))
}
fn fix_rpath(exe_path: Utf8PathBuf) -> Result<()> {
let status = Command::new("install_name_tool")
.arg("-add_rpath")
.arg("@executable_path/../Frameworks")
.arg(exe_path.as_str())
.status()?;
assert!(status.success());
Ok(())
}
fn get_plist(anki_version: &str) -> plist::Dictionary {
let reader = std::io::Cursor::new(include_bytes!("Info.plist"));
let mut plist = Value::from_reader(reader)
.unwrap()
.into_dictionary()
.unwrap();
plist.insert(
"CFBundleShortVersionString".into(),
Value::from(anki_version),
);
plist
}