mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 14:02:21 -04:00

* 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.
227 lines
6.4 KiB
Rust
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
|
|
}
|