mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 14:02:21 -04:00
Extend launcher to other platforms + more
- Switch to Qt 6.8 for repo default, as 6.7 depends on an older libwebp/tiff which is unavailable on newer installs - Drop tools/mac-x86, as we no longer need to test against Qt 5 - Add flags to cross compile wheels on Mac and Linux - Bump glibc target to 2_36, building on Debian Stable - Increase mpv timeout on macOS to allow for initial gatekeeper checks - Ship both arm64 and amd64 uv on Linux, with a bash stub to pick the appropriate arch.
This commit is contained in:
parent
cefb1a2997
commit
b32a972f1f
39 changed files with 2178 additions and 1123 deletions
|
@ -35,6 +35,9 @@
|
|||
},
|
||||
"rust-analyzer.cargo.buildScripts.enable": true,
|
||||
"python.analysis.typeCheckingMode": "off",
|
||||
"python.analysis.exclude": [
|
||||
"out/launcher/**"
|
||||
],
|
||||
"terminal.integrated.env.windows": {
|
||||
"PATH": "c:\\msys64\\usr\\bin;${env:Path}"
|
||||
}
|
||||
|
|
98
Cargo.lock
generated
98
Cargo.lock
generated
|
@ -1936,6 +1936,20 @@ version = "0.1.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f878075b9794c1e4ac788c95b728f26aa6366d32eeb10c7051389f898f7d067"
|
||||
|
||||
[[package]]
|
||||
name = "embed-resource"
|
||||
version = "2.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b68b6f9f63a0b6a38bc447d4ce84e2b388f3ec95c99c641c8ff0dd3ef89a6379"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"memchr",
|
||||
"rustc_version",
|
||||
"toml 0.8.21",
|
||||
"vswhom",
|
||||
"winreg 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "encoding_rs"
|
||||
version = "0.8.35"
|
||||
|
@ -3579,7 +3593,14 @@ dependencies = [
|
|||
name = "launcher"
|
||||
version = "1.0.0"
|
||||
dependencies = [
|
||||
"anki_io",
|
||||
"anki_process",
|
||||
"anyhow",
|
||||
"dirs 5.0.1",
|
||||
"embed-resource",
|
||||
"libc",
|
||||
"libc-stdhandle",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -3594,6 +3615,16 @@ version = "0.2.172"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
|
||||
|
||||
[[package]]
|
||||
name = "libc-stdhandle"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6dac2473dc28934c5e0b82250dab231c9d3b94160d91fe9ff483323b05797551"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libdbus-sys"
|
||||
version = "0.2.5"
|
||||
|
@ -3906,7 +3937,7 @@ dependencies = [
|
|||
"shlex",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"toml",
|
||||
"toml 0.5.11",
|
||||
"topological-sort",
|
||||
"walkdir",
|
||||
"warp",
|
||||
|
@ -5400,7 +5431,7 @@ dependencies = [
|
|||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"winreg",
|
||||
"winreg 0.50.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -5900,6 +5931,15 @@ dependencies = [
|
|||
"syn 2.0.101",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "0.6.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_tuple"
|
||||
version = "0.5.0"
|
||||
|
@ -6589,11 +6629,26 @@ dependencies = [
|
|||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.8.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "900f6c86a685850b1bc9f6223b20125115ee3f31e01207d81655bbcc0aea9231"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"toml_edit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "0.6.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
|
@ -6602,10 +6657,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "10558ed0bd2a1562e630926a2d1f0b98c827da99fabd3fe20920a59642504485"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"toml_write",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_write"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
|
||||
|
||||
[[package]]
|
||||
name = "topological-sort"
|
||||
version = "0.2.2"
|
||||
|
@ -7020,6 +7084,26 @@ version = "0.9.5"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "vswhom"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"vswhom-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vswhom-sys"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.5.0"
|
||||
|
@ -7878,6 +7962,16 @@ dependencies = [
|
|||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winreg"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wiremock"
|
||||
version = "0.6.3"
|
||||
|
|
|
@ -74,6 +74,7 @@ data-encoding = "2.6.0"
|
|||
difflib = "0.4.0"
|
||||
dirs = "5.0.1"
|
||||
dunce = "1.0.5"
|
||||
embed-resource = "2.4"
|
||||
envy = "0.4.2"
|
||||
flate2 = "1.0.34"
|
||||
fluent = "0.16.1"
|
||||
|
@ -92,6 +93,8 @@ intl-memoizer = "0.5.2"
|
|||
itertools = "0.13.0"
|
||||
junction = "1.2.0"
|
||||
lazy_static = "1.5.0"
|
||||
libc = "0.2"
|
||||
libc-stdhandle = "0.1"
|
||||
maplit = "1.0.2"
|
||||
nom = "7.1.3"
|
||||
num-format = "0.4.4"
|
||||
|
@ -141,6 +144,7 @@ unic-ucd-category = "0.9.0"
|
|||
unicode-normalization = "0.1.24"
|
||||
walkdir = "2.5.0"
|
||||
which = "5.0.0"
|
||||
winapi = { version = "0.3", features = ["wincon"] }
|
||||
wiremock = "0.6.2"
|
||||
xz2 = "0.1.7"
|
||||
zip = { version = "0.6.6", default-features = false, features = ["deflate", "time"] }
|
||||
|
|
|
@ -1,36 +1,20 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
#![allow(dead_code)]
|
||||
#![allow(unused_imports)]
|
||||
|
||||
use std::env;
|
||||
|
||||
use anyhow::Result;
|
||||
use ninja_gen::action::BuildAction;
|
||||
use ninja_gen::archives::download_and_extract;
|
||||
use ninja_gen::archives::empty_manifest;
|
||||
use ninja_gen::archives::with_exe;
|
||||
use ninja_gen::archives::OnlineArchive;
|
||||
use ninja_gen::archives::Platform;
|
||||
use ninja_gen::build::BuildProfile;
|
||||
use ninja_gen::cargo::CargoBuild;
|
||||
use ninja_gen::cargo::RustOutput;
|
||||
use ninja_gen::command::RunCommand;
|
||||
use ninja_gen::git::SyncSubmodule;
|
||||
use ninja_gen::glob;
|
||||
use ninja_gen::hashmap;
|
||||
use ninja_gen::input::BuildInput;
|
||||
use ninja_gen::inputs;
|
||||
use ninja_gen::python::PythonEnvironment;
|
||||
use ninja_gen::Build;
|
||||
use ninja_gen::Utf8Path;
|
||||
|
||||
use crate::anki_version;
|
||||
use crate::platform::overriden_python_target_platform;
|
||||
use crate::platform::overriden_rust_target_triple;
|
||||
|
||||
pub fn setup_uv_universal(build: &mut Build) -> Result<()> {
|
||||
if !cfg!(target_arch = "aarch64") {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
build.add_action(
|
||||
"launcher:uv_universal",
|
||||
RunCommand {
|
||||
|
|
|
@ -20,7 +20,7 @@ use ninja_gen::protobuf::check_proto;
|
|||
use ninja_gen::protobuf::setup_protoc;
|
||||
use ninja_gen::python::setup_uv;
|
||||
use ninja_gen::Build;
|
||||
use platform::overriden_python_target_platform;
|
||||
use platform::overriden_python_venv_platform;
|
||||
use pylib::build_pylib;
|
||||
use pylib::check_pylib;
|
||||
use python::check_python;
|
||||
|
@ -50,7 +50,7 @@ fn main() -> Result<()> {
|
|||
if env::var("OFFLINE_BUILD").is_err() {
|
||||
setup_uv(
|
||||
build,
|
||||
overriden_python_target_platform().unwrap_or(build.host_platform),
|
||||
overriden_python_venv_platform().unwrap_or(build.host_platform),
|
||||
)?;
|
||||
}
|
||||
setup_venv(build)?;
|
||||
|
|
|
@ -7,18 +7,28 @@ use ninja_gen::archives::Platform;
|
|||
|
||||
/// Please see [`overriden_python_target_platform()`] for details.
|
||||
pub fn overriden_rust_target_triple() -> Option<&'static str> {
|
||||
overriden_python_target_platform().map(|p| p.as_rust_triple())
|
||||
overriden_python_wheel_platform().map(|p| p.as_rust_triple())
|
||||
}
|
||||
|
||||
/// Usually None to use the host architecture, except on Windows which
|
||||
/// always uses x86_64.
|
||||
/// On a Mac, set MAC_X86 to build for x86_64 on Apple Silicon.
|
||||
pub fn overriden_python_target_platform() -> Option<Platform> {
|
||||
/// always uses x86_64, since WebEngine is unavailable for ARM64.
|
||||
pub fn overriden_python_venv_platform() -> Option<Platform> {
|
||||
if cfg!(target_os = "windows") {
|
||||
Some(Platform::WindowsX64)
|
||||
} else if env::var("MAC_X86").is_ok() {
|
||||
Some(Platform::MacX64)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Like [`overriden_python_venv_platform`], but:
|
||||
/// If MAC_X86 is set, an X86 wheel will be built on macOS ARM.
|
||||
/// If LIN_ARM64 is set, an ARM64 wheel will be built on Linux AMD64.
|
||||
pub fn overriden_python_wheel_platform() -> Option<Platform> {
|
||||
if env::var("MAC_X86").is_ok() {
|
||||
Some(Platform::MacX64)
|
||||
} else if env::var("LIN_ARM64").is_ok() {
|
||||
Some(Platform::LinuxArm)
|
||||
} else {
|
||||
overriden_python_venv_platform()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ use ninja_gen::python::PythonTest;
|
|||
use ninja_gen::Build;
|
||||
|
||||
use crate::anki_version;
|
||||
use crate::platform::overriden_python_target_platform;
|
||||
use crate::platform::overriden_python_wheel_platform;
|
||||
use crate::python::BuildWheel;
|
||||
use crate::python::GenPythonProto;
|
||||
|
||||
|
@ -64,7 +64,7 @@ pub fn build_pylib(build: &mut Build) -> Result<()> {
|
|||
BuildWheel {
|
||||
name: "anki",
|
||||
version: anki_version(),
|
||||
platform: overriden_python_target_platform().or(Some(build.host_platform)),
|
||||
platform: overriden_python_wheel_platform().or(Some(build.host_platform)),
|
||||
deps: inputs![
|
||||
":pylib:anki",
|
||||
glob!("pylib/anki/**"),
|
||||
|
|
|
@ -113,8 +113,8 @@ impl BuildAction for BuildWheel {
|
|||
// Calculate the wheel filename that uv will generate
|
||||
let tag = if let Some(platform) = self.platform {
|
||||
let platform_tag = match platform {
|
||||
Platform::LinuxX64 => "manylinux_2_35_x86_64",
|
||||
Platform::LinuxArm => "manylinux_2_35_aarch64",
|
||||
Platform::LinuxX64 => "manylinux_2_36_x86_64",
|
||||
Platform::LinuxArm => "manylinux_2_36_aarch64",
|
||||
Platform::MacX64 => "macosx_12_0_x86_64",
|
||||
Platform::MacArm => "macosx_12_0_arm64",
|
||||
Platform::WindowsX64 => "win_amd64",
|
||||
|
|
|
@ -87,14 +87,27 @@ pub fn setup_uv(build: &mut Build, platform: Platform) -> Result<()> {
|
|||
build.add_dependency("uv_binary", uv_binary);
|
||||
|
||||
// Our macOS packaging needs access to the x86 binary on ARM.
|
||||
download_and_extract(
|
||||
build,
|
||||
"uv_mac_x86",
|
||||
uv_archive(Platform::MacX64),
|
||||
hashmap! { "bin" => [
|
||||
with_exe("uv")
|
||||
] },
|
||||
)?;
|
||||
if cfg!(target_arch = "aarch64") {
|
||||
download_and_extract(
|
||||
build,
|
||||
"uv_mac_x86",
|
||||
uv_archive(Platform::MacX64),
|
||||
hashmap! { "bin" => [
|
||||
with_exe("uv")
|
||||
] },
|
||||
)?;
|
||||
}
|
||||
// Our Linux packaging needs access to the ARM binary on x86
|
||||
if cfg!(target_arch = "x86_64") {
|
||||
download_and_extract(
|
||||
build,
|
||||
"uv_lin_arm",
|
||||
uv_archive(Platform::LinuxArm),
|
||||
hashmap! { "bin" => [
|
||||
with_exe("uv")
|
||||
] },
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -3905,6 +3905,15 @@
|
|||
"license_file": null,
|
||||
"description": "Derive Serialize and Deserialize that delegates to the underlying repr of a C-like enum."
|
||||
},
|
||||
{
|
||||
"name": "serde_spanned",
|
||||
"version": "0.6.9",
|
||||
"authors": null,
|
||||
"repository": "https://github.com/toml-rs/toml",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"license_file": null,
|
||||
"description": "Serde-compatible spanned Value"
|
||||
},
|
||||
{
|
||||
"name": "serde_tuple",
|
||||
"version": "0.5.0",
|
||||
|
@ -4454,6 +4463,15 @@
|
|||
"license_file": null,
|
||||
"description": "Yet another format-preserving TOML parser."
|
||||
},
|
||||
{
|
||||
"name": "toml_write",
|
||||
"version": "0.1.2",
|
||||
"authors": null,
|
||||
"repository": "https://github.com/toml-rs/toml",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"license_file": null,
|
||||
"description": "A low-level interface for writing out TOML"
|
||||
},
|
||||
{
|
||||
"name": "tower",
|
||||
"version": "0.5.2",
|
||||
|
|
2
ninja
2
ninja
|
@ -8,7 +8,7 @@ else
|
|||
out="$BUILD_ROOT"
|
||||
fi
|
||||
export CARGO_TARGET_DIR=$out/rust
|
||||
export RECONFIGURE_KEY="${MAC_X86};${SOURCEMAP};${HMR}"
|
||||
export RECONFIGURE_KEY="${MAC_X86};${LIN_ARM64};${SOURCEMAP};${HMR}"
|
||||
|
||||
if [ "$SKIP_RUNNER_BUILD" = "1" ]; then
|
||||
echo "Runner not rebuilt."
|
||||
|
|
|
@ -177,7 +177,8 @@ class MPVBase:
|
|||
startup.
|
||||
"""
|
||||
start = time.time()
|
||||
while self.is_running() and time.time() < start + 10:
|
||||
timeout = 60 if is_mac else 10
|
||||
while self.is_running() and time.time() < start + timeout:
|
||||
time.sleep(0.1)
|
||||
if is_win:
|
||||
# named pipe
|
||||
|
|
1
qt/launcher/.gitignore
vendored
1
qt/launcher/.gitignore
vendored
|
@ -1 +0,0 @@
|
|||
release/pyproject.toml
|
|
@ -1,7 +1,26 @@
|
|||
[package]
|
||||
name = "launcher"
|
||||
version = "1.0.0"
|
||||
edition = "2024"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
publish = false
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
dirs = "5.0"
|
||||
anki_io.workspace = true
|
||||
anki_process.workspace = true
|
||||
anyhow.workspace = true
|
||||
dirs.workspace = true
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
winapi.workspace = true
|
||||
libc.workspace = true
|
||||
libc-stdhandle.workspace = true
|
||||
|
||||
[[bin]]
|
||||
name = "build_win"
|
||||
path = "src/bin/build_win.rs"
|
||||
|
||||
[target.'cfg(windows)'.build-dependencies]
|
||||
embed-resource.workspace = true
|
||||
|
|
8
qt/launcher/build.rs
Normal file
8
qt/launcher/build.rs
Normal file
|
@ -0,0 +1,8 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
fn main() {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
embed_resource::compile("win/anki-manifest.rc", embed_resource::NONE);
|
||||
}
|
||||
}
|
30
qt/launcher/lin/anki
Normal file
30
qt/launcher/lin/anki
Normal file
|
@ -0,0 +1,30 @@
|
|||
#!/bin/bash
|
||||
# Universal Anki launcher script
|
||||
|
||||
# Get the directory where this script is located
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# Determine architecture
|
||||
ARCH=$(uname -m)
|
||||
case "$ARCH" in
|
||||
x86_64|amd64)
|
||||
LAUNCHER="$SCRIPT_DIR/launcher.amd64"
|
||||
;;
|
||||
aarch64|arm64)
|
||||
LAUNCHER="$SCRIPT_DIR/launcher.arm64"
|
||||
;;
|
||||
*)
|
||||
echo "Error: Unsupported architecture: $ARCH"
|
||||
echo "Supported architectures: x86_64, aarch64"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Check if launcher exists
|
||||
if [ ! -f "$LAUNCHER" ]; then
|
||||
echo "Error: Launcher not found: $LAUNCHER"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Execute the appropriate launcher with all arguments
|
||||
exec "$LAUNCHER" "$@"
|
66
qt/launcher/lin/build.sh
Executable file
66
qt/launcher/lin/build.sh
Executable file
|
@ -0,0 +1,66 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
# Add Linux cross-compilation target
|
||||
rustup target add aarch64-unknown-linux-gnu
|
||||
|
||||
# Define output paths
|
||||
OUTPUT_DIR="../../../out/launcher"
|
||||
LAUNCHER_DIR="$OUTPUT_DIR/anki-launcher"
|
||||
|
||||
# Clean existing output directory
|
||||
rm -rf "$LAUNCHER_DIR"
|
||||
|
||||
# Build binaries for both Linux architectures
|
||||
cargo build -p launcher --release --target x86_64-unknown-linux-gnu
|
||||
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc \
|
||||
cargo build -p launcher --release --target aarch64-unknown-linux-gnu
|
||||
(cd ../../.. && ./ninja extract:uv_lin_arm)
|
||||
|
||||
# Create output directory
|
||||
mkdir -p "$LAUNCHER_DIR"
|
||||
|
||||
# Copy binaries and support files
|
||||
TARGET_DIR=${CARGO_TARGET_DIR:-../../../target}
|
||||
|
||||
# Copy launcher binaries with architecture suffixes
|
||||
cp "$TARGET_DIR/x86_64-unknown-linux-gnu/release/launcher" "$LAUNCHER_DIR/launcher.amd64"
|
||||
cp "$TARGET_DIR/aarch64-unknown-linux-gnu/release/launcher" "$LAUNCHER_DIR/launcher.arm64"
|
||||
|
||||
# Copy uv binaries with architecture suffixes
|
||||
cp "../../../out/extracted/uv/uv" "$LAUNCHER_DIR/uv.amd64"
|
||||
cp "../../../out/extracted/uv_lin_arm/uv" "$LAUNCHER_DIR/uv.arm64"
|
||||
|
||||
# Copy support files from lin directory
|
||||
for file in README.md anki.1 anki.desktop anki.png anki.xml anki.xpm install.sh uninstall.sh anki; do
|
||||
cp "$file" "$LAUNCHER_DIR/"
|
||||
done
|
||||
|
||||
# Copy additional files from parent directory
|
||||
cp ../pyproject.toml "$LAUNCHER_DIR/"
|
||||
cp ../../../.python-version "$LAUNCHER_DIR/"
|
||||
|
||||
# Set executable permissions
|
||||
chmod +x \
|
||||
"$LAUNCHER_DIR/anki" \
|
||||
"$LAUNCHER_DIR/launcher.amd64" \
|
||||
"$LAUNCHER_DIR/launcher.arm64" \
|
||||
"$LAUNCHER_DIR/uv.amd64" \
|
||||
"$LAUNCHER_DIR/uv.arm64" \
|
||||
"$LAUNCHER_DIR/install.sh" \
|
||||
"$LAUNCHER_DIR/uninstall.sh"
|
||||
|
||||
# Set proper permissions and create tarball
|
||||
chmod -R a+r "$LAUNCHER_DIR"
|
||||
|
||||
# Create tarball using the same options as the Rust template
|
||||
ZSTD="zstd -c --long -T0 -18"
|
||||
TRANSFORM="s%^.%anki-launcher%S"
|
||||
TARBALL="$OUTPUT_DIR/anki-launcher.tar.zst"
|
||||
|
||||
tar -I "$ZSTD" --transform "$TRANSFORM" -cf "$TARBALL" -C "$LAUNCHER_DIR" .
|
||||
|
||||
echo "Build complete:"
|
||||
echo "Universal launcher: $LAUNCHER_DIR"
|
||||
echo "Tarball: $TARBALL"
|
|
@ -13,7 +13,7 @@ fi
|
|||
|
||||
rm -rf "$PREFIX"/share/anki "$PREFIX"/bin/anki
|
||||
mkdir -p "$PREFIX"/share/anki
|
||||
cp -av --no-preserve=owner,context -- * "$PREFIX"/share/anki/
|
||||
cp -av --no-preserve=owner,context -- * .python-version "$PREFIX"/share/anki/
|
||||
mkdir -p "$PREFIX"/bin
|
||||
ln -sf "$PREFIX"/share/anki/anki "$PREFIX"/bin/anki
|
||||
# fix a previous packaging issue where we created this as a file
|
||||
|
|
81
qt/launcher/release/pyproject.toml
Normal file
81
qt/launcher/release/pyproject.toml
Normal file
|
@ -0,0 +1,81 @@
|
|||
[project]
|
||||
name = "anki-release"
|
||||
version = "0.1.2"
|
||||
description = "A package to lock Anki's dependencies"
|
||||
requires-python = ">=3.9"
|
||||
dependencies = [
|
||||
"anki==0.1.2",
|
||||
"aqt==0.1.2",
|
||||
"anki-audio==0.1.0 ; sys_platform == 'darwin' or sys_platform == 'win32'",
|
||||
"attrs==25.3.0",
|
||||
"beautifulsoup4==4.12.3",
|
||||
"blinker==1.9.0",
|
||||
"certifi==2025.4.26",
|
||||
"charset-normalizer==3.4.2",
|
||||
"click==8.1.8 ; python_full_version < '3.10'",
|
||||
"click==8.2.1 ; python_full_version >= '3.10'",
|
||||
"colorama==0.4.6 ; sys_platform == 'win32'",
|
||||
"decorator==5.2.1",
|
||||
"distro==1.9.0 ; sys_platform != 'darwin' and sys_platform != 'win32'",
|
||||
"flask==3.1.1",
|
||||
"flask-cors==6.0.0",
|
||||
"idna==3.10",
|
||||
"importlib-metadata==8.7.0 ; python_full_version < '3.10'",
|
||||
"itsdangerous==2.2.0",
|
||||
"jinja2==3.1.6",
|
||||
"jsonschema==4.24.0",
|
||||
"jsonschema-specifications==2025.4.1",
|
||||
"markdown==3.8",
|
||||
"markupsafe==3.0.2",
|
||||
"mock==5.2.0",
|
||||
"orjson==3.10.18",
|
||||
"pip-system-certs==4.0",
|
||||
"protobuf==6.31.1",
|
||||
"psutil==7.0.0 ; sys_platform == 'win32'",
|
||||
"pyqt6==6.7.1",
|
||||
"pyqt6-qt6==6.7.3",
|
||||
"pyqt6-sip==13.10.2",
|
||||
"pyqt6-webengine==6.7.0",
|
||||
"pyqt6-webengine-qt6==6.7.3",
|
||||
"pyqt6-webenginesubwheel-qt6==6.7.3",
|
||||
"pysocks==1.7.1",
|
||||
"pywin32==310 ; sys_platform == 'win32'",
|
||||
"referencing==0.36.2",
|
||||
"requests==2.32.3",
|
||||
"rpds-py==0.25.1",
|
||||
"send2trash==1.8.3",
|
||||
"soupsieve==2.7",
|
||||
"types-click==7.1.8",
|
||||
"types-decorator==5.2.0.20250324",
|
||||
"types-flask==1.1.6",
|
||||
"types-flask-cors==6.0.0.20250520",
|
||||
"types-jinja2==2.11.9",
|
||||
"types-markdown==3.8.0.20250415",
|
||||
"types-markupsafe==1.1.10",
|
||||
"types-orjson==3.6.2",
|
||||
"types-protobuf==6.30.2.20250516",
|
||||
"types-pywin32==310.0.0.20250516",
|
||||
"types-requests==2.32.0.20250602",
|
||||
"types-waitress==3.0.1.20241117",
|
||||
"types-werkzeug==1.0.9",
|
||||
"typing-extensions==4.14.0",
|
||||
"urllib3==2.4.0",
|
||||
"waitress==3.0.2",
|
||||
"werkzeug==3.1.3",
|
||||
"wrapt==1.17.2",
|
||||
"zipp==3.23.0 ; python_full_version < '3.10'",
|
||||
]
|
||||
|
||||
[[tool.uv.index]]
|
||||
name = "testpypi"
|
||||
url = "https://test.pypi.org/simple/"
|
||||
publish-url = "https://test.pypi.org/legacy/"
|
||||
explicit = true
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
# hatch throws an error if nothing is included
|
||||
[tool.hatch.build.targets.wheel]
|
||||
include = ["no-such-file"]
|
295
qt/launcher/src/bin/build_win.rs
Normal file
295
qt/launcher/src/bin/build_win.rs
Normal file
|
@ -0,0 +1,295 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
use std::env;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
|
||||
use anki_io::copy_file;
|
||||
use anki_io::create_dir_all;
|
||||
use anki_io::remove_dir_all;
|
||||
use anki_io::write_file;
|
||||
use anki_process::CommandExt;
|
||||
use anyhow::Result;
|
||||
|
||||
const OUTPUT_DIR: &str = "../../../out/launcher";
|
||||
const LAUNCHER_EXE_DIR: &str = "../../../out/launcher_exe";
|
||||
const NSIS_DIR: &str = "../../../out/nsis";
|
||||
const CARGO_TARGET_DIR: &str = "../../../out/rust";
|
||||
const NSIS_PATH: &str = "C:\\Program Files (x86)\\NSIS\\makensis.exe";
|
||||
|
||||
fn main() -> Result<()> {
|
||||
println!("Building Windows launcher...");
|
||||
|
||||
let output_dir = PathBuf::from(OUTPUT_DIR);
|
||||
let launcher_exe_dir = PathBuf::from(LAUNCHER_EXE_DIR);
|
||||
let nsis_dir = PathBuf::from(NSIS_DIR);
|
||||
|
||||
setup_directories(&output_dir, &launcher_exe_dir, &nsis_dir)?;
|
||||
build_launcher_binary()?;
|
||||
extract_nsis_plugins()?;
|
||||
copy_files(&output_dir)?;
|
||||
sign_binaries(&output_dir)?;
|
||||
copy_nsis_files(&nsis_dir)?;
|
||||
build_uninstaller(&output_dir, &nsis_dir)?;
|
||||
sign_file(&output_dir.join("uninstall.exe"))?;
|
||||
generate_install_manifest(&output_dir)?;
|
||||
build_installer(&output_dir, &nsis_dir)?;
|
||||
sign_file(&PathBuf::from("../../../out/launcher_exe/anki-install.exe"))?;
|
||||
|
||||
println!("Build completed successfully!");
|
||||
println!("Output directory: {}", output_dir.display());
|
||||
println!("Installer: ../../../out/launcher_exe/anki-install.exe");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn setup_directories(output_dir: &Path, launcher_exe_dir: &Path, nsis_dir: &Path) -> Result<()> {
|
||||
println!("Setting up directories...");
|
||||
|
||||
// Remove existing output directories
|
||||
if output_dir.exists() {
|
||||
remove_dir_all(output_dir)?;
|
||||
}
|
||||
if launcher_exe_dir.exists() {
|
||||
remove_dir_all(launcher_exe_dir)?;
|
||||
}
|
||||
if nsis_dir.exists() {
|
||||
remove_dir_all(nsis_dir)?;
|
||||
}
|
||||
|
||||
// Create output directories
|
||||
create_dir_all(output_dir)?;
|
||||
create_dir_all(launcher_exe_dir)?;
|
||||
create_dir_all(nsis_dir)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_launcher_binary() -> Result<()> {
|
||||
println!("Building launcher binary...");
|
||||
|
||||
env::set_var("CARGO_TARGET_DIR", CARGO_TARGET_DIR);
|
||||
|
||||
Command::new("cargo")
|
||||
.args([
|
||||
"build",
|
||||
"-p",
|
||||
"launcher",
|
||||
"--release",
|
||||
"--target",
|
||||
"x86_64-pc-windows-msvc",
|
||||
])
|
||||
.ensure_success()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn extract_nsis_plugins() -> Result<()> {
|
||||
println!("Extracting NSIS plugins...");
|
||||
|
||||
// Change to the anki root directory and run tools/ninja.bat
|
||||
Command::new("cmd")
|
||||
.args([
|
||||
"/c",
|
||||
"cd",
|
||||
"/d",
|
||||
"..\\..\\..\\",
|
||||
"&&",
|
||||
"tools\\ninja.bat",
|
||||
"extract:nsis_plugins",
|
||||
])
|
||||
.ensure_success()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn copy_files(output_dir: &Path) -> Result<()> {
|
||||
println!("Copying binaries...");
|
||||
|
||||
// Copy launcher binary as anki.exe
|
||||
let launcher_src =
|
||||
PathBuf::from(CARGO_TARGET_DIR).join("x86_64-pc-windows-msvc/release/launcher.exe");
|
||||
let launcher_dst = output_dir.join("anki.exe");
|
||||
copy_file(&launcher_src, &launcher_dst)?;
|
||||
|
||||
// Copy uv.exe
|
||||
let uv_src = PathBuf::from("../../../out/extracted/uv/uv.exe");
|
||||
let uv_dst = output_dir.join("uv.exe");
|
||||
copy_file(&uv_src, &uv_dst)?;
|
||||
|
||||
println!("Copying support files...");
|
||||
|
||||
// Copy pyproject.toml
|
||||
copy_file("../pyproject.toml", output_dir.join("pyproject.toml"))?;
|
||||
|
||||
// Copy .python-version
|
||||
copy_file(
|
||||
"../../../.python-version",
|
||||
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("uv.exe"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn sign_file(file_path: &Path) -> Result<()> {
|
||||
let codesign = env::var("CODESIGN").unwrap_or_default();
|
||||
if codesign != "1" {
|
||||
println!(
|
||||
"Skipping code signing for {} (CODESIGN not set to 1)",
|
||||
file_path.display()
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let signtool_path = find_signtool()?;
|
||||
println!("Signing {}...", file_path.display());
|
||||
|
||||
Command::new(&signtool_path)
|
||||
.args([
|
||||
"sign",
|
||||
"/sha1",
|
||||
"dccfc6d312fc0432197bb7be951478e5866eebf8",
|
||||
"/fd",
|
||||
"sha256",
|
||||
"/tr",
|
||||
"http://time.certum.pl",
|
||||
"/td",
|
||||
"sha256",
|
||||
"/v",
|
||||
])
|
||||
.arg(file_path)
|
||||
.ensure_success()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn find_signtool() -> Result<PathBuf> {
|
||||
println!("Locating signtool.exe...");
|
||||
|
||||
let output = Command::new("where")
|
||||
.args([
|
||||
"/r",
|
||||
"C:\\Program Files (x86)\\Windows Kits",
|
||||
"signtool.exe",
|
||||
])
|
||||
.utf8_output()?;
|
||||
|
||||
// Find signtool.exe with "arm64" in the path (as per original batch logic)
|
||||
for line in output.stdout.lines() {
|
||||
if line.contains("\\arm64\\") {
|
||||
let signtool_path = PathBuf::from(line.trim());
|
||||
println!("Using signtool: {}", signtool_path.display());
|
||||
return Ok(signtool_path);
|
||||
}
|
||||
}
|
||||
|
||||
anyhow::bail!("Could not find signtool.exe with arm64 architecture");
|
||||
}
|
||||
|
||||
fn generate_install_manifest(output_dir: &Path) -> Result<()> {
|
||||
println!("Generating install manifest...");
|
||||
|
||||
let mut manifest_content = String::new();
|
||||
let entries = anki_io::read_dir_files(output_dir)?;
|
||||
|
||||
for entry in entries {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
if let Some(file_name) = path.file_name() {
|
||||
let file_name_str = file_name.to_string_lossy();
|
||||
// Skip manifest file and uninstaller (can't delete itself)
|
||||
if file_name_str != "anki.install-manifest" && file_name_str != "uninstall.exe" {
|
||||
if let Ok(relative_path) = path.strip_prefix(output_dir) {
|
||||
// Convert to Windows-style backslashes for NSIS
|
||||
let windows_path = relative_path.display().to_string().replace('/', "\\");
|
||||
// Use Windows line endings (\r\n) as expected by NSIS
|
||||
manifest_content.push_str(&format!("{}\r\n", windows_path));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
write_file(output_dir.join("anki.install-manifest"), manifest_content)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn copy_nsis_files(nsis_dir: &Path) -> Result<()> {
|
||||
println!("Copying NSIS support files...");
|
||||
|
||||
// Copy anki.template.nsi as anki.nsi
|
||||
copy_file("anki.template.nsi", nsis_dir.join("anki.nsi"))?;
|
||||
|
||||
// Copy fileassoc.nsh
|
||||
copy_file("fileassoc.nsh", nsis_dir.join("fileassoc.nsh"))?;
|
||||
|
||||
// Copy nsProcess.dll
|
||||
copy_file(
|
||||
"../../../out/extracted/nsis_plugins/nsProcess.dll",
|
||||
nsis_dir.join("nsProcess.dll"),
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_uninstaller(output_dir: &Path, nsis_dir: &Path) -> Result<()> {
|
||||
println!("Building uninstaller...");
|
||||
|
||||
let mut flags = vec!["-V3", "-DWRITE_UNINSTALLER"];
|
||||
if env::var("NO_COMPRESS").unwrap_or_default() == "1" {
|
||||
println!("NO_COMPRESS=1 detected, disabling compression");
|
||||
flags.push("-DNO_COMPRESS");
|
||||
}
|
||||
|
||||
run_nsis(
|
||||
&PathBuf::from("anki.nsi"),
|
||||
&flags,
|
||||
nsis_dir, // Run from nsis directory
|
||||
)?;
|
||||
|
||||
// Copy uninstaller from nsis directory to output directory
|
||||
copy_file(
|
||||
nsis_dir.join("uninstall.exe"),
|
||||
output_dir.join("uninstall.exe"),
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_installer(_output_dir: &Path, nsis_dir: &Path) -> Result<()> {
|
||||
println!("Building installer...");
|
||||
|
||||
let mut flags = vec!["-V3"];
|
||||
if env::var("NO_COMPRESS").unwrap_or_default() == "1" {
|
||||
println!("NO_COMPRESS=1 detected, disabling compression");
|
||||
flags.push("-DNO_COMPRESS");
|
||||
}
|
||||
|
||||
run_nsis(
|
||||
&PathBuf::from("anki.nsi"),
|
||||
&flags,
|
||||
nsis_dir, // Run from nsis directory
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_nsis(script_path: &Path, flags: &[&str], working_dir: &Path) -> Result<()> {
|
||||
let mut cmd = Command::new(NSIS_PATH);
|
||||
cmd.args(flags).arg(script_path).current_dir(working_dir);
|
||||
|
||||
cmd.ensure_success()?;
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -1,114 +1,117 @@
|
|||
use std::os::unix::process::CommandExt;
|
||||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
#![windows_subsystem = "windows"]
|
||||
|
||||
use std::io::stdin;
|
||||
use std::process::Command;
|
||||
use std::process::Stdio;
|
||||
|
||||
use anki_io::copy_file;
|
||||
use anki_io::create_dir_all;
|
||||
use anki_io::metadata;
|
||||
use anki_io::remove_file;
|
||||
use anki_io::write_file;
|
||||
use anki_process::CommandExt;
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
|
||||
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::handle_terminal_launch;
|
||||
use crate::platform::initial_terminal_setup;
|
||||
use crate::platform::launch_anki_detached;
|
||||
|
||||
mod platform;
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Config {
|
||||
pub show_console: bool,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let Some(uv_install_root) =
|
||||
dirs::data_local_dir().map(|data_dir| data_dir.join("AnkiProgramFiles"))
|
||||
else {
|
||||
println!("Unable to determine data_dir");
|
||||
if let Err(e) = run() {
|
||||
eprintln!("Error: {:#}", e);
|
||||
eprintln!("Press enter to close...");
|
||||
let mut input = String::new();
|
||||
let _ = stdin().read_line(&mut input);
|
||||
|
||||
std::process::exit(1);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn run() -> Result<()> {
|
||||
let mut config = Config::default();
|
||||
initial_terminal_setup(&mut config);
|
||||
|
||||
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 exe_dir = std::env::current_exe()
|
||||
.unwrap()
|
||||
.parent()
|
||||
.unwrap()
|
||||
.to_owned();
|
||||
let resources_dir = exe_dir.parent().unwrap().join("Resources");
|
||||
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 pyproject_has_changed =
|
||||
!user_pyproject_path.exists() || !sync_complete_marker.exists() || {
|
||||
let pyproject_toml_time = std::fs::metadata(&user_pyproject_path)
|
||||
.unwrap()
|
||||
let pyproject_toml_time = metadata(&user_pyproject_path)?
|
||||
.modified()
|
||||
.unwrap();
|
||||
let sync_complete_time = std::fs::metadata(&sync_complete_marker)
|
||||
.unwrap()
|
||||
.context("Failed to get pyproject.toml modified time")?;
|
||||
let sync_complete_time = metadata(&sync_complete_marker)?
|
||||
.modified()
|
||||
.unwrap();
|
||||
pyproject_toml_time > sync_complete_time
|
||||
};
|
||||
.context("Failed to get sync marker modified time")?;
|
||||
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
|
||||
let anki_bin = get_anki_binary_path(&uv_install_root);
|
||||
exec_anki(&anki_bin, &config)?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// we'll need to launch uv; reinvoke ourselves in a terminal so the user can see
|
||||
if pyproject_has_changed {
|
||||
let stdout_is_terminal = std::io::IsTerminal::is_terminal(&std::io::stdout());
|
||||
if stdout_is_terminal {
|
||||
print!("\x1B[2J\x1B[H"); // Clear screen and move cursor to top
|
||||
println!("\x1B[1mPreparing to start Anki...\x1B[0m\n");
|
||||
} else {
|
||||
// If launched from GUI, relaunch in Terminal.app
|
||||
let current_exe = std::env::current_exe().unwrap();
|
||||
Command::new("open")
|
||||
.args(["-a", "Terminal"])
|
||||
.arg(current_exe)
|
||||
.spawn()
|
||||
.unwrap();
|
||||
std::process::exit(0);
|
||||
}
|
||||
handle_terminal_launch()?;
|
||||
|
||||
// Create install directory and copy project files in
|
||||
create_dir_all(&uv_install_root)?;
|
||||
if !user_pyproject_path.exists() {
|
||||
copy_file(&dist_pyproject_path, &user_pyproject_path)?;
|
||||
copy_file(&dist_python_version_path, &user_python_version_path)?;
|
||||
}
|
||||
|
||||
if pyproject_has_changed {
|
||||
let uv_path: std::path::PathBuf = exe_dir.join("uv");
|
||||
// Remove sync marker before attempting sync
|
||||
let _ = remove_file(&sync_complete_marker);
|
||||
|
||||
// Create install directory and copy pyproject.toml in if missing
|
||||
std::fs::create_dir_all(&uv_install_root).unwrap();
|
||||
if !user_pyproject_path.exists() {
|
||||
std::fs::copy(&dist_pyproject_path, &user_pyproject_path).unwrap();
|
||||
std::fs::copy(&dist_python_version_path, &user_python_version_path).unwrap();
|
||||
}
|
||||
|
||||
// Remove sync marker before attempting sync
|
||||
let _ = std::fs::remove_file(&sync_complete_marker);
|
||||
|
||||
// Sync the venv
|
||||
let sync_result = Command::new(&uv_path)
|
||||
.current_dir(&uv_install_root)
|
||||
.args(["sync"])
|
||||
.status()
|
||||
.unwrap();
|
||||
|
||||
if !sync_result.success() {
|
||||
println!("uv sync failed");
|
||||
println!("Press enter to close");
|
||||
let mut input = String::new();
|
||||
let _ = std::io::stdin().read_line(&mut input);
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
// Write marker file to indicate successful sync
|
||||
std::fs::write(&sync_complete_marker, "").unwrap();
|
||||
// Sync the venv
|
||||
if let Err(e) = Command::new(&uv_path)
|
||||
.current_dir(&uv_install_root)
|
||||
.args(["sync", "--refresh"])
|
||||
.ensure_success()
|
||||
{
|
||||
// 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);
|
||||
return Err(e.into());
|
||||
}
|
||||
|
||||
// invoke anki from the synced venv
|
||||
if pyproject_has_changed {
|
||||
// Pre-validate by running --version to trigger any Gatekeeper checks
|
||||
let anki_bin = uv_install_root.join(".venv/bin/anki");
|
||||
println!("\n\x1B[1mThis may take a few minutes. Please wait...\x1B[0m");
|
||||
let _ = Command::new(&anki_bin)
|
||||
.env("ANKI_FIRST_RUN", "1")
|
||||
.arg("--version")
|
||||
.status();
|
||||
// Write marker file to indicate successful sync
|
||||
write_file(&sync_complete_marker, "")?;
|
||||
|
||||
// Then launch the binary as detached subprocess so the terminal can close
|
||||
let child = Command::new(&anki_bin)
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.process_group(0)
|
||||
.spawn()
|
||||
.unwrap();
|
||||
std::mem::forget(child);
|
||||
} else {
|
||||
// If venv already existed, exec as normal
|
||||
println!(
|
||||
"Anki return code: {:?}",
|
||||
Command::new(uv_install_root.join(".venv/bin/anki")).exec()
|
||||
);
|
||||
}
|
||||
// First launch
|
||||
let anki_bin = get_anki_binary_path(&uv_install_root);
|
||||
handle_first_launch(&anki_bin)?;
|
||||
|
||||
// Then launch the binary as detached subprocess so the terminal can close
|
||||
launch_anki_detached(&anki_bin, &config)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
80
qt/launcher/src/platform/mac.rs
Normal file
80
qt/launcher/src/platform/mac.rs
Normal file
|
@ -0,0 +1,80 @@
|
|||
// 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::process::Command;
|
||||
|
||||
use anki_process::CommandExt as AnkiCommandExt;
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
|
||||
// Re-export Unix functions that macOS uses
|
||||
pub use super::unix::{
|
||||
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);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn handle_terminal_launch() -> Result<()> {
|
||||
let stdout_is_terminal = std::io::IsTerminal::is_terminal(&std::io::stdout());
|
||||
if stdout_is_terminal {
|
||||
print!("\x1B[2J\x1B[H"); // Clear screen and move cursor to top
|
||||
println!("\x1B[1mPreparing to start Anki...\x1B[0m\n");
|
||||
} else {
|
||||
// If launched from GUI, relaunch in Terminal.app
|
||||
relaunch_in_terminal()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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<()> {
|
||||
// Pre-validate by running --version to trigger any Gatekeeper checks
|
||||
println!("\n\x1B[1mThis may take a few minutes. Please wait...\x1B[0m");
|
||||
let _ = Command::new(anki_bin)
|
||||
.env("ANKI_FIRST_RUN", "1")
|
||||
.arg("--version")
|
||||
.ensure_success();
|
||||
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"
|
||||
}
|
18
qt/launcher/src/platform/mod.rs
Normal file
18
qt/launcher/src/platform/mod.rs
Normal file
|
@ -0,0 +1,18 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
#[cfg(unix)]
|
||||
mod unix;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
mod mac;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
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::*;
|
74
qt/launcher/src/platform/unix.rs
Normal file
74
qt/launcher/src/platform/unix.rs
Normal file
|
@ -0,0 +1,74 @@
|
|||
// 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::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 handle_terminal_launch() -> Result<()> {
|
||||
print!("\x1B[2J\x1B[H"); // Clear screen and move cursor to top
|
||||
println!("\x1B[1mPreparing to start Anki...\x1B[0m\n");
|
||||
// Skip terminal relaunch on non-macOS Unix systems as we don't know which
|
||||
// terminal is installed
|
||||
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"
|
||||
}
|
||||
}
|
118
qt/launcher/src/platform/windows.rs
Normal file
118
qt/launcher/src/platform/windows.rs
Normal file
|
@ -0,0 +1,118 @@
|
|||
// 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 crate::Config;
|
||||
|
||||
pub fn handle_terminal_launch() -> Result<()> {
|
||||
// uv will do this itself
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 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) {
|
||||
use std::ffi::CString;
|
||||
|
||||
use libc_stdhandle::*;
|
||||
use winapi::um::wincon;
|
||||
|
||||
let console_attached = unsafe { wincon::AttachConsole(wincon::ATTACH_PARENT_PROCESS) };
|
||||
if console_attached == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let conin = CString::new("CONIN$").unwrap();
|
||||
let conout = CString::new("CONOUT$").unwrap();
|
||||
let r = CString::new("r").unwrap();
|
||||
let w = CString::new("w").unwrap();
|
||||
|
||||
// Python uses the CRT for I/O, and it requires the descriptors are reopened.
|
||||
unsafe {
|
||||
libc::freopen(conin.as_ptr(), r.as_ptr(), stdin());
|
||||
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 {
|
||||
// Windows uses standard uv binary name
|
||||
"uv.exe"
|
||||
}
|
|
@ -1,9 +1,25 @@
|
|||
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||
<assemblyIdentity type="win32" name="Anki" version="1.0.0.0" />
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
|
||||
<security>
|
||||
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings>
|
||||
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||
<longPathAware xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">true</longPathAware>
|
||||
<activeCodePage xmlns="http://schemas.microsoft.com/SMI/2019/WindowsSettings">UTF-8</activeCodePage>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />
|
||||
</application>
|
||||
</compatibility>
|
||||
</assembly>
|
||||
|
|
|
@ -23,8 +23,8 @@ Name "Anki"
|
|||
|
||||
Unicode true
|
||||
|
||||
; The file to write (make relative to repo root instead of out/bundle)
|
||||
OutFile "..\..\@@INSTALLER@@"
|
||||
; The file to write (relative to nsis directory)
|
||||
OutFile "..\launcher_exe\anki-install.exe"
|
||||
|
||||
; Non elevated
|
||||
RequestExecutionLevel user
|
||||
|
@ -62,7 +62,7 @@ Function .onInit
|
|||
FunctionEnd
|
||||
|
||||
!ifdef WRITE_UNINSTALLER
|
||||
!uninstfinalize 'copy "%1" "std\uninstall.exe"'
|
||||
!uninstfinalize 'copy "%1" "uninstall.exe"'
|
||||
!endif
|
||||
|
||||
;--------------------------------
|
||||
|
@ -191,7 +191,7 @@ Section ""
|
|||
|
||||
; Add files to installer
|
||||
!ifndef WRITE_UNINSTALLER
|
||||
File /r ..\..\@@SRC@@\*.*
|
||||
File /r ..\launcher\*.*
|
||||
!endif
|
||||
|
||||
!insertmacro APP_ASSOCIATE_HKCU "apkg" "anki.apkg" \
|
||||
|
@ -213,8 +213,8 @@ Section ""
|
|||
WriteRegStr HKCU Software\Anki "Install_Dir64" "$INSTDIR"
|
||||
|
||||
; Write the uninstall keys for Windows
|
||||
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\Anki" "DisplayName" "Anki"
|
||||
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\Anki" "DisplayVersion" "@@VERSION@@"
|
||||
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\Anki" "DisplayName" "Anki Launcher"
|
||||
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\Anki" "DisplayVersion" "1.0.0"
|
||||
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\Anki" "UninstallString" '"$INSTDIR\uninstall.exe"'
|
||||
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\Anki" "QuietUninstallString" '"$INSTDIR\uninstall.exe" /S'
|
||||
WriteRegDWORD HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\Anki" "NoModify" 1
|
||||
|
@ -252,9 +252,15 @@ Section "Uninstall"
|
|||
!insertmacro APP_UNASSOCIATE_HKCU "ankiaddon" "anki.ankiaddon"
|
||||
!insertmacro UPDATEFILEASSOC
|
||||
|
||||
; Schedule uninstaller for deletion on reboot
|
||||
Delete /REBOOTOK "$INSTDIR\uninstall.exe"
|
||||
|
||||
; try to remove top level folder if empty
|
||||
RMDir "$INSTDIR"
|
||||
|
||||
; Remove AnkiProgramData folder created during runtime
|
||||
RMDir /r "$LOCALAPPDATA\AnkiProgramFiles"
|
||||
|
||||
; Remove registry keys
|
||||
DeleteRegKey HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\Anki"
|
||||
DeleteRegKey HKCU Software\Anki
|
||||
|
|
5
qt/launcher/win/build.bat
Normal file
5
qt/launcher/win/build.bat
Normal file
|
@ -0,0 +1,5 @@
|
|||
@echo off
|
||||
|
||||
set CODESIGN=1
|
||||
REM set NO_COMPRESS=1
|
||||
cargo run --bin build_win
|
|
@ -37,14 +37,14 @@ qt66 = [
|
|||
"pyqt6-webengine-qt6==6.6.2",
|
||||
"pyqt6_sip==13.6.0",
|
||||
]
|
||||
qt = [
|
||||
qt67 = [
|
||||
"pyqt6==6.7.1",
|
||||
"pyqt6-qt6==6.7.3",
|
||||
"pyqt6-webengine==6.7.0",
|
||||
"pyqt6-webengine-qt6==6.7.3",
|
||||
"pyqt6_sip==13.10.2",
|
||||
]
|
||||
qt68 = [
|
||||
qt = [
|
||||
"pyqt6==6.8.0",
|
||||
"pyqt6-qt6==6.8.1",
|
||||
"pyqt6-webengine==6.8.0",
|
||||
|
@ -57,7 +57,7 @@ conflicts = [
|
|||
[
|
||||
{ extra = "qt" },
|
||||
{ extra = "qt66" },
|
||||
{ extra = "qt68" },
|
||||
{ extra = "qt67" },
|
||||
],
|
||||
]
|
||||
|
||||
|
|
|
@ -2,13 +2,12 @@
|
|||
|
||||
export UV_PUBLISH_TOKEN=$(pass show w/pypi-api-test)
|
||||
|
||||
# Get the project root (two levels up from qt/bundle)
|
||||
# Get the project root (two levels up from qt/release)
|
||||
PROJ_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
|
||||
# Use extracted uv binary
|
||||
UV="$PROJ_ROOT/out/extracted/uv/uv"
|
||||
|
||||
cd release
|
||||
rm -rf dist
|
||||
"$UV" build --wheel
|
||||
"$UV" publish --index testpypi
|
80
qt/release/pyproject.toml
Normal file
80
qt/release/pyproject.toml
Normal file
|
@ -0,0 +1,80 @@
|
|||
[project]
|
||||
name = "anki-release"
|
||||
version = "0.1.3"
|
||||
description = "A package to lock Anki's dependencies"
|
||||
requires-python = ">=3.9"
|
||||
dependencies = [
|
||||
"anki==0.1.2",
|
||||
"aqt==0.1.2",
|
||||
"anki-audio==0.1.0 ; sys_platform == 'darwin' or sys_platform == 'win32'",
|
||||
"attrs==25.3.0",
|
||||
"beautifulsoup4==4.12.3",
|
||||
"blinker==1.9.0",
|
||||
"certifi==2025.4.26",
|
||||
"charset-normalizer==3.4.2",
|
||||
"click==8.1.8 ; python_full_version < '3.10'",
|
||||
"click==8.2.1 ; python_full_version >= '3.10'",
|
||||
"colorama==0.4.6 ; sys_platform == 'win32'",
|
||||
"decorator==5.2.1",
|
||||
"distro==1.9.0 ; sys_platform != 'darwin' and sys_platform != 'win32'",
|
||||
"flask==3.1.1",
|
||||
"flask-cors==6.0.0",
|
||||
"idna==3.10",
|
||||
"importlib-metadata==8.7.0 ; python_full_version < '3.10'",
|
||||
"itsdangerous==2.2.0",
|
||||
"jinja2==3.1.6",
|
||||
"jsonschema==4.24.0",
|
||||
"jsonschema-specifications==2025.4.1",
|
||||
"markdown==3.8",
|
||||
"markupsafe==3.0.2",
|
||||
"mock==5.2.0",
|
||||
"orjson==3.10.18",
|
||||
"pip-system-certs==4.0",
|
||||
"protobuf==6.31.1",
|
||||
"psutil==7.0.0 ; sys_platform == 'win32'",
|
||||
"pyqt6==6.8.0",
|
||||
"pyqt6-qt6==6.8.1",
|
||||
"pyqt6-sip==13.10.2",
|
||||
"pyqt6-webengine==6.8.0",
|
||||
"pyqt6-webengine-qt6==6.8.1",
|
||||
"pysocks==1.7.1",
|
||||
"pywin32==310 ; sys_platform == 'win32'",
|
||||
"referencing==0.36.2",
|
||||
"requests==2.32.3",
|
||||
"rpds-py==0.25.1",
|
||||
"send2trash==1.8.3",
|
||||
"soupsieve==2.7",
|
||||
"types-click==7.1.8",
|
||||
"types-decorator==5.2.0.20250324",
|
||||
"types-flask==1.1.6",
|
||||
"types-flask-cors==6.0.0.20250520",
|
||||
"types-jinja2==2.11.9",
|
||||
"types-markdown==3.8.0.20250415",
|
||||
"types-markupsafe==1.1.10",
|
||||
"types-orjson==3.6.2",
|
||||
"types-protobuf==6.30.2.20250516",
|
||||
"types-pywin32==310.0.0.20250516",
|
||||
"types-requests==2.32.0.20250602",
|
||||
"types-waitress==3.0.1.20241117",
|
||||
"types-werkzeug==1.0.9",
|
||||
"typing-extensions==4.14.0",
|
||||
"urllib3==2.4.0",
|
||||
"waitress==3.0.2",
|
||||
"werkzeug==3.1.3",
|
||||
"wrapt==1.17.2",
|
||||
"zipp==3.23.0 ; python_full_version < '3.10'",
|
||||
]
|
||||
|
||||
[[tool.uv.index]]
|
||||
name = "testpypi"
|
||||
url = "https://test.pypi.org/simple/"
|
||||
publish-url = "https://test.pypi.org/legacy/"
|
||||
explicit = true
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
# hatch throws an error if nothing is included
|
||||
[tool.hatch.build.targets.wheel]
|
||||
include = ["no-such-file"]
|
|
@ -1,7 +1,13 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
# Get the project root (two levels up from qt/bundle)
|
||||
test -f update-release.sh || {
|
||||
echo "run from release folder"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Get the project root (two levels up from qt/release)
|
||||
PROJ_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
|
||||
# Use extracted uv binary
|
||||
|
@ -10,15 +16,13 @@ UV="$PROJ_ROOT/out/extracted/uv/uv"
|
|||
# Prompt for wheel version
|
||||
read -p "Wheel version: " VERSION
|
||||
|
||||
# Create release directory if it doesn't exist
|
||||
mkdir -p release
|
||||
|
||||
# Export dependencies using uv
|
||||
echo "Exporting dependencies..."
|
||||
rm -f pyproject.toml
|
||||
DEPS=$("$UV" export --no-hashes --no-annotate --no-header --extra audio --extra qt --all-packages --no-dev --no-emit-workspace)
|
||||
|
||||
# Generate the pyproject.toml file
|
||||
cat > release/pyproject.toml << EOF
|
||||
cat > pyproject.toml << EOF
|
||||
[project]
|
||||
name = "anki-release"
|
||||
version = "$VERSION"
|
||||
|
@ -32,12 +36,12 @@ EOF
|
|||
# Add the exported dependencies to the file
|
||||
echo "$DEPS" | while IFS= read -r line; do
|
||||
if [[ -n "$line" ]]; then
|
||||
echo " \"$line\"," >> release/pyproject.toml
|
||||
echo " \"$line\"," >> pyproject.toml
|
||||
fi
|
||||
done
|
||||
|
||||
# Complete the pyproject.toml file
|
||||
cat >> release/pyproject.toml << 'EOF'
|
||||
cat >> pyproject.toml << 'EOF'
|
||||
]
|
||||
|
||||
[[tool.uv.index]]
|
||||
|
@ -55,4 +59,4 @@ build-backend = "hatchling.build"
|
|||
include = ["no-such-file"]
|
||||
EOF
|
||||
|
||||
echo "Generated release/pyproject.toml with version $VERSION"
|
||||
echo "Generated pyproject.toml with version $VERSION"
|
|
@ -57,6 +57,9 @@ pub trait CommandExt {
|
|||
|
||||
fn ensure_success(&mut self) -> Result<&mut Self>;
|
||||
fn utf8_output(&mut self) -> Result<Utf8Output>;
|
||||
fn ensure_spawn(&mut self) -> Result<std::process::Child>;
|
||||
#[cfg(unix)]
|
||||
fn ensure_exec(&mut self) -> Result<()>;
|
||||
}
|
||||
|
||||
impl CommandExt for Command {
|
||||
|
@ -94,6 +97,23 @@ impl CommandExt for Command {
|
|||
})?,
|
||||
})
|
||||
}
|
||||
|
||||
fn ensure_spawn(&mut self) -> Result<std::process::Child> {
|
||||
self.spawn().with_context(|_| DidNotExecuteSnafu {
|
||||
cmdline: get_cmdline(self),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn ensure_exec(&mut self) -> Result<()> {
|
||||
use std::os::unix::process::CommandExt as UnixCommandExt;
|
||||
let cmdline = get_cmdline(self);
|
||||
let error = self.exec();
|
||||
Err(Error::DidNotExecute {
|
||||
cmdline,
|
||||
source: error,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn get_cmdline(arg: &mut Command) -> String {
|
||||
|
|
12
tools/build-arm-lin
Executable file
12
tools/build-arm-lin
Executable file
|
@ -0,0 +1,12 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
# sudo apt install libc6-dev-arm64-cross gcc-aarch64-linux-gnu
|
||||
rustup target add aarch64-unknown-linux-gnu
|
||||
|
||||
export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc
|
||||
export LIN_ARM64=1
|
||||
|
||||
RELEASE=2 ./ninja wheels:anki
|
||||
echo "wheels are in out/wheels"
|
10
tools/build-x64-mac
Executable file
10
tools/build-x64-mac
Executable file
|
@ -0,0 +1,10 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
rustup target add x86_64-apple-darwin
|
||||
|
||||
export MAC_X86=1
|
||||
|
||||
RELEASE=2 ./ninja wheels:anki
|
||||
echo "wheels are in out/wheels"
|
|
@ -1,5 +1,7 @@
|
|||
@echo off
|
||||
pushd "%~dp0"\..
|
||||
set RELEASE=1
|
||||
if exist out\wheels rmdir /s /q out\wheels
|
||||
set RELEASE=2
|
||||
tools\ninja wheels || exit /b 1
|
||||
echo wheels are in out/wheels
|
||||
popd
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
#!/bin/bash
|
||||
#
|
||||
# Run a command with an alternative buildroot and Intel architecture target, for building Intel on an ARM Mac.
|
||||
# Eg ./tools/mac-x86 ./tools/run-qt5.14
|
||||
#
|
||||
# Uses hard-coded paths to Python and build folders.
|
||||
#
|
||||
|
||||
export BUILD_ROOT=~/Local/build/anki-x86
|
||||
export NORMAL_BUILD_ROOT=~/Local/build/anki
|
||||
export MAC_X86=1
|
||||
|
||||
|
||||
# run provided command
|
||||
$*
|
||||
|
||||
BUILD_ROOT=$NORMAL_BUILD_ROOT ./ninja just-to-restore-build-root-and-failure-is-expected
|
|
@ -2,8 +2,6 @@
|
|||
|
||||
set -e
|
||||
|
||||
./tools/build
|
||||
|
||||
export UV_PUBLISH_TOKEN=$(pass show w/pypi-api-test)
|
||||
out/extracted/uv/uv publish --index testpypi out/wheels/*
|
||||
|
||||
|
|
Loading…
Reference in a new issue