mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 14:02: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_process",
|
||||
"anyhow",
|
||||
"camino",
|
||||
"dirs 6.0.0",
|
||||
"embed-resource",
|
||||
"libc",
|
||||
|
|
|
@ -138,7 +138,7 @@ unic-ucd-category = "0.9.0"
|
|||
unicode-normalization = "0.1.24"
|
||||
walkdir = "2.5.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"] }
|
||||
wiremock = "0.6.3"
|
||||
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-create-backup = Create &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-vertical-enabled = Vertical 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
|
||||
## to be translated
|
||||
|
|
|
@ -46,7 +46,7 @@
|
|||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>667</width>
|
||||
<height>24</height>
|
||||
<height>43</height>
|
||||
</rect>
|
||||
</property>
|
||||
<widget class="QMenu" name="menuHelp">
|
||||
|
@ -93,6 +93,7 @@
|
|||
<addaction name="actionAdd_ons"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="actionNoteTypes"/>
|
||||
<addaction name="action_upgrade_downgrade"/>
|
||||
<addaction name="actionPreferences"/>
|
||||
</widget>
|
||||
<widget class="QMenu" name="menuqt_accel_view">
|
||||
|
@ -130,7 +131,7 @@
|
|||
<string notr="true">Ctrl+P</string>
|
||||
</property>
|
||||
<property name="menuRole">
|
||||
<enum>QAction::PreferencesRole</enum>
|
||||
<enum>QAction::MenuRole::PreferencesRole</enum>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionAbout">
|
||||
|
@ -283,6 +284,11 @@
|
|||
<string>qt_accel_load_backup</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_upgrade_downgrade">
|
||||
<property name="text">
|
||||
<string>qt_accel_upgrade_downgrade</string>
|
||||
</property>
|
||||
</action>
|
||||
</widget>
|
||||
<resources>
|
||||
<include location="icons.qrc"/>
|
||||
|
|
|
@ -1308,6 +1308,14 @@ title="{}" {}>{}</button>""".format(
|
|||
def onPrefs(self) -> None:
|
||||
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:
|
||||
import aqt.models
|
||||
|
||||
|
@ -1389,6 +1397,8 @@ title="{}" {}>{}</button>""".format(
|
|||
##########################################################################
|
||||
|
||||
def setupMenus(self) -> None:
|
||||
from aqt.update import have_launcher
|
||||
|
||||
m = self.form
|
||||
|
||||
# File
|
||||
|
@ -1418,6 +1428,9 @@ title="{}" {}>{}</button>""".format(
|
|||
qconnect(m.actionCreateFiltered.triggered, self.onCram)
|
||||
qconnect(m.actionEmptyCards.triggered, self.onEmptyCards)
|
||||
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)
|
||||
|
||||
# View
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
# Copyright: Ankitects Pty Ltd and contributors
|
||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import os
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
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 aqt.operations import QueryOp
|
||||
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:
|
||||
|
@ -80,22 +84,56 @@ def prompt_to_update(mw: aqt.AnkiQt, ver: str) -> None:
|
|||
# ignore this update
|
||||
mw.pm.meta["suppressUpdate"] = ver
|
||||
elif ret == QMessageBox.StandardButton.Yes:
|
||||
update_and_restart()
|
||||
if have_launcher():
|
||||
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:
|
||||
"""Download and install the update, then restart Anki."""
|
||||
update_on_next_run()
|
||||
# todo: do this automatically in the future
|
||||
show_info(tr.qt_misc_please_restart_to_update_anki())
|
||||
from aqt import mw
|
||||
|
||||
launcher = _anki_launcher_path()
|
||||
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."""
|
||||
try:
|
||||
# Get the local data directory equivalent to Rust's dirs::data_local_dir()
|
||||
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:
|
||||
data_dir = Path.home() / "Library" / "Application Support"
|
||||
else: # Linux
|
||||
|
|
|
@ -11,6 +11,7 @@ rust-version.workspace = true
|
|||
anki_io.workspace = true
|
||||
anki_process.workspace = true
|
||||
anyhow.workspace = true
|
||||
camino.workspace = true
|
||||
dirs.workspace = true
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
|
@ -22,5 +23,9 @@ libc-stdhandle.workspace = true
|
|||
name = "build_win"
|
||||
path = "src/bin/build_win.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "anki-console"
|
||||
path = "src/bin/anki_console.rs"
|
||||
|
||||
[target.'cfg(windows)'.build-dependencies]
|
||||
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");
|
||||
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
|
||||
let uv_src = PathBuf::from("../../../out/extracted/uv/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"),
|
||||
)?;
|
||||
|
||||
// Copy anki-console.bat
|
||||
copy_file("anki-console.bat", output_dir.join("anki-console.bat"))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn sign_binaries(output_dir: &Path) -> Result<()> {
|
||||
sign_file(&output_dir.join("anki.exe"))?;
|
||||
sign_file(&output_dir.join("anki-console.exe"))?;
|
||||
sign_file(&output_dir.join("uv.exe"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -16,21 +16,34 @@ use anki_io::modified_time;
|
|||
use anki_io::read_file;
|
||||
use anki_io::remove_file;
|
||||
use anki_io::write_file;
|
||||
use anki_io::ToUtf8Path;
|
||||
use anki_process::CommandExt;
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
|
||||
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_uv_binary_name;
|
||||
use crate::platform::handle_first_launch;
|
||||
use crate::platform::initial_terminal_setup;
|
||||
use crate::platform::launch_anki_detached;
|
||||
use crate::platform::launch_anki_after_update;
|
||||
use crate::platform::launch_anki_normally;
|
||||
|
||||
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)]
|
||||
pub enum VersionKind {
|
||||
PyOxidizer(String),
|
||||
|
@ -46,16 +59,8 @@ pub enum MainMenuChoice {
|
|||
Quit,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Config {
|
||||
pub show_console: bool,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
if let Err(e) = run() {
|
||||
let mut config: Config = Config::default();
|
||||
initial_terminal_setup(&mut config);
|
||||
|
||||
eprintln!("Error: {:#}", e);
|
||||
eprintln!("Press enter to close...");
|
||||
let mut input = String::new();
|
||||
|
@ -66,58 +71,92 @@ fn main() {
|
|||
}
|
||||
|
||||
fn run() -> Result<()> {
|
||||
let mut config: Config = Config::default();
|
||||
|
||||
let uv_install_root = dirs::data_local_dir()
|
||||
.context("Unable to determine data_dir")?
|
||||
.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 dist_pyproject_path = resources_dir.join("pyproject.toml");
|
||||
let user_pyproject_path = uv_install_root.join("pyproject.toml");
|
||||
let dist_python_version_path = resources_dir.join(".python-version");
|
||||
let user_python_version_path = uv_install_root.join(".python-version");
|
||||
let uv_lock_path = uv_install_root.join("uv.lock");
|
||||
let uv_path: std::path::PathBuf = exe_dir.join(get_uv_binary_name());
|
||||
|
||||
let state = State {
|
||||
has_existing_install: uv_install_root.join(".sync_complete").exists(),
|
||||
prerelease_marker: uv_install_root.join("prerelease"),
|
||||
uv_install_root: uv_install_root.clone(),
|
||||
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_dir_all(&uv_install_root)?;
|
||||
let had_user_pyproj = user_pyproject_path.exists();
|
||||
create_dir_all(&state.uv_install_root)?;
|
||||
let had_user_pyproj = state.user_pyproject_path.exists();
|
||||
if !had_user_pyproj {
|
||||
// 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(&dist_python_version_path, &user_python_version_path)?;
|
||||
copy_if_newer(&state.dist_pyproject_path, &state.user_pyproject_path)?;
|
||||
copy_if_newer(
|
||||
&state.dist_python_version_path,
|
||||
&state.user_python_version_path,
|
||||
)?;
|
||||
|
||||
let pyproject_has_changed = !sync_complete_marker.exists() || {
|
||||
let pyproject_toml_time = modified_time(&user_pyproject_path)?;
|
||||
let sync_complete_time = modified_time(&sync_complete_marker)?;
|
||||
let pyproject_has_changed = !state.sync_complete_marker.exists() || {
|
||||
let pyproject_toml_time = modified_time(&state.user_pyproject_path)?;
|
||||
let sync_complete_time = modified_time(&state.sync_complete_marker)?;
|
||||
Ok::<bool, anyhow::Error>(pyproject_toml_time > sync_complete_time)
|
||||
}
|
||||
.unwrap_or(true);
|
||||
|
||||
if !pyproject_has_changed {
|
||||
// If venv is already up to date, exec as normal
|
||||
initial_terminal_setup(&mut config);
|
||||
let anki_bin = get_anki_binary_path(&uv_install_root);
|
||||
exec_anki(&anki_bin, &config)?;
|
||||
// If venv is already up to date, launch Anki normally
|
||||
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||
let cmd = build_python_command(&state.uv_install_root, &args)?;
|
||||
launch_anki_normally(cmd)?;
|
||||
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()?;
|
||||
|
||||
print!("\x1B[2J\x1B[H"); // Clear screen and move cursor to top
|
||||
println!("\x1B[1mAnki Launcher\x1B[0m\n");
|
||||
|
||||
// Check if there's an existing installation before removing marker
|
||||
let has_existing_install = sync_complete_marker.exists();
|
||||
main_menu_loop(&state)?;
|
||||
|
||||
// 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 {
|
||||
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 {
|
||||
MainMenuChoice::Quit => std::process::exit(0),
|
||||
|
@ -127,40 +166,40 @@ fn run() -> Result<()> {
|
|||
}
|
||||
MainMenuChoice::ToggleBetas => {
|
||||
// Toggle beta prerelease file
|
||||
if prerelease_marker.exists() {
|
||||
let _ = remove_file(&prerelease_marker);
|
||||
if state.prerelease_marker.exists() {
|
||||
let _ = remove_file(&state.prerelease_marker);
|
||||
println!("Beta releases disabled.");
|
||||
} else {
|
||||
write_file(&prerelease_marker, "")?;
|
||||
write_file(&state.prerelease_marker, "")?;
|
||||
println!("Beta releases enabled.");
|
||||
}
|
||||
println!();
|
||||
continue;
|
||||
}
|
||||
_ => {
|
||||
choice @ (MainMenuChoice::Latest | MainMenuChoice::Version(_)) => {
|
||||
// For other choices, update project files and sync
|
||||
update_pyproject_for_version(
|
||||
menu_choice.clone(),
|
||||
dist_pyproject_path.clone(),
|
||||
user_pyproject_path.clone(),
|
||||
dist_python_version_path.clone(),
|
||||
user_python_version_path.clone(),
|
||||
choice,
|
||||
state.dist_pyproject_path.clone(),
|
||||
state.user_pyproject_path.clone(),
|
||||
state.dist_python_version_path.clone(),
|
||||
state.user_python_version_path.clone(),
|
||||
)?;
|
||||
|
||||
// Remove sync marker before attempting sync
|
||||
let _ = remove_file(&sync_complete_marker);
|
||||
let _ = remove_file(&state.sync_complete_marker);
|
||||
|
||||
// Sync the venv
|
||||
let mut command = Command::new(&uv_path);
|
||||
command.current_dir(&uv_install_root).args([
|
||||
let mut command = Command::new(&state.uv_path);
|
||||
command.current_dir(&state.uv_install_root).args([
|
||||
"sync",
|
||||
"--upgrade",
|
||||
"--managed-python",
|
||||
]);
|
||||
|
||||
// Add python version if .python-version file exists
|
||||
if user_python_version_path.exists() {
|
||||
let python_version = read_file(&user_python_version_path)?;
|
||||
if state.user_python_version_path.exists() {
|
||||
let python_version = read_file(&state.user_python_version_path)?;
|
||||
let python_version_str = String::from_utf8(python_version)
|
||||
.context("Invalid UTF-8 in .python-version")?;
|
||||
let python_version_trimmed = python_version_str.trim();
|
||||
|
@ -168,7 +207,7 @@ fn run() -> Result<()> {
|
|||
}
|
||||
|
||||
// Set UV_PRERELEASE=allow if beta mode is enabled
|
||||
if prerelease_marker.exists() {
|
||||
if state.prerelease_marker.exists() {
|
||||
command.env("UV_PRERELEASE", "allow");
|
||||
}
|
||||
|
||||
|
@ -182,7 +221,7 @@ fn run() -> Result<()> {
|
|||
Err(e) => {
|
||||
// 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.
|
||||
let _ = remove_file(&uv_lock_path);
|
||||
let _ = remove_file(&state.uv_lock_path);
|
||||
println!("Install failed: {:#}", e);
|
||||
println!();
|
||||
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(())
|
||||
}
|
||||
|
||||
|
@ -403,3 +426,25 @@ fn parse_version_kind(version: &str) -> Option<VersionKind> {
|
|||
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
|
||||
// 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::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
@ -13,45 +14,7 @@ use anki_process::CommandExt as AnkiCommandExt;
|
|||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
|
||||
// Re-export Unix functions that macOS uses
|
||||
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,
|
||||
};
|
||||
|
||||
pub fn prepare_for_launch_after_update(mut cmd: Command) -> Result<()> {
|
||||
// Pre-validate by running --version to trigger any Gatekeeper checks
|
||||
print!("\n\x1B[1mThis may take a few minutes. Please wait\x1B[0m");
|
||||
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")
|
||||
.arg("--version")
|
||||
.stdout(std::process::Stdio::null())
|
||||
|
@ -81,22 +44,11 @@ pub fn handle_first_launch(anki_bin: &std::path::Path) -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_exe_and_resources_dirs() -> Result<(std::path::PathBuf, std::path::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 = 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"
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -1,18 +1,108 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// 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;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
mod mac;
|
||||
pub mod mac;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
mod windows;
|
||||
pub mod windows;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub use mac::*;
|
||||
#[cfg(all(unix, not(target_os = "macos")))]
|
||||
pub use unix::*;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use windows::*;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anki_process::CommandExt;
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
|
||||
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
|
||||
// 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 anki_process::CommandExt as AnkiCommandExt;
|
||||
use anyhow::Context;
|
||||
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<()> {
|
||||
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
|
||||
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
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
|
||||
use anki_process::CommandExt;
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use winapi::um::consoleapi;
|
||||
use winapi::um::errhandlingapi;
|
||||
use winapi::um::wincon;
|
||||
|
||||
use crate::Config;
|
||||
|
||||
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 {
|
||||
if !wincon::GetConsoleWindow().is_null() {
|
||||
return;
|
||||
// We already have a console, no need to spawn anki-console.exe
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if consoleapi::AllocConsole() == 0 {
|
||||
let error_code = errhandlingapi::GetLastError();
|
||||
eprintln!("unexpected AllocConsole error: {}", error_code);
|
||||
return;
|
||||
}
|
||||
|
||||
// This black magic triggers Windows to switch to the new
|
||||
// ANSI-supporting console host, which is usually only available
|
||||
// when the app is built with the console subsystem.
|
||||
let _ = Command::new("cmd").args(&["/C", ""]).status();
|
||||
}
|
||||
|
||||
if std::env::var("ANKI_IMPLICIT_CONSOLE").is_ok() && attach_to_parent_console() {
|
||||
// Successfully attached to parent console
|
||||
reconnect_stdio_to_console();
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// No console available, spawn anki-console.exe and exit
|
||||
let current_exe = std::env::current_exe().context("Failed to get current executable path")?;
|
||||
let exe_dir = current_exe
|
||||
.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 {
|
||||
if !wincon::GetConsoleWindow().is_null() {
|
||||
// we have a console already
|
||||
println!("attach: already had console, false");
|
||||
return false;
|
||||
}
|
||||
|
||||
if wincon::AttachConsole(wincon::ATTACH_PARENT_PROCESS) != 0 {
|
||||
// successfully attached to parent
|
||||
println!("attach: true");
|
||||
reconnect_stdio_to_console();
|
||||
true
|
||||
} else {
|
||||
println!("attach: false");
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// If parent process has a console (eg cmd.exe), redirect our output there.
|
||||
/// Sets config.show_console to true if successfully attached to console.
|
||||
pub fn initial_terminal_setup(config: &mut Config) {
|
||||
/// Reconnect stdin/stdout/stderr to the console.
|
||||
fn reconnect_stdio_to_console() {
|
||||
use std::ffi::CString;
|
||||
|
||||
use libc_stdhandle::*;
|
||||
|
||||
if !attach_to_parent_console() {
|
||||
return;
|
||||
}
|
||||
|
||||
// we launched without a console, so we'll need to open stdin/out/err
|
||||
let conin = CString::new("CONIN$").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(), 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
|
||||
|
||||
set CODESIGN=1
|
||||
REM set NO_COMPRESS=1
|
||||
if "%NOCOMP%"=="1" (
|
||||
set NO_COMPRESS=1
|
||||
set CODESIGN=0
|
||||
) else (
|
||||
set CODESIGN=1
|
||||
set NO_COMPRESS=0
|
||||
)
|
||||
cargo run --bin build_win
|
||||
|
|
|
@ -13,4 +13,9 @@ path = "main.rs"
|
|||
name = "anki-sync-server"
|
||||
|
||||
[dependencies]
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
anki = { workspace = true, features = ["native-tls"] }
|
||||
|
||||
[target.'cfg(not(windows))'.dependencies]
|
||||
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