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",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "launcher"
|
||||||
|
version = "1.0.0"
|
||||||
|
dependencies = [
|
||||||
|
"dirs 5.0.1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lazy_static"
|
name = "lazy_static"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
|
@ -4918,9 +4925,9 @@ checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "plist"
|
name = "plist"
|
||||||
version = "1.7.1"
|
version = "1.7.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "eac26e981c03a6e53e0aee43c113e3202f5581d5360dae7bd2c70e800dd0451d"
|
checksum = "3d77244ce2d584cd84f6a15f86195b8c9b2a0dfbfd817c09e0464244091a58ed"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"indexmap",
|
"indexmap",
|
||||||
|
@ -5249,9 +5256,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quick-xml"
|
name = "quick-xml"
|
||||||
version = "0.32.0"
|
version = "0.37.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1d3a6e5838b60e0e8fa7a43f22ade549a37d61f8bdbe636d0d7816191de969c2"
|
checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
|
@ -12,6 +12,7 @@ members = [
|
||||||
"build/runner",
|
"build/runner",
|
||||||
"ftl",
|
"ftl",
|
||||||
"pylib/rsbridge",
|
"pylib/rsbridge",
|
||||||
|
"qt/bundle/launcher",
|
||||||
"qt/bundle/mac",
|
"qt/bundle/mac",
|
||||||
"qt/bundle/win",
|
"qt/bundle/win",
|
||||||
"rslib",
|
"rslib",
|
||||||
|
@ -23,7 +24,6 @@ members = [
|
||||||
"rslib/sync",
|
"rslib/sync",
|
||||||
"tools/minilints",
|
"tools/minilints",
|
||||||
]
|
]
|
||||||
exclude = ["qt/bundle"]
|
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
[workspace.dependencies.percent-encoding-iri]
|
[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