mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 22:12:21 -04:00
Refactor launcher + various tweaks
- Launcher can now be accessed via Tools>Upgrade/Downgrade - Anki closes automatically on update - When launcher not available, show update link like in the past - It appears that access to the modern console host requires an app to be built with the windows console subsystem, so we introduce an extra anki-console.exe binary to relaunch ourselves with. Solves https://forums.ankiweb.net/t/new-online-installer-launcher/62745/50 - Windows now requires you to close the terminal like on a Mac, as I couldn't figure out how to have it automatically close. Suggestions welcome! - Reduce the amount of duplicate/near-duplicate code in the various platform files, and improve readability - Add a helper to install the current code into the launcher env - Fix cargo test failing to build on ARM64 Windows
This commit is contained in:
parent
73edf23954
commit
de7de82f76
20 changed files with 435 additions and 354 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -3543,6 +3543,7 @@ dependencies = [
|
||||||
"anki_io",
|
"anki_io",
|
||||||
"anki_process",
|
"anki_process",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"camino",
|
||||||
"dirs 6.0.0",
|
"dirs 6.0.0",
|
||||||
"embed-resource",
|
"embed-resource",
|
||||||
"libc",
|
"libc",
|
||||||
|
|
|
@ -138,7 +138,7 @@ unic-ucd-category = "0.9.0"
|
||||||
unicode-normalization = "0.1.24"
|
unicode-normalization = "0.1.24"
|
||||||
walkdir = "2.5.0"
|
walkdir = "2.5.0"
|
||||||
which = "8.0.0"
|
which = "8.0.0"
|
||||||
winapi = { version = "0.3", features = ["wincon", "errhandlingapi", "consoleapi"] }
|
winapi = { version = "0.3", features = ["wincon"] }
|
||||||
windows = { version = "0.61.3", features = ["Media_SpeechSynthesis", "Media_Core", "Foundation_Collections", "Storage_Streams"] }
|
windows = { version = "0.61.3", features = ["Media_SpeechSynthesis", "Media_Core", "Foundation_Collections", "Storage_Streams"] }
|
||||||
wiremock = "0.6.3"
|
wiremock = "0.6.3"
|
||||||
xz2 = "0.1.7"
|
xz2 = "0.1.7"
|
||||||
|
|
|
@ -46,3 +46,4 @@ qt-accel-zoom-editor-in = Zoom Editor &In
|
||||||
qt-accel-zoom-editor-out = Zoom Editor &Out
|
qt-accel-zoom-editor-out = Zoom Editor &Out
|
||||||
qt-accel-create-backup = Create &Backup
|
qt-accel-create-backup = Create &Backup
|
||||||
qt-accel-load-backup = &Revert to Backup
|
qt-accel-load-backup = &Revert to Backup
|
||||||
|
qt-accel-upgrade-downgrade = Upgrade/Downgrade
|
||||||
|
|
|
@ -73,7 +73,7 @@ qt-misc-second =
|
||||||
qt-misc-layout-auto-enabled = Responsive layout enabled
|
qt-misc-layout-auto-enabled = Responsive layout enabled
|
||||||
qt-misc-layout-vertical-enabled = Vertical layout enabled
|
qt-misc-layout-vertical-enabled = Vertical layout enabled
|
||||||
qt-misc-layout-horizontal-enabled = Horizontal layout enabled
|
qt-misc-layout-horizontal-enabled = Horizontal layout enabled
|
||||||
qt-misc-please-restart-to-update-anki = Please restart Anki to update to the latest version.
|
qt-misc-open-anki-launcher = Change to a different Anki version?
|
||||||
|
|
||||||
## deprecated- these strings will be removed in the future, and do not need
|
## deprecated- these strings will be removed in the future, and do not need
|
||||||
## to be translated
|
## to be translated
|
||||||
|
|
|
@ -46,7 +46,7 @@
|
||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>667</width>
|
<width>667</width>
|
||||||
<height>24</height>
|
<height>43</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<widget class="QMenu" name="menuHelp">
|
<widget class="QMenu" name="menuHelp">
|
||||||
|
@ -93,6 +93,7 @@
|
||||||
<addaction name="actionAdd_ons"/>
|
<addaction name="actionAdd_ons"/>
|
||||||
<addaction name="separator"/>
|
<addaction name="separator"/>
|
||||||
<addaction name="actionNoteTypes"/>
|
<addaction name="actionNoteTypes"/>
|
||||||
|
<addaction name="action_upgrade_downgrade"/>
|
||||||
<addaction name="actionPreferences"/>
|
<addaction name="actionPreferences"/>
|
||||||
</widget>
|
</widget>
|
||||||
<widget class="QMenu" name="menuqt_accel_view">
|
<widget class="QMenu" name="menuqt_accel_view">
|
||||||
|
@ -130,7 +131,7 @@
|
||||||
<string notr="true">Ctrl+P</string>
|
<string notr="true">Ctrl+P</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="menuRole">
|
<property name="menuRole">
|
||||||
<enum>QAction::PreferencesRole</enum>
|
<enum>QAction::MenuRole::PreferencesRole</enum>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
<action name="actionAbout">
|
<action name="actionAbout">
|
||||||
|
@ -283,6 +284,11 @@
|
||||||
<string>qt_accel_load_backup</string>
|
<string>qt_accel_load_backup</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
|
<action name="action_upgrade_downgrade">
|
||||||
|
<property name="text">
|
||||||
|
<string>qt_accel_upgrade_downgrade</string>
|
||||||
|
</property>
|
||||||
|
</action>
|
||||||
</widget>
|
</widget>
|
||||||
<resources>
|
<resources>
|
||||||
<include location="icons.qrc"/>
|
<include location="icons.qrc"/>
|
||||||
|
|
|
@ -1308,6 +1308,14 @@ title="{}" {}>{}</button>""".format(
|
||||||
def onPrefs(self) -> None:
|
def onPrefs(self) -> None:
|
||||||
aqt.dialogs.open("Preferences", self)
|
aqt.dialogs.open("Preferences", self)
|
||||||
|
|
||||||
|
def on_upgrade_downgrade(self) -> None:
|
||||||
|
if not askUser(tr.qt_misc_open_anki_launcher()):
|
||||||
|
return
|
||||||
|
|
||||||
|
from aqt.update import update_and_restart
|
||||||
|
|
||||||
|
update_and_restart()
|
||||||
|
|
||||||
def onNoteTypes(self) -> None:
|
def onNoteTypes(self) -> None:
|
||||||
import aqt.models
|
import aqt.models
|
||||||
|
|
||||||
|
@ -1389,6 +1397,8 @@ title="{}" {}>{}</button>""".format(
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
def setupMenus(self) -> None:
|
def setupMenus(self) -> None:
|
||||||
|
from aqt.update import have_launcher
|
||||||
|
|
||||||
m = self.form
|
m = self.form
|
||||||
|
|
||||||
# File
|
# File
|
||||||
|
@ -1418,6 +1428,9 @@ title="{}" {}>{}</button>""".format(
|
||||||
qconnect(m.actionCreateFiltered.triggered, self.onCram)
|
qconnect(m.actionCreateFiltered.triggered, self.onCram)
|
||||||
qconnect(m.actionEmptyCards.triggered, self.onEmptyCards)
|
qconnect(m.actionEmptyCards.triggered, self.onEmptyCards)
|
||||||
qconnect(m.actionNoteTypes.triggered, self.onNoteTypes)
|
qconnect(m.actionNoteTypes.triggered, self.onNoteTypes)
|
||||||
|
qconnect(m.action_upgrade_downgrade.triggered, self.on_upgrade_downgrade)
|
||||||
|
if not have_launcher():
|
||||||
|
m.action_upgrade_downgrade.setVisible(False)
|
||||||
qconnect(m.actionPreferences.triggered, self.onPrefs)
|
qconnect(m.actionPreferences.triggered, self.onPrefs)
|
||||||
|
|
||||||
# View
|
# View
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
# Copyright: Ankitects Pty Ltd and contributors
|
# Copyright: Ankitects Pty Ltd and contributors
|
||||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import contextlib
|
||||||
import os
|
import os
|
||||||
|
import subprocess
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import aqt
|
import aqt
|
||||||
|
@ -10,7 +14,7 @@ from anki.collection import CheckForUpdateResponse, Collection
|
||||||
from anki.utils import dev_mode, int_time, int_version, is_mac, is_win, plat_desc
|
from anki.utils import dev_mode, int_time, int_version, is_mac, is_win, plat_desc
|
||||||
from aqt.operations import QueryOp
|
from aqt.operations import QueryOp
|
||||||
from aqt.qt import *
|
from aqt.qt import *
|
||||||
from aqt.utils import show_info, show_warning, showText, tr
|
from aqt.utils import openLink, show_warning, showText, tr
|
||||||
|
|
||||||
|
|
||||||
def check_for_update() -> None:
|
def check_for_update() -> None:
|
||||||
|
@ -80,22 +84,56 @@ def prompt_to_update(mw: aqt.AnkiQt, ver: str) -> None:
|
||||||
# ignore this update
|
# ignore this update
|
||||||
mw.pm.meta["suppressUpdate"] = ver
|
mw.pm.meta["suppressUpdate"] = ver
|
||||||
elif ret == QMessageBox.StandardButton.Yes:
|
elif ret == QMessageBox.StandardButton.Yes:
|
||||||
|
if have_launcher():
|
||||||
update_and_restart()
|
update_and_restart()
|
||||||
|
else:
|
||||||
|
openLink(aqt.appWebsiteDownloadSection)
|
||||||
|
|
||||||
|
|
||||||
|
def _anki_launcher_path() -> str | None:
|
||||||
|
return os.getenv("ANKI_LAUNCHER")
|
||||||
|
|
||||||
|
|
||||||
|
def have_launcher() -> bool:
|
||||||
|
return _anki_launcher_path() is not None
|
||||||
|
|
||||||
|
|
||||||
def update_and_restart() -> None:
|
def update_and_restart() -> None:
|
||||||
"""Download and install the update, then restart Anki."""
|
from aqt import mw
|
||||||
update_on_next_run()
|
|
||||||
# todo: do this automatically in the future
|
launcher = _anki_launcher_path()
|
||||||
show_info(tr.qt_misc_please_restart_to_update_anki())
|
assert launcher
|
||||||
|
|
||||||
|
_trigger_launcher_run()
|
||||||
|
|
||||||
|
with contextlib.suppress(ResourceWarning):
|
||||||
|
env = os.environ.copy()
|
||||||
|
creationflags = 0
|
||||||
|
if sys.platform == "win32":
|
||||||
|
creationflags = (
|
||||||
|
subprocess.CREATE_NEW_PROCESS_GROUP | subprocess.DETACHED_PROCESS
|
||||||
|
)
|
||||||
|
subprocess.Popen(
|
||||||
|
[launcher],
|
||||||
|
start_new_session=True,
|
||||||
|
stdin=subprocess.DEVNULL,
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
env=env,
|
||||||
|
creationflags=creationflags,
|
||||||
|
)
|
||||||
|
|
||||||
|
mw.app.quit()
|
||||||
|
|
||||||
|
|
||||||
def update_on_next_run() -> None:
|
def _trigger_launcher_run() -> None:
|
||||||
"""Bump the mtime on pyproject.toml in the local data directory to trigger an update on next run."""
|
"""Bump the mtime on pyproject.toml in the local data directory to trigger an update on next run."""
|
||||||
try:
|
try:
|
||||||
# Get the local data directory equivalent to Rust's dirs::data_local_dir()
|
# Get the local data directory equivalent to Rust's dirs::data_local_dir()
|
||||||
if is_win:
|
if is_win:
|
||||||
data_dir = Path(os.environ.get("LOCALAPPDATA", ""))
|
from .winpaths import get_local_appdata
|
||||||
|
|
||||||
|
data_dir = Path(get_local_appdata())
|
||||||
elif is_mac:
|
elif is_mac:
|
||||||
data_dir = Path.home() / "Library" / "Application Support"
|
data_dir = Path.home() / "Library" / "Application Support"
|
||||||
else: # Linux
|
else: # Linux
|
||||||
|
|
|
@ -11,6 +11,7 @@ rust-version.workspace = true
|
||||||
anki_io.workspace = true
|
anki_io.workspace = true
|
||||||
anki_process.workspace = true
|
anki_process.workspace = true
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
|
camino.workspace = true
|
||||||
dirs.workspace = true
|
dirs.workspace = true
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
|
@ -22,5 +23,9 @@ libc-stdhandle.workspace = true
|
||||||
name = "build_win"
|
name = "build_win"
|
||||||
path = "src/bin/build_win.rs"
|
path = "src/bin/build_win.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "anki-console"
|
||||||
|
path = "src/bin/anki_console.rs"
|
||||||
|
|
||||||
[target.'cfg(windows)'.build-dependencies]
|
[target.'cfg(windows)'.build-dependencies]
|
||||||
embed-resource.workspace = true
|
embed-resource.workspace = true
|
||||||
|
|
58
qt/launcher/src/bin/anki_console.rs
Normal file
58
qt/launcher/src/bin/anki_console.rs
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
#![windows_subsystem = "console"]
|
||||||
|
|
||||||
|
use std::env;
|
||||||
|
use std::io::stdin;
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
use anyhow::Context;
|
||||||
|
use anyhow::Result;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
if let Err(e) = run() {
|
||||||
|
eprintln!("Error: {:#}", e);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run() -> Result<()> {
|
||||||
|
let current_exe = env::current_exe().context("Failed to get current executable path")?;
|
||||||
|
let exe_dir = current_exe
|
||||||
|
.parent()
|
||||||
|
.context("Failed to get executable directory")?;
|
||||||
|
|
||||||
|
let anki_exe = exe_dir.join("anki.exe");
|
||||||
|
|
||||||
|
if !anki_exe.exists() {
|
||||||
|
anyhow::bail!("anki.exe not found in the same directory");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Forward all command line arguments to anki.exe
|
||||||
|
let args: Vec<String> = env::args().skip(1).collect();
|
||||||
|
|
||||||
|
let mut cmd = Command::new(&anki_exe);
|
||||||
|
cmd.args(&args);
|
||||||
|
|
||||||
|
if std::env::var("ANKI_IMPLICIT_CONSOLE").is_err() {
|
||||||
|
// if directly invoked by the user, signal the launcher that the
|
||||||
|
// user wants a Python console
|
||||||
|
std::env::set_var("ANKI_CONSOLE", "1");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for the process to complete and forward its exit code
|
||||||
|
let status = cmd.status().context("Failed to execute anki.exe")?;
|
||||||
|
if !status.success() {
|
||||||
|
println!("\nPress enter to close.");
|
||||||
|
let mut input = String::new();
|
||||||
|
let _ = stdin().read_line(&mut input);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(code) = status.code() {
|
||||||
|
std::process::exit(code);
|
||||||
|
} else {
|
||||||
|
// Process was terminated by a signal
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
|
@ -114,6 +114,12 @@ fn copy_files(output_dir: &Path) -> Result<()> {
|
||||||
let launcher_dst = output_dir.join("anki.exe");
|
let launcher_dst = output_dir.join("anki.exe");
|
||||||
copy_file(&launcher_src, &launcher_dst)?;
|
copy_file(&launcher_src, &launcher_dst)?;
|
||||||
|
|
||||||
|
// Copy anki-console binary
|
||||||
|
let console_src =
|
||||||
|
PathBuf::from(CARGO_TARGET_DIR).join("x86_64-pc-windows-msvc/release/anki-console.exe");
|
||||||
|
let console_dst = output_dir.join("anki-console.exe");
|
||||||
|
copy_file(&console_src, &console_dst)?;
|
||||||
|
|
||||||
// Copy uv.exe and uvw.exe
|
// Copy uv.exe and uvw.exe
|
||||||
let uv_src = PathBuf::from("../../../out/extracted/uv/uv.exe");
|
let uv_src = PathBuf::from("../../../out/extracted/uv/uv.exe");
|
||||||
let uv_dst = output_dir.join("uv.exe");
|
let uv_dst = output_dir.join("uv.exe");
|
||||||
|
@ -133,14 +139,12 @@ fn copy_files(output_dir: &Path) -> Result<()> {
|
||||||
output_dir.join(".python-version"),
|
output_dir.join(".python-version"),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
// Copy anki-console.bat
|
|
||||||
copy_file("anki-console.bat", output_dir.join("anki-console.bat"))?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sign_binaries(output_dir: &Path) -> Result<()> {
|
fn sign_binaries(output_dir: &Path) -> Result<()> {
|
||||||
sign_file(&output_dir.join("anki.exe"))?;
|
sign_file(&output_dir.join("anki.exe"))?;
|
||||||
|
sign_file(&output_dir.join("anki-console.exe"))?;
|
||||||
sign_file(&output_dir.join("uv.exe"))?;
|
sign_file(&output_dir.join("uv.exe"))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,21 +16,34 @@ use anki_io::modified_time;
|
||||||
use anki_io::read_file;
|
use anki_io::read_file;
|
||||||
use anki_io::remove_file;
|
use anki_io::remove_file;
|
||||||
use anki_io::write_file;
|
use anki_io::write_file;
|
||||||
|
use anki_io::ToUtf8Path;
|
||||||
use anki_process::CommandExt;
|
use anki_process::CommandExt;
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
use crate::platform::ensure_terminal_shown;
|
use crate::platform::ensure_terminal_shown;
|
||||||
use crate::platform::exec_anki;
|
|
||||||
use crate::platform::get_anki_binary_path;
|
|
||||||
use crate::platform::get_exe_and_resources_dirs;
|
use crate::platform::get_exe_and_resources_dirs;
|
||||||
use crate::platform::get_uv_binary_name;
|
use crate::platform::get_uv_binary_name;
|
||||||
use crate::platform::handle_first_launch;
|
use crate::platform::launch_anki_after_update;
|
||||||
use crate::platform::initial_terminal_setup;
|
use crate::platform::launch_anki_normally;
|
||||||
use crate::platform::launch_anki_detached;
|
|
||||||
|
|
||||||
mod platform;
|
mod platform;
|
||||||
|
|
||||||
|
// todo: -c appearing as app name now
|
||||||
|
|
||||||
|
struct State {
|
||||||
|
has_existing_install: bool,
|
||||||
|
prerelease_marker: std::path::PathBuf,
|
||||||
|
uv_install_root: std::path::PathBuf,
|
||||||
|
uv_path: std::path::PathBuf,
|
||||||
|
user_pyproject_path: std::path::PathBuf,
|
||||||
|
user_python_version_path: std::path::PathBuf,
|
||||||
|
dist_pyproject_path: std::path::PathBuf,
|
||||||
|
dist_python_version_path: std::path::PathBuf,
|
||||||
|
uv_lock_path: std::path::PathBuf,
|
||||||
|
sync_complete_marker: std::path::PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum VersionKind {
|
pub enum VersionKind {
|
||||||
PyOxidizer(String),
|
PyOxidizer(String),
|
||||||
|
@ -46,16 +59,8 @@ pub enum MainMenuChoice {
|
||||||
Quit,
|
Quit,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default)]
|
|
||||||
pub struct Config {
|
|
||||||
pub show_console: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
if let Err(e) = run() {
|
if let Err(e) = run() {
|
||||||
let mut config: Config = Config::default();
|
|
||||||
initial_terminal_setup(&mut config);
|
|
||||||
|
|
||||||
eprintln!("Error: {:#}", e);
|
eprintln!("Error: {:#}", e);
|
||||||
eprintln!("Press enter to close...");
|
eprintln!("Press enter to close...");
|
||||||
let mut input = String::new();
|
let mut input = String::new();
|
||||||
|
@ -66,58 +71,92 @@ fn main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run() -> Result<()> {
|
fn run() -> Result<()> {
|
||||||
let mut config: Config = Config::default();
|
|
||||||
|
|
||||||
let uv_install_root = dirs::data_local_dir()
|
let uv_install_root = dirs::data_local_dir()
|
||||||
.context("Unable to determine data_dir")?
|
.context("Unable to determine data_dir")?
|
||||||
.join("AnkiProgramFiles");
|
.join("AnkiProgramFiles");
|
||||||
|
|
||||||
let sync_complete_marker = uv_install_root.join(".sync_complete");
|
|
||||||
let prerelease_marker = uv_install_root.join("prerelease");
|
|
||||||
let (exe_dir, resources_dir) = get_exe_and_resources_dirs()?;
|
let (exe_dir, resources_dir) = get_exe_and_resources_dirs()?;
|
||||||
let dist_pyproject_path = resources_dir.join("pyproject.toml");
|
|
||||||
let user_pyproject_path = uv_install_root.join("pyproject.toml");
|
let state = State {
|
||||||
let dist_python_version_path = resources_dir.join(".python-version");
|
has_existing_install: uv_install_root.join(".sync_complete").exists(),
|
||||||
let user_python_version_path = uv_install_root.join(".python-version");
|
prerelease_marker: uv_install_root.join("prerelease"),
|
||||||
let uv_lock_path = uv_install_root.join("uv.lock");
|
uv_install_root: uv_install_root.clone(),
|
||||||
let uv_path: std::path::PathBuf = exe_dir.join(get_uv_binary_name());
|
uv_path: exe_dir.join(get_uv_binary_name()),
|
||||||
|
user_pyproject_path: uv_install_root.join("pyproject.toml"),
|
||||||
|
user_python_version_path: uv_install_root.join(".python-version"),
|
||||||
|
dist_pyproject_path: resources_dir.join("pyproject.toml"),
|
||||||
|
dist_python_version_path: resources_dir.join(".python-version"),
|
||||||
|
uv_lock_path: uv_install_root.join("uv.lock"),
|
||||||
|
sync_complete_marker: uv_install_root.join(".sync_complete"),
|
||||||
|
};
|
||||||
|
|
||||||
// Create install directory and copy project files in
|
// Create install directory and copy project files in
|
||||||
create_dir_all(&uv_install_root)?;
|
create_dir_all(&state.uv_install_root)?;
|
||||||
let had_user_pyproj = user_pyproject_path.exists();
|
let had_user_pyproj = state.user_pyproject_path.exists();
|
||||||
if !had_user_pyproj {
|
if !had_user_pyproj {
|
||||||
// during initial launcher testing, enable betas by default
|
// during initial launcher testing, enable betas by default
|
||||||
write_file(&prerelease_marker, "")?;
|
write_file(&state.prerelease_marker, "")?;
|
||||||
}
|
}
|
||||||
|
|
||||||
copy_if_newer(&dist_pyproject_path, &user_pyproject_path)?;
|
copy_if_newer(&state.dist_pyproject_path, &state.user_pyproject_path)?;
|
||||||
copy_if_newer(&dist_python_version_path, &user_python_version_path)?;
|
copy_if_newer(
|
||||||
|
&state.dist_python_version_path,
|
||||||
|
&state.user_python_version_path,
|
||||||
|
)?;
|
||||||
|
|
||||||
let pyproject_has_changed = !sync_complete_marker.exists() || {
|
let pyproject_has_changed = !state.sync_complete_marker.exists() || {
|
||||||
let pyproject_toml_time = modified_time(&user_pyproject_path)?;
|
let pyproject_toml_time = modified_time(&state.user_pyproject_path)?;
|
||||||
let sync_complete_time = modified_time(&sync_complete_marker)?;
|
let sync_complete_time = modified_time(&state.sync_complete_marker)?;
|
||||||
Ok::<bool, anyhow::Error>(pyproject_toml_time > sync_complete_time)
|
Ok::<bool, anyhow::Error>(pyproject_toml_time > sync_complete_time)
|
||||||
}
|
}
|
||||||
.unwrap_or(true);
|
.unwrap_or(true);
|
||||||
|
|
||||||
if !pyproject_has_changed {
|
if !pyproject_has_changed {
|
||||||
// If venv is already up to date, exec as normal
|
// If venv is already up to date, launch Anki normally
|
||||||
initial_terminal_setup(&mut config);
|
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||||
let anki_bin = get_anki_binary_path(&uv_install_root);
|
let cmd = build_python_command(&state.uv_install_root, &args)?;
|
||||||
exec_anki(&anki_bin, &config)?;
|
launch_anki_normally(cmd)?;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
// we'll need to launch uv; reinvoke ourselves in a terminal so the user can see
|
// If we weren't in a terminal, respawn ourselves in one
|
||||||
ensure_terminal_shown()?;
|
ensure_terminal_shown()?;
|
||||||
|
|
||||||
print!("\x1B[2J\x1B[H"); // Clear screen and move cursor to top
|
print!("\x1B[2J\x1B[H"); // Clear screen and move cursor to top
|
||||||
println!("\x1B[1mAnki Launcher\x1B[0m\n");
|
println!("\x1B[1mAnki Launcher\x1B[0m\n");
|
||||||
|
|
||||||
// Check if there's an existing installation before removing marker
|
main_menu_loop(&state)?;
|
||||||
let has_existing_install = sync_complete_marker.exists();
|
|
||||||
|
|
||||||
|
// Write marker file to indicate we've completed the sync process
|
||||||
|
write_sync_marker(&state.sync_complete_marker)?;
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
let cmd = build_python_command(&state.uv_install_root, &[])?;
|
||||||
|
platform::mac::prepare_for_launch_after_update(cmd)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg!(unix) && !cfg!(target_os = "macos") {
|
||||||
|
println!("\nPress enter to start Anki.");
|
||||||
|
let mut input = String::new();
|
||||||
|
let _ = stdin().read_line(&mut input);
|
||||||
|
} else {
|
||||||
|
// on Windows/macOS, the user needs to close the terminal/console
|
||||||
|
// currently, but ideas on how we can avoid this would be good!
|
||||||
|
println!("Anki will start shortly.");
|
||||||
|
println!("\x1B[1mYou can close this window.\x1B[0m\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
let cmd = build_python_command(&state.uv_install_root, &[])?;
|
||||||
|
launch_anki_after_update(cmd)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main_menu_loop(state: &State) -> Result<()> {
|
||||||
loop {
|
loop {
|
||||||
let menu_choice = get_main_menu_choice(has_existing_install, &prerelease_marker);
|
let menu_choice =
|
||||||
|
get_main_menu_choice(state.has_existing_install, &state.prerelease_marker);
|
||||||
|
|
||||||
match menu_choice {
|
match menu_choice {
|
||||||
MainMenuChoice::Quit => std::process::exit(0),
|
MainMenuChoice::Quit => std::process::exit(0),
|
||||||
|
@ -127,40 +166,40 @@ fn run() -> Result<()> {
|
||||||
}
|
}
|
||||||
MainMenuChoice::ToggleBetas => {
|
MainMenuChoice::ToggleBetas => {
|
||||||
// Toggle beta prerelease file
|
// Toggle beta prerelease file
|
||||||
if prerelease_marker.exists() {
|
if state.prerelease_marker.exists() {
|
||||||
let _ = remove_file(&prerelease_marker);
|
let _ = remove_file(&state.prerelease_marker);
|
||||||
println!("Beta releases disabled.");
|
println!("Beta releases disabled.");
|
||||||
} else {
|
} else {
|
||||||
write_file(&prerelease_marker, "")?;
|
write_file(&state.prerelease_marker, "")?;
|
||||||
println!("Beta releases enabled.");
|
println!("Beta releases enabled.");
|
||||||
}
|
}
|
||||||
println!();
|
println!();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
_ => {
|
choice @ (MainMenuChoice::Latest | MainMenuChoice::Version(_)) => {
|
||||||
// For other choices, update project files and sync
|
// For other choices, update project files and sync
|
||||||
update_pyproject_for_version(
|
update_pyproject_for_version(
|
||||||
menu_choice.clone(),
|
choice,
|
||||||
dist_pyproject_path.clone(),
|
state.dist_pyproject_path.clone(),
|
||||||
user_pyproject_path.clone(),
|
state.user_pyproject_path.clone(),
|
||||||
dist_python_version_path.clone(),
|
state.dist_python_version_path.clone(),
|
||||||
user_python_version_path.clone(),
|
state.user_python_version_path.clone(),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
// Remove sync marker before attempting sync
|
// Remove sync marker before attempting sync
|
||||||
let _ = remove_file(&sync_complete_marker);
|
let _ = remove_file(&state.sync_complete_marker);
|
||||||
|
|
||||||
// Sync the venv
|
// Sync the venv
|
||||||
let mut command = Command::new(&uv_path);
|
let mut command = Command::new(&state.uv_path);
|
||||||
command.current_dir(&uv_install_root).args([
|
command.current_dir(&state.uv_install_root).args([
|
||||||
"sync",
|
"sync",
|
||||||
"--upgrade",
|
"--upgrade",
|
||||||
"--managed-python",
|
"--managed-python",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Add python version if .python-version file exists
|
// Add python version if .python-version file exists
|
||||||
if user_python_version_path.exists() {
|
if state.user_python_version_path.exists() {
|
||||||
let python_version = read_file(&user_python_version_path)?;
|
let python_version = read_file(&state.user_python_version_path)?;
|
||||||
let python_version_str = String::from_utf8(python_version)
|
let python_version_str = String::from_utf8(python_version)
|
||||||
.context("Invalid UTF-8 in .python-version")?;
|
.context("Invalid UTF-8 in .python-version")?;
|
||||||
let python_version_trimmed = python_version_str.trim();
|
let python_version_trimmed = python_version_str.trim();
|
||||||
|
@ -168,7 +207,7 @@ fn run() -> Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set UV_PRERELEASE=allow if beta mode is enabled
|
// Set UV_PRERELEASE=allow if beta mode is enabled
|
||||||
if prerelease_marker.exists() {
|
if state.prerelease_marker.exists() {
|
||||||
command.env("UV_PRERELEASE", "allow");
|
command.env("UV_PRERELEASE", "allow");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -182,7 +221,7 @@ fn run() -> Result<()> {
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
// If sync fails due to things like a missing wheel on pypi,
|
// If sync fails due to things like a missing wheel on pypi,
|
||||||
// we need to remove the lockfile or uv will cache the bad result.
|
// we need to remove the lockfile or uv will cache the bad result.
|
||||||
let _ = remove_file(&uv_lock_path);
|
let _ = remove_file(&state.uv_lock_path);
|
||||||
println!("Install failed: {:#}", e);
|
println!("Install failed: {:#}", e);
|
||||||
println!();
|
println!();
|
||||||
continue;
|
continue;
|
||||||
|
@ -191,22 +230,6 @@ fn run() -> Result<()> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write marker file to indicate we've completed the sync process
|
|
||||||
write_sync_marker(&sync_complete_marker)?;
|
|
||||||
|
|
||||||
// First launch
|
|
||||||
let anki_bin = get_anki_binary_path(&uv_install_root);
|
|
||||||
handle_first_launch(&anki_bin)?;
|
|
||||||
|
|
||||||
println!("\nPress enter to start Anki.");
|
|
||||||
|
|
||||||
let mut input = String::new();
|
|
||||||
let _ = stdin().read_line(&mut input);
|
|
||||||
|
|
||||||
// Then launch the binary as detached subprocess so the terminal can close
|
|
||||||
launch_anki_detached(&anki_bin, &config)?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -403,3 +426,25 @@ fn parse_version_kind(version: &str) -> Option<VersionKind> {
|
||||||
Some(VersionKind::Uv(version.to_string()))
|
Some(VersionKind::Uv(version.to_string()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn build_python_command(uv_install_root: &std::path::Path, args: &[String]) -> Result<Command> {
|
||||||
|
let python_exe = if cfg!(target_os = "windows") {
|
||||||
|
let show_console = std::env::var("ANKI_CONSOLE").is_ok();
|
||||||
|
if show_console {
|
||||||
|
uv_install_root.join(".venv/Scripts/python.exe")
|
||||||
|
} else {
|
||||||
|
uv_install_root.join(".venv/Scripts/pythonw.exe")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
uv_install_root.join(".venv/bin/python")
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut cmd = Command::new(python_exe);
|
||||||
|
cmd.args(["-c", "import aqt; aqt.run()"]);
|
||||||
|
cmd.args(args);
|
||||||
|
// tell the Python code it was invoked by the launcher, and updating is
|
||||||
|
// available
|
||||||
|
cmd.env("ANKI_LAUNCHER", std::env::current_exe()?.utf8()?.as_str());
|
||||||
|
|
||||||
|
Ok(cmd)
|
||||||
|
}
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
// Copyright: Ankitects Pty Ltd and contributors
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
use std::os::unix::process::CommandExt;
|
use std::io;
|
||||||
|
use std::io::Write;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
use std::sync::atomic::AtomicBool;
|
use std::sync::atomic::AtomicBool;
|
||||||
use std::sync::atomic::Ordering;
|
use std::sync::atomic::Ordering;
|
||||||
|
@ -13,45 +14,7 @@ use anki_process::CommandExt as AnkiCommandExt;
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
// Re-export Unix functions that macOS uses
|
pub fn prepare_for_launch_after_update(mut cmd: Command) -> Result<()> {
|
||||||
pub use super::unix::{
|
|
||||||
ensure_terminal_shown,
|
|
||||||
exec_anki,
|
|
||||||
get_anki_binary_path,
|
|
||||||
initial_terminal_setup,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn launch_anki_detached(anki_bin: &std::path::Path, _config: &crate::Config) -> Result<()> {
|
|
||||||
use std::process::Stdio;
|
|
||||||
|
|
||||||
let child = Command::new(anki_bin)
|
|
||||||
.stdin(Stdio::null())
|
|
||||||
.stdout(Stdio::null())
|
|
||||||
.stderr(Stdio::null())
|
|
||||||
.process_group(0)
|
|
||||||
.ensure_spawn()?;
|
|
||||||
std::mem::forget(child);
|
|
||||||
|
|
||||||
println!("Anki will start shortly.");
|
|
||||||
println!("\x1B[1mYou can close this window.\x1B[0m\n");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn relaunch_in_terminal() -> Result<()> {
|
|
||||||
let current_exe = std::env::current_exe().context("Failed to get current executable path")?;
|
|
||||||
Command::new("open")
|
|
||||||
.args(["-a", "Terminal"])
|
|
||||||
.arg(current_exe)
|
|
||||||
.ensure_spawn()?;
|
|
||||||
std::process::exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn handle_first_launch(anki_bin: &std::path::Path) -> Result<()> {
|
|
||||||
use std::io::Write;
|
|
||||||
use std::io::{
|
|
||||||
self,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Pre-validate by running --version to trigger any Gatekeeper checks
|
// Pre-validate by running --version to trigger any Gatekeeper checks
|
||||||
print!("\n\x1B[1mThis may take a few minutes. Please wait\x1B[0m");
|
print!("\n\x1B[1mThis may take a few minutes. Please wait\x1B[0m");
|
||||||
io::stdout().flush().unwrap();
|
io::stdout().flush().unwrap();
|
||||||
|
@ -67,7 +30,7 @@ pub fn handle_first_launch(anki_bin: &std::path::Path) -> Result<()> {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let _ = Command::new(anki_bin)
|
let _ = cmd
|
||||||
.env("ANKI_FIRST_RUN", "1")
|
.env("ANKI_FIRST_RUN", "1")
|
||||||
.arg("--version")
|
.arg("--version")
|
||||||
.stdout(std::process::Stdio::null())
|
.stdout(std::process::Stdio::null())
|
||||||
|
@ -81,22 +44,11 @@ pub fn handle_first_launch(anki_bin: &std::path::Path) -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_exe_and_resources_dirs() -> Result<(std::path::PathBuf, std::path::PathBuf)> {
|
pub fn relaunch_in_terminal() -> Result<()> {
|
||||||
let exe_dir = std::env::current_exe()
|
let current_exe = std::env::current_exe().context("Failed to get current executable path")?;
|
||||||
.context("Failed to get current executable path")?
|
Command::new("open")
|
||||||
.parent()
|
.args(["-a", "Terminal"])
|
||||||
.context("Failed to get executable directory")?
|
.arg(current_exe)
|
||||||
.to_owned();
|
.ensure_spawn()?;
|
||||||
|
std::process::exit(0);
|
||||||
let resources_dir = exe_dir
|
|
||||||
.parent()
|
|
||||||
.context("Failed to get parent directory")?
|
|
||||||
.join("Resources");
|
|
||||||
|
|
||||||
Ok((exe_dir, resources_dir))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_uv_binary_name() -> &'static str {
|
|
||||||
// macOS uses standard uv binary name
|
|
||||||
"uv"
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,108 @@
|
||||||
// Copyright: Ankitects Pty Ltd and contributors
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(all(unix, not(target_os = "macos")))]
|
||||||
mod unix;
|
mod unix;
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
mod mac;
|
pub mod mac;
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
mod windows;
|
pub mod windows;
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
use std::path::PathBuf;
|
||||||
pub use mac::*;
|
|
||||||
#[cfg(all(unix, not(target_os = "macos")))]
|
use anki_process::CommandExt;
|
||||||
pub use unix::*;
|
use anyhow::Context;
|
||||||
#[cfg(target_os = "windows")]
|
use anyhow::Result;
|
||||||
pub use windows::*;
|
|
||||||
|
pub fn get_exe_and_resources_dirs() -> Result<(PathBuf, PathBuf)> {
|
||||||
|
let exe_dir = std::env::current_exe()
|
||||||
|
.context("Failed to get current executable path")?
|
||||||
|
.parent()
|
||||||
|
.context("Failed to get executable directory")?
|
||||||
|
.to_owned();
|
||||||
|
|
||||||
|
let resources_dir = if cfg!(target_os = "macos") {
|
||||||
|
// On macOS, resources are in ../Resources relative to the executable
|
||||||
|
exe_dir
|
||||||
|
.parent()
|
||||||
|
.context("Failed to get parent directory")?
|
||||||
|
.join("Resources")
|
||||||
|
} else {
|
||||||
|
// On other platforms, resources are in the same directory as executable
|
||||||
|
exe_dir.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok((exe_dir, resources_dir))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_uv_binary_name() -> &'static str {
|
||||||
|
if cfg!(target_os = "windows") {
|
||||||
|
"uv.exe"
|
||||||
|
} else if cfg!(target_os = "macos") {
|
||||||
|
"uv"
|
||||||
|
} else if cfg!(target_arch = "x86_64") {
|
||||||
|
"uv.amd64"
|
||||||
|
} else {
|
||||||
|
"uv.arm64"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn launch_anki_after_update(mut cmd: std::process::Command) -> Result<()> {
|
||||||
|
use std::process::Stdio;
|
||||||
|
|
||||||
|
cmd.stdin(Stdio::null())
|
||||||
|
.stdout(Stdio::null())
|
||||||
|
.stderr(Stdio::null());
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
use std::os::windows::process::CommandExt;
|
||||||
|
const CREATE_NEW_PROCESS_GROUP: u32 = 0x00000200;
|
||||||
|
const DETACHED_PROCESS: u32 = 0x00000008;
|
||||||
|
cmd.creation_flags(CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
use std::os::unix::process::CommandExt;
|
||||||
|
cmd.process_group(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
let child = cmd.ensure_spawn()?;
|
||||||
|
std::mem::forget(child);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn launch_anki_normally(mut cmd: std::process::Command) -> Result<()> {
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
crate::platform::windows::attach_to_parent_console();
|
||||||
|
cmd.ensure_success()?;
|
||||||
|
}
|
||||||
|
#[cfg(unix)]
|
||||||
|
cmd.ensure_exec()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
pub use windows::ensure_terminal_shown;
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
pub fn ensure_terminal_shown() -> Result<()> {
|
||||||
|
use std::io::IsTerminal;
|
||||||
|
|
||||||
|
let stdout_is_terminal = IsTerminal::is_terminal(&std::io::stdout());
|
||||||
|
if !stdout_is_terminal {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
mac::relaunch_in_terminal()?;
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
unix::relaunch_in_terminal()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set terminal title to "Anki Launcher"
|
||||||
|
print!("\x1b]2;Anki Launcher\x07");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
|
@ -1,36 +1,11 @@
|
||||||
// Copyright: Ankitects Pty Ltd and contributors
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
#![allow(dead_code)]
|
|
||||||
|
|
||||||
use std::io::IsTerminal;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
use anki_process::CommandExt as AnkiCommandExt;
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
use crate::Config;
|
|
||||||
|
|
||||||
pub fn initial_terminal_setup(_config: &mut Config) {
|
|
||||||
// No special terminal setup needed on Unix
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn ensure_terminal_shown() -> Result<()> {
|
|
||||||
let stdout_is_terminal = IsTerminal::is_terminal(&std::io::stdout());
|
|
||||||
if !stdout_is_terminal {
|
|
||||||
// If launched from GUI, try to relaunch in a terminal
|
|
||||||
crate::platform::relaunch_in_terminal()?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set terminal title to "Anki Launcher"
|
|
||||||
print!("\x1b]2;Anki Launcher\x07");
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "macos"))]
|
|
||||||
pub fn relaunch_in_terminal() -> Result<()> {
|
pub fn relaunch_in_terminal() -> Result<()> {
|
||||||
let current_exe = std::env::current_exe().context("Failed to get current executable path")?;
|
let current_exe = std::env::current_exe().context("Failed to get current executable path")?;
|
||||||
|
|
||||||
|
@ -72,52 +47,3 @@ pub fn relaunch_in_terminal() -> Result<()> {
|
||||||
// If no terminal worked, continue without relaunching
|
// If no terminal worked, continue without relaunching
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_anki_binary_path(uv_install_root: &std::path::Path) -> PathBuf {
|
|
||||||
uv_install_root.join(".venv/bin/anki")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn launch_anki_detached(anki_bin: &std::path::Path, config: &Config) -> Result<()> {
|
|
||||||
// On non-macOS Unix systems, we don't need to detach since we never spawned a
|
|
||||||
// terminal
|
|
||||||
exec_anki(anki_bin, config)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn handle_first_launch(_anki_bin: &std::path::Path) -> Result<()> {
|
|
||||||
// No special first launch handling needed for generic Unix systems
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn exec_anki(anki_bin: &std::path::Path, _config: &Config) -> Result<()> {
|
|
||||||
let args: Vec<String> = std::env::args().skip(1).collect();
|
|
||||||
Command::new(anki_bin)
|
|
||||||
.args(args)
|
|
||||||
.ensure_exec()
|
|
||||||
.map_err(anyhow::Error::new)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_exe_and_resources_dirs() -> Result<(PathBuf, PathBuf)> {
|
|
||||||
let exe_dir = std::env::current_exe()
|
|
||||||
.context("Failed to get current executable path")?
|
|
||||||
.parent()
|
|
||||||
.context("Failed to get executable directory")?
|
|
||||||
.to_owned();
|
|
||||||
|
|
||||||
// On generic Unix systems, assume resources are in the same directory as
|
|
||||||
// executable
|
|
||||||
let resources_dir = exe_dir.clone();
|
|
||||||
|
|
||||||
Ok((exe_dir, resources_dir))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_uv_binary_name() -> &'static str {
|
|
||||||
// Use architecture-specific uv binary for non-Mac Unix systems
|
|
||||||
if cfg!(target_arch = "x86_64") {
|
|
||||||
"uv.amd64"
|
|
||||||
} else if cfg!(target_arch = "aarch64") {
|
|
||||||
"uv.arm64"
|
|
||||||
} else {
|
|
||||||
// Fallback to generic uv for other architectures
|
|
||||||
"uv"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,82 +1,71 @@
|
||||||
// Copyright: Ankitects Pty Ltd and contributors
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
use anki_process::CommandExt;
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use winapi::um::consoleapi;
|
|
||||||
use winapi::um::errhandlingapi;
|
|
||||||
use winapi::um::wincon;
|
use winapi::um::wincon;
|
||||||
|
|
||||||
use crate::Config;
|
|
||||||
|
|
||||||
pub fn ensure_terminal_shown() -> Result<()> {
|
pub fn ensure_terminal_shown() -> Result<()> {
|
||||||
ensure_console();
|
|
||||||
// // Check if we're already relaunched to prevent infinite recursion
|
|
||||||
// if std::env::var("ANKI_LAUNCHER_IN_TERMINAL").is_ok() {
|
|
||||||
// println!("Recurse: Preparing to start Anki...\n");
|
|
||||||
// return Ok(());
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if have_console {
|
|
||||||
// } else {
|
|
||||||
// relaunch_in_cmd()?;
|
|
||||||
// }
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ensure_console() {
|
|
||||||
unsafe {
|
unsafe {
|
||||||
if !wincon::GetConsoleWindow().is_null() {
|
if !wincon::GetConsoleWindow().is_null() {
|
||||||
return;
|
// We already have a console, no need to spawn anki-console.exe
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if consoleapi::AllocConsole() == 0 {
|
if std::env::var("ANKI_IMPLICIT_CONSOLE").is_ok() && attach_to_parent_console() {
|
||||||
let error_code = errhandlingapi::GetLastError();
|
// Successfully attached to parent console
|
||||||
eprintln!("unexpected AllocConsole error: {}", error_code);
|
reconnect_stdio_to_console();
|
||||||
return;
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
// This black magic triggers Windows to switch to the new
|
// No console available, spawn anki-console.exe and exit
|
||||||
// ANSI-supporting console host, which is usually only available
|
let current_exe = std::env::current_exe().context("Failed to get current executable path")?;
|
||||||
// when the app is built with the console subsystem.
|
let exe_dir = current_exe
|
||||||
let _ = Command::new("cmd").args(&["/C", ""]).status();
|
.parent()
|
||||||
|
.context("Failed to get executable directory")?;
|
||||||
|
|
||||||
|
let console_exe = exe_dir.join("anki-console.exe");
|
||||||
|
|
||||||
|
if !console_exe.exists() {
|
||||||
|
anyhow::bail!("anki-console.exe not found in the same directory");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Spawn anki-console.exe without waiting
|
||||||
|
Command::new(&console_exe)
|
||||||
|
.env("ANKI_IMPLICIT_CONSOLE", "1")
|
||||||
|
.spawn()
|
||||||
|
.context("Failed to spawn anki-console.exe")?;
|
||||||
|
|
||||||
|
// Exit immediately after spawning
|
||||||
|
std::process::exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn attach_to_parent_console() -> bool {
|
pub fn attach_to_parent_console() -> bool {
|
||||||
unsafe {
|
unsafe {
|
||||||
if !wincon::GetConsoleWindow().is_null() {
|
if !wincon::GetConsoleWindow().is_null() {
|
||||||
// we have a console already
|
// we have a console already
|
||||||
println!("attach: already had console, false");
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if wincon::AttachConsole(wincon::ATTACH_PARENT_PROCESS) != 0 {
|
if wincon::AttachConsole(wincon::ATTACH_PARENT_PROCESS) != 0 {
|
||||||
// successfully attached to parent
|
// successfully attached to parent
|
||||||
println!("attach: true");
|
reconnect_stdio_to_console();
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
println!("attach: false");
|
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// If parent process has a console (eg cmd.exe), redirect our output there.
|
/// Reconnect stdin/stdout/stderr to the console.
|
||||||
/// Sets config.show_console to true if successfully attached to console.
|
fn reconnect_stdio_to_console() {
|
||||||
pub fn initial_terminal_setup(config: &mut Config) {
|
|
||||||
use std::ffi::CString;
|
use std::ffi::CString;
|
||||||
|
|
||||||
use libc_stdhandle::*;
|
use libc_stdhandle::*;
|
||||||
|
|
||||||
if !attach_to_parent_console() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// we launched without a console, so we'll need to open stdin/out/err
|
// we launched without a console, so we'll need to open stdin/out/err
|
||||||
let conin = CString::new("CONIN$").unwrap();
|
let conin = CString::new("CONIN$").unwrap();
|
||||||
let conout = CString::new("CONOUT$").unwrap();
|
let conout = CString::new("CONOUT$").unwrap();
|
||||||
|
@ -89,79 +78,4 @@ pub fn initial_terminal_setup(config: &mut Config) {
|
||||||
libc::freopen(conout.as_ptr(), w.as_ptr(), stdout());
|
libc::freopen(conout.as_ptr(), w.as_ptr(), stdout());
|
||||||
libc::freopen(conout.as_ptr(), w.as_ptr(), stderr());
|
libc::freopen(conout.as_ptr(), w.as_ptr(), stderr());
|
||||||
}
|
}
|
||||||
|
|
||||||
config.show_console = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_anki_binary_path(uv_install_root: &std::path::Path) -> std::path::PathBuf {
|
|
||||||
uv_install_root.join(".venv/Scripts/anki.exe")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_python_command(
|
|
||||||
anki_bin: &std::path::Path,
|
|
||||||
args: &[String],
|
|
||||||
config: &Config,
|
|
||||||
) -> Result<Command> {
|
|
||||||
let venv_dir = anki_bin
|
|
||||||
.parent()
|
|
||||||
.context("Failed to get venv Scripts directory")?
|
|
||||||
.parent()
|
|
||||||
.context("Failed to get venv directory")?;
|
|
||||||
|
|
||||||
// Use python.exe if show_console is true, otherwise pythonw.exe
|
|
||||||
let python_exe = if config.show_console {
|
|
||||||
venv_dir.join("Scripts/python.exe")
|
|
||||||
} else {
|
|
||||||
venv_dir.join("Scripts/pythonw.exe")
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut cmd = Command::new(python_exe);
|
|
||||||
cmd.args(["-c", "import aqt; aqt.run()"]);
|
|
||||||
cmd.args(args);
|
|
||||||
|
|
||||||
Ok(cmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn launch_anki_detached(anki_bin: &std::path::Path, config: &Config) -> Result<()> {
|
|
||||||
use std::os::windows::process::CommandExt;
|
|
||||||
use std::process::Stdio;
|
|
||||||
|
|
||||||
const CREATE_NEW_PROCESS_GROUP: u32 = 0x00000200;
|
|
||||||
const DETACHED_PROCESS: u32 = 0x00000008;
|
|
||||||
|
|
||||||
let mut cmd = build_python_command(anki_bin, &[], config)?;
|
|
||||||
cmd.stdin(Stdio::null())
|
|
||||||
.stdout(Stdio::null())
|
|
||||||
.stderr(Stdio::null())
|
|
||||||
.creation_flags(CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS)
|
|
||||||
.ensure_spawn()?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn handle_first_launch(_anki_bin: &std::path::Path) -> Result<()> {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn exec_anki(anki_bin: &std::path::Path, config: &Config) -> Result<()> {
|
|
||||||
let args: Vec<String> = std::env::args().skip(1).collect();
|
|
||||||
let mut cmd = build_python_command(anki_bin, &args, config)?;
|
|
||||||
cmd.ensure_success()?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_exe_and_resources_dirs() -> Result<(PathBuf, PathBuf)> {
|
|
||||||
let exe_dir = std::env::current_exe()
|
|
||||||
.context("Failed to get current executable path")?
|
|
||||||
.parent()
|
|
||||||
.context("Failed to get executable directory")?
|
|
||||||
.to_owned();
|
|
||||||
|
|
||||||
// On Windows, resources dir is the same as exe_dir
|
|
||||||
let resources_dir = exe_dir.clone();
|
|
||||||
|
|
||||||
Ok((exe_dir, resources_dir))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_uv_binary_name() -> &'static str {
|
|
||||||
"uv.exe"
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
@echo off
|
|
||||||
"%~dp0"\anki %*
|
|
||||||
pause
|
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,10 @@
|
||||||
@echo off
|
@echo off
|
||||||
|
|
||||||
set CODESIGN=1
|
if "%NOCOMP%"=="1" (
|
||||||
REM set NO_COMPRESS=1
|
set NO_COMPRESS=1
|
||||||
|
set CODESIGN=0
|
||||||
|
) else (
|
||||||
|
set CODESIGN=1
|
||||||
|
set NO_COMPRESS=0
|
||||||
|
)
|
||||||
cargo run --bin build_win
|
cargo run --bin build_win
|
||||||
|
|
|
@ -13,4 +13,9 @@ path = "main.rs"
|
||||||
name = "anki-sync-server"
|
name = "anki-sync-server"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|
||||||
|
[target.'cfg(windows)'.dependencies]
|
||||||
|
anki = { workspace = true, features = ["native-tls"] }
|
||||||
|
|
||||||
|
[target.'cfg(not(windows))'.dependencies]
|
||||||
anki = { workspace = true, features = ["rustls"] }
|
anki = { workspace = true, features = ["rustls"] }
|
||||||
|
|
15
tools/update-launcher-env
Executable file
15
tools/update-launcher-env
Executable file
|
@ -0,0 +1,15 @@
|
||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# Install our latest anki/aqt code into the launcher venv
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
rm -rf out/wheels
|
||||||
|
./ninja wheels
|
||||||
|
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||||
|
export VIRTUAL_ENV=$HOME/Library/Application\ Support/AnkiProgramFiles/.venv
|
||||||
|
else
|
||||||
|
export VIRTUAL_ENV=$HOME/.local/share/AnkiProgramFiles/.venv
|
||||||
|
fi
|
||||||
|
./out/extracted/uv/uv pip install out/wheels/*
|
||||||
|
|
8
tools/update-launcher-env.bat
Normal file
8
tools/update-launcher-env.bat
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
@echo off
|
||||||
|
rem
|
||||||
|
rem Install our latest anki/aqt code into the launcher venv
|
||||||
|
|
||||||
|
rmdir /s /q out\wheels 2>nul
|
||||||
|
call tools\ninja wheels
|
||||||
|
set VIRTUAL_ENV=%LOCALAPPDATA%\AnkiProgramFiles\.venv
|
||||||
|
for %%f in (out\wheels\*.whl) do out\extracted\uv\uv pip install "%%f"
|
Loading…
Reference in a new issue