Migrate build system to uv (#4074)

* Migrate build system to uv

Closes #3787, and is a step towards #3081 and #4022

This change breaks our PyOxidizer bundling process. While we probably
could update it to work with the new venvs & lockfile, my intention
is to use this as a base to try out a uv-based packager/installer.

Some notes about the changes:

- Use uv for python download + venv installation
- Drop python/requirements* in favour of pyproject files / uv.lock
- Bumped to latest Python 3.9 version. The move to 3.13 should be
a fairly trivial change when we're ready.
- Dropped the old write_wheel.py in favour of uv/hatchling. This has
the unfortunate side-effect of dropping leading zeros in our wheels,
which we could try hack around in the future.
- Switch to Qt 6.7 for the dev repo, as it's the first PyQt version
with a Linux/ARM WebEngine wheel.
- Unified our macOS deployment target with minimum required for ARM.
- Dropped unused fluent python files
- Dropped unused python license generation
- Dropped helpers to run under Qt 5, as our wheels were already
requiring Qt 6 to install.

* Build action to create universal uv binary

* Drop some PyOxidizer-related files

* Use Windows ARM64 cargo/node binaries during build

We can't provide ARM64 wheels to users yet due to #4079, but we can
at least speed up the build.

The rustls -> native-tls change on Windows is because ring requires
clang to compile for ARM64, and I figured it's best to keep our Windows
deps consistent. We already built the wheels with native-tls.

* Make libankihelper a universal library

We were shipping a single arch library in a purelib, leading to
breakages when running on a different platform.

* Use Python wheel for mpv/lame on Windows/Mac

This is convenient, but suboptimal on a Mac at the moment. The first
run of mpv will take a number of seconds for security checks to run,
and our mpv code ends up timing out, repeating the process each time.
Our installer stub will need to invoke mpv once first to get it validated.

We could address this by distributing the audio with the installer/stub,
or perhaps by putting the binaries in a .pkg file that's notarized+stapled
and then included in the wheel.

* Add some helper scripts to build a fully-locked wheel

* Initial macOS launcher prototype

* Add a hidden env var to preload our libs and audio helpers on macOS

* qt/bundle -> qt/launcher

- remove more of the old bundling code
- handle app icon

* Fat binary, notarization & dmg

* Publish wheels on testpypi for testing

* Use our Python pin for the launcher too

* Python cleanups

* 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.

* Fix pylint on Linux

* Fix failure to run from /usr/local/bin

* Remove remaining pyoxidizer refs, and clean up duplicate release folder

* Rust dep updates

- Rust 1.87 for now (1.88 due out in around a week)
- Nom looks involved, so I left it for now
- prost-reflect depends on a new prost version that got yanked

* Python 3.13 + dep updates

Updated protoc binaries + add helper in order to try fix build breakage.
Ended up being due to an AI-generated update to pip-system-certs that
was not reviewed carefully enough:
https://gitlab.com/alelec/pip-system-certs/-/issues/36

The updated mypy/black needed some tweaks to our files.

* Windows compilation fixes

* Automatically run Anki after installing on Windows

* Touch pyproject.toml upon install, so we check for updates

* Update Python deps

- urllib3 for CVE
- pip-system-certs got fixed
- markdown/pytest also updated
This commit is contained in:
Damien Elmes 2025-06-19 14:03:16 +07:00 committed by GitHub
parent bbf533b172
commit 04996c77f3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
197 changed files with 5695 additions and 6648 deletions

View file

@ -5,7 +5,8 @@ DESCRIPTORS_BIN = { value = "out/rslib/proto/descriptors.bin", relative = true }
# build script will append .exe if necessary # build script will append .exe if necessary
PROTOC = { value = "out/extracted/protoc/bin/protoc", relative = true } PROTOC = { value = "out/extracted/protoc/bin/protoc", relative = true }
PYO3_NO_PYTHON = "1" PYO3_NO_PYTHON = "1"
MACOSX_DEPLOYMENT_TARGET = "10.13.4" MACOSX_DEPLOYMENT_TARGET = "11"
PYTHONDONTWRITEBYTECODE = "1" # prevent junk files on Windows
[term] [term]
color = "always" color = "always"

View file

@ -5,9 +5,6 @@
db-path = "~/.cargo/advisory-db" db-path = "~/.cargo/advisory-db"
db-urls = ["https://github.com/rustsec/advisory-db"] db-urls = ["https://github.com/rustsec/advisory-db"]
ignore = [ ignore = [
# pyoxidizer is stuck on an old ring version
"RUSTSEC-2025-0009",
"RUSTSEC-2025-0010",
# burn depends on an unmaintained package 'paste' # burn depends on an unmaintained package 'paste'
"RUSTSEC-2024-0436", "RUSTSEC-2024-0436",
] ]
@ -17,12 +14,11 @@ allow = [
"MIT", "MIT",
"Apache-2.0", "Apache-2.0",
"Apache-2.0 WITH LLVM-exception", "Apache-2.0 WITH LLVM-exception",
"CDLA-Permissive-2.0",
"ISC", "ISC",
"MPL-2.0", "MPL-2.0",
"Unicode-DFS-2016",
"BSD-2-Clause", "BSD-2-Clause",
"BSD-3-Clause", "BSD-3-Clause",
"OpenSSL",
"CC0-1.0", "CC0-1.0",
"Unlicense", "Unlicense",
"Zlib", "Zlib",

View file

@ -20,7 +20,6 @@
"ftl/usage", "ftl/usage",
"licenses.json", "licenses.json",
".dmypy.json", ".dmypy.json",
"qt/bundle/PyOxidizer",
"target", "target",
".mypy_cache", ".mypy_cache",
"extra", "extra",

6
.gitmodules vendored
View file

@ -6,9 +6,3 @@
path = ftl/qt-repo path = ftl/qt-repo
url = https://github.com/ankitects/anki-desktop-ftl.git url = https://github.com/ankitects/anki-desktop-ftl.git
shallow = true shallow = true
[submodule "qt/bundle/PyOxidizer"]
path = qt/bundle/PyOxidizer
url = https://github.com/ankitects/PyOxidizer.git
shallow = true
update = none

View file

@ -2,4 +2,3 @@
py_version=39 py_version=39
known_first_party=anki,aqt,tests known_first_party=anki,aqt,tests
profile=black profile=black
extend_skip=qt/bundle

View file

@ -18,7 +18,7 @@ mypy_path =
ftl, ftl,
pylib/tools, pylib/tools,
python python
exclude = (qt/bundle/PyOxidizer|pylib/anki/_vendor) exclude = (pylib/anki/_vendor)
[mypy-anki.*] [mypy-anki.*]
disallow_untyped_defs = True disallow_untyped_defs = True
@ -165,3 +165,5 @@ ignore_missing_imports = True
ignore_missing_imports = True ignore_missing_imports = True
[mypy-pip_system_certs.*] [mypy-pip_system_certs.*]
ignore_missing_imports = True ignore_missing_imports = True
[mypy-anki_audio]
ignore_missing_imports = True

1
.python-version Normal file
View file

@ -0,0 +1 @@
3.13.5

View file

@ -1,2 +1,2 @@
target-version = "py39" target-version = "py39"
extend-exclude = ["qt/bundle"] extend-exclude = []

View file

@ -31,11 +31,13 @@
"rust-analyzer.rustfmt.extraArgs": ["+nightly"], "rust-analyzer.rustfmt.extraArgs": ["+nightly"],
"search.exclude": { "search.exclude": {
"**/node_modules": true, "**/node_modules": true,
".bazel/**": true, ".bazel/**": true
"qt/bundle/PyOxidizer": true
}, },
"rust-analyzer.cargo.buildScripts.enable": true, "rust-analyzer.cargo.buildScripts.enable": true,
"python.analysis.typeCheckingMode": "off", "python.analysis.typeCheckingMode": "off",
"python.analysis.exclude": [
"out/launcher/**"
],
"terminal.integrated.env.windows": { "terminal.integrated.env.windows": {
"PATH": "c:\\msys64\\usr\\bin;${env:Path}" "PATH": "c:\\msys64\\usr\\bin;${env:Path}"
} }

1950
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -12,8 +12,7 @@ members = [
"build/runner", "build/runner",
"ftl", "ftl",
"pylib/rsbridge", "pylib/rsbridge",
"qt/bundle/mac", "qt/launcher",
"qt/bundle/win",
"rslib", "rslib",
"rslib/i18n", "rslib/i18n",
"rslib/io", "rslib/io",
@ -23,7 +22,6 @@ members = [
"rslib/sync", "rslib/sync",
"tools/minilints", "tools/minilints",
] ]
exclude = ["qt/bundle"]
resolver = "2" resolver = "2"
[workspace.dependencies.percent-encoding-iri] [workspace.dependencies.percent-encoding-iri]
@ -35,7 +33,7 @@ git = "https://github.com/ankitects/linkcheck.git"
rev = "184b2ca50ed39ca43da13f0b830a463861adb9ca" rev = "184b2ca50ed39ca43da13f0b830a463861adb9ca"
[workspace.dependencies.fsrs] [workspace.dependencies.fsrs]
version = "4.0.0" version = "4.1.1"
# git = "https://github.com/open-spaced-repetition/fsrs-rs.git" # git = "https://github.com/open-spaced-repetition/fsrs-rs.git"
# rev = "a7f7efc10f0a26b14ee348cc7402155685f2a24f" # rev = "a7f7efc10f0a26b14ee348cc7402155685f2a24f"
# path = "../open-spaced-repetition/fsrs-rs" # path = "../open-spaced-repetition/fsrs-rs"
@ -54,99 +52,98 @@ ninja_gen = { "path" = "build/ninja_gen" }
unicase = "=2.6.0" # any changes could invalidate sqlite indexes unicase = "=2.6.0" # any changes could invalidate sqlite indexes
# normal # normal
ammonia = "4.0.0" ammonia = "4.1.0"
anyhow = "1.0.90" anyhow = "1.0.98"
apple-bundles = "0.17.0" async-compression = { version = "0.4.24", features = ["zstd", "tokio"] }
async-compression = { version = "0.4.17", features = ["zstd", "tokio"] }
async-stream = "0.3.6" async-stream = "0.3.6"
async-trait = "0.1.83" async-trait = "0.1.88"
axum = { version = "0.7", features = ["multipart", "macros"] } axum = { version = "0.8.4", features = ["multipart", "macros"] }
axum-client-ip = "0.6" axum-client-ip = "1.1.3"
axum-extra = { version = "0.9.4", features = ["typed-header"] } axum-extra = { version = "0.10.1", features = ["typed-header"] }
blake3 = "1.5.4" blake3 = "1.8.2"
bytes = "1.7.2" bytes = "1.10.1"
camino = "1.1.9" camino = "1.1.10"
chrono = { version = "0.4.38", default-features = false, features = ["std", "clock"] } chrono = { version = "0.4.41", default-features = false, features = ["std", "clock"] }
clap = { version = "4.5.20", features = ["derive"] } clap = { version = "4.5.40", features = ["derive"] }
coarsetime = "0.1.34" coarsetime = "0.1.36"
convert_case = "0.6.0" convert_case = "0.8.0"
criterion = { version = "0.5.1" } criterion = { version = "0.6.0" }
csv = "1.3.0" csv = "1.3.1"
data-encoding = "2.6.0" data-encoding = "2.9.0"
difflib = "0.4.0" difflib = "0.4.0"
dirs = "5.0.1" dirs = "6.0.0"
dunce = "1.0.5" dunce = "1.0.5"
embed-resource = "3.0.4"
envy = "0.4.2" envy = "0.4.2"
flate2 = "1.0.34" flate2 = "1.1.2"
fluent = "0.16.1" fluent = "0.17.0"
fluent-bundle = "0.15.3" fluent-bundle = "0.16.0"
fluent-syntax = "0.11.1" fluent-syntax = "0.12.0"
fnv = "1.0.7" fnv = "1.0.7"
futures = "0.3.31" futures = "0.3.31"
glob = "0.3.1" globset = "0.4.16"
globset = "0.4.15"
hex = "0.4.3" hex = "0.4.3"
htmlescape = "0.3.1" htmlescape = "0.3.1"
hyper = "1" hyper = "1"
id_tree = "1.8.0" id_tree = "1.8.0"
inflections = "1.1.1" inflections = "1.1.1"
intl-memoizer = "0.5.2" intl-memoizer = "0.5.3"
itertools = "0.13.0" itertools = "0.14.0"
junction = "1.2.0" junction = "1.2.0"
lazy_static = "1.5.0" libc = "0.2"
libc-stdhandle = "0.1"
maplit = "1.0.2" maplit = "1.0.2"
nom = "7.1.3" nom = "7.1.3"
num-format = "0.4.4" num-format = "0.4.4"
num_cpus = "1.16.0" num_cpus = "1.17.0"
num_enum = "0.7.3" num_enum = "0.7.3"
once_cell = "1.20.2" once_cell = "1.21.3"
pbkdf2 = { version = "0.12", features = ["simple"] } pbkdf2 = { version = "0.12", features = ["simple"] }
phf = { version = "0.11.2", features = ["macros"] } phf = { version = "0.11.3", features = ["macros"] }
pin-project = "1.1.6" pin-project = "1.1.10"
plist = "1.7.0" prettyplease = "0.2.34"
prettyplease = "0.2.24"
prost = "0.13" prost = "0.13"
prost-build = "0.13" prost-build = "0.13"
prost-reflect = "0.14" prost-reflect = "0.14.7"
prost-types = "0.13" prost-types = "0.13"
pulldown-cmark = "0.9.6" pulldown-cmark = "0.13.0"
pyo3 = { version = "0.24", features = ["extension-module", "abi3", "abi3-py39"] } pyo3 = { version = "0.25.1", features = ["extension-module", "abi3", "abi3-py39"] }
rand = "0.8.5" rand = "0.9.1"
regex = "1.11.0" regex = "1.11.1"
reqwest = { version = "0.12.8", default-features = false, features = ["json", "socks", "stream", "multipart"] } reqwest = { version = "0.12.20", default-features = false, features = ["json", "socks", "stream", "multipart"] }
rusqlite = { version = "0.30.0", features = ["trace", "functions", "collation", "bundled"] } rusqlite = { version = "0.36.0", features = ["trace", "functions", "collation", "bundled"] }
rustls-pemfile = "2.2.0" rustls-pemfile = "2.2.0"
scopeguard = "1.2.0" scopeguard = "1.2.0"
serde = { version = "1.0.210", features = ["derive"] } serde = { version = "1.0.219", features = ["derive"] }
serde-aux = "4.5.0" serde-aux = "4.7.0"
serde_json = "1.0.132" serde_json = "1.0.140"
serde_repr = "0.1.19" serde_repr = "0.1.20"
serde_tuple = "0.5.0" serde_tuple = "1.1.0"
sha1 = "0.10.6" sha1 = "0.10.6"
sha2 = { version = "0.10.8" } sha2 = { version = "0.10.9" }
simple-file-manifest = "0.11.0"
snafu = { version = "0.8.6", features = ["rust_1_61"] } snafu = { version = "0.8.6", features = ["rust_1_61"] }
strum = { version = "0.26.3", features = ["derive"] } strum = { version = "0.27.1", features = ["derive"] }
syn = { version = "2.0.82", features = ["parsing", "printing"] } syn = { version = "2.0.103", features = ["parsing", "printing"] }
tar = "0.4.42" tar = "0.4.44"
tempfile = "3.13.0" tempfile = "3.20.0"
termcolor = "1.4.1" termcolor = "1.4.1"
tokio = { version = "1.40", features = ["fs", "rt-multi-thread", "macros", "signal"] } tokio = { version = "1.45", features = ["fs", "rt-multi-thread", "macros", "signal"] }
tokio-util = { version = "0.7.12", features = ["io"] } tokio-util = { version = "0.7.15", features = ["io"] }
tower-http = { version = "0.5", features = ["trace"] } tower-http = { version = "0.6.6", features = ["trace"] }
tracing = { version = "0.1.40", features = ["max_level_trace", "release_max_level_debug"] } tracing = { version = "0.1.41", features = ["max_level_trace", "release_max_level_debug"] }
tracing-appender = "0.2.3" tracing-appender = "0.2.3"
tracing-subscriber = { version = "0.3.18", features = ["fmt", "env-filter"] } tracing-subscriber = { version = "0.3.19", features = ["fmt", "env-filter"] }
tugger-windows-codesign = "0.10.0" unic-langid = { version = "0.9.6", features = ["macros"] }
unic-langid = { version = "0.9.5", features = ["macros"] }
unic-ucd-category = "0.9.0" unic-ucd-category = "0.9.0"
unicode-normalization = "0.1.24" unicode-normalization = "0.1.24"
walkdir = "2.5.0" walkdir = "2.5.0"
which = "5.0.0" which = "8.0.0"
wiremock = "0.6.2" 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" xz2 = "0.1.7"
zip = { version = "0.6.6", default-features = false, features = ["deflate", "time"] } zip = { version = "4.1.0", default-features = false, features = ["deflate", "time"] }
zstd = { version = "0.13.2", features = ["zstdmt"] } zstd = { version = "0.13.3", features = ["zstdmt"] }
# Apply mild optimizations to our dependencies in dev mode, which among other things # Apply mild optimizations to our dependencies in dev mode, which among other things
# improves sha2 performance by about 21x. Opt 1 chosen due to # improves sha2 performance by about 21x. Opt 1 chosen due to

View file

@ -364,20 +364,14 @@ fn build_wheel(build: &mut Build) -> Result<()> {
BuildWheel { BuildWheel {
name: "aqt", name: "aqt",
version: anki_version(), version: anki_version(),
src_folder: "qt/aqt",
gen_folder: "$builddir/qt/_aqt",
platform: None, platform: None,
deps: inputs![":qt:aqt", glob!("qt/aqt/**"), "python/requirements.aqt.in"], deps: inputs![":qt:aqt", glob!("qt/aqt/**"), "qt/pyproject.toml"],
}, },
) )
} }
fn check_python(build: &mut Build) -> Result<()> { fn check_python(build: &mut Build) -> Result<()> {
python_format( python_format(build, "qt", inputs![glob!("qt/**/*.py")])?;
build,
"qt",
inputs![glob!("qt/**/*.py", "qt/bundle/PyOxidizer/**")],
)?;
build.add_action( build.add_action(
"check:pytest:aqt", "check:pytest:aqt",

View file

@ -1,442 +0,0 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
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::git::SyncSubmodule;
use ninja_gen::glob;
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;
#[derive(Debug, PartialEq, Eq)]
enum DistKind {
Standard,
}
impl DistKind {
fn folder_name(&self) -> &'static str {
match self {
DistKind::Standard => "std",
}
}
fn name(&self) -> &'static str {
match self {
DistKind::Standard => "standard",
}
}
}
pub fn build_bundle(build: &mut Build) -> Result<()> {
// install into venv
setup_primary_venv(build)?;
install_anki_wheels(build)?;
// bundle venv into output binary + extra_files
build_pyoxidizer(build)?;
build_artifacts(build)?;
build_binary(build)?;
// package up outputs with Qt/other deps
download_dist_folder_deps(build)?;
build_dist_folder(build, DistKind::Standard)?;
build_packages(build)?;
Ok(())
}
fn targetting_macos_arm() -> bool {
cfg!(all(target_os = "macos", target_arch = "aarch64"))
&& overriden_python_target_platform().is_none()
}
const WIN_AUDIO: OnlineArchive = OnlineArchive {
url: "https://github.com/ankitects/anki-bundle-extras/releases/download/anki-2022-02-09/audio-win-amd64.tar.gz",
sha256: "0815a601baba05e03bc36b568cdc2332b1cf4aa17125fc33c69de125f8dd687f",
};
const MAC_ARM_AUDIO: OnlineArchive = OnlineArchive {
url: "https://github.com/ankitects/anki-bundle-extras/releases/download/anki-2022-05-26/audio-mac-arm64.tar.gz",
sha256: "f6c4af9be59ae1c82a16f5c6307f13cbf31b49ad7b69ce1cb6e0e7b403cfdb8f",
};
const MAC_AMD_AUDIO: OnlineArchive = OnlineArchive {
url: "https://github.com/ankitects/anki-bundle-extras/releases/download/anki-2022-05-26/audio-mac-amd64.tar.gz",
sha256: "ecbb3c878805cdd58b1a0b8e3fd8c753b8ce3ad36c8b5904a79111f9db29ff42",
};
const MAC_ARM_QT6: OnlineArchive = OnlineArchive {
url: "https://github.com/ankitects/anki-bundle-extras/releases/download/anki-2024-02-29/pyqt6.6-mac-arm64.tar.zst",
sha256: "9b2ade4ae9b80506689062845e83e8c60f7fa9843545bf7bb2d11d3e2f105878",
};
const MAC_AMD_QT6: OnlineArchive = OnlineArchive {
url: "https://github.com/ankitects/anki-bundle-extras/releases/download/anki-2024-02-29/pyqt6.6-mac-amd64.tar.zst",
sha256: "dbd0871e4da22820d1fa9ab29220d631467d1178038dcab4b15169ad7f499b1b",
};
const LINUX_QT_PLUGINS: OnlineArchive = OnlineArchive {
url: "https://github.com/ankitects/anki-bundle-extras/releases/download/anki-2023-05-02/qt-plugins-linux-amd64.tar.gz",
sha256: "66bb568aca7242bc55ad419bf5c96755ca15d2a743e1c3a09cba8b83230b138b",
};
const NSIS_PLUGINS: OnlineArchive = OnlineArchive {
url: "https://github.com/ankitects/anki-bundle-extras/releases/download/anki-2023-05-19/nsis.tar.zst",
sha256: "6133f730ece699de19714d0479c73bc848647d277e9cc80dda9b9ebe532b40a8",
};
fn download_dist_folder_deps(build: &mut Build) -> Result<()> {
let mut bundle_deps = vec![":wheels"];
if cfg!(windows) {
download_and_extract(build, "win_amd64_audio", WIN_AUDIO, empty_manifest())?;
download_and_extract(build, "nsis_plugins", NSIS_PLUGINS, empty_manifest())?;
bundle_deps.extend([":extract:win_amd64_audio", ":extract:nsis_plugins"]);
} else if cfg!(target_os = "macos") {
if targetting_macos_arm() {
download_and_extract(build, "mac_arm_audio", MAC_ARM_AUDIO, empty_manifest())?;
download_and_extract(build, "mac_arm_qt6", MAC_ARM_QT6, empty_manifest())?;
bundle_deps.extend([":extract:mac_arm_audio", ":extract:mac_arm_qt6"]);
} else {
download_and_extract(build, "mac_amd_audio", MAC_AMD_AUDIO, empty_manifest())?;
download_and_extract(build, "mac_amd_qt6", MAC_AMD_QT6, empty_manifest())?;
bundle_deps.extend([":extract:mac_amd_audio", ":extract:mac_amd_qt6"]);
}
} else {
download_and_extract(
build,
"linux_qt_plugins",
LINUX_QT_PLUGINS,
empty_manifest(),
)?;
bundle_deps.extend([":extract:linux_qt_plugins"]);
}
build.add_dependency(
"bundle:deps",
inputs![bundle_deps
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()],
);
Ok(())
}
struct Venv {
label: &'static str,
path_without_builddir: &'static str,
}
impl Venv {
fn label_as_target(&self, suffix: &str) -> String {
format!(":{}{suffix}", self.label)
}
}
const PRIMARY_VENV: Venv = Venv {
label: "bundle:pyenv",
path_without_builddir: "bundle/pyenv",
};
fn setup_primary_venv(build: &mut Build) -> Result<()> {
let mut qt6_reqs = inputs![
"python/requirements.bundle.txt",
"python/requirements.qt6_6.txt",
];
if cfg!(windows) {
qt6_reqs = inputs![qt6_reqs, "python/requirements.win.txt"];
}
build.add_action(
PRIMARY_VENV.label,
PythonEnvironment {
folder: PRIMARY_VENV.path_without_builddir,
base_requirements_txt: "python/requirements.base.txt".into(),
requirements_txt: qt6_reqs,
extra_binary_exports: &[],
},
)?;
Ok(())
}
struct InstallAnkiWheels {
venv: Venv,
}
impl BuildAction for InstallAnkiWheels {
fn command(&self) -> &str {
"$pip install --force-reinstall --no-deps $in"
}
fn files(&mut self, build: &mut impl ninja_gen::build::FilesHandle) {
build.add_inputs("pip", inputs![self.venv.label_as_target(":pip")]);
build.add_inputs("in", inputs![":wheels"]);
build.add_output_stamp("bundle/wheels.stamp");
}
}
fn install_anki_wheels(build: &mut Build) -> Result<()> {
build.add_action(
"bundle:add_wheels:qt6",
InstallAnkiWheels { venv: PRIMARY_VENV },
)?;
Ok(())
}
fn build_pyoxidizer(build: &mut Build) -> Result<()> {
let offline_build = env::var("OFFLINE_BUILD").is_ok();
build.add_action(
"bundle:pyoxidizer:repo",
SyncSubmodule {
path: "qt/bundle/PyOxidizer",
offline_build,
},
)?;
let target =
overriden_rust_target_triple().unwrap_or_else(|| Platform::current().as_rust_triple());
let output_bin = format!("bundle/rust/{target}/release/pyoxidizer",);
build.add_action(
"bundle:pyoxidizer:bin",
CargoBuild {
inputs: inputs![
":bundle:pyoxidizer:repo",
"out/env",
glob!["qt/bundle/PyOxidizer/**"]
],
// can't use ::Binary() here, as we're in a separate workspace
outputs: &[RustOutput::Data("bin", &with_exe(&output_bin))],
target: Some(target),
extra_args: &format!(
"--manifest-path={} --target-dir={} -p pyoxidizer",
"qt/bundle/PyOxidizer/Cargo.toml", "$builddir/bundle/rust"
),
release_override: Some(BuildProfile::Release),
},
)?;
Ok(())
}
struct BuildArtifacts {}
impl BuildAction for BuildArtifacts {
fn command(&self) -> &str {
"$runner build-artifacts $bundle_root $pyoxidizer_bin"
}
fn files(&mut self, build: &mut impl ninja_gen::build::FilesHandle) {
build.add_inputs("pyoxidizer_bin", inputs![":bundle:pyoxidizer:bin"]);
build.add_inputs("", inputs![PRIMARY_VENV.label_as_target("")]);
build.add_inputs("", inputs![":bundle:add_wheels:qt6", glob!["qt/bundle/**"]]);
build.add_variable("bundle_root", "$builddir/bundle");
build.add_outputs_ext(
"pyo3_config",
vec!["bundle/artifacts/pyo3-build-config-file.txt"],
true,
);
}
fn check_output_timestamps(&self) -> bool {
true
}
}
fn build_artifacts(build: &mut Build) -> Result<()> {
build.add_action("bundle:artifacts", BuildArtifacts {})
}
struct BuildBundle {}
impl BuildAction for BuildBundle {
fn command(&self) -> &str {
"$runner build-bundle-binary"
}
fn files(&mut self, build: &mut impl ninja_gen::build::FilesHandle) {
build.add_inputs("", inputs![":bundle:artifacts", glob!["qt/bundle/**"]]);
build.add_outputs(
"",
vec![RustOutput::Binary("anki").path(
Utf8Path::new("$builddir/bundle/rust"),
Some(
overriden_rust_target_triple()
.unwrap_or_else(|| Platform::current().as_rust_triple()),
),
// our pyoxidizer bin uses lto on the release profile
BuildProfile::Release,
)],
);
}
}
fn build_binary(build: &mut Build) -> Result<()> {
build.add_action("bundle:binary", BuildBundle {})
}
struct BuildDistFolder {
kind: DistKind,
deps: BuildInput,
}
impl BuildAction for BuildDistFolder {
fn command(&self) -> &str {
"$runner build-dist-folder $kind $out_folder "
}
fn files(&mut self, build: &mut impl ninja_gen::build::FilesHandle) {
build.add_inputs("", &self.deps);
build.add_variable("kind", self.kind.name());
let folder = match self.kind {
DistKind::Standard => "bundle/std",
};
build.add_outputs("out_folder", vec![folder]);
build.add_outputs("stamp", vec![format!("{folder}.stamp")]);
}
fn check_output_timestamps(&self) -> bool {
true
}
}
fn build_dist_folder(build: &mut Build, kind: DistKind) -> Result<()> {
let deps = inputs![":bundle:deps", ":bundle:binary", glob!["qt/bundle/**"]];
let group = match kind {
DistKind::Standard => "bundle:folder:std",
};
build.add_action(group, BuildDistFolder { kind, deps })
}
fn build_packages(build: &mut Build) -> Result<()> {
if cfg!(windows) {
build_windows_installers(build)
} else if cfg!(target_os = "macos") {
build_mac_app(build, DistKind::Standard)?;
build_dmgs(build)
} else {
build_tarball(build, DistKind::Standard)
}
}
struct BuildTarball {
kind: DistKind,
}
impl BuildAction for BuildTarball {
fn command(&self) -> &str {
"chmod -R a+r $folder && tar -I '$zstd' --transform $transform -cf $tarball -C $folder ."
}
fn files(&mut self, build: &mut impl ninja_gen::build::FilesHandle) {
let input_folder_name = self.kind.folder_name();
let input_folder_target = format!(":bundle:folder:{input_folder_name}");
let input_folder_path = format!("$builddir/bundle/{input_folder_name}");
let version = anki_version();
let qt = match self.kind {
DistKind::Standard => "qt6",
};
let output_folder_base = format!("anki-{version}-linux-{qt}");
let output_tarball = format!("bundle/package/{output_folder_base}.tar.zst");
build.add_inputs("", inputs![input_folder_target]);
build.add_variable("zstd", "zstd -c --long -T0 -18");
build.add_variable("transform", format!("s%^.%{output_folder_base}%S"));
build.add_variable("folder", input_folder_path);
build.add_outputs("tarball", vec![output_tarball]);
}
}
fn build_tarball(build: &mut Build, kind: DistKind) -> Result<()> {
let name = kind.folder_name();
build.add_action(format!("bundle:package:{name}"), BuildTarball { kind })
}
struct BuildWindowsInstallers {}
impl BuildAction for BuildWindowsInstallers {
fn command(&self) -> &str {
"cargo run -p makeexe --target-dir=out/rust -- $version $src_root $bundle_root $out"
}
fn files(&mut self, build: &mut impl ninja_gen::build::FilesHandle) {
let version = anki_version();
let outputs = ["qt6"].iter().map(|qt| {
let output_base = format!("anki-{version}-windows-{qt}");
format!("bundle/package/{output_base}.exe")
});
build.add_inputs("", inputs![":bundle:folder:std"]);
build.add_variable("version", &version);
build.add_variable("bundle_root", "$builddir/bundle");
build.add_outputs("out", outputs);
}
}
fn build_windows_installers(build: &mut Build) -> Result<()> {
build.add_action("bundle:package", BuildWindowsInstallers {})
}
struct BuildMacApp {
kind: DistKind,
}
impl BuildAction for BuildMacApp {
fn command(&self) -> &str {
"cargo run -p makeapp --target-dir=out/rust -- build-app $version $kind $stamp"
}
fn files(&mut self, build: &mut impl ninja_gen::build::FilesHandle) {
let folder_name = self.kind.folder_name();
build.add_inputs("", inputs![format!(":bundle:folder:{folder_name}")]);
build.add_variable("version", anki_version());
build.add_variable("kind", self.kind.name());
build.add_outputs("stamp", vec![format!("bundle/app/{folder_name}.stamp")]);
}
}
fn build_mac_app(build: &mut Build, kind: DistKind) -> Result<()> {
build.add_action(format!("bundle:app:{}", kind.name()), BuildMacApp { kind })
}
struct BuildDmgs {}
impl BuildAction for BuildDmgs {
fn command(&self) -> &str {
"cargo run -p makeapp --target-dir=out/rust -- build-dmgs $dmgs"
}
fn files(&mut self, build: &mut impl ninja_gen::build::FilesHandle) {
let version = anki_version();
let platform = if targetting_macos_arm() {
"apple"
} else {
"intel"
};
let qt = &["qt6"][..];
let dmgs = qt
.iter()
.map(|qt| format!("bundle/dmg/anki-{version}-mac-{platform}-{qt}.dmg"));
build.add_inputs("", inputs![":bundle:app"]);
build.add_outputs("dmgs", dmgs);
}
}
fn build_dmgs(build: &mut Build) -> Result<()> {
build.add_action("bundle:dmg", BuildDmgs {})
}

View file

@ -0,0 +1,44 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use anyhow::Result;
use ninja_gen::archives::download_and_extract;
use ninja_gen::archives::empty_manifest;
use ninja_gen::archives::OnlineArchive;
use ninja_gen::command::RunCommand;
use ninja_gen::hashmap;
use ninja_gen::inputs;
use ninja_gen::Build;
pub fn setup_uv_universal(build: &mut Build) -> Result<()> {
if !cfg!(target_arch = "aarch64") {
return Ok(());
}
build.add_action(
"launcher:uv_universal",
RunCommand {
command: "/usr/bin/lipo",
args: "-create -output $out $arm_bin $x86_bin",
inputs: hashmap! {
"arm_bin" => inputs![":extract:uv:bin"],
"x86_bin" => inputs![":extract:uv_mac_x86:bin"],
},
outputs: hashmap! {
"out" => vec!["launcher/uv"],
},
},
)
}
pub fn build_launcher(build: &mut Build) -> Result<()> {
setup_uv_universal(build)?;
download_and_extract(build, "nsis_plugins", NSIS_PLUGINS, empty_manifest())?;
Ok(())
}
const NSIS_PLUGINS: OnlineArchive = OnlineArchive {
url: "https://github.com/ankitects/anki-bundle-extras/releases/download/anki-2023-05-19/nsis.tar.zst",
sha256: "6133f730ece699de19714d0479c73bc848647d277e9cc80dda9b9ebe532b40a8",
};

View file

@ -2,7 +2,7 @@
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
mod aqt; mod aqt;
mod bundle; mod launcher;
mod platform; mod platform;
mod pylib; mod pylib;
mod python; mod python;
@ -13,13 +13,14 @@ use std::env;
use anyhow::Result; use anyhow::Result;
use aqt::build_and_check_aqt; use aqt::build_and_check_aqt;
use bundle::build_bundle; use launcher::build_launcher;
use ninja_gen::glob; use ninja_gen::glob;
use ninja_gen::inputs; use ninja_gen::inputs;
use ninja_gen::protobuf::check_proto; use ninja_gen::protobuf::check_proto;
use ninja_gen::protobuf::setup_protoc; use ninja_gen::protobuf::setup_protoc;
use ninja_gen::python::setup_python; use ninja_gen::python::setup_uv;
use ninja_gen::Build; use ninja_gen::Build;
use platform::overriden_python_venv_platform;
use pylib::build_pylib; use pylib::build_pylib;
use pylib::check_pylib; use pylib::check_pylib;
use python::check_python; use python::check_python;
@ -47,7 +48,10 @@ fn main() -> Result<()> {
check_proto(build, inputs![glob!["proto/**/*.proto"]])?; check_proto(build, inputs![glob!["proto/**/*.proto"]])?;
if env::var("OFFLINE_BUILD").is_err() { if env::var("OFFLINE_BUILD").is_err() {
setup_python(build)?; setup_uv(
build,
overriden_python_venv_platform().unwrap_or(build.host_platform),
)?;
} }
setup_venv(build)?; setup_venv(build)?;
@ -57,7 +61,7 @@ fn main() -> Result<()> {
build_and_check_aqt(build)?; build_and_check_aqt(build)?;
if env::var("OFFLINE_BUILD").is_err() { if env::var("OFFLINE_BUILD").is_err() {
build_bundle(build)?; build_launcher(build)?;
} }
setup_sphinx(build)?; setup_sphinx(build)?;

View file

@ -5,18 +5,30 @@ use std::env;
use ninja_gen::archives::Platform; use ninja_gen::archives::Platform;
/// Usually None to use the host architecture; can be overriden by setting /// Please see [`overriden_python_target_platform()`] for details.
/// MAC_X86 to build for x86_64 on Apple Silicon
pub fn overriden_rust_target_triple() -> Option<&'static str> { 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; can be overriden by setting /// Usually None to use the host architecture, except on Windows which
/// MAC_X86 to build for x86_64 on Apple Silicon /// always uses x86_64, since WebEngine is unavailable for ARM64.
pub fn overriden_python_target_platform() -> Option<Platform> { pub fn overriden_python_venv_platform() -> Option<Platform> {
if env::var("MAC_X86").is_ok() { if cfg!(target_os = "windows") {
Some(Platform::MacX64) Some(Platform::WindowsX64)
} else { } else {
None 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()
}
}

View file

@ -14,7 +14,7 @@ use ninja_gen::python::PythonTest;
use ninja_gen::Build; use ninja_gen::Build;
use crate::anki_version; 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::BuildWheel;
use crate::python::GenPythonProto; use crate::python::GenPythonProto;
@ -50,7 +50,7 @@ pub fn build_pylib(build: &mut Build) -> Result<()> {
output: &format!( output: &format!(
"pylib/anki/_rsbridge.{}", "pylib/anki/_rsbridge.{}",
match build.host_platform { match build.host_platform {
Platform::WindowsX64 => "pyd", Platform::WindowsX64 | Platform::WindowsArm => "pyd",
_ => "so", _ => "so",
} }
), ),
@ -64,13 +64,11 @@ pub fn build_pylib(build: &mut Build) -> Result<()> {
BuildWheel { BuildWheel {
name: "anki", name: "anki",
version: anki_version(), version: anki_version(),
src_folder: "pylib/anki", platform: overriden_python_wheel_platform().or(Some(build.host_platform)),
gen_folder: "$builddir/pylib/anki",
platform: overriden_python_target_platform().or(Some(build.host_platform)),
deps: inputs![ deps: inputs![
":pylib:anki", ":pylib:anki",
glob!("pylib/anki/**"), glob!("pylib/anki/**"),
"python/requirements.anki.in", "pylib/pyproject.toml"
], ],
}, },
)?; )?;

View file

@ -20,74 +20,27 @@ use ninja_gen::python::PythonTypecheck;
use ninja_gen::rsync::RsyncFiles; use ninja_gen::rsync::RsyncFiles;
use ninja_gen::Build; use ninja_gen::Build;
// When updating Qt, make sure to update the .txt file in bundle.rs as well.
pub fn setup_venv(build: &mut Build) -> Result<()> { pub fn setup_venv(build: &mut Build) -> Result<()> {
let platform_deps = if cfg!(windows) { let extra_binary_exports = &[
inputs![ "mypy",
"python/requirements.qt6_6.txt", "black",
"python/requirements.win.txt", "isort",
] "pylint",
} else if cfg!(target_os = "macos") { "pytest",
inputs!["python/requirements.qt6_6.txt",] "protoc-gen-mypy",
} else if std::env::var("PYTHONPATH").is_ok() { ];
// assume we have a system-provided Qt
inputs![]
} else if cfg!(target_arch = "aarch64") {
inputs!["python/requirements.qt6_8.txt"]
} else {
inputs!["python/requirements.qt6_6.txt"]
};
let requirements_txt = inputs!["python/requirements.dev.txt", platform_deps];
build.add_action( build.add_action(
"pyenv", "pyenv",
PythonEnvironment { PythonEnvironment {
folder: "pyenv", venv_folder: "pyenv",
base_requirements_txt: inputs!["python/requirements.base.txt"], deps: inputs![
requirements_txt, "pyproject.toml",
extra_binary_exports: &[ "pylib/pyproject.toml",
"pip-compile", "qt/pyproject.toml",
"pip-sync", "uv.lock"
"mypy",
"black", // Required for offline build
"isort",
"pylint",
"pytest",
"protoc-gen-mypy", // ditto
], ],
}, extra_args: "--all-packages --extra qt --extra audio",
)?; extra_binary_exports,
// optional venvs for testing other Qt versions
let mut venv_reqs = inputs!["python/requirements.bundle.txt"];
if cfg!(windows) {
venv_reqs = inputs![venv_reqs, "python/requirements.win.txt"];
}
build.add_action(
"pyenv-qt6.8",
PythonEnvironment {
folder: "pyenv-qt6.8",
base_requirements_txt: inputs!["python/requirements.base.txt"],
requirements_txt: inputs![&venv_reqs, "python/requirements.qt6_8.txt"],
extra_binary_exports: &[],
},
)?;
build.add_action(
"pyenv-qt5.15",
PythonEnvironment {
folder: "pyenv-qt5.15",
base_requirements_txt: inputs!["python/requirements.base.txt"],
requirements_txt: inputs![&venv_reqs, "python/requirements.qt5_15.txt"],
extra_binary_exports: &[],
},
)?;
build.add_action(
"pyenv-qt5.14",
PythonEnvironment {
folder: "pyenv-qt5.14",
base_requirements_txt: inputs!["python/requirements.base.txt"],
requirements_txt: inputs![venv_reqs, "python/requirements.qt5_14.txt"],
extra_binary_exports: &[],
}, },
)?; )?;
@ -133,45 +86,66 @@ impl BuildAction for GenPythonProto {
pub struct BuildWheel { pub struct BuildWheel {
pub name: &'static str, pub name: &'static str,
pub version: String, pub version: String,
pub src_folder: &'static str,
pub gen_folder: &'static str,
pub platform: Option<Platform>, pub platform: Option<Platform>,
pub deps: BuildInput, pub deps: BuildInput,
} }
impl BuildAction for BuildWheel { impl BuildAction for BuildWheel {
fn command(&self) -> &str { fn command(&self) -> &str {
"$pyenv_bin $script $src $gen $out" "$uv build --wheel --out-dir=$out_dir --project=$project_dir"
} }
fn files(&mut self, build: &mut impl FilesHandle) { fn files(&mut self, build: &mut impl FilesHandle) {
build.add_inputs("pyenv_bin", inputs![":pyenv:bin"]); build.add_inputs("uv", inputs![":uv_binary"]);
build.add_inputs("script", inputs!["python/write_wheel.py"]);
build.add_inputs("", &self.deps); build.add_inputs("", &self.deps);
build.add_variable("src", self.src_folder);
build.add_variable("gen", self.gen_folder);
// Set the project directory based on which package we're building
let project_dir = if self.name == "anki" { "pylib" } else { "qt" };
build.add_variable("project_dir", project_dir);
// Set environment variable for uv to use our pyenv
build.add_variable("pyenv_path", "$builddir/pyenv");
build.add_env_var("UV_PROJECT_ENVIRONMENT", "$pyenv_path");
// Set output directory
build.add_variable("out_dir", "$builddir/wheels/");
// Calculate the wheel filename that uv will generate
let tag = if let Some(platform) = self.platform { let tag = if let Some(platform) = self.platform {
let platform = match platform { let platform_tag = match platform {
Platform::LinuxX64 => "manylinux_2_35_x86_64", Platform::LinuxX64 => "manylinux_2_36_x86_64",
Platform::LinuxArm => "manylinux_2_35_aarch64", Platform::LinuxArm => "manylinux_2_36_aarch64",
Platform::MacX64 => "macosx_12_0_x86_64", Platform::MacX64 => "macosx_12_0_x86_64",
Platform::MacArm => "macosx_12_0_arm64", Platform::MacArm => "macosx_12_0_arm64",
Platform::WindowsX64 => "win_amd64", Platform::WindowsX64 => "win_amd64",
Platform::WindowsArm => "win_arm64",
}; };
format!("cp39-abi3-{platform}") format!("cp39-abi3-{platform_tag}")
} else { } else {
"py3-none-any".into() "py3-none-any".into()
}; };
// Set environment variable for hatch_build.py to use the correct platform tag
build.add_variable("wheel_tag", &tag);
build.add_env_var("ANKI_WHEEL_TAG", "$wheel_tag");
let name = self.name; let name = self.name;
let version = &self.version;
let wheel_path = format!("wheels/{name}-{version}-{tag}.whl"); // Normalize version like hatchling does: remove leading zeros from version
// parts
let normalized_version = self
.version
.split('.')
.map(|part| part.parse::<u32>().unwrap_or(0).to_string())
.collect::<Vec<_>>()
.join(".");
let wheel_path = format!("wheels/{name}-{normalized_version}-{tag}.whl");
build.add_outputs("out", vec![wheel_path]); build.add_outputs("out", vec![wheel_path]);
} }
} }
pub fn check_python(build: &mut Build) -> Result<()> { pub fn check_python(build: &mut Build) -> Result<()> {
python_format(build, "ftl", inputs![glob!("ftl/**/*.py")])?;
python_format(build, "tools", inputs![glob!("tools/**/*.py")])?; python_format(build, "tools", inputs![glob!("tools/**/*.py")])?;
build.add_action( build.add_action(
@ -183,7 +157,6 @@ pub fn check_python(build: &mut Build) -> Result<()> {
"qt/tools", "qt/tools",
"out/pylib/anki", "out/pylib/anki",
"out/qt/_aqt", "out/qt/_aqt",
"ftl",
"python", "python",
"tools", "tools",
], ],
@ -262,8 +235,7 @@ struct Sphinx {
impl BuildAction for Sphinx { impl BuildAction for Sphinx {
fn command(&self) -> &str { fn command(&self) -> &str {
if env::var("OFFLINE_BUILD").is_err() { if env::var("OFFLINE_BUILD").is_err() {
"$pip install sphinx sphinx_rtd_theme sphinx-autoapi \ "$uv sync --extra sphinx && $python python/sphinx/build.py"
&& $python python/sphinx/build.py"
} else { } else {
"$python python/sphinx/build.py" "$python python/sphinx/build.py"
} }
@ -271,7 +243,10 @@ impl BuildAction for Sphinx {
fn files(&mut self, build: &mut impl FilesHandle) { fn files(&mut self, build: &mut impl FilesHandle) {
if env::var("OFFLINE_BUILD").is_err() { if env::var("OFFLINE_BUILD").is_err() {
build.add_inputs("pip", inputs![":pyenv:pip"]); build.add_inputs("uv", inputs![":uv_binary"]);
// Set environment variable to use the existing pyenv
build.add_variable("pyenv_path", "$builddir/pyenv");
build.add_env_var("UV_PROJECT_ENVIRONMENT", "$pyenv_path");
} }
build.add_inputs("python", inputs![":pyenv:bin"]); build.add_inputs("python", inputs![":pyenv:bin"]);
build.add_inputs("", &self.deps); build.add_inputs("", &self.deps);
@ -294,7 +269,12 @@ pub(crate) fn setup_sphinx(build: &mut Build) -> Result<()> {
build.add_action( build.add_action(
"python:sphinx", "python:sphinx",
Sphinx { Sphinx {
deps: inputs![":pylib", ":qt", ":python:sphinx:copy_conf"], deps: inputs![
":pylib",
":qt",
":python:sphinx:copy_conf",
"pyproject.toml"
],
}, },
)?; )?;
Ok(()) Ok(())

View file

@ -154,7 +154,7 @@ fn build_rsbridge(build: &mut Build) -> Result<()> {
"$builddir/buildhash", "$builddir/buildhash",
// building on Windows requires python3.lib // building on Windows requires python3.lib
if cfg!(windows) { if cfg!(windows) {
inputs![":extract:python"] inputs![":pyenv:bin"]
} else { } else {
inputs![] inputs![]
} }
@ -247,7 +247,7 @@ pub fn check_minilints(build: &mut Build) -> Result<()> {
let files = inputs![ let files = inputs![
glob![ glob![
"**/*.{py,rs,ts,svelte,mjs,md}", "**/*.{py,rs,ts,svelte,mjs,md}",
"{node_modules,qt/bundle/PyOxidizer,ts/.svelte-kit}/**" "{node_modules,ts/.svelte-kit}/**"
], ],
"Cargo.lock" "Cargo.lock"
]; ];

View file

@ -16,5 +16,22 @@ globset.workspace = true
itertools.workspace = true itertools.workspace = true
maplit.workspace = true maplit.workspace = true
num_cpus.workspace = true num_cpus.workspace = true
regex.workspace = true
serde_json.workspace = true
sha2.workspace = true
walkdir.workspace = true walkdir.workspace = true
which.workspace = true which.workspace = true
[target.'cfg(windows)'.dependencies]
reqwest = { workspace = true, features = ["blocking", "json", "native-tls"] }
[target.'cfg(not(windows))'.dependencies]
reqwest = { workspace = true, features = ["blocking", "json", "rustls-tls"] }
[[bin]]
name = "update_uv"
path = "src/bin/update_uv.rs"
[[bin]]
name = "update_protoc"
path = "src/bin/update_protoc.rs"

View file

@ -26,22 +26,21 @@ pub enum Platform {
MacX64, MacX64,
MacArm, MacArm,
WindowsX64, WindowsX64,
WindowsArm,
} }
impl Platform { impl Platform {
pub fn current() -> Self { pub fn current() -> Self {
if cfg!(windows) { let os = std::env::consts::OS;
Self::WindowsX64 let arch = std::env::consts::ARCH;
} else { match (os, arch) {
let os = std::env::consts::OS; ("linux", "x86_64") => Self::LinuxX64,
let arch = std::env::consts::ARCH; ("linux", "aarch64") => Self::LinuxArm,
match (os, arch) { ("macos", "x86_64") => Self::MacX64,
("linux", "x86_64") => Self::LinuxX64, ("macos", "aarch64") => Self::MacArm,
("linux", "aarch64") => Self::LinuxArm, ("windows", "x86_64") => Self::WindowsX64,
("macos", "x86_64") => Self::MacX64, ("windows", "aarch64") => Self::WindowsArm,
("macos", "aarch64") => Self::MacArm, _ => panic!("unsupported os/arch {os} {arch} - PR welcome!"),
_ => panic!("unsupported os/arch {os} {arch} - PR welcome!"),
}
} }
} }
@ -62,6 +61,7 @@ impl Platform {
Platform::MacX64 => "x86_64-apple-darwin", Platform::MacX64 => "x86_64-apple-darwin",
Platform::MacArm => "aarch64-apple-darwin", Platform::MacArm => "aarch64-apple-darwin",
Platform::WindowsX64 => "x86_64-pc-windows-msvc", Platform::WindowsX64 => "x86_64-pc-windows-msvc",
Platform::WindowsArm => "aarch64-pc-windows-msvc",
} }
} }
} }

View file

@ -0,0 +1,126 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use std::error::Error;
use std::fs;
use std::path::Path;
use regex::Regex;
use reqwest::blocking::Client;
use serde_json::Value;
use sha2::Digest;
use sha2::Sha256;
fn fetch_protoc_release_info() -> Result<String, Box<dyn Error>> {
let client = Client::new();
println!("Fetching latest protoc release info from GitHub...");
// Fetch latest release info
let response = client
.get("https://api.github.com/repos/protocolbuffers/protobuf/releases/latest")
.header("User-Agent", "Anki-Build-Script")
.send()?;
let release_info: Value = response.json()?;
let assets = release_info["assets"]
.as_array()
.expect("assets should be an array");
// Map platform names to their corresponding asset patterns
let platform_patterns = [
("LinuxX64", "linux-x86_64"),
("LinuxArm", "linux-aarch_64"),
("MacX64", "osx-universal_binary"), // Mac uses universal binary for both
("MacArm", "osx-universal_binary"),
("WindowsX64", "win64"), // Windows uses x86 binary for both archs
("WindowsArm", "win64"),
];
let mut match_blocks = Vec::new();
for (platform, pattern) in platform_patterns {
// Find the asset matching the platform pattern
let asset = assets.iter().find(|asset| {
let name = asset["name"].as_str().unwrap_or("");
name.starts_with("protoc-") && name.contains(pattern) && name.ends_with(".zip")
});
if asset.is_none() {
eprintln!("No asset found for platform {platform} pattern {pattern}");
continue;
}
let asset = asset.unwrap();
let download_url = asset["browser_download_url"].as_str().unwrap();
let asset_name = asset["name"].as_str().unwrap();
// Download the file and calculate SHA256 locally
println!("Downloading and checksumming {asset_name} for {platform}...");
let response = client
.get(download_url)
.header("User-Agent", "Anki-Build-Script")
.send()?;
let bytes = response.bytes()?;
let mut hasher = Sha256::new();
hasher.update(&bytes);
let sha256 = format!("{:x}", hasher.finalize());
// Handle platform-specific match patterns
let match_pattern = match platform {
"MacX64" => "Platform::MacX64 | Platform::MacArm",
"MacArm" => continue, // Skip MacArm since it's handled with MacX64
"WindowsX64" => "Platform::WindowsX64 | Platform::WindowsArm",
"WindowsArm" => continue, // Skip WindowsArm since it's handled with WindowsX64
_ => &format!("Platform::{}", platform),
};
match_blocks.push(format!(
" {} => {{\n OnlineArchive {{\n url: \"{}\",\n sha256: \"{}\",\n }}\n }}",
match_pattern, download_url, sha256
));
}
Ok(format!(
"pub fn protoc_archive(platform: Platform) -> OnlineArchive {{\n match platform {{\n{}\n }}\n}}",
match_blocks.join(",\n")
))
}
fn read_protobuf_rs() -> Result<String, Box<dyn Error>> {
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());
let path = Path::new(&manifest_dir).join("src/protobuf.rs");
println!("Reading {}", path.display());
let content = fs::read_to_string(path)?;
Ok(content)
}
fn update_protoc_text(old_text: &str, new_protoc_text: &str) -> Result<String, Box<dyn Error>> {
let re =
Regex::new(r"(?ms)^pub fn protoc_archive\(platform: Platform\) -> OnlineArchive \{.*?\n\}")
.unwrap();
if !re.is_match(old_text) {
return Err("Could not find protoc_archive function block to replace".into());
}
let new_content = re.replace(old_text, new_protoc_text).to_string();
println!("Original lines: {}", old_text.lines().count());
println!("Updated lines: {}", new_content.lines().count());
Ok(new_content)
}
fn write_protobuf_rs(content: &str) -> Result<(), Box<dyn Error>> {
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());
let path = Path::new(&manifest_dir).join("src/protobuf.rs");
println!("Writing to {}", path.display());
fs::write(path, content)?;
Ok(())
}
fn main() -> Result<(), Box<dyn Error>> {
let new_protoc_archive = fetch_protoc_release_info()?;
let content = read_protobuf_rs()?;
let updated_content = update_protoc_text(&content, &new_protoc_archive)?;
write_protobuf_rs(&updated_content)?;
println!("Successfully updated protoc_archive function in protobuf.rs");
Ok(())
}

View file

@ -0,0 +1,144 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use std::error::Error;
use std::fs;
use std::path::Path;
use regex::Regex;
use reqwest::blocking::Client;
use serde_json::Value;
fn fetch_uv_release_info() -> Result<String, Box<dyn Error>> {
let client = Client::new();
println!("Fetching latest uv release info from GitHub...");
// Fetch latest release info
let response = client
.get("https://api.github.com/repos/astral-sh/uv/releases/latest")
.header("User-Agent", "Anki-Build-Script")
.send()?;
let release_info: Value = response.json()?;
let assets = release_info["assets"]
.as_array()
.expect("assets should be an array");
// Map platform names to their corresponding asset patterns
let platform_patterns = [
("LinuxX64", "x86_64-unknown-linux-gnu"),
("LinuxArm", "aarch64-unknown-linux-gnu"),
("MacX64", "x86_64-apple-darwin"),
("MacArm", "aarch64-apple-darwin"),
("WindowsX64", "x86_64-pc-windows-msvc"),
("WindowsArm", "aarch64-pc-windows-msvc"),
];
let mut match_blocks = Vec::new();
for (platform, pattern) in platform_patterns {
// Find the asset matching the platform pattern (the binary)
let asset = assets.iter().find(|asset| {
let name = asset["name"].as_str().unwrap_or("");
name.contains(pattern) && (name.ends_with(".tar.gz") || name.ends_with(".zip"))
});
if asset.is_none() {
eprintln!("No asset found for platform {platform} pattern {pattern}");
continue;
}
let asset = asset.unwrap();
let download_url = asset["browser_download_url"].as_str().unwrap();
let asset_name = asset["name"].as_str().unwrap();
// Find the corresponding .sha256 or .sha256sum asset
let sha_asset = assets.iter().find(|a| {
let name = a["name"].as_str().unwrap_or("");
name == format!("{}.sha256", asset_name) || name == format!("{}.sha256sum", asset_name)
});
if sha_asset.is_none() {
eprintln!("No sha256 asset found for {asset_name}");
continue;
}
let sha_asset = sha_asset.unwrap();
let sha_url = sha_asset["browser_download_url"].as_str().unwrap();
println!("Fetching SHA256 for {platform}...");
let sha_text = client
.get(sha_url)
.header("User-Agent", "Anki-Build-Script")
.send()?
.text()?;
// The sha file is usually of the form: "<sha256> <filename>"
let sha256 = sha_text.split_whitespace().next().unwrap_or("");
match_blocks.push(format!(
" Platform::{} => {{\n OnlineArchive {{\n url: \"{}\",\n sha256: \"{}\",\n }}\n }}",
platform, download_url, sha256
));
}
Ok(format!(
"pub fn uv_archive(platform: Platform) -> OnlineArchive {{\n match platform {{\n{}\n }}",
match_blocks.join(",\n")
))
}
fn read_python_rs() -> Result<String, Box<dyn Error>> {
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());
let path = Path::new(&manifest_dir).join("src/python.rs");
println!("Reading {}", path.display());
let content = fs::read_to_string(path)?;
Ok(content)
}
fn update_uv_text(old_text: &str, new_uv_text: &str) -> Result<String, Box<dyn Error>> {
let re = Regex::new(r"(?ms)^pub fn uv_archive\(platform: Platform\) -> OnlineArchive \{.*?\n\s*\}\s*\n\s*\}\s*\n\s*\}").unwrap();
if !re.is_match(old_text) {
return Err("Could not find uv_archive function block to replace".into());
}
let new_content = re.replace(old_text, new_uv_text).to_string();
println!("Original lines: {}", old_text.lines().count());
println!("Updated lines: {}", new_content.lines().count());
Ok(new_content)
}
fn write_python_rs(content: &str) -> Result<(), Box<dyn Error>> {
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());
let path = Path::new(&manifest_dir).join("src/python.rs");
println!("Writing to {}", path.display());
fs::write(path, content)?;
Ok(())
}
fn main() -> Result<(), Box<dyn Error>> {
let new_uv_archive = fetch_uv_release_info()?;
let content = read_python_rs()?;
let updated_content = update_uv_text(&content, &new_uv_archive)?;
write_python_rs(&updated_content)?;
println!("Successfully updated uv_archive function in python.rs");
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_update_uv_text_with_actual_file() {
let content = fs::read_to_string("src/python.rs").unwrap();
let original_lines = content.lines().count();
const EXPECTED_LINES_REMOVED: usize = 38;
let updated = update_uv_text(&content, "").unwrap();
let updated_lines = updated.lines().count();
assert_eq!(
updated_lines,
original_lines - EXPECTED_LINES_REMOVED,
"Expected line count to decrease by exactly {} lines (original: {}, updated: {})",
EXPECTED_LINES_REMOVED,
original_lines,
updated_lines
);
}
}

View file

@ -38,6 +38,10 @@ pub fn node_archive(platform: Platform) -> OnlineArchive {
url: "https://nodejs.org/dist/v20.11.0/node-v20.11.0-win-x64.zip", url: "https://nodejs.org/dist/v20.11.0/node-v20.11.0-win-x64.zip",
sha256: "893115cd92ad27bf178802f15247115e93c0ef0c753b93dca96439240d64feb5", sha256: "893115cd92ad27bf178802f15247115e93c0ef0c753b93dca96439240d64feb5",
}, },
Platform::WindowsArm => OnlineArchive {
url: "https://nodejs.org/dist/v20.11.0/node-v20.11.0-win-arm64.zip",
sha256: "89c1f7034dcd6ff5c17f2af61232a96162a1902f862078347dcf274a938b6142",
},
} }
} }

View file

@ -21,26 +21,26 @@ pub fn protoc_archive(platform: Platform) -> OnlineArchive {
match platform { match platform {
Platform::LinuxX64 => { Platform::LinuxX64 => {
OnlineArchive { OnlineArchive {
url: "https://github.com/protocolbuffers/protobuf/releases/download/v21.8/protoc-21.8-linux-x86_64.zip", url: "https://github.com/protocolbuffers/protobuf/releases/download/v31.1/protoc-31.1-linux-x86_64.zip",
sha256: "f90d0dd59065fef94374745627336d622702b67f0319f96cee894d41a974d47a", sha256: "96553041f1a91ea0efee963cb16f462f5985b4d65365f3907414c360044d8065",
} }
} },
Platform::LinuxArm => { Platform::LinuxArm => {
OnlineArchive { OnlineArchive {
url: "https://github.com/protocolbuffers/protobuf/releases/download/v21.8/protoc-21.8-linux-aarch_64.zip", url: "https://github.com/protocolbuffers/protobuf/releases/download/v31.1/protoc-31.1-linux-aarch_64.zip",
sha256: "f3d8eb5839d6186392d8c7b54fbeabbb6fcdd90618a500b77cb2e24faa245cad", sha256: "6c554de11cea04c56ebf8e45b54434019b1cd85223d4bbd25c282425e306ecc2",
} }
} },
Platform::MacX64 | Platform::MacArm => { Platform::MacX64 | Platform::MacArm => {
OnlineArchive { OnlineArchive {
url: "https://github.com/protocolbuffers/protobuf/releases/download/v21.8/protoc-21.8-osx-universal_binary.zip", url: "https://github.com/protocolbuffers/protobuf/releases/download/v31.1/protoc-31.1-osx-universal_binary.zip",
sha256: "e3324d3bc2e9bc967a0bec2472e0ec73b26f952c7c87f2403197414f780c3c6c", sha256: "99ea004549c139f46da5638187a85bbe422d78939be0fa01af1aa8ab672e395f",
} }
} },
Platform::WindowsX64 => { Platform::WindowsX64 | Platform::WindowsArm => {
OnlineArchive { OnlineArchive {
url: "https://github.com/protocolbuffers/protobuf/releases/download/v21.8/protoc-21.8-win64.zip", url: "https://github.com/protocolbuffers/protobuf/releases/download/v31.1/protoc-31.1-win64.zip",
sha256: "3657053024faa439ff5f8c1dd2ee06bac0f9b9a3d660e99944f015a7451e87ec", sha256: "70381b116ab0d71cb6a5177d9b17c7c13415866603a0fd40d513dafe32d56c35",
} }
} }
} }
@ -67,7 +67,7 @@ fn clang_format_archive(platform: Platform) -> OnlineArchive {
sha256: "238be68d9478163a945754f06a213483473044f5a004c4125d3d9d8d3556466e", sha256: "238be68d9478163a945754f06a213483473044f5a004c4125d3d9d8d3556466e",
} }
} }
Platform::WindowsX64 => { Platform::WindowsX64 | Platform::WindowsArm=> {
OnlineArchive { OnlineArchive {
url: "https://github.com/ankitects/clang-format-binaries/releases/download/anki-2021-01-09/clang-format_windows_x86_64.zip", url: "https://github.com/ankitects/clang-format-binaries/releases/download/anki-2021-01-09/clang-format_windows_x86_64.zip",
sha256: "7d9f6915e3f0fb72407830f0fc37141308d2e6915daba72987a52f309fbeaccc", sha256: "7d9f6915e3f0fb72407830f0fc37141308d2e6915daba72987a52f309fbeaccc",

View file

@ -9,6 +9,7 @@ use maplit::hashmap;
use crate::action::BuildAction; use crate::action::BuildAction;
use crate::archives::download_and_extract; use crate::archives::download_and_extract;
use crate::archives::with_exe;
use crate::archives::OnlineArchive; use crate::archives::OnlineArchive;
use crate::archives::Platform; use crate::archives::Platform;
use crate::hash::simple_hash; use crate::hash::simple_hash;
@ -16,82 +17,113 @@ use crate::input::BuildInput;
use crate::inputs; use crate::inputs;
use crate::Build; use crate::Build;
/// When updating this, pyoxidizer.bzl needs updating too, but it uses different // To update, run 'cargo run --bin update_uv'.
/// files. // You'll need to do this when bumping Python versions, as uv bakes in
pub fn python_archive(platform: Platform) -> OnlineArchive { // the latest known version.
// When updating Python version, make sure to update version tag in BuildWheel
// too.
pub fn uv_archive(platform: Platform) -> OnlineArchive {
match platform { match platform {
Platform::LinuxX64 => { Platform::LinuxX64 => {
OnlineArchive { OnlineArchive {
url: "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.9.18+20240107-x86_64_v2-unknown-linux-gnu-install_only.tar.gz", url: "https://github.com/astral-sh/uv/releases/download/0.7.13/uv-x86_64-unknown-linux-gnu.tar.gz",
sha256: "9426bca501ae0a257392b10719e2e20ff5fa5e22a3ce4599d6ad0b3139f86417", sha256: "909278eb197c5ed0e9b5f16317d1255270d1f9ea4196e7179ce934d48c4c2545",
} }
} },
Platform::LinuxArm => { Platform::LinuxArm => {
OnlineArchive { OnlineArchive {
url: "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.9.18+20240107-aarch64-unknown-linux-gnu-install_only.tar.gz", url: "https://github.com/astral-sh/uv/releases/download/0.7.13/uv-aarch64-unknown-linux-gnu.tar.gz",
sha256: "7d19e1ecd6e582423f7c74a0c67491eaa982ce9d5c5f35f0e4289f83127abcb8", sha256: "0b2ad9fe4295881615295add8cc5daa02549d29cc9a61f0578e397efcf12f08f",
} }
} },
Platform::MacX64 => { Platform::MacX64 => {
OnlineArchive { OnlineArchive {
url: "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.9.18+20240107-x86_64-apple-darwin-install_only.tar.gz", url: "https://github.com/astral-sh/uv/releases/download/0.7.13/uv-x86_64-apple-darwin.tar.gz",
sha256: "5a0bf895a5cb08d6d008140abb41bb2c8cd638a665273f7d8eb258bc89de439b", sha256: "d785753ac092e25316180626aa691c5dfe1fb075290457ba4fdb72c7c5661321",
} }
} },
Platform::MacArm => { Platform::MacArm => {
OnlineArchive { OnlineArchive {
url: "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.9.18+20240107-aarch64-apple-darwin-install_only.tar.gz", url: "https://github.com/astral-sh/uv/releases/download/0.7.13/uv-aarch64-apple-darwin.tar.gz",
sha256: "bf0cd90204a2cc6da48cae1e4b32f48c9f7031fbe1238c5972104ccb0155d368", sha256: "721f532b73171586574298d4311a91d5ea2c802ef4db3ebafc434239330090c6",
} }
} },
Platform::WindowsX64 => { Platform::WindowsX64 => {
OnlineArchive { OnlineArchive {
url: "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.9.18+20240107-x86_64-pc-windows-msvc-shared-install_only.tar.gz", url: "https://github.com/astral-sh/uv/releases/download/0.7.13/uv-x86_64-pc-windows-msvc.zip",
sha256: "8f0544cd593984f7ecb90c685931249c579302124b9821064873f3a14ed07005", sha256: "e199b10bef1a7cc540014483e7f60f825a174988f41020e9d2a6b01bd60f0669",
}
},
Platform::WindowsArm => {
OnlineArchive {
url: "https://github.com/astral-sh/uv/releases/download/0.7.13/uv-aarch64-pc-windows-msvc.zip",
sha256: "bb40708ad549ad6a12209cb139dd751bf0ede41deb679ce7513ce197bd9ef234",
} }
} }
} }
} }
/// Returns the Python binary, which can be used to create venvs. pub fn setup_uv(build: &mut Build, platform: Platform) -> Result<()> {
/// Downloads if missing. let uv_binary = match env::var("UV_BINARY") {
pub fn setup_python(build: &mut Build) -> Result<()> {
// if changing this, make sure you remove out/pyenv
let python_binary = match env::var("PYTHON_BINARY") {
Ok(path) => { Ok(path) => {
assert!( assert!(
Utf8Path::new(&path).is_absolute(), Utf8Path::new(&path).is_absolute(),
"PYTHON_BINARY must be absolute" "UV_BINARY must be absolute"
); );
path.into() path.into()
} }
Err(_) => { Err(_) => {
download_and_extract( download_and_extract(
build, build,
"python", "uv",
python_archive(build.host_platform), uv_archive(platform),
hashmap! { "bin" => [ hashmap! { "bin" => [
if cfg!(windows) { "python.exe" } else { "bin/python3"} with_exe("uv")
] }, ] },
)?; )?;
inputs![":extract:python:bin"] inputs![":extract:uv:bin"]
} }
}; };
build.add_dependency("python_binary", python_binary); build.add_dependency("uv_binary", uv_binary);
// Our macOS packaging needs access to the x86 binary on ARM.
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(()) Ok(())
} }
pub struct PythonEnvironment { pub struct PythonEnvironment {
pub folder: &'static str, pub deps: BuildInput,
pub base_requirements_txt: BuildInput, // todo: rename
pub requirements_txt: BuildInput, pub venv_folder: &'static str,
pub extra_args: &'static str,
pub extra_binary_exports: &'static [&'static str], pub extra_binary_exports: &'static [&'static str],
} }
impl BuildAction for PythonEnvironment { impl BuildAction for PythonEnvironment {
fn command(&self) -> &str { fn command(&self) -> &str {
if env::var("OFFLINE_BUILD").is_err() { if env::var("OFFLINE_BUILD").is_err() {
"$runner pyenv $python_binary $builddir/$pyenv_folder $system_pkgs $base_requirements $requirements" "$runner pyenv $uv_binary $builddir/$pyenv_folder -- $extra_args"
} else { } else {
"echo 'OFFLINE_BUILD is set. Using the existing PythonEnvironment.'" "echo 'OFFLINE_BUILD is set. Using the existing PythonEnvironment.'"
} }
@ -99,7 +131,7 @@ impl BuildAction for PythonEnvironment {
fn files(&mut self, build: &mut impl crate::build::FilesHandle) { fn files(&mut self, build: &mut impl crate::build::FilesHandle) {
let bin_path = |binary: &str| -> Vec<String> { let bin_path = |binary: &str| -> Vec<String> {
let folder = self.folder; let folder = self.venv_folder;
let path = if cfg!(windows) { let path = if cfg!(windows) {
format!("{folder}/scripts/{binary}.exe") format!("{folder}/scripts/{binary}.exe")
} else { } else {
@ -108,21 +140,24 @@ impl BuildAction for PythonEnvironment {
vec![path] vec![path]
}; };
build.add_inputs("", &self.deps);
build.add_variable("pyenv_folder", self.venv_folder);
if env::var("OFFLINE_BUILD").is_err() { if env::var("OFFLINE_BUILD").is_err() {
build.add_inputs("python_binary", inputs![":python_binary"]); build.add_inputs("uv_binary", inputs![":uv_binary"]);
build.add_variable("pyenv_folder", self.folder);
build.add_inputs("base_requirements", &self.base_requirements_txt); // Add --python flag to extra_args if PYTHON_BINARY is set
build.add_inputs("requirements", &self.requirements_txt); let mut args = self.extra_args.to_string();
build.add_outputs_ext("pip", bin_path("pip"), true); if let Ok(python_binary) = env::var("PYTHON_BINARY") {
args = format!("--python {} {}", python_binary, args);
}
build.add_variable("extra_args", args);
} }
build.add_outputs_ext("bin", bin_path("python"), true); build.add_outputs_ext("bin", bin_path("python"), true);
for binary in self.extra_binary_exports { for binary in self.extra_binary_exports {
build.add_outputs_ext(*binary, bin_path(binary), true); build.add_outputs_ext(*binary, bin_path(binary), true);
} }
} build.add_output_stamp(format!("{}/.stamp", self.venv_folder));
fn check_output_timestamps(&self) -> bool {
true
} }
} }

View file

@ -15,7 +15,6 @@ camino.workspace = true
clap.workspace = true clap.workspace = true
flate2.workspace = true flate2.workspace = true
junction.workspace = true junction.workspace = true
reqwest = { workspace = true, features = ["rustls-tls", "rustls-tls-native-roots"] }
sha2.workspace = true sha2.workspace = true
tar.workspace = true tar.workspace = true
termcolor.workspace = true termcolor.workspace = true
@ -24,3 +23,9 @@ which.workspace = true
xz2.workspace = true xz2.workspace = true
zip.workspace = true zip.workspace = true
zstd.workspace = true zstd.workspace = true
[target.'cfg(windows)'.dependencies]
reqwest = { workspace = true, features = ["native-tls"] }
[target.'cfg(not(windows))'.dependencies]
reqwest = { workspace = true, features = ["rustls-tls", "rustls-tls-native-roots"] }

View file

@ -1,62 +0,0 @@
// 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::fs;
use std::process::Command;
use camino::Utf8PathBuf;
use clap::Args;
use crate::run::run_command;
#[derive(Args, Debug)]
pub struct BuildArtifactsArgs {
bundle_root: Utf8PathBuf,
pyoxidizer_bin: String,
}
pub fn build_artifacts(args: BuildArtifactsArgs) {
// build.rs doesn't declare inputs from venv, so we need to force a rebuild to
// ensure changes to our libs/the venv get included
let artifacts = args.bundle_root.join("artifacts");
if artifacts.exists() {
fs::remove_dir_all(&artifacts).unwrap();
}
let bundle_root = args.bundle_root.canonicalize_utf8().unwrap();
let build_folder = bundle_root.join("build");
if build_folder.exists() {
fs::remove_dir_all(&build_folder).unwrap();
}
run_command(
Command::new(&args.pyoxidizer_bin)
.args([
"--system-rust",
"run-build-script",
"qt/bundle/build.rs",
"--var",
"venv",
"out/bundle/pyenv",
"--var",
"build",
build_folder.as_str(),
])
.env("CARGO_MANIFEST_DIR", "qt/bundle")
.env("CARGO_TARGET_DIR", "out/bundle/rust")
.env("PROFILE", "release")
.env("OUT_DIR", &artifacts)
.env("TARGET", env!("TARGET"))
.env("SDKROOT", "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk")
.env("MACOSX_DEPLOYMENT_TARGET", macos_deployment_target())
.env("CARGO_BUILD_TARGET", env!("TARGET")),
);
}
pub fn macos_deployment_target() -> &'static str {
if env!("TARGET") == "x86_64-apple-darwin" {
"10.13.4"
} else {
"11"
}
}

View file

@ -1,53 +0,0 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use std::process::Command;
use anki_process::CommandExt;
use camino::Utf8Path;
use camino::Utf8PathBuf;
use super::artifacts::macos_deployment_target;
use crate::run::run_command;
pub fn build_bundle_binary() {
let mut features = String::from("build-mode-prebuilt-artifacts");
if cfg!(target_os = "linux") || cfg!(target_os = "macos") {
features.push_str(",global-allocator-jemalloc,allocator-jemalloc");
}
let mut command = Command::new("cargo");
command
.args([
"build",
"--manifest-path=qt/bundle/Cargo.toml",
"--target-dir=out/bundle/rust",
"--release",
"--no-default-features",
])
.arg(format!("--features={features}"))
.env(
"DEFAULT_PYTHON_CONFIG_RS",
// included in main.rs, so relative to qt/bundle/src
"../../../out/bundle/artifacts/",
)
.env(
"PYO3_CONFIG_FILE",
Utf8Path::new("out/bundle/artifacts/pyo3-build-config-file.txt")
.canonicalize_utf8()
.unwrap(),
)
.env("MACOSX_DEPLOYMENT_TARGET", macos_deployment_target())
.env("SDKROOT", "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk")
.env("CARGO_BUILD_TARGET", env!("TARGET"));
if env!("TARGET") == "x86_64-apple-darwin" {
let xcode_path = Command::run_with_output(["xcode-select", "-p"]).unwrap();
let ld_classic = Utf8PathBuf::from(xcode_path.stdout.trim())
.join("Toolchains/XcodeDefault.xctoolchain/usr/bin/ld-classic");
if ld_classic.exists() {
// work around XCode 15's default linker not supporting macOS 10.15-12.
command.env("RUSTFLAGS", format!("-Clink-arg=-fuse-ld={ld_classic}"));
}
}
run_command(&mut command);
}

View file

@ -1,156 +0,0 @@
// 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::fs;
use std::process::Command;
use camino::Utf8Path;
use camino::Utf8PathBuf;
use clap::Args;
use clap::ValueEnum;
use crate::paths::absolute_msys_path;
use crate::paths::unix_path;
use crate::run::run_command;
#[derive(Clone, Copy, ValueEnum, Debug)]
enum DistKind {
Standard,
Alternate,
}
#[derive(Args, Debug)]
pub struct BuildDistFolderArgs {
kind: DistKind,
folder_root: Utf8PathBuf,
}
pub fn build_dist_folder(args: BuildDistFolderArgs) {
let BuildDistFolderArgs { kind, folder_root } = args;
fs::create_dir_all(&folder_root).unwrap();
// Start with Qt, as it's the largest, and we use --delete to ensure there are
// no stale files in lib/. Skipped on macOS as Qt is handled later.
if !cfg!(target_os = "macos") {
copy_qt_from_venv(kind, &folder_root);
}
clean_top_level_files(&folder_root);
copy_binary_and_pylibs(&folder_root);
if cfg!(target_os = "linux") {
copy_linux_extras(kind, &folder_root);
} else if cfg!(windows) {
copy_windows_extras(&folder_root);
}
fs::write(folder_root.with_extension("stamp"), b"").unwrap();
}
fn copy_qt_from_venv(kind: DistKind, folder_root: &Utf8Path) {
let python39 = if cfg!(windows) { "" } else { "python3.9/" };
let qt_root = match kind {
DistKind::Standard => {
folder_root.join(format!("../pyenv/lib/{python39}site-packages/PyQt6"))
}
DistKind::Alternate => {
folder_root.join(format!("../pyenv-qt5/lib/{python39}site-packages/PyQt5"))
}
};
let src_path = absolute_msys_path(&qt_root);
let lib_path = folder_root.join("lib");
fs::create_dir_all(&lib_path).unwrap();
let dst_path = with_slash(absolute_msys_path(&lib_path));
run_command(Command::new("rsync").args([
"-a",
"--delete",
"--exclude-from",
"qt/bundle/qt.exclude",
&src_path,
&dst_path,
]));
}
fn copy_linux_extras(kind: DistKind, folder_root: &Utf8Path) {
// add README, installer, etc
run_command(Command::new("rsync").args(["-a", "qt/bundle/lin/", &with_slash(folder_root)]));
// add extra IME plugins from download
let lib_path = folder_root.join("lib");
let src_path = folder_root
.join("../../extracted/linux_qt_plugins")
.join(match kind {
DistKind::Standard => "qt6",
DistKind::Alternate => "qt5",
});
let dst_path = lib_path.join(match kind {
DistKind::Standard => "PyQt6/Qt6/plugins",
DistKind::Alternate => "PyQt5/Qt5/plugins",
});
run_command(Command::new("rsync").args(["-a", &with_slash(src_path), &with_slash(dst_path)]));
}
fn copy_windows_extras(folder_root: &Utf8Path) {
run_command(Command::new("rsync").args([
"-a",
"out/extracted/win_amd64_audio/",
&with_slash(folder_root),
]));
}
fn clean_top_level_files(folder_root: &Utf8Path) {
let mut to_remove = vec![];
for entry in fs::read_dir(folder_root).unwrap() {
let entry = entry.unwrap();
if entry.file_name() == "lib" {
continue;
} else {
to_remove.push(entry.path());
}
}
for path in to_remove {
if path.is_dir() {
fs::remove_dir_all(path).unwrap()
} else {
fs::remove_file(path).unwrap()
}
}
}
fn with_slash<P>(path: P) -> String
where
P: AsRef<str>,
{
format!("{}/", path.as_ref())
}
fn copy_binary_and_pylibs(folder_root: &Utf8Path) {
let binary = folder_root
.join("../rust")
.join(env!("TARGET"))
.join("release")
.join(if cfg!(windows) { "anki.exe" } else { "anki" });
let extra_files = folder_root
.join("../build")
.join(env!("TARGET"))
.join("release/resources/extra_files");
run_command(Command::new("rsync").args([
"-a",
"--exclude",
"PyQt6",
// misleading, as it misses the GPL PyQt, and our Rust/JS
// dependencies
"--exclude",
"COPYING.txt",
&unix_path(&binary),
&with_slash(unix_path(&extra_files)),
&with_slash(unix_path(folder_root)),
]));
let google_py = if cfg!(windows) {
folder_root.join("../pyenv/lib/site-packages/google")
} else {
folder_root.join("../pyenv/lib/python3.9/site-packages/google")
};
run_command(Command::new("rsync").args([
"-a",
&unix_path(&google_py),
&with_slash(unix_path(&folder_root.join("lib"))),
]));
}

View file

@ -1,6 +0,0 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
pub mod artifacts;
pub mod binary;
pub mod folder;

View file

@ -7,7 +7,6 @@
mod archive; mod archive;
mod build; mod build;
mod bundle;
mod paths; mod paths;
mod pyenv; mod pyenv;
mod rsync; mod rsync;
@ -19,11 +18,6 @@ use archive::archive_command;
use archive::ArchiveArgs; use archive::ArchiveArgs;
use build::run_build; use build::run_build;
use build::BuildArgs; use build::BuildArgs;
use bundle::artifacts::build_artifacts;
use bundle::artifacts::BuildArtifactsArgs;
use bundle::binary::build_bundle_binary;
use bundle::folder::build_dist_folder;
use bundle::folder::BuildDistFolderArgs;
use clap::Parser; use clap::Parser;
use clap::Subcommand; use clap::Subcommand;
use pyenv::setup_pyenv; use pyenv::setup_pyenv;
@ -48,9 +42,6 @@ enum Command {
Rsync(RsyncArgs), Rsync(RsyncArgs),
Run(RunArgs), Run(RunArgs),
Build(BuildArgs), Build(BuildArgs),
BuildArtifacts(BuildArtifactsArgs),
BuildBundleBinary,
BuildDistFolder(BuildDistFolderArgs),
#[clap(subcommand)] #[clap(subcommand)]
Archive(ArchiveArgs), Archive(ArchiveArgs),
} }
@ -62,9 +53,6 @@ fn main() -> Result<()> {
Command::Rsync(args) => rsync_files(args), Command::Rsync(args) => rsync_files(args),
Command::Yarn(args) => setup_yarn(args), Command::Yarn(args) => setup_yarn(args),
Command::Build(args) => run_build(args), Command::Build(args) => run_build(args),
Command::BuildArtifacts(args) => build_artifacts(args),
Command::BuildBundleBinary => build_bundle_binary(),
Command::BuildDistFolder(args) => build_dist_folder(args),
Command::Archive(args) => archive_command(args)?, Command::Archive(args) => archive_command(args)?,
}; };
Ok(()) Ok(())

View file

@ -16,8 +16,3 @@ pub fn absolute_msys_path(path: &Utf8Path) -> String {
// and \ -> / // and \ -> /
format!("/{drive}/{}", path[7..].replace('\\', "/")) format!("/{drive}/{}", path[7..].replace('\\', "/"))
} }
/// Converts backslashes to forward slashes
pub fn unix_path(path: &Utf8Path) -> String {
path.as_str().replace('\\', "/")
}

View file

@ -1,6 +1,7 @@
// Copyright: Ankitects Pty Ltd and contributors // Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use std::fs;
use std::process::Command; use std::process::Command;
use camino::Utf8Path; use camino::Utf8Path;
@ -10,12 +11,10 @@ use crate::run::run_command;
#[derive(Args)] #[derive(Args)]
pub struct PyenvArgs { pub struct PyenvArgs {
python_bin: String, uv_bin: String,
pyenv_folder: String, pyenv_folder: String,
initial_reqs: String, #[arg(trailing_var_arg = true)]
reqs: Vec<String>, extra_args: Vec<String>,
#[arg(long, allow_hyphen_values(true))]
venv_args: Vec<String>,
} }
/// Set up a venv if one doesn't already exist, and then sync packages with /// Set up a venv if one doesn't already exist, and then sync packages with
@ -23,42 +22,23 @@ pub struct PyenvArgs {
pub fn setup_pyenv(args: PyenvArgs) { pub fn setup_pyenv(args: PyenvArgs) {
let pyenv_folder = Utf8Path::new(&args.pyenv_folder); let pyenv_folder = Utf8Path::new(&args.pyenv_folder);
let pyenv_bin_folder = pyenv_folder.join(if cfg!(windows) { "scripts" } else { "bin" }); // On first run, ninja creates an empty bin/ folder which breaks the initial
let pyenv_python = pyenv_bin_folder.join("python"); // install. But we don't want to indiscriminately remove the folder, or
let pip_sync = pyenv_bin_folder.join("pip-sync"); // macOS Gatekeeper needs to rescan the files each time.
if pyenv_folder.exists() {
// Ensure the venv gets recreated properly if it was created by our uv branch let cache_tag = pyenv_folder.join("CACHEDIR.TAG");
let cache_tag = pyenv_folder.join("CACHEDIR.TAG"); if !cache_tag.exists() {
if cache_tag.exists() { fs::remove_dir_all(pyenv_folder).expect("Failed to remove existing pyenv folder");
println!("Cleaning up uv pyenv...");
std::fs::remove_dir_all(pyenv_folder).expect("Failed to remove pyenv folder");
}
if !pyenv_python.exists() {
run_command(
Command::new(&args.python_bin)
.args(["-m", "venv"])
.args(args.venv_args)
.arg(pyenv_folder),
);
if cfg!(windows) {
// the first install on Windows throws an error the first time pip is upgraded,
// so we install it twice and swallow the first error
let _output = Command::new(&pyenv_python)
.args(["-m", "pip", "install", "-r", &args.initial_reqs])
.output()
.unwrap();
} }
run_command(Command::new(pyenv_python).args([
"-m",
"pip",
"install",
"-r",
&args.initial_reqs,
]));
} }
run_command(Command::new(pip_sync).args(&args.reqs)); run_command(
Command::new(args.uv_bin)
.env("UV_PROJECT_ENVIRONMENT", args.pyenv_folder.clone())
.args(["sync", "--frozen"])
.args(args.extra_args),
);
// Write empty stamp file
fs::write(pyenv_folder.join(".stamp"), "").expect("Failed to write stamp file");
} }

View file

@ -1,7 +1,6 @@
// Copyright: Ankitects Pty Ltd and contributors // Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use std::io::ErrorKind;
use std::process::Command; use std::process::Command;
use anki_io::create_dir_all; use anki_io::create_dir_all;
@ -44,7 +43,7 @@ fn split_env(s: &str) -> Result<(String, String), std::io::Error> {
if let Some((k, v)) = s.split_once('=') { if let Some((k, v)) = s.split_once('=') {
Ok((k.into(), v.into())) Ok((k.into(), v.into()))
} else { } else {
Err(std::io::Error::new(ErrorKind::Other, "invalid env var")) Err(std::io::Error::other("invalid env var"))
} }
} }

File diff suppressed because it is too large Load diff

View file

@ -190,13 +190,10 @@ in the collection2.log file will also be printed on stdout.
If ANKI_PROFILE_CODE is set, Python profiling data will be written on exit. If ANKI_PROFILE_CODE is set, Python profiling data will be written on exit.
# Binary Bundles # Installer/launcher
Anki's official binary packages are created with `./ninja bundle`. The bundling - The anki-release package is created/published with the scripts in qt/release.
process was created specifically for the official builds, and is provided as-is; - The installer/launcher is created with the build scripts in qt/launcher/{platform}.
we are unfortunately not able to provide assistance with any issues you may run
into when using it. You'll need to run
`git submodule update --checkout qt/bundle/PyOxidizer` first.
## Mixing development and study ## Mixing development and study

View file

@ -9,7 +9,12 @@ You must be running 64 bit Windows 10, version 1703 or newer.
**Rustup**: **Rustup**:
As mentioned in development.md, rustup must be installed. If you're on As mentioned in development.md, rustup must be installed. If you're on
ARM Windows, you must set the default target to x86_64-pc-windows-msvc. ARM Windows and install the ARM64 version of rust-up, from this project folder,
run
```
rustup target add x86_64-pc-windows-msvc
```
**Visual Studio**: **Visual Studio**:

View file

@ -1,36 +0,0 @@
#!/usr/bin/env python3
# Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
"""
Tool to extract core strings and keys from .ftl files.
"""
import glob
import json
import os
from fluent.syntax import parse
from fluent.syntax.ast import Junk, Message
from fluent.syntax.serializer import serialize_element
root = ".."
ftl_files = glob.glob(os.path.join(root, "ftl", "core", "*.ftl"), recursive=True)
keys_by_value: dict[str, list[str]] = {}
for path in ftl_files:
obj = parse(open(path, encoding="utf8").read(), with_spans=False)
for ent in obj.body:
if isinstance(ent, Junk):
raise Exception(f"file had junk! {path} {ent}")
if isinstance(ent, Message):
key = ent.id.name
val = "".join(serialize_element(elem) for elem in ent.value.elements)
if val in keys_by_value:
print("duplicate found:", keys_by_value[val], key)
keys_by_value.setdefault(val, []).append(key)
json.dump(
keys_by_value, open(os.path.join(root, "keys_by_value.json"), "w", encoding="utf8")
)
print("keys:", len(keys_by_value))

View file

@ -1,99 +0,0 @@
#!/usr/bin/env python3
# Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
"""
Parse and re-serialize ftl files to get them in a consistent form.
"""
import difflib
import glob
import os
from typing import List
from compare_locales import parser
from compare_locales.checks.fluent import ReferenceMessageVisitor
from compare_locales.paths import File
from fluent.syntax import parse, serialize
from fluent.syntax.ast import Junk
def check_missing_terms(path: str) -> bool:
"True if file is ok."
file = File(path, os.path.basename(path))
content = open(path, "rb").read()
p = parser.getParser(file.file)
p.readContents(content)
refList = p.parse()
p.readContents(content)
for e in p.parse():
ref_data = ReferenceMessageVisitor()
ref_data.visit(e.entry)
for attr_or_val, refs in ref_data.entry_refs.items():
for ref, ref_type in refs.items():
if ref not in refList:
print(f"In {path}:{e}, missing '{ref}'")
return False
return True
def check_file(path: str, fix: bool) -> bool:
"True if file is ok."
orig_text = open(path, encoding="utf8").read()
obj = parse(orig_text, with_spans=False)
# make sure there's no junk
for ent in obj.body:
if isinstance(ent, Junk):
raise Exception(f"file had junk! {path} {ent}")
# serialize
new_text = serialize(obj)
# make sure serializing did not introduce new junk
obj = parse(new_text, with_spans=False)
for ent in obj.body:
if isinstance(ent, Junk):
raise Exception(f"file introduced junk! {path} {ent}")
if new_text == orig_text:
return check_missing_terms(path)
if fix:
print(f"Fixing {path}")
open(path, "w", newline="\n", encoding="utf8").write(new_text)
return True
else:
print(f"Bad formatting in {path}")
print(
"\n".join(
difflib.unified_diff(
orig_text.splitlines(),
new_text.splitlines(),
fromfile="bad",
tofile="good",
lineterm="",
)
)
)
return False
def check_files(files: List[str], fix: bool) -> bool:
"True if files ok."
found_bad = False
for path in files:
ok = check_file(path, fix)
if not ok:
found_bad = True
return not found_bad
if __name__ == "__main__":
template_root = os.environ["BUILD_WORKSPACE_DIRECTORY"]
template_files = glob.glob(
os.path.join(template_root, "ftl", "*", "*.ftl"), recursive=True
)
check_files(template_files, fix=True)

View file

@ -1,14 +0,0 @@
# Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import glob
import os
import sys
import format
template_root = os.path.dirname(sys.argv[1])
template_files = glob.glob(os.path.join(template_root, "*", "*.ftl"), recursive=True)
if not format.check_files(template_files, fix=False):
sys.exit(1)

2
ninja
View file

@ -8,7 +8,7 @@ else
out="$BUILD_ROOT" out="$BUILD_ROOT"
fi fi
export CARGO_TARGET_DIR=$out/rust 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 if [ "$SKIP_RUNNER_BUILD" = "1" ]; then
echo "Runner not rebuilt." echo "Runner not rebuilt."

View file

@ -1,8 +1,7 @@
# Copyright: Ankitects Pty Ltd and contributors # Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
"""Helpers for serializing third-party collections to a common JSON form. """Helpers for serializing third-party collections to a common JSON form."""
"""
from __future__ import annotations from __future__ import annotations

View file

@ -167,9 +167,9 @@ class NoteImporter(Importer):
firsts[fld0] = True firsts[fld0] = True
# already exists? # already exists?
found = False found = False
if csum in csums: if csum in csums: # type: ignore[comparison-overlap]
# csum is not a guarantee; have to check # csum is not a guarantee; have to check
for id in csums[csum]: for id in csums[csum]: # type: ignore[index]
flds = self.col.db.scalar("select flds from notes where id = ?", id) flds = self.col.db.scalar("select flds from notes where id = ?", id)
sflds = split_fields(flds) sflds = split_fields(flds)
if fld0 == sflds[0]: if fld0 == sflds[0]:

View file

@ -198,7 +198,9 @@ def get_def_lang(user_lang: str | None = None) -> tuple[int, str]:
# getdefaultlocale() is deprecated since Python 3.11, but we need to keep using it as getlocale() behaves differently: https://bugs.python.org/issue38805 # getdefaultlocale() is deprecated since Python 3.11, but we need to keep using it as getlocale() behaves differently: https://bugs.python.org/issue38805
with warnings.catch_warnings(): with warnings.catch_warnings():
warnings.simplefilter("ignore", DeprecationWarning) warnings.simplefilter("ignore", DeprecationWarning)
(sys_lang, enc) = locale.getdefaultlocale() (sys_lang, enc) = (
locale.getdefaultlocale() # pylint: disable=deprecated-method
)
except AttributeError: except AttributeError:
# this will return a different format on Windows (e.g. Italian_Italy), resulting in us falling back to en_US # this will return a different format on Windows (e.g. Italian_Italy), resulting in us falling back to en_US
# further below # further below

View file

@ -42,6 +42,7 @@ from anki.utils import ids2str, int_time
class SchedulerBase(DeprecatedNamesMixin): class SchedulerBase(DeprecatedNamesMixin):
"Actions shared between schedulers." "Actions shared between schedulers."
version = 0 version = 0
def __init__(self, col: anki.collection.Collection) -> None: def __init__(self, col: anki.collection.Collection) -> None:

View file

@ -174,7 +174,7 @@ from revlog where type != {REVLOG_RESCHED} and id > ? """
cards=cards, seconds=float(thetime) cards=cards, seconds=float(thetime)
) )
# again/pass count # again/pass count
b += "<br>" + "Again count: %s" % bold(failed) b += "<br>" + "Again count: %s" % bold(str(failed))
if cards: if cards:
b += " " + "(%s correct)" % bold( b += " " + "(%s correct)" % bold(
"%0.1f%%" % ((1 - failed / float(cards)) * 100) "%0.1f%%" % ((1 - failed / float(cards)) * 100)
@ -182,7 +182,10 @@ from revlog where type != {REVLOG_RESCHED} and id > ? """
# type breakdown # type breakdown
b += "<br>" b += "<br>"
b += "Learn: %(a)s, Review: %(b)s, Relearn: %(c)s, Filtered: %(d)s" % dict( b += "Learn: %(a)s, Review: %(b)s, Relearn: %(c)s, Filtered: %(d)s" % dict(
a=bold(lrn), b=bold(rev), c=bold(relrn), d=bold(filt) a=bold(str(lrn)),
b=bold(str(rev)),
c=bold(str(relrn)),
d=bold(str(filt)),
) )
# mature today # mature today
mcnt, msum = self.col.db.first( mcnt, msum = self.col.db.first(

View file

@ -279,6 +279,7 @@ class TemplateRenderContext:
@dataclass @dataclass
class TemplateRenderOutput: class TemplateRenderOutput:
"Stores the rendered templates and extracted AV tags." "Stores the rendered templates and extracted AV tags."
question_text: str question_text: str
answer_text: str answer_text: str
question_av_tags: list[AVTag] question_av_tags: list[AVTag]

View file

@ -244,8 +244,8 @@ def call(argv: list[str], wait: bool = True, **kwargs: Any) -> int:
# OS helpers # OS helpers
############################################################################## ##############################################################################
is_mac = sys.platform.startswith("darwin") is_mac = sys.platform == "darwin"
is_win = sys.platform.startswith("win32") is_win = sys.platform == "win32"
# also covers *BSD # also covers *BSD
is_lin = not is_mac and not is_win is_lin = not is_mac and not is_win
is_gnome = ( is_gnome = (

42
pylib/hatch_build.py Normal file
View file

@ -0,0 +1,42 @@
# Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import os
import platform
import sys
from pathlib import Path
from typing import Any, Dict
from hatchling.builders.hooks.plugin.interface import BuildHookInterface
class CustomBuildHook(BuildHookInterface):
"""Build hook to include compiled rsbridge from out/pylib."""
PLUGIN_NAME = "custom"
def initialize(self, version: str, build_data: Dict[str, Any]) -> None:
"""Initialize the build hook."""
force_include = build_data.setdefault("force_include", {})
# Set platform-specific wheel tag
if not (platform_tag := os.environ.get("ANKI_WHEEL_TAG")):
# On Windows, uv invokes this build hook during the initial uv sync,
# when the tag has not been declared by our build script.
return
build_data.setdefault("tag", platform_tag)
# Mark as non-pure Python since we include compiled extension
build_data["pure_python"] = False
# Look for generated files in out/pylib/anki
project_root = Path(self.root).parent
generated_root = project_root / "out" / "pylib" / "anki"
assert generated_root.exists(), "you should build with --wheel"
for path in generated_root.rglob("*"):
if path.is_file():
relative_path = path.relative_to(generated_root)
# Place files under anki/ in the distribution
dist_path = "anki" / relative_path
force_include[str(path)] = str(dist_path)

35
pylib/pyproject.toml Normal file
View file

@ -0,0 +1,35 @@
[project]
name = "anki"
# dynamic = ["version"]
version = "0.1.2"
requires-python = ">=3.9"
license = "AGPL-3.0-or-later"
dependencies = [
"beautifulsoup4",
"decorator",
"markdown",
"orjson",
"protobuf>=4.21",
"requests[socks]",
"typing_extensions",
"types-protobuf",
"types-requests",
"types-orjson",
# platform-specific dependencies
"distro; sys_platform != 'darwin' and sys_platform != 'win32'",
"psutil; sys_platform == 'win32'",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["anki"]
[tool.hatch.version]
source = "code"
path = "../python/version.py"
[tool.hatch.build.hooks.custom]
path = "hatch_build.py"

View file

@ -1,21 +1,33 @@
// Copyright: Ankitects Pty Ltd and contributors // Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use std::path::Path;
fn main() { fn main() {
// macOS needs special link flags for PyO3 // macOS needs special link flags for PyO3
if cfg!(target_os = "macos") { if cfg!(target_os = "macos") {
println!("cargo:rustc-link-arg=-undefined"); println!("cargo:rustc-link-arg=-undefined");
println!("cargo:rustc-link-arg=dynamic_lookup"); println!("cargo:rustc-link-arg=dynamic_lookup");
println!("cargo:rustc-link-arg=-mmacosx-version-min=10.13"); println!("cargo:rustc-link-arg=-mmacosx-version-min=11");
} }
// On Windows, we need to be able to link with python3.lib // On Windows, we need to be able to link with python3.lib
if cfg!(windows) { if cfg!(windows) {
let lib_path = Path::new("../../out/extracted/python/libs") use std::process::Command;
.canonicalize()
.expect("libs"); // Run Python to get sysconfig paths
println!("cargo:rustc-link-search={}", lib_path.display()); let output = Command::new("../../out/pyenv/scripts/python")
.args([
"-c",
"import sysconfig; print(sysconfig.get_paths()['stdlib'])",
])
.output()
.expect("Failed to execute Python");
let stdlib_path = String::from_utf8(output.stdout)
.expect("Failed to parse Python output")
.trim()
.to_string();
let libs_path = stdlib_path + "s";
println!("cargo:rustc-link-search={}", libs_path);
} }
} }

View file

@ -1,8 +1,36 @@
[tool.black] [project]
target-version = ["py39", "py310", "py311", "py312"] name = "anki-dev"
extend-exclude = "qt/bundle" version = "0.0.0"
description = "Local-only environment"
requires-python = ">=3.9"
classifiers = ["Private :: Do Not Upload"]
[tool.pyright] [dependency-groups]
include = ["pylib/anki", "qt/aqt"] dev = [
stubPath = "" "black",
pythonVersion = "3.9" "isort",
"mypy",
"mypy-protobuf",
"pylint",
"pytest",
"PyChromeDevTools",
"colorama", # for isort --color
"wheel",
"hatchling", # for type checking hatch_build.py files
]
[project.optional-dependencies]
sphinx = [
"sphinx",
"sphinx_rtd_theme",
"sphinx-autoapi",
]
[tool.uv.workspace]
members = ["pylib", "qt"]
[[tool.uv.index]]
name = "testpypi"
url = "https://test.pypi.org/simple/"
publish-url = "https://test.pypi.org/legacy/"
explicit = true

View file

@ -1,3 +0,0 @@
- To achieve reproducible builds we use pip-tools to lock packages to a particular version - see
update_python_deps.sh
- write_wheel.py is used to generate our wheels.

View file

@ -1,152 +0,0 @@
[
{
"License": "BSD License",
"Name": "Flask",
"Version": "1.1.2"
},
{
"License": "MIT License",
"Name": "Flask-Cors",
"Version": "3.0.9"
},
{
"License": "BSD License",
"Name": "Jinja2",
"Version": "2.11.2"
},
{
"License": "BSD License",
"Name": "Markdown",
"Version": "3.3.3"
},
{
"License": "BSD License",
"Name": "MarkupSafe",
"Version": "1.1.1"
},
{
"License": "GPL v3",
"Name": "PyQt5",
"Version": "5.15.1"
},
{
"License": "SIP",
"Name": "PyQt5-sip",
"Version": "12.8.1"
},
{
"License": "GPL v3",
"Name": "PyQtWebEngine",
"Version": "5.15.1"
},
{
"License": "BSD",
"Name": "PySocks",
"Version": "1.7.1"
},
{
"License": "BSD License",
"Name": "Send2Trash",
"Version": "1.5.0"
},
{
"License": "BSD License",
"Name": "Werkzeug",
"Version": "1.0.1"
},
{
"License": "MIT License",
"Name": "attrs",
"Version": "20.3.0"
},
{
"License": "MIT License",
"Name": "beautifulsoup4",
"Version": "4.9.3"
},
{
"License": "Mozilla Public License 2.0 (MPL 2.0)",
"Name": "certifi",
"Version": "2020.11.8"
},
{
"License": "GNU Library or Lesser General Public License (LGPL)",
"Name": "chardet",
"Version": "3.0.4"
},
{
"License": "BSD License",
"Name": "click",
"Version": "7.1.2"
},
{
"License": "BSD License",
"Name": "decorator",
"Version": "4.4.2"
},
{
"License": "BSD License",
"Name": "idna",
"Version": "2.10"
},
{
"License": "BSD License",
"Name": "itsdangerous",
"Version": "1.1.0"
},
{
"License": "MIT License",
"Name": "jsonschema",
"Version": "3.2.0"
},
{
"License": "Apache Software License, MIT License",
"Name": "orjson",
"Version": "3.4.3"
},
{
"License": "3-Clause BSD License",
"Name": "protobuf",
"Version": "3.13.0"
},
{
"License": "BSD License",
"Name": "psutil",
"Version": "5.7.3"
},
{
"License": "MIT License",
"Name": "pyrsistent",
"Version": "0.17.3"
},
{
"License": "Python Software Foundation License",
"Name": "pywin32",
"Version": "228"
},
{
"License": "Apache Software License",
"Name": "requests",
"Version": "2.25.0"
},
{
"License": "MIT License",
"Name": "six",
"Version": "1.15.0"
},
{
"License": "MIT License",
"Name": "soupsieve",
"Version": "2.0.1"
},
{
"License": "MIT License",
"Name": "urllib3",
"Version": "1.26.1"
},
{
"License": "Zope Public License",
"Name": "waitress",
"Version": "1.4.4"
}
]

View file

@ -1,23 +0,0 @@
#!/bin/bash
#
# Install runtime requirements into a venv and extract their licenses.
# As Windows currently uses extra deps, running this on Windows should
# capture all packages.
# Run with 'bash licenses.sh' to update 'license.json'
set -e
# setup venv
python -m venv venv
# build wheels
../bazel.bat --output_base=/c/bazel/anki/base build //pylib/anki:wheel //qt/aqt:wheel
# install wheels, bound to constrained versions
venv/tools/pip install -c requirements.txt ../bazel-bin/pylib/anki/*.whl ../bazel-bin/qt/aqt/*.whl pip-licenses
# dump licenses - ptable is a pip-licenses dep
venv/tools/pip-licenses --format=json --ignore-packages anki aqt pip-license PTable > licenses.json
# clean up
rm -rf venv

View file

@ -1,9 +0,0 @@
beautifulsoup4
decorator
markdown
orjson
protobuf>=4.21
requests[socks]
distro; sys_platform != "darwin" and sys_platform != "win32"
psutil; sys_platform == "win32"
typing_extensions

View file

@ -1,10 +0,0 @@
beautifulsoup4
flask
flask_cors
jsonschema
requests
send2trash
waitress>=2.0.0
psutil; sys.platform == "win32"
pywin32; sys.platform == "win32"
pip-system-certs

View file

@ -1,2 +0,0 @@
pip-tools
colorama # required on windows

View file

@ -1,54 +0,0 @@
build==1.2.1 \
--hash=sha256:526263f4870c26f26c433545579475377b2b7588b6f1eac76a001e873ae3e19d \
--hash=sha256:75e10f767a433d9a86e50d83f418e83efc18ede923ee5ff7df93b6cb0306c5d4
# via pip-tools
click==8.1.7 \
--hash=sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28 \
--hash=sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de
# via pip-tools
colorama==0.4.6 \
--hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \
--hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6
# via -r requirements.base.in
importlib-metadata==8.4.0 \
--hash=sha256:66f342cc6ac9818fc6ff340576acd24d65ba0b3efabb2b4ac08b598965a4a2f1 \
--hash=sha256:9a547d3bc3608b025f93d403fdd1aae741c24fbb8314df4b155675742ce303c5
# via build
packaging==24.1 \
--hash=sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002 \
--hash=sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124
# via build
pip-tools==7.4.1 \
--hash=sha256:4c690e5fbae2f21e87843e89c26191f0d9454f362d8acdbd695716493ec8b3a9 \
--hash=sha256:864826f5073864450e24dbeeb85ce3920cdfb09848a3d69ebf537b521f14bcc9
# via -r requirements.base.in
pyproject-hooks==1.1.0 \
--hash=sha256:4b37730834edbd6bd37f26ece6b44802fb1c1ee2ece0e54ddff8bfc06db86965 \
--hash=sha256:7ceeefe9aec63a1064c18d939bdc3adf2d8aa1988a510afec15151578b232aa2
# via
# build
# pip-tools
tomli==2.0.1 \
--hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \
--hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f
# via
# build
# pip-tools
wheel==0.44.0 \
--hash=sha256:2376a90c98cc337d18623527a97c31797bd02bad0033d41547043a1cbfbe448f \
--hash=sha256:a29c3f2817e95ab89aa4660681ad547c0e9547f20e75b0562fe7723c9a2a9d49
# via pip-tools
zipp==3.20.1 \
--hash=sha256:9960cd8967c8f85a56f920d5d507274e74f9ff813a0ab8889a5b5be2daf44064 \
--hash=sha256:c22b14cc4763c5a5b04134207736c107db42e9d3ef2d9779d465f5f1bcba572b
# via importlib-metadata
# The following packages are considered to be unsafe in a requirements file:
pip==24.2 \
--hash=sha256:2cd581cf58ab7fcfca4ce8efa6dcacd0de5bf8d0a3eb9ec927e07405f4d9e2a2 \
--hash=sha256:5b5e490b5e9cb275c879595064adce9ebd31b854e3e803740b72f9ccf34a45b8
# via pip-tools
setuptools==74.1.1 \
--hash=sha256:2353af060c06388be1cecbf5953dcdb1f38362f87a2356c480b6b4d5fcfc8847 \
--hash=sha256:fc91b5f89e392ef5b77fe143b17e32f65d3024744fba66dc3afe07201684d766
# via pip-tools

View file

@ -1,8 +0,0 @@
# currently broken in pyoxidizer
jsonschema<4.2
setuptools<70
-r requirements.base.in
-r requirements.anki.in
-r requirements.aqt.in

View file

@ -1,494 +0,0 @@
attrs==24.2.0 \
--hash=sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346 \
--hash=sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2
# via jsonschema
beautifulsoup4==4.12.3 \
--hash=sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051 \
--hash=sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed
# via
# -r requirements.anki.in
# -r requirements.aqt.in
blinker==1.8.2 \
--hash=sha256:1779309f71bf239144b9399d06ae925637cf6634cf6bd131104184531bf67c01 \
--hash=sha256:8f77b09d3bf7c795e969e9486f39c2c5e9c39d4ee07424be2bc594ece9642d83
# via flask
build==1.2.1 \
--hash=sha256:526263f4870c26f26c433545579475377b2b7588b6f1eac76a001e873ae3e19d \
--hash=sha256:75e10f767a433d9a86e50d83f418e83efc18ede923ee5ff7df93b6cb0306c5d4
# via pip-tools
certifi==2024.8.30 \
--hash=sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8 \
--hash=sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9
# via requests
charset-normalizer==3.3.2 \
--hash=sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027 \
--hash=sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087 \
--hash=sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786 \
--hash=sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8 \
--hash=sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09 \
--hash=sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185 \
--hash=sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574 \
--hash=sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e \
--hash=sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519 \
--hash=sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898 \
--hash=sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269 \
--hash=sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3 \
--hash=sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f \
--hash=sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6 \
--hash=sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8 \
--hash=sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a \
--hash=sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73 \
--hash=sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc \
--hash=sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714 \
--hash=sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2 \
--hash=sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc \
--hash=sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce \
--hash=sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d \
--hash=sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e \
--hash=sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6 \
--hash=sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269 \
--hash=sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96 \
--hash=sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d \
--hash=sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a \
--hash=sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4 \
--hash=sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77 \
--hash=sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d \
--hash=sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0 \
--hash=sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed \
--hash=sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068 \
--hash=sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac \
--hash=sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25 \
--hash=sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8 \
--hash=sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab \
--hash=sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26 \
--hash=sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2 \
--hash=sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db \
--hash=sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f \
--hash=sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5 \
--hash=sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99 \
--hash=sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c \
--hash=sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d \
--hash=sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811 \
--hash=sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa \
--hash=sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a \
--hash=sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03 \
--hash=sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b \
--hash=sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04 \
--hash=sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c \
--hash=sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001 \
--hash=sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458 \
--hash=sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389 \
--hash=sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99 \
--hash=sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985 \
--hash=sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537 \
--hash=sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238 \
--hash=sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f \
--hash=sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d \
--hash=sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796 \
--hash=sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a \
--hash=sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143 \
--hash=sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8 \
--hash=sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c \
--hash=sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5 \
--hash=sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5 \
--hash=sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711 \
--hash=sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4 \
--hash=sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6 \
--hash=sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c \
--hash=sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7 \
--hash=sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4 \
--hash=sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b \
--hash=sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae \
--hash=sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12 \
--hash=sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c \
--hash=sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae \
--hash=sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8 \
--hash=sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887 \
--hash=sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b \
--hash=sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4 \
--hash=sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f \
--hash=sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5 \
--hash=sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33 \
--hash=sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519 \
--hash=sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561
# via requests
click==8.1.7 \
--hash=sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28 \
--hash=sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de
# via
# flask
# pip-tools
colorama==0.4.6 \
--hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \
--hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6
# via -r requirements.base.in
decorator==5.1.1 \
--hash=sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330 \
--hash=sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186
# via -r requirements.anki.in
distro==1.9.0 ; sys_platform != "darwin" and sys_platform != "win32" \
--hash=sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed \
--hash=sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2
# via -r requirements.anki.in
flask==3.0.3 \
--hash=sha256:34e815dfaa43340d1d15a5c3a02b8476004037eb4840b34910c6e21679d288f3 \
--hash=sha256:ceb27b0af3823ea2737928a4d99d125a06175b8512c445cbd9a9ce200ef76842
# via
# -r requirements.aqt.in
# flask-cors
flask-cors==6.0.0 \
--hash=sha256:4592c1570246bf7beee96b74bc0adbbfcb1b0318f6ba05c412e8909eceec3393 \
--hash=sha256:6332073356452343a8ccddbfec7befdc3fdd040141fe776ec9b94c262f058657
# via -r requirements.aqt.in
idna==3.8 \
--hash=sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac \
--hash=sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603
# via requests
importlib-metadata==8.4.0 \
--hash=sha256:66f342cc6ac9818fc6ff340576acd24d65ba0b3efabb2b4ac08b598965a4a2f1 \
--hash=sha256:9a547d3bc3608b025f93d403fdd1aae741c24fbb8314df4b155675742ce303c5
# via
# build
# flask
# markdown
itsdangerous==2.2.0 \
--hash=sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef \
--hash=sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173
# via flask
jinja2==3.1.5 \
--hash=sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb \
--hash=sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb
# via flask
jsonschema==4.1.2 \
--hash=sha256:166870c8ab27bd712a8627e0598de4685bd8d199c4d7bd7cacc3d941ba0c6ca0 \
--hash=sha256:5c1a282ee6b74235057421fd0f766ac5f2972f77440927f6471c9e8493632fac
# via
# -r requirements.aqt.in
# -r requirements.bundle.in
markdown==3.7 \
--hash=sha256:2ae2471477cfd02dbbf038d5d9bc226d40def84b4fe2986e49b59b6b472bbed2 \
--hash=sha256:7eb6df5690b81a1d7942992c97fad2938e956e79df20cbc6186e9c3a77b1c803
# via -r requirements.anki.in
markupsafe==2.1.5 \
--hash=sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf \
--hash=sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff \
--hash=sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f \
--hash=sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3 \
--hash=sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532 \
--hash=sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f \
--hash=sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617 \
--hash=sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df \
--hash=sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4 \
--hash=sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906 \
--hash=sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f \
--hash=sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4 \
--hash=sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8 \
--hash=sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371 \
--hash=sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2 \
--hash=sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465 \
--hash=sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52 \
--hash=sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6 \
--hash=sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169 \
--hash=sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad \
--hash=sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2 \
--hash=sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0 \
--hash=sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029 \
--hash=sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f \
--hash=sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a \
--hash=sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced \
--hash=sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5 \
--hash=sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c \
--hash=sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf \
--hash=sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9 \
--hash=sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb \
--hash=sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad \
--hash=sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3 \
--hash=sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1 \
--hash=sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46 \
--hash=sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc \
--hash=sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a \
--hash=sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee \
--hash=sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900 \
--hash=sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5 \
--hash=sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea \
--hash=sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f \
--hash=sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5 \
--hash=sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e \
--hash=sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a \
--hash=sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f \
--hash=sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50 \
--hash=sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a \
--hash=sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b \
--hash=sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4 \
--hash=sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff \
--hash=sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2 \
--hash=sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46 \
--hash=sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b \
--hash=sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf \
--hash=sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5 \
--hash=sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5 \
--hash=sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab \
--hash=sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd \
--hash=sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68
# via
# jinja2
# werkzeug
orjson==3.10.7 \
--hash=sha256:084e537806b458911137f76097e53ce7bf5806dda33ddf6aaa66a028f8d43a23 \
--hash=sha256:09b2d92fd95ad2402188cf51573acde57eb269eddabaa60f69ea0d733e789fe9 \
--hash=sha256:0fa5886854673222618638c6df7718ea7fe2f3f2384c452c9ccedc70b4a510a5 \
--hash=sha256:11748c135f281203f4ee695b7f80bb1358a82a63905f9f0b794769483ea854ad \
--hash=sha256:1193b2416cbad1a769f868b1749535d5da47626ac29445803dae7cc64b3f5c98 \
--hash=sha256:144888c76f8520e39bfa121b31fd637e18d4cc2f115727865fdf9fa325b10412 \
--hash=sha256:1d9c0e733e02ada3ed6098a10a8ee0052dd55774de3d9110d29868d24b17faa1 \
--hash=sha256:23820a1563a1d386414fef15c249040042b8e5d07b40ab3fe3efbfbbcbcb8864 \
--hash=sha256:33cfb96c24034a878d83d1a9415799a73dc77480e6c40417e5dda0710d559ee6 \
--hash=sha256:348bdd16b32556cf8d7257b17cf2bdb7ab7976af4af41ebe79f9796c218f7e91 \
--hash=sha256:34a566f22c28222b08875b18b0dfbf8a947e69df21a9ed5c51a6bf91cfb944ac \
--hash=sha256:3dcfbede6737fdbef3ce9c37af3fb6142e8e1ebc10336daa05872bfb1d87839c \
--hash=sha256:430ee4d85841e1483d487e7b81401785a5dfd69db5de01314538f31f8fbf7ee1 \
--hash=sha256:44a96f2d4c3af51bfac6bc4ef7b182aa33f2f054fd7f34cc0ee9a320d051d41f \
--hash=sha256:479fd0844ddc3ca77e0fd99644c7fe2de8e8be1efcd57705b5c92e5186e8a250 \
--hash=sha256:480f455222cb7a1dea35c57a67578848537d2602b46c464472c995297117fa09 \
--hash=sha256:4829cf2195838e3f93b70fd3b4292156fc5e097aac3739859ac0dcc722b27ac0 \
--hash=sha256:4b6146e439af4c2472c56f8540d799a67a81226e11992008cb47e1267a9b3225 \
--hash=sha256:4e6c3da13e5a57e4b3dca2de059f243ebec705857522f188f0180ae88badd354 \
--hash=sha256:5b24a579123fa884f3a3caadaed7b75eb5715ee2b17ab5c66ac97d29b18fe57f \
--hash=sha256:6b0dd04483499d1de9c8f6203f8975caf17a6000b9c0c54630cef02e44ee624e \
--hash=sha256:6ea2b2258eff652c82652d5e0f02bd5e0463a6a52abb78e49ac288827aaa1469 \
--hash=sha256:7122a99831f9e7fe977dc45784d3b2edc821c172d545e6420c375e5a935f5a1c \
--hash=sha256:74f4544f5a6405b90da8ea724d15ac9c36da4d72a738c64685003337401f5c12 \
--hash=sha256:75ef0640403f945f3a1f9f6400686560dbfb0fb5b16589ad62cd477043c4eee3 \
--hash=sha256:76ac14cd57df0572453543f8f2575e2d01ae9e790c21f57627803f5e79b0d3c3 \
--hash=sha256:77d325ed866876c0fa6492598ec01fe30e803272a6e8b10e992288b009cbe149 \
--hash=sha256:7c4c17f8157bd520cdb7195f75ddbd31671997cbe10aee559c2d613592e7d7eb \
--hash=sha256:7db8539039698ddfb9a524b4dd19508256107568cdad24f3682d5773e60504a2 \
--hash=sha256:8272527d08450ab16eb405f47e0f4ef0e5ff5981c3d82afe0efd25dcbef2bcd2 \
--hash=sha256:82763b46053727a7168d29c772ed5c870fdae2f61aa8a25994c7984a19b1021f \
--hash=sha256:8a9c9b168b3a19e37fe2778c0003359f07822c90fdff8f98d9d2a91b3144d8e0 \
--hash=sha256:8de062de550f63185e4c1c54151bdddfc5625e37daf0aa1e75d2a1293e3b7d9a \
--hash=sha256:974683d4618c0c7dbf4f69c95a979734bf183d0658611760017f6e70a145af58 \
--hash=sha256:9ea2c232deedcb605e853ae1db2cc94f7390ac776743b699b50b071b02bea6fe \
--hash=sha256:a0c6a008e91d10a2564edbb6ee5069a9e66df3fbe11c9a005cb411f441fd2c09 \
--hash=sha256:a763bc0e58504cc803739e7df040685816145a6f3c8a589787084b54ebc9f16e \
--hash=sha256:a7e19150d215c7a13f39eb787d84db274298d3f83d85463e61d277bbd7f401d2 \
--hash=sha256:ac7cf6222b29fbda9e3a472b41e6a5538b48f2c8f99261eecd60aafbdb60690c \
--hash=sha256:b48b3db6bb6e0a08fa8c83b47bc169623f801e5cc4f24442ab2b6617da3b5313 \
--hash=sha256:b58d3795dafa334fc8fd46f7c5dc013e6ad06fd5b9a4cc98cb1456e7d3558bd6 \
--hash=sha256:bdbb61dcc365dd9be94e8f7df91975edc9364d6a78c8f7adb69c1cdff318ec93 \
--hash=sha256:bf6ba8ebc8ef5792e2337fb0419f8009729335bb400ece005606336b7fd7bab7 \
--hash=sha256:c31008598424dfbe52ce8c5b47e0752dca918a4fdc4a2a32004efd9fab41d866 \
--hash=sha256:cb61938aec8b0ffb6eef484d480188a1777e67b05d58e41b435c74b9d84e0b9c \
--hash=sha256:d2d9f990623f15c0ae7ac608103c33dfe1486d2ed974ac3f40b693bad1a22a7b \
--hash=sha256:d352ee8ac1926d6193f602cbe36b1643bbd1bbcb25e3c1a657a4390f3000c9a5 \
--hash=sha256:d374d36726746c81a49f3ff8daa2898dccab6596864ebe43d50733275c629175 \
--hash=sha256:de817e2f5fc75a9e7dd350c4b0f54617b280e26d1631811a43e7e968fa71e3e9 \
--hash=sha256:e724cebe1fadc2b23c6f7415bad5ee6239e00a69f30ee423f319c6af70e2a5c0 \
--hash=sha256:e72591bcfe7512353bd609875ab38050efe3d55e18934e2f18950c108334b4ff \
--hash=sha256:e76be12658a6fa376fcd331b1ea4e58f5a06fd0220653450f0d415b8fd0fbe20 \
--hash=sha256:eb8d384a24778abf29afb8e41d68fdd9a156cf6e5390c04cc07bbc24b89e98b5 \
--hash=sha256:ed350d6978d28b92939bfeb1a0570c523f6170efc3f0a0ef1f1df287cd4f4960 \
--hash=sha256:eef44224729e9525d5261cc8d28d6b11cafc90e6bd0be2157bde69a52ec83024 \
--hash=sha256:f4db56635b58cd1a200b0a23744ff44206ee6aa428185e2b6c4a65b3197abdcd \
--hash=sha256:fdf5197a21dd660cf19dfd2a3ce79574588f8f5e2dbf21bda9ee2d2b46924d84
# via -r requirements.anki.in
packaging==24.1 \
--hash=sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002 \
--hash=sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124
# via build
pip-system-certs==4.0 \
--hash=sha256:47202b9403a6f40783a9674bbc8873f5fc86544ec01a49348fa913e99e2ff68b \
--hash=sha256:db8e6a31388d9795ec9139957df1a89fa5274fb66164456fd091a5d3e94c350c
# via -r requirements.aqt.in
pip-tools==7.4.1 \
--hash=sha256:4c690e5fbae2f21e87843e89c26191f0d9454f362d8acdbd695716493ec8b3a9 \
--hash=sha256:864826f5073864450e24dbeeb85ce3920cdfb09848a3d69ebf537b521f14bcc9
# via -r requirements.base.in
protobuf==5.28.2 \
--hash=sha256:2c69461a7fcc8e24be697624c09a839976d82ae75062b11a0972e41fd2cd9132 \
--hash=sha256:35cfcb15f213449af7ff6198d6eb5f739c37d7e4f1c09b5d0641babf2cc0c68f \
--hash=sha256:52235802093bd8a2811abbe8bf0ab9c5f54cca0a751fdd3f6ac2a21438bffece \
--hash=sha256:59379674ff119717404f7454647913787034f03fe7049cbef1d74a97bb4593f0 \
--hash=sha256:5e8a95246d581eef20471b5d5ba010d55f66740942b95ba9b872d918c459452f \
--hash=sha256:87317e9bcda04a32f2ee82089a204d3a2f0d3c8aeed16568c7daf4756e4f1fe0 \
--hash=sha256:8ddc60bf374785fb7cb12510b267f59067fa10087325b8e1855b898a0d81d276 \
--hash=sha256:a8b9403fc70764b08d2f593ce44f1d2920c5077bf7d311fefec999f8c40f78b7 \
--hash=sha256:c0ea0123dac3399a2eeb1a1443d82b7afc9ff40241433296769f7da42d142ec3 \
--hash=sha256:ca53faf29896c526863366a52a8f4d88e69cd04ec9571ed6082fa117fac3ab36 \
--hash=sha256:eeea10f3dc0ac7e6b4933d32db20662902b4ab81bf28df12218aa389e9c2102d
# via -r requirements.anki.in
pyproject-hooks==1.1.0 \
--hash=sha256:4b37730834edbd6bd37f26ece6b44802fb1c1ee2ece0e54ddff8bfc06db86965 \
--hash=sha256:7ceeefe9aec63a1064c18d939bdc3adf2d8aa1988a510afec15151578b232aa2
# via
# build
# pip-tools
pyrsistent==0.20.0 \
--hash=sha256:0724c506cd8b63c69c7f883cc233aac948c1ea946ea95996ad8b1380c25e1d3f \
--hash=sha256:09848306523a3aba463c4b49493a760e7a6ca52e4826aa100ee99d8d39b7ad1e \
--hash=sha256:0f3b1bcaa1f0629c978b355a7c37acd58907390149b7311b5db1b37648eb6958 \
--hash=sha256:21cc459636983764e692b9eba7144cdd54fdec23ccdb1e8ba392a63666c60c34 \
--hash=sha256:2e14c95c16211d166f59c6611533d0dacce2e25de0f76e4c140fde250997b3ca \
--hash=sha256:2e2c116cc804d9b09ce9814d17df5edf1df0c624aba3b43bc1ad90411487036d \
--hash=sha256:4021a7f963d88ccd15b523787d18ed5e5269ce57aa4037146a2377ff607ae87d \
--hash=sha256:4c48f78f62ab596c679086084d0dd13254ae4f3d6c72a83ffdf5ebdef8f265a4 \
--hash=sha256:4f5c2d012671b7391803263419e31b5c7c21e7c95c8760d7fc35602353dee714 \
--hash=sha256:58b8f6366e152092194ae68fefe18b9f0b4f89227dfd86a07770c3d86097aebf \
--hash=sha256:59a89bccd615551391f3237e00006a26bcf98a4d18623a19909a2c48b8e986ee \
--hash=sha256:5cdd7ef1ea7a491ae70d826b6cc64868de09a1d5ff9ef8d574250d0940e275b8 \
--hash=sha256:6288b3fa6622ad8a91e6eb759cfc48ff3089e7c17fb1d4c59a919769314af224 \
--hash=sha256:6d270ec9dd33cdb13f4d62c95c1a5a50e6b7cdd86302b494217137f760495b9d \
--hash=sha256:79ed12ba79935adaac1664fd7e0e585a22caa539dfc9b7c7c6d5ebf91fb89054 \
--hash=sha256:7d29c23bdf6e5438c755b941cef867ec2a4a172ceb9f50553b6ed70d50dfd656 \
--hash=sha256:8441cf9616d642c475684d6cf2520dd24812e996ba9af15e606df5f6fd9d04a7 \
--hash=sha256:881bbea27bbd32d37eb24dd320a5e745a2a5b092a17f6debc1349252fac85423 \
--hash=sha256:8c3aba3e01235221e5b229a6c05f585f344734bd1ad42a8ac51493d74722bbce \
--hash=sha256:a14798c3005ec892bbada26485c2eea3b54109cb2533713e355c806891f63c5e \
--hash=sha256:b14decb628fac50db5e02ee5a35a9c0772d20277824cfe845c8a8b717c15daa3 \
--hash=sha256:b318ca24db0f0518630e8b6f3831e9cba78f099ed5c1d65ffe3e023003043ba0 \
--hash=sha256:c1beb78af5423b879edaf23c5591ff292cf7c33979734c99aa66d5914ead880f \
--hash=sha256:c55acc4733aad6560a7f5f818466631f07efc001fd023f34a6c203f8b6df0f0b \
--hash=sha256:ca52d1ceae015859d16aded12584c59eb3825f7b50c6cfd621d4231a6cc624ce \
--hash=sha256:cae40a9e3ce178415040a0383f00e8d68b569e97f31928a3a8ad37e3fde6df6a \
--hash=sha256:e78d0c7c1e99a4a45c99143900ea0546025e41bb59ebc10182e947cf1ece9174 \
--hash=sha256:ef3992833fbd686ee783590639f4b8343a57f1f75de8633749d984dc0eb16c86 \
--hash=sha256:f058a615031eea4ef94ead6456f5ec2026c19fb5bd6bfe86e9665c4158cf802f \
--hash=sha256:f5ac696f02b3fc01a710427585c855f65cd9c640e14f52abe52020722bb4906b \
--hash=sha256:f920385a11207dc372a028b3f1e1038bb244b3ec38d448e6d8e43c6b3ba20e98 \
--hash=sha256:fed2c3216a605dc9a6ea50c7e84c82906e3684c4e80d2908208f662a6cbf9022
# via jsonschema
pysocks==1.7.1 \
--hash=sha256:08e69f092cc6dbe92a0fdd16eeb9b9ffbc13cadfe5ca4c7bd92ffb078b293299 \
--hash=sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5 \
--hash=sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0
# via requests
requests==2.32.4 \
--hash=sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c \
--hash=sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422
# via
# -r requirements.anki.in
# -r requirements.aqt.in
send2trash==1.8.3 \
--hash=sha256:0c31227e0bd08961c7665474a3d1ef7193929fedda4233843689baa056be46c9 \
--hash=sha256:b18e7a3966d99871aefeb00cfbcfdced55ce4871194810fc71f4aa484b953abf
# via -r requirements.aqt.in
soupsieve==2.6 \
--hash=sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb \
--hash=sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9
# via beautifulsoup4
tomli==2.0.1 \
--hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \
--hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f
# via
# build
# pip-tools
typing-extensions==4.13.2 \
--hash=sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c \
--hash=sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef
# via -r requirements.anki.in
urllib3==2.2.2 \
--hash=sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472 \
--hash=sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168
# via requests
waitress==3.0.1 \
--hash=sha256:26cdbc593093a15119351690752c99adc13cbc6786d75f7b6341d1234a3730ac \
--hash=sha256:ef0c1f020d9f12a515c4ec65c07920a702613afcad1dbfdc3bcec256b6c072b3
# via -r requirements.aqt.in
werkzeug==3.0.6 \
--hash=sha256:1bc0c2310d2fbb07b1dd1105eba2f7af72f322e1e455f2f93c993bee8c8a5f17 \
--hash=sha256:a8dd59d4de28ca70471a34cba79bed5f7ef2e036a76b3ab0835474246eb41f8d
# via
# flask
# flask-cors
wheel==0.44.0 \
--hash=sha256:2376a90c98cc337d18623527a97c31797bd02bad0033d41547043a1cbfbe448f \
--hash=sha256:a29c3f2817e95ab89aa4660681ad547c0e9547f20e75b0562fe7723c9a2a9d49
# via pip-tools
wrapt==1.16.0 \
--hash=sha256:0d2691979e93d06a95a26257adb7bfd0c93818e89b1406f5a28f36e0d8c1e1fc \
--hash=sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81 \
--hash=sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09 \
--hash=sha256:1acd723ee2a8826f3d53910255643e33673e1d11db84ce5880675954183ec47e \
--hash=sha256:1ca9b6085e4f866bd584fb135a041bfc32cab916e69f714a7d1d397f8c4891ca \
--hash=sha256:1dd50a2696ff89f57bd8847647a1c363b687d3d796dc30d4dd4a9d1689a706f0 \
--hash=sha256:2076fad65c6736184e77d7d4729b63a6d1ae0b70da4868adeec40989858eb3fb \
--hash=sha256:2a88e6010048489cda82b1326889ec075a8c856c2e6a256072b28eaee3ccf487 \
--hash=sha256:3ebf019be5c09d400cf7b024aa52b1f3aeebeff51550d007e92c3c1c4afc2a40 \
--hash=sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c \
--hash=sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060 \
--hash=sha256:44a2754372e32ab315734c6c73b24351d06e77ffff6ae27d2ecf14cf3d229202 \
--hash=sha256:490b0ee15c1a55be9c1bd8609b8cecd60e325f0575fc98f50058eae366e01f41 \
--hash=sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9 \
--hash=sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b \
--hash=sha256:5f15814a33e42b04e3de432e573aa557f9f0f56458745c2074952f564c50e664 \
--hash=sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d \
--hash=sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362 \
--hash=sha256:66dfbaa7cfa3eb707bbfcd46dab2bc6207b005cbc9caa2199bcbc81d95071a00 \
--hash=sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc \
--hash=sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1 \
--hash=sha256:6a42cd0cfa8ffc1915aef79cb4284f6383d8a3e9dcca70c445dcfdd639d51267 \
--hash=sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956 \
--hash=sha256:6f6eac2360f2d543cc875a0e5efd413b6cbd483cb3ad7ebf888884a6e0d2e966 \
--hash=sha256:72554a23c78a8e7aa02abbd699d129eead8b147a23c56e08d08dfc29cfdddca1 \
--hash=sha256:73870c364c11f03ed072dda68ff7aea6d2a3a5c3fe250d917a429c7432e15228 \
--hash=sha256:73aa7d98215d39b8455f103de64391cb79dfcad601701a3aa0dddacf74911d72 \
--hash=sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d \
--hash=sha256:7bd2d7ff69a2cac767fbf7a2b206add2e9a210e57947dd7ce03e25d03d2de292 \
--hash=sha256:807cc8543a477ab7422f1120a217054f958a66ef7314f76dd9e77d3f02cdccd0 \
--hash=sha256:8e9723528b9f787dc59168369e42ae1c3b0d3fadb2f1a71de14531d321ee05b0 \
--hash=sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36 \
--hash=sha256:9153ed35fc5e4fa3b2fe97bddaa7cbec0ed22412b85bcdaf54aeba92ea37428c \
--hash=sha256:9159485323798c8dc530a224bd3ffcf76659319ccc7bbd52e01e73bd0241a0c5 \
--hash=sha256:941988b89b4fd6b41c3f0bfb20e92bd23746579736b7343283297c4c8cbae68f \
--hash=sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73 \
--hash=sha256:98b5e1f498a8ca1858a1cdbffb023bfd954da4e3fa2c0cb5853d40014557248b \
--hash=sha256:9b201ae332c3637a42f02d1045e1d0cccfdc41f1f2f801dafbaa7e9b4797bfc2 \
--hash=sha256:a0ea261ce52b5952bf669684a251a66df239ec6d441ccb59ec7afa882265d593 \
--hash=sha256:a33a747400b94b6d6b8a165e4480264a64a78c8a4c734b62136062e9a248dd39 \
--hash=sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389 \
--hash=sha256:a86373cf37cd7764f2201b76496aba58a52e76dedfaa698ef9e9688bfd9e41cf \
--hash=sha256:ac83a914ebaf589b69f7d0a1277602ff494e21f4c2f743313414378f8f50a4cf \
--hash=sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89 \
--hash=sha256:b3646eefa23daeba62643a58aac816945cadc0afaf21800a1421eeba5f6cfb9c \
--hash=sha256:b47cfad9e9bbbed2339081f4e346c93ecd7ab504299403320bf85f7f85c7d46c \
--hash=sha256:b935ae30c6e7400022b50f8d359c03ed233d45b725cfdd299462f41ee5ffba6f \
--hash=sha256:bb2dee3874a500de01c93d5c71415fcaef1d858370d405824783e7a8ef5db440 \
--hash=sha256:bc57efac2da352a51cc4658878a68d2b1b67dbe9d33c36cb826ca449d80a8465 \
--hash=sha256:bf5703fdeb350e36885f2875d853ce13172ae281c56e509f4e6eca049bdfb136 \
--hash=sha256:c31f72b1b6624c9d863fc095da460802f43a7c6868c5dda140f51da24fd47d7b \
--hash=sha256:c5cd603b575ebceca7da5a3a251e69561bec509e0b46e4993e1cac402b7247b8 \
--hash=sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3 \
--hash=sha256:d462f28826f4657968ae51d2181a074dfe03c200d6131690b7d65d55b0f360f8 \
--hash=sha256:d5e49454f19ef621089e204f862388d29e6e8d8b162efce05208913dde5b9ad6 \
--hash=sha256:da4813f751142436b075ed7aa012a8778aa43a99f7b36afe9b742d3ed8bdc95e \
--hash=sha256:db2e408d983b0e61e238cf579c09ef7020560441906ca990fe8412153e3b291f \
--hash=sha256:db98ad84a55eb09b3c32a96c576476777e87c520a34e2519d3e59c44710c002c \
--hash=sha256:dbed418ba5c3dce92619656802cc5355cb679e58d0d89b50f116e4a9d5a9603e \
--hash=sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8 \
--hash=sha256:decbfa2f618fa8ed81c95ee18a387ff973143c656ef800c9f24fb7e9c16054e2 \
--hash=sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020 \
--hash=sha256:eb1b046be06b0fce7249f1d025cd359b4b80fc1c3e24ad9eca33e0dcdb2e4a35 \
--hash=sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d \
--hash=sha256:ed867c42c268f876097248e05b6117a65bcd1e63b779e916fe2e33cd6fd0d3c3 \
--hash=sha256:edfad1d29c73f9b863ebe7082ae9321374ccb10879eeabc84ba3b69f2579d537 \
--hash=sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809 \
--hash=sha256:f6b2d0c6703c988d334f297aa5df18c45e97b0af3679bb75059e0e0bd8b1069d \
--hash=sha256:f8212564d49c50eb4565e502814f694e240c55551a5f1bc841d4fcaabb0a9b8a \
--hash=sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4
# via pip-system-certs
zipp==3.20.1 \
--hash=sha256:9960cd8967c8f85a56f920d5d507274e74f9ff813a0ab8889a5b5be2daf44064 \
--hash=sha256:c22b14cc4763c5a5b04134207736c107db42e9d3ef2d9779d465f5f1bcba572b
# via importlib-metadata
# The following packages are considered to be unsafe in a requirements file:
pip==24.2 \
--hash=sha256:2cd581cf58ab7fcfca4ce8efa6dcacd0de5bf8d0a3eb9ec927e07405f4d9e2a2 \
--hash=sha256:5b5e490b5e9cb275c879595064adce9ebd31b854e3e803740b72f9ccf34a45b8
# via pip-tools
setuptools==69.5.1 \
--hash=sha256:6c1fccdac05a97e598fb0ae3bbed5904ccb317337a51139dcd51453611bbb987 \
--hash=sha256:c636ac361bc47580504644275c9ad802c50415c7522212252c033bd15f301f32
# via
# -r requirements.bundle.in
# pip-tools

View file

@ -1,27 +0,0 @@
-r requirements.base.in
-r requirements.anki.in
-r requirements.aqt.in
black
compare-locales
isort
mock
mypy
mypy-protobuf
pip-tools
pylint
pytest
PyChromeDevTools
fluent.syntax
types-decorator
types-flask
types-flask-cors
types-markdown
types-orjson
types-protobuf
types-requests
types-waitress
# transitive windows dependencies
atomicwrites
colorama

View file

@ -1,715 +0,0 @@
astroid==3.2.4 \
--hash=sha256:0e14202810b30da1b735827f78f5157be2bbd4a7a59b7707ca0bfc2fb4c0063a \
--hash=sha256:413658a61eeca6202a59231abb473f932038fbcbf1666587f66d482083413a25
# via pylint
atomicwrites==1.4.1 \
--hash=sha256:81b2c9071a49367a7f770170e5eec8cb66567cfbbc8c73d20ce5ca4a8d71cf11
# via -r requirements.dev.in
attrs==24.2.0 \
--hash=sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346 \
--hash=sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2
# via jsonschema
beautifulsoup4==4.12.3 \
--hash=sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051 \
--hash=sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed
# via
# -r requirements.anki.in
# -r requirements.aqt.in
black==24.8.0 \
--hash=sha256:09cdeb74d494ec023ded657f7092ba518e8cf78fa8386155e4a03fdcc44679e6 \
--hash=sha256:1f13f7f386f86f8121d76599114bb8c17b69d962137fc70efe56137727c7047e \
--hash=sha256:2500945420b6784c38b9ee885af039f5e7471ef284ab03fa35ecdde4688cd83f \
--hash=sha256:2b59b250fdba5f9a9cd9d0ece6e6d993d91ce877d121d161e4698af3eb9c1018 \
--hash=sha256:3c4285573d4897a7610054af5a890bde7c65cb466040c5f0c8b732812d7f0e5e \
--hash=sha256:505289f17ceda596658ae81b61ebbe2d9b25aa78067035184ed0a9d855d18afd \
--hash=sha256:62e8730977f0b77998029da7971fa896ceefa2c4c4933fcd593fa599ecbf97a4 \
--hash=sha256:649f6d84ccbae73ab767e206772cc2d7a393a001070a4c814a546afd0d423aed \
--hash=sha256:6e55d30d44bed36593c3163b9bc63bf58b3b30e4611e4d88a0c3c239930ed5b2 \
--hash=sha256:707a1ca89221bc8a1a64fb5e15ef39cd755633daa672a9db7498d1c19de66a42 \
--hash=sha256:72901b4913cbac8972ad911dc4098d5753704d1f3c56e44ae8dce99eecb0e3af \
--hash=sha256:73bbf84ed136e45d451a260c6b73ed674652f90a2b3211d6a35e78054563a9bb \
--hash=sha256:7c046c1d1eeb7aea9335da62472481d3bbf3fd986e093cffd35f4385c94ae368 \
--hash=sha256:81c6742da39f33b08e791da38410f32e27d632260e599df7245cccee2064afeb \
--hash=sha256:837fd281f1908d0076844bc2b801ad2d369c78c45cf800cad7b61686051041af \
--hash=sha256:972085c618ee94f402da1af548a4f218c754ea7e5dc70acb168bfaca4c2542ed \
--hash=sha256:9e84e33b37be070ba135176c123ae52a51f82306def9f7d063ee302ecab2cf47 \
--hash=sha256:b19c9ad992c7883ad84c9b22aaa73562a16b819c1d8db7a1a1a49fb7ec13c7d2 \
--hash=sha256:d6417535d99c37cee4091a2f24eb2b6d5ec42b144d50f1f2e436d9fe1916fe1a \
--hash=sha256:eab4dd44ce80dea27dc69db40dab62d4ca96112f87996bca68cd75639aeb2e4c \
--hash=sha256:f490dbd59680d809ca31efdae20e634f3fae27fba3ce0ba3208333b713bc3920 \
--hash=sha256:fb6e2c0b86bbd43dee042e48059c9ad7830abd5c94b0bc518c0eeec57c3eddc1
# via -r requirements.dev.in
blinker==1.8.2 \
--hash=sha256:1779309f71bf239144b9399d06ae925637cf6634cf6bd131104184531bf67c01 \
--hash=sha256:8f77b09d3bf7c795e969e9486f39c2c5e9c39d4ee07424be2bc594ece9642d83
# via flask
build==1.2.1 \
--hash=sha256:526263f4870c26f26c433545579475377b2b7588b6f1eac76a001e873ae3e19d \
--hash=sha256:75e10f767a433d9a86e50d83f418e83efc18ede923ee5ff7df93b6cb0306c5d4
# via pip-tools
certifi==2024.8.30 \
--hash=sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8 \
--hash=sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9
# via requests
charset-normalizer==3.3.2 \
--hash=sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027 \
--hash=sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087 \
--hash=sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786 \
--hash=sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8 \
--hash=sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09 \
--hash=sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185 \
--hash=sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574 \
--hash=sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e \
--hash=sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519 \
--hash=sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898 \
--hash=sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269 \
--hash=sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3 \
--hash=sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f \
--hash=sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6 \
--hash=sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8 \
--hash=sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a \
--hash=sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73 \
--hash=sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc \
--hash=sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714 \
--hash=sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2 \
--hash=sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc \
--hash=sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce \
--hash=sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d \
--hash=sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e \
--hash=sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6 \
--hash=sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269 \
--hash=sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96 \
--hash=sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d \
--hash=sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a \
--hash=sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4 \
--hash=sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77 \
--hash=sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d \
--hash=sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0 \
--hash=sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed \
--hash=sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068 \
--hash=sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac \
--hash=sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25 \
--hash=sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8 \
--hash=sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab \
--hash=sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26 \
--hash=sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2 \
--hash=sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db \
--hash=sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f \
--hash=sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5 \
--hash=sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99 \
--hash=sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c \
--hash=sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d \
--hash=sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811 \
--hash=sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa \
--hash=sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a \
--hash=sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03 \
--hash=sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b \
--hash=sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04 \
--hash=sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c \
--hash=sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001 \
--hash=sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458 \
--hash=sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389 \
--hash=sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99 \
--hash=sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985 \
--hash=sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537 \
--hash=sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238 \
--hash=sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f \
--hash=sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d \
--hash=sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796 \
--hash=sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a \
--hash=sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143 \
--hash=sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8 \
--hash=sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c \
--hash=sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5 \
--hash=sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5 \
--hash=sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711 \
--hash=sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4 \
--hash=sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6 \
--hash=sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c \
--hash=sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7 \
--hash=sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4 \
--hash=sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b \
--hash=sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae \
--hash=sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12 \
--hash=sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c \
--hash=sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae \
--hash=sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8 \
--hash=sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887 \
--hash=sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b \
--hash=sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4 \
--hash=sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f \
--hash=sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5 \
--hash=sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33 \
--hash=sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519 \
--hash=sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561
# via requests
click==8.1.7 \
--hash=sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28 \
--hash=sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de
# via
# black
# flask
# pip-tools
colorama==0.4.6 \
--hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \
--hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6
# via
# -r requirements.base.in
# -r requirements.dev.in
compare-locales==9.0.4 \
--hash=sha256:73d0d384aefa0bc96f5fd8521c08c8bb89b16a37316701323a77960accabd551 \
--hash=sha256:933d2b6e20f460d3ac2d3176295684505a42085b25e6c31944fcafbaf52f1cc0
# via -r requirements.dev.in
decorator==5.1.1 \
--hash=sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330 \
--hash=sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186
# via -r requirements.anki.in
dill==0.3.8 \
--hash=sha256:3ebe3c479ad625c4553aca177444d89b486b1d84982eeacded644afc0cf797ca \
--hash=sha256:c36ca9ffb54365bdd2f8eb3eff7d2a21237f8452b57ace88b1ac615b7e815bd7
# via pylint
distro==1.9.0 ; sys_platform != "darwin" and sys_platform != "win32" \
--hash=sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed \
--hash=sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2
# via -r requirements.anki.in
exceptiongroup==1.2.2 \
--hash=sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b \
--hash=sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc
# via pytest
flask==3.0.3 \
--hash=sha256:34e815dfaa43340d1d15a5c3a02b8476004037eb4840b34910c6e21679d288f3 \
--hash=sha256:ceb27b0af3823ea2737928a4d99d125a06175b8512c445cbd9a9ce200ef76842
# via
# -r requirements.aqt.in
# flask-cors
# types-flask-cors
flask-cors==6.0.0 \
--hash=sha256:4592c1570246bf7beee96b74bc0adbbfcb1b0318f6ba05c412e8909eceec3393 \
--hash=sha256:6332073356452343a8ccddbfec7befdc3fdd040141fe776ec9b94c262f058657
# via -r requirements.aqt.in
fluent-syntax==0.19.0 \
--hash=sha256:920326d7f46864b9758f0044e9968e3112198bc826acee16ddd8f11d359004fd \
--hash=sha256:b352b3475fac6c6ed5f06527921f432aac073d764445508ee5218aeccc7cc5c4
# via
# -r requirements.dev.in
# compare-locales
idna==3.8 \
--hash=sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac \
--hash=sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603
# via requests
importlib-metadata==8.4.0 \
--hash=sha256:66f342cc6ac9818fc6ff340576acd24d65ba0b3efabb2b4ac08b598965a4a2f1 \
--hash=sha256:9a547d3bc3608b025f93d403fdd1aae741c24fbb8314df4b155675742ce303c5
# via
# build
# flask
# markdown
iniconfig==2.0.0 \
--hash=sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3 \
--hash=sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374
# via pytest
isort==5.13.2 \
--hash=sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109 \
--hash=sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6
# via
# -r requirements.dev.in
# pylint
itsdangerous==2.2.0 \
--hash=sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef \
--hash=sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173
# via flask
jinja2==3.1.5 \
--hash=sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb \
--hash=sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb
# via flask
jsonschema==4.1.2 \
--hash=sha256:166870c8ab27bd712a8627e0598de4685bd8d199c4d7bd7cacc3d941ba0c6ca0 \
--hash=sha256:5c1a282ee6b74235057421fd0f766ac5f2972f77440927f6471c9e8493632fac
# via -r requirements.aqt.in
markdown==3.7 \
--hash=sha256:2ae2471477cfd02dbbf038d5d9bc226d40def84b4fe2986e49b59b6b472bbed2 \
--hash=sha256:7eb6df5690b81a1d7942992c97fad2938e956e79df20cbc6186e9c3a77b1c803
# via -r requirements.anki.in
markupsafe==2.1.5 \
--hash=sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf \
--hash=sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff \
--hash=sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f \
--hash=sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3 \
--hash=sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532 \
--hash=sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f \
--hash=sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617 \
--hash=sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df \
--hash=sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4 \
--hash=sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906 \
--hash=sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f \
--hash=sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4 \
--hash=sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8 \
--hash=sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371 \
--hash=sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2 \
--hash=sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465 \
--hash=sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52 \
--hash=sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6 \
--hash=sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169 \
--hash=sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad \
--hash=sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2 \
--hash=sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0 \
--hash=sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029 \
--hash=sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f \
--hash=sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a \
--hash=sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced \
--hash=sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5 \
--hash=sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c \
--hash=sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf \
--hash=sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9 \
--hash=sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb \
--hash=sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad \
--hash=sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3 \
--hash=sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1 \
--hash=sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46 \
--hash=sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc \
--hash=sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a \
--hash=sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee \
--hash=sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900 \
--hash=sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5 \
--hash=sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea \
--hash=sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f \
--hash=sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5 \
--hash=sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e \
--hash=sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a \
--hash=sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f \
--hash=sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50 \
--hash=sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a \
--hash=sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b \
--hash=sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4 \
--hash=sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff \
--hash=sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2 \
--hash=sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46 \
--hash=sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b \
--hash=sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf \
--hash=sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5 \
--hash=sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5 \
--hash=sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab \
--hash=sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd \
--hash=sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68
# via
# jinja2
# werkzeug
mccabe==0.7.0 \
--hash=sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325 \
--hash=sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e
# via pylint
mock==5.1.0 \
--hash=sha256:18c694e5ae8a208cdb3d2c20a993ca1a7b0efa258c247a1e565150f477f83744 \
--hash=sha256:5e96aad5ccda4718e0a229ed94b2024df75cc2d55575ba5762d31f5767b8767d
# via -r requirements.dev.in
mypy==1.11.2 \
--hash=sha256:06d26c277962f3fb50e13044674aa10553981ae514288cb7d0a738f495550b36 \
--hash=sha256:2ff93107f01968ed834f4256bc1fc4475e2fecf6c661260066a985b52741ddce \
--hash=sha256:36383a4fcbad95f2657642a07ba22ff797de26277158f1cc7bd234821468b1b6 \
--hash=sha256:37c7fa6121c1cdfcaac97ce3d3b5588e847aa79b580c1e922bb5d5d2902df19b \
--hash=sha256:3a66169b92452f72117e2da3a576087025449018afc2d8e9bfe5ffab865709ca \
--hash=sha256:3f14cd3d386ac4d05c5a39a51b84387403dadbd936e17cb35882134d4f8f0d24 \
--hash=sha256:41ea707d036a5307ac674ea172875f40c9d55c5394f888b168033177fce47383 \
--hash=sha256:478db5f5036817fe45adb7332d927daa62417159d49783041338921dcf646fc7 \
--hash=sha256:4a8a53bc3ffbd161b5b2a4fff2f0f1e23a33b0168f1c0778ec70e1a3d66deb86 \
--hash=sha256:539c570477a96a4e6fb718b8d5c3e0c0eba1f485df13f86d2970c91f0673148d \
--hash=sha256:57555a7715c0a34421013144a33d280e73c08df70f3a18a552938587ce9274f4 \
--hash=sha256:6e658bd2d20565ea86da7d91331b0eed6d2eee22dc031579e6297f3e12c758c8 \
--hash=sha256:6e7184632d89d677973a14d00ae4d03214c8bc301ceefcdaf5c474866814c987 \
--hash=sha256:75746e06d5fa1e91bfd5432448d00d34593b52e7e91a187d981d08d1f33d4385 \
--hash=sha256:7f9993ad3e0ffdc95c2a14b66dee63729f021968bff8ad911867579c65d13a79 \
--hash=sha256:801780c56d1cdb896eacd5619a83e427ce436d86a3bdf9112527f24a66618fef \
--hash=sha256:801ca29f43d5acce85f8e999b1e431fb479cb02d0e11deb7d2abb56bdaf24fd6 \
--hash=sha256:969ea3ef09617aff826885a22ece0ddef69d95852cdad2f60c8bb06bf1f71f70 \
--hash=sha256:a976775ab2256aadc6add633d44f100a2517d2388906ec4f13231fafbb0eccca \
--hash=sha256:af8d155170fcf87a2afb55b35dc1a0ac21df4431e7d96717621962e4b9192e70 \
--hash=sha256:b499bc07dbdcd3de92b0a8b29fdf592c111276f6a12fe29c30f6c417dd546d12 \
--hash=sha256:cd953f221ac1379050a8a646585a29574488974f79d8082cedef62744f0a0104 \
--hash=sha256:d42a6dd818ffce7be66cce644f1dff482f1d97c53ca70908dff0b9ddc120b77a \
--hash=sha256:e8960dbbbf36906c5c0b7f4fbf2f0c7ffb20f4898e6a879fcf56a41a08b0d318 \
--hash=sha256:edb91dded4df17eae4537668b23f0ff6baf3707683734b6a818d5b9d0c0c31a1 \
--hash=sha256:ee23de8530d99b6db0573c4ef4bd8f39a2a6f9b60655bf7a1357e585a3486f2b \
--hash=sha256:f7821776e5c4286b6a13138cc935e2e9b6fde05e081bdebf5cdb2bb97c9df81d
# via -r requirements.dev.in
mypy-extensions==1.0.0 \
--hash=sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d \
--hash=sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782
# via
# black
# mypy
mypy-protobuf==3.6.0 \
--hash=sha256:02f242eb3409f66889f2b1a3aa58356ec4d909cdd0f93115622e9e70366eca3c \
--hash=sha256:56176e4d569070e7350ea620262478b49b7efceba4103d468448f1d21492fd6c
# via -r requirements.dev.in
orjson==3.10.7 \
--hash=sha256:084e537806b458911137f76097e53ce7bf5806dda33ddf6aaa66a028f8d43a23 \
--hash=sha256:09b2d92fd95ad2402188cf51573acde57eb269eddabaa60f69ea0d733e789fe9 \
--hash=sha256:0fa5886854673222618638c6df7718ea7fe2f3f2384c452c9ccedc70b4a510a5 \
--hash=sha256:11748c135f281203f4ee695b7f80bb1358a82a63905f9f0b794769483ea854ad \
--hash=sha256:1193b2416cbad1a769f868b1749535d5da47626ac29445803dae7cc64b3f5c98 \
--hash=sha256:144888c76f8520e39bfa121b31fd637e18d4cc2f115727865fdf9fa325b10412 \
--hash=sha256:1d9c0e733e02ada3ed6098a10a8ee0052dd55774de3d9110d29868d24b17faa1 \
--hash=sha256:23820a1563a1d386414fef15c249040042b8e5d07b40ab3fe3efbfbbcbcb8864 \
--hash=sha256:33cfb96c24034a878d83d1a9415799a73dc77480e6c40417e5dda0710d559ee6 \
--hash=sha256:348bdd16b32556cf8d7257b17cf2bdb7ab7976af4af41ebe79f9796c218f7e91 \
--hash=sha256:34a566f22c28222b08875b18b0dfbf8a947e69df21a9ed5c51a6bf91cfb944ac \
--hash=sha256:3dcfbede6737fdbef3ce9c37af3fb6142e8e1ebc10336daa05872bfb1d87839c \
--hash=sha256:430ee4d85841e1483d487e7b81401785a5dfd69db5de01314538f31f8fbf7ee1 \
--hash=sha256:44a96f2d4c3af51bfac6bc4ef7b182aa33f2f054fd7f34cc0ee9a320d051d41f \
--hash=sha256:479fd0844ddc3ca77e0fd99644c7fe2de8e8be1efcd57705b5c92e5186e8a250 \
--hash=sha256:480f455222cb7a1dea35c57a67578848537d2602b46c464472c995297117fa09 \
--hash=sha256:4829cf2195838e3f93b70fd3b4292156fc5e097aac3739859ac0dcc722b27ac0 \
--hash=sha256:4b6146e439af4c2472c56f8540d799a67a81226e11992008cb47e1267a9b3225 \
--hash=sha256:4e6c3da13e5a57e4b3dca2de059f243ebec705857522f188f0180ae88badd354 \
--hash=sha256:5b24a579123fa884f3a3caadaed7b75eb5715ee2b17ab5c66ac97d29b18fe57f \
--hash=sha256:6b0dd04483499d1de9c8f6203f8975caf17a6000b9c0c54630cef02e44ee624e \
--hash=sha256:6ea2b2258eff652c82652d5e0f02bd5e0463a6a52abb78e49ac288827aaa1469 \
--hash=sha256:7122a99831f9e7fe977dc45784d3b2edc821c172d545e6420c375e5a935f5a1c \
--hash=sha256:74f4544f5a6405b90da8ea724d15ac9c36da4d72a738c64685003337401f5c12 \
--hash=sha256:75ef0640403f945f3a1f9f6400686560dbfb0fb5b16589ad62cd477043c4eee3 \
--hash=sha256:76ac14cd57df0572453543f8f2575e2d01ae9e790c21f57627803f5e79b0d3c3 \
--hash=sha256:77d325ed866876c0fa6492598ec01fe30e803272a6e8b10e992288b009cbe149 \
--hash=sha256:7c4c17f8157bd520cdb7195f75ddbd31671997cbe10aee559c2d613592e7d7eb \
--hash=sha256:7db8539039698ddfb9a524b4dd19508256107568cdad24f3682d5773e60504a2 \
--hash=sha256:8272527d08450ab16eb405f47e0f4ef0e5ff5981c3d82afe0efd25dcbef2bcd2 \
--hash=sha256:82763b46053727a7168d29c772ed5c870fdae2f61aa8a25994c7984a19b1021f \
--hash=sha256:8a9c9b168b3a19e37fe2778c0003359f07822c90fdff8f98d9d2a91b3144d8e0 \
--hash=sha256:8de062de550f63185e4c1c54151bdddfc5625e37daf0aa1e75d2a1293e3b7d9a \
--hash=sha256:974683d4618c0c7dbf4f69c95a979734bf183d0658611760017f6e70a145af58 \
--hash=sha256:9ea2c232deedcb605e853ae1db2cc94f7390ac776743b699b50b071b02bea6fe \
--hash=sha256:a0c6a008e91d10a2564edbb6ee5069a9e66df3fbe11c9a005cb411f441fd2c09 \
--hash=sha256:a763bc0e58504cc803739e7df040685816145a6f3c8a589787084b54ebc9f16e \
--hash=sha256:a7e19150d215c7a13f39eb787d84db274298d3f83d85463e61d277bbd7f401d2 \
--hash=sha256:ac7cf6222b29fbda9e3a472b41e6a5538b48f2c8f99261eecd60aafbdb60690c \
--hash=sha256:b48b3db6bb6e0a08fa8c83b47bc169623f801e5cc4f24442ab2b6617da3b5313 \
--hash=sha256:b58d3795dafa334fc8fd46f7c5dc013e6ad06fd5b9a4cc98cb1456e7d3558bd6 \
--hash=sha256:bdbb61dcc365dd9be94e8f7df91975edc9364d6a78c8f7adb69c1cdff318ec93 \
--hash=sha256:bf6ba8ebc8ef5792e2337fb0419f8009729335bb400ece005606336b7fd7bab7 \
--hash=sha256:c31008598424dfbe52ce8c5b47e0752dca918a4fdc4a2a32004efd9fab41d866 \
--hash=sha256:cb61938aec8b0ffb6eef484d480188a1777e67b05d58e41b435c74b9d84e0b9c \
--hash=sha256:d2d9f990623f15c0ae7ac608103c33dfe1486d2ed974ac3f40b693bad1a22a7b \
--hash=sha256:d352ee8ac1926d6193f602cbe36b1643bbd1bbcb25e3c1a657a4390f3000c9a5 \
--hash=sha256:d374d36726746c81a49f3ff8daa2898dccab6596864ebe43d50733275c629175 \
--hash=sha256:de817e2f5fc75a9e7dd350c4b0f54617b280e26d1631811a43e7e968fa71e3e9 \
--hash=sha256:e724cebe1fadc2b23c6f7415bad5ee6239e00a69f30ee423f319c6af70e2a5c0 \
--hash=sha256:e72591bcfe7512353bd609875ab38050efe3d55e18934e2f18950c108334b4ff \
--hash=sha256:e76be12658a6fa376fcd331b1ea4e58f5a06fd0220653450f0d415b8fd0fbe20 \
--hash=sha256:eb8d384a24778abf29afb8e41d68fdd9a156cf6e5390c04cc07bbc24b89e98b5 \
--hash=sha256:ed350d6978d28b92939bfeb1a0570c523f6170efc3f0a0ef1f1df287cd4f4960 \
--hash=sha256:eef44224729e9525d5261cc8d28d6b11cafc90e6bd0be2157bde69a52ec83024 \
--hash=sha256:f4db56635b58cd1a200b0a23744ff44206ee6aa428185e2b6c4a65b3197abdcd \
--hash=sha256:fdf5197a21dd660cf19dfd2a3ce79574588f8f5e2dbf21bda9ee2d2b46924d84
# via -r requirements.anki.in
packaging==24.1 \
--hash=sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002 \
--hash=sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124
# via
# black
# build
# pytest
pathspec==0.12.1 \
--hash=sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08 \
--hash=sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712
# via black
pip-system-certs==4.0 \
--hash=sha256:47202b9403a6f40783a9674bbc8873f5fc86544ec01a49348fa913e99e2ff68b \
--hash=sha256:db8e6a31388d9795ec9139957df1a89fa5274fb66164456fd091a5d3e94c350c
# via -r requirements.aqt.in
pip-tools==7.4.1 \
--hash=sha256:4c690e5fbae2f21e87843e89c26191f0d9454f362d8acdbd695716493ec8b3a9 \
--hash=sha256:864826f5073864450e24dbeeb85ce3920cdfb09848a3d69ebf537b521f14bcc9
# via
# -r requirements.base.in
# -r requirements.dev.in
platformdirs==4.2.2 \
--hash=sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee \
--hash=sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3
# via
# black
# pylint
pluggy==1.5.0 \
--hash=sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1 \
--hash=sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669
# via pytest
protobuf==5.28.2 \
--hash=sha256:2c69461a7fcc8e24be697624c09a839976d82ae75062b11a0972e41fd2cd9132 \
--hash=sha256:35cfcb15f213449af7ff6198d6eb5f739c37d7e4f1c09b5d0641babf2cc0c68f \
--hash=sha256:52235802093bd8a2811abbe8bf0ab9c5f54cca0a751fdd3f6ac2a21438bffece \
--hash=sha256:59379674ff119717404f7454647913787034f03fe7049cbef1d74a97bb4593f0 \
--hash=sha256:5e8a95246d581eef20471b5d5ba010d55f66740942b95ba9b872d918c459452f \
--hash=sha256:87317e9bcda04a32f2ee82089a204d3a2f0d3c8aeed16568c7daf4756e4f1fe0 \
--hash=sha256:8ddc60bf374785fb7cb12510b267f59067fa10087325b8e1855b898a0d81d276 \
--hash=sha256:a8b9403fc70764b08d2f593ce44f1d2920c5077bf7d311fefec999f8c40f78b7 \
--hash=sha256:c0ea0123dac3399a2eeb1a1443d82b7afc9ff40241433296769f7da42d142ec3 \
--hash=sha256:ca53faf29896c526863366a52a8f4d88e69cd04ec9571ed6082fa117fac3ab36 \
--hash=sha256:eeea10f3dc0ac7e6b4933d32db20662902b4ab81bf28df12218aa389e9c2102d
# via
# -r requirements.anki.in
# mypy-protobuf
pychromedevtools==1.0.3 \
--hash=sha256:a429968bb18d34322da4ed1b727980d35fbd8104d4e764f6d1850b4ffc6e563b
# via -r requirements.dev.in
pylint==3.2.7 \
--hash=sha256:02f4aedeac91be69fb3b4bea997ce580a4ac68ce58b89eaefeaf06749df73f4b \
--hash=sha256:1b7a721b575eaeaa7d39db076b6e7743c993ea44f57979127c517c6c572c803e
# via -r requirements.dev.in
pyproject-hooks==1.1.0 \
--hash=sha256:4b37730834edbd6bd37f26ece6b44802fb1c1ee2ece0e54ddff8bfc06db86965 \
--hash=sha256:7ceeefe9aec63a1064c18d939bdc3adf2d8aa1988a510afec15151578b232aa2
# via
# build
# pip-tools
pyrsistent==0.20.0 \
--hash=sha256:0724c506cd8b63c69c7f883cc233aac948c1ea946ea95996ad8b1380c25e1d3f \
--hash=sha256:09848306523a3aba463c4b49493a760e7a6ca52e4826aa100ee99d8d39b7ad1e \
--hash=sha256:0f3b1bcaa1f0629c978b355a7c37acd58907390149b7311b5db1b37648eb6958 \
--hash=sha256:21cc459636983764e692b9eba7144cdd54fdec23ccdb1e8ba392a63666c60c34 \
--hash=sha256:2e14c95c16211d166f59c6611533d0dacce2e25de0f76e4c140fde250997b3ca \
--hash=sha256:2e2c116cc804d9b09ce9814d17df5edf1df0c624aba3b43bc1ad90411487036d \
--hash=sha256:4021a7f963d88ccd15b523787d18ed5e5269ce57aa4037146a2377ff607ae87d \
--hash=sha256:4c48f78f62ab596c679086084d0dd13254ae4f3d6c72a83ffdf5ebdef8f265a4 \
--hash=sha256:4f5c2d012671b7391803263419e31b5c7c21e7c95c8760d7fc35602353dee714 \
--hash=sha256:58b8f6366e152092194ae68fefe18b9f0b4f89227dfd86a07770c3d86097aebf \
--hash=sha256:59a89bccd615551391f3237e00006a26bcf98a4d18623a19909a2c48b8e986ee \
--hash=sha256:5cdd7ef1ea7a491ae70d826b6cc64868de09a1d5ff9ef8d574250d0940e275b8 \
--hash=sha256:6288b3fa6622ad8a91e6eb759cfc48ff3089e7c17fb1d4c59a919769314af224 \
--hash=sha256:6d270ec9dd33cdb13f4d62c95c1a5a50e6b7cdd86302b494217137f760495b9d \
--hash=sha256:79ed12ba79935adaac1664fd7e0e585a22caa539dfc9b7c7c6d5ebf91fb89054 \
--hash=sha256:7d29c23bdf6e5438c755b941cef867ec2a4a172ceb9f50553b6ed70d50dfd656 \
--hash=sha256:8441cf9616d642c475684d6cf2520dd24812e996ba9af15e606df5f6fd9d04a7 \
--hash=sha256:881bbea27bbd32d37eb24dd320a5e745a2a5b092a17f6debc1349252fac85423 \
--hash=sha256:8c3aba3e01235221e5b229a6c05f585f344734bd1ad42a8ac51493d74722bbce \
--hash=sha256:a14798c3005ec892bbada26485c2eea3b54109cb2533713e355c806891f63c5e \
--hash=sha256:b14decb628fac50db5e02ee5a35a9c0772d20277824cfe845c8a8b717c15daa3 \
--hash=sha256:b318ca24db0f0518630e8b6f3831e9cba78f099ed5c1d65ffe3e023003043ba0 \
--hash=sha256:c1beb78af5423b879edaf23c5591ff292cf7c33979734c99aa66d5914ead880f \
--hash=sha256:c55acc4733aad6560a7f5f818466631f07efc001fd023f34a6c203f8b6df0f0b \
--hash=sha256:ca52d1ceae015859d16aded12584c59eb3825f7b50c6cfd621d4231a6cc624ce \
--hash=sha256:cae40a9e3ce178415040a0383f00e8d68b569e97f31928a3a8ad37e3fde6df6a \
--hash=sha256:e78d0c7c1e99a4a45c99143900ea0546025e41bb59ebc10182e947cf1ece9174 \
--hash=sha256:ef3992833fbd686ee783590639f4b8343a57f1f75de8633749d984dc0eb16c86 \
--hash=sha256:f058a615031eea4ef94ead6456f5ec2026c19fb5bd6bfe86e9665c4158cf802f \
--hash=sha256:f5ac696f02b3fc01a710427585c855f65cd9c640e14f52abe52020722bb4906b \
--hash=sha256:f920385a11207dc372a028b3f1e1038bb244b3ec38d448e6d8e43c6b3ba20e98 \
--hash=sha256:fed2c3216a605dc9a6ea50c7e84c82906e3684c4e80d2908208f662a6cbf9022
# via jsonschema
pysocks==1.7.1 \
--hash=sha256:08e69f092cc6dbe92a0fdd16eeb9b9ffbc13cadfe5ca4c7bd92ffb078b293299 \
--hash=sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5 \
--hash=sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0
# via requests
pytest==8.3.2 \
--hash=sha256:4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5 \
--hash=sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce
# via -r requirements.dev.in
requests==2.32.4 \
--hash=sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c \
--hash=sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422
# via
# -r requirements.anki.in
# -r requirements.aqt.in
# pychromedevtools
send2trash==1.8.3 \
--hash=sha256:0c31227e0bd08961c7665474a3d1ef7193929fedda4233843689baa056be46c9 \
--hash=sha256:b18e7a3966d99871aefeb00cfbcfdced55ce4871194810fc71f4aa484b953abf
# via -r requirements.aqt.in
six==1.16.0 \
--hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \
--hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254
# via compare-locales
soupsieve==2.6 \
--hash=sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb \
--hash=sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9
# via beautifulsoup4
toml==0.10.2 \
--hash=sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b \
--hash=sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f
# via compare-locales
tomli==2.0.1 \
--hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \
--hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f
# via
# black
# build
# mypy
# pip-tools
# pylint
# pytest
tomlkit==0.13.2 \
--hash=sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde \
--hash=sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79
# via pylint
types-click==7.1.8 \
--hash=sha256:8cb030a669e2e927461be9827375f83c16b8178c365852c060a34e24871e7e81 \
--hash=sha256:b6604968be6401dc516311ca50708a0a28baa7a0cb840efd7412f0dbbff4e092
# via types-flask
types-decorator==5.1.8.20240310 \
--hash=sha256:3af75dc38f5baf65b9b53ea6661ce2056c5ca7d70d620d0b1f620285c1242757 \
--hash=sha256:52e316b03783886a8a2abdc228f7071680ba65894545cd2085ebe3cf88684a0e
# via -r requirements.dev.in
types-flask==1.1.6 \
--hash=sha256:6ab8a9a5e258b76539d652f6341408867298550b19b81f0e41e916825fc39087 \
--hash=sha256:aac777b3abfff9436e6b01f6d08171cf23ea6e5be71cbf773aaabb1c5763e9cf
# via -r requirements.dev.in
types-flask-cors==5.0.0.20240902 \
--hash=sha256:595e5f36056cd128ab905832e055f2e5d116fbdc685356eea4490bc77df82137 \
--hash=sha256:8921b273bf7cd9636df136b66408efcfa6338a935e5c8f53f5eff1cee03f3394
# via -r requirements.dev.in
types-jinja2==2.11.9 \
--hash=sha256:60a1e21e8296979db32f9374d8a239af4cb541ff66447bb915d8ad398f9c63b2 \
--hash=sha256:dbdc74a40aba7aed520b7e4d89e8f0fe4286518494208b35123bcf084d4b8c81
# via types-flask
types-markdown==3.7.0.20240822 \
--hash=sha256:183557c9f4f865bdefd8f5f96a38145c31819271cde111d35557c3bd2069e78d \
--hash=sha256:bec91c410aaf2470ffdb103e38438fbcc53689b00133f19e64869eb138432ad7
# via -r requirements.dev.in
types-markupsafe==1.1.10 \
--hash=sha256:85b3a872683d02aea3a5ac2a8ef590193c344092032f58457287fbf8e06711b1 \
--hash=sha256:ca2bee0f4faafc45250602567ef38d533e877d2ddca13003b319c551ff5b3cc5
# via types-jinja2
types-orjson==3.6.2 \
--hash=sha256:22ee9a79236b6b0bfb35a0684eded62ad930a88a56797fa3c449b026cf7dbfe4 \
--hash=sha256:cf9afcc79a86325c7aff251790338109ed6f6b1bab09d2d4262dd18c85a3c638
# via -r requirements.dev.in
types-protobuf==5.27.0.20240626 \
--hash=sha256:683ba14043bade6785e3f937a7498f243b37881a91ac8d81b9202ecf8b191e9c \
--hash=sha256:688e8f7e8d9295db26bc560df01fb731b27a25b77cbe4c1ce945647f7024f5c1
# via
# -r requirements.dev.in
# mypy-protobuf
types-pywin32==306.0.0.20240822 \
--hash=sha256:31a16f7eaf711166e8aec50ee1ddf0f16b4512e19ecc92a019ae7a0860b64bad \
--hash=sha256:34d22b58aaa2cc86fe585b6e2e1eda88a60b010badea0e0e4a410ebe28744645
# via -r requirements.dev.in
types-requests==2.32.0.20240712 \
--hash=sha256:90c079ff05e549f6bf50e02e910210b98b8ff1ebdd18e19c873cd237737c1358 \
--hash=sha256:f754283e152c752e46e70942fa2a146b5bc70393522257bb85bd1ef7e019dcc3
# via -r requirements.dev.in
types-waitress==3.0.0.20240423 \
--hash=sha256:7e9f77a3bc3c20436b9b7ef93da88c8fe0d1e2205d5891ae7526cbd93554f5a4 \
--hash=sha256:ec3af592b5868ccf151645afc74d2e606cd5dec3ed326c9fd0259691b39430fe
# via -r requirements.dev.in
types-werkzeug==1.0.9 \
--hash=sha256:194bd5715a13c598f05c63e8a739328657590943bce941e8a3619a6b5d4a54ec \
--hash=sha256:5cc269604c400133d452a40cee6397655f878fc460e03fde291b9e3a5eaa518c
# via types-flask
typing-extensions==4.12.2 \
--hash=sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d \
--hash=sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8
# via
# -r requirements.anki.in
# astroid
# black
# fluent-syntax
# mypy
# pylint
urllib3==2.2.2 \
--hash=sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472 \
--hash=sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168
# via
# requests
# types-requests
waitress==3.0.1 \
--hash=sha256:26cdbc593093a15119351690752c99adc13cbc6786d75f7b6341d1234a3730ac \
--hash=sha256:ef0c1f020d9f12a515c4ec65c07920a702613afcad1dbfdc3bcec256b6c072b3
# via -r requirements.aqt.in
websocket-client==1.8.0 \
--hash=sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526 \
--hash=sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da
# via pychromedevtools
werkzeug==3.0.6 \
--hash=sha256:1bc0c2310d2fbb07b1dd1105eba2f7af72f322e1e455f2f93c993bee8c8a5f17 \
--hash=sha256:a8dd59d4de28ca70471a34cba79bed5f7ef2e036a76b3ab0835474246eb41f8d
# via
# flask
# flask-cors
wheel==0.44.0 \
--hash=sha256:2376a90c98cc337d18623527a97c31797bd02bad0033d41547043a1cbfbe448f \
--hash=sha256:a29c3f2817e95ab89aa4660681ad547c0e9547f20e75b0562fe7723c9a2a9d49
# via pip-tools
wrapt==1.16.0 \
--hash=sha256:0d2691979e93d06a95a26257adb7bfd0c93818e89b1406f5a28f36e0d8c1e1fc \
--hash=sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81 \
--hash=sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09 \
--hash=sha256:1acd723ee2a8826f3d53910255643e33673e1d11db84ce5880675954183ec47e \
--hash=sha256:1ca9b6085e4f866bd584fb135a041bfc32cab916e69f714a7d1d397f8c4891ca \
--hash=sha256:1dd50a2696ff89f57bd8847647a1c363b687d3d796dc30d4dd4a9d1689a706f0 \
--hash=sha256:2076fad65c6736184e77d7d4729b63a6d1ae0b70da4868adeec40989858eb3fb \
--hash=sha256:2a88e6010048489cda82b1326889ec075a8c856c2e6a256072b28eaee3ccf487 \
--hash=sha256:3ebf019be5c09d400cf7b024aa52b1f3aeebeff51550d007e92c3c1c4afc2a40 \
--hash=sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c \
--hash=sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060 \
--hash=sha256:44a2754372e32ab315734c6c73b24351d06e77ffff6ae27d2ecf14cf3d229202 \
--hash=sha256:490b0ee15c1a55be9c1bd8609b8cecd60e325f0575fc98f50058eae366e01f41 \
--hash=sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9 \
--hash=sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b \
--hash=sha256:5f15814a33e42b04e3de432e573aa557f9f0f56458745c2074952f564c50e664 \
--hash=sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d \
--hash=sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362 \
--hash=sha256:66dfbaa7cfa3eb707bbfcd46dab2bc6207b005cbc9caa2199bcbc81d95071a00 \
--hash=sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc \
--hash=sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1 \
--hash=sha256:6a42cd0cfa8ffc1915aef79cb4284f6383d8a3e9dcca70c445dcfdd639d51267 \
--hash=sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956 \
--hash=sha256:6f6eac2360f2d543cc875a0e5efd413b6cbd483cb3ad7ebf888884a6e0d2e966 \
--hash=sha256:72554a23c78a8e7aa02abbd699d129eead8b147a23c56e08d08dfc29cfdddca1 \
--hash=sha256:73870c364c11f03ed072dda68ff7aea6d2a3a5c3fe250d917a429c7432e15228 \
--hash=sha256:73aa7d98215d39b8455f103de64391cb79dfcad601701a3aa0dddacf74911d72 \
--hash=sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d \
--hash=sha256:7bd2d7ff69a2cac767fbf7a2b206add2e9a210e57947dd7ce03e25d03d2de292 \
--hash=sha256:807cc8543a477ab7422f1120a217054f958a66ef7314f76dd9e77d3f02cdccd0 \
--hash=sha256:8e9723528b9f787dc59168369e42ae1c3b0d3fadb2f1a71de14531d321ee05b0 \
--hash=sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36 \
--hash=sha256:9153ed35fc5e4fa3b2fe97bddaa7cbec0ed22412b85bcdaf54aeba92ea37428c \
--hash=sha256:9159485323798c8dc530a224bd3ffcf76659319ccc7bbd52e01e73bd0241a0c5 \
--hash=sha256:941988b89b4fd6b41c3f0bfb20e92bd23746579736b7343283297c4c8cbae68f \
--hash=sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73 \
--hash=sha256:98b5e1f498a8ca1858a1cdbffb023bfd954da4e3fa2c0cb5853d40014557248b \
--hash=sha256:9b201ae332c3637a42f02d1045e1d0cccfdc41f1f2f801dafbaa7e9b4797bfc2 \
--hash=sha256:a0ea261ce52b5952bf669684a251a66df239ec6d441ccb59ec7afa882265d593 \
--hash=sha256:a33a747400b94b6d6b8a165e4480264a64a78c8a4c734b62136062e9a248dd39 \
--hash=sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389 \
--hash=sha256:a86373cf37cd7764f2201b76496aba58a52e76dedfaa698ef9e9688bfd9e41cf \
--hash=sha256:ac83a914ebaf589b69f7d0a1277602ff494e21f4c2f743313414378f8f50a4cf \
--hash=sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89 \
--hash=sha256:b3646eefa23daeba62643a58aac816945cadc0afaf21800a1421eeba5f6cfb9c \
--hash=sha256:b47cfad9e9bbbed2339081f4e346c93ecd7ab504299403320bf85f7f85c7d46c \
--hash=sha256:b935ae30c6e7400022b50f8d359c03ed233d45b725cfdd299462f41ee5ffba6f \
--hash=sha256:bb2dee3874a500de01c93d5c71415fcaef1d858370d405824783e7a8ef5db440 \
--hash=sha256:bc57efac2da352a51cc4658878a68d2b1b67dbe9d33c36cb826ca449d80a8465 \
--hash=sha256:bf5703fdeb350e36885f2875d853ce13172ae281c56e509f4e6eca049bdfb136 \
--hash=sha256:c31f72b1b6624c9d863fc095da460802f43a7c6868c5dda140f51da24fd47d7b \
--hash=sha256:c5cd603b575ebceca7da5a3a251e69561bec509e0b46e4993e1cac402b7247b8 \
--hash=sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3 \
--hash=sha256:d462f28826f4657968ae51d2181a074dfe03c200d6131690b7d65d55b0f360f8 \
--hash=sha256:d5e49454f19ef621089e204f862388d29e6e8d8b162efce05208913dde5b9ad6 \
--hash=sha256:da4813f751142436b075ed7aa012a8778aa43a99f7b36afe9b742d3ed8bdc95e \
--hash=sha256:db2e408d983b0e61e238cf579c09ef7020560441906ca990fe8412153e3b291f \
--hash=sha256:db98ad84a55eb09b3c32a96c576476777e87c520a34e2519d3e59c44710c002c \
--hash=sha256:dbed418ba5c3dce92619656802cc5355cb679e58d0d89b50f116e4a9d5a9603e \
--hash=sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8 \
--hash=sha256:decbfa2f618fa8ed81c95ee18a387ff973143c656ef800c9f24fb7e9c16054e2 \
--hash=sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020 \
--hash=sha256:eb1b046be06b0fce7249f1d025cd359b4b80fc1c3e24ad9eca33e0dcdb2e4a35 \
--hash=sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d \
--hash=sha256:ed867c42c268f876097248e05b6117a65bcd1e63b779e916fe2e33cd6fd0d3c3 \
--hash=sha256:edfad1d29c73f9b863ebe7082ae9321374ccb10879eeabc84ba3b69f2579d537 \
--hash=sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809 \
--hash=sha256:f6b2d0c6703c988d334f297aa5df18c45e97b0af3679bb75059e0e0bd8b1069d \
--hash=sha256:f8212564d49c50eb4565e502814f694e240c55551a5f1bc841d4fcaabb0a9b8a \
--hash=sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4
# via pip-system-certs
zipp==3.20.1 \
--hash=sha256:9960cd8967c8f85a56f920d5d507274e74f9ff813a0ab8889a5b5be2daf44064 \
--hash=sha256:c22b14cc4763c5a5b04134207736c107db42e9d3ef2d9779d465f5f1bcba572b
# via importlib-metadata
# The following packages are considered to be unsafe in a requirements file:
pip==24.2 \
--hash=sha256:2cd581cf58ab7fcfca4ce8efa6dcacd0de5bf8d0a3eb9ec927e07405f4d9e2a2 \
--hash=sha256:5b5e490b5e9cb275c879595064adce9ebd31b854e3e803740b72f9ccf34a45b8
# via pip-tools
setuptools==75.1.0 \
--hash=sha256:35ab7fd3bcd95e6b7fd704e4a1539513edad446c097797f2985e0e4b960772f2 \
--hash=sha256:d59a21b17a275fb872a9c3dae73963160ae079f1049ed956880cd7c09b120538
# via pip-tools

View file

@ -1,3 +0,0 @@
pyqt5==5.14.1
pyqtwebengine==5.14.0
pyqt5_sip==12.8.1

View file

@ -1,42 +0,0 @@
pyqt5==5.14.1 \
--hash=sha256:2d94ec761fb656707050c68b41958e3a9f755bb1df96c064470f4096d2899e32 \
--hash=sha256:2f230f2dbd767099de7a0cb915abdf0cbc3256a0b5bb910eb09b99117db7a65b \
--hash=sha256:31b142a868152d60c6323e0527edb692fdf05fd7cb4fe2fe9ce07d1ce560221a \
--hash=sha256:713b9a201f5e7b2fca8691373e5d5c8c2552a51d87ca9ffbb1461e34e3241211 \
--hash=sha256:a0bfe9fd718bca4de3e33000347e048f73126b6dc46530eb020b0251a638ee9d
# via
# -r requirements.in
# pyqtwebengine
pyqt5-sip==12.8.1 \
--hash=sha256:0304ca9114b9817a270f67f421355075b78ff9fc25ac58ffd72c2601109d2194 \
--hash=sha256:0cd969be528c27bbd4755bd323dff4a79a8fdda28215364e6ce3e069cb56c2a9 \
--hash=sha256:2f35e82fd7ec1e1f6716e9154721c7594956a4f5bd4f826d8c6a6453833cc2f0 \
--hash=sha256:30e944db9abee9cc757aea16906d4198129558533eb7fadbe48c5da2bd18e0bd \
--hash=sha256:34dcd29be47553d5f016ff86e89e24cbc5eebae92eb2f96fb32d2d7ba028c43c \
--hash=sha256:5a011aeff89660622a6d5c3388d55a9d76932f3b82c95e82fc31abd8b1d2990d \
--hash=sha256:6c1ebee60f1d2b3c70aff866b7933d8d8d7646011f7c32f9321ee88c290aa4f9 \
--hash=sha256:7b81382ce188d63890a0e35abe0f9bb946cabc873a31873b73583b0fc84ac115 \
--hash=sha256:832fd60a264de4134c2824d393320838f3ab648180c9c357ec58a74524d24507 \
--hash=sha256:84ba7746762bd223bed22428e8561aa267a229c28344c2d28c5d5d3f8970cffb \
--hash=sha256:9312ec47cac4e33c11503bc1cbeeb0bdae619620472f38e2078c5a51020a930f \
--hash=sha256:a1b8ef013086e224b8e86c93f880f776d01b59195bdfa2a8e0b23f0480678fec \
--hash=sha256:a29e2ac399429d3b7738f73e9081e50783e61ac5d29344e0802d0dcd6056c5a2 \
--hash=sha256:b6d42250baec52a5f77de64e2951d001c5501c3a2df2179f625b241cbaec3369 \
--hash=sha256:bb5a87b66fc1445915104ee97f7a20a69decb42f52803e3b0795fa17ff88226c \
--hash=sha256:c317ab1263e6417c498b81f5c970a9b1af7acefab1f80b4cc0f2f8e661f29fc5 \
--hash=sha256:c9800729badcb247765e4ffe2241549d02da1fa435b9db224845bc37c3e99cb0 \
--hash=sha256:c9d6d448c29dc6606bb7974696608f81f4316c8234f7c7216396ed110075e777 \
--hash=sha256:da9c9f1e65b9d09e73bd75befc82961b6b61b5a3b9d0a7c832168e1415f163c6 \
--hash=sha256:ed897c58acf4a3cdca61469daa31fe6e44c33c6c06a37c3f21fab31780b3b86a \
--hash=sha256:f168f0a7f32b81bfeffdf003c36f25d81c97dee5eb67072a5183e761fe250f13
# via
# -r requirements.in
# pyqt5
# pyqtwebengine
pyqtwebengine==5.14.0 \
--hash=sha256:01cd7f38ba4efa5f4c0983219ab15dad7747a0ca9378c7832a3077a53988f5ea \
--hash=sha256:37c4a820c5bcc82a6cb43ad33b8c81eee4c4772fc03e180a8fa37a59f99f6a48 \
--hash=sha256:3d0cba04f64d4f66087cc92e254ff8b33ec4a4e6c7751417fe2bd53c3ed740a7 \
--hash=sha256:85e1fac1b2c9bebf0b2e8cd9a75c14a38aad75165a8d8bcb8f6318944b779b25 \
--hash=sha256:e11595051f8bfbfa49175d899b2c8c2eea3a3deac4141edf4db68c3555221c92
# via -r requirements.in

View file

@ -1,3 +0,0 @@
pyqt5==5.15.5
pyqtwebengine==5.15.5
pyqt5_sip==12.9.0

View file

@ -1,54 +0,0 @@
pyqt5==5.15.5 \
--hash=sha256:521130eea1eaac55cc6867b1dc627d292b6468fb8e525ce2a015cdf39028d6e8 \
--hash=sha256:5966fb291f316f8e35bc8775dda63acf1bb9855baeb5af3e33d3e7c4f1cd98d4 \
--hash=sha256:85e76b7a96995b9da12083850bf2a9f4f0aeba2b0b99461b3337ad7e44f428c3 \
--hash=sha256:b411b7a8fa03901c9feb1dcbac7ea1fc3ce20b9ae682762b777cd5398749ca2b \
--hash=sha256:b8e23c1a3fe1b7749c9106f36fba0bd4676dc77bcacca95304c6b840b782e24d
# via
# -r requirements.in
# pyqtwebengine
pyqt5-qt5==5.15.2 \
--hash=sha256:1988f364ec8caf87a6ee5d5a3a5210d57539988bf8e84714c7d60972692e2f4a \
--hash=sha256:750b78e4dba6bdf1607febedc08738e318ea09e9b10aea9ff0d73073f11f6962 \
--hash=sha256:76980cd3d7ae87e3c7a33bfebfaee84448fd650bad6840471d6cae199b56e154 \
--hash=sha256:9cc7a768b1921f4b982ebc00a318ccb38578e44e45316c7a4a850e953e1dd327
# via pyqt5
pyqt5-sip==12.9.0 \
--hash=sha256:055581c6fed44ba4302b70eeb82e979ff70400037358908f251cd85cbb3dbd93 \
--hash=sha256:0fc9aefacf502696710b36cdc9fa2a61487f55ee883dbcf2c2a6477e261546f7 \
--hash=sha256:42274a501ab4806d2c31659170db14c282b8313d2255458064666d9e70d96206 \
--hash=sha256:4347bd81d30c8e3181e553b3734f91658cfbdd8f1a19f254777f906870974e6d \
--hash=sha256:485972daff2fb0311013f471998f8ec8262ea381bded244f9d14edaad5f54271 \
--hash=sha256:4f8e05fe01d54275877c59018d8e82dcdd0bc5696053a8b830eecea3ce806121 \
--hash=sha256:69a3ad4259172e2b1aa9060de211efac39ddd734a517b1924d9c6c0cc4f55f96 \
--hash=sha256:6a8701892a01a5a2a4720872361197cc80fdd5f49c8482d488ddf38c9c84f055 \
--hash=sha256:6d5bca2fc222d58e8093ee8a81a6e3437067bb22bc3f86d06ec8be721e15e90a \
--hash=sha256:83c3220b1ca36eb8623ba2eb3766637b19eb0ce9f42336ad8253656d32750c0a \
--hash=sha256:a25b9843c7da6a1608f310879c38e6434331aab1dc2fe6cb65c14f1ecf33780e \
--hash=sha256:ac57d796c78117eb39edd1d1d1aea90354651efac9d3590aac67fa4983f99f1f \
--hash=sha256:b09f4cd36a4831229fb77c424d89635fa937d97765ec90685e2f257e56a2685a \
--hash=sha256:c446971c360a0a1030282a69375a08c78e8a61d568bfd6dab3dcc5cf8817f644 \
--hash=sha256:c5216403d4d8d857ec4a61f631d3945e44fa248aa2415e9ee9369ab7c8a4d0c7 \
--hash=sha256:d3e4489d7c2b0ece9d203ae66e573939f7f60d4d29e089c9f11daa17cfeaae32 \
--hash=sha256:d59af63120d1475b2bf94fe8062610720a9be1e8940ea146c7f42bb449d49067 \
--hash=sha256:d85002238b5180bce4b245c13d6face848faa1a7a9e5c6e292025004f2fd619a \
--hash=sha256:d8b2bdff7bbf45bc975c113a03b14fd669dc0c73e1327f02706666a7dd51a197 \
--hash=sha256:dd05c768c2b55ffe56a9d49ce6cc77cdf3d53dbfad935258a9e347cbfd9a5850 \
--hash=sha256:fc43f2d7c438517ee33e929e8ae77132749c15909afab6aeece5fcf4147ffdb5
# via
# -r requirements.in
# pyqt5
# pyqtwebengine
pyqtwebengine==5.15.5 \
--hash=sha256:30cebf455406990d5a0b859eac261ba6b45c32ce18956271733e0e96dbdca9b7 \
--hash=sha256:5c77f71d88d871bc7400c68ef6433fadc5bd57b86d1a9c4d8094cea42f3607f1 \
--hash=sha256:782aeee6bc8699bc029fe5c169a045c2bc9533d781cf3f5e9fb424b85a204e68 \
--hash=sha256:ab47608dccf2b5e4b950d5a3cc704b17711af035024d07a9b71ad29fc103b941 \
--hash=sha256:b827ad7ba0a65d5cd176797478f0ec8f599df6746b06c548649ff5674482a022
# via -r requirements.in
pyqtwebengine-qt5==5.15.2 \
--hash=sha256:24231f19e1595018779977de6722b5c69f3d03f34a5f7574ff21cd1e764ef76d \
--hash=sha256:9e80b408d8de09d4e708d5d84c3ceaf3603292ff8f5e566ae44bb0320fa59c33 \
--hash=sha256:bc7b1fd1f4f8138d59b0b0245d601fb2c5c0aa1e1e7e853b713e52a3165d147e \
--hash=sha256:ec2acb1780c0124ef060c310e00ca701f388d8b6c35bba9127f7a6f0dc536f77
# via pyqtwebengine

View file

@ -1,5 +0,0 @@
pyqt6==6.6.1
pyqt6-qt6==6.6.2
pyqt6-webengine==6.6.0
pyqt6-webengine-qt6==6.6.2
pyqt6_sip==13.6.0

View file

@ -1,56 +0,0 @@
pyqt6==6.6.1 \
--hash=sha256:03a656d5dc5ac31b6a9ad200f7f4f7ef49fa00ad7ce7a991b9bb691617141d12 \
--hash=sha256:5aa0e833cb5a79b93813f8181d9f145517dd5a46f4374544bcd1e93a8beec537 \
--hash=sha256:6b43878d0bbbcf8b7de165d305ec0cb87113c8930c92de748a11c473a6db5085 \
--hash=sha256:9f158aa29d205142c56f0f35d07784b8df0be28378d20a97bcda8bd64ffd0379
# via
# -r requirements.qt6_6.in
# pyqt6-webengine
pyqt6-qt6==6.6.2 \
--hash=sha256:5a41fe9d53b9e29e9ec5c23f3c5949dba160f90ca313ee8b96b8ffe6a5059387 \
--hash=sha256:7ef446d3ffc678a8586ff6dc9f0d27caf4dff05dea02c353540d2f614386faf9 \
--hash=sha256:8d7f674a4ec43ca00191e14945ca4129acbe37a2172ed9d08214ad58b170bc11 \
--hash=sha256:b8363d88623342a72ac17da9127dc12f259bb3148796ea029762aa2d499778d9
# via
# -r requirements.qt6_6.in
# pyqt6
pyqt6-sip==13.6.0 \
--hash=sha256:0dfd22cfedd87e96f9d51e0778ca2ba3dc0be83e424e9e0f98f6994d8d9c90f0 \
--hash=sha256:13885361ca2cb2f5085d50359ba61b3fabd41b139fb58f37332acbe631ef2357 \
--hash=sha256:24441032a29791e82beb7dfd76878339058def0e97fdb7c1cea517f3a0e6e96b \
--hash=sha256:2486e1588071943d4f6657ba09096dc9fffd2322ad2c30041e78ea3f037b5778 \
--hash=sha256:3075d8b325382750829e6cde6971c943352309d35768a4d4da0587459606d562 \
--hash=sha256:33ea771fe777eb0d1a2c3ef35bcc3f7a286eb3ff09cd5b2fdd3d87d1f392d7e8 \
--hash=sha256:39854dba35f8e5a4288da26ecb5f40b4c5ec1932efffb3f49d5ea435a7f37fb3 \
--hash=sha256:3bf03e130fbfd75c9c06e687b86ba375410c7a9e835e4e03285889e61dd4b0c4 \
--hash=sha256:43fb8551796030aae3d66d6e35e277494071ec6172cd182c9569ab7db268a2f5 \
--hash=sha256:58f68a48400e0b3d1ccb18090090299bad26e3aed7ccb7057c65887b79b8aeea \
--hash=sha256:5b9c6b6f9cfccb48cbb78a59603145a698fb4ffd176764d7083e5bf47631d8df \
--hash=sha256:747f6ca44af81777a2c696bd501bc4815a53ec6fc94d4e25830e10bc1391f8ab \
--hash=sha256:86a7b67c64436e32bffa9c28c9f21bf14a9faa54991520b12c3f6f435f24df7f \
--hash=sha256:8c282062125eea5baf830c6998587d98c50be7c3a817a057fb95fef647184012 \
--hash=sha256:8f9df9f7ccd8a9f0f1d36948c686f03ce1a1281543a3e636b7b7d5e086e1a436 \
--hash=sha256:98bf954103b087162fa63b3a78f30b0b63da22fd6450b610ec1b851dbb798228 \
--hash=sha256:9adf672f9114687533a74d5c2d4c03a9a929ad5ad9c3e88098a7da1a440ab916 \
--hash=sha256:a6ce80bc24618d8a41be8ca51ad9f10e8bc4296dd90ab2809573df30a23ae0e5 \
--hash=sha256:d6b5f699aaed0ac1fcd23e8fbca70d8a77965831b7c1ce474b81b1678817a49d \
--hash=sha256:fa759b6339ff7e25f9afe2a6b651b775f0a36bcb3f5fa85e81a90d3b033c83f4 \
--hash=sha256:fa7b10af7488efc5e53b41dd42c0f421bde6c2865a107af7ae259aff9d841da9
# via
# -r requirements.qt6_6.in
# pyqt6
# pyqt6-webengine
pyqt6-webengine==6.6.0 \
--hash=sha256:9d542738ed6e11c1978ce59035c07627def7c63eef0f59581d327f01209141bc \
--hash=sha256:cb7793f06525ca054fcc6039afd93e23b82228b880d0b1301ce635f7f3ed2edf \
--hash=sha256:d50b984c3f85e409e692b156132721522d4e8cf9b6c25e0cf927eea2dfb39487 \
--hash=sha256:fded35fba636c4916fec84aa7c6840ad2e75d211462feb3e966f9545a59d56e6
# via -r requirements.qt6_6.in
pyqt6-webengine-qt6==6.6.2 \
--hash=sha256:27b1b6a6f4ea115b3dd300d2df906d542009d9eb0e62b05e6b7cb85dfe68e9c3 \
--hash=sha256:3da4db9ddd984b647d0b79fa10fc6cf65364dfe283cd702b12cb7164be2307cd \
--hash=sha256:5d6f3ae521115cee77fea22b0248e7b219995390b951b51e4d519aef9c304ca8 \
--hash=sha256:f2364dfa3a6e751ead71b7ba759081be677fcf1c6bbd8a2a2a250eb5f06432e8
# via
# -r requirements.qt6_6.in
# pyqt6-webengine

View file

@ -1,5 +0,0 @@
pyqt6==6.8.0
pyqt6-qt6==6.8.1
pyqt6-webengine==6.8.0
pyqt6-webengine-qt6==6.8.1
pyqt6_sip==13.9.1

View file

@ -1,71 +0,0 @@
pyqt6==6.8.0 \
--hash=sha256:3a4354816f11e812b727206a9ea6e79ff3774f1bb7228ad4b9318442d2c64ff9 \
--hash=sha256:452bae5840077bf0f146c798d7777f70d7bdd0c7dcfa9ee7a415c1daf2d10038 \
--hash=sha256:48bace7b87676bba5e6114482f3a20ca20be90c7f261b5d340464313f5f2bf5e \
--hash=sha256:6d8628de4c2a050f0b74462e4c9cb97f839bf6ffabbca91711722ffb281570d9 \
--hash=sha256:8c5c05f5fdff31a5887dbc29b27615b09df467631238d7b449283809ffca6228 \
--hash=sha256:a9913d479f1ffee804bf7f232079baea4fb4b221a8f4890117588917a54ea30d \
--hash=sha256:cf7123caea14e7ecf10bd12cae48e8d9970ef7caf627bc7d7988b0baa209adb3
# via
# -r requirements.qt6_8.in
# pyqt6-webengine
pyqt6-qt6==6.8.1 \
--hash=sha256:006d786693d0511fbcf184a862edbd339c6ed1bb3bd9de363d73a19ed4b23dff \
--hash=sha256:08065d595f1e6fc2dde9f4450eeff89082f4bad26f600a8e9b9cc5966716bfcf \
--hash=sha256:1eb8460a1fdb38d0b2458c2974c01d471c1e59e4eb19ea63fc447aaba3ad530e \
--hash=sha256:20843cb86bd94942d1cd99e39bf1aeabb875b241a35a8ab273e4bbbfa63776db \
--hash=sha256:2f4b8b55b1414b93f340f22e8c88d25550efcdebc4b65a3927dd947b73bd4358 \
--hash=sha256:98aa99fe38ae68c5318284cd28f3479ba538c40bf6ece293980abae0925c1b24 \
--hash=sha256:9f3790c4ce4dc576e48b8718d55fb8743057e6cbd53a6ca1dd253ffbac9b7287 \
--hash=sha256:a8bc2ed4ee5e7c6ff4dd1c7db0b27705d151fee5dc232bbd1bf17618f937f515 \
--hash=sha256:d6ca5d2b9d2ec0ee4a814b2175f641a5c4299cb80b45e0f5f8356632663f89b3
# via
# -r requirements.qt6_8.in
# pyqt6
pyqt6-sip==13.9.1 \
--hash=sha256:14f95c6352e3b85dc26bf59cfbf77a470ecbd5fcdcf00af4b648f0e1b9eefb9e \
--hash=sha256:15be741d1ae8c82bb7afe9a61f3cf8c50457f7d61229a1c39c24cd6e8f4d86dc \
--hash=sha256:1d322ded1d1fea339cc6ac65b768e72c69c486eebb7db6ccde061b5786d74cc5 \
--hash=sha256:1ec52e962f54137a19208b6e95b6bd9f7a403eb25d7237768a99306cd9db26d1 \
--hash=sha256:1fb405615970e85b622b13b4cad140ff1e4182eb8334a0b27a4698e6217b89b0 \
--hash=sha256:22d66256b800f552ade51a463510bf905f3cb318aae00ff4288fae4de5d0e600 \
--hash=sha256:2ab85aaf155828331399c59ebdd4d3b0358e42c08250e86b43d56d9873df148a \
--hash=sha256:3c269052c770c09b61fce2f2f9ea934a67dfc65f443d59629b4ccc8f89751890 \
--hash=sha256:5004514b08b045ad76425cf3618187091a668d972b017677b1b4b193379ef553 \
--hash=sha256:552ff8fdc41f5769d3eccc661f022ed496f55f6e0a214c20aaf56e56385d61b6 \
--hash=sha256:5643c92424fe62cb0b33378fef3d28c1525f91ada79e8a15bd9a05414a09503d \
--hash=sha256:56ce0afb19cd8a8c63ff93ae506dffb74f844b88adaa6673ebc0dec43af48a76 \
--hash=sha256:57b5312ef13c1766bdf69b317041140b184eb24a51e1e23ce8fc5386ba8dffb2 \
--hash=sha256:5d7726556d1ca7a7ed78e19ba53285b64a2a8f6ad7ff4cb18a1832efca1a3102 \
--hash=sha256:69a879cfc94f4984d180321b76f52923861cd5bf4969aa885eef7591ee932517 \
--hash=sha256:6e6c1e2592187934f4e790c0c099d0033e986dcef7bdd3c06e3895ffa995e9fc \
--hash=sha256:8b2ac36d6e04db6099614b9c1178a2f87788c7ffc3826571fb63d36ddb4c401d \
--hash=sha256:8c207528992d59b0801458aa6fcff118e5c099608ef0fc6ff8bccbdc23f29c04 \
--hash=sha256:976c7758f668806d4df7a8853f390ac123d5d1f73591ed368bdb8963574ff589 \
--hash=sha256:accab6974b2758296400120fdcc9d1f37785b2ea2591f00656e1776f058ded6c \
--hash=sha256:c1942e107b0243ced9e510d507e0f27aeea9d6b13e0a1b7c06fd52a62e0d41f7 \
--hash=sha256:c800db3464481e87b1d2b84523b075df1e8fc7856c6f9623dc243f89be1cb604 \
--hash=sha256:e996d320744ca8342cad6f9454345330d4f06bce129812d032bda3bad6967c5c \
--hash=sha256:fa27b51ae4c7013b3700cf0ecf46907d1333ae396fc6511311920485cbce094b
# via
# -r requirements.qt6_8.in
# pyqt6
# pyqt6-webengine
pyqt6-webengine==6.8.0 \
--hash=sha256:5b5090dcc71dd36172ca8370db7dcaadfa0a022a8e58f6e172301289036c666b \
--hash=sha256:5b9231b58014965b72504e49f39a6dbc3ecd05d4d725af011d75e6c8a7e2d5f7 \
--hash=sha256:64045ea622b6a41882c2b18f55ae9714b8660acff06a54e910eb72822c2f3ff2 \
--hash=sha256:c549f0f72c285eeea94000f6764dfaebf6bb3b13224580c7169a409bf1bf1bb7 \
--hash=sha256:c7a5731923112acf23fbf93efad91f7b1545221063572106273e34c15a029fe7 \
--hash=sha256:d7366809d681bcc096fa565f2a81d0ab040f7da5bb4f12f78e834a2b173c87d1
# via -r requirements.qt6_8.in
pyqt6-webengine-qt6==6.8.1 \
--hash=sha256:0405b6ce35f406affb27547c6c3608dc82405568af71505fefae4081c8b4ac39 \
--hash=sha256:0ced2a10433da2571cfa29ed882698e0e164184d54068d17ba73799c45af5f0f \
--hash=sha256:79f67a459ecb452f865e04f19122a1d6f30c83d9a1ffd06e7e6f0d652204083a \
--hash=sha256:8059118591641cc9da6616343d893c77fbd065bef3e0764679543345e2c75123 \
--hash=sha256:a375dbb34e03707b0ab4830b61e4d77a31dc3ef880421c8936472f2af34a3f80 \
--hash=sha256:e36574aa55b30633a12aa000835f01e488a0f0c13513fd9a0d50c2281e0a9068
# via
# -r requirements.qt6_8.in
# pyqt6-webengine

View file

@ -1,2 +0,0 @@
pywin32

View file

@ -1,16 +0,0 @@
pywin32==305 \
--hash=sha256:109f98980bfb27e78f4df8a51a8198e10b0f347257d1e265bb1a32993d0c973d \
--hash=sha256:13362cc5aa93c2beaf489c9c9017c793722aeb56d3e5166dadd5ef82da021fe1 \
--hash=sha256:19ca459cd2e66c0e2cc9a09d589f71d827f26d47fe4a9d09175f6aa0256b51c2 \
--hash=sha256:326f42ab4cfff56e77e3e595aeaf6c216712bbdd91e464d167c6434b28d65990 \
--hash=sha256:421f6cd86e84bbb696d54563c48014b12a23ef95a14e0bdba526be756d89f116 \
--hash=sha256:48d8b1659284f3c17b68587af047d110d8c44837736b8932c034091683e05863 \
--hash=sha256:4ecd404b2c6eceaca52f8b2e3e91b2187850a1ad3f8b746d0796a98b4cea04db \
--hash=sha256:50768c6b7c3f0b38b7fb14dd4104da93ebced5f1a50dc0e834594bff6fbe1271 \
--hash=sha256:56d7a9c6e1a6835f521788f53b5af7912090674bb84ef5611663ee1595860fc7 \
--hash=sha256:73e819c6bed89f44ff1d690498c0a811948f73777e5f97c494c152b850fad478 \
--hash=sha256:742eb905ce2187133a29365b428e6c3b9001d79accdc30aa8969afba1d8470f4 \
--hash=sha256:9d968c677ac4d5cbdaa62fd3014ab241718e619d8e36ef8e11fb930515a1e918 \
--hash=sha256:9dd98384da775afa009bc04863426cb30596fd78c6f8e4e2e5bbf4edf8029504 \
--hash=sha256:a55db448124d1c1484df22fa8bbcbc45c64da5e6eae74ab095b9ea62e6d00496
# via -r requirements.win.in

View file

@ -1,25 +0,0 @@
#!/bin/bash
set -e
if [ "$1" == "all" ]; then
upgrade="--upgrade"
elif [ "$1" != "" ]; then
upgrade="--upgrade-package $1"
else
upgrade=""
fi
args="--resolver=backtracking --allow-unsafe --no-header --strip-extras --generate-hashes"
# initial pyenv bootstrap
../out/pyenv/bin/pip-compile $args $upgrade requirements.base.in
# during build/development/testing
../out/pyenv/bin/pip-compile $args $upgrade requirements.dev.in
# during bundle
../out/pyenv/bin/pip-compile $args $upgrade requirements.bundle.in
for i in requirements.{bundle,qt6*}.in; do ../out/pyenv/bin/pip-compile $args $upgrade $i; done

View file

@ -1 +0,0 @@
..\out\pyenv\scripts\pip-compile --resolver=backtracking --allow-unsafe --no-header --strip-extras --generate-hashes requirements.win.in

10
python/version.py Normal file
View file

@ -0,0 +1,10 @@
# Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
"""Version helper for wheel builds."""
import pathlib
# Read version from .version file in project root
_version_file = pathlib.Path(__file__).parent.parent / ".version"
__version__ = _version_file.read_text().strip()

View file

@ -1,191 +0,0 @@
# Based on https://github.com/ziglang/zig-pypi/blob/de14cf728fa35c014821f62a4fa9abd9f4bb560e/make_wheels.py
# MIT
from __future__ import annotations
import os
import sys
from email.message import EmailMessage
from pathlib import Path
from typing import Sequence
from zipfile import ZIP_DEFLATED, ZipInfo
from wheel.wheelfile import WheelFile
def make_message(headers, payload=None):
msg = EmailMessage()
for name, value in headers.items():
if name == "_dependencies":
for dep in value:
if isinstance(dep, ExtraRequires):
msg["Provides-Extra"] = dep.name
for inner_dep in dep.deps:
msg["Requires-Dist"] = f"{inner_dep}; extra == '{dep.name}'"
else:
msg["Requires-Dist"] = dep
elif isinstance(value, list):
for value_part in value:
msg[name] = value_part
else:
msg[name] = value
if payload:
msg.set_payload(payload)
# EmailMessage wraps the license line, which results in an invalid file
out = bytes(msg)
out = out.replace(b"License v3 or\n later", b"License v3 or later")
return out
def write_wheel_file(filename, contents):
with WheelFile(filename, "w") as wheel:
for member_info, member_source in contents.items():
if not isinstance(member_info, ZipInfo):
member_info = ZipInfo(member_info)
member_info.external_attr = 0o644 << 16
member_info.file_size = len(member_source)
member_info.compress_type = ZIP_DEFLATED
wheel.writestr(member_info, bytes(member_source))
return filename
def write_wheel(
wheel_path,
*,
name,
version,
tag,
metadata,
description,
contents,
entrypoints: list[str] | None = None,
top_level: list[str] | None = None,
):
dist_info = f"{name}-{version}.dist-info"
extra = {}
if entrypoints:
entrypoints_joined = "\n".join(entrypoints)
text = f"[console_scripts]\n{entrypoints_joined}"
file = f"{dist_info}/entry_points.txt"
extra[file] = text.encode("utf8")
if top_level:
top_level_joined = "\n".join(top_level) + "\n"
file = f"{dist_info}/top_level.txt"
extra[file] = top_level_joined.encode("utf8")
return write_wheel_file(
wheel_path,
{
**contents,
**extra,
f"{dist_info}/METADATA": make_message(
{
"Metadata-Version": "2.1",
"Name": name,
"Version": version,
**metadata,
},
description,
),
f"{dist_info}/WHEEL": make_message(
{
"Wheel-Version": "1.0",
"Generator": "anki write_wheel.py",
"Root-Is-Purelib": "false",
"Tag": tag,
}
),
},
)
def merge_sources(contents, root, exclude):
root = Path(root)
for path in root.glob("**/*"):
if path.is_dir() or exclude(path):
continue
path_str = str(path.relative_to(root.parent))
if path_str.endswith(".pyc"):
continue
contents[path_str] = path.read_bytes()
def split_wheel_path(path: str):
path2 = Path(path)
components = path2.stem.split("-", maxsplit=2)
return components
class ExtraRequires:
def __init__(self, name, deps):
self.name = name
self.deps = deps
src_root = sys.argv[1]
generated_root = sys.argv[2]
wheel_path = sys.argv[3]
name, version, tag = split_wheel_path(wheel_path)
def exclude_aqt(path: Path) -> bool:
if path.suffix in [".ui", ".scss", ".map", ".ts"]:
return True
if path.name.startswith("tsconfig"):
return True
if "/aqt/data" in str(path):
return True
return False
def exclude_nothing(path: Path) -> bool:
return False
def extract_requirements(path: Path) -> list[str]:
return path.read_text().splitlines()
if name == "aqt":
exclude = exclude_aqt
else:
exclude = exclude_nothing
contents: dict[str, str] = {}
merge_sources(contents, src_root, exclude)
merge_sources(contents, generated_root, exclude)
all_requires: Sequence[str | ExtraRequires]
if name == "anki":
all_requires = extract_requirements(Path("python/requirements.anki.in"))
entrypoints = None
top_level = None
else:
all_requires = extract_requirements(Path("python/requirements.aqt.in")) + [
"anki==" + version,
"pyqt6>=6.2",
"pyqt6-webengine>=6.2",
]
entrypoints = ["anki = aqt:run"]
top_level = ["aqt", "_aqt"]
# reproducible builds
os.environ["SOURCE_DATE_EPOCH"] = "0"
write_wheel(
wheel_path,
name=name,
version=version,
tag=tag,
metadata={
"License": "AGPL-3",
"Classifier": [
"License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)",
],
"Requires-Python": ">=3.9",
"_dependencies": all_requires,
},
description="Please see https://apps.ankiweb.net\n\n",
contents=contents,
entrypoints=entrypoints,
top_level=top_level,
)

View file

@ -5,10 +5,16 @@ from __future__ import annotations
import atexit import atexit
import logging import logging
import os
import sys import sys
from collections.abc import Callable from collections.abc import Callable
from typing import TYPE_CHECKING, Any, Union, cast from typing import TYPE_CHECKING, Any, Union, cast
if "ANKI_FIRST_RUN" in os.environ:
from .package import first_run_setup
first_run_setup()
try: try:
import pip_system_certs.wrapt_requests import pip_system_certs.wrapt_requests
except ModuleNotFoundError: except ModuleNotFoundError:
@ -32,24 +38,14 @@ if "--syncserver" in sys.argv:
from anki.syncserver import run_sync_server from anki.syncserver import run_sync_server
from anki.utils import is_mac from anki.utils import is_mac
from .package import _fix_protobuf_path
if is_mac and getattr(sys, "frozen", False):
_fix_protobuf_path()
# does not return # does not return
run_sync_server() run_sync_server()
from .package import packaged_build_setup
packaged_build_setup()
import argparse import argparse
import builtins import builtins
import cProfile import cProfile
import getpass import getpass
import locale import locale
import os
import tempfile import tempfile
import traceback import traceback
from pathlib import Path from pathlib import Path
@ -270,13 +266,7 @@ def setupLangAndBackend(
# load qt translations # load qt translations
_qtrans = QTranslator() _qtrans = QTranslator()
if is_mac and getattr(sys, "frozen", False): qt_dir = QLibraryInfo.path(QLibraryInfo.LibraryPath.TranslationsPath)
qt_dir = os.path.join(sys.prefix, "../Resources/qt_translations")
else:
if qtmajor == 5:
qt_dir = QLibraryInfo.location(QLibraryInfo.TranslationsPath) # type: ignore
else:
qt_dir = QLibraryInfo.path(QLibraryInfo.LibraryPath.TranslationsPath)
qt_lang = lang.replace("-", "_") qt_lang = lang.replace("-", "_")
if _qtrans.load(f"qtbase_{qt_lang}", qt_dir): if _qtrans.load(f"qtbase_{qt_lang}", qt_dir):
app.installTranslator(_qtrans) app.installTranslator(_qtrans)
@ -607,14 +597,13 @@ def _run(argv: list[str] | None = None, exec: bool = True) -> AnkiApp | None:
profiler = cProfile.Profile() profiler = cProfile.Profile()
profiler.enable() profiler.enable()
packaged = getattr(sys, "frozen", False)
x11_available = os.getenv("DISPLAY") x11_available = os.getenv("DISPLAY")
wayland_configured = qtmajor > 5 and ( wayland_configured = qtmajor > 5 and (
os.getenv("QT_QPA_PLATFORM") == "wayland" or os.getenv("WAYLAND_DISPLAY") os.getenv("QT_QPA_PLATFORM") == "wayland" or os.getenv("WAYLAND_DISPLAY")
) )
wayland_forced = os.getenv("ANKI_WAYLAND") wayland_forced = os.getenv("ANKI_WAYLAND")
if (packaged or is_gnome) and wayland_configured: if is_gnome and wayland_configured:
if wayland_forced or not x11_available: if wayland_forced or not x11_available:
# Work around broken fractional scaling in Wayland # Work around broken fractional scaling in Wayland
# https://bugreports.qt.io/browse/QTBUG-113574 # https://bugreports.qt.io/browse/QTBUG-113574

View file

@ -14,12 +14,7 @@ import aqt.utils
class _MacOSHelper: class _MacOSHelper:
def __init__(self) -> None: def __init__(self) -> None:
if getattr(sys, "frozen", False): path = os.path.join(aqt.utils.aqt_data_folder(), "lib", "libankihelper.dylib")
path = os.path.join(sys.prefix, "libankihelper.dylib")
else:
path = os.path.join(
aqt.utils.aqt_data_folder(), "lib", "libankihelper.dylib"
)
self._dll = CDLL(path) self._dll = CDLL(path)
self._dll.system_is_dark.restype = c_bool self._dll.system_is_dark.restype = c_bool

View file

@ -2,8 +2,6 @@
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from __future__ import annotations from __future__ import annotations
from typing import cast
import aqt import aqt
import aqt.browser import aqt.browser
from aqt.browser.sidebar.item import SidebarItem from aqt.browser.sidebar.item import SidebarItem
@ -107,11 +105,11 @@ class SidebarModel(QAbstractItemModel):
return self.sidebar._on_rename(index.internalPointer(), text) return self.sidebar._on_rename(index.internalPointer(), text)
def supportedDropActions(self) -> Qt.DropAction: def supportedDropActions(self) -> Qt.DropAction:
return cast(Qt.DropAction, Qt.DropAction.MoveAction) return Qt.DropAction.MoveAction
def flags(self, index: QModelIndex) -> Qt.ItemFlag: def flags(self, index: QModelIndex) -> Qt.ItemFlag:
if not index.isValid(): if not index.isValid():
return cast(Qt.ItemFlag, Qt.ItemFlag.ItemIsEnabled) return Qt.ItemFlag.ItemIsEnabled
flags = ( flags = (
Qt.ItemFlag.ItemIsEnabled Qt.ItemFlag.ItemIsEnabled
| Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsSelectable

View file

@ -1003,17 +1003,20 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
warnings.simplefilter("ignore", UserWarning) warnings.simplefilter("ignore", UserWarning)
doc = BeautifulSoup(html, "html.parser") doc = BeautifulSoup(html, "html.parser")
tag: bs4.element.Tag
if not internal: if not internal:
for tag in self.removeTags: for tag_name in self.removeTags:
for node in doc(tag): for node in doc(tag_name):
node.decompose() node.decompose()
# convert p tags to divs # convert p tags to divs
for node in doc("p"): for node in doc("p"):
node.name = "div" if hasattr(node, "name"):
node.name = "div"
for tag in doc("img"): for element in doc("img"):
if not isinstance(element, bs4.Tag):
continue
tag = element
try: try:
src = tag["src"] src = tag["src"]
except KeyError: except KeyError:
@ -1023,18 +1026,18 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
# in internal pastes, rewrite mediasrv references to relative # in internal pastes, rewrite mediasrv references to relative
if internal: if internal:
m = re.match(r"http://127.0.0.1:\d+/(.*)$", src) m = re.match(r"http://127.0.0.1:\d+/(.*)$", str(src))
if m: if m:
tag["src"] = m.group(1) tag["src"] = m.group(1)
else: else:
# in external pastes, download remote media # in external pastes, download remote media
if self.isURL(src): if isinstance(src, str) and self.isURL(src):
fname = self._retrieveURL(src) fname = self._retrieveURL(src)
if fname: if fname:
tag["src"] = fname tag["src"] = fname
elif src.startswith("data:image/"): elif isinstance(src, str) and src.startswith("data:image/"):
# and convert inlined data # and convert inlined data
tag["src"] = self.inlinedImageToFilename(src) tag["src"] = self.inlinedImageToFilename(str(src))
html = str(doc) html = str(doc)
return html return html

View file

@ -165,6 +165,7 @@ _mbox: QMessageBox | None = None
class ErrorHandler(QObject): class ErrorHandler(QObject):
"Catch stderr and write into buffer." "Catch stderr and write into buffer."
ivl = 100 ivl = 100
fatal_error_encountered = False fatal_error_encountered = False

View file

@ -252,14 +252,8 @@ def _handle_local_file_request(request: LocalFileRequest) -> Response:
def _builtin_data(path: str) -> bytes: def _builtin_data(path: str) -> bytes:
"""Return data from file in aqt/data folder. """Return data from file in aqt/data folder.
Path must use forward slash separators.""" Path must use forward slash separators."""
# packaged build? full_path = aqt_data_path() / ".." / path
if getattr(sys, "frozen", False): return full_path.read_bytes()
reader = aqt.__loader__.get_resource_reader("_aqt") # type: ignore
with reader.open_resource(path) as f:
return f.read()
else:
full_path = aqt_data_path() / ".." / path
return full_path.read_bytes()
def _handle_builtin_file_request(request: BundledFileRequest) -> Response: def _handle_builtin_file_request(request: BundledFileRequest) -> Response:

View file

@ -177,7 +177,8 @@ class MPVBase:
startup. startup.
""" """
start = time.time() 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) time.sleep(0.1)
if is_win: if is_win:
# named pipe # named pipe

View file

@ -5,93 +5,65 @@
from __future__ import annotations from __future__ import annotations
import os import subprocess
import sys
from pathlib import Path from pathlib import Path
from anki.utils import is_mac
def _fix_pywin32() -> None:
# extend sys.path with .pth files
import site
site.addsitedir(sys.path[0])
# use updated sys.path to locate dll folder and add it to path
path = sys.path[-1]
path = path.replace("Pythonwin", "pywin32_system32")
os.environ["PATH"] += ";" + path
# import Python modules from .dll files
import importlib.machinery
for name in "pythoncom", "pywintypes":
filename = os.path.join(path, name + "39.dll")
loader = importlib.machinery.ExtensionFileLoader(name, filename)
spec = importlib.machinery.ModuleSpec(name=name, loader=loader, origin=filename)
_mod = importlib._bootstrap._load(spec) # type: ignore
def _patch_pkgutil() -> None: # pylint: disable=unused-import,import-error
"""Teach pkgutil.get_data() how to read files from in-memory resources. def first_run_setup() -> None:
"""Code run the first time after install/upgrade.
This is required for jsonschema.""" Currently, we just import our main libraries and invoke
import importlib mpv/lame on macOS, which is slow on the first run, and doing
import pkgutil it this way shows progress being made.
"""
def get_data_custom(package: str, resource: str) -> bytes | None: if not is_mac:
try:
module = importlib.import_module(package)
reader = module.__loader__.get_resource_reader(package) # type: ignore
with reader.open_resource(resource) as f:
return f.read()
except Exception:
return None
pkgutil.get_data = get_data_custom
def _patch_certifi() -> None:
"""Tell certifi (and thus requests) to use a file in our package folder.
By default it creates a copy of the data in a temporary folder, which then gets
cleaned up by macOS's temp file cleaner."""
import certifi
def where() -> str:
prefix = Path(sys.prefix)
if sys.platform == "darwin":
path = prefix / "../Resources/certifi/cacert.pem"
else:
path = prefix / "lib" / "certifi" / "cacert.pem"
return str(path)
certifi.where = where
def _fix_protobuf_path() -> None:
sys.path.append(str(Path(sys.prefix) / "../Resources"))
def packaged_build_setup() -> None:
if not getattr(sys, "frozen", False):
return return
print("Initial setup...") def _dot():
print(".", flush=True, end="")
if sys.platform == "win32": _dot()
_fix_pywin32() import anki.collection
elif sys.platform == "darwin":
_fix_protobuf_path()
_patch_pkgutil() _dot()
_patch_certifi() import PyQt6.sip
# escape hatch for debugging issues with packaged build startup _dot()
if os.getenv("ANKI_STARTUP_REPL"): import PyQt6.QtCore
# mypy incorrectly thinks this does not exist on Windows
is_tty = os.isatty(sys.stdin.fileno()) # type: ignore
if is_tty:
import code
code.InteractiveConsole().interact() _dot()
sys.exit(0) import PyQt6.QtGui
_dot()
import PyQt6.QtNetwork
_dot()
import PyQt6.QtQuick
_dot()
import PyQt6.QtWebChannel
_dot()
import PyQt6.QtWebEngineCore
_dot()
import PyQt6.QtWebEngineWidgets
_dot()
import anki_audio
import PyQt6.QtWidgets
audio_pkg_path = Path(anki_audio.__file__).parent
# Invoke mpv and lame
cmd = [Path(""), "--version"]
for cmd_name in ["mpv", "lame"]:
_dot()
cmd[0] = audio_pkg_path / cmd_name
subprocess.run([str(cmd[0]), str(cmd[1])], check=True, capture_output=True)
print()

View file

@ -12,7 +12,7 @@ from PyQt6 import sip
from PyQt6.QtCore import * from PyQt6.QtCore import *
# conflicting Qt and qFuzzyCompare definitions require an ignore # conflicting Qt and qFuzzyCompare definitions require an ignore
from PyQt6.QtGui import * # type: ignore[misc,assignment] from PyQt6.QtGui import * # type: ignore[no-redef,assignment]
from PyQt6.QtNetwork import QLocalServer, QLocalSocket, QNetworkProxy from PyQt6.QtNetwork import QLocalServer, QLocalSocket, QNetworkProxy
from PyQt6.QtQuick import * from PyQt6.QtQuick import *
from PyQt6.QtWebChannel import QWebChannel from PyQt6.QtWebChannel import QWebChannel

View file

@ -18,7 +18,10 @@ import aqt.operations
from anki.cards import Card, CardId from anki.cards import Card, CardId
from anki.collection import Config, OpChanges, OpChangesWithCount from anki.collection import Config, OpChanges, OpChangesWithCount
from anki.scheduler.base import ScheduleCardsAsNew from anki.scheduler.base import ScheduleCardsAsNew
from anki.scheduler.v3 import CardAnswer, QueuedCards from anki.scheduler.v3 import (
CardAnswer,
QueuedCards,
)
from anki.scheduler.v3 import Scheduler as V3Scheduler from anki.scheduler.v3 import Scheduler as V3Scheduler
from anki.scheduler.v3 import ( from anki.scheduler.v3 import (
SchedulingContext, SchedulingContext,

View file

@ -279,12 +279,25 @@ def _packagedCmd(cmd: list[str]) -> tuple[Any, dict[str, str]]:
if "LD_LIBRARY_PATH" in env and "SNAP" not in env: if "LD_LIBRARY_PATH" in env and "SNAP" not in env:
del env["LD_LIBRARY_PATH"] del env["LD_LIBRARY_PATH"]
if is_win: # Try to find binary in anki-audio package for Windows/Mac
packaged_path = Path(sys.prefix) / (cmd[0] + ".exe") if is_win or is_mac:
elif is_mac: try:
packaged_path = Path(sys.prefix) / ".." / "Resources" / cmd[0] import anki_audio
else:
packaged_path = Path(sys.prefix) / cmd[0] audio_pkg_path = Path(anki_audio.__file__).parent
if is_win:
packaged_path = audio_pkg_path / (cmd[0] + ".exe")
else: # is_mac
packaged_path = audio_pkg_path / cmd[0]
if packaged_path.exists():
cmd[0] = str(packaged_path)
return cmd, env
except ImportError:
# anki-audio not available, fall back to old behavior
pass
packaged_path = Path(sys.prefix) / cmd[0]
if packaged_path.exists(): if packaged_path.exists():
cmd[0] = str(packaged_path) cmd[0] = str(packaged_path)

View file

@ -120,7 +120,7 @@ class CustomStyles:
QLabel:disabled {{ QLabel:disabled {{
color: {tm.var(colors.FG_DISABLED)}; color: {tm.var(colors.FG_DISABLED)};
}} }}
QToolTip {{ color: {tm.var(colors.FG)}; background-color: {tm.var(colors.CANVAS)}; }} QToolTip {{ color: {tm.var(colors.FG)}; background-color: {tm.var(colors.CANVAS)}; }}
""" """
def menu(self, tm: ThemeManager) -> str: def menu(self, tm: ThemeManager) -> str:
@ -404,18 +404,18 @@ class CustomStyles:
}; };
}} }}
QHeaderView::section:first {{ QHeaderView::section:first {{
border-left: 1px solid {tm.var(colors.BORDER_SUBTLE)}; border-left: 1px solid {tm.var(colors.BORDER_SUBTLE)};
border-top-left-radius: {tm.var(props.BORDER_RADIUS)}; border-top-left-radius: {tm.var(props.BORDER_RADIUS)};
}} }}
QHeaderView::section:!first {{ QHeaderView::section:!first {{
border-left: none; border-left: none;
}} }}
QHeaderView::section:last {{ QHeaderView::section:last {{
border-right: 1px solid {tm.var(colors.BORDER_SUBTLE)}; border-right: 1px solid {tm.var(colors.BORDER_SUBTLE)};
border-top-right-radius: {tm.var(props.BORDER_RADIUS)}; border-top-right-radius: {tm.var(props.BORDER_RADIUS)};
}} }}
QHeaderView::section:only-one {{ QHeaderView::section:only-one {{
border-left: 1px solid {tm.var(colors.BORDER_SUBTLE)}; border-left: 1px solid {tm.var(colors.BORDER_SUBTLE)};
border-right: 1px solid {tm.var(colors.BORDER_SUBTLE)}; border-right: 1px solid {tm.var(colors.BORDER_SUBTLE)};
border-top-left-radius: {tm.var(props.BORDER_RADIUS)}; border-top-left-radius: {tm.var(props.BORDER_RADIUS)};
border-top-right-radius: {tm.var(props.BORDER_RADIUS)}; border-top-right-radius: {tm.var(props.BORDER_RADIUS)};
@ -579,19 +579,19 @@ class CustomStyles:
}} }}
QScrollBar::handle:pressed {{ QScrollBar::handle:pressed {{
background-color: {tm.var(colors.SCROLLBAR_BG_ACTIVE)}; background-color: {tm.var(colors.SCROLLBAR_BG_ACTIVE)};
}} }}
QScrollBar:horizontal {{ QScrollBar:horizontal {{
height: 12px; height: 12px;
}} }}
QScrollBar::handle:horizontal {{ QScrollBar::handle:horizontal {{
min-width: 60px; min-width: 60px;
}} }}
QScrollBar:vertical {{ QScrollBar:vertical {{
width: 12px; width: 12px;
}} }}
QScrollBar::handle:vertical {{ QScrollBar::handle:vertical {{
min-height: 60px; min-height: 60px;
}} }}
QScrollBar::add-line {{ QScrollBar::add-line {{
border: none; border: none;
background: none; background: none;

View file

@ -87,24 +87,15 @@ if TYPE_CHECKING:
def aqt_data_path() -> Path: def aqt_data_path() -> Path:
# packaged? import _aqt.colors
if getattr(sys, "frozen", False):
prefix = Path(sys.prefix)
path = prefix / "lib/_aqt/data"
if path.exists():
return path
else:
return prefix / "../Resources/_aqt/data"
else:
import _aqt.colors
data_folder = Path(inspect.getfile(_aqt.colors)).with_name("data") data_folder = Path(inspect.getfile(_aqt.colors)).with_name("data")
if data_folder.exists(): if data_folder.exists():
return data_folder.absolute() return data_folder.absolute()
else: else:
# should only happen when running unit tests # should only happen when running unit tests
print("warning, data folder not found") print("warning, data folder not found")
return Path(".") return Path(".")
def aqt_data_folder() -> str: def aqt_data_folder() -> str:
@ -1207,12 +1198,11 @@ def supportText() -> str:
platname = platform.platform() platname = platform.platform()
return """\ return """\
Anki {} {} {} Anki {} {}
Python {} Qt {} PyQt {} Python {} Qt {} PyQt {}
Platform: {} Platform: {}
""".format( """.format(
version_with_build(), version_with_build(),
"(src)" if not getattr(sys, "frozen", False) else "",
"(ao)" if mw.addonManager.dirty else "", "(ao)" if mw.addonManager.dirty else "",
platform.python_version(), platform.python_version(),
qVersion(), qVersion(),

629
qt/bundle/Cargo.lock generated
View file

@ -1,629 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "aho-corasick"
version = "0.7.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f"
dependencies = [
"memchr",
]
[[package]]
name = "anki"
version = "0.0.0"
dependencies = [
"embed-resource",
"jemallocator",
"libc",
"libc-stdhandle",
"mimalloc",
"pyembed",
"snmalloc-rs",
"winapi",
]
[[package]]
name = "anyhow"
version = "1.0.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61604a8f862e1d5c3229fdd78f8b02c68dcf73a4c4b05fd636d12240aaa242c1"
[[package]]
name = "autocfg"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "base64"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b25d992356d2eb0ed82172f5248873db5560c4721f564b13cb5193bda5e668e"
dependencies = [
"byteorder",
]
[[package]]
name = "base64"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "byteorder"
version = "1.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
[[package]]
name = "cc"
version = "1.0.71"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79c2681d6594606957bbb8631c4b90a7fcaaa72cdb714743a437b156d6a7eedd"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "charset"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f426e64df1c3de26cbf44593c6ffff5dbfd43bbf9de0d075058558126b3fc73"
dependencies = [
"base64 0.10.1",
"encoding_rs",
]
[[package]]
name = "cmake"
version = "0.1.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7b858541263efe664aead4a5209a4ae5c5d2811167d4ed4ee0944503f8d2089"
dependencies = [
"cc",
]
[[package]]
name = "cty"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b365fabc795046672053e29c954733ec3b05e4be654ab130fe8f1f94d7051f35"
[[package]]
name = "dunce"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "453440c271cf5577fd2a40e4942540cb7d0d2f85e27c8d07dd0023c925a67541"
[[package]]
name = "either"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
[[package]]
name = "embed-resource"
version = "1.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85505eb239fc952b300f29f0556d2d884082a83566768d980278d8faf38c780d"
dependencies = [
"cc",
"vswhom",
"winreg",
]
[[package]]
name = "encoding_rs"
version = "0.8.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a74ea89a0a1b98f6332de42c95baff457ada66d1cb4030f9ff151b2041a1c746"
dependencies = [
"cfg-if",
]
[[package]]
name = "fs_extra"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2022715d62ab30faffd124d40b76f4134a550a87792276512b18d63272333394"
[[package]]
name = "indoc"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adab1eaa3408fb7f0c777a73e7465fd5656136fc93b670eb6df3c88c2c1344e3"
[[package]]
name = "instant"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
dependencies = [
"cfg-if",
]
[[package]]
name = "itertools"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69ddb889f9d0d08a67338271fa9b62996bc788c7796a5c18cf057420aaed5eaf"
dependencies = [
"either",
]
[[package]]
name = "jemalloc-sys"
version = "0.5.2+5.3.0-patched"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "134163979b6eed9564c98637b710b40979939ba351f59952708234ea11b5f3f8"
dependencies = [
"cc",
"fs_extra",
"libc",
]
[[package]]
name = "jemallocator"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16c2514137880c52b0b4822b563fadd38257c1f380858addb74a400889696ea6"
dependencies = [
"jemalloc-sys",
"libc",
]
[[package]]
name = "libc"
version = "0.2.105"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "869d572136620d55835903746bcb5cdc54cb2851fd0aeec53220b4bb65ef3013"
[[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 = "libmimalloc-sys"
version = "0.1.39"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23aa6811d3bd4deb8a84dde645f943476d13b248d818edcf8ce0b2f37f036b44"
dependencies = [
"cc",
"cty",
"libc",
]
[[package]]
name = "lock_api"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712a4d093c9976e24e7dbca41db895dabcbac38eb5f4045393d17a95bdfb1109"
dependencies = [
"scopeguard",
]
[[package]]
name = "mailparse"
version = "0.13.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ee6e1ca1c8396da58f8128176f6980dd57bec84c8670a479519d3655f2d6734"
dependencies = [
"base64 0.13.0",
"charset",
"quoted_printable",
]
[[package]]
name = "memchr"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
[[package]]
name = "memmap2"
version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b182332558b18d807c4ce1ca8ca983b34c3ee32765e47b3f0f69b90355cc1dc"
dependencies = [
"libc",
]
[[package]]
name = "memoffset"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce"
dependencies = [
"autocfg",
]
[[package]]
name = "memory-module-sys"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3bbdce2925c681860b08875119254fb5543dbf6337c56ff93afebeed9c686da3"
dependencies = [
"cc",
"libc",
"winapi",
]
[[package]]
name = "mimalloc"
version = "0.1.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68914350ae34959d83f732418d51e2427a794055d0b9529f48259ac07af65633"
dependencies = [
"libmimalloc-sys",
]
[[package]]
name = "once_cell"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56"
[[package]]
name = "parking_lot"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99"
dependencies = [
"instant",
"lock_api",
"parking_lot_core",
]
[[package]]
name = "parking_lot_core"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216"
dependencies = [
"cfg-if",
"instant",
"libc",
"redox_syscall",
"smallvec",
"winapi",
]
[[package]]
name = "proc-macro2"
version = "1.0.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edc3358ebc67bc8b7fa0c007f945b0b18226f78437d61bec735a9eb96b61ee70"
dependencies = [
"unicode-xid",
]
[[package]]
name = "pyembed"
version = "0.24.0-pre"
dependencies = [
"anyhow",
"dunce",
"jemalloc-sys",
"libc",
"libmimalloc-sys",
"once_cell",
"pyo3",
"pyo3-build-config",
"python-oxidized-importer",
"python-packaging",
"snmalloc-sys",
]
[[package]]
name = "pyo3"
version = "0.17.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "268be0c73583c183f2b14052337465768c07726936a260f480f0857cb95ba543"
dependencies = [
"cfg-if",
"indoc",
"libc",
"memoffset",
"parking_lot",
"pyo3-build-config",
"pyo3-ffi",
"pyo3-macros",
"unindent",
]
[[package]]
name = "pyo3-build-config"
version = "0.17.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28fcd1e73f06ec85bf3280c48c67e731d8290ad3d730f8be9dc07946923005c8"
dependencies = [
"once_cell",
"target-lexicon",
]
[[package]]
name = "pyo3-ffi"
version = "0.17.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f6cb136e222e49115b3c51c32792886defbfb0adead26a688142b346a0b9ffc"
dependencies = [
"libc",
"pyo3-build-config",
]
[[package]]
name = "pyo3-macros"
version = "0.17.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94144a1266e236b1c932682136dc35a9dee8d3589728f68130c7c3861ef96b28"
dependencies = [
"proc-macro2",
"pyo3-macros-backend",
"quote",
"syn",
]
[[package]]
name = "pyo3-macros-backend"
version = "0.17.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8df9be978a2d2f0cdebabb03206ed73b11314701a5bfe71b0d753b81997777f"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "python-oxidized-importer"
version = "0.9.0-pre"
dependencies = [
"anyhow",
"memmap2",
"memory-module-sys",
"once_cell",
"pyo3",
"python-packaging",
"python-packed-resources",
"simple-file-manifest",
"winapi",
]
[[package]]
name = "python-packaging"
version = "0.16.0-pre"
dependencies = [
"anyhow",
"byteorder",
"encoding_rs",
"itertools",
"mailparse",
"once_cell",
"python-packed-resources",
"regex",
"simple-file-manifest",
"spdx",
"walkdir",
]
[[package]]
name = "python-packed-resources"
version = "0.12.0-pre"
dependencies = [
"anyhow",
"byteorder",
]
[[package]]
name = "quote"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05"
dependencies = [
"proc-macro2",
]
[[package]]
name = "quoted_printable"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1238256b09923649ec89b08104c4dfe9f6cb2fea734a5db5384e44916d59e9c5"
[[package]]
name = "redox_syscall"
version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff"
dependencies = [
"bitflags",
]
[[package]]
name = "regex"
version = "1.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d83f127d94bdbcda4c8cc2e50f6f84f4b611f69c902699ca385a39c3a75f9ff1"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.6.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64"
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]]
name = "scopeguard"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
name = "simple-file-manifest"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5dd19be0257552dd56d1bb6946f89f193c6e5b9f13cc9327c4bc84a357507c74"
[[package]]
name = "smallvec"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309"
[[package]]
name = "snmalloc-rs"
version = "0.2.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36acaace2719c972eab3ef6a6b3aee4495f0bf300f59715bb9cff6c5acf4ae20"
dependencies = [
"snmalloc-sys",
]
[[package]]
name = "snmalloc-sys"
version = "0.2.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35a7e6e7d5fe756bee058ddedefc7e0a9f9c8dbaa9401b48ed3c17d6578e40b5"
dependencies = [
"cc",
"cmake",
]
[[package]]
name = "spdx"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a346909b3fd07776f9b96b98d4a58e3666f831c9a672c279b10f795a34c36425"
dependencies = [
"smallvec",
]
[[package]]
name = "syn"
version = "1.0.80"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d010a1623fbd906d51d650a9916aaefc05ffa0e4053ff7fe601167f3e715d194"
dependencies = [
"proc-macro2",
"quote",
"unicode-xid",
]
[[package]]
name = "target-lexicon"
version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9410d0f6853b1d94f0e519fb95df60f29d2c1eff2d921ffdf01a4c8a3b54f12d"
[[package]]
name = "unicode-xid"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
[[package]]
name = "unindent"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f14ee04d9415b52b3aeab06258a3f07093182b88ba0f9b8d203f211a7a7d41c7"
[[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.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc2f5402d3d0e79a069714f7b48e3ecc60be7775a2c049cb839457457a239532"
dependencies = [
"cc",
"libc",
]
[[package]]
name = "walkdir"
version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56"
dependencies = [
"same-file",
"winapi",
"winapi-util",
]
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
dependencies = [
"winapi",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "winreg"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d"
dependencies = [
"winapi",
]

View file

@ -1,60 +0,0 @@
[package]
name = "anki"
version = "0.0.0"
authors = ["Ankitects Pty Ltd and contributors <https://help.ankiweb.net>"]
build = "build.rs"
edition = "2021"
license = "AGPL-3.0-or-later"
publish = false
rust-version = "1.64"
[dependencies]
pyembed = { path = "./PyOxidizer/pyembed", default-features = false }
[target.'cfg(windows)'.dependencies]
winapi = { version = "0.3", features = ["wincon"] }
libc = "0.2"
libc-stdhandle = "=0.1.0"
[dependencies.jemallocator]
version = "0.5"
optional = true
[dependencies.mimalloc]
version = "0.1"
optional = true
features = ["local_dynamic_tls", "override", "secure"]
[dependencies.snmalloc-rs]
version = "0.2"
optional = true
[build-dependencies]
embed-resource = "1.6"
[features]
default = ["build-mode-standalone"]
global-allocator-jemalloc = ["jemallocator"]
global-allocator-mimalloc = ["mimalloc"]
global-allocator-snmalloc = ["snmalloc-rs"]
allocator-jemalloc = ["pyembed/allocator-jemalloc"]
allocator-mimalloc = ["pyembed/allocator-mimalloc"]
allocator-snmalloc = ["pyembed/allocator-snmalloc"]
# Build this crate in isolation, without using PyOxidizer.
build-mode-standalone = []
# Build this crate by executing a `pyoxidizer` executable to build
# required artifacts.
build-mode-pyoxidizer-exe = []
# Build this crate by reusing artifacts generated by `pyoxidizer` out-of-band.
# In this mode, the PYOXIDIZER_ARTIFACT_DIR environment variable can refer
# to the directory containing build artifacts produced by `pyoxidizer`. If not
# set, OUT_DIR will be used.
build-mode-prebuilt-artifacts = []
[profile.release]
lto = true

@ -1 +0,0 @@
Subproject commit 12a249f686484c5e212ba800e1e7f18c7c4b1b27

View file

@ -1,95 +0,0 @@
// Based off PyOxidizer's 'init-rust-project'.
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
use std::path::{Path, PathBuf};
use embed_resource;
const DEFAULT_PYTHON_CONFIG_FILENAME: &str = "default_python_config.rs";
const DEFAULT_PYTHON_CONFIG: &str = "\
pub fn default_python_config<'a>() -> pyembed::OxidizedPythonInterpreterConfig<'a> {
pyembed::OxidizedPythonInterpreterConfig::default()
}
";
/// Build by calling a `pyoxidizer` executable to generate build artifacts.
fn build_with_pyoxidizer_exe(exe: Option<String>, resolve_target: Option<&str>) {
let pyoxidizer_exe = if let Some(path) = exe {
path
} else {
"pyoxidizer".to_string()
};
let mut args = vec!["run-build-script", "build.rs"];
if let Some(target) = resolve_target {
args.push("--target");
args.push(target);
}
match std::process::Command::new(pyoxidizer_exe)
.args(args)
.status()
{
Ok(status) => {
if !status.success() {
panic!("`pyoxidizer run-build-script` failed");
}
}
Err(e) => panic!("`pyoxidizer run-build-script` failed: {}", e.to_string()),
}
}
#[allow(clippy::if_same_then_else)]
fn main() {
if std::env::var("CARGO_FEATURE_BUILD_MODE_STANDALONE").is_ok() {
let path = PathBuf::from(std::env::var("OUT_DIR").expect("OUT_DIR not defined"));
let path = path.join(DEFAULT_PYTHON_CONFIG_FILENAME);
std::fs::write(&path, DEFAULT_PYTHON_CONFIG.as_bytes())
.expect("failed to write default python config");
println!(
"cargo:rustc-env=DEFAULT_PYTHON_CONFIG_RS={}",
path.display()
);
} else if std::env::var("CARGO_FEATURE_BUILD_MODE_PYOXIDIZER_EXE").is_ok() {
let target = if let Ok(target) = std::env::var("PYOXIDIZER_BUILD_TARGET") {
Some(target)
} else {
None
};
build_with_pyoxidizer_exe(
std::env::var("PYOXIDIZER_EXE").ok(),
target.as_ref().map(|target| target.as_ref()),
);
} else if std::env::var("CARGO_FEATURE_BUILD_MODE_PREBUILT_ARTIFACTS").is_ok() {
// relative to src/
let artifacts = Path::new("../../../out/bundle/artifacts/");
let config_rs = artifacts.join("default_python_config.rs");
println!(
"cargo:rustc-env=DEFAULT_PYTHON_CONFIG_RS={}",
config_rs.display()
);
let config_txt = artifacts.join("pyo3-build-config-file.txt");
println!("cargo:rustc-env=PYO3_CONFIG_FILE={}", config_txt.display());
let link_arg = if cfg!(target_os = "macos") {
"-rdynamic"
} else {
"-Wl,-export-dynamic"
};
println!("cargo:rustc-link-arg={link_arg}");
} else {
panic!("build-mode-* feature not set");
}
let target_family =
std::env::var("CARGO_CFG_TARGET_FAMILY").expect("CARGO_CFG_TARGET_FAMILY not defined");
// embed manifest and icon
if target_family == "windows" {
embed_resource::compile("win/anki-manifest.rc");
}
}

View file

@ -1,20 +0,0 @@
[package]
name = "makeapp"
version.workspace = true
authors.workspace = true
edition.workspace = true
license.workspace = true
publish = false
rust-version.workspace = true
[dependencies]
anyhow.workspace = true
apple-bundles.workspace = true
camino.workspace = true
clap.workspace = true
glob.workspace = true
plist.workspace = true
serde.workspace = true
serde_json.workspace = true
simple-file-manifest.workspace = true
walkdir.workspace = true

View file

@ -1,43 +0,0 @@
// 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::process::Command;
use anyhow::bail;
use anyhow::Result;
use camino::Utf8Path;
use camino::Utf8PathBuf;
const CODESIGN_ARGS: &[&str] = &["-vvvv", "-o", "runtime", "-s", "Developer ID Application:"];
pub fn codesign_python_libs(bundle_dir: &Utf8PathBuf) -> Result<()> {
for entry in glob::glob(bundle_dir.join("Contents/MacOS/lib/**/*.so").as_str())? {
let entry = entry?;
let entry = Utf8PathBuf::from_path_buf(entry).unwrap();
codesign_file(&entry, &[])?;
}
codesign_file(&bundle_dir.join("Contents/MacOS/libankihelper.dylib"), &[])
}
pub fn codesign_app(bundle_dir: &Utf8PathBuf) -> Result<()> {
codesign_file(
bundle_dir,
&["--entitlements", "qt/bundle/mac/entitlements.python.xml"],
)
}
fn codesign_file(path: &Utf8Path, extra_args: &[&str]) -> Result<()> {
if env::var("ANKI_CODESIGN").is_ok() {
let status = Command::new("codesign")
.args(CODESIGN_ARGS)
.args(extra_args)
.arg(path.as_str())
.status()?;
if !status.success() {
bail!("codesign failed");
}
}
Ok(())
}

View file

@ -1,51 +0,0 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use std::fs;
use std::process::Command;
use anyhow::Context;
use anyhow::Result;
use camino::Utf8Path;
use camino::Utf8PathBuf;
use clap::Args;
use crate::notarize::wait_then_staple_app;
#[derive(Args)]
pub struct BuildDmgsArgs {
qt6_dmg: Utf8PathBuf,
qt5_dmg: Option<Utf8PathBuf>,
}
pub fn make_dmgs(args: BuildDmgsArgs) -> Result<()> {
let root = Utf8Path::new("out/bundle/app");
let mut variants = vec![("std", args.qt6_dmg)];
if let Some(alt) = args.qt5_dmg {
variants.push(("alt", alt));
}
for (variant, dmg) in variants {
let app = root.join(variant).join("Anki.app");
if std::env::var("ANKI_CODESIGN").is_ok() {
let uuid = fs::read_to_string(app.with_extension("uuid")).context("read uuid")?;
wait_then_staple_app(&app, uuid)?;
}
make_dmg(&app, &dmg)?;
}
Ok(())
}
fn make_dmg(app_folder: &Utf8Path, dmg: &Utf8Path) -> Result<()> {
assert!(
Command::new("qt/bundle/mac/dmg/build.sh")
.args([app_folder.parent().unwrap().as_str(), dmg.as_str()])
.status()
.context("dmg")?
.success(),
"dmg"
);
Ok(())
}

View file

@ -1,239 +0,0 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
#![cfg(unix)]
//! Munge the output of PyOxidizer into a macOS app bundle, and combine it
//! with our other runtime dependencies.
mod codesign;
mod dmg;
mod notarize;
use std::env;
use std::fs;
use std::os::unix::prelude::PermissionsExt;
use std::process::Command;
use anyhow::bail;
use anyhow::Result;
use apple_bundles::MacOsApplicationBundleBuilder;
use camino::Utf8Path;
use camino::Utf8PathBuf;
use clap::Parser;
use clap::Subcommand;
use clap::ValueEnum;
use codesign::codesign_app;
use codesign::codesign_python_libs;
use dmg::make_dmgs;
use dmg::BuildDmgsArgs;
use notarize::notarize_app;
use plist::Value;
use simple_file_manifest::FileEntry;
use walkdir::WalkDir;
#[derive(Clone, ValueEnum)]
enum DistKind {
Standard,
Alternate,
}
impl DistKind {
fn folder_name(&self) -> &'static str {
match self {
DistKind::Standard => "std",
DistKind::Alternate => "alt",
}
}
fn input_folder(&self) -> Utf8PathBuf {
Utf8Path::new("out/bundle").join(self.folder_name())
}
fn output_folder(&self) -> Utf8PathBuf {
Utf8Path::new("out/bundle/app")
.join(self.folder_name())
.join("Anki.app")
}
fn macos_min(&self) -> &str {
match self {
DistKind::Standard => {
if env::var("MAC_X86").is_ok() {
"11"
} else {
"12"
}
}
DistKind::Alternate => "10.13.4",
}
}
fn qt_repo(&self) -> &Utf8Path {
Utf8Path::new(match self {
DistKind::Standard => {
if cfg!(target_arch = "aarch64") && env::var("MAC_X86").is_err() {
"out/extracted/mac_arm_qt6"
} else {
"out/extracted/mac_amd_qt6"
}
}
DistKind::Alternate => "out/extracted/mac_amd_qt5",
})
}
}
#[derive(Parser)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
BuildApp {
version: String,
kind: DistKind,
stamp: Utf8PathBuf,
},
BuildDmgs(BuildDmgsArgs),
}
fn main() -> Result<()> {
match Cli::parse().command {
Commands::BuildApp {
version,
kind,
stamp,
} => {
let plist = get_plist(&version);
make_app(kind, plist, &stamp)
}
Commands::BuildDmgs(args) => make_dmgs(args),
}
}
fn make_app(kind: DistKind, mut plist: plist::Dictionary, stamp: &Utf8Path) -> Result<()> {
let input_folder = kind.input_folder();
let output_folder = kind.output_folder();
let output_variant = output_folder.parent().unwrap();
if output_variant.exists() {
fs::remove_dir_all(output_variant)?;
}
fs::create_dir_all(&output_folder)?;
let mut builder = MacOsApplicationBundleBuilder::new("Anki")?;
plist.insert(
"LSMinimumSystemVersion".into(),
Value::from(kind.macos_min()),
);
builder.set_info_plist_from_dictionary(plist)?;
builder.add_file_resources("Assets.car", &include_bytes!("../icon/Assets.car")[..])?;
for entry in WalkDir::new(&input_folder)
.into_iter()
.map(Result::unwrap)
.filter(|e| !e.file_type().is_dir())
{
let path = entry.path();
let entry = FileEntry::try_from(path)?;
let relative_path = path.strip_prefix(&input_folder)?;
let path_str = relative_path.to_str().unwrap();
if path_str.contains("libankihelper") {
builder.add_file_macos("libankihelper.dylib", entry)?;
} else if path_str.contains("aqt/data")
|| path_str.contains("certifi")
|| path_str.contains("google/protobuf")
{
builder.add_file_resources(relative_path.strip_prefix("lib").unwrap(), entry)?;
} else {
if path_str.contains("__pycache__") {
continue;
}
builder.add_file_macos(relative_path, entry)?;
}
}
builder.files().materialize_files(&output_folder)?;
fix_rpath(output_folder.join("Contents/MacOS/anki"))?;
codesign_python_libs(&output_folder)?;
copy_in_audio(&output_folder)?;
copy_in_qt(&output_folder, kind)?;
codesign_app(&output_folder)?;
fixup_perms(&output_folder)?;
notarize_app(&output_folder)?;
fs::write(stamp, b"")?;
Ok(())
}
/// The bundle builder writes some files without world read/execute perms,
/// which prevents them from being opened by a non-admin user.
fn fixup_perms(dir: &Utf8Path) -> Result<()> {
let status = Command::new("find")
.arg(dir)
.args(["-not", "-perm", "-a=r", "-exec", "chmod", "a+r", "{}", ";"])
.status()?;
if !status.success() {
bail!("error setting perms");
}
fs::set_permissions(
dir.join("Contents/MacOS/anki"),
PermissionsExt::from_mode(0o755),
)?;
Ok(())
}
/// Copy everything at the provided path into the Contents/ folder of our app.
fn extend_app_contents(source: &Utf8Path, target_dir: &Utf8Path) -> Result<()> {
let status = Command::new("rsync")
.arg("-a")
.arg(format!("{}/", source.as_str()))
.arg(target_dir)
.status()?;
if !status.success() {
bail!("error syncing {source:?}");
}
Ok(())
}
fn copy_in_audio(bundle_dir: &Utf8Path) -> Result<()> {
println!("Copying in audio...");
let src_folder = Utf8Path::new(
if cfg!(target_arch = "aarch64") && env::var("MAC_X86").is_err() {
"out/extracted/mac_arm_audio"
} else {
"out/extracted/mac_amd_audio"
},
);
extend_app_contents(src_folder, &bundle_dir.join("Contents/Resources"))
}
fn copy_in_qt(bundle_dir: &Utf8Path, kind: DistKind) -> Result<()> {
println!("Copying in Qt...");
extend_app_contents(kind.qt_repo(), &bundle_dir.join("Contents"))
}
fn fix_rpath(exe_path: Utf8PathBuf) -> Result<()> {
let status = Command::new("install_name_tool")
.arg("-add_rpath")
.arg("@executable_path/../Frameworks")
.arg(exe_path.as_str())
.status()?;
assert!(status.success());
Ok(())
}
fn get_plist(anki_version: &str) -> plist::Dictionary {
let reader = std::io::Cursor::new(include_bytes!("Info.plist"));
let mut plist = Value::from_reader(reader)
.unwrap()
.into_dictionary()
.unwrap();
plist.insert(
"CFBundleShortVersionString".into(),
Value::from(anki_version),
);
plist
}

View file

@ -1,103 +0,0 @@
// 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::fs;
use std::process::Command;
use anyhow::bail;
use anyhow::Context;
use anyhow::Result;
use camino::Utf8Path;
use serde::Deserialize;
#[derive(Deserialize)]
struct NotarySubmitOutput {
id: String,
}
pub fn notarize_app(output_folder: &Utf8Path) -> Result<()> {
if env::var("ANKI_CODESIGN").is_err() {
return Ok(());
}
if env::var("ANKI_NO_NOTARIZE").is_ok() {
return Ok(());
}
let zip_file = output_folder.with_extension("zip");
assert!(
Command::new("ditto")
.args([
"-c",
"-k",
"--keepParent",
output_folder.as_str(),
zip_file.as_str(),
])
.status()
.unwrap()
.success(),
"zip build"
);
let output = Command::new("xcrun")
.args([
"notarytool",
"submit",
zip_file.as_str(),
"-f",
"json",
"-p",
"default",
])
.output()
.expect("notarytool");
if !output.status.success() {
panic!(
"notarytool submit failed: {} {}",
String::from_utf8_lossy(&output.stderr),
String::from_utf8_lossy(&output.stdout)
)
}
let output: NotarySubmitOutput = match serde_json::from_slice(&output.stdout) {
Ok(out) => out,
Err(err) => panic!(
"unable to parse notary output: {err} {} {}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
),
};
let uuid_path = output_folder.with_extension("uuid");
fs::write(uuid_path, output.id).expect("write uuid");
Ok(())
}
#[derive(Deserialize)]
struct NotaryWaitOutput {
status: String,
}
pub fn wait_then_staple_app(app: &Utf8Path, uuid: String) -> Result<()> {
let output = Command::new("xcrun")
.args(["notarytool", "wait", &uuid, "-p", "default", "-f", "json"])
.output()
.context("notary wait")?;
let output: NotaryWaitOutput = serde_json::from_slice(&output.stdout)
.with_context(|| String::from_utf8_lossy(&output.stderr).to_string())?;
if output.status != "Accepted" {
bail!("unexpected status: {}", output.status);
}
assert!(
Command::new("xcrun")
.args(["stapler", "staple", app.as_str()])
.status()
.context("staple")?
.success(),
"staple"
);
// clean up temporary files
fs::remove_file(app.with_extension("zip")).context("app.zip")?;
fs::remove_file(app.with_extension("uuid")).context("app.uuid")?;
Ok(())
}

Some files were not shown because too many files have changed in this diff Show more