mirror of
https://github.com/ankitects/anki.git
synced 2025-09-19 06:22:22 -04:00
Initial macOS launcher prototype
This commit is contained in:
parent
b8a22f3078
commit
3d69083f67
8 changed files with 240 additions and 56 deletions
15
Cargo.lock
generated
15
Cargo.lock
generated
|
@ -3687,6 +3687,13 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "launcher"
|
||||
version = "1.0.0"
|
||||
dependencies = [
|
||||
"dirs 5.0.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
|
@ -4918,9 +4925,9 @@ checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
|||
|
||||
[[package]]
|
||||
name = "plist"
|
||||
version = "1.7.1"
|
||||
version = "1.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eac26e981c03a6e53e0aee43c113e3202f5581d5360dae7bd2c70e800dd0451d"
|
||||
checksum = "3d77244ce2d584cd84f6a15f86195b8c9b2a0dfbfd817c09e0464244091a58ed"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"indexmap",
|
||||
|
@ -5249,9 +5256,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.32.0"
|
||||
version = "0.37.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d3a6e5838b60e0e8fa7a43f22ade549a37d61f8bdbe636d0d7816191de969c2"
|
||||
checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
|
|
@ -12,6 +12,7 @@ members = [
|
|||
"build/runner",
|
||||
"ftl",
|
||||
"pylib/rsbridge",
|
||||
"qt/bundle/launcher",
|
||||
"qt/bundle/mac",
|
||||
"qt/bundle/win",
|
||||
"rslib",
|
||||
|
@ -23,7 +24,6 @@ members = [
|
|||
"rslib/sync",
|
||||
"tools/minilints",
|
||||
]
|
||||
exclude = ["qt/bundle"]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.dependencies.percent-encoding-iri]
|
||||
|
|
44
qt/bundle/build-mac-launcher.sh
Executable file
44
qt/bundle/build-mac-launcher.sh
Executable file
|
@ -0,0 +1,44 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
# Define output path
|
||||
OUTPUT_DIR="../../out/bundle"
|
||||
APP_BUNDLE="$OUTPUT_DIR/Anki.app"
|
||||
|
||||
# Build rust binary in debug mode
|
||||
cargo build -p launcher
|
||||
(cd ../.. && ./ninja bundle:uv_universal)
|
||||
|
||||
# Ensure output directory exists
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
|
||||
# Remove existing app bundle
|
||||
rm -rf "$APP_BUNDLE"
|
||||
|
||||
# Create app bundle structure
|
||||
mkdir -p "$APP_BUNDLE/Contents/MacOS" "$APP_BUNDLE/Contents/Resources"
|
||||
|
||||
# Copy binaries
|
||||
TARGET_DIR=${CARGO_TARGET_DIR:-target}
|
||||
cp $TARGET_DIR/debug/launcher "$APP_BUNDLE/Contents/MacOS/"
|
||||
cp "$OUTPUT_DIR/uv" "$APP_BUNDLE/Contents/MacOS/"
|
||||
|
||||
# Copy support files
|
||||
cp launcher/Info.plist "$APP_BUNDLE/Contents/"
|
||||
cp launcher/pyproject.toml "$APP_BUNDLE/Contents/Resources/"
|
||||
|
||||
# Codesign
|
||||
for i in "$APP_BUNDLE/Contents/MacOS/uv" "$APP_BUNDLE/Contents/MacOS/launcher" "$APP_BUNDLE"; do
|
||||
codesign --force -vvvv -o runtime -s "Developer ID Application:" \
|
||||
--entitlements $c/desktop/anki/qt/bundle/mac/entitlements.python.xml \
|
||||
"$i"
|
||||
done
|
||||
|
||||
# Check
|
||||
codesign -vvv "$APP_BUNDLE"
|
||||
spctl -a "$APP_BUNDLE"
|
||||
|
||||
# Mark as quarantined
|
||||
#xattr -w com.apple.quarantine "0181;$(date +%s);Safari;" "$APP_BUNDLE"
|
||||
|
7
qt/bundle/launcher/Cargo.toml
Normal file
7
qt/bundle/launcher/Cargo.toml
Normal file
|
@ -0,0 +1,7 @@
|
|||
[package]
|
||||
name = "launcher"
|
||||
version = "1.0.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
dirs = "5.0"
|
51
qt/bundle/launcher/Info.plist
Normal file
51
qt/bundle/launcher/Info.plist
Normal file
|
@ -0,0 +1,51 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Anki</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>11</string>
|
||||
<key>CFBundleDocumentTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeExtensions</key>
|
||||
<array>
|
||||
<string>colpkg</string>
|
||||
<string>apkg</string>
|
||||
<string>ankiaddon</string>
|
||||
</array>
|
||||
<key>CFBundleTypeIconName</key>
|
||||
<string>AppIcon</string>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>Anki File</string>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>launcher</string>
|
||||
<key>CFBundleIconName</key>
|
||||
<string>AppIcon</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>net.ankiweb.launcher</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>Anki</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<true/>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>The microphone will only be used when you tap the record button.</string>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>Add-ons may access your camera.</string>
|
||||
<key>NSRequiresAquaSystemAppearance</key>
|
||||
<true/>
|
||||
<key>NSSupportsAutomaticGraphicsSwitching</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
17
qt/bundle/launcher/pyproject.toml
Normal file
17
qt/bundle/launcher/pyproject.toml
Normal file
|
@ -0,0 +1,17 @@
|
|||
[project]
|
||||
name = "anki-launcher"
|
||||
version = "0.1.0"
|
||||
description = "UV-based launcher for Anki."
|
||||
requires-python = ">=3.9"
|
||||
dependencies = [
|
||||
"anki-release",
|
||||
]
|
||||
|
||||
[tool.uv.sources]
|
||||
anki-release = { index = "testpypi" }
|
||||
|
||||
[[tool.uv.index]]
|
||||
name = "testpypi"
|
||||
url = "https://test.pypi.org/simple/"
|
||||
publish-url = "https://test.pypi.org/legacy/"
|
||||
explicit = true
|
109
qt/bundle/launcher/src/main.rs
Normal file
109
qt/bundle/launcher/src/main.rs
Normal file
|
@ -0,0 +1,109 @@
|
|||
use std::os::unix::process::CommandExt;
|
||||
use std::process::Command;
|
||||
use std::process::Stdio;
|
||||
|
||||
fn main() {
|
||||
let Some(uv_install_root) =
|
||||
dirs::data_local_dir().map(|data_dir| data_dir.join("AnkiProgramFiles"))
|
||||
else {
|
||||
println!("Unable to determine data_dir");
|
||||
std::process::exit(1);
|
||||
};
|
||||
|
||||
let sync_complete_marker = uv_install_root.join(".sync_complete");
|
||||
let exe_dir = std::env::current_exe()
|
||||
.unwrap()
|
||||
.parent()
|
||||
.unwrap()
|
||||
.to_owned();
|
||||
let resources_dir = exe_dir.parent().unwrap().join("Resources");
|
||||
let dist_pyproject_path = resources_dir.join("pyproject.toml");
|
||||
let user_pyproject_path = uv_install_root.join("pyproject.toml");
|
||||
|
||||
let pyproject_has_changed =
|
||||
!user_pyproject_path.exists() || !sync_complete_marker.exists() || {
|
||||
let pyproject_toml_time = std::fs::metadata(&user_pyproject_path)
|
||||
.unwrap()
|
||||
.modified()
|
||||
.unwrap();
|
||||
let sync_complete_time = std::fs::metadata(&sync_complete_marker)
|
||||
.unwrap()
|
||||
.modified()
|
||||
.unwrap();
|
||||
pyproject_toml_time > sync_complete_time
|
||||
};
|
||||
|
||||
// we'll need to launch uv; reinvoke ourselves in a terminal so the user can see
|
||||
if pyproject_has_changed {
|
||||
let stdout_is_terminal = std::io::IsTerminal::is_terminal(&std::io::stdout());
|
||||
if stdout_is_terminal {
|
||||
print!("\x1B[2J\x1B[H"); // Clear screen and move cursor to top
|
||||
println!("\x1B[1mPreparing to start Anki...\x1B[0m\n");
|
||||
} else {
|
||||
// If launched from GUI, relaunch in Terminal.app
|
||||
let current_exe = std::env::current_exe().unwrap();
|
||||
Command::new("open")
|
||||
.args(["-a", "Terminal"])
|
||||
.arg(current_exe)
|
||||
.spawn()
|
||||
.unwrap();
|
||||
std::process::exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
if pyproject_has_changed {
|
||||
let uv_path: std::path::PathBuf = exe_dir.join("uv");
|
||||
|
||||
// Create install directory and copy pyproject.toml in if missing
|
||||
std::fs::create_dir_all(&uv_install_root).unwrap();
|
||||
if !user_pyproject_path.exists() {
|
||||
std::fs::copy(&dist_pyproject_path, &user_pyproject_path).unwrap();
|
||||
}
|
||||
|
||||
// Remove sync marker before attempting sync
|
||||
let _ = std::fs::remove_file(&sync_complete_marker);
|
||||
|
||||
// Sync the venv
|
||||
let sync_result = Command::new(&uv_path)
|
||||
.current_dir(&uv_install_root)
|
||||
.args(["sync"])
|
||||
.status()
|
||||
.unwrap();
|
||||
|
||||
if !sync_result.success() {
|
||||
println!("uv sync failed");
|
||||
println!("Press enter to close");
|
||||
let mut input = String::new();
|
||||
let _ = std::io::stdin().read_line(&mut input);
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
// Write marker file to indicate successful sync
|
||||
std::fs::write(&sync_complete_marker, "").unwrap();
|
||||
}
|
||||
|
||||
// invoke anki from the synced venv
|
||||
if pyproject_has_changed {
|
||||
// Pre-validate by running --version to trigger any Gatekeeper checks
|
||||
let anki_bin = uv_install_root.join(".venv/bin/anki");
|
||||
println!("\n\x1B[1mThis may take a few minutes. Please wait...\x1B[0m");
|
||||
let _ = Command::new(&anki_bin).arg("--version").output();
|
||||
|
||||
// Then launch the binary as detached subprocess so the terminal can close
|
||||
let child = Command::new(&anki_bin)
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.process_group(0)
|
||||
.spawn()
|
||||
.unwrap();
|
||||
std::mem::forget(child);
|
||||
println!("Anki launched successfully");
|
||||
} else {
|
||||
// If venv already existed, exec as normal
|
||||
println!(
|
||||
"Anki return code: {:?}",
|
||||
Command::new(uv_install_root.join(".venv/bin/anki")).exec()
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Anki</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2.1.46</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>10.14.0</string>
|
||||
<key>CFBundleDocumentTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeExtensions</key>
|
||||
<array>
|
||||
<string>colpkg</string>
|
||||
<string>apkg</string>
|
||||
<string>ankiaddon</string>
|
||||
</array>
|
||||
<key>CFBundleTypeIconName</key>
|
||||
<string>AppIcon</string>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>Anki File</string>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>anki</string>
|
||||
<key>CFBundleIconName</key>
|
||||
<string>AppIcon</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>net.ankiweb.dtop</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>Anki</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<true/>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>The microphone will only be used when you tap the record button.</string>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>Add-ons may access your camera.</string>
|
||||
<key>NSRequiresAquaSystemAppearance</key>
|
||||
<true/>
|
||||
<key>NSSupportsAutomaticGraphicsSwitching</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
Loading…
Reference in a new issue