Anki/qt/bundle/mac/src/main.rs
Damien Elmes fa625d7ad8
Minor Rust cleanups (#2272)
* Run cargo +nightly fmt

* Latest prost-build includes clippy workaround

* Tweak Rust protobuf imports

- Avoid use of stringify!(), as JetBrains editors get confused by it
- Stop merging all protobuf symbols into a single namespace

* Remove some unnecessary qualifications

Found via IntelliJ lint

* Migrate some asserts to assert_eq/ne

* Remove mention of node_modules exclusion

This no longer seems to be necessary after migrating away from Bazel,
and excluding it means TS/Svelte files can't be edited properly.
2022-12-16 21:40:27 +10:00

227 lines
6.4 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.14",
}
}
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") {
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
}