diff --git a/.cargo/config.toml b/.cargo/config.toml index 67f0dea34..3fbb3be1b 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -5,7 +5,8 @@ DESCRIPTORS_BIN = { value = "out/rslib/proto/descriptors.bin", relative = true } # build script will append .exe if necessary PROTOC = { value = "out/extracted/protoc/bin/protoc", relative = true } PYO3_NO_PYTHON = "1" -MACOSX_DEPLOYMENT_TARGET = "10.13.4" +MACOSX_DEPLOYMENT_TARGET = "11" +PYTHONDONTWRITEBYTECODE = "1" # prevent junk files on Windows [term] color = "always" diff --git a/.deny.toml b/.deny.toml index 8a379fa55..7cdf0cf99 100644 --- a/.deny.toml +++ b/.deny.toml @@ -5,9 +5,6 @@ db-path = "~/.cargo/advisory-db" db-urls = ["https://github.com/rustsec/advisory-db"] ignore = [ - # pyoxidizer is stuck on an old ring version - "RUSTSEC-2025-0009", - "RUSTSEC-2025-0010", # burn depends on an unmaintained package 'paste' "RUSTSEC-2024-0436", ] @@ -17,12 +14,11 @@ allow = [ "MIT", "Apache-2.0", "Apache-2.0 WITH LLVM-exception", + "CDLA-Permissive-2.0", "ISC", "MPL-2.0", - "Unicode-DFS-2016", "BSD-2-Clause", "BSD-3-Clause", - "OpenSSL", "CC0-1.0", "Unlicense", "Zlib", diff --git a/.dprint.json b/.dprint.json index 8e9f19b40..4230cdcd6 100644 --- a/.dprint.json +++ b/.dprint.json @@ -20,7 +20,6 @@ "ftl/usage", "licenses.json", ".dmypy.json", - "qt/bundle/PyOxidizer", "target", ".mypy_cache", "extra", diff --git a/.gitignore b/.gitignore index 91a949329..768716ca4 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,5 @@ node_modules yarn-error.log ts/.svelte-kit .yarn +.claude/settings.local.json +CLAUDE.local.md diff --git a/.gitmodules b/.gitmodules index 90cec9ca9..50b5aa9f3 100644 --- a/.gitmodules +++ b/.gitmodules @@ -6,9 +6,3 @@ path = ftl/qt-repo url = https://github.com/ankitects/anki-desktop-ftl.git shallow = true -[submodule "qt/bundle/PyOxidizer"] - path = qt/bundle/PyOxidizer - url = https://github.com/ankitects/PyOxidizer.git - shallow = true - update = none - diff --git a/.isort.cfg b/.isort.cfg index 109f5c21e..a26991a95 100644 --- a/.isort.cfg +++ b/.isort.cfg @@ -2,4 +2,3 @@ py_version=39 known_first_party=anki,aqt,tests profile=black -extend_skip=qt/bundle diff --git a/.mypy.ini b/.mypy.ini index 648c6a6ea..9fb8d3689 100644 --- a/.mypy.ini +++ b/.mypy.ini @@ -18,7 +18,7 @@ mypy_path = ftl, pylib/tools, python -exclude = (qt/bundle/PyOxidizer|pylib/anki/_vendor) +exclude = (pylib/anki/_vendor) [mypy-anki.*] disallow_untyped_defs = True @@ -165,3 +165,5 @@ ignore_missing_imports = True ignore_missing_imports = True [mypy-pip_system_certs.*] ignore_missing_imports = True +[mypy-anki_audio] +ignore_missing_imports = True diff --git a/.python-version b/.python-version new file mode 100644 index 000000000..86f8c02eb --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.13.5 diff --git a/.ruff.toml b/.ruff.toml index 498ecbdac..fb6ffa2d8 100644 --- a/.ruff.toml +++ b/.ruff.toml @@ -1,2 +1,2 @@ target-version = "py39" -extend-exclude = ["qt/bundle"] +extend-exclude = [] diff --git a/.version b/.version index 5951b08da..9079196d0 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -25.05 +25.06b4 diff --git a/.vscode.dist/settings.json b/.vscode.dist/settings.json index ffac17cae..ab91e06ab 100644 --- a/.vscode.dist/settings.json +++ b/.vscode.dist/settings.json @@ -31,11 +31,13 @@ "rust-analyzer.rustfmt.extraArgs": ["+nightly"], "search.exclude": { "**/node_modules": true, - ".bazel/**": true, - "qt/bundle/PyOxidizer": true + ".bazel/**": true }, "rust-analyzer.cargo.buildScripts.enable": true, "python.analysis.typeCheckingMode": "off", + "python.analysis.exclude": [ + "out/launcher/**" + ], "terminal.integrated.env.windows": { "PATH": "c:\\msys64\\usr\\bin;${env:Path}" } diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..6ec6db642 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,82 @@ +# Claude Code Configuration + +## Project Overview + +Anki is a spaced repetition flashcard program with a multi-layered architecture. Main components: + +- Web frontend: Svelte/TypeScript in ts/ +- PyQt GUI, which embeds the web components in aqt/ +- Python library which wraps our rust Layer (pylib/, with Rust module in pylib/rsbridge) +- Core Rust layer in rslib/ +- Protobuf definitions in proto/ that are used by the different layers to + talk to each other. + +## Building/checking + +./check (check.bat) will format the code and run the main build & checks. +Please do this as a final step before marking a task as completed. + +## Quick iteration + +During development, you can build/check subsections of our code: + +- Rust: 'cargo check' +- Python: './tools/dmypy' +- TypeScript/Svelte: './ninja check:svelte' + +Be mindful that some changes (such as modifications to .proto files) may +need a full build with './check' first. + +## Build tooling + +'./check' and './ninja' invoke our build system, which is implemented in build/. It takes care of downloading required deps and invoking our build +steps. + +## Translations + +ftl/ contains our Fluent translation files. We have scripts in rslib/i18n +to auto-generate an API for Rust, TypeScript and Python so that our code can +access the translations in a type-safe manner. Changes should be made to +ftl/core or ftl/qt. Except for features specific to our Qt interface, prefer +the core module. When adding new strings, confirm the appropriate ftl file +first, and try to match the existing style. + +## Protobuf and IPC + +Our build scripts use the .proto files to define our Rust library's +non-Rust API. pylib/rsbridge exposes that API, and _backend.py exposes +snake_case methods for each protobuf RPC that call into the API. +Similar tooling creates a @generated/backend TypeScript module for +communicating with the Rust backend (which happens over POST requests). + +## Fixing errors + +When dealing with build errors or failing tests, invoke 'check' or one +of the quick iteration commands regularly. This helps verify your changes +are correct. To locate other instances of a problem, run the check again - +don't attempt to grep the codebase. + +## Ignores + +The files in out/ are auto-generated. Mostly you should ignore that folder, +though you may sometimes find it useful to view out/{pylib/anki,qt/_aqt,ts/lib/generated} when dealing with cross-language communication or our other generated sourcecode. + +## Launcher/installer + +The code for our launcher is in qt/launcher, with separate code for each +platform. + +## Rust dependencies + +Prefer adding to the root workspace, and using dep.workspace = true in the individual Rust project. + +## Rust utilities + +rslib/{process,io} contain some helpers for file and process operations, +which provide better error messages/context and some ergonomics. Use them +when possible. + +## Rust error handling + +in rslib, use error/mod.rs's AnkiError/Result and snafu. In our other Rust modules, prefer anyhow + additional context where appropriate. Unwrapping +in build scripts/tests is fine. diff --git a/CONTRIBUTORS b/CONTRIBUTORS index 068760e6b..d334540fb 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -148,7 +148,7 @@ user1823 <92206575+user1823@users.noreply.github.com> Gustaf Carefall virinci snowtimeglass -brishtibheja +brishtibheja <136738526+brishtibheja@users.noreply.github.com> Ben Olson Akash Reddy Lucio Sauer @@ -230,6 +230,7 @@ KolbyML Adnane Taghi Spiritual Father Emmanuel Ferdman +Sunong2008 Marvin Kopf ******************** diff --git a/Cargo.lock b/Cargo.lock index f73263c28..66173027b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13,20 +13,20 @@ dependencies = [ [[package]] name = "adler2" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "ahash" -version = "0.8.11" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if", "once_cell", "version_check", - "zerocopy 0.7.35", + "zerocopy", ] [[package]] @@ -103,7 +103,7 @@ dependencies = [ "csv", "data-encoding", "difflib", - "dirs 5.0.1", + "dirs 6.0.0", "envy", "flate2", "fluent", @@ -116,7 +116,7 @@ dependencies = [ "hyper 1.6.0", "id_tree", "inflections", - "itertools 0.13.0", + "itertools 0.14.0", "nom", "num_cpus", "num_enum", @@ -128,10 +128,10 @@ dependencies = [ "prettyplease", "prost", "prost-reflect", - "pulldown-cmark 0.9.6", - "rand 0.8.5", + "pulldown-cmark 0.13.0", + "rand 0.9.1", "regex", - "reqwest 0.12.15", + "reqwest 0.12.20", "rusqlite", "rustls-pemfile 2.2.0", "scopeguard", @@ -142,8 +142,8 @@ dependencies = [ "serde_tuple", "sha1", "snafu", - "strum 0.26.3", - "syn 2.0.101", + "strum 0.27.1", + "syn 2.0.103", "tempfile", "tokio", "tokio-util", @@ -154,9 +154,9 @@ dependencies = [ "unic-ucd-category", "unicase", "unicode-normalization", - "windows 0.56.0", + "windows 0.61.3", "wiremock", - "zip 0.6.6", + "zip 4.1.0", "zstd", ] @@ -178,7 +178,7 @@ dependencies = [ "fluent-syntax", "inflections", "intl-memoizer", - "itertools 0.13.0", + "itertools 0.14.0", "num-format", "phf 0.11.3", "serde", @@ -199,7 +199,7 @@ dependencies = [ name = "anki_process" version = "0.0.0" dependencies = [ - "itertools 0.13.0", + "itertools 0.14.0", "snafu", ] @@ -211,14 +211,14 @@ dependencies = [ "anki_proto_gen", "anyhow", "inflections", - "itertools 0.13.0", + "itertools 0.14.0", "prost", "prost-build", "prost-reflect", "prost-types", "serde", "snafu", - "strum 0.26.3", + "strum 0.27.1", ] [[package]] @@ -229,7 +229,7 @@ dependencies = [ "anyhow", "camino", "inflections", - "itertools 0.13.0", + "itertools 0.14.0", "prost-reflect", "prost-types", "regex", @@ -238,9 +238,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.18" +version = "0.6.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" dependencies = [ "anstyle", "anstyle-parse", @@ -253,36 +253,36 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" [[package]] name = "anstyle-parse" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" dependencies = [ "windows-sys 0.59.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.7" +version = "3.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" dependencies = [ "anstyle", - "once_cell", + "once_cell_polyfill", "windows-sys 0.59.0", ] @@ -292,18 +292,6 @@ version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" -[[package]] -name = "apple-bundles" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "716b8a7bacf7325eb3e7a1a7f5ead4da91e1e16d9b56a25edea0e1e4ba21fd8e" -dependencies = [ - "anyhow", - "plist", - "simple-file-manifest", - "walkdir", -] - [[package]] name = "arbitrary" version = "1.4.1" @@ -358,9 +346,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.23" +version = "0.4.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b37fc50485c4f3f736a4fb14199f6d5f5ba008d7f28fe710306c92780f004c07" +checksum = "d615619615a650c571269c00dca41db04b9210037fa76ed8239f70404ab56985" dependencies = [ "futures-core", "memchr", @@ -389,7 +377,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -400,7 +388,7 @@ checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -423,14 +411,14 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "axum" -version = "0.7.9" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" +checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5" dependencies = [ - "async-trait", "axum-core", "axum-macros", "bytes", + "form_urlencoded", "futures-util", "http 1.3.1", "http-body 1.0.1", @@ -459,24 +447,23 @@ dependencies = [ [[package]] name = "axum-client-ip" -version = "0.6.1" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9eefda7e2b27e1bda4d6fa8a06b50803b8793769045918bc37ad062d48a6efac" +checksum = "3f08a543641554404b42acd0d2494df12ca2be034d7b8ee4dbbf7446f940a2ef" dependencies = [ "axum", - "forwarded-header-value", + "client-ip", "serde", ] [[package]] name = "axum-core" -version = "0.4.5" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6" dependencies = [ - "async-trait", "bytes", - "futures-util", + "futures-core", "http 1.3.1", "http-body 1.0.1", "http-body-util", @@ -491,22 +478,21 @@ dependencies = [ [[package]] name = "axum-extra" -version = "0.9.6" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c794b30c904f0a1c2fb7740f7df7f7972dfaa14ef6f57cb6178dc63e5dca2f04" +checksum = "45bf463831f5131b7d3c756525b305d40f1185b688565648a92e1392ca35713d" dependencies = [ "axum", "axum-core", "bytes", - "fastrand", "futures-util", - "headers 0.4.0", + "headers 0.4.1", "http 1.3.1", "http-body 1.0.1", "http-body-util", "mime", - "multer", "pin-project-lite", + "rustversion", "serde", "tower", "tower-layer", @@ -515,20 +501,20 @@ dependencies = [ [[package]] name = "axum-macros" -version = "0.4.2" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57d123550fa8d071b7255cb0cc04dc302baa6c8c4a79f55701552684d8399bce" +checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] name = "backtrace" -version = "0.3.74" +version = "0.3.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" dependencies = [ "addr2line", "cfg-if", @@ -539,12 +525,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "base64" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" - [[package]] name = "base64" version = "0.21.7" @@ -559,9 +539,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.7.3" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3" +checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" [[package]] name = "bincode" @@ -596,9 +576,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.0" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" dependencies = [ "serde", ] @@ -631,15 +611,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "block-padding" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" -dependencies = [ - "generic-array", -] - [[package]] name = "bstr" version = "1.12.0" @@ -653,15 +624,15 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.17.0" +version = "3.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" +checksum = "793db76d6187cd04dff33004d8e6c9cc4e05cd330500379d2394209271b4aeee" [[package]] name = "burn" -version = "0.17.0" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57dc51cb3c3dda2a055a2c41a56b337232c2c85cceef7bf1d0ef0cd9ada30b0b" +checksum = "ec639306f45bd663957465e840cfb07bcd2ae18f7c045dd9aba8cb7a69c0654a" dependencies = [ "burn-autodiff", "burn-candle", @@ -676,14 +647,14 @@ dependencies = [ [[package]] name = "burn-autodiff" -version = "0.17.0" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66382a92cd12f1c3c9b9f2d9afa3ee78e04347ada628340500622d7640d56ae9" +checksum = "a178966322ab7ce71405f1324cdc14f79256d85a47138bbd2c8c4f0056148601" dependencies = [ "burn-common", "burn-tensor", "derive-new 0.7.0", - "hashbrown 0.15.2", + "hashbrown 0.15.4", "log", "num-traits", "portable-atomic", @@ -692,9 +663,9 @@ dependencies = [ [[package]] name = "burn-candle" -version = "0.17.0" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80408838316581095207a424d9bf4c7e8cbffc3cbc24da679c9b70be155d78ad" +checksum = "9ed0981b3c1d07e9df0f5bef1042921b6db6e88b5d91916fa5dbdd7f0ca921c3" dependencies = [ "burn-tensor", "candle-core", @@ -704,9 +675,9 @@ dependencies = [ [[package]] name = "burn-common" -version = "0.17.0" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9bcc4cfe21815abde8fb228687536a8a6fc5a3bcb6d3ef715e962a9d57cbcca" +checksum = "1c3fae76798ea4dd14e6290b6753eb6235ac28c6ceaf6da35ff8396775d5494d" dependencies = [ "cubecl-common", "rayon", @@ -715,9 +686,9 @@ dependencies = [ [[package]] name = "burn-core" -version = "0.17.0" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b101aaf9ac402ae1a2a1cb4dc375161a3e3e995e588bea567f34d7a0d4bb783a" +checksum = "2afa81c868c1a9b3fad25c31176945d0cc5181ba7b77c0456bc05cf57fca975c" dependencies = [ "ahash", "bincode", @@ -729,7 +700,7 @@ dependencies = [ "derive-new 0.7.0", "flate2", "half", - "hashbrown 0.15.2", + "hashbrown 0.15.4", "log", "num-traits", "portable-atomic-util", @@ -743,9 +714,9 @@ dependencies = [ [[package]] name = "burn-cubecl" -version = "0.17.0" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec1aaa4c832ccd15fe0372a8100a23a0987d6ed84a9879a3dc42b0a69d88c2a5" +checksum = "c547cbe414274ab4022abcc85993e1e41aa7cdccc92395ba5658acfdac285e07" dependencies = [ "burn-common", "burn-ir", @@ -756,7 +727,7 @@ dependencies = [ "derive-new 0.7.0", "futures-lite", "half", - "hashbrown 0.15.2", + "hashbrown 0.15.4", "log", "num-traits", "rand 0.9.1", @@ -767,9 +738,9 @@ dependencies = [ [[package]] name = "burn-cuda" -version = "0.17.0" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "982b12c42fde50aa57bb700cdac8a926846e4e3bda450539b862c256ebbec782" +checksum = "995bd0b3f52a4cfe0cfe47c16b40b3fd33285d17a086dd583e5b432074857e02" dependencies = [ "burn-cubecl", "burn-tensor", @@ -782,9 +753,9 @@ dependencies = [ [[package]] name = "burn-dataset" -version = "0.17.0" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d5c3da8b6d6903aed74321599843f5aca008c72977201676abbd1dca4e5e4c" +checksum = "136c784dfc474c822f34d69e865f88a5675e9de9803ef38cee4ce14cdba34d54" dependencies = [ "csv", "derive-new 0.7.0", @@ -801,33 +772,33 @@ dependencies = [ [[package]] name = "burn-derive" -version = "0.17.0" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03cb5fe6b202e86dcb00ee2a415f34d0bfdd7658cde80ac19ff8c4e6baba7031" +checksum = "12e9f07ccc658ef072bce2e996f0c38c80ee4c241598b6557afe1877dd87ae98" dependencies = [ "derive-new 0.7.0", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] name = "burn-ir" -version = "0.17.0" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f23a288203334c9c40f8606263cdb79d84ebbe408b96d9aea0e36d0b237a8e4" +checksum = "d63629f2c8b82ee52dbb9c18becded5117c2faf57365dc271a55c16d139cd91a" dependencies = [ "burn-tensor", - "hashbrown 0.15.2", + "hashbrown 0.15.4", "portable-atomic-util", "serde", ] [[package]] name = "burn-ndarray" -version = "0.17.0" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd950877f789f294214f9deda18f5e900a1372d3e75ba129afbec634e8d0a113" +checksum = "7e883846578e6915e1dbaeeb5bce32cc04cff03e7cb79c5836e1e888bbce974f" dependencies = [ "atomic_float", "burn-autodiff", @@ -851,9 +822,9 @@ dependencies = [ [[package]] name = "burn-rocm" -version = "0.17.0" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1852e36e662dbfaf05adb69404b5967e8435e20f00a07660329c8f2e4c5e49ea" +checksum = "bd39d58202558b65b575921b57bff933845e6171296e2b8faf6a9d3610a344c5" dependencies = [ "burn-cubecl", "burn-tensor", @@ -866,23 +837,23 @@ dependencies = [ [[package]] name = "burn-router" -version = "0.17.0" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12a4e9f7100722312bdb772018e711aed8482eafa291be34a658775ca747713b" +checksum = "22ed8614e180f7a58f77e658bd52e206d2f4a1ee37fcb4665c635ea9da90ea8b" dependencies = [ "burn-common", "burn-ir", "burn-tensor", - "hashbrown 0.15.2", + "hashbrown 0.15.4", "log", "spin 0.10.0", ] [[package]] name = "burn-tensor" -version = "0.17.0" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c597b733f75cef016a0aa4ecc72879fd12efb3f75e9ec0843d6989c2b854cc8e" +checksum = "2a70d1562c0d00083939e34daad61dabebb0f8bc8c250d1ef2f5efc31eb93aaf" dependencies = [ "burn-common", "bytemuck", @@ -890,19 +861,19 @@ dependencies = [ "cubecl", "derive-new 0.7.0", "half", - "hashbrown 0.15.2", + "hashbrown 0.15.4", "num-traits", "rand 0.9.1", - "rand_distr 0.5.1", + "rand_distr", "serde", "serde_bytes", ] [[package]] name = "burn-train" -version = "0.17.0" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fde5d0de24bb8e46f674a723591649f63cd721e1ba9d8f43de805396711209f1" +checksum = "140182cf5f1255d60e1d8c677fa45c6f71018c3c3c66aad093a9e4c3c222cf1c" dependencies = [ "async-channel", "burn-core", @@ -921,9 +892,9 @@ dependencies = [ [[package]] name = "burn-wgpu" -version = "0.17.0" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "781f379aee5cf5b13d40f5bb0efbcb99aca9d1882bd0e4e65e3f9659af0cd7e3" +checksum = "215bf0e641a27e17bcd3941a11867dcda411c9cb009488c6b6650c8206437c30" dependencies = [ "burn-cubecl", "burn-tensor", @@ -932,9 +903,9 @@ dependencies = [ [[package]] name = "bytemuck" -version = "1.22.0" +version = "1.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6b1fc10dbac614ebc03540c9dbd60e83887fda27794998c6528f1782047d540" +checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422" dependencies = [ "bytemuck_derive", ] @@ -947,7 +918,7 @@ checksum = "7ecc273b49b3205b83d648f0690daa588925572cc5063745bfe547fe7ec8e1a1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -970,9 +941,9 @@ checksum = "2e93abca9e28e0a1b9877922aacb20576e05d4679ffa78c3d6dc22a26a216659" [[package]] name = "camino" -version = "1.1.9" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3" +checksum = "0da45bc31171d8d6960122e222a67740df867c1dd53b4d51caa297084c185cab" [[package]] name = "candle-core" @@ -987,12 +958,12 @@ dependencies = [ "num-traits", "num_cpus", "rand 0.9.1", - "rand_distr 0.5.1", + "rand_distr", "rayon", "safetensors", "thiserror 1.0.69", "ug", - "yoke", + "yoke 0.7.5", "zip 1.1.4", ] @@ -1002,20 +973,11 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" -[[package]] -name = "cbc" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" -dependencies = [ - "cipher", -] - [[package]] name = "cc" -version = "1.2.20" +version = "1.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04da6a0d40b948dfc4fa8f5bbf402b0fc1a64a28dbf7d12ffd683550f2c1b63a" +checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc" dependencies = [ "jobserver", "libc", @@ -1024,9 +986,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" [[package]] name = "cfg_aliases" @@ -1036,9 +998,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" -version = "0.4.40" +version = "0.4.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" dependencies = [ "android-tzdata", "iana-time-zone", @@ -1073,21 +1035,11 @@ dependencies = [ "half", ] -[[package]] -name = "cipher" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" -dependencies = [ - "crypto-common", - "inout", -] - [[package]] name = "clap" -version = "4.5.37" +version = "4.5.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eccb054f56cbd38340b380d4a8e69ef1f02f1af43db2f0cc817a4774d80ae071" +checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f" dependencies = [ "clap_builder", "clap_derive", @@ -1095,9 +1047,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.37" +version = "4.5.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efd9466fac8543255d3b1fcad4762c5e116ffe808c8a3043d4263cd4fd4862a2" +checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e" dependencies = [ "anstream", "anstyle", @@ -1108,30 +1060,39 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.5.47" +version = "4.5.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06f5378ea264ad4f82bbc826628b5aad714a75abf6ece087e923010eb937fb6" +checksum = "aad5b1b4de04fead402672b48897030eec1f3bfe1550776322f59f6d6e6a5677" dependencies = [ "clap", ] [[package]] name = "clap_derive" -version = "4.5.32" +version = "4.5.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" +checksum = "d2c7947ae4cc3d851207c1adb5b5e260ff0cca11446b1d6d1423788e442257ce" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] name = "clap_lex" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" + +[[package]] +name = "client-ip" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31211fc26899744f5b22521fdc971e5f3875991d8880537537470685a0e9552d" +dependencies = [ + "http 1.3.1", +] [[package]] name = "coarsetime" @@ -1173,14 +1134,14 @@ checksum = "fe6d2e5af09e8c8ad56c969f2157a3d4238cebc7c55f0a517728c38f7b200f81" dependencies = [ "serde", "termcolor", - "unicode-width 0.2.0", + "unicode-width 0.2.1", ] [[package]] name = "colorchoice" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "colored" @@ -1205,7 +1166,7 @@ name = "configure" version = "0.0.0" dependencies = [ "anyhow", - "itertools 0.13.0", + "itertools 0.14.0", "ninja_gen", ] @@ -1217,9 +1178,9 @@ checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" [[package]] name = "convert_case" -version = "0.6.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +checksum = "baaaa0ecca5b51987b9423ccdc971514dd8b0bb7b4060b983d3664dad3f1f89f" dependencies = [ "unicode-segmentation", ] @@ -1236,9 +1197,9 @@ dependencies = [ [[package]] name = "core-foundation" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" dependencies = [ "core-foundation-sys", "libc", @@ -1281,25 +1242,22 @@ dependencies = [ [[package]] name = "criterion" -version = "0.5.1" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +checksum = "3bf7af66b0989381bd0be551bd7cc91912a655a58c6918420c9527b1fd8b4679" dependencies = [ "anes", "cast", "ciborium", "clap", "criterion-plot", - "is-terminal", - "itertools 0.10.5", + "itertools 0.13.0", "num-traits", - "once_cell", "oorandom", "plotters", "rayon", "regex", "serde", - "serde_derive", "serde_json", "tinytemplate", "walkdir", @@ -1385,7 +1343,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -1456,7 +1414,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b03bf4211cdbd68bb0fb8291e0ed825c13da0d1ac01b7c02dce3cee44a6138be" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "bytemuck", "cubecl-common", "cubecl-ir", @@ -1527,11 +1485,12 @@ dependencies = [ [[package]] name = "cubecl-hip-sys" -version = "6.4.0" +version = "6.4.4348200" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7557762176858fa0357504025f09ae6e979c3547776ff8b6a1025ef0702450" +checksum = "283fa7401056c53fb27e18f5d1806246bb5f937c4ecbd2453896f7a9ec495c73" dependencies = [ "libc", + "regex", ] [[package]] @@ -1582,7 +1541,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -1594,7 +1553,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -1696,7 +1655,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -1707,7 +1666,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -1716,17 +1675,6 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" -[[package]] -name = "dbus" -version = "0.9.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bb21987b9fb1613058ba3843121dd18b163b254d8a6e797e144cbac14d96d1b" -dependencies = [ - "libc", - "libdbus-sys", - "winapi", -] - [[package]] name = "deadpool" version = "0.10.0" @@ -1762,7 +1710,7 @@ checksum = "d150dea618e920167e5973d70ae6ece4385b7164e0d799fe7c122dd0a5d912ad" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -1773,7 +1721,7 @@ checksum = "2cdc8d50f426189eef89dac62fabfa0abb27d5cc008f25bf4156a0203325becc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -1784,7 +1732,7 @@ checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -1805,7 +1753,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -1815,7 +1763,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" dependencies = [ "derive_builder_core", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -1835,19 +1783,10 @@ checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", "unicode-xid", ] -[[package]] -name = "des" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffdd80ce8ce993de27e9f063a444a4d53ce8e8db4c1f00cc03af5ad5a9867a1e" -dependencies = [ - "cipher", -] - [[package]] name = "difflib" version = "0.4.0" @@ -1904,7 +1843,7 @@ dependencies = [ "libc", "option-ext", "redox_users 0.5.0", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -1915,7 +1854,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -1942,18 +1881,6 @@ dependencies = [ "dtoa", ] -[[package]] -name = "duct" -version = "0.13.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4ab5718d1224b63252cd0c6f74f6480f9ffeb117438a2e0f5cf6d9a4798929c" -dependencies = [ - "libc", - "once_cell", - "os_pipe", - "shared_child", -] - [[package]] name = "dunce" version = "1.0.5" @@ -2003,6 +1930,20 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f878075b9794c1e4ac788c95b728f26aa6366d32eeb10c7051389f898f7d067" +[[package]] +name = "embed-resource" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0963f530273dc3022ab2bdc3fcd6d488e850256f2284a82b7413cb9481ee85dd" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 0.8.23", + "vswhom", + "winreg 0.55.0", +] + [[package]] name = "encoding_rs" version = "0.8.35" @@ -2021,7 +1962,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -2034,6 +1975,12 @@ dependencies = [ "regex", ] +[[package]] +name = "env_home" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe" + [[package]] name = "env_logger" version = "0.11.8" @@ -2064,9 +2011,9 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.11" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" +checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" dependencies = [ "libc", "windows-sys 0.59.0", @@ -2123,17 +2070,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "find-winsdk" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8cbf17b871570c1f8612b763bac3e86290602bcf5dc3c5ce657e0e1e9071d9e" -dependencies = [ - "serde", - "serde_derive", - "winreg 0.5.1", -] - [[package]] name = "fixedbitset" version = "0.5.7" @@ -2142,11 +2078,12 @@ checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" [[package]] name = "flate2" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" +checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" dependencies = [ "crc32fast", + "libz-rs-sys", "miniz_oxide", ] @@ -2158,9 +2095,9 @@ checksum = "8ce81f49ae8a0482e4c55ea62ebbd7e5a686af544c00b9d090bba3ff9be97b3d" [[package]] name = "fluent" -version = "0.16.1" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb74634707bebd0ce645a981148e8fb8c7bccd4c33c652aeffd28bf2f96d555a" +checksum = "8137a6d5a2c50d6b0ebfcb9aaa91a28154e0a70605f112d30cb0cd4a78670477" dependencies = [ "fluent-bundle", "unic-langid", @@ -2168,16 +2105,16 @@ dependencies = [ [[package]] name = "fluent-bundle" -version = "0.15.3" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe0a21ee80050c678013f82edf4b705fe2f26f1f9877593d13198612503f493" +checksum = "01203cb8918f5711e73891b347816d932046f95f54207710bda99beaeb423bf4" dependencies = [ "fluent-langneg", "fluent-syntax", "intl-memoizer", "intl_pluralrules", - "rustc-hash 1.1.0", - "self_cell 0.10.3", + "rustc-hash 2.1.1", + "self_cell", "smallvec", "unic-langid", ] @@ -2193,11 +2130,12 @@ dependencies = [ [[package]] name = "fluent-syntax" -version = "0.11.1" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a530c4694a6a8d528794ee9bbd8ba0122e779629ac908d15ad5a7ae7763a33d" +checksum = "54f0d287c53ffd184d04d8677f590f4ac5379785529e5e08b1c8083acdd5c198" dependencies = [ - "thiserror 1.0.69", + "memchr", + "thiserror 2.0.12", ] [[package]] @@ -2239,7 +2177,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -2263,26 +2201,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "forwarded-header-value" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8835f84f38484cc86f110a805655697908257fb9a7af005234060891557198e9" -dependencies = [ - "nonempty", - "thiserror 1.0.69", -] - -[[package]] -name = "fs2" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" -dependencies = [ - "libc", - "winapi", -] - [[package]] name = "fsevent-sys" version = "4.1.0" @@ -2294,15 +2212,16 @@ dependencies = [ [[package]] name = "fsrs" -version = "4.0.0" -source = "git+https://github.com/open-spaced-repetition/fsrs-rs.git?rev=33ec3ee4d5d73e704633469cf5bf1a42e620a524#33ec3ee4d5d73e704633469cf5bf1a42e620a524" +version = "4.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1f3a8c3df2c324ebab71461178fe8c1fe2d7373cf603f312b652befd026f06d" dependencies = [ "burn", "itertools 0.14.0", "log", "ndarray", - "ndarray-rand", "priority-queue", + "rand 0.9.1", "rayon", "serde", "snafu", @@ -2319,7 +2238,7 @@ dependencies = [ "camino", "clap", "fluent-syntax", - "itertools 0.13.0", + "itertools 0.14.0", "regex", "serde_json", "snafu", @@ -2405,7 +2324,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -2578,7 +2497,7 @@ dependencies = [ "num-traits", "once_cell", "paste", - "pulp 0.21.4", + "pulp 0.21.5", "raw-cpuid 11.5.0", "rayon", "seq-macro", @@ -2693,11 +2612,11 @@ dependencies = [ [[package]] name = "getopts" -version = "0.2.21" +version = "0.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" +checksum = "cba6ae63eb948698e300f645f87c70f76630d505f23b8907cf1e193ee85048c1" dependencies = [ - "unicode-width 0.1.14", + "unicode-width 0.2.1", ] [[package]] @@ -2709,15 +2628,15 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi 0.11.1+wasi-snapshot-preview1", "wasm-bindgen", ] [[package]] name = "getrandom" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", "js-sys", @@ -2790,7 +2709,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "gpu-alloc-types", ] @@ -2800,7 +2719,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", ] [[package]] @@ -2817,13 +2736,13 @@ dependencies = [ [[package]] name = "gpu-descriptor" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf29e94d6d243368b7a56caa16bc213e4f9f8ed38c4d9557069527b5d5281ca" +checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "gpu-descriptor-types", - "hashbrown 0.15.2", + "hashbrown 0.15.4", ] [[package]] @@ -2832,7 +2751,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", ] [[package]] @@ -2856,9 +2775,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75249d144030531f8dee69fe9cea04d3edf809a017ae445e2abdff6629e86633" +checksum = "a9421a676d1b147b16b82c9225157dc629087ef8ec4d5e2960f9437a90dac0a5" dependencies = [ "atomic-waker", "bytes", @@ -2884,7 +2803,7 @@ dependencies = [ "crunchy", "num-traits", "rand 0.9.1", - "rand_distr 0.5.1", + "rand_distr", "serde", ] @@ -2926,9 +2845,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.2" +version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" dependencies = [ "allocator-api2", "equivalent", @@ -2938,11 +2857,11 @@ dependencies = [ [[package]] name = "hashlink" -version = "0.8.4" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" dependencies = [ - "hashbrown 0.14.5", + "hashbrown 0.15.4", ] [[package]] @@ -2962,11 +2881,11 @@ dependencies = [ [[package]] name = "headers" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "322106e6bd0cba2d5ead589ddb8150a13d7c4217cf80d7c4f682ca994ccc6aa9" +checksum = "b3314d5adb5d94bcdf56771f2e50dbbc80bb4bdf88967526706205ac9eff24eb" dependencies = [ - "base64 0.21.7", + "base64 0.22.1", "bytes", "headers-core 0.3.0", "http 1.3.1", @@ -3001,15 +2920,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" -version = "0.3.9" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" - -[[package]] -name = "hermit-abi" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbd780fe5cc30f81464441920d82ac8740e2e46b29a6fad543ddd075229ce37e" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" [[package]] name = "hex" @@ -3032,15 +2945,6 @@ dependencies = [ "digest", ] -[[package]] -name = "home" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" -dependencies = [ - "windows-sys 0.59.0", -] - [[package]] name = "html5ever" version = "0.26.0" @@ -3174,7 +3078,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "h2 0.4.9", + "h2 0.4.10", "http 1.3.1", "http-body 1.0.1", "httparse", @@ -3188,35 +3092,20 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.24.2" +version = "0.27.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ - "futures-util", - "http 0.2.12", - "hyper 0.14.32", - "rustls 0.21.12", - "tokio", - "tokio-rustls 0.24.1", -] - -[[package]] -name = "hyper-rustls" -version = "0.27.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" -dependencies = [ - "futures-util", "http 1.3.1", "hyper 1.6.0", "hyper-util", - "rustls 0.23.26", + "rustls", "rustls-native-certs", "rustls-pki-types", "tokio", - "tokio-rustls 0.26.2", + "tokio-rustls", "tower-service", - "webpki-roots 0.26.8", + "webpki-roots", ] [[package]] @@ -3250,17 +3139,21 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.11" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497bbc33a26fdd4af9ed9c70d63f61cf56a938375fbb32df34db9b1cd6d643f2" +checksum = "dc2fdfdbff08affe55bb779f33b053aa1fe5dd5b54c257343c17edfa55711bdb" dependencies = [ + "base64 0.22.1", "bytes", "futures-channel", + "futures-core", "futures-util", "http 1.3.1", "http-body 1.0.1", "hyper 1.6.0", + "ipnet", "libc", + "percent-encoding", "pin-project-lite", "socket2", "tokio", @@ -3280,7 +3173,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.61.0", + "windows-core 0.61.2", ] [[package]] @@ -3294,21 +3187,22 @@ dependencies = [ [[package]] name = "icu_collections" -version = "1.5.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" dependencies = [ "displaydoc", - "yoke", + "potential_utf", + "yoke 0.8.0", "zerofrom", "zerovec", ] [[package]] -name = "icu_locid" -version = "1.5.0" +name = "icu_locale_core" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" dependencies = [ "displaydoc", "litemap", @@ -3317,31 +3211,11 @@ dependencies = [ "zerovec", ] -[[package]] -name = "icu_locid_transform" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_locid_transform_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_locid_transform_data" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7515e6d781098bf9f7205ab3fc7e9709d34554ae0b21ddbcb5febfa4bc7df11d" - [[package]] name = "icu_normalizer" -version = "1.5.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" dependencies = [ "displaydoc", "icu_collections", @@ -3349,67 +3223,54 @@ dependencies = [ "icu_properties", "icu_provider", "smallvec", - "utf16_iter", - "utf8_iter", - "write16", "zerovec", ] [[package]] name = "icu_normalizer_data" -version = "1.5.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5e8338228bdc8ab83303f16b797e177953730f601a96c25d10cb3ab0daa0cb7" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" [[package]] name = "icu_properties" -version = "1.5.1" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" dependencies = [ "displaydoc", "icu_collections", - "icu_locid_transform", + "icu_locale_core", "icu_properties_data", "icu_provider", - "tinystr", + "potential_utf", + "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "1.5.1" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85fb8799753b75aee8d2a21d7c14d9f38921b54b3dbda10f5a3c7a7b82dba5e2" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" [[package]] name = "icu_provider" -version = "1.5.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" dependencies = [ "displaydoc", - "icu_locid", - "icu_provider_macros", + "icu_locale_core", "stable_deref_trait", "tinystr", "writeable", - "yoke", + "yoke 0.8.0", "zerofrom", + "zerotrie", "zerovec", ] -[[package]] -name = "icu_provider_macros" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.101", -] - [[package]] name = "id_tree" version = "1.8.0" @@ -3438,9 +3299,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" dependencies = [ "icu_normalizer", "icu_properties", @@ -3469,7 +3330,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" dependencies = [ "equivalent", - "hashbrown 0.15.2", + "hashbrown 0.15.4", ] [[package]] @@ -3490,7 +3351,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "inotify-sys", "libc", ] @@ -3504,21 +3365,11 @@ dependencies = [ "libc", ] -[[package]] -name = "inout" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" -dependencies = [ - "block-padding", - "generic-array", -] - [[package]] name = "intl-memoizer" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe22e020fce238ae18a6d5d8c502ee76a52a6e880d99477657e6acc30ec57bda" +checksum = "310da2e345f5eb861e7a07ee182262e94975051db9e4223e909ba90f392f163f" dependencies = [ "type-map", "unic-langid", @@ -3540,14 +3391,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" [[package]] -name = "is-terminal" -version = "0.4.16" +name = "iri-string" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" dependencies = [ - "hermit-abi 0.5.0", - "libc", - "windows-sys 0.59.0", + "memchr", + "serde", ] [[package]] @@ -3591,9 +3441,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jiff" -version = "0.2.10" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a064218214dc6a10fbae5ec5fa888d80c45d611aba169222fc272072bf7aef6" +checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" dependencies = [ "jiff-static", "log", @@ -3604,13 +3454,13 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.10" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "199b7932d97e325aff3a7030e141eafe7f2c6268e1d1b24859b753a627f45254" +checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -3625,7 +3475,7 @@ version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" dependencies = [ - "getrandom 0.3.2", + "getrandom 0.3.3", "libc", ] @@ -3668,9 +3518,9 @@ checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" [[package]] name = "kqueue" -version = "1.0.8" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7447f1ca1b7b563588a205fe93dea8df60fd981423a768bc1c0ded35ed147d0c" +checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a" dependencies = [ "kqueue-sys", "libc", @@ -3686,6 +3536,20 @@ dependencies = [ "libc", ] +[[package]] +name = "launcher" +version = "1.0.0" +dependencies = [ + "anki_io", + "anki_process", + "anyhow", + "dirs 6.0.0", + "embed-resource", + "libc", + "libc-stdhandle", + "winapi", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -3694,35 +3558,35 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.172" +version = "0.2.173" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" +checksum = "d8cfeafaffdbc32176b64fb251369d52ea9f0a8fbc6f8759edffef7b525d64bb" [[package]] -name = "libdbus-sys" -version = "0.2.5" +name = "libc-stdhandle" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06085512b750d640299b79be4bad3d2fa90a9c00b1fd9e1b46364f66f0485c72" +checksum = "6dac2473dc28934c5e0b82250dab231c9d3b94160d91fe9ff483323b05797551" dependencies = [ "cc", - "pkg-config", + "libc", ] [[package]] name = "libloading" -version = "0.8.6" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" +checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" dependencies = [ "cfg-if", - "windows-targets 0.52.6", + "windows-targets 0.53.2", ] [[package]] name = "libm" -version = "0.2.13" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9627da5196e5d8ed0b0495e61e518847578da83483c37288316d9b2e03a7f72" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" [[package]] name = "libredox" @@ -3730,22 +3594,31 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "libc", "redox_syscall", ] [[package]] name = "libsqlite3-sys" -version = "0.27.0" +version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf4e226dcd58b4be396f7bd3c20da8fdee2911400705297ba7d2d7cc2c30f716" +checksum = "91632f3b4fb6bd1d72aa3d78f41ffecfcf2b1a6648d8c241dbe7dbfaf4875e15" dependencies = [ "cc", "pkg-config", "vcpkg", ] +[[package]] +name = "libz-rs-sys" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "172a788537a2221661b480fee8dc5f96c580eb34fa88764d3205dc356c7e4221" +dependencies = [ + "zlib-rs", +] + [[package]] name = "linkcheck" version = "0.4.1" @@ -3776,11 +3649,11 @@ version = "0.0.0" dependencies = [ "anki", "futures", - "itertools 0.13.0", + "itertools 0.14.0", "linkcheck", "regex", - "reqwest 0.12.15", - "strum 0.26.3", + "reqwest 0.12.20", + "strum 0.27.1", "tokio", ] @@ -3793,12 +3666,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "linux-raw-sys" -version = "0.4.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" - [[package]] name = "linux-raw-sys" version = "0.9.4" @@ -3807,9 +3674,9 @@ checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" [[package]] name = "litemap" -version = "0.7.5" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" [[package]] name = "litrs" @@ -3819,9 +3686,9 @@ checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" [[package]] name = "lock_api" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" dependencies = [ "autocfg", "scopeguard", @@ -3833,6 +3700,12 @@ version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + [[package]] name = "lzma-sys" version = "0.1.20" @@ -3852,54 +3725,29 @@ checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" [[package]] name = "macerator" -version = "0.2.6" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f684f0f95ca0724667e4baf9bf60dc662cb2f6235fd9402d754f5512440efe0e" +checksum = "bce07f822458c4c303081d133a90610406162e7c8df17434956ac1892faf447b" dependencies = [ "bytemuck", + "cfg_aliases", "half", "macerator-macros", + "moddef", "num-traits", "paste", ] [[package]] name = "macerator-macros" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "806a19478649154a009ef47e9361db11ed392a8f7978590eed4a590ceef01bdd" +checksum = "a2b955a106dca78c0577269d67a6d56114abb8644b810fc995a22348276bb9dd" dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.101", -] - -[[package]] -name = "makeapp" -version = "0.0.0" -dependencies = [ - "anyhow", - "apple-bundles", - "camino", - "clap", - "glob", - "plist", - "serde", - "serde_json", - "simple-file-manifest", - "walkdir", -] - -[[package]] -name = "makeexe" -version = "0.0.0" -dependencies = [ - "anyhow", - "camino", - "clap", - "tugger-windows-codesign", - "walkdir", + "syn 2.0.103", ] [[package]] @@ -3962,7 +3810,7 @@ checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -3982,15 +3830,15 @@ checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" [[package]] name = "matchit" -version = "0.7.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" [[package]] name = "matrixmultiply" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9380b911e3e96d10c1f415da0876389aaf1b56759054eeb0de7df940c456ba1a" +checksum = "a06de3016e9fae57a36fd14dba131fccf49f74b40b7fbdb472f96e361ec71a08" dependencies = [ "autocfg", "num_cpus", @@ -4007,9 +3855,9 @@ checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" [[package]] name = "mdbook" -version = "0.4.48" +version = "0.4.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b6fbb4ac2d9fd7aa987c3510309ea3c80004a968d063c42f0d34fea070817c1" +checksum = "a87e65420ab45ca9c1b8cdf698f95b710cc826d373fa550f0f7fad82beac9328" dependencies = [ "ammonia", "anyhow", @@ -4026,7 +3874,6 @@ dependencies = [ "memchr", "notify", "notify-debouncer-mini", - "once_cell", "opener", "pathdiff", "pulldown-cmark 0.10.3", @@ -4037,7 +3884,7 @@ dependencies = [ "shlex", "tempfile", "tokio", - "toml", + "toml 0.5.11", "topological-sort", "walkdir", "warp", @@ -4045,9 +3892,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.4" +version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" [[package]] name = "memmap2" @@ -4074,7 +3921,7 @@ version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f569fb946490b5743ad69813cb19629130ce9374034abe31614a36402d18f99e" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "block", "core-graphics-types", "foreign-types 0.5.0", @@ -4119,25 +3966,31 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", ] [[package]] name = "mio" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", "log", - "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.52.0", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.59.0", ] +[[package]] +name = "moddef" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e519fd9c6131c1c9a4a67f8bdc4f32eb4105b16c1468adea1b8e68c98c85ec4" + [[package]] name = "multer" version = "3.1.0" @@ -4157,9 +4010,9 @@ dependencies = [ [[package]] name = "multimap" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03" +checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" [[package]] name = "naga" @@ -4169,11 +4022,11 @@ checksum = "2b977c445f26e49757f9aca3631c3b8b836942cb278d69a92e7b80d3b24da632" dependencies = [ "arrayvec", "bit-set", - "bitflags 2.9.0", + "bitflags 2.9.1", "cfg_aliases", "codespan-reporting 0.12.0", "half", - "hashbrown 0.15.2", + "hashbrown 0.15.4", "hexf-parse", "indexmap", "log", @@ -4219,17 +4072,6 @@ dependencies = [ "rayon", ] -[[package]] -name = "ndarray-rand" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f093b3db6fd194718dcdeea6bd8c829417deae904e3fcc7732dabcd4416d25d8" -dependencies = [ - "ndarray", - "rand 0.8.5", - "rand_distr 0.4.3", -] - [[package]] name = "ndk-sys" version = "0.5.0+25.2.9519653" @@ -4254,9 +4096,13 @@ dependencies = [ "camino", "dunce", "globset", - "itertools 0.13.0", + "itertools 0.14.0", "maplit", "num_cpus", + "regex", + "reqwest 0.12.20", + "serde_json", + "sha2", "walkdir", "which", ] @@ -4271,12 +4117,6 @@ dependencies = [ "minimal-lexical", ] -[[package]] -name = "nonempty" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9e591e719385e6ebaeb5ce5d3887f7d5676fceca6411d1925ccc95745f3d6f7" - [[package]] name = "normpath" version = "1.3.0" @@ -4292,7 +4132,7 @@ version = "8.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fee8403b3d66ac7b26aee6e40a897d85dc5ce26f44da36b8b73e987cc52e943" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "filetime", "fsevent-sys", "inotify", @@ -4450,11 +4290,11 @@ dependencies = [ [[package]] name = "num_cpus" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" dependencies = [ - "hermit-abi 0.3.9", + "hermit-abi", "libc", ] @@ -4476,7 +4316,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -4485,7 +4325,7 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c9bff0aa1d48904a1385ea2a8b97576fbdcbc9a3cfccd0d31fe978e1c4038c5" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "libloading", "nvml-wrapper-sys", "static_assertions", @@ -4526,6 +4366,12 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" + [[package]] name = "oorandom" version = "11.1.5" @@ -4534,23 +4380,22 @@ checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" [[package]] name = "opener" -version = "0.7.2" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0812e5e4df08da354c851a3376fead46db31c2214f849d3de356d774d057681" +checksum = "771b9704f8cd8b424ec747a320b30b47517a6966ba2c7da90047c16f4a962223" dependencies = [ "bstr", - "dbus", "normpath", "windows-sys 0.59.0", ] [[package]] name = "openssl" -version = "0.10.72" +version = "0.10.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da" +checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "cfg-if", "foreign-types 0.3.2", "libc", @@ -4567,7 +4412,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -4578,9 +4423,9 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-sys" -version = "0.9.107" +version = "0.9.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8288979acd84749c744a9014b4382d42b8f7b2592847b5afb2ed29e5d16ede07" +checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" dependencies = [ "cc", "libc", @@ -4612,39 +4457,12 @@ dependencies = [ "num-traits", ] -[[package]] -name = "os_pipe" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ffd2b0a5634335b135d5728d84c5e0fd726954b87111f7506a61c502280d982" -dependencies = [ - "libc", - "windows-sys 0.59.0", -] - [[package]] name = "overload" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" -[[package]] -name = "p12" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4873306de53fe82e7e484df31e1e947d61514b6ea2ed6cd7b45d63006fd9224" -dependencies = [ - "cbc", - "cipher", - "des", - "getrandom 0.2.16", - "hmac", - "lazy_static", - "rc2", - "sha1", - "yasna", -] - [[package]] name = "parking" version = "2.2.1" @@ -4653,9 +4471,9 @@ checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "parking_lot" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" dependencies = [ "lock_api", "parking_lot_core", @@ -4663,9 +4481,9 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.10" +version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" dependencies = [ "cfg-if", "libc", @@ -4709,15 +4527,6 @@ dependencies = [ "sha2", ] -[[package]] -name = "pem" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8835c273a76a90455d7344889b0964598e3316e2a79ede8e36f16bdcf2228b8" -dependencies = [ - "base64 0.13.1", -] - [[package]] name = "percent-encoding" version = "2.3.1" @@ -4731,9 +4540,9 @@ source = "git+https://github.com/ankitects/rust-url.git?rev=bb930b8d089f4d30d7d1 [[package]] name = "pest" -version = "2.8.0" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "198db74531d58c70a361c42201efde7e2591e976d518caf7662a47dc5720e7b6" +checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323" dependencies = [ "memchr", "thiserror 2.0.12", @@ -4742,9 +4551,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.8.0" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d725d9cfd79e87dccc9341a2ef39d1b6f6353d68c4b33c177febbe1a402c97c5" +checksum = "bb056d9e8ea77922845ec74a1c4e8fb17e7c218cc4fc11a15c5d25e189aa40bc" dependencies = [ "pest", "pest_generator", @@ -4752,24 +4561,23 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.8.0" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db7d01726be8ab66ab32f9df467ae8b1148906685bbe75c82d1e65d7f5b3f841" +checksum = "87e404e638f781eb3202dc82db6760c8ae8a1eeef7fb3fa8264b2ef280504966" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] name = "pest_meta" -version = "2.8.0" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f9f832470494906d1fca5329f8ab5791cc60beb230c74815dff541cbd2b5ca0" +checksum = "edd1101f170f5903fde0914f899bb503d9ff5271d7ba76bbb70bea63690cc0d5" dependencies = [ - "once_cell", "pest", "sha2", ] @@ -4853,7 +4661,7 @@ dependencies = [ "phf_shared 0.11.3", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -4891,7 +4699,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -4912,19 +4720,6 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" -[[package]] -name = "plist" -version = "1.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac26e981c03a6e53e0aee43c113e3202f5581d5360dae7bd2c70e800dd0451d" -dependencies = [ - "base64 0.22.1", - "indexmap", - "quick-xml", - "serde", - "time", -] - [[package]] name = "plotters" version = "0.3.7" @@ -4955,9 +4750,9 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" dependencies = [ "serde", ] @@ -4971,6 +4766,15 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "potential_utf" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +dependencies = [ + "zerovec", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -4983,7 +4787,7 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ - "zerocopy 0.8.25", + "zerocopy", ] [[package]] @@ -5000,19 +4804,19 @@ checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" [[package]] name = "prettyplease" -version = "0.2.32" +version = "0.2.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "664ec5419c51e34154eec046ebcba56312d5a2fc3b09a06da188e1ad21afadf6" +checksum = "6837b9e10d61f45f987d50808f83d1ee3d206c66acf650c3e4ae2e1f6ddedf55" dependencies = [ "proc-macro2", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] name = "priority-queue" -version = "2.3.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef08705fa1589a1a59aa924ad77d14722cb0cd97b67dd5004ed5f4a4873fce8d" +checksum = "5676d703dda103cbb035b653a9f11448c0a7216c7926bd35fcb5865475d0c970" dependencies = [ "autocfg", "equivalent", @@ -5075,7 +4879,7 @@ dependencies = [ "prost", "prost-types", "regex", - "syn 2.0.101", + "syn 2.0.103", "tempfile", ] @@ -5089,7 +4893,7 @@ dependencies = [ "itertools 0.14.0", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -5126,25 +4930,26 @@ dependencies = [ [[package]] name = "pulldown-cmark" -version = "0.9.6" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57206b407293d2bcd3af849ce869d52068623f19e1b5ff8e8778e3309439682b" +checksum = "76979bea66e7875e7509c4ec5300112b316af87fa7a252ca91c448b32dfe3993" dependencies = [ - "bitflags 2.9.0", - "getopts", + "bitflags 2.9.1", "memchr", + "pulldown-cmark-escape 0.10.1", "unicase", ] [[package]] name = "pulldown-cmark" -version = "0.10.3" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76979bea66e7875e7509c4ec5300112b316af87fa7a252ca91c448b32dfe3993" +checksum = "1e8bbe1a966bd2f362681a44f6edce3c2310ac21e4d5067a6e7ec396297a6ea0" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", + "getopts", "memchr", - "pulldown-cmark-escape", + "pulldown-cmark-escape 0.11.0", "unicase", ] @@ -5154,6 +4959,12 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd348ff538bc9caeda7ee8cad2d1d48236a1f443c1fa3913c6a02fe0043b1dd3" +[[package]] +name = "pulldown-cmark-escape" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" + [[package]] name = "pulp" version = "0.18.22" @@ -5168,9 +4979,9 @@ dependencies = [ [[package]] name = "pulp" -version = "0.21.4" +version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95fb7a99b37aaef4c7dd2fd15a819eb8010bfc7a2c2155230d51f497316cad6d" +checksum = "96b86df24f0a7ddd5e4b95c94fc9ed8a98f1ca94d3b01bdce2824097e7835907" dependencies = [ "bytemuck", "cfg-if", @@ -5182,11 +4993,10 @@ dependencies = [ [[package]] name = "pyo3" -version = "0.24.2" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5203598f366b11a02b13aa20cab591229ff0a89fd121a308a5df751d5fc9219" +checksum = "8970a78afe0628a3e3430376fc5fd76b6b45c4d43360ffd6cdd40bdde72b682a" dependencies = [ - "cfg-if", "indoc", "libc", "memoffset", @@ -5200,9 +5010,9 @@ dependencies = [ [[package]] name = "pyo3-build-config" -version = "0.24.2" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99636d423fa2ca130fa5acde3059308006d46f98caac629418e53f7ebb1e9999" +checksum = "458eb0c55e7ece017adeba38f2248ff3ac615e53660d7c71a238d7d2a01c7598" dependencies = [ "once_cell", "target-lexicon", @@ -5210,9 +5020,9 @@ dependencies = [ [[package]] name = "pyo3-ffi" -version = "0.24.2" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78f9cf92ba9c409279bc3305b5409d90db2d2c22392d443a87df3a1adad59e33" +checksum = "7114fe5457c61b276ab77c5055f206295b812608083644a5c5b2640c3102565c" dependencies = [ "libc", "pyo3-build-config", @@ -5220,43 +5030,34 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.24.2" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b999cb1a6ce21f9a6b147dcf1be9ffedf02e0043aec74dc390f3007047cecd9" +checksum = "a8725c0a622b374d6cb051d11a0983786448f7785336139c3c94f5aa6bef7e50" dependencies = [ "proc-macro2", "pyo3-macros-backend", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] name = "pyo3-macros-backend" -version = "0.24.2" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "822ece1c7e1012745607d5cf0bcb2874769f0f7cb34c4cde03b9358eb9ef911a" +checksum = "4109984c22491085343c05b0dbc54ddc405c3cf7b4374fc533f5c3313a572ccc" dependencies = [ "heck", "proc-macro2", "pyo3-build-config", "quote", - "syn 2.0.101", -] - -[[package]] -name = "quick-xml" -version = "0.32.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d3a6e5838b60e0e8fa7a43f22ade549a37d61f8bdbe636d0d7816191de969c2" -dependencies = [ - "memchr", + "syn 2.0.103", ] [[package]] name = "quinn" -version = "0.11.7" +version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3bd15a6f2967aef83887dcb9fec0014580467e33720d073560cf015a5683012" +checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8" dependencies = [ "bytes", "cfg_aliases", @@ -5264,7 +5065,7 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash 2.1.1", - "rustls 0.23.26", + "rustls", "socket2", "thiserror 2.0.12", "tokio", @@ -5274,16 +5075,17 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.11" +version = "0.11.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcbafbbdbb0f638fe3f35f3c56739f77a8a1d070cb25603226c83339b391472b" +checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e" dependencies = [ "bytes", - "getrandom 0.3.2", + "getrandom 0.3.3", + "lru-slab", "rand 0.9.1", - "ring 0.17.14", + "ring", "rustc-hash 2.1.1", - "rustls 0.23.26", + "rustls", "rustls-pki-types", "slab", "thiserror 2.0.12", @@ -5294,9 +5096,9 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.11" +version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "541d0f57c6ec747a90738a52741d3221f7960e8ac2f0ff4b1a63680e033b4ab5" +checksum = "ee4e529991f949c5e25755532370b8af5d114acae52326361d68d47af64aa842" dependencies = [ "cfg_aliases", "libc", @@ -5377,17 +5179,7 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ - "getrandom 0.3.2", -] - -[[package]] -name = "rand_distr" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" -dependencies = [ - "num-traits", - "rand 0.8.5", + "getrandom 0.3.3", ] [[package]] @@ -5421,7 +5213,7 @@ version = "11.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6df7ab838ed27997ba19a4664507e6f82b41fe6e20be42929332156e5e85146" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", ] [[package]] @@ -5456,27 +5248,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "rc2" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62c64daa8e9438b84aaae55010a93f396f8e60e3911590fcba770d04643fc1dd" -dependencies = [ - "cipher", -] - -[[package]] -name = "rcgen" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffbe84efe2f38dea12e9bfc1f65377fdf03e53a18cb3b995faedf7934c7e785b" -dependencies = [ - "pem", - "ring 0.16.20", - "time", - "yasna", -] - [[package]] name = "reborrow" version = "0.5.5" @@ -5485,11 +5256,11 @@ checksum = "03251193000f4bd3b042892be858ee50e8b3719f2b08e5833ac4353724632430" [[package]] name = "redox_syscall" -version = "0.5.11" +version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2f103c6d277498fbceb16e84d317e2a400f160f46904d5f5410848c829511a3" +checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", ] [[package]] @@ -5585,7 +5356,6 @@ dependencies = [ "http 0.2.12", "http-body 0.4.6", "hyper 0.14.32", - "hyper-rustls 0.24.2", "hyper-tls 0.5.0", "ipnet", "js-sys", @@ -5595,7 +5365,6 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", - "rustls 0.21.12", "rustls-pemfile 1.0.4", "serde", "serde_json", @@ -5604,46 +5373,41 @@ dependencies = [ "system-configuration", "tokio", "tokio-native-tls", - "tokio-rustls 0.24.1", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots 0.25.4", "winreg 0.50.0", ] [[package]] name = "reqwest" -version = "0.12.15" +version = "0.12.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb" +checksum = "eabf4c97d9130e2bf606614eb937e86edac8292eaa6f422f995d7e8de1eb1813" dependencies = [ "base64 0.22.1", "bytes", + "futures-channel", "futures-core", "futures-util", "http 1.3.1", "http-body 1.0.1", "http-body-util", "hyper 1.6.0", - "hyper-rustls 0.27.5", + "hyper-rustls", "hyper-tls 0.6.0", "hyper-util", - "ipnet", "js-sys", "log", - "mime", "mime_guess", "native-tls", - "once_cell", "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.26", + "rustls", "rustls-native-certs", - "rustls-pemfile 2.2.0", "rustls-pki-types", "serde", "serde_json", @@ -5651,33 +5415,17 @@ dependencies = [ "sync_wrapper 1.0.2", "tokio", "tokio-native-tls", - "tokio-rustls 0.26.2", - "tokio-socks", + "tokio-rustls", "tokio-util", "tower", + "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "wasm-streams", "web-sys", - "webpki-roots 0.26.8", - "windows-registry", -] - -[[package]] -name = "ring" -version = "0.16.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" -dependencies = [ - "cc", - "libc", - "once_cell", - "spin 0.5.2", - "untrusted 0.7.1", - "web-sys", - "winapi", + "webpki-roots", ] [[package]] @@ -5690,7 +5438,7 @@ dependencies = [ "cfg-if", "getrandom 0.2.16", "libc", - "untrusted 0.9.0", + "untrusted", "windows-sys 0.52.0", ] @@ -5750,7 +5498,7 @@ dependencies = [ "regex", "relative-path", "rustc_version", - "syn 2.0.101", + "syn 2.0.103", "unicode-ident", ] @@ -5765,24 +5513,24 @@ dependencies = [ "clap", "flate2", "junction", - "reqwest 0.12.15", + "reqwest 0.12.20", "sha2", "tar", "termcolor", "tokio", "which", "xz2", - "zip 0.6.6", + "zip 4.1.0", "zstd", ] [[package]] name = "rusqlite" -version = "0.30.0" +version = "0.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a78046161564f5e7cd9008aff3b2990b3850dc8e0349119b98e8f251e099f24d" +checksum = "3de23c3319433716cf134eed225fe9986bc24f63bed9be9f20c329029e672dc7" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "fallible-iterator", "fallible-streaming-iterator", "hashlink", @@ -5792,9 +5540,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.24" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" [[package]] name = "rustc-hash" @@ -5819,52 +5567,27 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.44" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "errno", "libc", - "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", -] - -[[package]] -name = "rustix" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf" -dependencies = [ - "bitflags 2.9.0", - "errno", - "libc", - "linux-raw-sys 0.9.4", + "linux-raw-sys", "windows-sys 0.59.0", ] [[package]] name = "rustls" -version = "0.21.12" +version = "0.23.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" -dependencies = [ - "log", - "ring 0.17.14", - "rustls-webpki 0.101.7", - "sct", -] - -[[package]] -name = "rustls" -version = "0.23.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df51b5869f3a441595eac5e8ff14d486ff285f7b8c0df8770e49c3b56351f0f0" +checksum = "7160e3e10bf4535308537f3c4e1641468cd0e485175d6163087c0393c7d46643" dependencies = [ "once_cell", - "ring 0.17.14", + "ring", "rustls-pki-types", - "rustls-webpki 0.103.1", + "rustls-webpki", "subtle", "zeroize", ] @@ -5901,39 +5624,30 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" dependencies = [ "web-time", + "zeroize", ] [[package]] name = "rustls-webpki" -version = "0.101.7" +version = "0.103.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" dependencies = [ - "ring 0.17.14", - "untrusted 0.9.0", -] - -[[package]] -name = "rustls-webpki" -version = "0.103.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fef8b8769aaccf73098557a87cd1816b4f9c7c16811c9c77142aa695c16f2c03" -dependencies = [ - "ring 0.17.14", + "ring", "rustls-pki-types", - "untrusted 0.9.0", + "untrusted", ] [[package]] name = "rustversion" -version = "1.0.20" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" +checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" [[package]] name = "ryu" @@ -6000,23 +5714,13 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "sct" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" -dependencies = [ - "ring 0.17.14", - "untrusted 0.9.0", -] - [[package]] name = "security-framework" version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "core-foundation 0.9.4", "core-foundation-sys", "libc", @@ -6029,8 +5733,8 @@ version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" dependencies = [ - "bitflags 2.9.0", - "core-foundation 0.10.0", + "bitflags 2.9.1", + "core-foundation 0.10.1", "core-foundation-sys", "libc", "security-framework-sys", @@ -6046,15 +5750,6 @@ dependencies = [ "libc", ] -[[package]] -name = "self_cell" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e14e4d63b804dc0c7ec4a1e52bcb63f02c7ac94476755aa579edac21e01f915d" -dependencies = [ - "self_cell 1.2.0", -] - [[package]] name = "self_cell" version = "1.2.0" @@ -6121,7 +5816,7 @@ checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -6154,14 +5849,23 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", ] [[package]] name = "serde_tuple" -version = "0.5.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4f025b91216f15a2a32aa39669329a475733590a015835d1783549a56d09427" +checksum = "f0f9b739e59a0e07b7a73bc11c3dcd6abf790d0f54042b67a422d4bd1f6cf6c0" dependencies = [ "serde", "serde_tuple_macros", @@ -6169,9 +5873,9 @@ dependencies = [ [[package]] name = "serde_tuple_macros" -version = "0.5.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4076151d1a2b688e25aaf236997933c66e18b870d0369f8b248b8ab2be630d7e" +checksum = "9e87546e85c5047d03b454d12ee25266fc269a461a4029956ca58d246b9aefae" dependencies = [ "proc-macro2", "quote", @@ -6203,9 +5907,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.8" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures", @@ -6221,16 +5925,6 @@ dependencies = [ "lazy_static", ] -[[package]] -name = "shared_child" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09fa9338aed9a1df411814a5b2252f7cd206c55ae9bf2fa763f8de84603aa60c" -dependencies = [ - "libc", - "windows-sys 0.59.0", -] - [[package]] name = "shlex" version = "1.3.0" @@ -6247,10 +5941,10 @@ dependencies = [ ] [[package]] -name = "simple-file-manifest" -version = "0.11.0" +name = "simd-adler32" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd19be0257552dd56d1bb6946f89f193c6e5b9f13cc9327c4bc84a357507c74" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" [[package]] name = "siphasher" @@ -6266,12 +5960,9 @@ checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" [[package]] name = "slab" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] +checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" [[package]] name = "slotmap" @@ -6284,29 +5975,29 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.15.0" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "snafu" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "223891c85e2a29c3fe8fb900c1fae5e69c2e42415e3177752e8718475efa5019" +checksum = "320b01e011bf8d5d7a4a4a4be966d9160968935849c83b918827f6a435e7f627" dependencies = [ "snafu-derive", ] [[package]] name = "snafu-derive" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c3c6b7927ffe7ecaa769ee0e3994da3b8cafc8f444578982c83ecb161af917" +checksum = "1961e2ef424c1424204d3a5d6975f934f56b6d50ff5732382d84ebf460e147f7" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -6317,20 +6008,14 @@ checksum = "27207bb65232eda1f588cf46db2fee75c0808d557f6b3cf19a75f5d6d7c94df1" [[package]] name = "socket2" -version = "0.5.9" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" dependencies = [ "libc", "windows-sys 0.52.0", ] -[[package]] -name = "spin" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" - [[package]] name = "spin" version = "0.9.8" @@ -6357,7 +6042,7 @@ version = "0.3.0+sdk-1.3.268.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", ] [[package]] @@ -6431,7 +6116,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -6444,7 +6129,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -6466,9 +6151,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.101" +version = "2.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" +checksum = "e4307e30089d6fd6aff212f2da3a1f9e32f3223b1f010fb09b7c95f90f3ca1e8" dependencies = [ "proc-macro2", "quote", @@ -6492,13 +6177,13 @@ dependencies = [ [[package]] name = "synstructure" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -6507,7 +6192,7 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec7dddc5f0fee506baf8b9fdb989e242f17e4b11c61dfbb0635b705217199eea" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "byteorder", "enum-as-inner", "libc", @@ -6521,7 +6206,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01198a2debb237c62b6826ec7081082d951f46dbb64b0e8c7649a452230d1dfc" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "byteorder", "enum-as-inner", "libc", @@ -6540,7 +6225,7 @@ dependencies = [ "memchr", "ntapi", "rayon", - "windows 0.57.0", + "windows 0.56.0", ] [[package]] @@ -6597,14 +6282,14 @@ checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a" [[package]] name = "tempfile" -version = "3.19.1" +version = "3.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" +checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" dependencies = [ "fastrand", - "getrandom 0.3.2", + "getrandom 0.3.3", "once_cell", - "rustix 1.0.5", + "rustix", "windows-sys 0.59.0", ] @@ -6634,7 +6319,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "45c6481c4829e4cc63825e62c49186a34538b7b2750b73b266581ffb612fb5ed" dependencies = [ - "rustix 1.0.5", + "rustix", "windows-sys 0.59.0", ] @@ -6675,7 +6360,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -6686,7 +6371,7 @@ checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -6700,12 +6385,11 @@ dependencies = [ [[package]] name = "thread_local" -version = "1.1.8" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" dependencies = [ "cfg-if", - "once_cell", ] [[package]] @@ -6741,9 +6425,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.7.6" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" dependencies = [ "displaydoc", "zerovec", @@ -6776,9 +6460,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.44.2" +version = "1.45.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48" +checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" dependencies = [ "backtrace", "bytes", @@ -6799,7 +6483,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -6812,35 +6496,13 @@ dependencies = [ "tokio", ] -[[package]] -name = "tokio-rustls" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" -dependencies = [ - "rustls 0.21.12", - "tokio", -] - [[package]] name = "tokio-rustls" version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" dependencies = [ - "rustls 0.23.26", - "tokio", -] - -[[package]] -name = "tokio-socks" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d4770b8024672c1101b3f6733eab95b18007dbe0847a8afe341fcf79e06043f" -dependencies = [ - "either", - "futures-util", - "thiserror 1.0.69", + "rustls", "tokio", ] @@ -6879,22 +6541,46 @@ dependencies = [ ] [[package]] -name = "toml_datetime" -version = "0.6.9" +name = "toml" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] [[package]] name = "toml_edit" -version = "0.22.25" +version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10558ed0bd2a1562e630926a2d1f0b98c827da99fabd3fe20920a59642504485" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ "indexmap", + "serde", + "serde_spanned", "toml_datetime", + "toml_write", "winnow", ] +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + [[package]] name = "topological-sort" version = "0.2.2" @@ -6919,16 +6605,18 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.5.2" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "bytes", + "futures-util", "http 1.3.1", "http-body 1.0.1", - "http-body-util", + "iri-string", "pin-project-lite", + "tower", "tower-layer", "tower-service", "tracing", @@ -6972,20 +6660,20 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.28" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +checksum = "1b1ffbcf9c6f6b99d386e7444eb608ba646ae452a36b39737deb9663b610f662" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] name = "tracing-core" -version = "0.1.33" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" dependencies = [ "once_cell", "valuable", @@ -7026,58 +6714,6 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" -[[package]] -name = "tugger-common" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90d950380afdb1a6bbe74f29485a04e821869dfad11f5929ff1c5b1dac09d02" -dependencies = [ - "anyhow", - "fs2", - "glob", - "hex", - "log", - "once_cell", - "reqwest 0.11.27", - "sha2", - "tempfile", - "url", - "zip 0.6.6", -] - -[[package]] -name = "tugger-windows" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9f181ac4fc7f8facfd418824d13045cd068ee73de44319a6116868c22789782" -dependencies = [ - "anyhow", - "duct", - "find-winsdk", - "glob", - "once_cell", - "semver", - "tugger-common", - "winapi", -] - -[[package]] -name = "tugger-windows-codesign" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed3f09f8bdace495373cec3fc607bc39f00720a984ba82e310cc9382462fd364" -dependencies = [ - "anyhow", - "duct", - "log", - "p12", - "rcgen", - "time", - "tugger-common", - "tugger-windows", - "yasna", -] - [[package]] name = "tungstenite" version = "0.21.0" @@ -7099,11 +6735,11 @@ dependencies = [ [[package]] name = "type-map" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "deb68604048ff8fa93347f02441e4487594adc20bb8a084f9e564d2b827a0a9f" +checksum = "cb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90" dependencies = [ - "rustc-hash 1.1.0", + "rustc-hash 2.1.1", ] [[package]] @@ -7136,7 +6772,7 @@ dependencies = [ "serde", "thiserror 1.0.69", "tracing", - "yoke", + "yoke 0.7.5", ] [[package]] @@ -7162,9 +6798,9 @@ checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" [[package]] name = "unic-langid" -version = "0.9.5" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23dd9d1e72a73b25e07123a80776aae3e7b0ec461ef94f9151eed6ec88005a44" +checksum = "a28ba52c9b05311f4f6e62d5d9d46f094bd6e84cb8df7b3ef952748d752a7d05" dependencies = [ "unic-langid-impl", "unic-langid-macros", @@ -7172,18 +6808,18 @@ dependencies = [ [[package]] name = "unic-langid-impl" -version = "0.9.5" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a5422c1f65949306c99240b81de9f3f15929f5a8bfe05bb44b034cc8bf593e5" +checksum = "dce1bf08044d4b7a94028c93786f8566047edc11110595914de93362559bc658" dependencies = [ "tinystr", ] [[package]] name = "unic-langid-macros" -version = "0.9.5" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0da1cd2c042d3c7569a1008806b02039e7a4a2bdf8f8e96bd3c792434a0e275e" +checksum = "d5957eb82e346d7add14182a3315a7e298f04e1ba4baac36f7f0dbfedba5fc25" dependencies = [ "proc-macro-hack", "tinystr", @@ -7193,13 +6829,13 @@ dependencies = [ [[package]] name = "unic-langid-macros-impl" -version = "0.9.5" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ed7f4237ba393424195053097c1516bd4590dc82b84f2f97c5c69e12704555b" +checksum = "a1249a628de3ad34b821ecb1001355bca3940bcb2f88558f1a8bd82e977f75b5" dependencies = [ "proc-macro-hack", "quote", - "syn 2.0.101", + "syn 2.0.103", "unic-langid-impl", ] @@ -7262,9 +6898,9 @@ checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] name = "unicode-width" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" +checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" [[package]] name = "unicode-xid" @@ -7278,12 +6914,6 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" -[[package]] -name = "untrusted" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" - [[package]] name = "untrusted" version = "0.9.0" @@ -7314,12 +6944,6 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" -[[package]] -name = "utf16_iter" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" - [[package]] name = "utf8_iter" version = "1.0.4" @@ -7334,9 +6958,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" +checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" [[package]] name = "valuable" @@ -7352,7 +6976,7 @@ checksum = "41b6d82be61465f97d42bd1d15bf20f3b0a3a0905018f38f9d6f6962055b0b5c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -7367,6 +6991,26 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "walkdir" version = "2.5.0" @@ -7416,9 +7060,9 @@ dependencies = [ [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasi" @@ -7435,7 +7079,7 @@ version = "0.12.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1fbb4ef9bbca0c1170e0b00dd28abc9e3b68669821600cad1caaed606583c6d" dependencies = [ - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi 0.11.1+wasi-snapshot-preview1", ] [[package]] @@ -7460,7 +7104,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", "wasm-bindgen-shared", ] @@ -7495,7 +7139,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -7544,9 +7188,9 @@ dependencies = [ [[package]] name = "web_atoms" -version = "0.1.0" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "954c5a41f2bcb7314344079d0891505458cc2f4b422bdea1d5bfbe6d1a04903b" +checksum = "57ffde1dc01240bdf9992e3205668b235e59421fd085e8a317ed98da0178d414" dependencies = [ "phf 0.11.3", "phf_codegen 0.11.3", @@ -7556,30 +7200,24 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.25.4" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" - -[[package]] -name = "webpki-roots" -version = "0.26.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2210b291f7ea53617fbafcc4939f10914214ec15aace5ba62293a668f322c5c9" +checksum = "2853738d1cc4f2da3a225c18ec6c3721abb31961096e9dbf5ab35fa88b19cfdb" dependencies = [ "rustls-pki-types", ] [[package]] name = "wgpu" -version = "25.0.0" +version = "25.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca6049eb2014a0e0d8689f9b787605dd71d5bbfdc74095ead499f3cff705c229" +checksum = "ec8fb398f119472be4d80bc3647339f56eb63b2a331f6a3d16e25d8144197dd9" dependencies = [ "arrayvec", - "bitflags 2.9.0", + "bitflags 2.9.1", "cfg_aliases", "document-features", - "hashbrown 0.15.2", + "hashbrown 0.15.4", "js-sys", "log", "naga", @@ -7599,17 +7237,17 @@ dependencies = [ [[package]] name = "wgpu-core" -version = "25.0.1" +version = "25.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a19813e647da7aa3cdaa84f5846e2c64114970ea7c86b1e6aae8be08091f4bdc" +checksum = "f7b882196f8368511d613c6aeec80655160db6646aebddf8328879a88d54e500" dependencies = [ "arrayvec", "bit-set", "bit-vec", - "bitflags 2.9.0", + "bitflags 2.9.1", "cfg_aliases", "document-features", - "hashbrown 0.15.2", + "hashbrown 0.15.4", "indexmap", "log", "naga", @@ -7657,15 +7295,15 @@ dependencies = [ [[package]] name = "wgpu-hal" -version = "25.0.1" +version = "25.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb7c4a1dc42ff14c23c9b11ebf1ee85cde661a9b1cf0392f79c1faca5bc559fb" +checksum = "f968767fe4d3d33747bbd1473ccd55bf0f6451f55d733b5597e67b5deab4ad17" dependencies = [ "android_system_properties", "arrayvec", "ash", "bit-set", - "bitflags 2.9.0", + "bitflags 2.9.1", "block", "bytemuck", "cfg-if", @@ -7676,7 +7314,7 @@ dependencies = [ "gpu-alloc", "gpu-allocator", "gpu-descriptor", - "hashbrown 0.15.2", + "hashbrown 0.15.4", "js-sys", "khronos-egl", "libc", @@ -7708,7 +7346,7 @@ version = "25.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2aa49460c2a8ee8edba3fca54325540d904dd85b2e086ada762767e17d06e8bc" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "bytemuck", "js-sys", "log", @@ -7718,15 +7356,13 @@ dependencies = [ [[package]] name = "which" -version = "5.0.0" +version = "8.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bf3ea8596f3a0dd5980b46430f2058dfe2c36a27ccfbb1845d6fbfcd9ba6e14" +checksum = "d3fabb953106c3c8eea8306e4393700d7657561cb43122571b172bbfb7c7ba1d" dependencies = [ - "either", - "home", - "once_cell", - "rustix 0.38.44", - "windows-sys 0.48.0", + "env_home", + "rustix", + "winsafe", ] [[package]] @@ -7770,16 +7406,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows" -version = "0.57.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" -dependencies = [ - "windows-core 0.57.0", - "windows-targets 0.52.6", -] - [[package]] name = "windows" version = "0.58.0" @@ -7790,6 +7416,28 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + [[package]] name = "windows-core" version = "0.56.0" @@ -7802,18 +7450,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-core" -version = "0.57.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" -dependencies = [ - "windows-implement 0.57.0", - "windows-interface 0.57.0", - "windows-result 0.1.2", - "windows-targets 0.52.6", -] - [[package]] name = "windows-core" version = "0.58.0" @@ -7829,15 +7465,26 @@ dependencies = [ [[package]] name = "windows-core" -version = "0.61.0" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ "windows-implement 0.60.0", "windows-interface 0.59.1", "windows-link", - "windows-result 0.3.2", - "windows-strings 0.4.0", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link", + "windows-threading", ] [[package]] @@ -7848,18 +7495,7 @@ checksum = "f6fc35f58ecd95a9b71c4f2329b911016e6bec66b3f2e6a4aad86bd2e99e2f9b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", -] - -[[package]] -name = "windows-implement" -version = "0.57.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -7870,7 +7506,7 @@ checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -7881,7 +7517,7 @@ checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -7892,18 +7528,7 @@ checksum = "08990546bf4edef8f431fa6326e032865f27138718c587dc21bc0265bbcb57cc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", -] - -[[package]] -name = "windows-interface" -version = "0.57.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -7914,7 +7539,7 @@ checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -7925,24 +7550,23 @@ checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] name = "windows-link" -version = "0.1.1" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" [[package]] -name = "windows-registry" -version = "0.4.0" +name = "windows-numerics" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" dependencies = [ - "windows-result 0.3.2", - "windows-strings 0.3.1", - "windows-targets 0.53.0", + "windows-core 0.61.2", + "windows-link", ] [[package]] @@ -7965,9 +7589,9 @@ dependencies = [ [[package]] name = "windows-result" -version = "0.3.2" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ "windows-link", ] @@ -7984,18 +7608,9 @@ dependencies = [ [[package]] name = "windows-strings" -version = "0.3.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ "windows-link", ] @@ -8027,6 +7642,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.2", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -8060,9 +7684,9 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.0" +version = "0.53.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" +checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" dependencies = [ "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", @@ -8074,6 +7698,15 @@ dependencies = [ "windows_x86_64_msvc 0.53.0", ] +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -8214,23 +7847,13 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" [[package]] name = "winnow" -version = "0.7.7" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cb8234a863ea0e8cd7284fcdd4f145233eb00fee02bbdd9861aec44e6477bc5" +checksum = "74c7b26e3480b707944fc872477815d29a8e429d2f93a1ce000f5fa84a15cbcd" dependencies = [ "memchr", ] -[[package]] -name = "winreg" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a27a759395c1195c4cc5cda607ef6f8f6498f64e78f7900f5de0a127a424704a" -dependencies = [ - "serde", - "winapi", -] - [[package]] name = "winreg" version = "0.50.0" @@ -8241,6 +7864,22 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "winreg" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" +dependencies = [ + "cfg-if", + "windows-sys 0.59.0", +] + +[[package]] +name = "winsafe" +version = "0.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" + [[package]] name = "wiremock" version = "0.6.3" @@ -8271,7 +7910,7 @@ version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", ] [[package]] @@ -8283,20 +7922,14 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] -[[package]] -name = "write16" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" - [[package]] name = "writeable" -version = "0.5.5" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" [[package]] name = "xattr" @@ -8305,7 +7938,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d65cbf2f12c15564212d48f4e3dfb87923d25d611f2aed18f4cb23f0413d89e" dependencies = [ "libc", - "rustix 1.0.5", + "rustix", ] [[package]] @@ -8334,15 +7967,6 @@ dependencies = [ "lzma-sys", ] -[[package]] -name = "yasna" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" -dependencies = [ - "time", -] - [[package]] name = "yoke" version = "0.7.5" @@ -8351,7 +7975,19 @@ checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" dependencies = [ "serde", "stable_deref_trait", - "yoke-derive", + "yoke-derive 0.7.5", + "zerofrom", +] + +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive 0.8.0", "zerofrom", ] @@ -8363,17 +7999,20 @@ checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", "synstructure", ] [[package]] -name = "zerocopy" -version = "0.7.35" +name = "yoke-derive" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ - "zerocopy-derive 0.7.35", + "proc-macro2", + "quote", + "syn 2.0.103", + "synstructure", ] [[package]] @@ -8382,18 +8021,7 @@ version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" dependencies = [ - "zerocopy-derive 0.8.25", -] - -[[package]] -name = "zerocopy-derive" -version = "0.7.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.101", + "zerocopy-derive", ] [[package]] @@ -8404,7 +8032,7 @@ checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", ] [[package]] @@ -8424,7 +8052,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.103", "synstructure", ] @@ -8435,38 +8063,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" [[package]] -name = "zerovec" -version = "0.10.4" +name = "zerotrie" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" dependencies = [ - "yoke", + "displaydoc", + "yoke 0.8.0", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" +dependencies = [ + "yoke 0.8.0", "zerofrom", "zerovec-derive", ] [[package]] name = "zerovec-derive" -version = "0.10.3" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", -] - -[[package]] -name = "zip" -version = "0.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" -dependencies = [ - "byteorder", - "crc32fast", - "crossbeam-utils", - "flate2", - "time", + "syn 2.0.103", ] [[package]] @@ -8484,6 +8110,39 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "zip" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af7dcdb4229c0e79c2531a24de7726a0e980417a74fb4d030a35f535665439a0" +dependencies = [ + "arbitrary", + "crc32fast", + "flate2", + "indexmap", + "memchr", + "time", + "zopfli", +] + +[[package]] +name = "zlib-rs" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "626bd9fa9734751fc50d6060752170984d7053f5a39061f524cda68023d4db8a" + +[[package]] +name = "zopfli" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edfc5ee405f504cd4984ecc6f14d02d55cfda60fa4b689434ef4102aae150cd7" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] + [[package]] name = "zstd" version = "0.13.3" diff --git a/Cargo.toml b/Cargo.toml index 7d1645fc4..980956b05 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,8 +12,7 @@ members = [ "build/runner", "ftl", "pylib/rsbridge", - "qt/bundle/mac", - "qt/bundle/win", + "qt/launcher", "rslib", "rslib/i18n", "rslib/io", @@ -23,7 +22,6 @@ members = [ "rslib/sync", "tools/minilints", ] -exclude = ["qt/bundle"] resolver = "2" [workspace.dependencies.percent-encoding-iri] @@ -35,9 +33,9 @@ git = "https://github.com/ankitects/linkcheck.git" rev = "184b2ca50ed39ca43da13f0b830a463861adb9ca" [workspace.dependencies.fsrs] -# version = "3.0.0" -git = "https://github.com/open-spaced-repetition/fsrs-rs.git" -rev = "33ec3ee4d5d73e704633469cf5bf1a42e620a524" +version = "4.1.1" +# git = "https://github.com/open-spaced-repetition/fsrs-rs.git" +# rev = "a7f7efc10f0a26b14ee348cc7402155685f2a24f" # path = "../open-spaced-repetition/fsrs-rs" [workspace.dependencies] @@ -54,99 +52,98 @@ ninja_gen = { "path" = "build/ninja_gen" } unicase = "=2.6.0" # any changes could invalidate sqlite indexes # normal -ammonia = "4.0.0" -anyhow = "1.0.90" -apple-bundles = "0.17.0" -async-compression = { version = "0.4.17", features = ["zstd", "tokio"] } +ammonia = "4.1.0" +anyhow = "1.0.98" +async-compression = { version = "0.4.24", features = ["zstd", "tokio"] } async-stream = "0.3.6" -async-trait = "0.1.83" -axum = { version = "0.7", features = ["multipart", "macros"] } -axum-client-ip = "0.6" -axum-extra = { version = "0.9.4", features = ["typed-header"] } -blake3 = "1.5.4" -bytes = "1.7.2" -camino = "1.1.9" -chrono = { version = "0.4.38", default-features = false, features = ["std", "clock"] } -clap = { version = "4.5.20", features = ["derive"] } -coarsetime = "0.1.34" -convert_case = "0.6.0" -criterion = { version = "0.5.1" } -csv = "1.3.0" -data-encoding = "2.6.0" +async-trait = "0.1.88" +axum = { version = "0.8.4", features = ["multipart", "macros"] } +axum-client-ip = "1.1.3" +axum-extra = { version = "0.10.1", features = ["typed-header"] } +blake3 = "1.8.2" +bytes = "1.10.1" +camino = "1.1.10" +chrono = { version = "0.4.41", default-features = false, features = ["std", "clock"] } +clap = { version = "4.5.40", features = ["derive"] } +coarsetime = "0.1.36" +convert_case = "0.8.0" +criterion = { version = "0.6.0" } +csv = "1.3.1" +data-encoding = "2.9.0" difflib = "0.4.0" -dirs = "5.0.1" +dirs = "6.0.0" dunce = "1.0.5" +embed-resource = "3.0.4" envy = "0.4.2" -flate2 = "1.0.34" -fluent = "0.16.1" -fluent-bundle = "0.15.3" -fluent-syntax = "0.11.1" +flate2 = "1.1.2" +fluent = "0.17.0" +fluent-bundle = "0.16.0" +fluent-syntax = "0.12.0" fnv = "1.0.7" futures = "0.3.31" -glob = "0.3.1" -globset = "0.4.15" +globset = "0.4.16" hex = "0.4.3" htmlescape = "0.3.1" hyper = "1" id_tree = "1.8.0" inflections = "1.1.1" -intl-memoizer = "0.5.2" -itertools = "0.13.0" +intl-memoizer = "0.5.3" +itertools = "0.14.0" junction = "1.2.0" -lazy_static = "1.5.0" +libc = "0.2" +libc-stdhandle = "0.1" maplit = "1.0.2" nom = "7.1.3" num-format = "0.4.4" -num_cpus = "1.16.0" +num_cpus = "1.17.0" num_enum = "0.7.3" -once_cell = "1.20.2" +once_cell = "1.21.3" pbkdf2 = { version = "0.12", features = ["simple"] } -phf = { version = "0.11.2", features = ["macros"] } -pin-project = "1.1.6" -plist = "1.7.0" -prettyplease = "0.2.24" +phf = { version = "0.11.3", features = ["macros"] } +pin-project = "1.1.10" +prettyplease = "0.2.34" prost = "0.13" prost-build = "0.13" -prost-reflect = "0.14" +prost-reflect = "0.14.7" prost-types = "0.13" -pulldown-cmark = "0.9.6" -pyo3 = { version = "0.24", features = ["extension-module", "abi3", "abi3-py39"] } -rand = "0.8.5" -regex = "1.11.0" -reqwest = { version = "0.12.8", default-features = false, features = ["json", "socks", "stream", "multipart"] } -rusqlite = { version = "0.30.0", features = ["trace", "functions", "collation", "bundled"] } +pulldown-cmark = "0.13.0" +pyo3 = { version = "0.25.1", features = ["extension-module", "abi3", "abi3-py39"] } +rand = "0.9.1" +regex = "1.11.1" +reqwest = { version = "0.12.20", default-features = false, features = ["json", "socks", "stream", "multipart"] } +rusqlite = { version = "0.36.0", features = ["trace", "functions", "collation", "bundled"] } rustls-pemfile = "2.2.0" scopeguard = "1.2.0" -serde = { version = "1.0.210", features = ["derive"] } -serde-aux = "4.5.0" -serde_json = "1.0.132" -serde_repr = "0.1.19" -serde_tuple = "0.5.0" +serde = { version = "1.0.219", features = ["derive"] } +serde-aux = "4.7.0" +serde_json = "1.0.140" +serde_repr = "0.1.20" +serde_tuple = "1.1.0" sha1 = "0.10.6" -sha2 = { version = "0.10.8" } -simple-file-manifest = "0.11.0" -snafu = { version = "0.8.5", features = ["rust_1_61"] } -strum = { version = "0.26.3", features = ["derive"] } -syn = { version = "2.0.82", features = ["parsing", "printing"] } -tar = "0.4.42" -tempfile = "3.13.0" +sha2 = { version = "0.10.9" } +snafu = { version = "0.8.6", features = ["rust_1_61"] } +strum = { version = "0.27.1", features = ["derive"] } +syn = { version = "2.0.103", features = ["parsing", "printing"] } +tar = "0.4.44" +tempfile = "3.20.0" termcolor = "1.4.1" -tokio = { version = "1.40", features = ["fs", "rt-multi-thread", "macros", "signal"] } -tokio-util = { version = "0.7.12", features = ["io"] } -tower-http = { version = "0.5", features = ["trace"] } -tracing = { version = "0.1.40", features = ["max_level_trace", "release_max_level_debug"] } +tokio = { version = "1.45", features = ["fs", "rt-multi-thread", "macros", "signal"] } +tokio-util = { version = "0.7.15", features = ["io"] } +tower-http = { version = "0.6.6", features = ["trace"] } +tracing = { version = "0.1.41", features = ["max_level_trace", "release_max_level_debug"] } tracing-appender = "0.2.3" -tracing-subscriber = { version = "0.3.18", features = ["fmt", "env-filter"] } -tugger-windows-codesign = "0.10.0" -unic-langid = { version = "0.9.5", features = ["macros"] } +tracing-subscriber = { version = "0.3.19", features = ["fmt", "env-filter"] } +unic-langid = { version = "0.9.6", features = ["macros"] } unic-ucd-category = "0.9.0" unicode-normalization = "0.1.24" walkdir = "2.5.0" -which = "5.0.0" -wiremock = "0.6.2" +which = "8.0.0" +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" -zip = { version = "0.6.6", default-features = false, features = ["deflate", "time"] } -zstd = { version = "0.13.2", features = ["zstdmt"] } +zip = { version = "4.1.0", default-features = false, features = ["deflate", "time"] } +zstd = { version = "0.13.3", features = ["zstdmt"] } # Apply mild optimizations to our dependencies in dev mode, which among other things # improves sha2 performance by about 21x. Opt 1 chosen due to diff --git a/build/configure/src/aqt.rs b/build/configure/src/aqt.rs index c3fb5ffac..35cd626a5 100644 --- a/build/configure/src/aqt.rs +++ b/build/configure/src/aqt.rs @@ -27,7 +27,6 @@ pub fn build_and_check_aqt(build: &mut Build) -> Result<()> { build_forms(build)?; build_generated_sources(build)?; build_data_folder(build)?; - build_macos_helper(build)?; build_wheel(build)?; check_python(build)?; Ok(()) @@ -39,7 +38,6 @@ fn build_forms(build: &mut Build) -> Result<()> { let mut py_files = vec![]; for path in ui_files.resolve() { let outpath = outdir.join(path.file_name().unwrap()).into_string(); - py_files.push(outpath.replace(".ui", "_qt5.py")); py_files.push(outpath.replace(".ui", "_qt6.py")); } build.add_action( @@ -332,47 +330,20 @@ impl BuildAction for BuildThemedIcon<'_> { } } -fn build_macos_helper(build: &mut Build) -> Result<()> { - if cfg!(target_os = "macos") { - build.add_action( - "qt:aqt:data:lib:libankihelper", - RunCommand { - command: ":pyenv:bin", - args: "$script $out $in", - inputs: hashmap! { - "script" => inputs!["qt/mac/helper_build.py"], - "in" => inputs![glob!["qt/mac/*.swift"]], - "" => inputs!["out/env"], - }, - outputs: hashmap! { - "out" => vec!["qt/_aqt/data/lib/libankihelper.dylib"], - }, - }, - )?; - } - Ok(()) -} - fn build_wheel(build: &mut Build) -> Result<()> { build.add_action( "wheels:aqt", BuildWheel { name: "aqt", version: anki_version(), - src_folder: "qt/aqt", - gen_folder: "$builddir/qt/_aqt", 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<()> { - python_format( - build, - "qt", - inputs![glob!("qt/**/*.py", "qt/bundle/PyOxidizer/**")], - )?; + python_format(build, "qt", inputs![glob!("qt/**/*.py")])?; build.add_action( "check:pytest:aqt", diff --git a/build/configure/src/bundle.rs b/build/configure/src/bundle.rs deleted file mode 100644 index 50fe7414c..000000000 --- a/build/configure/src/bundle.rs +++ /dev/null @@ -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::>()], - ); - 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 {}) -} diff --git a/build/configure/src/launcher.rs b/build/configure/src/launcher.rs new file mode 100644 index 000000000..4a1927289 --- /dev/null +++ b/build/configure/src/launcher.rs @@ -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", +}; diff --git a/build/configure/src/main.rs b/build/configure/src/main.rs index f88a32155..afd1cfb4a 100644 --- a/build/configure/src/main.rs +++ b/build/configure/src/main.rs @@ -2,7 +2,7 @@ // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html mod aqt; -mod bundle; +mod launcher; mod platform; mod pylib; mod python; @@ -13,13 +13,14 @@ use std::env; use anyhow::Result; use aqt::build_and_check_aqt; -use bundle::build_bundle; +use launcher::build_launcher; use ninja_gen::glob; use ninja_gen::inputs; use ninja_gen::protobuf::check_proto; use ninja_gen::protobuf::setup_protoc; -use ninja_gen::python::setup_python; +use ninja_gen::python::setup_uv; use ninja_gen::Build; +use platform::overriden_python_venv_platform; use pylib::build_pylib; use pylib::check_pylib; use python::check_python; @@ -47,7 +48,10 @@ fn main() -> Result<()> { check_proto(build, inputs![glob!["proto/**/*.proto"]])?; 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)?; @@ -57,7 +61,7 @@ fn main() -> Result<()> { build_and_check_aqt(build)?; if env::var("OFFLINE_BUILD").is_err() { - build_bundle(build)?; + build_launcher(build)?; } setup_sphinx(build)?; diff --git a/build/configure/src/platform.rs b/build/configure/src/platform.rs index 4aec36e65..ce8a7a5ba 100644 --- a/build/configure/src/platform.rs +++ b/build/configure/src/platform.rs @@ -5,18 +5,30 @@ use std::env; use ninja_gen::archives::Platform; -/// Usually None to use the host architecture; can be overriden by setting -/// MAC_X86 to build for x86_64 on Apple Silicon +/// Please see [`overriden_python_target_platform()`] for details. pub fn overriden_rust_target_triple() -> Option<&'static str> { - overriden_python_target_platform().map(|p| p.as_rust_triple()) + overriden_python_wheel_platform().map(|p| p.as_rust_triple()) } -/// Usually None to use the host architecture; can be overriden by setting -/// MAC_X86 to build for x86_64 on Apple Silicon -pub fn overriden_python_target_platform() -> Option { - if env::var("MAC_X86").is_ok() { - Some(Platform::MacX64) +/// Usually None to use the host architecture, except on Windows which +/// always uses x86_64, since WebEngine is unavailable for ARM64. +pub fn overriden_python_venv_platform() -> Option { + if cfg!(target_os = "windows") { + Some(Platform::WindowsX64) } else { None } } + +/// Like [`overriden_python_venv_platform`], but: +/// If MAC_X86 is set, an X86 wheel will be built on macOS ARM. +/// If LIN_ARM64 is set, an ARM64 wheel will be built on Linux AMD64. +pub fn overriden_python_wheel_platform() -> Option { + 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() + } +} diff --git a/build/configure/src/pylib.rs b/build/configure/src/pylib.rs index 7d269cbd2..bcef1ecc4 100644 --- a/build/configure/src/pylib.rs +++ b/build/configure/src/pylib.rs @@ -14,7 +14,7 @@ use ninja_gen::python::PythonTest; use ninja_gen::Build; use crate::anki_version; -use crate::platform::overriden_python_target_platform; +use crate::platform::overriden_python_wheel_platform; use crate::python::BuildWheel; use crate::python::GenPythonProto; @@ -50,7 +50,7 @@ pub fn build_pylib(build: &mut Build) -> Result<()> { output: &format!( "pylib/anki/_rsbridge.{}", match build.host_platform { - Platform::WindowsX64 => "pyd", + Platform::WindowsX64 | Platform::WindowsArm => "pyd", _ => "so", } ), @@ -64,13 +64,11 @@ pub fn build_pylib(build: &mut Build) -> Result<()> { BuildWheel { name: "anki", version: anki_version(), - src_folder: "pylib/anki", - gen_folder: "$builddir/pylib/anki", - platform: overriden_python_target_platform().or(Some(build.host_platform)), + platform: overriden_python_wheel_platform().or(Some(build.host_platform)), deps: inputs![ ":pylib:anki", glob!("pylib/anki/**"), - "python/requirements.anki.in", + "pylib/pyproject.toml" ], }, )?; diff --git a/build/configure/src/python.rs b/build/configure/src/python.rs index 17dddde16..474a55f31 100644 --- a/build/configure/src/python.rs +++ b/build/configure/src/python.rs @@ -20,74 +20,66 @@ use ninja_gen::python::PythonTypecheck; use ninja_gen::rsync::RsyncFiles; use ninja_gen::Build; -// When updating Qt, make sure to update the .txt file in bundle.rs as well. +/// Normalize version string by removing leading zeros from numeric parts +/// while preserving pre-release markers (b1, rc2, a3, etc.) +fn normalize_version(version: &str) -> String { + version + .split('.') + .map(|part| { + // Check if the part contains only digits + if part.chars().all(|c| c.is_ascii_digit()) { + // Numeric part: remove leading zeros + part.parse::().unwrap_or(0).to_string() + } else { + // Mixed part (contains both numbers and pre-release markers) + // Split on first non-digit character and normalize the numeric prefix + let chars = part.chars(); + let mut numeric_prefix = String::new(); + let mut rest = String::new(); + let mut found_non_digit = false; + + for ch in chars { + if ch.is_ascii_digit() && !found_non_digit { + numeric_prefix.push(ch); + } else { + found_non_digit = true; + rest.push(ch); + } + } + + if numeric_prefix.is_empty() { + part.to_string() + } else { + let normalized_prefix = numeric_prefix.parse::().unwrap_or(0).to_string(); + format!("{}{}", normalized_prefix, rest) + } + } + }) + .collect::>() + .join(".") +} + pub fn setup_venv(build: &mut Build) -> Result<()> { - let platform_deps = if cfg!(windows) { - inputs![ - "python/requirements.qt6_6.txt", - "python/requirements.win.txt", - ] - } else if cfg!(target_os = "macos") { - inputs!["python/requirements.qt6_6.txt",] - } 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]; + let extra_binary_exports = &[ + "mypy", + "black", + "isort", + "pylint", + "pytest", + "protoc-gen-mypy", + ]; build.add_action( "pyenv", PythonEnvironment { - folder: "pyenv", - base_requirements_txt: inputs!["python/requirements.base.txt"], - requirements_txt, - extra_binary_exports: &[ - "pip-compile", - "pip-sync", - "mypy", - "black", // Required for offline build - "isort", - "pylint", - "pytest", - "protoc-gen-mypy", // ditto + venv_folder: "pyenv", + deps: inputs![ + "pyproject.toml", + "pylib/pyproject.toml", + "qt/pyproject.toml", + "uv.lock" ], - }, - )?; - - // 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: &[], + extra_args: "--all-packages --extra qt --extra audio", + extra_binary_exports, }, )?; @@ -133,45 +125,59 @@ impl BuildAction for GenPythonProto { pub struct BuildWheel { pub name: &'static str, pub version: String, - pub src_folder: &'static str, - pub gen_folder: &'static str, pub platform: Option, pub deps: BuildInput, } impl BuildAction for BuildWheel { 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) { - build.add_inputs("pyenv_bin", inputs![":pyenv:bin"]); - build.add_inputs("script", inputs!["python/write_wheel.py"]); + build.add_inputs("uv", inputs![":uv_binary"]); 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 platform = match platform { - Platform::LinuxX64 => "manylinux_2_35_x86_64", - Platform::LinuxArm => "manylinux_2_35_aarch64", + let platform_tag = match platform { + Platform::LinuxX64 => "manylinux_2_36_x86_64", + Platform::LinuxArm => "manylinux_2_36_aarch64", Platform::MacX64 => "macosx_12_0_x86_64", Platform::MacArm => "macosx_12_0_arm64", Platform::WindowsX64 => "win_amd64", + Platform::WindowsArm => "win_arm64", }; - format!("cp39-abi3-{platform}") + format!("cp39-abi3-{platform_tag}") } else { "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 version = &self.version; - let wheel_path = format!("wheels/{name}-{version}-{tag}.whl"); + + let normalized_version = normalize_version(&self.version); + + let wheel_path = format!("wheels/{name}-{normalized_version}-{tag}.whl"); build.add_outputs("out", vec![wheel_path]); } } pub fn check_python(build: &mut Build) -> Result<()> { - python_format(build, "ftl", inputs![glob!("ftl/**/*.py")])?; python_format(build, "tools", inputs![glob!("tools/**/*.py")])?; build.add_action( @@ -183,7 +189,6 @@ pub fn check_python(build: &mut Build) -> Result<()> { "qt/tools", "out/pylib/anki", "out/qt/_aqt", - "ftl", "python", "tools", ], @@ -262,8 +267,7 @@ struct Sphinx { impl BuildAction for Sphinx { fn command(&self) -> &str { if env::var("OFFLINE_BUILD").is_err() { - "$pip install sphinx sphinx_rtd_theme sphinx-autoapi \ - && $python python/sphinx/build.py" + "$uv sync --extra sphinx && $python python/sphinx/build.py" } else { "$python python/sphinx/build.py" } @@ -271,7 +275,10 @@ impl BuildAction for Sphinx { fn files(&mut self, build: &mut impl FilesHandle) { 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("", &self.deps); @@ -294,8 +301,35 @@ pub(crate) fn setup_sphinx(build: &mut Build) -> Result<()> { build.add_action( "python:sphinx", Sphinx { - deps: inputs![":pylib", ":qt", ":python:sphinx:copy_conf"], + deps: inputs![ + ":pylib", + ":qt", + ":python:sphinx:copy_conf", + "pyproject.toml" + ], }, )?; Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_normalize_version_basic() { + assert_eq!(normalize_version("1.2.3"), "1.2.3"); + assert_eq!(normalize_version("01.02.03"), "1.2.3"); + assert_eq!(normalize_version("1.0.0"), "1.0.0"); + } + + #[test] + fn test_normalize_version_with_prerelease() { + assert_eq!(normalize_version("1.2.3b1"), "1.2.3b1"); + assert_eq!(normalize_version("01.02.03b1"), "1.2.3b1"); + assert_eq!(normalize_version("1.0.0rc2"), "1.0.0rc2"); + assert_eq!(normalize_version("2.1.0a3"), "2.1.0a3"); + assert_eq!(normalize_version("1.2.3beta1"), "1.2.3beta1"); + assert_eq!(normalize_version("1.2.3alpha1"), "1.2.3alpha1"); + } +} diff --git a/build/configure/src/rust.rs b/build/configure/src/rust.rs index f5da67086..1ff0fc97c 100644 --- a/build/configure/src/rust.rs +++ b/build/configure/src/rust.rs @@ -154,7 +154,7 @@ fn build_rsbridge(build: &mut Build) -> Result<()> { "$builddir/buildhash", // building on Windows requires python3.lib if cfg!(windows) { - inputs![":extract:python"] + inputs![":pyenv:bin"] } else { inputs![] } @@ -247,7 +247,7 @@ pub fn check_minilints(build: &mut Build) -> Result<()> { let files = inputs![ glob![ "**/*.{py,rs,ts,svelte,mjs,md}", - "{node_modules,qt/bundle/PyOxidizer,ts/.svelte-kit}/**" + "{node_modules,ts/.svelte-kit}/**" ], "Cargo.lock" ]; diff --git a/build/ninja_gen/Cargo.toml b/build/ninja_gen/Cargo.toml index 7757116c6..cacab6a7b 100644 --- a/build/ninja_gen/Cargo.toml +++ b/build/ninja_gen/Cargo.toml @@ -16,5 +16,22 @@ globset.workspace = true itertools.workspace = true maplit.workspace = true num_cpus.workspace = true +regex.workspace = true +serde_json.workspace = true +sha2.workspace = true walkdir.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" diff --git a/build/ninja_gen/src/archives.rs b/build/ninja_gen/src/archives.rs index 9dd784bdd..3f87d3ff5 100644 --- a/build/ninja_gen/src/archives.rs +++ b/build/ninja_gen/src/archives.rs @@ -26,22 +26,21 @@ pub enum Platform { MacX64, MacArm, WindowsX64, + WindowsArm, } impl Platform { pub fn current() -> Self { - if cfg!(windows) { - Self::WindowsX64 - } else { - let os = std::env::consts::OS; - let arch = std::env::consts::ARCH; - match (os, arch) { - ("linux", "x86_64") => Self::LinuxX64, - ("linux", "aarch64") => Self::LinuxArm, - ("macos", "x86_64") => Self::MacX64, - ("macos", "aarch64") => Self::MacArm, - _ => panic!("unsupported os/arch {os} {arch} - PR welcome!"), - } + let os = std::env::consts::OS; + let arch = std::env::consts::ARCH; + match (os, arch) { + ("linux", "x86_64") => Self::LinuxX64, + ("linux", "aarch64") => Self::LinuxArm, + ("macos", "x86_64") => Self::MacX64, + ("macos", "aarch64") => Self::MacArm, + ("windows", "x86_64") => Self::WindowsX64, + ("windows", "aarch64") => Self::WindowsArm, + _ => panic!("unsupported os/arch {os} {arch} - PR welcome!"), } } @@ -62,6 +61,7 @@ impl Platform { Platform::MacX64 => "x86_64-apple-darwin", Platform::MacArm => "aarch64-apple-darwin", Platform::WindowsX64 => "x86_64-pc-windows-msvc", + Platform::WindowsArm => "aarch64-pc-windows-msvc", } } } diff --git a/build/ninja_gen/src/bin/update_protoc.rs b/build/ninja_gen/src/bin/update_protoc.rs new file mode 100644 index 000000000..224dbaa50 --- /dev/null +++ b/build/ninja_gen/src/bin/update_protoc.rs @@ -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> { + 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> { + 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> { + 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> { + 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> { + 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(()) +} diff --git a/build/ninja_gen/src/bin/update_uv.rs b/build/ninja_gen/src/bin/update_uv.rs new file mode 100644 index 000000000..39cf87668 --- /dev/null +++ b/build/ninja_gen/src/bin/update_uv.rs @@ -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> { + 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: " " + 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> { + 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> { + 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> { + 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> { + 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 + ); + } +} diff --git a/build/ninja_gen/src/cargo.rs b/build/ninja_gen/src/cargo.rs index 645203170..2a3397704 100644 --- a/build/ninja_gen/src/cargo.rs +++ b/build/ninja_gen/src/cargo.rs @@ -162,7 +162,7 @@ impl BuildAction for CargoTest { "cargo-nextest", CargoInstall { binary_name: "cargo-nextest", - args: "cargo-nextest --version 0.9.57 --locked", + args: "cargo-nextest --version 0.9.99 --locked --no-default-features --features default-no-update", }, )?; setup_flags(build) diff --git a/build/ninja_gen/src/node.rs b/build/ninja_gen/src/node.rs index dac056c10..10b3e6184 100644 --- a/build/ninja_gen/src/node.rs +++ b/build/ninja_gen/src/node.rs @@ -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", sha256: "893115cd92ad27bf178802f15247115e93c0ef0c753b93dca96439240d64feb5", }, + Platform::WindowsArm => OnlineArchive { + url: "https://nodejs.org/dist/v20.11.0/node-v20.11.0-win-arm64.zip", + sha256: "89c1f7034dcd6ff5c17f2af61232a96162a1902f862078347dcf274a938b6142", + }, } } diff --git a/build/ninja_gen/src/protobuf.rs b/build/ninja_gen/src/protobuf.rs index 2643c2ab1..1198f653d 100644 --- a/build/ninja_gen/src/protobuf.rs +++ b/build/ninja_gen/src/protobuf.rs @@ -21,26 +21,26 @@ pub fn protoc_archive(platform: Platform) -> OnlineArchive { match platform { Platform::LinuxX64 => { OnlineArchive { - url: "https://github.com/protocolbuffers/protobuf/releases/download/v21.8/protoc-21.8-linux-x86_64.zip", - sha256: "f90d0dd59065fef94374745627336d622702b67f0319f96cee894d41a974d47a", + url: "https://github.com/protocolbuffers/protobuf/releases/download/v31.1/protoc-31.1-linux-x86_64.zip", + sha256: "96553041f1a91ea0efee963cb16f462f5985b4d65365f3907414c360044d8065", } - } + }, Platform::LinuxArm => { OnlineArchive { - url: "https://github.com/protocolbuffers/protobuf/releases/download/v21.8/protoc-21.8-linux-aarch_64.zip", - sha256: "f3d8eb5839d6186392d8c7b54fbeabbb6fcdd90618a500b77cb2e24faa245cad", + url: "https://github.com/protocolbuffers/protobuf/releases/download/v31.1/protoc-31.1-linux-aarch_64.zip", + sha256: "6c554de11cea04c56ebf8e45b54434019b1cd85223d4bbd25c282425e306ecc2", } - } + }, Platform::MacX64 | Platform::MacArm => { OnlineArchive { - url: "https://github.com/protocolbuffers/protobuf/releases/download/v21.8/protoc-21.8-osx-universal_binary.zip", - sha256: "e3324d3bc2e9bc967a0bec2472e0ec73b26f952c7c87f2403197414f780c3c6c", + url: "https://github.com/protocolbuffers/protobuf/releases/download/v31.1/protoc-31.1-osx-universal_binary.zip", + sha256: "99ea004549c139f46da5638187a85bbe422d78939be0fa01af1aa8ab672e395f", } - } - Platform::WindowsX64 => { + }, + Platform::WindowsX64 | Platform::WindowsArm => { OnlineArchive { - url: "https://github.com/protocolbuffers/protobuf/releases/download/v21.8/protoc-21.8-win64.zip", - sha256: "3657053024faa439ff5f8c1dd2ee06bac0f9b9a3d660e99944f015a7451e87ec", + url: "https://github.com/protocolbuffers/protobuf/releases/download/v31.1/protoc-31.1-win64.zip", + sha256: "70381b116ab0d71cb6a5177d9b17c7c13415866603a0fd40d513dafe32d56c35", } } } @@ -67,7 +67,7 @@ fn clang_format_archive(platform: Platform) -> OnlineArchive { sha256: "238be68d9478163a945754f06a213483473044f5a004c4125d3d9d8d3556466e", } } - Platform::WindowsX64 => { + Platform::WindowsX64 | Platform::WindowsArm=> { OnlineArchive { url: "https://github.com/ankitects/clang-format-binaries/releases/download/anki-2021-01-09/clang-format_windows_x86_64.zip", sha256: "7d9f6915e3f0fb72407830f0fc37141308d2e6915daba72987a52f309fbeaccc", diff --git a/build/ninja_gen/src/python.rs b/build/ninja_gen/src/python.rs index 3a8931697..7ac65e85f 100644 --- a/build/ninja_gen/src/python.rs +++ b/build/ninja_gen/src/python.rs @@ -9,6 +9,7 @@ use maplit::hashmap; use crate::action::BuildAction; use crate::archives::download_and_extract; +use crate::archives::with_exe; use crate::archives::OnlineArchive; use crate::archives::Platform; use crate::hash::simple_hash; @@ -16,82 +17,113 @@ use crate::input::BuildInput; use crate::inputs; use crate::Build; -/// When updating this, pyoxidizer.bzl needs updating too, but it uses different -/// files. -pub fn python_archive(platform: Platform) -> OnlineArchive { +// To update, run 'cargo run --bin update_uv'. +// You'll need to do this when bumping Python versions, as uv bakes in +// 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 { Platform::LinuxX64 => { 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", - sha256: "9426bca501ae0a257392b10719e2e20ff5fa5e22a3ce4599d6ad0b3139f86417", + url: "https://github.com/astral-sh/uv/releases/download/0.7.13/uv-x86_64-unknown-linux-gnu.tar.gz", + sha256: "909278eb197c5ed0e9b5f16317d1255270d1f9ea4196e7179ce934d48c4c2545", } - } + }, Platform::LinuxArm => { 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", - sha256: "7d19e1ecd6e582423f7c74a0c67491eaa982ce9d5c5f35f0e4289f83127abcb8", + url: "https://github.com/astral-sh/uv/releases/download/0.7.13/uv-aarch64-unknown-linux-gnu.tar.gz", + sha256: "0b2ad9fe4295881615295add8cc5daa02549d29cc9a61f0578e397efcf12f08f", } - } + }, Platform::MacX64 => { 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", - sha256: "5a0bf895a5cb08d6d008140abb41bb2c8cd638a665273f7d8eb258bc89de439b", + url: "https://github.com/astral-sh/uv/releases/download/0.7.13/uv-x86_64-apple-darwin.tar.gz", + sha256: "d785753ac092e25316180626aa691c5dfe1fb075290457ba4fdb72c7c5661321", } - } + }, Platform::MacArm => { OnlineArchive { - url: "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.9.18+20240107-aarch64-apple-darwin-install_only.tar.gz", - sha256: "bf0cd90204a2cc6da48cae1e4b32f48c9f7031fbe1238c5972104ccb0155d368", + url: "https://github.com/astral-sh/uv/releases/download/0.7.13/uv-aarch64-apple-darwin.tar.gz", + sha256: "721f532b73171586574298d4311a91d5ea2c802ef4db3ebafc434239330090c6", } - } + }, Platform::WindowsX64 => { 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", - sha256: "8f0544cd593984f7ecb90c685931249c579302124b9821064873f3a14ed07005", + url: "https://github.com/astral-sh/uv/releases/download/0.7.13/uv-x86_64-pc-windows-msvc.zip", + 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. -/// Downloads if missing. -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") { +pub fn setup_uv(build: &mut Build, platform: Platform) -> Result<()> { + let uv_binary = match env::var("UV_BINARY") { Ok(path) => { assert!( Utf8Path::new(&path).is_absolute(), - "PYTHON_BINARY must be absolute" + "UV_BINARY must be absolute" ); path.into() } Err(_) => { download_and_extract( build, - "python", - python_archive(build.host_platform), + "uv", + uv_archive(platform), 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(()) } pub struct PythonEnvironment { - pub folder: &'static str, - pub base_requirements_txt: BuildInput, - pub requirements_txt: BuildInput, + pub deps: BuildInput, + // todo: rename + pub venv_folder: &'static str, + pub extra_args: &'static str, pub extra_binary_exports: &'static [&'static str], } impl BuildAction for PythonEnvironment { fn command(&self) -> &str { 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 { "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) { let bin_path = |binary: &str| -> Vec { - let folder = self.folder; + let folder = self.venv_folder; let path = if cfg!(windows) { format!("{folder}/scripts/{binary}.exe") } else { @@ -108,21 +140,24 @@ impl BuildAction for PythonEnvironment { vec![path] }; + build.add_inputs("", &self.deps); + build.add_variable("pyenv_folder", self.venv_folder); if env::var("OFFLINE_BUILD").is_err() { - build.add_inputs("python_binary", inputs![":python_binary"]); - build.add_variable("pyenv_folder", self.folder); - build.add_inputs("base_requirements", &self.base_requirements_txt); - build.add_inputs("requirements", &self.requirements_txt); - build.add_outputs_ext("pip", bin_path("pip"), true); + build.add_inputs("uv_binary", inputs![":uv_binary"]); + + // Add --python flag to extra_args if PYTHON_BINARY is set + let mut args = self.extra_args.to_string(); + 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); for binary in self.extra_binary_exports { build.add_outputs_ext(*binary, bin_path(binary), true); } - } - - fn check_output_timestamps(&self) -> bool { - true + build.add_output_stamp(format!("{}/.stamp", self.venv_folder)); } } diff --git a/build/runner/Cargo.toml b/build/runner/Cargo.toml index 54722f01d..1fffe7050 100644 --- a/build/runner/Cargo.toml +++ b/build/runner/Cargo.toml @@ -15,7 +15,6 @@ camino.workspace = true clap.workspace = true flate2.workspace = true junction.workspace = true -reqwest = { workspace = true, features = ["rustls-tls", "rustls-tls-native-roots"] } sha2.workspace = true tar.workspace = true termcolor.workspace = true @@ -24,3 +23,9 @@ which.workspace = true xz2.workspace = true zip.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"] } diff --git a/build/runner/src/bundle/artifacts.rs b/build/runner/src/bundle/artifacts.rs deleted file mode 100644 index ec5506717..000000000 --- a/build/runner/src/bundle/artifacts.rs +++ /dev/null @@ -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" - } -} diff --git a/build/runner/src/bundle/binary.rs b/build/runner/src/bundle/binary.rs deleted file mode 100644 index e9119220a..000000000 --- a/build/runner/src/bundle/binary.rs +++ /dev/null @@ -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); -} diff --git a/build/runner/src/bundle/folder.rs b/build/runner/src/bundle/folder.rs deleted file mode 100644 index cdbfd21e8..000000000 --- a/build/runner/src/bundle/folder.rs +++ /dev/null @@ -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

(path: P) -> String -where - P: AsRef, -{ - 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"))), - ])); -} diff --git a/build/runner/src/bundle/mod.rs b/build/runner/src/bundle/mod.rs deleted file mode 100644 index 30a3608ab..000000000 --- a/build/runner/src/bundle/mod.rs +++ /dev/null @@ -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; diff --git a/build/runner/src/main.rs b/build/runner/src/main.rs index 8fdf8f06f..41cc1fa2e 100644 --- a/build/runner/src/main.rs +++ b/build/runner/src/main.rs @@ -7,7 +7,6 @@ mod archive; mod build; -mod bundle; mod paths; mod pyenv; mod rsync; @@ -19,11 +18,6 @@ use archive::archive_command; use archive::ArchiveArgs; use build::run_build; 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::Subcommand; use pyenv::setup_pyenv; @@ -48,9 +42,6 @@ enum Command { Rsync(RsyncArgs), Run(RunArgs), Build(BuildArgs), - BuildArtifacts(BuildArtifactsArgs), - BuildBundleBinary, - BuildDistFolder(BuildDistFolderArgs), #[clap(subcommand)] Archive(ArchiveArgs), } @@ -62,9 +53,6 @@ fn main() -> Result<()> { Command::Rsync(args) => rsync_files(args), Command::Yarn(args) => setup_yarn(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)?, }; Ok(()) diff --git a/build/runner/src/paths.rs b/build/runner/src/paths.rs index 2021120cb..c28dde1b9 100644 --- a/build/runner/src/paths.rs +++ b/build/runner/src/paths.rs @@ -16,8 +16,3 @@ pub fn absolute_msys_path(path: &Utf8Path) -> String { // and \ -> / format!("/{drive}/{}", path[7..].replace('\\', "/")) } - -/// Converts backslashes to forward slashes -pub fn unix_path(path: &Utf8Path) -> String { - path.as_str().replace('\\', "/") -} diff --git a/build/runner/src/pyenv.rs b/build/runner/src/pyenv.rs index 82b3b49ed..0bd5ec662 100644 --- a/build/runner/src/pyenv.rs +++ b/build/runner/src/pyenv.rs @@ -1,6 +1,7 @@ // 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 camino::Utf8Path; @@ -10,12 +11,10 @@ use crate::run::run_command; #[derive(Args)] pub struct PyenvArgs { - python_bin: String, + uv_bin: String, pyenv_folder: String, - initial_reqs: String, - reqs: Vec, - #[arg(long, allow_hyphen_values(true))] - venv_args: Vec, + #[arg(trailing_var_arg = true)] + extra_args: Vec, } /// Set up a venv if one doesn't already exist, and then sync packages with @@ -23,35 +22,23 @@ pub struct PyenvArgs { pub fn setup_pyenv(args: PyenvArgs) { let pyenv_folder = Utf8Path::new(&args.pyenv_folder); - let pyenv_bin_folder = pyenv_folder.join(if cfg!(windows) { "scripts" } else { "bin" }); - let pyenv_python = pyenv_bin_folder.join("python"); - let pip_sync = pyenv_bin_folder.join("pip-sync"); - - 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(); + // On first run, ninja creates an empty bin/ folder which breaks the initial + // install. But we don't want to indiscriminately remove the folder, or + // macOS Gatekeeper needs to rescan the files each time. + if pyenv_folder.exists() { + let cache_tag = pyenv_folder.join("CACHEDIR.TAG"); + if !cache_tag.exists() { + fs::remove_dir_all(pyenv_folder).expect("Failed to remove existing pyenv folder"); } - - 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"); } diff --git a/build/runner/src/run.rs b/build/runner/src/run.rs index 5b60ab80c..bff88eb97 100644 --- a/build/runner/src/run.rs +++ b/build/runner/src/run.rs @@ -1,7 +1,6 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -use std::io::ErrorKind; use std::process::Command; 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('=') { Ok((k.into(), v.into())) } else { - Err(std::io::Error::new(ErrorKind::Other, "invalid env var")) + Err(std::io::Error::other("invalid env var")) } } diff --git a/cargo/licenses.json b/cargo/licenses.json index 4854cb085..c16b20aa7 100644 --- a/cargo/licenses.json +++ b/cargo/licenses.json @@ -10,7 +10,7 @@ }, { "name": "adler2", - "version": "2.0.0", + "version": "2.0.1", "authors": "Jonas Schievink |oyvindln ", "repository": "https://github.com/oyvindln/adler2", "license": "0BSD OR Apache-2.0 OR MIT", @@ -19,7 +19,7 @@ }, { "name": "ahash", - "version": "0.8.11", + "version": "0.8.12", "authors": "Tom Kaitchuck ", "repository": "https://github.com/tkaitchuck/ahash", "license": "Apache-2.0 OR MIT", @@ -181,7 +181,7 @@ }, { "name": "async-compression", - "version": "0.4.23", + "version": "0.4.24", "authors": "Wim Looman |Allen Bui ", "repository": "https://github.com/Nullus157/async-compression", "license": "Apache-2.0 OR MIT", @@ -244,7 +244,7 @@ }, { "name": "axum", - "version": "0.7.9", + "version": "0.8.4", "authors": null, "repository": "https://github.com/tokio-rs/axum", "license": "MIT", @@ -253,7 +253,7 @@ }, { "name": "axum-client-ip", - "version": "0.6.1", + "version": "1.1.3", "authors": null, "repository": "https://github.com/imbolc/axum-client-ip", "license": "MIT", @@ -262,7 +262,7 @@ }, { "name": "axum-core", - "version": "0.4.5", + "version": "0.5.2", "authors": null, "repository": "https://github.com/tokio-rs/axum", "license": "MIT", @@ -271,7 +271,7 @@ }, { "name": "axum-extra", - "version": "0.9.6", + "version": "0.10.1", "authors": null, "repository": "https://github.com/tokio-rs/axum", "license": "MIT", @@ -280,7 +280,7 @@ }, { "name": "axum-macros", - "version": "0.4.2", + "version": "0.5.0", "authors": null, "repository": "https://github.com/tokio-rs/axum", "license": "MIT", @@ -289,22 +289,13 @@ }, { "name": "backtrace", - "version": "0.3.74", + "version": "0.3.75", "authors": "The Rust Project Developers", "repository": "https://github.com/rust-lang/backtrace-rs", "license": "Apache-2.0 OR MIT", "license_file": null, "description": "A library to acquire a stack trace (backtrace) at runtime in a Rust program." }, - { - "name": "base64", - "version": "0.21.7", - "authors": "Alice Maz |Marshall Pierce ", - "repository": "https://github.com/marshallpierce/rust-base64", - "license": "Apache-2.0 OR MIT", - "license_file": null, - "description": "encodes and decodes base64 as bytes or utf8" - }, { "name": "base64", "version": "0.22.1", @@ -316,7 +307,7 @@ }, { "name": "base64ct", - "version": "1.7.3", + "version": "1.8.0", "authors": "RustCrypto Developers", "repository": "https://github.com/RustCrypto/formats", "license": "Apache-2.0 OR MIT", @@ -361,7 +352,7 @@ }, { "name": "bitflags", - "version": "2.9.0", + "version": "2.9.1", "authors": "The Rust Project Developers", "repository": "https://github.com/bitflags/bitflags", "license": "Apache-2.0 OR MIT", @@ -397,7 +388,7 @@ }, { "name": "bumpalo", - "version": "3.17.0", + "version": "3.18.1", "authors": "Nick Fitzgerald ", "repository": "https://github.com/fitzgen/bumpalo", "license": "Apache-2.0 OR MIT", @@ -406,7 +397,7 @@ }, { "name": "burn", - "version": "0.17.0", + "version": "0.17.1", "authors": "nathanielsimard ", "repository": "https://github.com/tracel-ai/burn", "license": "Apache-2.0 OR MIT", @@ -415,7 +406,7 @@ }, { "name": "burn-autodiff", - "version": "0.17.0", + "version": "0.17.1", "authors": "nathanielsimard ", "repository": "https://github.com/tracel-ai/burn/tree/main/crates/burn-autodiff", "license": "Apache-2.0 OR MIT", @@ -424,7 +415,7 @@ }, { "name": "burn-candle", - "version": "0.17.0", + "version": "0.17.1", "authors": "louisfd ", "repository": "https://github.com/tracel-ai/burn/tree/main/crates/burn-candle", "license": "Apache-2.0 OR MIT", @@ -433,7 +424,7 @@ }, { "name": "burn-common", - "version": "0.17.0", + "version": "0.17.1", "authors": "Dilshod Tadjibaev (@antimora)", "repository": "https://github.com/tracel-ai/burn/tree/main/crates/burn-common", "license": "Apache-2.0 OR MIT", @@ -442,7 +433,7 @@ }, { "name": "burn-core", - "version": "0.17.0", + "version": "0.17.1", "authors": "nathanielsimard ", "repository": "https://github.com/tracel-ai/burn/tree/main/crates/burn-core", "license": "Apache-2.0 OR MIT", @@ -451,7 +442,7 @@ }, { "name": "burn-cubecl", - "version": "0.17.0", + "version": "0.17.1", "authors": "nathanielsimard ", "repository": "https://github.com/tracel-ai/burn/tree/main/crates/burn-cubecl", "license": "Apache-2.0 OR MIT", @@ -460,7 +451,7 @@ }, { "name": "burn-cuda", - "version": "0.17.0", + "version": "0.17.1", "authors": "nathanielsimard ", "repository": "https://github.com/tracel-ai/burn/tree/main/crates/burn-cuda", "license": "Apache-2.0 OR MIT", @@ -469,7 +460,7 @@ }, { "name": "burn-dataset", - "version": "0.17.0", + "version": "0.17.1", "authors": "nathanielsimard ", "repository": "https://github.com/tracel-ai/burn/tree/main/crates/burn-dataset", "license": "Apache-2.0 OR MIT", @@ -478,7 +469,7 @@ }, { "name": "burn-derive", - "version": "0.17.0", + "version": "0.17.1", "authors": "nathanielsimard ", "repository": "https://github.com/tracel-ai/burn/tree/main/crates/burn-derive", "license": "Apache-2.0 OR MIT", @@ -487,7 +478,7 @@ }, { "name": "burn-ir", - "version": "0.17.0", + "version": "0.17.1", "authors": "laggui |nathanielsimard ", "repository": "https://github.com/tracel-ai/burn/tree/main/crates/burn-ir", "license": "Apache-2.0 OR MIT", @@ -496,7 +487,7 @@ }, { "name": "burn-ndarray", - "version": "0.17.0", + "version": "0.17.1", "authors": "nathanielsimard ", "repository": "https://github.com/tracel-ai/burn/tree/main/crates/burn-ndarray", "license": "Apache-2.0 OR MIT", @@ -505,7 +496,7 @@ }, { "name": "burn-rocm", - "version": "0.17.0", + "version": "0.17.1", "authors": "nathanielsimard ", "repository": "https://github.com/tracel-ai/burn/tree/main/crates/burn-rocm", "license": "Apache-2.0 OR MIT", @@ -514,7 +505,7 @@ }, { "name": "burn-router", - "version": "0.17.0", + "version": "0.17.1", "authors": "laggui |nathanielsimard ", "repository": "https://github.com/tracel-ai/burn/tree/main/crates/burn-router", "license": "Apache-2.0 OR MIT", @@ -523,7 +514,7 @@ }, { "name": "burn-tensor", - "version": "0.17.0", + "version": "0.17.1", "authors": "nathanielsimard ", "repository": "https://github.com/tracel-ai/burn/tree/main/crates/burn-tensor", "license": "Apache-2.0 OR MIT", @@ -532,7 +523,7 @@ }, { "name": "burn-train", - "version": "0.17.0", + "version": "0.17.1", "authors": "nathanielsimard ", "repository": "https://github.com/tracel-ai/burn/tree/main/crates/burn-train", "license": "Apache-2.0 OR MIT", @@ -541,7 +532,7 @@ }, { "name": "burn-wgpu", - "version": "0.17.0", + "version": "0.17.1", "authors": "nathanielsimard ", "repository": "https://github.com/tracel-ai/burn/tree/main/crates/burn-wgpu", "license": "Apache-2.0 OR MIT", @@ -550,7 +541,7 @@ }, { "name": "bytemuck", - "version": "1.22.0", + "version": "1.23.1", "authors": "Lokathor ", "repository": "https://github.com/Lokathor/bytemuck", "license": "Apache-2.0 OR MIT OR Zlib", @@ -595,7 +586,7 @@ }, { "name": "camino", - "version": "1.1.9", + "version": "1.1.10", "authors": "Without Boats |Ashley Williams |Steve Klabnik |Rain ", "repository": "https://github.com/camino-rs/camino", "license": "Apache-2.0 OR MIT", @@ -613,7 +604,7 @@ }, { "name": "cc", - "version": "1.2.20", + "version": "1.2.27", "authors": "Alex Crichton ", "repository": "https://github.com/rust-lang/cc-rs", "license": "Apache-2.0 OR MIT", @@ -622,9 +613,9 @@ }, { "name": "cfg-if", - "version": "1.0.0", + "version": "1.0.1", "authors": "Alex Crichton ", - "repository": "https://github.com/alexcrichton/cfg-if", + "repository": "https://github.com/rust-lang/cfg-if", "license": "Apache-2.0 OR MIT", "license_file": null, "description": "A macro to ergonomically define an item depending on a large number of #[cfg] parameters. Structured like an if-else chain, the first matching branch is the item that gets emitted." @@ -640,13 +631,22 @@ }, { "name": "chrono", - "version": "0.4.40", + "version": "0.4.41", "authors": null, "repository": "https://github.com/chronotope/chrono", "license": "Apache-2.0 OR MIT", "license_file": null, "description": "Date and time library for Rust" }, + { + "name": "client-ip", + "version": "0.1.1", + "authors": null, + "repository": "https://github.com/imbolc/client-ip", + "license": "MIT", + "license_file": null, + "description": "HTTP client IP address extractors" + }, { "name": "coarsetime", "version": "0.1.36", @@ -694,8 +694,8 @@ }, { "name": "convert_case", - "version": "0.6.0", - "authors": "Rutrum ", + "version": "0.8.0", + "authors": "rutrum ", "repository": "https://github.com/rutrum/convert-case", "license": "MIT", "license_file": null, @@ -712,7 +712,7 @@ }, { "name": "core-foundation", - "version": "0.10.0", + "version": "0.10.1", "authors": "The Servo Project Developers", "repository": "https://github.com/servo/core-foundation-rs", "license": "Apache-2.0 OR MIT", @@ -901,7 +901,7 @@ }, { "name": "cubecl-hip-sys", - "version": "6.4.0", + "version": "6.4.4348200", "authors": "Tracel Technologies Inc.", "repository": "https://github.com/tracel-ai/cubecl-hip/tree/main/crates/cubecl-hip-sys", "license": "Apache-2.0 OR MIT", @@ -1261,7 +1261,7 @@ }, { "name": "errno", - "version": "0.3.11", + "version": "0.3.12", "authors": "Chris Wong |Dan Gohman ", "repository": "https://github.com/lambda-fairy/rust-errno", "license": "Apache-2.0 OR MIT", @@ -1324,7 +1324,7 @@ }, { "name": "flate2", - "version": "1.1.1", + "version": "1.1.2", "authors": "Alex Crichton |Josh Triplett ", "repository": "https://github.com/rust-lang/flate2-rs", "license": "Apache-2.0 OR MIT", @@ -1342,21 +1342,21 @@ }, { "name": "fluent", - "version": "0.16.1", - "authors": "Zibi Braniecki |Staś Małolepszy ", + "version": "0.17.0", + "authors": "Caleb Maclennan |Bruce Mitchener |Staś Małolepszy ", "repository": "https://github.com/projectfluent/fluent-rs", "license": "Apache-2.0 OR MIT", "license_file": null, - "description": "A localization system designed to unleash the entire expressive power of natural language translations." + "description": "An umbrella crate exposing the combined features of fluent-rs crates with additional convenience macros for Project Fluent, a localization system designed to unleash the entire expressive power of natural language translations." }, { "name": "fluent-bundle", - "version": "0.15.3", - "authors": "Zibi Braniecki |Staś Małolepszy ", + "version": "0.16.0", + "authors": "Caleb Maclennan |Bruce Mitchener |Staś Małolepszy ", "repository": "https://github.com/projectfluent/fluent-rs", "license": "Apache-2.0 OR MIT", "license_file": null, - "description": "A localization system designed to unleash the entire expressive power of natural language translations." + "description": "A low-level implementation of a collection of localization messages for a single locale for Project Fluent, a localization system designed to unleash the entire expressive power of natural language translations." }, { "name": "fluent-langneg", @@ -1369,12 +1369,12 @@ }, { "name": "fluent-syntax", - "version": "0.11.1", - "authors": "Zibi Braniecki |Staś Małolepszy ", + "version": "0.12.0", + "authors": "Caleb Maclennan |Bruce Mitchener |Staś Małolepszy ", "repository": "https://github.com/projectfluent/fluent-rs", "license": "Apache-2.0 OR MIT", "license_file": null, - "description": "Parser/Serializer tools for Fluent Syntax." + "description": "A low-level parser, AST, and serializer API for the syntax used by Project Fluent, a localization system designed to unleash the entire expressive power of natural language translations." }, { "name": "fnv", @@ -1448,18 +1448,9 @@ "license_file": null, "description": "Parser and serializer for the application/x-www-form-urlencoded syntax, as used by HTML forms." }, - { - "name": "forwarded-header-value", - "version": "0.1.1", - "authors": "James Brown ", - "repository": "https://github.com/EasyPost/rust-forwarded-header-value", - "license": "ISC", - "license_file": null, - "description": "Parser for values from the Forwarded header (RFC 7239)" - }, { "name": "fsrs", - "version": "4.0.0", + "version": "4.1.1", "authors": "Open Spaced Repetition", "repository": "https://github.com/open-spaced-repetition/fsrs-rs", "license": "BSD-3-Clause", @@ -1711,12 +1702,12 @@ }, { "name": "getopts", - "version": "0.2.21", + "version": "0.2.23", "authors": "The Rust Project Developers", "repository": "https://github.com/rust-lang/getopts", "license": "Apache-2.0 OR MIT", "license_file": null, - "description": "getopts-like option parsing." + "description": "getopts-like option parsing" }, { "name": "getrandom", @@ -1729,7 +1720,7 @@ }, { "name": "getrandom", - "version": "0.3.2", + "version": "0.3.3", "authors": "The Rand Project Developers", "repository": "https://github.com/rust-random/getrandom", "license": "Apache-2.0 OR MIT", @@ -1810,7 +1801,7 @@ }, { "name": "gpu-descriptor", - "version": "0.3.1", + "version": "0.3.2", "authors": "Zakarum ", "repository": "https://github.com/zakarumych/gpu-descriptor", "license": "Apache-2.0 OR MIT", @@ -1828,7 +1819,7 @@ }, { "name": "h2", - "version": "0.4.9", + "version": "0.4.10", "authors": "Carl Lerche |Sean McArthur ", "repository": "https://github.com/hyperium/h2", "license": "MIT", @@ -1864,7 +1855,7 @@ }, { "name": "hashbrown", - "version": "0.15.2", + "version": "0.15.4", "authors": "Amanieu d'Antras ", "repository": "https://github.com/rust-lang/hashbrown", "license": "Apache-2.0 OR MIT", @@ -1873,7 +1864,7 @@ }, { "name": "hashlink", - "version": "0.8.4", + "version": "0.10.0", "authors": "kyren ", "repository": "https://github.com/kyren/hashlink", "license": "Apache-2.0 OR MIT", @@ -1882,7 +1873,7 @@ }, { "name": "headers", - "version": "0.4.0", + "version": "0.4.1", "authors": "Sean McArthur ", "repository": "https://github.com/hyperium/headers", "license": "MIT", @@ -1909,7 +1900,7 @@ }, { "name": "hermit-abi", - "version": "0.3.9", + "version": "0.5.2", "authors": "Stefan Lankes", "repository": "https://github.com/hermit-os/hermit-rs", "license": "Apache-2.0 OR MIT", @@ -2017,7 +2008,7 @@ }, { "name": "hyper-rustls", - "version": "0.27.5", + "version": "0.27.7", "authors": null, "repository": "https://github.com/rustls/hyper-rustls", "license": "Apache-2.0 OR ISC OR MIT", @@ -2035,7 +2026,7 @@ }, { "name": "hyper-util", - "version": "0.1.11", + "version": "0.1.14", "authors": "Sean McArthur ", "repository": "https://github.com/hyperium/hyper-util", "license": "MIT", @@ -2062,7 +2053,7 @@ }, { "name": "icu_collections", - "version": "1.5.0", + "version": "2.0.0", "authors": "The ICU4X Project Developers", "repository": "https://github.com/unicode-org/icu4x", "license": "Unicode-3.0", @@ -2070,35 +2061,17 @@ "description": "Collection of API for use in ICU libraries." }, { - "name": "icu_locid", - "version": "1.5.0", + "name": "icu_locale_core", + "version": "2.0.0", "authors": "The ICU4X Project Developers", "repository": "https://github.com/unicode-org/icu4x", "license": "Unicode-3.0", "license_file": null, "description": "API for managing Unicode Language and Locale Identifiers" }, - { - "name": "icu_locid_transform", - "version": "1.5.0", - "authors": "The ICU4X Project Developers", - "repository": "https://github.com/unicode-org/icu4x", - "license": "Unicode-3.0", - "license_file": null, - "description": "API for Unicode Language and Locale Identifiers canonicalization" - }, - { - "name": "icu_locid_transform_data", - "version": "1.5.1", - "authors": "The ICU4X Project Developers", - "repository": "https://github.com/unicode-org/icu4x", - "license": "Unicode-3.0", - "license_file": null, - "description": "Data for the icu_locid_transform crate" - }, { "name": "icu_normalizer", - "version": "1.5.0", + "version": "2.0.0", "authors": "The ICU4X Project Developers", "repository": "https://github.com/unicode-org/icu4x", "license": "Unicode-3.0", @@ -2107,7 +2080,7 @@ }, { "name": "icu_normalizer_data", - "version": "1.5.1", + "version": "2.0.0", "authors": "The ICU4X Project Developers", "repository": "https://github.com/unicode-org/icu4x", "license": "Unicode-3.0", @@ -2116,7 +2089,7 @@ }, { "name": "icu_properties", - "version": "1.5.1", + "version": "2.0.1", "authors": "The ICU4X Project Developers", "repository": "https://github.com/unicode-org/icu4x", "license": "Unicode-3.0", @@ -2125,7 +2098,7 @@ }, { "name": "icu_properties_data", - "version": "1.5.1", + "version": "2.0.1", "authors": "The ICU4X Project Developers", "repository": "https://github.com/unicode-org/icu4x", "license": "Unicode-3.0", @@ -2134,22 +2107,13 @@ }, { "name": "icu_provider", - "version": "1.5.0", + "version": "2.0.0", "authors": "The ICU4X Project Developers", "repository": "https://github.com/unicode-org/icu4x", "license": "Unicode-3.0", "license_file": null, "description": "Trait and struct definitions for the ICU data provider" }, - { - "name": "icu_provider_macros", - "version": "1.5.0", - "authors": "The ICU4X Project Developers", - "repository": "https://github.com/unicode-org/icu4x", - "license": "Unicode-3.0", - "license_file": null, - "description": "Proc macros for ICU data providers" - }, { "name": "id_tree", "version": "1.8.0", @@ -2179,7 +2143,7 @@ }, { "name": "idna_adapter", - "version": "1.2.0", + "version": "1.2.1", "authors": "The rust-url developers", "repository": "https://github.com/hsivonen/idna_adapter", "license": "Apache-2.0 OR MIT", @@ -2206,12 +2170,12 @@ }, { "name": "intl-memoizer", - "version": "0.5.2", - "authors": "Zibi Braniecki |Manish Goregaokar ", + "version": "0.5.3", + "authors": "Caleb Maclennan |Bruce Mitchener |Staś Małolepszy ", "repository": "https://github.com/projectfluent/fluent-rs", "license": "Apache-2.0 OR MIT", "license_file": null, - "description": "A memoizer specifically tailored for storing lazy-initialized intl formatters." + "description": "A memoizer specifically tailored for storing lazy-initialized intl formatters for Project Fluent, a localization system designed to unleash the entire expressive power of natural language translations." }, { "name": "intl_pluralrules", @@ -2232,13 +2196,13 @@ "description": "Provides types and useful methods for working with IPv4 and IPv6 network addresses, commonly called IP prefixes. The new `IpNet`, `Ipv4Net`, and `Ipv6Net` types build on the existing `IpAddr`, `Ipv4Addr`, and `Ipv6Addr` types already provided in Rust's standard library and align to their design to stay consistent. The module also provides useful traits that extend `Ipv4Addr` and `Ipv6Addr` with methods for `Add`, `Sub`, `BitAnd`, and `BitOr` operations. The module only uses stable feature so it is guaranteed to compile using the stable toolchain." }, { - "name": "itertools", - "version": "0.13.0", - "authors": "bluss", - "repository": "https://github.com/rust-itertools/itertools", + "name": "iri-string", + "version": "0.7.8", + "authors": "YOSHIOKA Takuma ", + "repository": "https://github.com/lo48576/iri-string", "license": "Apache-2.0 OR MIT", "license_file": null, - "description": "Extra iterator adaptors, iterator methods, free functions, and macros." + "description": "IRI as string types" }, { "name": "itertools", @@ -2314,7 +2278,7 @@ }, { "name": "libc", - "version": "0.2.172", + "version": "0.2.173", "authors": "The Rust Project Developers", "repository": "https://github.com/rust-lang/libc", "license": "Apache-2.0 OR MIT", @@ -2323,7 +2287,7 @@ }, { "name": "libloading", - "version": "0.8.6", + "version": "0.8.8", "authors": "Simonas Kazlauskas ", "repository": "https://github.com/nagisa/rust_libloading/", "license": "ISC", @@ -2332,7 +2296,7 @@ }, { "name": "libm", - "version": "0.2.13", + "version": "0.2.15", "authors": "Jorge Aparicio ", "repository": "https://github.com/rust-lang/compiler-builtins", "license": "MIT", @@ -2350,13 +2314,22 @@ }, { "name": "libsqlite3-sys", - "version": "0.27.0", + "version": "0.34.0", "authors": "The rusqlite developers", "repository": "https://github.com/rusqlite/rusqlite", "license": "MIT", "license_file": null, "description": "Native bindings to the libsqlite3 library" }, + { + "name": "libz-rs-sys", + "version": "0.5.1", + "authors": null, + "repository": "https://github.com/trifectatechfoundation/zlib-rs", + "license": "Zlib", + "license_file": null, + "description": "A memory-safe zlib implementation written in rust" + }, { "name": "linux-raw-sys", "version": "0.9.4", @@ -2368,7 +2341,7 @@ }, { "name": "litemap", - "version": "0.7.5", + "version": "0.8.0", "authors": "The ICU4X Project Developers", "repository": "https://github.com/unicode-org/icu4x", "license": "Unicode-3.0", @@ -2386,7 +2359,7 @@ }, { "name": "lock_api", - "version": "0.4.12", + "version": "0.4.13", "authors": "Amanieu d'Antras ", "repository": "https://github.com/Amanieu/parking_lot", "license": "Apache-2.0 OR MIT", @@ -2402,6 +2375,15 @@ "license_file": null, "description": "A lightweight logging facade for Rust" }, + { + "name": "lru-slab", + "version": "0.1.2", + "authors": "Benjamin Saunders ", + "repository": "https://github.com/Ralith/lru-slab", + "license": "Apache-2.0 OR MIT OR Zlib", + "license_file": null, + "description": "Pre-allocated storage with constant-time LRU tracking" + }, { "name": "mac", "version": "0.1.1", @@ -2413,7 +2395,7 @@ }, { "name": "macerator", - "version": "0.2.6", + "version": "0.2.8", "authors": "Genna Wingert", "repository": "https://github.com/wingertge/macerator", "license": "Apache-2.0 OR MIT", @@ -2422,7 +2404,7 @@ }, { "name": "macerator-macros", - "version": "0.1.1", + "version": "0.1.2", "authors": "Genna Wingert", "repository": "https://github.com/wingertge/macerator", "license": "Apache-2.0 OR MIT", @@ -2485,7 +2467,7 @@ }, { "name": "matchit", - "version": "0.7.3", + "version": "0.8.4", "authors": "Ibraheem Ahmed ", "repository": "https://github.com/ibraheemdev/matchit", "license": "MIT AND BSD-3-Clause", @@ -2494,7 +2476,7 @@ }, { "name": "matrixmultiply", - "version": "0.3.9", + "version": "0.3.10", "authors": "bluss|R. Janis Goldschmidt", "repository": "https://github.com/bluss/matrixmultiply/", "license": "Apache-2.0 OR MIT", @@ -2512,7 +2494,7 @@ }, { "name": "memchr", - "version": "2.7.4", + "version": "2.7.5", "authors": "Andrew Gallant |bluss", "repository": "https://github.com/BurntSushi/memchr", "license": "MIT OR Unlicense", @@ -2566,7 +2548,7 @@ }, { "name": "miniz_oxide", - "version": "0.8.8", + "version": "0.8.9", "authors": "Frommi |oyvindln |Rich Geldreich richgel99@gmail.com", "repository": "https://github.com/Frommi/miniz_oxide/tree/master/miniz_oxide", "license": "Apache-2.0 OR MIT OR Zlib", @@ -2575,13 +2557,22 @@ }, { "name": "mio", - "version": "1.0.3", + "version": "1.0.4", "authors": "Carl Lerche |Thomas de Zeeuw |Tokio Contributors ", "repository": "https://github.com/tokio-rs/mio", "license": "MIT", "license_file": null, "description": "Lightweight non-blocking I/O." }, + { + "name": "moddef", + "version": "0.2.6", + "authors": null, + "repository": "https://github.com/sigurd4/moddef", + "license": "MIT", + "license_file": null, + "description": "Macro for convenient module declaration. Each module can be put in a group, and visibility can be applied to the whole group with ease." + }, { "name": "multer", "version": "3.1.0", @@ -2593,7 +2584,7 @@ }, { "name": "multimap", - "version": "0.10.0", + "version": "0.10.1", "authors": "Håvar Nøvik ", "repository": "https://github.com/havarnov/multimap", "license": "Apache-2.0 OR MIT", @@ -2627,15 +2618,6 @@ "license_file": null, "description": "An n-dimensional array for general elements and for numerics. Lightweight array views and slicing; views support chunking and splitting." }, - { - "name": "ndarray-rand", - "version": "0.15.0", - "authors": "bluss", - "repository": "https://github.com/rust-ndarray/ndarray", - "license": "Apache-2.0 OR MIT", - "license_file": null, - "description": "Constructors for randomized arrays. `rand` integration for `ndarray`." - }, { "name": "ndk-sys", "version": "0.5.0+25.2.9519653", @@ -2663,15 +2645,6 @@ "license_file": null, "description": "A byte-oriented, zero-copy, parser combinators library" }, - { - "name": "nonempty", - "version": "0.7.0", - "authors": "Alexis Sellier ", - "repository": "https://github.com/cloudhead/nonempty", - "license": "MIT", - "license_file": null, - "description": "Correct by construction non-empty vector" - }, { "name": "ntapi", "version": "0.4.1", @@ -2773,7 +2746,7 @@ }, { "name": "num_cpus", - "version": "1.16.0", + "version": "1.17.0", "authors": "Sean McArthur ", "repository": "https://github.com/seanmonstar/num_cpus", "license": "Apache-2.0 OR MIT", @@ -2845,7 +2818,7 @@ }, { "name": "openssl", - "version": "0.10.72", + "version": "0.10.73", "authors": "Steven Fackler ", "repository": "https://github.com/sfackler/rust-openssl", "license": "Apache-2.0", @@ -2872,7 +2845,7 @@ }, { "name": "openssl-sys", - "version": "0.9.107", + "version": "0.9.109", "authors": "Alex Crichton |Steven Fackler ", "repository": "https://github.com/sfackler/rust-openssl", "license": "MIT", @@ -2926,7 +2899,7 @@ }, { "name": "parking_lot", - "version": "0.12.3", + "version": "0.12.4", "authors": "Amanieu d'Antras ", "repository": "https://github.com/Amanieu/parking_lot", "license": "Apache-2.0 OR MIT", @@ -2935,7 +2908,7 @@ }, { "name": "parking_lot_core", - "version": "0.9.10", + "version": "0.9.11", "authors": "Amanieu d'Antras ", "repository": "https://github.com/Amanieu/parking_lot", "license": "Apache-2.0 OR MIT", @@ -3088,7 +3061,7 @@ }, { "name": "portable-atomic", - "version": "1.11.0", + "version": "1.11.1", "authors": null, "repository": "https://github.com/taiki-e/portable-atomic", "license": "Apache-2.0 OR MIT", @@ -3104,6 +3077,15 @@ "license_file": null, "description": "Synchronization primitives built with portable-atomic." }, + { + "name": "potential_utf", + "version": "0.1.2", + "authors": "The ICU4X Project Developers", + "repository": "https://github.com/unicode-org/icu4x", + "license": "Unicode-3.0", + "license_file": null, + "description": "Unvalidated string and character types" + }, { "name": "powerfmt", "version": "0.2.0", @@ -3142,7 +3124,7 @@ }, { "name": "prettyplease", - "version": "0.2.32", + "version": "0.2.34", "authors": "David Tolnay ", "repository": "https://github.com/dtolnay/prettyplease", "license": "Apache-2.0 OR MIT", @@ -3151,7 +3133,7 @@ }, { "name": "priority-queue", - "version": "2.3.1", + "version": "2.5.0", "authors": "Gianmarco Garrisi ", "repository": "https://github.com/garro95/priority-queue", "license": "LGPL-3.0-or-later OR MPL-2.0", @@ -3241,13 +3223,22 @@ }, { "name": "pulldown-cmark", - "version": "0.9.6", + "version": "0.13.0", "authors": "Raph Levien |Marcus Klaas de Vries ", "repository": "https://github.com/raphlinus/pulldown-cmark", "license": "MIT", "license_file": null, "description": "A pull parser for CommonMark" }, + { + "name": "pulldown-cmark-escape", + "version": "0.11.0", + "authors": "Raph Levien |Marcus Klaas de Vries ", + "repository": "https://github.com/raphlinus/pulldown-cmark", + "license": "MIT", + "license_file": null, + "description": "An escape library for HTML created in the pulldown-cmark project" + }, { "name": "pulp", "version": "0.18.22", @@ -3259,7 +3250,7 @@ }, { "name": "pulp", - "version": "0.21.4", + "version": "0.21.5", "authors": "sarah <>", "repository": "https://github.com/sarah-ek/pulp/", "license": "MIT", @@ -3268,7 +3259,7 @@ }, { "name": "quinn", - "version": "0.11.7", + "version": "0.11.8", "authors": null, "repository": "https://github.com/quinn-rs/quinn", "license": "Apache-2.0 OR MIT", @@ -3277,7 +3268,7 @@ }, { "name": "quinn-proto", - "version": "0.11.11", + "version": "0.11.12", "authors": null, "repository": "https://github.com/quinn-rs/quinn", "license": "Apache-2.0 OR MIT", @@ -3286,7 +3277,7 @@ }, { "name": "quinn-udp", - "version": "0.5.11", + "version": "0.5.12", "authors": null, "repository": "https://github.com/quinn-rs/quinn", "license": "Apache-2.0 OR MIT", @@ -3365,15 +3356,6 @@ "license_file": null, "description": "Core random number generator traits and tools for implementation." }, - { - "name": "rand_distr", - "version": "0.4.3", - "authors": "The Rand Project Developers", - "repository": "https://github.com/rust-random/rand", - "license": "Apache-2.0 OR MIT", - "license_file": null, - "description": "Sampling from random number distributions" - }, { "name": "rand_distr", "version": "0.5.1", @@ -3457,7 +3439,7 @@ }, { "name": "redox_syscall", - "version": "0.5.11", + "version": "0.5.13", "authors": "Jeremy Soller ", "repository": "https://gitlab.redox-os.org/redox-os/syscall", "license": "MIT", @@ -3547,7 +3529,7 @@ }, { "name": "reqwest", - "version": "0.12.15", + "version": "0.12.20", "authors": "Sean McArthur ", "repository": "https://github.com/seanmonstar/reqwest", "license": "Apache-2.0 OR MIT", @@ -3601,7 +3583,7 @@ }, { "name": "rusqlite", - "version": "0.30.0", + "version": "0.36.0", "authors": "The rusqlite developers", "repository": "https://github.com/rusqlite/rusqlite", "license": "MIT", @@ -3610,7 +3592,7 @@ }, { "name": "rustc-demangle", - "version": "0.1.24", + "version": "0.1.25", "authors": "Alex Crichton ", "repository": "https://github.com/rust-lang/rustc-demangle", "license": "Apache-2.0 OR MIT", @@ -3646,7 +3628,7 @@ }, { "name": "rustix", - "version": "1.0.5", + "version": "1.0.7", "authors": "Dan Gohman |Jakub Konka ", "repository": "https://github.com/bytecodealliance/rustix", "license": "Apache-2.0 OR Apache-2.0 WITH LLVM-exception OR MIT", @@ -3655,7 +3637,7 @@ }, { "name": "rustls", - "version": "0.23.26", + "version": "0.23.28", "authors": null, "repository": "https://github.com/rustls/rustls", "license": "Apache-2.0 OR ISC OR MIT", @@ -3682,7 +3664,7 @@ }, { "name": "rustls-pki-types", - "version": "1.11.0", + "version": "1.12.0", "authors": null, "repository": "https://github.com/rustls/pki-types", "license": "Apache-2.0 OR MIT", @@ -3691,7 +3673,7 @@ }, { "name": "rustls-webpki", - "version": "0.103.1", + "version": "0.103.3", "authors": null, "repository": "https://github.com/rustls/webpki", "license": "ISC", @@ -3700,7 +3682,7 @@ }, { "name": "rustversion", - "version": "1.0.20", + "version": "1.0.21", "authors": "David Tolnay ", "repository": "https://github.com/dtolnay/rustversion", "license": "Apache-2.0 OR MIT", @@ -3797,15 +3779,6 @@ "license_file": null, "description": "Apple `Security.framework` low-level FFI bindings" }, - { - "name": "self_cell", - "version": "0.10.3", - "authors": "Lukas Bergdoll ", - "repository": "https://github.com/Voultapher/self_cell", - "license": "Apache-2.0", - "license_file": null, - "description": "Safe-to-use proc-macro-free self-referential structs in stable Rust." - }, { "name": "self_cell", "version": "1.2.0", @@ -3905,9 +3878,18 @@ "license_file": null, "description": "Derive Serialize and Deserialize that delegates to the underlying repr of a C-like enum." }, + { + "name": "serde_spanned", + "version": "0.6.9", + "authors": null, + "repository": "https://github.com/toml-rs/toml", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Serde-compatible spanned Value" + }, { "name": "serde_tuple", - "version": "0.5.0", + "version": "1.1.0", "authors": "Jacob Brown ", "repository": "https://github.com/kardeiz/serde_tuple", "license": "MIT", @@ -3916,12 +3898,12 @@ }, { "name": "serde_tuple_macros", - "version": "0.5.0", + "version": "1.0.1", "authors": "Jacob Brown ", "repository": "https://github.com/kardeiz/serde_tuple", "license": "MIT", "license_file": null, - "description": "De/serialize structs with named fields as array of values" + "description": "Internal proc-macro crate for serde_tuple" }, { "name": "serde_urlencoded", @@ -3943,7 +3925,7 @@ }, { "name": "sha2", - "version": "0.10.8", + "version": "0.10.9", "authors": "RustCrypto Developers", "repository": "https://github.com/RustCrypto/hashes", "license": "Apache-2.0 OR MIT", @@ -3977,6 +3959,15 @@ "license_file": null, "description": "Backend crate for signal-hook" }, + { + "name": "simd-adler32", + "version": "0.3.7", + "authors": "Marvin Countryman ", + "repository": "https://github.com/mcountryman/simd-adler32", + "license": "MIT", + "license_file": null, + "description": "A SIMD-accelerated Adler-32 hash algorithm implementation." + }, { "name": "siphasher", "version": "1.0.1", @@ -3988,7 +3979,7 @@ }, { "name": "slab", - "version": "0.4.9", + "version": "0.4.10", "authors": "Carl Lerche ", "repository": "https://github.com/tokio-rs/slab", "license": "MIT", @@ -4006,7 +3997,7 @@ }, { "name": "smallvec", - "version": "1.15.0", + "version": "1.15.1", "authors": "The Servo Project Developers", "repository": "https://github.com/servo/rust-smallvec", "license": "Apache-2.0 OR MIT", @@ -4015,7 +4006,7 @@ }, { "name": "snafu", - "version": "0.8.5", + "version": "0.8.6", "authors": "Jake Goulding ", "repository": "https://github.com/shepmaster/snafu", "license": "Apache-2.0 OR MIT", @@ -4024,7 +4015,7 @@ }, { "name": "snafu-derive", - "version": "0.8.5", + "version": "0.8.6", "authors": "Jake Goulding ", "repository": "https://github.com/shepmaster/snafu", "license": "Apache-2.0 OR MIT", @@ -4042,7 +4033,7 @@ }, { "name": "socket2", - "version": "0.5.9", + "version": "0.5.10", "authors": "Alex Crichton |Thomas de Zeeuw ", "repository": "https://github.com/rust-lang/socket2", "license": "Apache-2.0 OR MIT", @@ -4177,7 +4168,7 @@ }, { "name": "syn", - "version": "2.0.101", + "version": "2.0.103", "authors": "David Tolnay ", "repository": "https://github.com/dtolnay/syn", "license": "Apache-2.0 OR MIT", @@ -4195,7 +4186,7 @@ }, { "name": "synstructure", - "version": "0.13.1", + "version": "0.13.2", "authors": "Nika Layzell ", "repository": "https://github.com/mystor/synstructure", "license": "MIT", @@ -4240,7 +4231,7 @@ }, { "name": "tempfile", - "version": "3.19.1", + "version": "3.20.0", "authors": "Steven Allen |The Rust Project Developers|Ashley Mannix |Jason White ", "repository": "https://github.com/Stebalien/tempfile", "license": "Apache-2.0 OR MIT", @@ -4321,7 +4312,7 @@ }, { "name": "thread_local", - "version": "1.1.8", + "version": "1.1.9", "authors": "Amanieu d'Antras ", "repository": "https://github.com/Amanieu/thread_local-rs", "license": "Apache-2.0 OR MIT", @@ -4357,7 +4348,7 @@ }, { "name": "tinystr", - "version": "0.7.6", + "version": "0.8.1", "authors": "The ICU4X Project Developers", "repository": "https://github.com/unicode-org/icu4x", "license": "Unicode-3.0", @@ -4384,7 +4375,7 @@ }, { "name": "tokio", - "version": "1.44.2", + "version": "1.45.1", "authors": "Tokio Contributors ", "repository": "https://github.com/tokio-rs/tokio", "license": "MIT", @@ -4418,15 +4409,6 @@ "license_file": null, "description": "Asynchronous TLS/SSL streams for Tokio using Rustls." }, - { - "name": "tokio-socks", - "version": "0.5.2", - "authors": "Yilin Chen ", - "repository": "https://github.com/sticnarf/tokio-socks", - "license": "MIT", - "license_file": null, - "description": "Asynchronous SOCKS proxy support for Rust." - }, { "name": "tokio-util", "version": "0.7.15", @@ -4438,8 +4420,8 @@ }, { "name": "toml_datetime", - "version": "0.6.9", - "authors": "Alex Crichton ", + "version": "0.6.11", + "authors": null, "repository": "https://github.com/toml-rs/toml", "license": "Apache-2.0 OR MIT", "license_file": null, @@ -4447,13 +4429,22 @@ }, { "name": "toml_edit", - "version": "0.22.25", - "authors": "Andronik Ordian |Ed Page ", + "version": "0.22.27", + "authors": null, "repository": "https://github.com/toml-rs/toml", "license": "Apache-2.0 OR MIT", "license_file": null, "description": "Yet another format-preserving TOML parser." }, + { + "name": "toml_write", + "version": "0.1.2", + "authors": null, + "repository": "https://github.com/toml-rs/toml", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "A low-level interface for writing out TOML" + }, { "name": "tower", "version": "0.5.2", @@ -4465,7 +4456,7 @@ }, { "name": "tower-http", - "version": "0.5.2", + "version": "0.6.6", "authors": "Tower Maintainers ", "repository": "https://github.com/tower-rs/tower-http", "license": "MIT", @@ -4510,7 +4501,7 @@ }, { "name": "tracing-attributes", - "version": "0.1.28", + "version": "0.1.29", "authors": "Tokio Contributors |Eliza Weisman |David Barsky ", "repository": "https://github.com/tokio-rs/tracing", "license": "MIT", @@ -4519,7 +4510,7 @@ }, { "name": "tracing-core", - "version": "0.1.33", + "version": "0.1.34", "authors": "Tokio Contributors ", "repository": "https://github.com/tokio-rs/tracing", "license": "MIT", @@ -4555,7 +4546,7 @@ }, { "name": "type-map", - "version": "0.5.0", + "version": "0.5.1", "authors": "Jacob Brown ", "repository": "https://github.com/kardeiz/type-map", "license": "Apache-2.0 OR MIT", @@ -4609,7 +4600,7 @@ }, { "name": "unic-langid", - "version": "0.9.5", + "version": "0.9.6", "authors": "Zibi Braniecki ", "repository": "https://github.com/zbraniecki/unic-locale", "license": "Apache-2.0 OR MIT", @@ -4618,7 +4609,7 @@ }, { "name": "unic-langid-impl", - "version": "0.9.5", + "version": "0.9.6", "authors": "Zibi Braniecki ", "repository": "https://github.com/zbraniecki/unic-locale", "license": "Apache-2.0 OR MIT", @@ -4627,7 +4618,7 @@ }, { "name": "unic-langid-macros", - "version": "0.9.5", + "version": "0.9.6", "authors": "Zibi Braniecki ", "repository": "https://github.com/zbraniecki/unic-locale", "license": "Apache-2.0 OR MIT", @@ -4636,7 +4627,7 @@ }, { "name": "unic-langid-macros-impl", - "version": "0.9.5", + "version": "0.9.6", "authors": "Zibi Braniecki ", "repository": "https://github.com/zbraniecki/unic-locale", "license": "Apache-2.0 OR MIT", @@ -4699,16 +4690,7 @@ }, { "name": "unicode-width", - "version": "0.1.14", - "authors": "kwantam |Manish Goregaokar ", - "repository": "https://github.com/unicode-rs/unicode-width", - "license": "Apache-2.0 OR MIT", - "license_file": null, - "description": "Determine displayed width of `char` and `str` types according to Unicode Standard Annex #11 rules." - }, - { - "name": "unicode-width", - "version": "0.2.0", + "version": "0.2.1", "authors": "kwantam |Manish Goregaokar ", "repository": "https://github.com/unicode-rs/unicode-width", "license": "Apache-2.0 OR MIT", @@ -4760,15 +4742,6 @@ "license_file": null, "description": "Incremental, zero-copy UTF-8 decoding with error handling" }, - { - "name": "utf16_iter", - "version": "1.0.5", - "authors": "Henri Sivonen ", - "repository": "https://github.com/hsivonen/utf16_iter", - "license": "Apache-2.0 OR MIT", - "license_file": null, - "description": "Iterator by char over potentially-invalid UTF-16 in &[u16]" - }, { "name": "utf8_iter", "version": "1.0.4", @@ -4780,7 +4753,7 @@ }, { "name": "uuid", - "version": "1.16.0", + "version": "1.17.0", "authors": "Ashley Mannix|Dylan DPC|Hunar Roop Kahlon", "repository": "https://github.com/uuid-rs/uuid", "license": "Apache-2.0 OR MIT", @@ -4843,7 +4816,7 @@ }, { "name": "wasi", - "version": "0.11.0+wasi-snapshot-preview1", + "version": "0.11.1+wasi-snapshot-preview1", "authors": "The Cranelift Project Developers", "repository": "https://github.com/bytecodealliance/wasi", "license": "Apache-2.0 OR Apache-2.0 WITH LLVM-exception OR MIT", @@ -4951,7 +4924,7 @@ }, { "name": "web_atoms", - "version": "0.1.0", + "version": "0.1.3", "authors": "The html5ever Project Developers", "repository": "https://github.com/servo/html5ever", "license": "Apache-2.0 OR MIT", @@ -4960,16 +4933,16 @@ }, { "name": "webpki-roots", - "version": "0.26.8", + "version": "1.0.0", "authors": null, "repository": "https://github.com/rustls/webpki-roots", - "license": "MPL-2.0", + "license": "CDLA-Permissive-2.0", "license_file": null, "description": "Mozilla's CA root certificates for use with webpki" }, { "name": "wgpu", - "version": "25.0.0", + "version": "25.0.2", "authors": "gfx-rs developers", "repository": "https://github.com/gfx-rs/wgpu", "license": "Apache-2.0 OR MIT", @@ -4978,7 +4951,7 @@ }, { "name": "wgpu-core", - "version": "25.0.1", + "version": "25.0.2", "authors": "gfx-rs developers", "repository": "https://github.com/gfx-rs/wgpu", "license": "Apache-2.0 OR MIT", @@ -5014,7 +4987,7 @@ }, { "name": "wgpu-hal", - "version": "25.0.1", + "version": "25.0.2", "authors": "gfx-rs developers", "repository": "https://github.com/gfx-rs/wgpu", "license": "Apache-2.0 OR MIT", @@ -5077,7 +5050,7 @@ }, { "name": "windows", - "version": "0.57.0", + "version": "0.58.0", "authors": "Microsoft", "repository": "https://github.com/microsoft/windows-rs", "license": "Apache-2.0 OR MIT", @@ -5086,13 +5059,22 @@ }, { "name": "windows", - "version": "0.58.0", + "version": "0.61.3", "authors": "Microsoft", "repository": "https://github.com/microsoft/windows-rs", "license": "Apache-2.0 OR MIT", "license_file": null, "description": "Rust for Windows" }, + { + "name": "windows-collections", + "version": "0.2.0", + "authors": null, + "repository": "https://github.com/microsoft/windows-rs", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Windows collection types" + }, { "name": "windows-core", "version": "0.56.0", @@ -5102,15 +5084,6 @@ "license_file": null, "description": "Rust for Windows" }, - { - "name": "windows-core", - "version": "0.57.0", - "authors": "Microsoft", - "repository": "https://github.com/microsoft/windows-rs", - "license": "Apache-2.0 OR MIT", - "license_file": null, - "description": "Rust for Windows" - }, { "name": "windows-core", "version": "0.58.0", @@ -5122,7 +5095,7 @@ }, { "name": "windows-core", - "version": "0.61.0", + "version": "0.61.2", "authors": "Microsoft", "repository": "https://github.com/microsoft/windows-rs", "license": "Apache-2.0 OR MIT", @@ -5130,17 +5103,17 @@ "description": "Core type support for COM and Windows" }, { - "name": "windows-implement", - "version": "0.56.0", - "authors": "Microsoft", + "name": "windows-future", + "version": "0.2.1", + "authors": null, "repository": "https://github.com/microsoft/windows-rs", "license": "Apache-2.0 OR MIT", "license_file": null, - "description": "The implement macro for the windows crate" + "description": "Windows async types" }, { "name": "windows-implement", - "version": "0.57.0", + "version": "0.56.0", "authors": "Microsoft", "repository": "https://github.com/microsoft/windows-rs", "license": "Apache-2.0 OR MIT", @@ -5174,15 +5147,6 @@ "license_file": null, "description": "The interface macro for the windows crate" }, - { - "name": "windows-interface", - "version": "0.57.0", - "authors": "Microsoft", - "repository": "https://github.com/microsoft/windows-rs", - "license": "Apache-2.0 OR MIT", - "license_file": null, - "description": "The interface macro for the windows crate" - }, { "name": "windows-interface", "version": "0.58.0", @@ -5203,7 +5167,7 @@ }, { "name": "windows-link", - "version": "0.1.1", + "version": "0.1.3", "authors": "Microsoft", "repository": "https://github.com/microsoft/windows-rs", "license": "Apache-2.0 OR MIT", @@ -5211,13 +5175,13 @@ "description": "Linking for Windows" }, { - "name": "windows-registry", - "version": "0.4.0", - "authors": "Microsoft", + "name": "windows-numerics", + "version": "0.2.0", + "authors": null, "repository": "https://github.com/microsoft/windows-rs", "license": "Apache-2.0 OR MIT", "license_file": null, - "description": "Windows registry" + "description": "Windows numeric types" }, { "name": "windows-result", @@ -5239,7 +5203,7 @@ }, { "name": "windows-result", - "version": "0.3.2", + "version": "0.3.4", "authors": "Microsoft", "repository": "https://github.com/microsoft/windows-rs", "license": "Apache-2.0 OR MIT", @@ -5257,16 +5221,7 @@ }, { "name": "windows-strings", - "version": "0.3.1", - "authors": "Microsoft", - "repository": "https://github.com/microsoft/windows-rs", - "license": "Apache-2.0 OR MIT", - "license_file": null, - "description": "Windows string types" - }, - { - "name": "windows-strings", - "version": "0.4.0", + "version": "0.4.2", "authors": "Microsoft", "repository": "https://github.com/microsoft/windows-rs", "license": "Apache-2.0 OR MIT", @@ -5300,6 +5255,15 @@ "license_file": null, "description": "Rust for Windows" }, + { + "name": "windows-sys", + "version": "0.60.2", + "authors": "Microsoft", + "repository": "https://github.com/microsoft/windows-rs", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Rust for Windows" + }, { "name": "windows-targets", "version": "0.48.5", @@ -5320,13 +5284,22 @@ }, { "name": "windows-targets", - "version": "0.53.0", + "version": "0.53.2", "authors": "Microsoft", "repository": "https://github.com/microsoft/windows-rs", "license": "Apache-2.0 OR MIT", "license_file": null, "description": "Import libs for Windows" }, + { + "name": "windows-threading", + "version": "0.1.0", + "authors": "Microsoft", + "repository": "https://github.com/microsoft/windows-rs", + "license": "Apache-2.0 OR MIT", + "license_file": null, + "description": "Windows threading" + }, { "name": "windows_aarch64_gnullvm", "version": "0.48.5", @@ -5536,7 +5509,7 @@ }, { "name": "winnow", - "version": "0.7.7", + "version": "0.7.11", "authors": null, "repository": "https://github.com/winnow-rs/winnow", "license": "MIT", @@ -5570,18 +5543,9 @@ "license_file": null, "description": "Derive macro for nvml-wrapper, not for general use" }, - { - "name": "write16", - "version": "1.0.0", - "authors": null, - "repository": "https://github.com/hsivonen/write16", - "license": "Apache-2.0 OR MIT", - "license_file": null, - "description": "A UTF-16 analog of the Write trait" - }, { "name": "writeable", - "version": "0.5.5", + "version": "0.6.1", "authors": "The ICU4X Project Developers", "repository": "https://github.com/unicode-org/icu4x", "license": "Unicode-3.0", @@ -5606,6 +5570,15 @@ "license_file": null, "description": "Abstraction allowing borrowed data to be carried along with the backing data it borrows from" }, + { + "name": "yoke", + "version": "0.8.0", + "authors": "Manish Goregaokar ", + "repository": "https://github.com/unicode-org/icu4x", + "license": "Unicode-3.0", + "license_file": null, + "description": "Abstraction allowing borrowed data to be carried along with the backing data it borrows from" + }, { "name": "yoke-derive", "version": "0.7.5", @@ -5616,13 +5589,13 @@ "description": "Custom derive for the yoke crate" }, { - "name": "zerocopy", - "version": "0.7.35", - "authors": "Joshua Liebow-Feeser ", - "repository": "https://github.com/google/zerocopy", - "license": "Apache-2.0 OR BSD-2-Clause OR MIT", + "name": "yoke-derive", + "version": "0.8.0", + "authors": "Manish Goregaokar ", + "repository": "https://github.com/unicode-org/icu4x", + "license": "Unicode-3.0", "license_file": null, - "description": "Utilities for zero-copy parsing and serialization" + "description": "Custom derive for the yoke crate" }, { "name": "zerocopy", @@ -5633,15 +5606,6 @@ "license_file": null, "description": "Zerocopy makes zero-cost memory manipulation effortless. We write \"unsafe\" so you don't have to." }, - { - "name": "zerocopy-derive", - "version": "0.7.35", - "authors": "Joshua Liebow-Feeser ", - "repository": "https://github.com/google/zerocopy", - "license": "Apache-2.0 OR BSD-2-Clause OR MIT", - "license_file": null, - "description": "Custom derive for traits from the zerocopy crate" - }, { "name": "zerocopy-derive", "version": "0.8.25", @@ -5678,9 +5642,18 @@ "license_file": null, "description": "Securely clear secrets from memory with a simple trait built on stable Rust primitives which guarantee memory is zeroed using an operation will not be 'optimized away' by the compiler. Uses a portable pure Rust implementation that works everywhere, even WASM!" }, + { + "name": "zerotrie", + "version": "0.2.2", + "authors": "The ICU4X Project Developers", + "repository": "https://github.com/unicode-org/icu4x", + "license": "Unicode-3.0", + "license_file": null, + "description": "A data structure that efficiently maps strings to integers" + }, { "name": "zerovec", - "version": "0.10.4", + "version": "0.11.2", "authors": "The ICU4X Project Developers", "repository": "https://github.com/unicode-org/icu4x", "license": "Unicode-3.0", @@ -5689,22 +5662,13 @@ }, { "name": "zerovec-derive", - "version": "0.10.3", + "version": "0.11.1", "authors": "Manish Goregaokar ", "repository": "https://github.com/unicode-org/icu4x", "license": "Unicode-3.0", "license_file": null, "description": "Custom derive for the zerovec crate" }, - { - "name": "zip", - "version": "0.6.6", - "authors": "Mathijs van de Nes |Marli Frost |Ryan Levick ", - "repository": "https://github.com/zip-rs/zip.git", - "license": "MIT", - "license_file": null, - "description": "Library to support the reading and writing of zip files." - }, { "name": "zip", "version": "1.1.4", @@ -5714,6 +5678,33 @@ "license_file": null, "description": "Library to support the reading and writing of zip files." }, + { + "name": "zip", + "version": "4.1.0", + "authors": "Mathijs van de Nes |Marli Frost |Ryan Levick |Chris Hennick ", + "repository": "https://github.com/zip-rs/zip2.git", + "license": "MIT", + "license_file": null, + "description": "Library to support the reading and writing of zip files." + }, + { + "name": "zlib-rs", + "version": "0.5.1", + "authors": null, + "repository": "https://github.com/trifectatechfoundation/zlib-rs", + "license": "Zlib", + "license_file": null, + "description": "A memory-safe zlib implementation written in rust" + }, + { + "name": "zopfli", + "version": "0.8.2", + "authors": null, + "repository": "https://github.com/zopfli-rs/zopfli", + "license": "Apache-2.0", + "license_file": null, + "description": "A Rust implementation of the Zopfli compression algorithm." + }, { "name": "zstd", "version": "0.13.3", diff --git a/docs/development.md b/docs/development.md index a057b5c10..c963aec02 100644 --- a/docs/development.md +++ b/docs/development.md @@ -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. -# Binary Bundles +# Installer/launcher -Anki's official binary packages are created with `./ninja bundle`. The bundling -process was created specifically for the official builds, and is provided as-is; -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. +- The anki-release package is created/published with the scripts in qt/release. +- The installer/launcher is created with the build scripts in qt/launcher/{platform}. ## Mixing development and study diff --git a/docs/linux.md b/docs/linux.md index 27e3ceeda..55794e074 100644 --- a/docs/linux.md +++ b/docs/linux.md @@ -51,13 +51,8 @@ Anki requires a recent glibc. If you are using a distro that uses musl, Anki will not work. -If your glibc version is 2.35+ on AMD64 or 2.39+ on ARM64, you can skip the rest of this section. - -If your system has an older glibc, you won't be able to use the PyQt wheels that are -available in pip/PyPy, and will need to use your system-installed PyQt instead. -Your distro will also need to have Python 3.9 or later. - -After installing the system libraries (eg: +You can use your system's Qt libraries if they are Qt 6.2 or later, if +you wish. After installing the system libraries (eg: 'sudo apt install python3-pyqt6.qt{quick,webengine} python3-venv pyqt6-dev-tools'), find the place they are installed (eg '/usr/lib/python3/dist-packages'). On modern Ubuntu, you'll also need 'sudo apt remove python3-protobuf'. Then before running any commands like './run', tell Anki where @@ -68,12 +63,6 @@ export PYTHONPATH=/usr/lib/python3/dist-packages export PYTHON_BINARY=/usr/bin/python3 ``` -There are a few things to be aware of: - -- You should use ./run and not tools/run-qt5\*, even if your system libraries are Qt5. -- If your system libraries are Qt5, when creating an aqt wheel, the wheel will not work - on Qt6 environments. - ## Packaging considerations Python, node and protoc are downloaded as part of the build. You can optionally define diff --git a/docs/windows.md b/docs/windows.md index aae9f6869..12f4e7c39 100644 --- a/docs/windows.md +++ b/docs/windows.md @@ -9,7 +9,12 @@ You must be running 64 bit Windows 10, version 1703 or newer. **Rustup**: 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**: diff --git a/ftl/core-repo b/ftl/core-repo index ca04132a8..2f8c9d956 160000 --- a/ftl/core-repo +++ b/ftl/core-repo @@ -1 +1 @@ -Subproject commit ca04132a8f82296f3e0ea22b74bb4221e1d11d3f +Subproject commit 2f8c9d9566aef8b86e3326fe9ff007d594b7ec83 diff --git a/ftl/core/deck-config.ftl b/ftl/core/deck-config.ftl index d16ce45bc..286e6bae8 100644 --- a/ftl/core/deck-config.ftl +++ b/ftl/core/deck-config.ftl @@ -307,16 +307,17 @@ deck-config-new-interval-tooltip = The multiplier applied to a review interval w deck-config-minimum-interval-tooltip = The minimum interval given to a review card after answering `Again`. deck-config-custom-scheduling = Custom scheduling deck-config-custom-scheduling-tooltip = Affects the entire collection. Use at your own risk! -# Easy Days section + +## Easy Days section. deck-config-easy-days-title = Easy Days -deck-config-easy-days-monday = Monday -deck-config-easy-days-tuesday = Tuesday -deck-config-easy-days-wednesday = Wednesday -deck-config-easy-days-thursday = Thursday -deck-config-easy-days-friday = Friday -deck-config-easy-days-saturday = Saturday -deck-config-easy-days-sunday = Sunday +deck-config-easy-days-monday = Mon +deck-config-easy-days-tuesday = Tue +deck-config-easy-days-wednesday = Wed +deck-config-easy-days-thursday = Thu +deck-config-easy-days-friday = Fri +deck-config-easy-days-saturday = Sat +deck-config-easy-days-sunday = Sun deck-config-easy-days-normal = Normal deck-config-easy-days-reduced = Reduced deck-config-easy-days-minimum = Minimum @@ -395,9 +396,11 @@ deck-config-weights = FSRS parameters deck-config-compute-optimal-weights = Optimize FSRS parameters deck-config-compute-minimum-recommended-retention = Minimum recommended retention deck-config-optimize-button = Optimize Current Preset +# Indicates that a given function or label, provided via the "text" variable, operates slowly. +deck-config-slow-suffix = { $text } (slow) deck-config-compute-button = Compute deck-config-ignore-before = Ignore cards reviewed before -deck-config-time-to-optimize = It's been a while - using the Optimize All button is recommended. +deck-config-time-to-optimize = It's been a while - using the Optimize All Presets button is recommended. deck-config-evaluate-button = Evaluate deck-config-desired-retention = Desired retention deck-config-historical-retention = Historical retention @@ -482,9 +485,12 @@ deck-config-percent-of-reviews = *[other] { $pct }% of { $reviews } reviews } deck-config-percent-input = { $pct }% +# This message appears during FSRS parameter optimization. +deck-config-checking-for-improvement = Checking for improvement... deck-config-optimizing-preset = Optimizing preset { $current_count }/{ $total_count }... deck-config-fsrs-must-be-enabled = FSRS must be enabled first. deck-config-fsrs-params-optimal = The FSRS parameters currently appear to be optimal. + deck-config-fsrs-params-no-reviews = No reviews found. Make sure this preset is assigned to all decks (including subdecks) that you want to optimize, and try again. deck-config-wait-for-audio = Wait for audio @@ -511,6 +517,23 @@ deck-config-save-options-to-preset = Save Changes to Preset # specific date. deck-config-fsrs-simulator-radio-memorized = Memorized +## Messages related to the FSRS scheduler’s health check. The health check determines whether the correlation between FSRS predictions and your memory is good or bad. It can be optionally triggered as part of the "Optimize" function. + +# Checkbox +deck-config-health-check = Check health when optimizing +# Message box showing the result of the health check +deck-config-fsrs-bad-fit-warning = Health Check: + Your memory is difficult for FSRS to predict. Recommendations: + + - Suspend or reformulate leeches. + - Use the answer buttons consistently. Keep in mind that "Hard" is a passing grade, not a failing grade. + - Understand before you memorize. + + If you follow these suggestions, performance will usually improve over the next few months. +# Message box showing the result of the health check +deck-config-fsrs-good-fit = Health Check: + FSRS can adapt to your memory well. + ## NO NEED TO TRANSLATE. This text is no longer used by Anki, and will be removed in the future. deck-config-a-100-day-interval = @@ -550,6 +573,8 @@ deck-config-compute-optimal-retention-tooltip = if it significantly differs from 0.9, it's a sign that the time you've allocated each day is either too low or too high for the amount of cards you're trying to learn. This number can be useful as a reference, but it is not recommended to copy it into the desired retention field. +deck-config-health-check-tooltip1 = This will show a warning if FSRS struggles to adapt to your memory. +deck-config-health-check-tooltip2 = Health check is performed only when using Optimize Current Preset. deck-config-compute-optimal-retention = Compute minimum recommended retention deck-config-predicted-optimal-retention = Minimum recommended retention: { $num } diff --git a/ftl/core/editing.ftl b/ftl/core/editing.ftl index 64c0db0c1..3aacb9746 100644 --- a/ftl/core/editing.ftl +++ b/ftl/core/editing.ftl @@ -96,6 +96,7 @@ editing-image-occlusion-rectangle-tool = Rectangle editing-image-occlusion-ellipse-tool = Ellipse editing-image-occlusion-polygon-tool = Polygon editing-image-occlusion-text-tool = Text +editing-image-occlusion-fill-tool = Fill with colour editing-image-occlusion-toggle-mask-editor = Toggle Mask Editor editing-image-occlusion-reset = Reset Image Occlusion editing-image-occlusion-confirm-reset = Are you sure you want to reset this image occlusion? diff --git a/ftl/extract-strings.py b/ftl/extract-strings.py deleted file mode 100644 index 7c3a63b3f..000000000 --- a/ftl/extract-strings.py +++ /dev/null @@ -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)) diff --git a/ftl/format.py b/ftl/format.py deleted file mode 100644 index a18aa5cfa..000000000 --- a/ftl/format.py +++ /dev/null @@ -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) diff --git a/ftl/format_check.py b/ftl/format_check.py deleted file mode 100644 index 7faeacd2c..000000000 --- a/ftl/format_check.py +++ /dev/null @@ -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) diff --git a/ftl/qt-repo b/ftl/qt-repo index f35acabb4..69f2dbaeb 160000 --- a/ftl/qt-repo +++ b/ftl/qt-repo @@ -1 +1 @@ -Subproject commit f35acabb46dc9197a62c47eb7f2ca062628b1d94 +Subproject commit 69f2dbaeba6f72ac62da0b35881f320603da5124 diff --git a/ftl/qt/qt-misc.ftl b/ftl/qt/qt-misc.ftl index 294cd8a83..60c22ef8b 100644 --- a/ftl/qt/qt-misc.ftl +++ b/ftl/qt/qt-misc.ftl @@ -73,6 +73,7 @@ qt-misc-second = qt-misc-layout-auto-enabled = Responsive layout enabled qt-misc-layout-vertical-enabled = Vertical layout enabled qt-misc-layout-horizontal-enabled = Horizontal layout enabled +qt-misc-please-restart-to-update-anki = Please restart Anki to update to the latest version. ## deprecated- these strings will be removed in the future, and do not need ## to be translated diff --git a/ninja b/ninja index 5feee474b..c44f8c330 100755 --- a/ninja +++ b/ninja @@ -8,7 +8,7 @@ else out="$BUILD_ROOT" fi export CARGO_TARGET_DIR=$out/rust -export RECONFIGURE_KEY="${MAC_X86};${SOURCEMAP};${HMR}" +export RECONFIGURE_KEY="${MAC_X86};${LIN_ARM64};${SOURCEMAP};${HMR}" if [ "$SKIP_RUNNER_BUILD" = "1" ]; then echo "Runner not rebuilt." diff --git a/proto/anki/deck_config.proto b/proto/anki/deck_config.proto index bb9ead778..831283931 100644 --- a/proto/anki/deck_config.proto +++ b/proto/anki/deck_config.proto @@ -235,6 +235,7 @@ message DeckConfigsForUpdate { // only applies to v3 scheduler bool new_cards_ignore_review_limit = 7; bool fsrs = 8; + bool fsrs_health_check = 11; bool apply_all_parent_limits = 9; uint32 days_since_last_fsrs_optimize = 10; } @@ -258,4 +259,5 @@ message UpdateDeckConfigsRequest { bool fsrs = 8; bool apply_all_parent_limits = 9; bool fsrs_reschedule = 10; + bool fsrs_health_check = 11; } diff --git a/proto/anki/scheduler.proto b/proto/anki/scheduler.proto index 364bf50ad..1b7d44a83 100644 --- a/proto/anki/scheduler.proto +++ b/proto/anki/scheduler.proto @@ -354,11 +354,13 @@ message ComputeFsrsParamsRequest { repeated float current_params = 2; int64 ignore_revlogs_before_ms = 3; uint32 num_of_relearning_steps = 4; + bool health_check = 5; } message ComputeFsrsParamsResponse { repeated float params = 1; uint32 fsrs_items = 2; + optional bool health_check_passed = 3; } message ComputeFsrsParamsFromItemsRequest { @@ -435,9 +437,9 @@ message GetOptimalRetentionParametersResponse { } message EvaluateParamsRequest { - repeated float params = 1; - string search = 2; - int64 ignore_revlogs_before_ms = 3; + string search = 1; + int64 ignore_revlogs_before_ms = 2; + uint32 num_of_relearning_steps = 3; } message EvaluateParamsResponse { @@ -448,6 +450,7 @@ message EvaluateParamsResponse { message ComputeMemoryStateResponse { optional cards.FsrsMemoryState state = 1; float desired_retention = 2; + float decay = 3; } message FuzzDeltaRequest { diff --git a/pylib/anki/collection.py b/pylib/anki/collection.py index 17ee08e2f..6cf38174c 100644 --- a/pylib/anki/collection.py +++ b/pylib/anki/collection.py @@ -122,6 +122,7 @@ class ComputedMemoryState: desired_retention: float stability: float | None = None difficulty: float | None = None + decay: float | None = None @dataclass @@ -1189,9 +1190,13 @@ class Collection(DeprecatedNamesMixin): desired_retention=resp.desired_retention, stability=resp.state.stability, difficulty=resp.state.difficulty, + decay=resp.decay, ) else: - return ComputedMemoryState(desired_retention=resp.desired_retention) + return ComputedMemoryState( + desired_retention=resp.desired_retention, + decay=resp.decay, + ) def fuzz_delta(self, card_id: CardId, interval: int) -> int: "The delta days of fuzz applied if reviewing the card in v3." diff --git a/pylib/anki/foreign_data/__init__.py b/pylib/anki/foreign_data/__init__.py index afcaf685e..8aac2cc42 100644 --- a/pylib/anki/foreign_data/__init__.py +++ b/pylib/anki/foreign_data/__init__.py @@ -1,8 +1,7 @@ # Copyright: Ankitects Pty Ltd and contributors # 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 diff --git a/pylib/anki/importing/noteimp.py b/pylib/anki/importing/noteimp.py index f69696ef8..f827a525a 100644 --- a/pylib/anki/importing/noteimp.py +++ b/pylib/anki/importing/noteimp.py @@ -167,9 +167,9 @@ class NoteImporter(Importer): firsts[fld0] = True # already exists? found = False - if csum in csums: + if csum in csums: # type: ignore[comparison-overlap] # 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) sflds = split_fields(flds) if fld0 == sflds[0]: diff --git a/pylib/anki/lang.py b/pylib/anki/lang.py index ca61f28bd..a0a6bf757 100644 --- a/pylib/anki/lang.py +++ b/pylib/anki/lang.py @@ -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 with warnings.catch_warnings(): warnings.simplefilter("ignore", DeprecationWarning) - (sys_lang, enc) = locale.getdefaultlocale() + (sys_lang, enc) = ( + locale.getdefaultlocale() # pylint: disable=deprecated-method + ) except AttributeError: # this will return a different format on Windows (e.g. Italian_Italy), resulting in us falling back to en_US # further below diff --git a/pylib/anki/media.py b/pylib/anki/media.py index 5d8653a97..8ba5d432c 100644 --- a/pylib/anki/media.py +++ b/pylib/anki/media.py @@ -76,7 +76,7 @@ class MediaManager(DeprecatedNamesMixin): return self.col._backend.strip_av_tags(text) def _extract_filenames(self, text: str) -> list[str]: - "This only exists do support a legacy function; do not use." + "This only exists to support a legacy function; do not use." out = self.col._backend.extract_av_tags(text=text, question_side=True) return [ x.filename diff --git a/pylib/anki/scheduler/base.py b/pylib/anki/scheduler/base.py index 83ef9d393..ffe4a6ef9 100644 --- a/pylib/anki/scheduler/base.py +++ b/pylib/anki/scheduler/base.py @@ -42,6 +42,7 @@ from anki.utils import ids2str, int_time class SchedulerBase(DeprecatedNamesMixin): "Actions shared between schedulers." + version = 0 def __init__(self, col: anki.collection.Collection) -> None: diff --git a/pylib/anki/sound.py b/pylib/anki/sound.py index 3d375f716..3af584dae 100644 --- a/pylib/anki/sound.py +++ b/pylib/anki/sound.py @@ -9,10 +9,14 @@ These can be accessed via eg card.question_av_tags() from __future__ import annotations +import os +import os.path import re from dataclasses import dataclass from typing import Union +from anki import hooks + @dataclass class TTSTag: @@ -34,10 +38,30 @@ class SoundOrVideoTag: """Contains the filename inside a [sound:...] tag. Video files also use [sound:...]. + + SECURITY: We should only ever construct this with basename(filename), + as passing arbitrary paths to mpv from a shared deck is a security issue. + + Anki add-ons can supply an absolute file path to play any file on disk + using the built-in media player. """ filename: str + def path(self, media_folder: str) -> str: + "Prepend the media folder to the filename." + if os.path.basename(self.filename) == self.filename: + # Path in the current collection's media folder. + # Turn it into a fully-qualified path so mpv can find it, and to + # ensure the filename doesn't get treated like a non-file scheme. + head, tail = media_folder, self.filename + else: + # Add-ons can use absolute paths to play arbitrary files on disk. + # Example: sound.av_player.play_tags([SoundOrVideoTag("/path/to/file")]) + head, tail = os.path.split(os.path.abspath(self.filename)) + tail = hooks.media_file_filter(tail) + return os.path.join(head, tail) + # note this does not include image tags, which are handled with HTML. AVTag = Union[SoundOrVideoTag, TTSTag] diff --git a/pylib/anki/stats.py b/pylib/anki/stats.py index 2f7de2e04..e6ca1cb97 100644 --- a/pylib/anki/stats.py +++ b/pylib/anki/stats.py @@ -174,7 +174,7 @@ from revlog where type != {REVLOG_RESCHED} and id > ? """ cards=cards, seconds=float(thetime) ) # again/pass count - b += "
" + "Again count: %s" % bold(failed) + b += "
" + "Again count: %s" % bold(str(failed)) if cards: b += " " + "(%s correct)" % bold( "%0.1f%%" % ((1 - failed / float(cards)) * 100) @@ -182,7 +182,10 @@ from revlog where type != {REVLOG_RESCHED} and id > ? """ # type breakdown b += "
" 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 mcnt, msum = self.col.db.first( diff --git a/pylib/anki/template.py b/pylib/anki/template.py index 3cc8afaf8..bc507f0f6 100644 --- a/pylib/anki/template.py +++ b/pylib/anki/template.py @@ -28,6 +28,7 @@ template_legacy.py file, using the legacy addHook() system. from __future__ import annotations +import os.path from collections.abc import Sequence from dataclasses import dataclass from typing import Any, Union @@ -95,7 +96,7 @@ class PartiallyRenderedCard: def av_tag_to_native(tag: card_rendering_pb2.AVTag) -> AVTag: val = tag.WhichOneof("value") if val == "sound_or_video": - return SoundOrVideoTag(filename=tag.sound_or_video) + return SoundOrVideoTag(filename=os.path.basename(tag.sound_or_video)) else: return TTSTag( field_text=tag.tts.field_text, @@ -278,6 +279,7 @@ class TemplateRenderContext: @dataclass class TemplateRenderOutput: "Stores the rendered templates and extracted AV tags." + question_text: str answer_text: str question_av_tags: list[AVTag] diff --git a/pylib/anki/utils.py b/pylib/anki/utils.py index 1b4212620..c61fd0588 100644 --- a/pylib/anki/utils.py +++ b/pylib/anki/utils.py @@ -244,8 +244,8 @@ def call(argv: list[str], wait: bool = True, **kwargs: Any) -> int: # OS helpers ############################################################################## -is_mac = sys.platform.startswith("darwin") -is_win = sys.platform.startswith("win32") +is_mac = sys.platform == "darwin" +is_win = sys.platform == "win32" # also covers *BSD is_lin = not is_mac and not is_win is_gnome = ( @@ -309,12 +309,17 @@ def int_version() -> int: """Anki's version as an integer in the form YYMMPP, e.g. 230900. (year, month, patch). In 2.1.x releases, this was just the last number.""" + import re + from anki.buildinfo import version + # Strip non-numeric characters (handles beta/rc suffixes like '25.02b1' or 'rc3') + numeric_version = re.sub(r"[^0-9.]", "", version) + try: - [year, month, patch] = version.split(".") + [year, month, patch] = numeric_version.split(".") except ValueError: - [year, month] = version.split(".") + [year, month] = numeric_version.split(".") patch = "0" year_num = int(year) diff --git a/pylib/hatch_build.py b/pylib/hatch_build.py new file mode 100644 index 000000000..c3539da56 --- /dev/null +++ b/pylib/hatch_build.py @@ -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) diff --git a/pylib/pyproject.toml b/pylib/pyproject.toml new file mode 100644 index 000000000..555f30c86 --- /dev/null +++ b/pylib/pyproject.toml @@ -0,0 +1,34 @@ +[project] +name = "anki" +dynamic = ["version"] +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" diff --git a/pylib/rsbridge/Cargo.toml b/pylib/rsbridge/Cargo.toml index fbe76c8a5..22dca83fa 100644 --- a/pylib/rsbridge/Cargo.toml +++ b/pylib/rsbridge/Cargo.toml @@ -12,6 +12,7 @@ description = "Anki's Rust library code Python bindings" name = "rsbridge" crate-type = ["cdylib"] path = "lib.rs" +test = false [dependencies] anki.workspace = true diff --git a/pylib/rsbridge/build.rs b/pylib/rsbridge/build.rs index d9809be73..2940563cb 100644 --- a/pylib/rsbridge/build.rs +++ b/pylib/rsbridge/build.rs @@ -1,21 +1,33 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -use std::path::Path; - fn main() { // macOS needs special link flags for PyO3 if cfg!(target_os = "macos") { println!("cargo:rustc-link-arg=-undefined"); 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 if cfg!(windows) { - let lib_path = Path::new("../../out/extracted/python/libs") - .canonicalize() - .expect("libs"); - println!("cargo:rustc-link-search={}", lib_path.display()); + use std::process::Command; + + // Run Python to get sysconfig paths + 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); } } diff --git a/pyproject.toml b/pyproject.toml index d88101a1e..f5443e229 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,8 +1,36 @@ -[tool.black] -target-version = ["py39", "py310", "py311", "py312"] -extend-exclude = "qt/bundle" +[project] +name = "anki-dev" +version = "0.0.0" +description = "Local-only environment" +requires-python = ">=3.9" +classifiers = ["Private :: Do Not Upload"] -[tool.pyright] -include = ["pylib/anki", "qt/aqt"] -stubPath = "" -pythonVersion = "3.9" +[dependency-groups] +dev = [ + "black", + "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 diff --git a/python/README.md b/python/README.md deleted file mode 100644 index 574c231e9..000000000 --- a/python/README.md +++ /dev/null @@ -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. diff --git a/python/licenses.json b/python/licenses.json deleted file mode 100644 index 67098b005..000000000 --- a/python/licenses.json +++ /dev/null @@ -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" - } -] diff --git a/python/licenses.sh b/python/licenses.sh deleted file mode 100755 index a7985a429..000000000 --- a/python/licenses.sh +++ /dev/null @@ -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 diff --git a/python/requirements.anki.in b/python/requirements.anki.in deleted file mode 100644 index 45796e6fe..000000000 --- a/python/requirements.anki.in +++ /dev/null @@ -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 diff --git a/python/requirements.aqt.in b/python/requirements.aqt.in deleted file mode 100644 index f18e65513..000000000 --- a/python/requirements.aqt.in +++ /dev/null @@ -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 diff --git a/python/requirements.base.in b/python/requirements.base.in deleted file mode 100644 index 705e50e83..000000000 --- a/python/requirements.base.in +++ /dev/null @@ -1,2 +0,0 @@ -pip-tools -colorama # required on windows diff --git a/python/requirements.base.txt b/python/requirements.base.txt deleted file mode 100644 index 24e3870f9..000000000 --- a/python/requirements.base.txt +++ /dev/null @@ -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 diff --git a/python/requirements.bundle.in b/python/requirements.bundle.in deleted file mode 100644 index 80835ce6f..000000000 --- a/python/requirements.bundle.in +++ /dev/null @@ -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 - diff --git a/python/requirements.dev.in b/python/requirements.dev.in deleted file mode 100644 index c981f127c..000000000 --- a/python/requirements.dev.in +++ /dev/null @@ -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 diff --git a/python/requirements.qt5_14.in b/python/requirements.qt5_14.in deleted file mode 100644 index 0d2b2be9d..000000000 --- a/python/requirements.qt5_14.in +++ /dev/null @@ -1,3 +0,0 @@ -pyqt5==5.14.1 -pyqtwebengine==5.14.0 -pyqt5_sip==12.8.1 diff --git a/python/requirements.qt5_14.txt b/python/requirements.qt5_14.txt deleted file mode 100644 index 6e609f027..000000000 --- a/python/requirements.qt5_14.txt +++ /dev/null @@ -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 diff --git a/python/requirements.qt5_15.in b/python/requirements.qt5_15.in deleted file mode 100644 index 66642166f..000000000 --- a/python/requirements.qt5_15.in +++ /dev/null @@ -1,3 +0,0 @@ -pyqt5==5.15.5 -pyqtwebengine==5.15.5 -pyqt5_sip==12.9.0 diff --git a/python/requirements.qt5_15.txt b/python/requirements.qt5_15.txt deleted file mode 100644 index 8ac1e2225..000000000 --- a/python/requirements.qt5_15.txt +++ /dev/null @@ -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 diff --git a/python/requirements.qt6_6.in b/python/requirements.qt6_6.in deleted file mode 100644 index af94affd8..000000000 --- a/python/requirements.qt6_6.in +++ /dev/null @@ -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 diff --git a/python/requirements.qt6_6.txt b/python/requirements.qt6_6.txt deleted file mode 100644 index 5b1a3ba9b..000000000 --- a/python/requirements.qt6_6.txt +++ /dev/null @@ -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 diff --git a/python/requirements.qt6_8.in b/python/requirements.qt6_8.in deleted file mode 100644 index e16ae92e8..000000000 --- a/python/requirements.qt6_8.in +++ /dev/null @@ -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 diff --git a/python/requirements.qt6_8.txt b/python/requirements.qt6_8.txt deleted file mode 100644 index 21b567ff7..000000000 --- a/python/requirements.qt6_8.txt +++ /dev/null @@ -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 diff --git a/python/requirements.win.in b/python/requirements.win.in deleted file mode 100644 index 24fd9cb64..000000000 --- a/python/requirements.win.in +++ /dev/null @@ -1,2 +0,0 @@ -pywin32 - diff --git a/python/requirements.win.txt b/python/requirements.win.txt deleted file mode 100644 index 65c2b6e3e..000000000 --- a/python/requirements.win.txt +++ /dev/null @@ -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 diff --git a/python/update_python_deps.sh b/python/update_python_deps.sh deleted file mode 100755 index 9040e3456..000000000 --- a/python/update_python_deps.sh +++ /dev/null @@ -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 - - diff --git a/python/update_win_deps.bat b/python/update_win_deps.bat deleted file mode 100644 index 2cee31f3f..000000000 --- a/python/update_win_deps.bat +++ /dev/null @@ -1 +0,0 @@ -..\out\pyenv\scripts\pip-compile --resolver=backtracking --allow-unsafe --no-header --strip-extras --generate-hashes requirements.win.in diff --git a/python/version.py b/python/version.py new file mode 100644 index 000000000..5cf3c4f12 --- /dev/null +++ b/python/version.py @@ -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() diff --git a/python/write_wheel.py b/python/write_wheel.py deleted file mode 100644 index 09899e292..000000000 --- a/python/write_wheel.py +++ /dev/null @@ -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, -) diff --git a/qt/aqt/__init__.py b/qt/aqt/__init__.py index cdbd05ebe..740dcbc9f 100644 --- a/qt/aqt/__init__.py +++ b/qt/aqt/__init__.py @@ -5,10 +5,16 @@ from __future__ import annotations import atexit import logging +import os import sys from collections.abc import Callable 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: import pip_system_certs.wrapt_requests except ModuleNotFoundError: @@ -32,24 +38,14 @@ if "--syncserver" in sys.argv: from anki.syncserver import run_sync_server 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 run_sync_server() -from .package import packaged_build_setup - -packaged_build_setup() - import argparse import builtins import cProfile import getpass import locale -import os import tempfile import traceback from pathlib import Path @@ -270,13 +266,7 @@ def setupLangAndBackend( # load qt translations _qtrans = QTranslator() - if is_mac and getattr(sys, "frozen", False): - 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_dir = QLibraryInfo.path(QLibraryInfo.LibraryPath.TranslationsPath) qt_lang = lang.replace("-", "_") if _qtrans.load(f"qtbase_{qt_lang}", qt_dir): app.installTranslator(_qtrans) @@ -294,7 +284,7 @@ def setupLangAndBackend( class NativeEventFilter(QAbstractNativeEventFilter): def nativeEventFilter( self, eventType: Any, message: Any - ) -> tuple[bool, sip.voidptr | None]: + ) -> tuple[bool, Any | None]: if eventType == "windows_generic_MSG": import ctypes.wintypes @@ -386,6 +376,8 @@ class AnkiApp(QApplication): def onRecv(self) -> None: sock = self._srv.nextPendingConnection() + if sock is None: + return if not sock.waitForReadyRead(self.TMOUT): sys.stderr.write(sock.errorString()) return @@ -416,14 +408,12 @@ class AnkiApp(QApplication): QRadioButton, QMenu, QSlider, - # classes with PyQt5 compatibility proxy - without_qt5_compat_wrapper(QToolButton), - without_qt5_compat_wrapper(QTabBar), + QToolButton, + QTabBar, ) if evt.type() in [QEvent.Type.Enter, QEvent.Type.HoverEnter]: if (isinstance(src, pointer_classes) and src.isEnabled()) or ( - isinstance(src, without_qt5_compat_wrapper(QComboBox)) - and not src.isEditable() + isinstance(src, QComboBox) and not src.isEditable() ): self.setOverrideCursor(QCursor(Qt.CursorShape.PointingHandCursor)) else: @@ -535,15 +525,12 @@ def setupGL(pm: aqt.profiles.ProfileManager) -> None: QQuickWindow.setGraphicsApi(QSGRendererInterface.GraphicsApi.OpenGL) elif driver in (VideoDriver.Software, VideoDriver.ANGLE): if is_win: - # on Windows, this appears to be sufficient on Qt5/Qt6. + # on Windows, this appears to be sufficient # On Qt6, ANGLE is excluded by the enum. os.environ["QT_OPENGL"] = driver.value elif is_mac: QCoreApplication.setAttribute(Qt.ApplicationAttribute.AA_UseSoftwareOpenGL) elif is_lin: - # Qt5 only - os.environ["QT_XCB_FORCE_SOFTWARE_OPENGL"] = "1" - # Required on Qt6 if "QTWEBENGINE_CHROMIUM_FLAGS" not in os.environ: os.environ["QTWEBENGINE_CHROMIUM_FLAGS"] = "--disable-gpu" if qtmajor > 5: @@ -607,14 +594,13 @@ def _run(argv: list[str] | None = None, exec: bool = True) -> AnkiApp | None: profiler = cProfile.Profile() profiler.enable() - packaged = getattr(sys, "frozen", False) x11_available = os.getenv("DISPLAY") wayland_configured = qtmajor > 5 and ( os.getenv("QT_QPA_PLATFORM") == "wayland" or os.getenv("WAYLAND_DISPLAY") ) 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: # Work around broken fractional scaling in Wayland # https://bugreports.qt.io/browse/QTBUG-113574 @@ -674,12 +660,6 @@ def _run(argv: list[str] | None = None, exec: bool = True) -> AnkiApp | None: if is_win and "QT_QPA_PLATFORM" not in os.environ: os.environ["QT_QPA_PLATFORM"] = "windows:altgr" - # Disable sandbox on Qt5 PyPi/packaged builds, as it causes blank screens on modern - # glibc versions. We check for specific patch versions, because distros may have - # fixed the issue in their own Qt builds. - if is_lin and qtfullversion in ([5, 15, 2], [5, 14, 1]): - os.environ["QTWEBENGINE_DISABLE_SANDBOX"] = "1" - # create the app QCoreApplication.setApplicationName("Anki") QGuiApplication.setDesktopFileName("anki") diff --git a/qt/aqt/_macos_helper.py b/qt/aqt/_macos_helper.py index 859cb4b0a..634d94756 100644 --- a/qt/aqt/_macos_helper.py +++ b/qt/aqt/_macos_helper.py @@ -3,55 +3,11 @@ from __future__ import annotations -import os import sys -from collections.abc import Callable -from ctypes import CDLL, CFUNCTYPE, c_bool, c_char_p -import aqt -import aqt.utils - - -class _MacOSHelper: - def __init__(self) -> None: - if getattr(sys, "frozen", False): - 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.system_is_dark.restype = c_bool - - def system_is_dark(self) -> bool: - return self._dll.system_is_dark() - - def set_darkmode_enabled(self, enabled: bool) -> bool: - return self._dll.set_darkmode_enabled(enabled) - - def start_wav_record(self, path: str, on_error: Callable[[str], None]) -> None: - global _on_audio_error - _on_audio_error = on_error - self._dll.start_wav_record(path.encode("utf8"), _audio_error_callback) - - def end_wav_record(self) -> None: - "On completion, file should be saved if no error has arrived." - self._dll.end_wav_record() - - -# this must not be overwritten or deallocated -@CFUNCTYPE(None, c_char_p) # type: ignore -def _audio_error_callback(msg: str) -> None: - if handler := _on_audio_error: - handler(msg) - - -_on_audio_error: Callable[[str], None] | None = None - -macos_helper: _MacOSHelper | None = None if sys.platform == "darwin": - try: - macos_helper = _MacOSHelper() - except Exception as e: - print("macos_helper:", e) + from anki_mac_helper import ( # pylint:disable=unused-import,import-error + macos_helper, + ) +else: + macos_helper = None diff --git a/qt/aqt/browser/browser.py b/qt/aqt/browser/browser.py index a53b21b2d..2ef8d3ed9 100644 --- a/qt/aqt/browser/browser.py +++ b/qt/aqt/browser/browser.py @@ -1139,6 +1139,9 @@ class Browser(QMainWindow): dialog=dialog, ), ) + if key := aqt.mw.pm.get_answer_key(ease): + QShortcut(key, dialog, activated=btn.click) # type: ignore + btn.setToolTip(tr.actions_shortcut_key(key)) layout.addWidget(btn) # Add cancel button diff --git a/qt/aqt/browser/sidebar/model.py b/qt/aqt/browser/sidebar/model.py index 286811aca..fd27926b5 100644 --- a/qt/aqt/browser/sidebar/model.py +++ b/qt/aqt/browser/sidebar/model.py @@ -2,8 +2,6 @@ # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html from __future__ import annotations -from typing import cast - import aqt import aqt.browser from aqt.browser.sidebar.item import SidebarItem @@ -107,11 +105,11 @@ class SidebarModel(QAbstractItemModel): return self.sidebar._on_rename(index.internalPointer(), text) def supportedDropActions(self) -> Qt.DropAction: - return cast(Qt.DropAction, Qt.DropAction.MoveAction) + return Qt.DropAction.MoveAction def flags(self, index: QModelIndex) -> Qt.ItemFlag: if not index.isValid(): - return cast(Qt.ItemFlag, Qt.ItemFlag.ItemIsEnabled) + return Qt.ItemFlag.ItemIsEnabled flags = ( Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsSelectable diff --git a/qt/aqt/browser/table/model.py b/qt/aqt/browser/table/model.py index 5b42c0ca3..e8d3bb7b6 100644 --- a/qt/aqt/browser/table/model.py +++ b/qt/aqt/browser/table/model.py @@ -325,15 +325,13 @@ class DataModel(QAbstractTableModel): return 0 return self.len_columns() - _QFont = without_qt5_compat_wrapper(QFont) - def data(self, index: QModelIndex = QModelIndex(), role: int = 0) -> Any: if not index.isValid(): return QVariant() if role == Qt.ItemDataRole.FontRole: if not self.column_at(index).uses_cell_font: return QVariant() - qfont = self._QFont() + qfont = QFont() row = self.get_row(index) qfont.setFamily(row.font_name) qfont.setPixelSize(row.font_size) diff --git a/qt/aqt/browser/table/table.py b/qt/aqt/browser/table/table.py index f3d543d93..fb921822b 100644 --- a/qt/aqt/browser/table/table.py +++ b/qt/aqt/browser/table/table.py @@ -382,10 +382,7 @@ class Table: hh.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) self._restore_header() qconnect(hh.customContextMenuRequested, self._on_header_context) - if qtmajor == 5: - qconnect(hh.sortIndicatorChanged, self._on_sort_column_changed_qt5) - else: - qconnect(hh.sortIndicatorChanged, self._on_sort_column_changed) + qconnect(hh.sortIndicatorChanged, self._on_sort_column_changed) qconnect(hh.sectionMoved, self._on_column_moved) # Slots @@ -495,12 +492,6 @@ class Table: if checked: self._scroll_to_column(self._model.len_columns() - 1) - def _on_sort_column_changed_qt5(self, section: int, order: int) -> None: - self._on_sort_column_changed( - section, - Qt.SortOrder.AscendingOrder if not order else Qt.SortOrder.DescendingOrder, - ) - def _on_sort_column_changed(self, section: int, order: Qt.SortOrder) -> None: column = self._model.column_at_section(section) sorting = column.sorting_notes if self.is_notes_mode() else column.sorting_cards diff --git a/qt/aqt/errors.py b/qt/aqt/errors.py index 1896affbe..af1036acd 100644 --- a/qt/aqt/errors.py +++ b/qt/aqt/errors.py @@ -165,6 +165,7 @@ _mbox: QMessageBox | None = None class ErrorHandler(QObject): "Catch stderr and write into buffer." + ivl = 100 fatal_error_encountered = False @@ -235,6 +236,8 @@ class ErrorHandler(QObject): if "unable to get local issuer certificate" in error and is_win: showWarning(tr.errors_windows_ssl_updates()) return + if is_chromium_cert_error(error): + return debug_text = supportText() + "\n" + error diff --git a/qt/aqt/forms/about.py b/qt/aqt/forms/about.py index 4faf97fb0..fe66f7da3 100644 --- a/qt/aqt/forms/about.py +++ b/qt/aqt/forms/about.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.about_qt6 import * -else: - from _aqt.forms.about_qt5 import * # type: ignore +from _aqt.forms.about_qt6 import * diff --git a/qt/aqt/forms/addcards.py b/qt/aqt/forms/addcards.py index ae2debe3e..8c501695e 100644 --- a/qt/aqt/forms/addcards.py +++ b/qt/aqt/forms/addcards.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.addcards_qt6 import * -else: - from _aqt.forms.addcards_qt5 import * # type: ignore +from _aqt.forms.addcards_qt6 import * diff --git a/qt/aqt/forms/addfield.py b/qt/aqt/forms/addfield.py index 57c697b4a..a2f9eed74 100644 --- a/qt/aqt/forms/addfield.py +++ b/qt/aqt/forms/addfield.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.addfield_qt6 import * -else: - from _aqt.forms.addfield_qt5 import * # type: ignore +from _aqt.forms.addfield_qt6 import * diff --git a/qt/aqt/forms/addmodel.py b/qt/aqt/forms/addmodel.py index 9a7d06b7e..0af313a45 100644 --- a/qt/aqt/forms/addmodel.py +++ b/qt/aqt/forms/addmodel.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.addmodel_qt6 import * -else: - from _aqt.forms.addmodel_qt5 import * # type: ignore +from _aqt.forms.addmodel_qt6 import * diff --git a/qt/aqt/forms/addonconf.py b/qt/aqt/forms/addonconf.py index cca92b7b9..d78ebb82a 100644 --- a/qt/aqt/forms/addonconf.py +++ b/qt/aqt/forms/addonconf.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.addonconf_qt6 import * -else: - from _aqt.forms.addonconf_qt5 import * # type: ignore +from _aqt.forms.addonconf_qt6 import * diff --git a/qt/aqt/forms/addons.py b/qt/aqt/forms/addons.py index fa00be08b..46d7532b4 100644 --- a/qt/aqt/forms/addons.py +++ b/qt/aqt/forms/addons.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.addons_qt6 import * -else: - from _aqt.forms.addons_qt5 import * # type: ignore +from _aqt.forms.addons_qt6 import * diff --git a/qt/aqt/forms/browser.py b/qt/aqt/forms/browser.py index 403f780c5..70214ba4c 100644 --- a/qt/aqt/forms/browser.py +++ b/qt/aqt/forms/browser.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.browser_qt6 import * -else: - from _aqt.forms.browser_qt5 import * # type: ignore +from _aqt.forms.browser_qt6 import * diff --git a/qt/aqt/forms/browserdisp.py b/qt/aqt/forms/browserdisp.py index 712e5a400..fc745a703 100644 --- a/qt/aqt/forms/browserdisp.py +++ b/qt/aqt/forms/browserdisp.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.browserdisp_qt6 import * -else: - from _aqt.forms.browserdisp_qt5 import * # type: ignore +from _aqt.forms.browserdisp_qt6 import * diff --git a/qt/aqt/forms/browseropts.py b/qt/aqt/forms/browseropts.py index 68602c85c..1ae696033 100644 --- a/qt/aqt/forms/browseropts.py +++ b/qt/aqt/forms/browseropts.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.browseropts_qt6 import * -else: - from _aqt.forms.browseropts_qt5 import * # type: ignore +from _aqt.forms.browseropts_qt6 import * diff --git a/qt/aqt/forms/changemap.py b/qt/aqt/forms/changemap.py index 6028b0d49..b48b49a83 100644 --- a/qt/aqt/forms/changemap.py +++ b/qt/aqt/forms/changemap.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.changemap_qt6 import * -else: - from _aqt.forms.changemap_qt5 import * # type: ignore +from _aqt.forms.changemap_qt6 import * diff --git a/qt/aqt/forms/changemodel.py b/qt/aqt/forms/changemodel.py index 73f7f6095..cd1931af8 100644 --- a/qt/aqt/forms/changemodel.py +++ b/qt/aqt/forms/changemodel.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.changemodel_qt6 import * -else: - from _aqt.forms.changemodel_qt5 import * # type: ignore +from _aqt.forms.changemodel_qt6 import * diff --git a/qt/aqt/forms/clayout_top.py b/qt/aqt/forms/clayout_top.py index 24f78be11..1a76c882a 100644 --- a/qt/aqt/forms/clayout_top.py +++ b/qt/aqt/forms/clayout_top.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.clayout_top_qt6 import * -else: - from _aqt.forms.clayout_top_qt5 import * # type: ignore +from _aqt.forms.clayout_top_qt6 import * diff --git a/qt/aqt/forms/customstudy.py b/qt/aqt/forms/customstudy.py index 393638b2c..3bfad32ac 100644 --- a/qt/aqt/forms/customstudy.py +++ b/qt/aqt/forms/customstudy.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.customstudy_qt6 import * -else: - from _aqt.forms.customstudy_qt5 import * # type: ignore +from _aqt.forms.customstudy_qt6 import * diff --git a/qt/aqt/forms/dconf.py b/qt/aqt/forms/dconf.py index e28db5c31..f39de7077 100644 --- a/qt/aqt/forms/dconf.py +++ b/qt/aqt/forms/dconf.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.dconf_qt6 import * -else: - from _aqt.forms.dconf_qt5 import * # type: ignore +from _aqt.forms.dconf_qt6 import * diff --git a/qt/aqt/forms/debug.py b/qt/aqt/forms/debug.py index 928ba7795..0880c49fc 100644 --- a/qt/aqt/forms/debug.py +++ b/qt/aqt/forms/debug.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.debug_qt6 import * -else: - from _aqt.forms.debug_qt5 import * # type: ignore +from _aqt.forms.debug_qt6 import * diff --git a/qt/aqt/forms/editcurrent.py b/qt/aqt/forms/editcurrent.py index 1281faafe..cfa9ab1d9 100644 --- a/qt/aqt/forms/editcurrent.py +++ b/qt/aqt/forms/editcurrent.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.editcurrent_qt6 import * -else: - from _aqt.forms.editcurrent_qt5 import * # type: ignore +from _aqt.forms.editcurrent_qt6 import * diff --git a/qt/aqt/forms/edithtml.py b/qt/aqt/forms/edithtml.py index 029977705..61b9e0fd2 100644 --- a/qt/aqt/forms/edithtml.py +++ b/qt/aqt/forms/edithtml.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.edithtml_qt6 import * -else: - from _aqt.forms.edithtml_qt5 import * # type: ignore +from _aqt.forms.edithtml_qt6 import * diff --git a/qt/aqt/forms/emptycards.py b/qt/aqt/forms/emptycards.py index 046c7eb3a..1cae290fd 100644 --- a/qt/aqt/forms/emptycards.py +++ b/qt/aqt/forms/emptycards.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.emptycards_qt6 import * -else: - from _aqt.forms.emptycards_qt5 import * # type: ignore +from _aqt.forms.emptycards_qt6 import * diff --git a/qt/aqt/forms/exporting.py b/qt/aqt/forms/exporting.py index 559e50ecd..d09e9cdd9 100644 --- a/qt/aqt/forms/exporting.py +++ b/qt/aqt/forms/exporting.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.exporting_qt6 import * -else: - from _aqt.forms.exporting_qt5 import * # type: ignore +from _aqt.forms.exporting_qt6 import * diff --git a/qt/aqt/forms/fields.py b/qt/aqt/forms/fields.py index fa379be67..cf7a39f75 100644 --- a/qt/aqt/forms/fields.py +++ b/qt/aqt/forms/fields.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.fields_qt6 import * -else: - from _aqt.forms.fields_qt5 import * # type: ignore +from _aqt.forms.fields_qt6 import * diff --git a/qt/aqt/forms/filtered_deck.py b/qt/aqt/forms/filtered_deck.py index 9b9589046..59870f5a0 100644 --- a/qt/aqt/forms/filtered_deck.py +++ b/qt/aqt/forms/filtered_deck.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.filtered_deck_qt6 import * -else: - from _aqt.forms.filtered_deck_qt5 import * # type: ignore +from _aqt.forms.filtered_deck_qt6 import * diff --git a/qt/aqt/forms/finddupes.py b/qt/aqt/forms/finddupes.py index 7bca9c4cd..43ac30549 100644 --- a/qt/aqt/forms/finddupes.py +++ b/qt/aqt/forms/finddupes.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.finddupes_qt6 import * -else: - from _aqt.forms.finddupes_qt5 import * # type: ignore +from _aqt.forms.finddupes_qt6 import * diff --git a/qt/aqt/forms/findreplace.py b/qt/aqt/forms/findreplace.py index 8f82e58fe..65d1f3555 100644 --- a/qt/aqt/forms/findreplace.py +++ b/qt/aqt/forms/findreplace.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.findreplace_qt6 import * -else: - from _aqt.forms.findreplace_qt5 import * # type: ignore +from _aqt.forms.findreplace_qt6 import * diff --git a/qt/aqt/forms/forget.py b/qt/aqt/forms/forget.py index 97425aed8..0d17803df 100644 --- a/qt/aqt/forms/forget.py +++ b/qt/aqt/forms/forget.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.forget_qt6 import * -else: - from _aqt.forms.forget_qt5 import * # type: ignore +from _aqt.forms.forget_qt6 import * diff --git a/qt/aqt/forms/getaddons.py b/qt/aqt/forms/getaddons.py index c47ed27a8..ecb6c23dd 100644 --- a/qt/aqt/forms/getaddons.py +++ b/qt/aqt/forms/getaddons.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.getaddons_qt6 import * -else: - from _aqt.forms.getaddons_qt5 import * # type: ignore +from _aqt.forms.getaddons_qt6 import * diff --git a/qt/aqt/forms/importing.py b/qt/aqt/forms/importing.py index f60b74a4e..39ade97c2 100644 --- a/qt/aqt/forms/importing.py +++ b/qt/aqt/forms/importing.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.importing_qt6 import * -else: - from _aqt.forms.importing_qt5 import * # type: ignore +from _aqt.forms.importing_qt6 import * diff --git a/qt/aqt/forms/main.py b/qt/aqt/forms/main.py index 068804a2d..7ec7107b3 100644 --- a/qt/aqt/forms/main.py +++ b/qt/aqt/forms/main.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.main_qt6 import * -else: - from _aqt.forms.main_qt5 import * # type: ignore +from _aqt.forms.main_qt6 import * diff --git a/qt/aqt/forms/modelopts.py b/qt/aqt/forms/modelopts.py index 0e4770c92..811b1fb7b 100644 --- a/qt/aqt/forms/modelopts.py +++ b/qt/aqt/forms/modelopts.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.modelopts_qt6 import * -else: - from _aqt.forms.modelopts_qt5 import * # type: ignore +from _aqt.forms.modelopts_qt6 import * diff --git a/qt/aqt/forms/models.py b/qt/aqt/forms/models.py index fb0b64e0a..43c75c62a 100644 --- a/qt/aqt/forms/models.py +++ b/qt/aqt/forms/models.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.models_qt6 import * -else: - from _aqt.forms.models_qt5 import * # type: ignore +from _aqt.forms.models_qt6 import * diff --git a/qt/aqt/forms/preferences.py b/qt/aqt/forms/preferences.py index de9fdc989..6fdb0bfd3 100644 --- a/qt/aqt/forms/preferences.py +++ b/qt/aqt/forms/preferences.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.preferences_qt6 import * -else: - from _aqt.forms.preferences_qt5 import * # type: ignore +from _aqt.forms.preferences_qt6 import * diff --git a/qt/aqt/forms/preview.py b/qt/aqt/forms/preview.py index ca938a396..bf735bd39 100644 --- a/qt/aqt/forms/preview.py +++ b/qt/aqt/forms/preview.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.preview_qt6 import * -else: - from _aqt.forms.preview_qt5 import * # type: ignore +from _aqt.forms.preview_qt6 import * diff --git a/qt/aqt/forms/profiles.py b/qt/aqt/forms/profiles.py index c7bcc10e1..7d5b8d6e0 100644 --- a/qt/aqt/forms/profiles.py +++ b/qt/aqt/forms/profiles.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.profiles_qt6 import * -else: - from _aqt.forms.profiles_qt5 import * # type: ignore +from _aqt.forms.profiles_qt6 import * diff --git a/qt/aqt/forms/progress.py b/qt/aqt/forms/progress.py index 47a57ce49..7a2a332d5 100644 --- a/qt/aqt/forms/progress.py +++ b/qt/aqt/forms/progress.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.progress_qt6 import * -else: - from _aqt.forms.progress_qt5 import * # type: ignore +from _aqt.forms.progress_qt6 import * diff --git a/qt/aqt/forms/reposition.py b/qt/aqt/forms/reposition.py index 646abf7c4..cfad6b55a 100644 --- a/qt/aqt/forms/reposition.py +++ b/qt/aqt/forms/reposition.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.reposition_qt6 import * -else: - from _aqt.forms.reposition_qt5 import * # type: ignore +from _aqt.forms.reposition_qt6 import * diff --git a/qt/aqt/forms/setgroup.py b/qt/aqt/forms/setgroup.py index 649e4f75a..617ef3b96 100644 --- a/qt/aqt/forms/setgroup.py +++ b/qt/aqt/forms/setgroup.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.setgroup_qt6 import * -else: - from _aqt.forms.setgroup_qt5 import * # type: ignore +from _aqt.forms.setgroup_qt6 import * diff --git a/qt/aqt/forms/setlang.py b/qt/aqt/forms/setlang.py index bb715ff92..efe14343b 100644 --- a/qt/aqt/forms/setlang.py +++ b/qt/aqt/forms/setlang.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.setlang_qt6 import * -else: - from _aqt.forms.setlang_qt5 import * # type: ignore +from _aqt.forms.setlang_qt6 import * diff --git a/qt/aqt/forms/stats.py b/qt/aqt/forms/stats.py index 212c03345..12b161f4e 100644 --- a/qt/aqt/forms/stats.py +++ b/qt/aqt/forms/stats.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.stats_qt6 import * -else: - from _aqt.forms.stats_qt5 import * # type: ignore +from _aqt.forms.stats_qt6 import * diff --git a/qt/aqt/forms/studydeck.py b/qt/aqt/forms/studydeck.py index b95bc7e87..497ab01cf 100644 --- a/qt/aqt/forms/studydeck.py +++ b/qt/aqt/forms/studydeck.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.studydeck_qt6 import * -else: - from _aqt.forms.studydeck_qt5 import * # type: ignore +from _aqt.forms.studydeck_qt6 import * diff --git a/qt/aqt/forms/synclog.py b/qt/aqt/forms/synclog.py index 97fefe300..ddd08456b 100644 --- a/qt/aqt/forms/synclog.py +++ b/qt/aqt/forms/synclog.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.synclog_qt6 import * -else: - from _aqt.forms.synclog_qt5 import * # type: ignore +from _aqt.forms.synclog_qt6 import * diff --git a/qt/aqt/forms/taglimit.py b/qt/aqt/forms/taglimit.py index 7a4763016..88262c657 100644 --- a/qt/aqt/forms/taglimit.py +++ b/qt/aqt/forms/taglimit.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.taglimit_qt6 import * -else: - from _aqt.forms.taglimit_qt5 import * # type: ignore +from _aqt.forms.taglimit_qt6 import * diff --git a/qt/aqt/forms/template.py b/qt/aqt/forms/template.py index 84f3d2a05..7540d72e0 100644 --- a/qt/aqt/forms/template.py +++ b/qt/aqt/forms/template.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.template_qt6 import * -else: - from _aqt.forms.template_qt5 import * # type: ignore +from _aqt.forms.template_qt6 import * diff --git a/qt/aqt/forms/widgets.py b/qt/aqt/forms/widgets.py index b91f7ae26..07dc11c6c 100644 --- a/qt/aqt/forms/widgets.py +++ b/qt/aqt/forms/widgets.py @@ -1,8 +1 @@ -from typing import TYPE_CHECKING - -from aqt.qt import qtmajor - -if qtmajor > 5 or TYPE_CHECKING: - from _aqt.forms.widgets_qt6 import * -else: - from _aqt.forms.widgets_qt5 import * # type: ignore +from _aqt.forms.widgets_qt6 import * diff --git a/qt/aqt/mediasrv.py b/qt/aqt/mediasrv.py index 0c41f1e8a..2621efb18 100644 --- a/qt/aqt/mediasrv.py +++ b/qt/aqt/mediasrv.py @@ -254,14 +254,8 @@ def _handle_local_file_request(request: LocalFileRequest) -> Response: def _builtin_data(path: str) -> bytes: """Return data from file in aqt/data folder. Path must use forward slash separators.""" - # packaged build? - if getattr(sys, "frozen", False): - 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() + full_path = aqt_data_path() / ".." / path + return full_path.read_bytes() def _handle_builtin_file_request(request: BundledFileRequest) -> Response: diff --git a/qt/aqt/mpv.py b/qt/aqt/mpv.py index 329a95538..e86675e41 100644 --- a/qt/aqt/mpv.py +++ b/qt/aqt/mpv.py @@ -69,6 +69,7 @@ if is_win: # pylint: disable=import-error import pywintypes import win32file # pytype: disable=import-error + import win32job import win32pipe import winerror @@ -131,6 +132,22 @@ class MPVBase: def _start_process(self): """Start the mpv process.""" self._proc = subprocess.Popen(self.argv, env=self.popenEnv) + if is_win: + # Ensure mpv gets terminated if Anki closes abruptly. + self._job = win32job.CreateJobObject(None, "") + extended_info = win32job.QueryInformationJobObject( + self._job, win32job.JobObjectExtendedLimitInformation + ) + extended_info["BasicLimitInformation"][ + "LimitFlags" + ] = win32job.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE + win32job.SetInformationJobObject( + self._job, + win32job.JobObjectExtendedLimitInformation, + extended_info, + ) + handle = self._proc._handle # pylint: disable=no-member + win32job.AssignProcessToJobObject(self._job, handle) def _stop_process(self): """Stop the mpv process.""" @@ -160,7 +177,8 @@ class MPVBase: startup. """ start = time.time() - while self.is_running() and time.time() < start + 10: + timeout = 60 if is_mac else 10 + while self.is_running() and time.time() < start + timeout: time.sleep(0.1) if is_win: # named pipe diff --git a/qt/aqt/overview.py b/qt/aqt/overview.py index da017714f..184a51cf5 100644 --- a/qt/aqt/overview.py +++ b/qt/aqt/overview.py @@ -2,6 +2,7 @@ # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html from __future__ import annotations +import html from collections.abc import Callable from dataclasses import dataclass from typing import Any @@ -197,6 +198,7 @@ class Overview: table=self._table(), ) gui_hooks.overview_will_render_content(self, content) + content.deck = html.escape(content.deck) self.web.stdHtml( self._body % content.__dict__, css=["css/overview.css"], diff --git a/qt/aqt/package.py b/qt/aqt/package.py index f1ee8cd79..a0642fca0 100644 --- a/qt/aqt/package.py +++ b/qt/aqt/package.py @@ -5,93 +5,65 @@ from __future__ import annotations -import os -import sys +import subprocess from pathlib import Path - -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 +from anki.utils import is_mac -def _patch_pkgutil() -> None: - """Teach pkgutil.get_data() how to read files from in-memory resources. +# pylint: disable=unused-import,import-error +def first_run_setup() -> None: + """Code run the first time after install/upgrade. - This is required for jsonschema.""" - import importlib - import pkgutil + Currently, we just import our main libraries and invoke + mpv/lame on macOS, which is slow on the first run, and doing + it this way shows progress being made. + """ - def get_data_custom(package: str, resource: str) -> bytes | None: - 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): + if not is_mac: return - print("Initial setup...") + def _dot(): + print(".", flush=True, end="") - if sys.platform == "win32": - _fix_pywin32() - elif sys.platform == "darwin": - _fix_protobuf_path() + _dot() + import anki.collection - _patch_pkgutil() - _patch_certifi() + _dot() + import PyQt6.sip - # escape hatch for debugging issues with packaged build startup - if os.getenv("ANKI_STARTUP_REPL"): - # mypy incorrectly thinks this does not exist on Windows - is_tty = os.isatty(sys.stdin.fileno()) # type: ignore - if is_tty: - import code + _dot() + import PyQt6.QtCore - code.InteractiveConsole().interact() - sys.exit(0) + _dot() + 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() diff --git a/qt/aqt/profiles.py b/qt/aqt/profiles.py index 273e6df3a..6597f6705 100644 --- a/qt/aqt/profiles.py +++ b/qt/aqt/profiles.py @@ -189,11 +189,8 @@ class ProfileManager: # return the bytes directly return args[0] elif name == "_unpickle_enum": - if qtmajor == 5: - return sip._unpickle_enum(module, klass, args) # type: ignore - else: - # old style enums can't be unpickled - return None + # old style enums can't be unpickled + return None else: return sip._unpickle_type(module, klass, args) # type: ignore diff --git a/qt/aqt/progress.py b/qt/aqt/progress.py index fbb0a7470..8c45c44ee 100644 --- a/qt/aqt/progress.py +++ b/qt/aqt/progress.py @@ -300,8 +300,7 @@ class ProgressManager: def _closeWin(self) -> None: # if the parent window has been deleted, the progress dialog may have # already been dropped; delete it if it hasn't been - if not sip.isdeleted(self._win): - assert self._win is not None + if self._win and not sip.isdeleted(self._win): self._win.cancel() self._win = None self._shown = 0 diff --git a/qt/aqt/qt/__init__.py b/qt/aqt/qt/__init__.py index ea1b4bd46..11670e90c 100644 --- a/qt/aqt/qt/__init__.py +++ b/qt/aqt/qt/__init__.py @@ -11,20 +11,12 @@ import traceback from collections.abc import Callable from typing import TypeVar, Union -try: - import PyQt6 -except Exception: - from .qt5 import * # type: ignore -else: - if os.getenv("ENABLE_QT5_COMPAT"): - print("Running with temporary Qt5 compatibility shims.") - from . import qt5_compat # needs to be imported first - from .qt6 import * +from anki._legacy import deprecated +# legacy code depends on these re-exports from anki.utils import is_mac, is_win -# fix buggy ubuntu12.04 display of language selector -os.environ["LIBOVERLAY_SCROLLBAR"] = "0" +from .qt6 import * def debug() -> None: @@ -52,7 +44,7 @@ qtminor = _version.minorVersion() qtpoint = _version.microVersion() qtfullversion = _version.segments() -if qtmajor < 5 or (qtmajor == 5 and qtminor < 14): +if qtmajor == 6 and qtminor < 2: raise Exception("Anki does not support your Qt version.") @@ -64,11 +56,6 @@ def qconnect(signal: Callable | pyqtSignal | pyqtBoundSignal, func: Callable) -> _T = TypeVar("_T") +@deprecated(info="no longer required, and now a no-op") def without_qt5_compat_wrapper(cls: _T) -> _T: - """Remove Qt5 compat wrapper from Qt class, if active. - - Only needed for a few Qt APIs that deal with QVariants.""" - if fn := getattr(cls, "_without_compat_wrapper", None): - return fn() - else: - return cls + return cls diff --git a/qt/aqt/qt/qt5.py b/qt/aqt/qt/qt5.py deleted file mode 100644 index 0a45dffb9..000000000 --- a/qt/aqt/qt/qt5.py +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright: Ankitects Pty Ltd and contributors -# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html - -# make sure not to optimize imports on this file -# pylint: skip-file - -""" -PyQt5 imports -""" - -from PyQt5.QtCore import * # type: ignore -from PyQt5.QtGui import * # type: ignore -from PyQt5.QtNetwork import QLocalServer, QLocalSocket, QNetworkProxy # type: ignore -from PyQt5.QtWebChannel import QWebChannel # type: ignore -from PyQt5.QtWebEngineCore import * # type: ignore -from PyQt5.QtWebEngineWidgets import * # type: ignore -from PyQt5.QtWidgets import * # type: ignore - -try: - from PyQt5 import sip # type: ignore -except ImportError: - import sip # type: ignore diff --git a/qt/aqt/qt/qt5_audio.py b/qt/aqt/qt/qt5_audio.py deleted file mode 100644 index cc8426a6e..000000000 --- a/qt/aqt/qt/qt5_audio.py +++ /dev/null @@ -1,99 +0,0 @@ -# Copyright: Ankitects Pty Ltd and contributors -# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html - -# pylint: skip-file - -""" -PyQt5-only audio code -""" - -import wave -from collections.abc import Callable -from concurrent.futures import Future -from typing import cast - -import aqt - -from . import * # isort:skip -from ..sound import Recorder # isort:skip -from ..utils import showWarning # isort:skip - - -class QtAudioInputRecorder(Recorder): - def __init__(self, output_path: str, mw: aqt.AnkiQt, parent: QWidget) -> None: - super().__init__(output_path) - - self.mw = mw - self._parent = parent - - from PyQt5.QtMultimedia import ( # type: ignore - QAudioDeviceInfo, - QAudioFormat, - QAudioInput, - ) - - format = QAudioFormat() - format.setChannelCount(1) - format.setSampleRate(44100) - format.setSampleSize(16) - format.setCodec("audio/pcm") - format.setByteOrder(QAudioFormat.LittleEndian) - format.setSampleType(QAudioFormat.SignedInt) - - device = QAudioDeviceInfo.defaultInputDevice() - if not device.isFormatSupported(format): - format = device.nearestFormat(format) - print("format changed") - print("channels", format.channelCount()) - print("rate", format.sampleRate()) - print("size", format.sampleSize()) - self._format = format - - self._audio_input = QAudioInput(device, format, parent) - - def start(self, on_done: Callable[[], None]) -> None: - self._iodevice = self._audio_input.start() - self._buffer = bytearray() - qconnect(self._iodevice.readyRead, self._on_read_ready) - super().start(on_done) - - def _on_read_ready(self) -> None: - self._buffer.extend(cast(bytes, self._iodevice.readAll())) - - def stop(self, on_done: Callable[[str], None]) -> None: - def on_stop_timer() -> None: - # read anything remaining in buffer & stop - self._on_read_ready() - self._audio_input.stop() - - if err := self._audio_input.error(): - showWarning(f"recording failed: {err}") - return - - def write_file() -> None: - # swallow the first 300ms to allow audio device to quiesce - wait = int(44100 * self.STARTUP_DELAY) - if len(self._buffer) <= wait: - return - self._buffer = self._buffer[wait:] - - # write out the wave file - wf = wave.open(self.output_path, "wb") - wf.setnchannels(self._format.channelCount()) - wf.setsampwidth(self._format.sampleSize() // 8) - wf.setframerate(self._format.sampleRate()) - wf.writeframes(self._buffer) - wf.close() - - def and_then(fut: Future) -> None: - fut.result() - Recorder.stop(self, on_done) - - self.mw.taskman.run_in_background(write_file, and_then) - - # schedule the stop for half a second in the future, - # to avoid truncating the end of the recording - self._stop_timer = t = QTimer(self._parent) - t.timeout.connect(on_stop_timer) # type: ignore - t.setSingleShot(True) - t.start(500) diff --git a/qt/aqt/qt/qt5_compat.py b/qt/aqt/qt/qt5_compat.py deleted file mode 100644 index ef281b87c..000000000 --- a/qt/aqt/qt/qt5_compat.py +++ /dev/null @@ -1,411 +0,0 @@ -# Copyright: Ankitects Pty Ltd and contributors -# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html - -# type: ignore -# pylint: disable=unused-import - -""" -Patches and aliases that provide a PyQt5 → PyQt6 compatibility shim for add-ons -""" - -import sys -import types -import typing - -import PyQt6.QtCore -import PyQt6.QtDBus -import PyQt6.QtGui -import PyQt6.QtNetwork -import PyQt6.QtPrintSupport -import PyQt6.QtWebChannel -import PyQt6.QtWebEngineCore -import PyQt6.QtWebEngineWidgets -import PyQt6.QtWidgets - -from anki._legacy import print_deprecation_warning - -# Globally alias PyQt5 to PyQt6 -# ######################################################################### - -sys.modules["PyQt5"] = PyQt6 -# Need to alias QtCore explicitly as sip otherwise complains about repeat registration -sys.modules["PyQt5.QtCore"] = PyQt6.QtCore -# Need to alias QtWidgets and QtGui explicitly to facilitate patches -sys.modules["PyQt5.QtGui"] = PyQt6.QtGui -sys.modules["PyQt5.QtWidgets"] = PyQt6.QtWidgets -# Needed to maintain import order between QtWebEngineWidgets and QCoreApplication: -sys.modules["PyQt5.QtWebEngineWidgets"] = PyQt6.QtWebEngineWidgets -# Register other aliased top-level Qt modules just to be safe: -sys.modules["PyQt5.QtWebEngineCore"] = PyQt6.QtWebEngineCore -sys.modules["PyQt5.QtWebChannel"] = PyQt6.QtWebChannel -sys.modules["PyQt5.QtNetwork"] = PyQt6.QtNetwork -# Alias sip -sys.modules["sip"] = PyQt6.sip - -# Restore QWebEnginePage.view() -# ######################################################################## - -from PyQt6.QtWebEngineCore import QWebEnginePage -from PyQt6.QtWebEngineWidgets import QWebEngineView - - -def qwebenginepage_view(page: QWebEnginePage) -> QWebEnginePage: - print_deprecation_warning( - "'QWebEnginePage.view()' is deprecated. " - "Please use 'QWebEngineView.forPage(page)'" - ) - return QWebEngineView.forPage(page) - - -PyQt6.QtWebEngineCore.QWebEnginePage.view = qwebenginepage_view - -# Alias removed exec_ methods to exec -# ######################################################################## - -from PyQt6.QtCore import QCoreApplication, QEventLoop, QThread -from PyQt6.QtGui import QDrag, QGuiApplication -from PyQt6.QtWidgets import QApplication, QDialog, QMenu - - -# This helper function is needed as aliasing exec_ to exec directly will cause -# an unbound method error, even when wrapped with types.MethodType -def qt_exec_(object, *args, **kwargs): - class_name = object.__class__.__name__ - print_deprecation_warning( - f"'{class_name}.exec_()' is deprecated. Please use '{class_name}.exec()'" - ) - return object.exec(*args, **kwargs) - - -QCoreApplication.exec_ = qt_exec_ -QEventLoop.exec_ = qt_exec_ -QThread.exec_ = qt_exec_ -QDrag.exec_ = qt_exec_ -QGuiApplication.exec_ = qt_exec_ -QApplication.exec_ = qt_exec_ -QDialog.exec_ = qt_exec_ -QMenu.exec_ = qt_exec_ - -# Graciously handle removed Qt resource system -# ######################################################################## - -# Given that add-ons mostly use the Qt resource system to equip UI elements with -# icons – which oftentimes are not essential to the core UX –, printing a warning -# instead of preventing the add-on from loading seems appropriate. - - -def qt_resource_system_call(*args, **kwargs): - print_deprecation_warning( - "The Qt resource system no longer works on PyQt6. " - "Use QDir.addSearchPath() or mw.addonManager.setWebExports() instead." - ) - - -PyQt6.QtCore.qRegisterResourceData = qt_resource_system_call -PyQt6.QtCore.qUnregisterResourceData = qt_resource_system_call - -# Patch unscoped enums back in, aliasing them to scoped enums -# ######################################################################## - -PyQt6.QtWidgets.QDockWidget.AllDockWidgetFeatures = ( - PyQt6.QtWidgets.QDockWidget.DockWidgetFeature.DockWidgetClosable - | PyQt6.QtWidgets.QDockWidget.DockWidgetFeature.DockWidgetMovable - | PyQt6.QtWidgets.QDockWidget.DockWidgetFeature.DockWidgetFloatable -) - -# when we subclass QIcon, icons fail to show when returned by getData() -# in a tableview/treeview, so we need to manually alias these -PyQt6.QtGui.QIcon.Active = PyQt6.QtGui.QIcon.Mode.Active -PyQt6.QtGui.QIcon.Disabled = PyQt6.QtGui.QIcon.Mode.Disabled -PyQt6.QtGui.QIcon.Normal = PyQt6.QtGui.QIcon.Mode.Normal -PyQt6.QtGui.QIcon.Selected = PyQt6.QtGui.QIcon.Mode.Selected -PyQt6.QtGui.QIcon.Off = PyQt6.QtGui.QIcon.State.Off -PyQt6.QtGui.QIcon.On = PyQt6.QtGui.QIcon.State.On - -# This is the subset of enums used in all public Anki add-ons as of 2021-10-19. -# Please note that this list is likely to be incomplete as the process used to -# find them probably missed dynamically constructed enums. -# Also, as mostly only public Anki add-ons were taken into consideration, -# some enums in other add-ons might not be included. In those cases please -# consider filing a PR to extend the assignments below. - -# Important: These patches are not meant to provide compatibility for all -# add-ons going forward, but simply to maintain support with already -# existing add-ons. Add-on authors should take heed to use scoped enums -# in any future code changes. - -# (module, [(type_name, enums)]) -_enum_map = ( - ( - PyQt6.QtCore, - [ - ("QEvent", ("Type",)), - ("QEventLoop", ("ProcessEventsFlag",)), - ("QIODevice", ("OpenModeFlag",)), - ("QItemSelectionModel", ("SelectionFlag",)), - ("QLocale", ("Country", "Language")), - ("QMetaType", ("Type",)), - ("QProcess", ("ProcessState", "ProcessChannel")), - ("QStandardPaths", ("StandardLocation",)), - ( - "Qt", - ( - "AlignmentFlag", - "ApplicationAttribute", - "ArrowType", - "AspectRatioMode", - "BrushStyle", - "CaseSensitivity", - "CheckState", - "ConnectionType", - "ContextMenuPolicy", - "CursorShape", - "DateFormat", - "DayOfWeek", - "DockWidgetArea", - "FindChildOption", - "FocusPolicy", - "FocusReason", - "GlobalColor", - "HighDpiScaleFactorRoundingPolicy", - "ImageConversionFlag", - "InputMethodHint", - "ItemDataRole", - "ItemFlag", - "KeyboardModifier", - "LayoutDirection", - "MatchFlag", - "Modifier", - "MouseButton", - "Orientation", - "PenCapStyle", - "PenJoinStyle", - "PenStyle", - "ScrollBarPolicy", - "ShortcutContext", - "SortOrder", - "TextElideMode", - "TextFlag", - "TextFormat", - "TextInteractionFlag", - "ToolBarArea", - "ToolButtonStyle", - "TransformationMode", - "WidgetAttribute", - "WindowModality", - "WindowState", - "WindowType", - "Key", - ), - ), - ("QThread", ("Priority",)), - ], - ), - (PyQt6.QtDBus, [("QDBus", ("CallMode",))]), - ( - PyQt6.QtGui, - [ - ("QAction", ("MenuRole", "ActionEvent")), - ("QClipboard", ("Mode",)), - ("QColor", ("NameFormat",)), - ("QFont", ("Style", "Weight", "StyleHint")), - ("QFontDatabase", ("WritingSystem", "SystemFont")), - ("QImage", ("Format",)), - ("QKeySequence", ("SequenceFormat", "StandardKey")), - ("QMovie", ("CacheMode",)), - ("QPageLayout", ("Orientation",)), - ("QPageSize", ("PageSizeId",)), - ("QPainter", ("RenderHint",)), - ("QPalette", ("ColorRole", "ColorGroup")), - ("QTextCharFormat", ("UnderlineStyle",)), - ("QTextCursor", ("MoveOperation", "MoveMode", "SelectionType")), - ("QTextFormat", ("Property",)), - ("QTextOption", ("WrapMode",)), - ("QValidator", ("State",)), - ], - ), - (PyQt6.QtNetwork, [("QHostAddress", ("SpecialAddress",))]), - (PyQt6.QtPrintSupport, [("QPrinter", ("Unit",))]), - ( - PyQt6.QtWebEngineCore, - [ - ("QWebEnginePage", ("WebWindowType", "FindFlag", "WebAction")), - ("QWebEngineProfile", ("PersistentCookiesPolicy", "HttpCacheType")), - ("QWebEngineScript", ("ScriptWorldId", "InjectionPoint")), - ("QWebEngineSettings", ("FontSize", "WebAttribute")), - ], - ), - ( - PyQt6.QtWidgets, - [ - ( - "QAbstractItemView", - ( - "CursorAction", - "DropIndicatorPosition", - "ScrollMode", - "EditTrigger", - "SelectionMode", - "SelectionBehavior", - "DragDropMode", - "ScrollHint", - ), - ), - ("QAbstractScrollArea", ("SizeAdjustPolicy",)), - ("QAbstractSpinBox", ("ButtonSymbols",)), - ("QBoxLayout", ("Direction",)), - ("QColorDialog", ("ColorDialogOption",)), - ("QComboBox", ("SizeAdjustPolicy", "InsertPolicy")), - ("QCompleter", ("CompletionMode",)), - ("QDateTimeEdit", ("Section",)), - ("QDialog", ("DialogCode",)), - ("QDialogButtonBox", ("StandardButton", "ButtonRole")), - ("QDockWidget", ("DockWidgetFeature",)), - ("QFileDialog", ("Option", "FileMode", "AcceptMode", "DialogLabel")), - ("QFormLayout", ("FieldGrowthPolicy", "ItemRole")), - ("QFrame", ("Shape", "Shadow")), - ("QGraphicsItem", ("GraphicsItemFlag",)), - ("QGraphicsPixmapItem", ("ShapeMode",)), - ("QGraphicsView", ("ViewportAnchor", "DragMode")), - ("QHeaderView", ("ResizeMode",)), - ("QLayout", ("SizeConstraint",)), - ("QLineEdit", ("EchoMode",)), - ( - "QListView", - ("Flow", "BrowserLayout", "ResizeMode", "Movement", "ViewMode"), - ), - ("QListWidgetItem", ("ItemType",)), - ("QMessageBox", ("StandardButton", "Icon", "ButtonRole")), - ("QPlainTextEdit", ("LineWrapMode",)), - ("QProgressBar", ("Direction",)), - ("QRubberBand", ("Shape",)), - ("QSizePolicy", ("ControlType", "Policy")), - ("QSlider", ("TickPosition",)), - ( - "QStyle", - ( - "SubElement", - "ComplexControl", - "StandardPixmap", - "ControlElement", - "PixelMetric", - "StateFlag", - "SubControl", - ), - ), - ("QSystemTrayIcon", ("MessageIcon", "ActivationReason")), - ("QTabBar", ("ButtonPosition",)), - ("QTabWidget", ("TabShape", "TabPosition")), - ("QTextEdit", ("LineWrapMode",)), - ("QToolButton", ("ToolButtonPopupMode",)), - ("QWizard", ("WizardStyle", "WizardOption")), - ], - ), -) - -_renamed_enum_cases = { - "QComboBox": { - "AdjustToMinimumContentsLength": "AdjustToMinimumContentsLengthWithIcon" - }, - "QDialogButtonBox": {"No": "NoButton"}, - "QPainter": {"HighQualityAntialiasing": "Antialiasing"}, - "QPalette": {"Background": "Window", "Foreground": "WindowText"}, - "Qt": {"MatchRegExp": "MatchRegularExpression", "MidButton": "MiddleButton"}, -} - - -# This works by wrapping each enum-containing Qt class (eg QAction) in a proxy. -# When an attribute is missing from the underlying Qt class, __getattr__ is -# called, and we try fetching the attribute from each of the declared enums -# for that module. If a match is found, a deprecation warning is printed. -# -# Looping through enumerations is not particularly efficient on a large type like -# Qt, but we only pay the cost when an attribute is not found. In the worst case, -# it's about 50ms per 1000 failed lookups on the Qt module. - - -def _instrument_type( - module: types.ModuleType, type_name: str, enums: list[str] -) -> None: - type = getattr(module, type_name) - renamed_attrs = _renamed_enum_cases.get(type_name, {}) - - class QtClassProxyType(type.__class__): - def __getattr__(cls, provided_name): # pylint: disable=no-self-argument - # we know this is not an enum - if provided_name == "__pyqtSignature__": - raise AttributeError - - name = renamed_attrs.get(provided_name) or provided_name - - for enum_name in enums: - enum = getattr(type, enum_name) - try: - val = getattr(enum, name) - except AttributeError: - continue - - print_deprecation_warning( - f"'{type_name}.{provided_name}' will stop working. Please use '{type_name}.{enum_name}.{name}' instead." - ) - return val - - return getattr(type, name) - - class QtClassProxy( - type, metaclass=QtClassProxyType - ): # pylint: disable=invalid-metaclass - @staticmethod - def _without_compat_wrapper(): - return type - - setattr(module, type_name, QtClassProxy) - - -for module, type_to_enum_list in _enum_map: - for type_name, enums in type_to_enum_list: - _instrument_type(module, type_name, enums) - -# Alias classes shifted between QtWidgets and QtGui -########################################################################## - -PyQt6.QtWidgets.QAction = PyQt6.QtGui.QAction -PyQt6.QtWidgets.QActionGroup = PyQt6.QtGui.QActionGroup -PyQt6.QtWidgets.QShortcut = PyQt6.QtGui.QShortcut - -# Alias classes shifted between QtWebEngineWidgets and QtWebEngineCore -########################################################################## - -PyQt6.QtWebEngineWidgets.QWebEnginePage = PyQt6.QtWebEngineCore.QWebEnginePage -PyQt6.QtWebEngineWidgets.QWebEngineHistory = PyQt6.QtWebEngineCore.QWebEngineHistory -PyQt6.QtWebEngineWidgets.QWebEngineProfile = PyQt6.QtWebEngineCore.QWebEngineProfile -PyQt6.QtWebEngineWidgets.QWebEngineScript = PyQt6.QtWebEngineCore.QWebEngineScript -PyQt6.QtWebEngineWidgets.QWebEngineScriptCollection = ( - PyQt6.QtWebEngineCore.QWebEngineScriptCollection -) -PyQt6.QtWebEngineWidgets.QWebEngineClientCertificateSelection = ( - PyQt6.QtWebEngineCore.QWebEngineClientCertificateSelection -) -PyQt6.QtWebEngineWidgets.QWebEngineSettings = PyQt6.QtWebEngineCore.QWebEngineSettings -PyQt6.QtWebEngineWidgets.QWebEngineFullScreenRequest = ( - PyQt6.QtWebEngineCore.QWebEngineFullScreenRequest -) -PyQt6.QtWebEngineWidgets.QWebEngineContextMenuData = ( - PyQt6.QtWebEngineCore.QWebEngineContextMenuRequest -) -PyQt6.QtWebEngineWidgets.QWebEngineDownloadItem = ( - PyQt6.QtWebEngineCore.QWebEngineDownloadRequest -) - -# Aliases for other miscellaneous class changes -########################################################################## - -PyQt6.QtCore.QRegExp = PyQt6.QtCore.QRegularExpression - - -# Mock the removed PyQt5.Qt module -########################################################################## - -sys.modules["PyQt5.Qt"] = sys.modules["aqt.qt"] -# support 'from PyQt5 import Qt', as it's an alias to PyQt6 -PyQt6.Qt = sys.modules["aqt.qt"] diff --git a/qt/aqt/qt/qt6.py b/qt/aqt/qt/qt6.py index df79d6b1a..2d387aabf 100644 --- a/qt/aqt/qt/qt6.py +++ b/qt/aqt/qt/qt6.py @@ -12,7 +12,7 @@ from PyQt6 import sip from PyQt6.QtCore import * # 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.QtQuick import * from PyQt6.QtWebChannel import QWebChannel diff --git a/qt/aqt/reviewer.py b/qt/aqt/reviewer.py index 05e9becf4..6e34a7931 100644 --- a/qt/aqt/reviewer.py +++ b/qt/aqt/reviewer.py @@ -18,7 +18,10 @@ import aqt.operations from anki.cards import Card, CardId from anki.collection import Config, OpChanges, OpChangesWithCount 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 ( SchedulingContext, diff --git a/qt/aqt/sound.py b/qt/aqt/sound.py index 11f957a84..8ff49024f 100644 --- a/qt/aqt/sound.py +++ b/qt/aqt/sound.py @@ -4,6 +4,7 @@ from __future__ import annotations import os +import os.path import platform import re import subprocess @@ -23,7 +24,6 @@ from markdown import markdown import aqt import aqt.mpv import aqt.qt -from anki import hooks from anki.cards import Card from anki.sound import AV_REF_RE, AVTag, SoundOrVideoTag from anki.utils import is_lin, is_mac, is_win, namedtmp @@ -177,15 +177,27 @@ class AVPlayer: self._stop_if_playing() def play_file(self, filename: str) -> None: + """Play the provided path. + + SECURITY: Filename may be an arbitrary path. For filenames coming from a collection, + you should only ever use the os.path.basename(filename) as the filename.""" self.play_tags([SoundOrVideoTag(filename=filename)]) def play_file_with_caller(self, filename: str, caller: Any) -> None: + """Play the provided path, noting down the caller. + + SECURITY: Filename may be an arbitrary path. For filenames coming from a collection, + you should only ever use the os.path.basename(filename) as the filename.""" if self.current_caller: self.current_caller_interrupted = True self.current_caller = caller self.play_file(filename) def insert_file(self, filename: str) -> None: + """Place the provided path at the top of the playlist. + + SECURITY: Filename may be an arbitrary path. For filenames coming from a collection, + you should only ever use the os.path.basename(filename) as the filename.""" self._enqueued.insert(0, SoundOrVideoTag(filename=filename)) self._play_next_if_idle() @@ -267,12 +279,25 @@ def _packagedCmd(cmd: list[str]) -> tuple[Any, dict[str, str]]: if "LD_LIBRARY_PATH" in env and "SNAP" not in env: del env["LD_LIBRARY_PATH"] - if is_win: - packaged_path = Path(sys.prefix) / (cmd[0] + ".exe") - elif is_mac: - packaged_path = Path(sys.prefix) / ".." / "Resources" / cmd[0] - else: - packaged_path = Path(sys.prefix) / cmd[0] + # Try to find binary in anki-audio package for Windows/Mac + if is_win or is_mac: + try: + import anki_audio + + 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(): cmd[0] = str(packaged_path) @@ -327,7 +352,7 @@ class SimpleProcessPlayer(Player): # pylint: disable=abstract-method def _play(self, tag: AVTag) -> None: assert isinstance(tag, SoundOrVideoTag) self._process = subprocess.Popen( - self.args + ["--", tag.filename], + self.args + ["--", tag.path(self._media_folder)], env=self.env, cwd=self._media_folder, stdout=subprocess.DEVNULL, @@ -453,8 +478,7 @@ class MpvManager(MPV, SoundOrVideoPlayer): def play(self, tag: AVTag, on_done: OnDoneCallback) -> None: assert isinstance(tag, SoundOrVideoTag) self._on_done = on_done - filename = hooks.media_file_filter(tag.filename) - path = os.path.join(self.media_folder, filename) + path = tag.path(self.media_folder) if self.mpv_version is None or self.mpv_version >= (0, 38, 0): self.command("loadfile", path, "replace", -1, "pause=no") @@ -506,10 +530,8 @@ class SimpleMplayerSlaveModePlayer(SimpleMplayerPlayer): def _play(self, tag: AVTag) -> None: assert isinstance(tag, SoundOrVideoTag) - filename = hooks.media_file_filter(tag.filename) - self._process = subprocess.Popen( - self.args + ["--", filename], + self.args + ["--", tag.path(self.media_folder)], env=self.env, cwd=self.media_folder, stdin=subprocess.PIPE, @@ -750,19 +772,14 @@ class RecordDialog(QDialog): saveGeom(self, "audioRecorder2") def _start_recording(self) -> None: - if qtmajor > 5: - if macos_helper and platform.machine() == "arm64": - self._recorder = NativeMacRecorder( - namedtmp("rec.wav"), - ) - else: - self._recorder = QtAudioInputRecorder( - namedtmp("rec.wav"), self.mw, self._parent - ) + if macos_helper and platform.machine() == "arm64": + self._recorder = NativeMacRecorder( + namedtmp("rec.wav"), + ) else: - from aqt.qt.qt5_audio import QtAudioInputRecorder as Qt5Recorder - - self._recorder = Qt5Recorder(namedtmp("rec.wav"), self.mw, self._parent) + self._recorder = QtAudioInputRecorder( + namedtmp("rec.wav"), self.mw, self._parent + ) self._recorder.start(self._start_timer) def _start_timer(self) -> None: diff --git a/qt/aqt/stylesheets.py b/qt/aqt/stylesheets.py index 35e47ef0d..0721e76d2 100644 --- a/qt/aqt/stylesheets.py +++ b/qt/aqt/stylesheets.py @@ -120,7 +120,7 @@ class CustomStyles: QLabel: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: @@ -404,18 +404,18 @@ class CustomStyles: }; }} 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)}; }} QHeaderView::section:!first {{ border-left: none; }} 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)}; }} 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-top-left-radius: {tm.var(props.BORDER_RADIUS)}; border-top-right-radius: {tm.var(props.BORDER_RADIUS)}; @@ -579,19 +579,19 @@ class CustomStyles: }} QScrollBar::handle:pressed {{ background-color: {tm.var(colors.SCROLLBAR_BG_ACTIVE)}; - }} + }} QScrollBar:horizontal {{ height: 12px; }} QScrollBar::handle:horizontal {{ min-width: 60px; - }} + }} QScrollBar:vertical {{ width: 12px; }} QScrollBar::handle:vertical {{ min-height: 60px; - }} + }} QScrollBar::add-line {{ border: none; background: none; diff --git a/qt/aqt/update.py b/qt/aqt/update.py index fd0e4eafd..d8e92426c 100644 --- a/qt/aqt/update.py +++ b/qt/aqt/update.py @@ -1,13 +1,16 @@ # Copyright: Ankitects Pty Ltd and contributors # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +import os +from pathlib import Path + import aqt from anki.buildinfo import buildhash from anki.collection import CheckForUpdateResponse, Collection -from anki.utils import dev_mode, int_time, int_version, plat_desc +from anki.utils import dev_mode, int_time, int_version, is_mac, is_win, plat_desc from aqt.operations import QueryOp from aqt.qt import * -from aqt.utils import openLink, show_warning, showText, tr +from aqt.utils import show_info, show_warning, showText, tr def check_for_update() -> None: @@ -77,4 +80,33 @@ def prompt_to_update(mw: aqt.AnkiQt, ver: str) -> None: # ignore this update mw.pm.meta["suppressUpdate"] = ver elif ret == QMessageBox.StandardButton.Yes: - openLink(aqt.appWebsiteDownloadSection) + update_and_restart() + + +def update_and_restart() -> None: + """Download and install the update, then restart Anki.""" + update_on_next_run() + # todo: do this automatically in the future + show_info(tr.qt_misc_please_restart_to_update_anki()) + + +def update_on_next_run() -> None: + """Bump the mtime on pyproject.toml in the local data directory to trigger an update on next run.""" + try: + # Get the local data directory equivalent to Rust's dirs::data_local_dir() + if is_win: + data_dir = Path(os.environ.get("LOCALAPPDATA", "")) + elif is_mac: + data_dir = Path.home() / "Library" / "Application Support" + else: # Linux + data_dir = Path( + os.environ.get("XDG_DATA_HOME", Path.home() / ".local" / "share") + ) + + pyproject_path = data_dir / "AnkiProgramFiles" / "pyproject.toml" + + if pyproject_path.exists(): + # Touch the file to update its mtime + pyproject_path.touch() + except Exception as e: + print(e) diff --git a/qt/aqt/utils.py b/qt/aqt/utils.py index 44e2557c5..097030533 100644 --- a/qt/aqt/utils.py +++ b/qt/aqt/utils.py @@ -90,24 +90,15 @@ if TYPE_CHECKING: def aqt_data_path() -> Path: - # packaged? - 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 + import _aqt.colors - data_folder = Path(inspect.getfile(_aqt.colors)).with_name("data") - if data_folder.exists(): - return data_folder.absolute() - else: - # should only happen when running unit tests - print("warning, data folder not found") - return Path(".") + data_folder = Path(inspect.getfile(_aqt.colors)).with_name("data") + if data_folder.exists(): + return data_folder.absolute() + else: + # should only happen when running unit tests + print("warning, data folder not found") + return Path(".") def aqt_data_folder() -> str: @@ -1253,12 +1244,11 @@ def supportText() -> str: platname = platform.platform() return """\ -Anki {} {} {} +Anki {} {} Python {} Qt {} PyQt {} Platform: {} """.format( version_with_build(), - "(src)" if not getattr(sys, "frozen", False) else "", "(ao)" if mw.addonManager.dirty else "", platform.python_version(), qVersion(), diff --git a/qt/bundle/Cargo.lock b/qt/bundle/Cargo.lock deleted file mode 100644 index 544276d6f..000000000 --- a/qt/bundle/Cargo.lock +++ /dev/null @@ -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", -] diff --git a/qt/bundle/Cargo.toml b/qt/bundle/Cargo.toml deleted file mode 100644 index 920e890d0..000000000 --- a/qt/bundle/Cargo.toml +++ /dev/null @@ -1,60 +0,0 @@ -[package] -name = "anki" -version = "0.0.0" -authors = ["Ankitects Pty Ltd and contributors "] -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 diff --git a/qt/bundle/PyOxidizer b/qt/bundle/PyOxidizer deleted file mode 160000 index 12a249f68..000000000 --- a/qt/bundle/PyOxidizer +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 12a249f686484c5e212ba800e1e7f18c7c4b1b27 diff --git a/qt/bundle/build.rs b/qt/bundle/build.rs deleted file mode 100644 index 4b0318dbf..000000000 --- a/qt/bundle/build.rs +++ /dev/null @@ -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, 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"); - } -} diff --git a/qt/bundle/mac/Cargo.toml b/qt/bundle/mac/Cargo.toml deleted file mode 100644 index a154b76f7..000000000 --- a/qt/bundle/mac/Cargo.toml +++ /dev/null @@ -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 diff --git a/qt/bundle/mac/src/codesign.rs b/qt/bundle/mac/src/codesign.rs deleted file mode 100644 index fb251521f..000000000 --- a/qt/bundle/mac/src/codesign.rs +++ /dev/null @@ -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(()) -} diff --git a/qt/bundle/mac/src/dmg.rs b/qt/bundle/mac/src/dmg.rs deleted file mode 100644 index 862150340..000000000 --- a/qt/bundle/mac/src/dmg.rs +++ /dev/null @@ -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, -} - -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(()) -} diff --git a/qt/bundle/mac/src/main.rs b/qt/bundle/mac/src/main.rs deleted file mode 100644 index 2182d808e..000000000 --- a/qt/bundle/mac/src/main.rs +++ /dev/null @@ -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 -} diff --git a/qt/bundle/mac/src/notarize.rs b/qt/bundle/mac/src/notarize.rs deleted file mode 100644 index 0688354f9..000000000 --- a/qt/bundle/mac/src/notarize.rs +++ /dev/null @@ -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(()) -} diff --git a/qt/bundle/pyoxidizer.bzl b/qt/bundle/pyoxidizer.bzl deleted file mode 100644 index dc4870606..000000000 --- a/qt/bundle/pyoxidizer.bzl +++ /dev/null @@ -1,189 +0,0 @@ -# type: ignore - -set_build_path(VARS.get("build")) - -excluded_source_prefixes = [ - "ctypes.test", - "distutils.tests", - "idlelib", - "lib2to3.tests", - "test", - "tkinter", - "win32comext", - "win32com", - "win32", - "pythonwin", - "PyQt6", - "pip", - "setuptools", - "google" -] - -excluded_resource_suffixes = [ - ".pyi", - ".pyc", - "py.typed", -] - -included_resource_packages = [ - "anki", - "aqt", - "_aqt", - "lib2to3", - "certifi", - "jsonschema", -] - - -def handle_resource(policy, resource): - if type(resource) == "PythonModuleSource": - resource.add_include = True - for prefix in excluded_source_prefixes: - if resource.name.startswith(prefix) and not resource.name.startswith("pip_system_certs"): - resource.add_include = False - - # if resource.add_include: - # print("src", resource.name, resource.add_include) - - elif type(resource) == "PythonExtensionModule": - resource.add_include = True - if resource.name.startswith("win32") or resource.name.startswith("PyQt6"): - resource.add_include = False - - # print("ext", resource.name, resource.add_include) - - elif type(resource) == "PythonPackageResource": - for prefix in included_resource_packages: - if resource.package.startswith(prefix): - resource.add_include = True - if resource.package == "certifi": - resource.add_location = "filesystem-relative:lib" - for suffix in excluded_resource_suffixes: - if resource.name.endswith(suffix): - resource.add_include = False - - # aqt web resources can be stored in binary - if resource.package.endswith("aqt"): - if not resource.name.startswith("data/web"): - resource.add_location = "filesystem-relative:lib" - - # if resource.add_include: - # print("rsrc", resource.package, resource.name, resource.add_include) - - elif type(resource) == "PythonPackageDistributionResource": - # print("dist", resource.package, resource.name, resource.add_include) - pass - - # elif type(resource) == "File": - # print(resource.path) - - elif type(resource) == "File": - if ( - resource.path.startswith("win32") - or resource.path.startswith("pythonwin") - or resource.path.startswith("pywin32") - ): - exclude = ( - "tests" in resource.path - or "benchmark" in resource.path - or "__pycache__" in resource.path - ) - if not exclude: - # print("add", resource.path) - resource.add_include = True - resource.add_location = "filesystem-relative:lib" - - if ".dist-info" in resource.path: - resource.add_include = False - - else: - print("unexpected type", type(resource)) - - -def make_exe(): - if BUILD_TARGET_TRIPLE == "x86_64-unknown-linux-gnu": - dist = PythonDistribution( - url = "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.9.18+20240107-x86_64_v2-unknown-linux-gnu-pgo-full.tar.zst", - sha256 = "7ccdc1b19599a6660040ec2f0ade755b32bb45c897ea75d0b7826236146b78cf", - ) - elif BUILD_TARGET_TRIPLE == "x86_64-apple-darwin": - dist = PythonDistribution( - url = "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.9.18+20240107-x86_64-apple-darwin-pgo-full.tar.zst", - sha256 = "b2f06f0f0ebbbed0eae87a6e8eede2e0d838735386a8b84257d4f02d16b9baec", - ) - elif BUILD_TARGET_TRIPLE == "aarch64-apple-darwin": - dist = PythonDistribution( - url = "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.9.18+20240107-aarch64-apple-darwin-pgo-full.tar.zst", - sha256 = "154dfa7cd6f9a6047a58811f84bef69b019ea459e5b42991c8af63e1285b445f", - ) - elif BUILD_TARGET_TRIPLE == "x86_64-pc-windows-msvc": - dist = PythonDistribution( - url = "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.9.18+20240107-x86_64-pc-windows-msvc-shared-pgo-full.tar.zst", - sha256 = "3b9c7d6ed94260b83ed8f44ee9a7b8fce392259ce6591e538601f7353061a884", - ) - else: - fail("unexpected arch") - - policy = dist.make_python_packaging_policy() - - policy.file_scanner_classify_files = True - policy.include_classified_resources = False - - policy.allow_files = True - policy.file_scanner_emit_files = True - policy.include_file_resources = False - - policy.include_distribution_sources = False - policy.include_distribution_resources = False - policy.include_non_distribution_sources = False - policy.include_test = False - - policy.resources_location = "in-memory" - policy.resources_location_fallback = "filesystem-relative:lib" - - policy.register_resource_callback(handle_resource) - - policy.bytecode_optimize_level_zero = False - policy.bytecode_optimize_level_two = True - - python_config = dist.make_python_interpreter_config() - - # detected libs do not need this, but we add extra afterwards - python_config.module_search_paths = ["$ORIGIN/lib"] - python_config.optimization_level = 2 - - python_config.run_command = "import aqt; aqt.run()" - - exe = dist.to_python_executable( - name="anki", - packaging_policy=policy, - config=python_config, - ) - - exe.windows_runtime_dlls_mode = "always" - - # set in main.rs - exe.windows_subsystem = "console" - - resources = exe.read_virtualenv(VARS.get("venv")) - exe.add_python_resources(resources) - - return exe - - -def make_embedded_resources(exe): - return exe.to_embedded_resources() - - -def make_install(exe): - files = FileManifest() - files.add_python_resource(".", exe) - return files - - -register_target("exe", make_exe) -register_target( - "resources", make_embedded_resources, depends=["exe"], default_build_script=True -) -register_target("install", make_install, depends=["exe"], default=True) -resolve_targets() diff --git a/qt/bundle/qt.exclude b/qt/bundle/qt.exclude deleted file mode 100644 index e5a6f252b..000000000 --- a/qt/bundle/qt.exclude +++ /dev/null @@ -1,10 +0,0 @@ -qml -bindings -uic -lupdate -qsci -*.pyc -*.pyi -*.sip -py.typed -__pycache__ diff --git a/qt/bundle/src/anki.rs b/qt/bundle/src/anki.rs deleted file mode 100644 index 3cf960028..000000000 --- a/qt/bundle/src/anki.rs +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright: Ankitects Pty Ltd and contributors -// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html - -pub(super) fn init() { - #[cfg(target_os = "windows")] - attach_console(); - - println!("Anki starting..."); -} - -/// If parent process has a console (eg cmd.exe), redirect our output there. -#[cfg(target_os = "windows")] -fn attach_console() { - use std::ffi::CString; - - use libc_stdhandle::*; - use winapi::um::wincon; - - let console_attached = unsafe { wincon::AttachConsole(wincon::ATTACH_PARENT_PROCESS) }; - if console_attached == 0 { - return; - } - - let conin = CString::new("CONIN$").unwrap(); - let conout = CString::new("CONOUT$").unwrap(); - let r = CString::new("r").unwrap(); - let w = CString::new("w").unwrap(); - - // Python uses the CRT for I/O, and it requires the descriptors are reopened. - unsafe { - libc::freopen(conin.as_ptr(), r.as_ptr(), stdin()); - libc::freopen(conout.as_ptr(), w.as_ptr(), stdout()); - libc::freopen(conout.as_ptr(), w.as_ptr(), stderr()); - } -} diff --git a/qt/bundle/src/main.rs b/qt/bundle/src/main.rs deleted file mode 100644 index d424d4f26..000000000 --- a/qt/bundle/src/main.rs +++ /dev/null @@ -1,32 +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/. - -#![windows_subsystem = "windows"] - -mod anki; - -use pyembed::{MainPythonInterpreter, OxidizedPythonInterpreterConfig}; - -#[cfg(feature = "global-allocator-jemalloc")] -#[global_allocator] -static GLOBAL: jemallocator::Jemalloc = jemallocator::Jemalloc; - -include!(env!("DEFAULT_PYTHON_CONFIG_RS")); - -fn main() { - anki::init(); - - let exit_code = { - let config: OxidizedPythonInterpreterConfig = default_python_config(); - match MainPythonInterpreter::new(config) { - Ok(interp) => interp.run(), - Err(msg) => { - eprintln!("error instantiating embedded Python interpreter: {}", msg); - 1 - } - } - }; - std::process::exit(exit_code); -} diff --git a/qt/bundle/win/Cargo.toml b/qt/bundle/win/Cargo.toml deleted file mode 100644 index 9c091b55f..000000000 --- a/qt/bundle/win/Cargo.toml +++ /dev/null @@ -1,15 +0,0 @@ -[package] -name = "makeexe" -version.workspace = true -authors.workspace = true -edition.workspace = true -license.workspace = true -publish = false -rust-version.workspace = true - -[dependencies] -anyhow.workspace = true -camino.workspace = true -clap.workspace = true -tugger-windows-codesign.workspace = true -walkdir.workspace = true diff --git a/qt/bundle/win/anki.exe.manifest b/qt/bundle/win/anki.exe.manifest deleted file mode 100644 index 6abbdfea9..000000000 --- a/qt/bundle/win/anki.exe.manifest +++ /dev/null @@ -1,9 +0,0 @@ - - - - - true - UTF-8 - - - diff --git a/qt/bundle/win/src/main.rs b/qt/bundle/win/src/main.rs deleted file mode 100644 index 2091f6a51..000000000 --- a/qt/bundle/win/src/main.rs +++ /dev/null @@ -1,153 +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::io::prelude::*; -use std::path::Path; -use std::process::Command; - -use anyhow::bail; -use anyhow::Context; -use anyhow::Result; -use camino::Utf8Path; -use camino::Utf8PathBuf; -use clap::Parser; -use tugger_windows_codesign::CodeSigningCertificate; -use tugger_windows_codesign::SigntoolSign; -use tugger_windows_codesign::SystemStore; -use tugger_windows_codesign::TimestampServer; -use walkdir::WalkDir; - -#[derive(Parser)] -struct Args { - version: String, - bundle_root: Utf8PathBuf, - qt6_setup_path: Utf8PathBuf, -} - -fn main() -> Result<()> { - let args = Args::parse(); - - let src_win_folder = Utf8Path::new("qt/bundle/win"); - let std_dist_folder = args.bundle_root.join("std"); - // folder->installer - let dists = [(&std_dist_folder, &args.qt6_setup_path)]; - - for (folder, _) in dists { - fs::copy( - src_win_folder.join("anki-console.bat"), - folder.join("anki-console.bat"), - ) - .context("anki-console")?; - } - - println!("--- Build uninstaller"); - build_installer( - &args.bundle_root, - &std_dist_folder, - &args.qt6_setup_path, - &args.version, - true, - ) - .context("uninstaller")?; - - // sign the anki.exe and uninstaller.exe in std - println!("--- Sign binaries"); - codesign([ - &std_dist_folder.join("anki.exe"), - &std_dist_folder.join("uninstall.exe"), - ])?; - - println!("--- Build manifest"); - for (folder, _) in dists { - build_manifest(folder).context("manifest")?; - } - - for (folder, installer) in dists { - println!("--- Build {}", installer); - build_installer(&args.bundle_root, folder, installer, &args.version, false)?; - } - - println!("--- Sign installers"); - codesign(dists.iter().map(|tup| tup.1))?; - - Ok(()) -} - -fn build_installer( - bundle_root: &Utf8Path, - dist_folder: &Utf8Path, - installer: &Utf8Path, - version: &str, - uninstaller: bool, -) -> Result<()> { - let rendered_nsi = include_str!("../anki.template.nsi") - .replace("@@SRC@@", dist_folder.as_str()) - .replace("@@INSTALLER@@", installer.as_str()) - .replace("@@VERSION@@", version); - let rendered_nsi_path = bundle_root.join("anki.nsi"); - fs::write(&rendered_nsi_path, rendered_nsi).context("anki.nsi")?; - fs::write( - bundle_root.join("fileassoc.nsh"), - include_str!("../fileassoc.nsh"), - )?; - fs::copy( - "out/extracted/nsis_plugins/nsProcess.dll", - bundle_root.join("nsProcess.dll"), - )?; - let mut cmd = Command::new("c:/program files (x86)/nsis/makensis.exe"); - cmd.arg("-V3"); - if uninstaller { - cmd.arg("-DWRITE_UNINSTALLER"); - }; - if option_env!("RELEASE").is_none() { - cmd.arg("-DNO_COMPRESS"); - } - cmd.arg(rendered_nsi_path); - let status = cmd.status()?; - if !status.success() { - bail!("makensis failed"); - } - Ok(()) -} - -fn codesign(paths: impl IntoIterator>) -> Result<()> { - if option_env!("ANKI_CODESIGN").is_none() { - return Ok(()); - } - let cert = CodeSigningCertificate::Sha1Thumbprint( - SystemStore::My, - "dccfc6d312fc0432197bb7be951478e5866eebf8".into(), - ); - let mut sign = SigntoolSign::new(cert); - sign.file_digest_algorithm("sha256") - .timestamp_server(TimestampServer::Rfc3161( - "http://time.certum.pl".into(), - "sha256".into(), - )) - .verbose(); - paths.into_iter().for_each(|path| { - sign.sign_file(path); - }); - sign.run() -} - -fn build_manifest(base_path: &Utf8Path) -> Result<()> { - let mut buf = vec![]; - for entry in WalkDir::new(base_path) - .min_depth(1) - .sort_by_file_name() - .into_iter() - { - let entry = entry?; - let path = entry.path(); - let relative_path = path.strip_prefix(base_path)?; - write!( - &mut buf, - "{}\r\n", - relative_path.to_str().context("relative_path utf8")? - )?; - } - fs::write(base_path.join("anki.install-manifest"), buf)?; - Ok(()) -} diff --git a/qt/hatch_build.py b/qt/hatch_build.py new file mode 100644 index 000000000..fc716a57f --- /dev/null +++ b/qt/hatch_build.py @@ -0,0 +1,77 @@ +# Copyright: Ankitects Pty Ltd and contributors +# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +import os +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 copy generated files into both sdist and wheel.""" + + 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", {}) + + # Pin anki== + self._set_anki_dependency(version, build_data) + + # Look for generated files in out/qt/_aqt + project_root = Path(self.root).parent + generated_root = project_root / "out" / "qt" / "_aqt" + + if not 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 + + assert generated_root.exists(), "you should build with --wheel" + self._add_aqt_files(force_include, generated_root) + + def _set_anki_dependency(self, version: str, build_data: Dict[str, Any]) -> None: + # Get current dependencies and replace 'anki' with exact version + dependencies = build_data.setdefault("dependencies", []) + + # Remove any existing anki dependency + dependencies[:] = [dep for dep in dependencies if not dep.startswith("anki")] + + # Handle version detection + actual_version = version + if version == "standard": + # Read actual version from .version file + project_root = Path(self.root).parent + version_file = project_root / ".version" + if version_file.exists(): + actual_version = version_file.read_text().strip() + + # Only add exact version for real releases, not editable installs + if actual_version != "editable": + dependencies.append(f"anki=={actual_version}") + else: + # For editable installs, just add anki without version constraint + dependencies.append("anki") + + def _add_aqt_files(self, force_include: Dict[str, str], aqt_root: Path) -> None: + """Add _aqt files to the build.""" + for path in aqt_root.rglob("*"): + if path.is_file() and not self._should_exclude(path): + relative_path = path.relative_to(aqt_root) + # Place files under _aqt/ in the distribution + dist_path = "_aqt" / relative_path + force_include[str(path)] = str(dist_path) + + def _should_exclude(self, path: Path) -> bool: + """Check if a file should be excluded from the wheel.""" + # Match the exclusions from write_wheel.py exclude_aqt function + 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 diff --git a/qt/launcher/Cargo.toml b/qt/launcher/Cargo.toml new file mode 100644 index 000000000..45ca11e9b --- /dev/null +++ b/qt/launcher/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "launcher" +version = "1.0.0" +authors.workspace = true +edition.workspace = true +license.workspace = true +publish = false +rust-version.workspace = true + +[dependencies] +anki_io.workspace = true +anki_process.workspace = true +anyhow.workspace = true +dirs.workspace = true + +[target.'cfg(windows)'.dependencies] +winapi.workspace = true +libc.workspace = true +libc-stdhandle.workspace = true + +[[bin]] +name = "build_win" +path = "src/bin/build_win.rs" + +[target.'cfg(windows)'.build-dependencies] +embed-resource.workspace = true diff --git a/qt/launcher/build.rs b/qt/launcher/build.rs new file mode 100644 index 000000000..3ba75b0e1 --- /dev/null +++ b/qt/launcher/build.rs @@ -0,0 +1,10 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +fn main() { + #[cfg(windows)] + { + embed_resource::compile("win/anki-manifest.rc", embed_resource::NONE) + .manifest_required() + .unwrap(); + } +} diff --git a/qt/bundle/lin/README.md b/qt/launcher/lin/README.md similarity index 100% rename from qt/bundle/lin/README.md rename to qt/launcher/lin/README.md diff --git a/qt/launcher/lin/anki b/qt/launcher/lin/anki new file mode 100644 index 000000000..2a4a46062 --- /dev/null +++ b/qt/launcher/lin/anki @@ -0,0 +1,30 @@ +#!/bin/bash +# Universal Anki launcher script + +# Get the directory where this script is located (resolve symlinks) +SCRIPT_DIR="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd)" + +# Determine architecture +ARCH=$(uname -m) +case "$ARCH" in + x86_64|amd64) + LAUNCHER="$SCRIPT_DIR/launcher.amd64" + ;; + aarch64|arm64) + LAUNCHER="$SCRIPT_DIR/launcher.arm64" + ;; + *) + echo "Error: Unsupported architecture: $ARCH" + echo "Supported architectures: x86_64, aarch64" + exit 1 + ;; +esac + +# Check if launcher exists +if [ ! -f "$LAUNCHER" ]; then + echo "Error: Launcher not found: $LAUNCHER" + exit 1 +fi + +# Execute the appropriate launcher with all arguments +exec "$LAUNCHER" "$@" \ No newline at end of file diff --git a/qt/bundle/lin/anki.1 b/qt/launcher/lin/anki.1 similarity index 100% rename from qt/bundle/lin/anki.1 rename to qt/launcher/lin/anki.1 diff --git a/qt/bundle/lin/anki.desktop b/qt/launcher/lin/anki.desktop similarity index 100% rename from qt/bundle/lin/anki.desktop rename to qt/launcher/lin/anki.desktop diff --git a/qt/bundle/lin/anki.png b/qt/launcher/lin/anki.png similarity index 100% rename from qt/bundle/lin/anki.png rename to qt/launcher/lin/anki.png diff --git a/qt/bundle/lin/anki.xml b/qt/launcher/lin/anki.xml similarity index 100% rename from qt/bundle/lin/anki.xml rename to qt/launcher/lin/anki.xml diff --git a/qt/bundle/lin/anki.xpm b/qt/launcher/lin/anki.xpm similarity index 100% rename from qt/bundle/lin/anki.xpm rename to qt/launcher/lin/anki.xpm diff --git a/qt/launcher/lin/build.sh b/qt/launcher/lin/build.sh new file mode 100755 index 000000000..e4ddce243 --- /dev/null +++ b/qt/launcher/lin/build.sh @@ -0,0 +1,66 @@ +#!/bin/bash + +set -e + +# Add Linux cross-compilation target +rustup target add aarch64-unknown-linux-gnu + +# Define output paths +OUTPUT_DIR="../../../out/launcher" +LAUNCHER_DIR="$OUTPUT_DIR/anki-launcher" + +# Clean existing output directory +rm -rf "$LAUNCHER_DIR" + +# Build binaries for both Linux architectures +cargo build -p launcher --release --target x86_64-unknown-linux-gnu +CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc \ + cargo build -p launcher --release --target aarch64-unknown-linux-gnu +(cd ../../.. && ./ninja extract:uv_lin_arm) + +# Create output directory +mkdir -p "$LAUNCHER_DIR" + +# Copy binaries and support files +TARGET_DIR=${CARGO_TARGET_DIR:-../../../target} + +# Copy launcher binaries with architecture suffixes +cp "$TARGET_DIR/x86_64-unknown-linux-gnu/release/launcher" "$LAUNCHER_DIR/launcher.amd64" +cp "$TARGET_DIR/aarch64-unknown-linux-gnu/release/launcher" "$LAUNCHER_DIR/launcher.arm64" + +# Copy uv binaries with architecture suffixes +cp "../../../out/extracted/uv/uv" "$LAUNCHER_DIR/uv.amd64" +cp "../../../out/extracted/uv_lin_arm/uv" "$LAUNCHER_DIR/uv.arm64" + +# Copy support files from lin directory +for file in README.md anki.1 anki.desktop anki.png anki.xml anki.xpm install.sh uninstall.sh anki; do + cp "$file" "$LAUNCHER_DIR/" +done + +# Copy additional files from parent directory +cp ../pyproject.toml "$LAUNCHER_DIR/" +cp ../../../.python-version "$LAUNCHER_DIR/" + +# Set executable permissions +chmod +x \ + "$LAUNCHER_DIR/anki" \ + "$LAUNCHER_DIR/launcher.amd64" \ + "$LAUNCHER_DIR/launcher.arm64" \ + "$LAUNCHER_DIR/uv.amd64" \ + "$LAUNCHER_DIR/uv.arm64" \ + "$LAUNCHER_DIR/install.sh" \ + "$LAUNCHER_DIR/uninstall.sh" + +# Set proper permissions and create tarball +chmod -R a+r "$LAUNCHER_DIR" + +# Create tarball using the same options as the Rust template +ZSTD="zstd -c --long -T0 -18" +TRANSFORM="s%^.%anki-launcher%S" +TARBALL="$OUTPUT_DIR/anki-launcher.tar.zst" + +tar -I "$ZSTD" --transform "$TRANSFORM" -cf "$TARBALL" -C "$LAUNCHER_DIR" . + +echo "Build complete:" +echo "Universal launcher: $LAUNCHER_DIR" +echo "Tarball: $TARBALL" diff --git a/qt/bundle/lin/install.sh b/qt/launcher/lin/install.sh similarity index 92% rename from qt/bundle/lin/install.sh rename to qt/launcher/lin/install.sh index 519a45fa2..c9f129654 100755 --- a/qt/bundle/lin/install.sh +++ b/qt/launcher/lin/install.sh @@ -13,7 +13,7 @@ fi rm -rf "$PREFIX"/share/anki "$PREFIX"/bin/anki mkdir -p "$PREFIX"/share/anki -cp -av --no-preserve=owner,context -- * "$PREFIX"/share/anki/ +cp -av --no-preserve=owner,context -- * .python-version "$PREFIX"/share/anki/ mkdir -p "$PREFIX"/bin ln -sf "$PREFIX"/share/anki/anki "$PREFIX"/bin/anki # fix a previous packaging issue where we created this as a file diff --git a/qt/bundle/lin/uninstall.sh b/qt/launcher/lin/uninstall.sh similarity index 100% rename from qt/bundle/lin/uninstall.sh rename to qt/launcher/lin/uninstall.sh diff --git a/qt/bundle/mac/src/Info.plist b/qt/launcher/mac/Info.plist similarity index 92% rename from qt/bundle/mac/src/Info.plist rename to qt/launcher/mac/Info.plist index 5933838e4..59b67605f 100644 --- a/qt/bundle/mac/src/Info.plist +++ b/qt/launcher/mac/Info.plist @@ -5,9 +5,9 @@ CFBundleDisplayName Anki CFBundleShortVersionString - 2.1.46 + 1.0 LSMinimumSystemVersion - 10.14.0 + 11 CFBundleDocumentTypes @@ -26,11 +26,11 @@ CFBundleExecutable - anki + launcher CFBundleIconName AppIcon CFBundleIdentifier - net.ankiweb.dtop + net.ankiweb.launcher CFBundleInfoDictionaryVersion 6.0 CFBundleName diff --git a/qt/launcher/mac/build.sh b/qt/launcher/mac/build.sh new file mode 100755 index 000000000..eb4483488 --- /dev/null +++ b/qt/launcher/mac/build.sh @@ -0,0 +1,54 @@ +#!/bin/bash + +set -e + +# Define output path +OUTPUT_DIR="../../../out/launcher" +APP_LAUNCHER="$OUTPUT_DIR/Anki.app" +rm -rf "$APP_LAUNCHER" + +# Build binaries for both architectures +rustup target add aarch64-apple-darwin x86_64-apple-darwin +cargo build -p launcher --release --target aarch64-apple-darwin +cargo build -p launcher --release --target x86_64-apple-darwin +(cd ../../.. && ./ninja launcher:uv_universal) + +# Ensure output directory exists +mkdir -p "$OUTPUT_DIR" + +# Remove existing app launcher +rm -rf "$APP_LAUNCHER" + +# Create app launcher structure +mkdir -p "$APP_LAUNCHER/Contents/MacOS" "$APP_LAUNCHER/Contents/Resources" + +# Copy binaries in +TARGET_DIR=${CARGO_TARGET_DIR:-target} +lipo -create \ + "$TARGET_DIR/aarch64-apple-darwin/release/launcher" \ + "$TARGET_DIR/x86_64-apple-darwin/release/launcher" \ + -output "$APP_LAUNCHER/Contents/MacOS/launcher" +cp "$OUTPUT_DIR/uv" "$APP_LAUNCHER/Contents/MacOS/" + +# Copy support files +cp Info.plist "$APP_LAUNCHER/Contents/" +cp icon/Assets.car "$APP_LAUNCHER/Contents/Resources/" +cp ../pyproject.toml "$APP_LAUNCHER/Contents/Resources/" +cp ../../../.python-version "$APP_LAUNCHER/Contents/Resources/" + +# Codesign +for i in "$APP_LAUNCHER/Contents/MacOS/uv" "$APP_LAUNCHER/Contents/MacOS/launcher" "$APP_LAUNCHER"; do + codesign --force -vvvv -o runtime -s "Developer ID Application:" \ + --entitlements entitlements.python.xml \ + "$i" +done + +# Check +codesign -vvv "$APP_LAUNCHER" +spctl -a "$APP_LAUNCHER" + +# Notarize +./notarize.sh "$OUTPUT_DIR" + +# Bundle +./dmg/build.sh "$OUTPUT_DIR" \ No newline at end of file diff --git a/qt/bundle/mac/dmg/anki-logo-bg.png b/qt/launcher/mac/dmg/anki-logo-bg.png similarity index 100% rename from qt/bundle/mac/dmg/anki-logo-bg.png rename to qt/launcher/mac/dmg/anki-logo-bg.png diff --git a/qt/bundle/mac/dmg/build.sh b/qt/launcher/mac/dmg/build.sh similarity index 87% rename from qt/bundle/mac/dmg/build.sh rename to qt/launcher/mac/dmg/build.sh index 6efb510a7..16b48c06a 100755 --- a/qt/bundle/mac/dmg/build.sh +++ b/qt/launcher/mac/dmg/build.sh @@ -4,9 +4,9 @@ set -e # base folder with Anki.app in it -dist=$1 -dmg_path=$2 -script_folder=$(dirname $0) +output="$1" +dist="$1/tmp" +dmg_path="$output/Anki.dmg" if [ -d "/Volumes/Anki" ] then @@ -14,9 +14,14 @@ then exit 1 fi +rm -rf $dist $dmg_path +mkdir -p $dist +rsync -av $output/Anki.app $dist/ +script_folder=$(dirname $0) + echo "bundling..." ln -s /Applications $dist/Applications -mkdir $dist/.background +mkdir -p $dist/.background cp ${script_folder}/anki-logo-bg.png $dist/.background cp ${script_folder}/dmg_ds_store $dist/.DS_Store diff --git a/qt/bundle/mac/dmg/dmg_ds_store b/qt/launcher/mac/dmg/dmg_ds_store similarity index 100% rename from qt/bundle/mac/dmg/dmg_ds_store rename to qt/launcher/mac/dmg/dmg_ds_store diff --git a/qt/bundle/mac/dmg/set-dmg-settings.app/Contents/Info.plist b/qt/launcher/mac/dmg/set-dmg-settings.app/Contents/Info.plist similarity index 100% rename from qt/bundle/mac/dmg/set-dmg-settings.app/Contents/Info.plist rename to qt/launcher/mac/dmg/set-dmg-settings.app/Contents/Info.plist diff --git a/qt/bundle/mac/dmg/set-dmg-settings.app/Contents/MacOS/applet b/qt/launcher/mac/dmg/set-dmg-settings.app/Contents/MacOS/applet similarity index 100% rename from qt/bundle/mac/dmg/set-dmg-settings.app/Contents/MacOS/applet rename to qt/launcher/mac/dmg/set-dmg-settings.app/Contents/MacOS/applet diff --git a/qt/bundle/mac/dmg/set-dmg-settings.app/Contents/PkgInfo b/qt/launcher/mac/dmg/set-dmg-settings.app/Contents/PkgInfo similarity index 100% rename from qt/bundle/mac/dmg/set-dmg-settings.app/Contents/PkgInfo rename to qt/launcher/mac/dmg/set-dmg-settings.app/Contents/PkgInfo diff --git a/qt/bundle/mac/dmg/set-dmg-settings.app/Contents/Resources/Scripts/main.scpt b/qt/launcher/mac/dmg/set-dmg-settings.app/Contents/Resources/Scripts/main.scpt similarity index 100% rename from qt/bundle/mac/dmg/set-dmg-settings.app/Contents/Resources/Scripts/main.scpt rename to qt/launcher/mac/dmg/set-dmg-settings.app/Contents/Resources/Scripts/main.scpt diff --git a/qt/bundle/mac/dmg/set-dmg-settings.app/Contents/Resources/applet.icns b/qt/launcher/mac/dmg/set-dmg-settings.app/Contents/Resources/applet.icns similarity index 100% rename from qt/bundle/mac/dmg/set-dmg-settings.app/Contents/Resources/applet.icns rename to qt/launcher/mac/dmg/set-dmg-settings.app/Contents/Resources/applet.icns diff --git a/qt/bundle/mac/dmg/set-dmg-settings.app/Contents/Resources/applet.rsrc b/qt/launcher/mac/dmg/set-dmg-settings.app/Contents/Resources/applet.rsrc similarity index 100% rename from qt/bundle/mac/dmg/set-dmg-settings.app/Contents/Resources/applet.rsrc rename to qt/launcher/mac/dmg/set-dmg-settings.app/Contents/Resources/applet.rsrc diff --git a/qt/bundle/mac/dmg/set-dmg-settings.app/Contents/Resources/description.rtfd/TXT.rtf b/qt/launcher/mac/dmg/set-dmg-settings.app/Contents/Resources/description.rtfd/TXT.rtf similarity index 100% rename from qt/bundle/mac/dmg/set-dmg-settings.app/Contents/Resources/description.rtfd/TXT.rtf rename to qt/launcher/mac/dmg/set-dmg-settings.app/Contents/Resources/description.rtfd/TXT.rtf diff --git a/qt/bundle/mac/dmg/set-dmg-settings.app/Contents/_CodeSignature/CodeResources b/qt/launcher/mac/dmg/set-dmg-settings.app/Contents/_CodeSignature/CodeResources similarity index 100% rename from qt/bundle/mac/dmg/set-dmg-settings.app/Contents/_CodeSignature/CodeResources rename to qt/launcher/mac/dmg/set-dmg-settings.app/Contents/_CodeSignature/CodeResources diff --git a/qt/bundle/mac/dmg/set-dmg-settings.scpt b/qt/launcher/mac/dmg/set-dmg-settings.scpt similarity index 100% rename from qt/bundle/mac/dmg/set-dmg-settings.scpt rename to qt/launcher/mac/dmg/set-dmg-settings.scpt diff --git a/qt/bundle/mac/entitlements.python.xml b/qt/launcher/mac/entitlements.python.xml similarity index 100% rename from qt/bundle/mac/entitlements.python.xml rename to qt/launcher/mac/entitlements.python.xml diff --git a/qt/bundle/mac/icon/Assets.car b/qt/launcher/mac/icon/Assets.car similarity index 100% rename from qt/bundle/mac/icon/Assets.car rename to qt/launcher/mac/icon/Assets.car diff --git a/qt/bundle/mac/icon/Assets.xcassets/AppIcon.appiconset/Contents.json b/qt/launcher/mac/icon/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from qt/bundle/mac/icon/Assets.xcassets/AppIcon.appiconset/Contents.json rename to qt/launcher/mac/icon/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/qt/bundle/mac/icon/Assets.xcassets/AppIcon.appiconset/round-1024-512.png b/qt/launcher/mac/icon/Assets.xcassets/AppIcon.appiconset/round-1024-512.png similarity index 100% rename from qt/bundle/mac/icon/Assets.xcassets/AppIcon.appiconset/round-1024-512.png rename to qt/launcher/mac/icon/Assets.xcassets/AppIcon.appiconset/round-1024-512.png diff --git a/qt/bundle/mac/icon/Assets.xcassets/Contents.json b/qt/launcher/mac/icon/Assets.xcassets/Contents.json similarity index 100% rename from qt/bundle/mac/icon/Assets.xcassets/Contents.json rename to qt/launcher/mac/icon/Assets.xcassets/Contents.json diff --git a/qt/bundle/mac/icon/build.sh b/qt/launcher/mac/icon/build.sh similarity index 100% rename from qt/bundle/mac/icon/build.sh rename to qt/launcher/mac/icon/build.sh diff --git a/qt/launcher/mac/notarize.sh b/qt/launcher/mac/notarize.sh new file mode 100755 index 000000000..d906bd7f8 --- /dev/null +++ b/qt/launcher/mac/notarize.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +set -e + +# Define output path +OUTPUT_DIR="$1" +APP_LAUNCHER="$OUTPUT_DIR/Anki.app" +ZIP_FILE="$OUTPUT_DIR/Anki.zip" + +# Create zip for notarization +(cd "$OUTPUT_DIR" && rm -rf Anki.zip && zip -r Anki.zip Anki.app) + +# Upload for notarization +xcrun notarytool submit "$ZIP_FILE" -p default --wait + +# Staple the app +xcrun stapler staple "$APP_LAUNCHER" \ No newline at end of file diff --git a/qt/launcher/pyproject.toml b/qt/launcher/pyproject.toml new file mode 100644 index 000000000..2a45626c7 --- /dev/null +++ b/qt/launcher/pyproject.toml @@ -0,0 +1,8 @@ +[project] +name = "anki-launcher" +version = "0.1.0" +description = "UV-based launcher for Anki." +requires-python = ">=3.9" +dependencies = [ + "anki-release", +] diff --git a/qt/launcher/src/bin/build_win.rs b/qt/launcher/src/bin/build_win.rs new file mode 100644 index 000000000..959034438 --- /dev/null +++ b/qt/launcher/src/bin/build_win.rs @@ -0,0 +1,295 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +use std::env; +use std::path::Path; +use std::path::PathBuf; +use std::process::Command; + +use anki_io::copy_file; +use anki_io::create_dir_all; +use anki_io::remove_dir_all; +use anki_io::write_file; +use anki_process::CommandExt; +use anyhow::Result; + +const OUTPUT_DIR: &str = "../../../out/launcher"; +const LAUNCHER_EXE_DIR: &str = "../../../out/launcher_exe"; +const NSIS_DIR: &str = "../../../out/nsis"; +const CARGO_TARGET_DIR: &str = "../../../out/rust"; +const NSIS_PATH: &str = "C:\\Program Files (x86)\\NSIS\\makensis.exe"; + +fn main() -> Result<()> { + println!("Building Windows launcher..."); + + let output_dir = PathBuf::from(OUTPUT_DIR); + let launcher_exe_dir = PathBuf::from(LAUNCHER_EXE_DIR); + let nsis_dir = PathBuf::from(NSIS_DIR); + + setup_directories(&output_dir, &launcher_exe_dir, &nsis_dir)?; + build_launcher_binary()?; + extract_nsis_plugins()?; + copy_files(&output_dir)?; + sign_binaries(&output_dir)?; + copy_nsis_files(&nsis_dir)?; + build_uninstaller(&output_dir, &nsis_dir)?; + sign_file(&output_dir.join("uninstall.exe"))?; + generate_install_manifest(&output_dir)?; + build_installer(&output_dir, &nsis_dir)?; + sign_file(&PathBuf::from("../../../out/launcher_exe/anki-install.exe"))?; + + println!("Build completed successfully!"); + println!("Output directory: {}", output_dir.display()); + println!("Installer: ../../../out/launcher_exe/anki-install.exe"); + + Ok(()) +} + +fn setup_directories(output_dir: &Path, launcher_exe_dir: &Path, nsis_dir: &Path) -> Result<()> { + println!("Setting up directories..."); + + // Remove existing output directories + if output_dir.exists() { + remove_dir_all(output_dir)?; + } + if launcher_exe_dir.exists() { + remove_dir_all(launcher_exe_dir)?; + } + if nsis_dir.exists() { + remove_dir_all(nsis_dir)?; + } + + // Create output directories + create_dir_all(output_dir)?; + create_dir_all(launcher_exe_dir)?; + create_dir_all(nsis_dir)?; + + Ok(()) +} + +fn build_launcher_binary() -> Result<()> { + println!("Building launcher binary..."); + + env::set_var("CARGO_TARGET_DIR", CARGO_TARGET_DIR); + + Command::new("cargo") + .args([ + "build", + "-p", + "launcher", + "--release", + "--target", + "x86_64-pc-windows-msvc", + ]) + .ensure_success()?; + + Ok(()) +} + +fn extract_nsis_plugins() -> Result<()> { + println!("Extracting NSIS plugins..."); + + // Change to the anki root directory and run tools/ninja.bat + Command::new("cmd") + .args([ + "/c", + "cd", + "/d", + "..\\..\\..\\", + "&&", + "tools\\ninja.bat", + "extract:nsis_plugins", + ]) + .ensure_success()?; + + Ok(()) +} + +fn copy_files(output_dir: &Path) -> Result<()> { + println!("Copying binaries..."); + + // Copy launcher binary as anki.exe + let launcher_src = + PathBuf::from(CARGO_TARGET_DIR).join("x86_64-pc-windows-msvc/release/launcher.exe"); + let launcher_dst = output_dir.join("anki.exe"); + copy_file(&launcher_src, &launcher_dst)?; + + // Copy uv.exe + let uv_src = PathBuf::from("../../../out/extracted/uv/uv.exe"); + let uv_dst = output_dir.join("uv.exe"); + copy_file(&uv_src, &uv_dst)?; + + println!("Copying support files..."); + + // Copy pyproject.toml + copy_file("../pyproject.toml", output_dir.join("pyproject.toml"))?; + + // Copy .python-version + copy_file( + "../../../.python-version", + output_dir.join(".python-version"), + )?; + + // Copy anki-console.bat + copy_file("anki-console.bat", output_dir.join("anki-console.bat"))?; + + Ok(()) +} + +fn sign_binaries(output_dir: &Path) -> Result<()> { + sign_file(&output_dir.join("anki.exe"))?; + sign_file(&output_dir.join("uv.exe"))?; + Ok(()) +} + +fn sign_file(file_path: &Path) -> Result<()> { + let codesign = env::var("CODESIGN").unwrap_or_default(); + if codesign != "1" { + println!( + "Skipping code signing for {} (CODESIGN not set to 1)", + file_path.display() + ); + return Ok(()); + } + + let signtool_path = find_signtool()?; + println!("Signing {}...", file_path.display()); + + Command::new(&signtool_path) + .args([ + "sign", + "/sha1", + "dccfc6d312fc0432197bb7be951478e5866eebf8", + "/fd", + "sha256", + "/tr", + "http://time.certum.pl", + "/td", + "sha256", + "/v", + ]) + .arg(file_path) + .ensure_success()?; + + Ok(()) +} + +fn find_signtool() -> Result { + println!("Locating signtool.exe..."); + + let output = Command::new("where") + .args([ + "/r", + "C:\\Program Files (x86)\\Windows Kits", + "signtool.exe", + ]) + .utf8_output()?; + + // Find signtool.exe with "arm64" in the path (as per original batch logic) + for line in output.stdout.lines() { + if line.contains("\\arm64\\") { + let signtool_path = PathBuf::from(line.trim()); + println!("Using signtool: {}", signtool_path.display()); + return Ok(signtool_path); + } + } + + anyhow::bail!("Could not find signtool.exe with arm64 architecture"); +} + +fn generate_install_manifest(output_dir: &Path) -> Result<()> { + println!("Generating install manifest..."); + + let mut manifest_content = String::new(); + let entries = anki_io::read_dir_files(output_dir)?; + + for entry in entries { + let entry = entry?; + let path = entry.path(); + if let Some(file_name) = path.file_name() { + let file_name_str = file_name.to_string_lossy(); + // Skip manifest file and uninstaller (can't delete itself) + if file_name_str != "anki.install-manifest" && file_name_str != "uninstall.exe" { + if let Ok(relative_path) = path.strip_prefix(output_dir) { + // Convert to Windows-style backslashes for NSIS + let windows_path = relative_path.display().to_string().replace('/', "\\"); + // Use Windows line endings (\r\n) as expected by NSIS + manifest_content.push_str(&format!("{}\r\n", windows_path)); + } + } + } + } + + write_file(output_dir.join("anki.install-manifest"), manifest_content)?; + + Ok(()) +} + +fn copy_nsis_files(nsis_dir: &Path) -> Result<()> { + println!("Copying NSIS support files..."); + + // Copy anki.template.nsi as anki.nsi + copy_file("anki.template.nsi", nsis_dir.join("anki.nsi"))?; + + // Copy fileassoc.nsh + copy_file("fileassoc.nsh", nsis_dir.join("fileassoc.nsh"))?; + + // Copy nsProcess.dll + copy_file( + "../../../out/extracted/nsis_plugins/nsProcess.dll", + nsis_dir.join("nsProcess.dll"), + )?; + + Ok(()) +} + +fn build_uninstaller(output_dir: &Path, nsis_dir: &Path) -> Result<()> { + println!("Building uninstaller..."); + + let mut flags = vec!["-V3", "-DWRITE_UNINSTALLER"]; + if env::var("NO_COMPRESS").unwrap_or_default() == "1" { + println!("NO_COMPRESS=1 detected, disabling compression"); + flags.push("-DNO_COMPRESS"); + } + + run_nsis( + &PathBuf::from("anki.nsi"), + &flags, + nsis_dir, // Run from nsis directory + )?; + + // Copy uninstaller from nsis directory to output directory + copy_file( + nsis_dir.join("uninstall.exe"), + output_dir.join("uninstall.exe"), + )?; + + Ok(()) +} + +fn build_installer(_output_dir: &Path, nsis_dir: &Path) -> Result<()> { + println!("Building installer..."); + + let mut flags = vec!["-V3"]; + if env::var("NO_COMPRESS").unwrap_or_default() == "1" { + println!("NO_COMPRESS=1 detected, disabling compression"); + flags.push("-DNO_COMPRESS"); + } + + run_nsis( + &PathBuf::from("anki.nsi"), + &flags, + nsis_dir, // Run from nsis directory + )?; + + Ok(()) +} + +fn run_nsis(script_path: &Path, flags: &[&str], working_dir: &Path) -> Result<()> { + let mut cmd = Command::new(NSIS_PATH); + cmd.args(flags).arg(script_path).current_dir(working_dir); + + cmd.ensure_success()?; + + Ok(()) +} diff --git a/qt/launcher/src/main.rs b/qt/launcher/src/main.rs new file mode 100644 index 000000000..4f397ad99 --- /dev/null +++ b/qt/launcher/src/main.rs @@ -0,0 +1,121 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +#![windows_subsystem = "windows"] + +use std::io::stdin; +use std::process::Command; + +use anki_io::copy_if_newer; +use anki_io::create_dir_all; +use anki_io::modified_time; +use anki_io::remove_file; +use anki_io::write_file; +use anki_process::CommandExt; +use anyhow::Context; +use anyhow::Result; + +use crate::platform::exec_anki; +use crate::platform::get_anki_binary_path; +use crate::platform::get_exe_and_resources_dirs; +use crate::platform::get_uv_binary_name; +use crate::platform::handle_first_launch; +use crate::platform::handle_terminal_launch; +use crate::platform::initial_terminal_setup; +use crate::platform::launch_anki_detached; + +mod platform; + +#[derive(Debug, Clone, Default)] +pub struct Config { + pub show_console: bool, +} + +fn main() { + if let Err(e) = run() { + eprintln!("Error: {:#}", e); + eprintln!("Press enter to close..."); + let mut input = String::new(); + let _ = stdin().read_line(&mut input); + + std::process::exit(1); + } +} + +fn run() -> Result<()> { + let mut config = Config::default(); + initial_terminal_setup(&mut config); + + let uv_install_root = dirs::data_local_dir() + .context("Unable to determine data_dir")? + .join("AnkiProgramFiles"); + + let sync_complete_marker = uv_install_root.join(".sync_complete"); + let prerelease_marker = uv_install_root.join("prerelease"); + let (exe_dir, resources_dir) = get_exe_and_resources_dirs()?; + let dist_pyproject_path = resources_dir.join("pyproject.toml"); + let user_pyproject_path = uv_install_root.join("pyproject.toml"); + let dist_python_version_path = resources_dir.join(".python-version"); + let user_python_version_path = uv_install_root.join(".python-version"); + let uv_lock_path = uv_install_root.join("uv.lock"); + let uv_path: std::path::PathBuf = exe_dir.join(get_uv_binary_name()); + + // Create install directory and copy project files in + create_dir_all(&uv_install_root)?; + copy_if_newer(&dist_pyproject_path, &user_pyproject_path)?; + copy_if_newer(&dist_python_version_path, &user_python_version_path)?; + + let pyproject_has_changed = + !user_pyproject_path.exists() || !sync_complete_marker.exists() || { + let pyproject_toml_time = modified_time(&user_pyproject_path)?; + let sync_complete_time = modified_time(&sync_complete_marker)?; + Ok::(pyproject_toml_time > sync_complete_time) + } + .unwrap_or(true); + + if !pyproject_has_changed { + // If venv is already up to date, exec as normal + let anki_bin = get_anki_binary_path(&uv_install_root); + exec_anki(&anki_bin, &config)?; + return Ok(()); + } + + // we'll need to launch uv; reinvoke ourselves in a terminal so the user can see + handle_terminal_launch()?; + + // Remove sync marker before attempting sync + let _ = remove_file(&sync_complete_marker); + + // Sync the venv + let mut command = Command::new(&uv_path); + command + .current_dir(&uv_install_root) + .args(["sync", "--upgrade", "--managed-python"]); + + // Set UV_PRERELEASE=allow if prerelease file exists + if prerelease_marker.exists() { + command.env("UV_PRERELEASE", "allow"); + } + + // temporarily force it on during initial beta testing + command.env("UV_PRERELEASE", "allow"); + + if let Err(e) = command.ensure_success() { + // If sync fails due to things like a missing wheel on pypi, + // we need to remove the lockfile or uv will cache the bad result. + let _ = remove_file(&uv_lock_path); + return Err(e.into()); + } + + // Write marker file to indicate successful sync + write_file(&sync_complete_marker, "")?; + + // First launch + let anki_bin = get_anki_binary_path(&uv_install_root); + handle_first_launch(&anki_bin)?; + + // Then launch the binary as detached subprocess so the terminal can close + launch_anki_detached(&anki_bin, &config)?; + + Ok(()) +} diff --git a/qt/launcher/src/platform/mac.rs b/qt/launcher/src/platform/mac.rs new file mode 100644 index 000000000..b5157dd4b --- /dev/null +++ b/qt/launcher/src/platform/mac.rs @@ -0,0 +1,80 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +use std::os::unix::process::CommandExt; +use std::process::Command; + +use anki_process::CommandExt as AnkiCommandExt; +use anyhow::Context; +use anyhow::Result; + +// Re-export Unix functions that macOS uses +pub use super::unix::{ + exec_anki, + get_anki_binary_path, + initial_terminal_setup, +}; + +pub fn launch_anki_detached(anki_bin: &std::path::Path, _config: &crate::Config) -> Result<()> { + use std::process::Stdio; + + let child = Command::new(anki_bin) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .process_group(0) + .ensure_spawn()?; + std::mem::forget(child); + Ok(()) +} + +pub fn handle_terminal_launch() -> Result<()> { + let stdout_is_terminal = std::io::IsTerminal::is_terminal(&std::io::stdout()); + if stdout_is_terminal { + print!("\x1B[2J\x1B[H"); // Clear screen and move cursor to top + println!("\x1B[1mPreparing to start Anki...\x1B[0m\n"); + } else { + // If launched from GUI, relaunch in Terminal.app + relaunch_in_terminal()?; + } + Ok(()) +} + +fn relaunch_in_terminal() -> Result<()> { + let current_exe = std::env::current_exe().context("Failed to get current executable path")?; + Command::new("open") + .args(["-a", "Terminal"]) + .arg(current_exe) + .ensure_spawn()?; + std::process::exit(0); +} + +pub fn handle_first_launch(anki_bin: &std::path::Path) -> Result<()> { + // Pre-validate by running --version to trigger any Gatekeeper checks + println!("\n\x1B[1mThis may take a few minutes. Please wait...\x1B[0m"); + let _ = Command::new(anki_bin) + .env("ANKI_FIRST_RUN", "1") + .arg("--version") + .ensure_success(); + Ok(()) +} + +pub fn get_exe_and_resources_dirs() -> Result<(std::path::PathBuf, std::path::PathBuf)> { + let exe_dir = std::env::current_exe() + .context("Failed to get current executable path")? + .parent() + .context("Failed to get executable directory")? + .to_owned(); + + let resources_dir = exe_dir + .parent() + .context("Failed to get parent directory")? + .join("Resources"); + + Ok((exe_dir, resources_dir)) +} + +pub fn get_uv_binary_name() -> &'static str { + // macOS uses standard uv binary name + "uv" +} diff --git a/qt/launcher/src/platform/mod.rs b/qt/launcher/src/platform/mod.rs new file mode 100644 index 000000000..bb7208abe --- /dev/null +++ b/qt/launcher/src/platform/mod.rs @@ -0,0 +1,18 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +#[cfg(unix)] +mod unix; + +#[cfg(target_os = "macos")] +mod mac; + +#[cfg(target_os = "windows")] +mod windows; + +#[cfg(target_os = "macos")] +pub use mac::*; +#[cfg(all(unix, not(target_os = "macos")))] +pub use unix::*; +#[cfg(target_os = "windows")] +pub use windows::*; diff --git a/qt/launcher/src/platform/unix.rs b/qt/launcher/src/platform/unix.rs new file mode 100644 index 000000000..4155f39d1 --- /dev/null +++ b/qt/launcher/src/platform/unix.rs @@ -0,0 +1,74 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +#![allow(dead_code)] + +use std::path::PathBuf; +use std::process::Command; + +use anki_process::CommandExt as AnkiCommandExt; +use anyhow::Context; +use anyhow::Result; + +use crate::Config; + +pub fn initial_terminal_setup(_config: &mut Config) { + // No special terminal setup needed on Unix +} + +pub fn handle_terminal_launch() -> Result<()> { + print!("\x1B[2J\x1B[H"); // Clear screen and move cursor to top + println!("\x1B[1mPreparing to start Anki...\x1B[0m\n"); + // Skip terminal relaunch on non-macOS Unix systems as we don't know which + // terminal is installed + Ok(()) +} + +pub fn get_anki_binary_path(uv_install_root: &std::path::Path) -> PathBuf { + uv_install_root.join(".venv/bin/anki") +} + +pub fn launch_anki_detached(anki_bin: &std::path::Path, config: &Config) -> Result<()> { + // On non-macOS Unix systems, we don't need to detach since we never spawned a + // terminal + exec_anki(anki_bin, config) +} + +pub fn handle_first_launch(_anki_bin: &std::path::Path) -> Result<()> { + // No special first launch handling needed for generic Unix systems + Ok(()) +} + +pub fn exec_anki(anki_bin: &std::path::Path, _config: &Config) -> Result<()> { + let args: Vec = std::env::args().skip(1).collect(); + Command::new(anki_bin) + .args(args) + .ensure_exec() + .map_err(anyhow::Error::new) +} + +pub fn get_exe_and_resources_dirs() -> Result<(PathBuf, PathBuf)> { + let exe_dir = std::env::current_exe() + .context("Failed to get current executable path")? + .parent() + .context("Failed to get executable directory")? + .to_owned(); + + // On generic Unix systems, assume resources are in the same directory as + // executable + let resources_dir = exe_dir.clone(); + + Ok((exe_dir, resources_dir)) +} + +pub fn get_uv_binary_name() -> &'static str { + // Use architecture-specific uv binary for non-Mac Unix systems + if cfg!(target_arch = "x86_64") { + "uv.amd64" + } else if cfg!(target_arch = "aarch64") { + "uv.arm64" + } else { + // Fallback to generic uv for other architectures + "uv" + } +} diff --git a/qt/launcher/src/platform/windows.rs b/qt/launcher/src/platform/windows.rs new file mode 100644 index 000000000..c536c8c76 --- /dev/null +++ b/qt/launcher/src/platform/windows.rs @@ -0,0 +1,118 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +use std::path::PathBuf; +use std::process::Command; + +use anki_process::CommandExt; +use anyhow::Context; +use anyhow::Result; + +use crate::Config; + +pub fn handle_terminal_launch() -> Result<()> { + // uv will do this itself + Ok(()) +} + +/// If parent process has a console (eg cmd.exe), redirect our output there. +/// Sets config.show_console to true if successfully attached to console. +pub fn initial_terminal_setup(config: &mut Config) { + use std::ffi::CString; + + use libc_stdhandle::*; + use winapi::um::wincon; + + let console_attached = unsafe { wincon::AttachConsole(wincon::ATTACH_PARENT_PROCESS) }; + if console_attached == 0 { + return; + } + + let conin = CString::new("CONIN$").unwrap(); + let conout = CString::new("CONOUT$").unwrap(); + let r = CString::new("r").unwrap(); + let w = CString::new("w").unwrap(); + + // Python uses the CRT for I/O, and it requires the descriptors are reopened. + unsafe { + libc::freopen(conin.as_ptr(), r.as_ptr(), stdin()); + libc::freopen(conout.as_ptr(), w.as_ptr(), stdout()); + libc::freopen(conout.as_ptr(), w.as_ptr(), stderr()); + } + + config.show_console = true; +} + +pub fn get_anki_binary_path(uv_install_root: &std::path::Path) -> std::path::PathBuf { + uv_install_root.join(".venv/Scripts/anki.exe") +} + +fn build_python_command( + anki_bin: &std::path::Path, + args: &[String], + config: &Config, +) -> Result { + let venv_dir = anki_bin + .parent() + .context("Failed to get venv Scripts directory")? + .parent() + .context("Failed to get venv directory")?; + + // Use python.exe if show_console is true, otherwise pythonw.exe + let python_exe = if config.show_console { + venv_dir.join("Scripts/python.exe") + } else { + venv_dir.join("Scripts/pythonw.exe") + }; + + let mut cmd = Command::new(python_exe); + cmd.args(["-c", "import aqt; aqt.run()"]); + cmd.args(args); + + Ok(cmd) +} + +pub fn launch_anki_detached(anki_bin: &std::path::Path, config: &Config) -> Result<()> { + use std::os::windows::process::CommandExt; + use std::process::Stdio; + + const CREATE_NEW_PROCESS_GROUP: u32 = 0x00000200; + const DETACHED_PROCESS: u32 = 0x00000008; + + let mut cmd = build_python_command(anki_bin, &[], config)?; + cmd.stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .creation_flags(CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS) + .ensure_spawn()?; + Ok(()) +} + +pub fn handle_first_launch(_anki_bin: &std::path::Path) -> Result<()> { + Ok(()) +} + +pub fn exec_anki(anki_bin: &std::path::Path, config: &Config) -> Result<()> { + let args: Vec = std::env::args().skip(1).collect(); + let mut cmd = build_python_command(anki_bin, &args, config)?; + cmd.ensure_success()?; + Ok(()) +} + +pub fn get_exe_and_resources_dirs() -> Result<(PathBuf, PathBuf)> { + let exe_dir = std::env::current_exe() + .context("Failed to get current executable path")? + .parent() + .context("Failed to get executable directory")? + .to_owned(); + + // On Windows, resources dir is the same as exe_dir + let resources_dir = exe_dir.clone(); + + Ok((exe_dir, resources_dir)) +} + +pub fn get_uv_binary_name() -> &'static str { + // Windows uses standard uv binary name + "uv.exe" +} diff --git a/qt/bundle/win/anki-console.bat b/qt/launcher/win/anki-console.bat similarity index 100% rename from qt/bundle/win/anki-console.bat rename to qt/launcher/win/anki-console.bat diff --git a/qt/bundle/win/anki-icon.ico b/qt/launcher/win/anki-icon.ico similarity index 100% rename from qt/bundle/win/anki-icon.ico rename to qt/launcher/win/anki-icon.ico diff --git a/qt/bundle/win/anki-manifest.rc b/qt/launcher/win/anki-manifest.rc similarity index 100% rename from qt/bundle/win/anki-manifest.rc rename to qt/launcher/win/anki-manifest.rc diff --git a/qt/launcher/win/anki.exe.manifest b/qt/launcher/win/anki.exe.manifest new file mode 100644 index 000000000..93e98c6ec --- /dev/null +++ b/qt/launcher/win/anki.exe.manifest @@ -0,0 +1,25 @@ + + + + + + + + + + + + + true + PerMonitorV2 + true + UTF-8 + + + + + + + + + diff --git a/qt/bundle/win/anki.template.nsi b/qt/launcher/win/anki.template.nsi similarity index 90% rename from qt/bundle/win/anki.template.nsi rename to qt/launcher/win/anki.template.nsi index 513c81bfb..7b2bfd8fc 100644 --- a/qt/bundle/win/anki.template.nsi +++ b/qt/launcher/win/anki.template.nsi @@ -23,8 +23,8 @@ Name "Anki" Unicode true -; The file to write (make relative to repo root instead of out/bundle) -OutFile "..\..\@@INSTALLER@@" +; The file to write (relative to nsis directory) +OutFile "..\launcher_exe\anki-install.exe" ; Non elevated RequestExecutionLevel user @@ -62,7 +62,7 @@ Function .onInit FunctionEnd !ifdef WRITE_UNINSTALLER -!uninstfinalize 'copy "%1" "std\uninstall.exe"' +!uninstfinalize 'copy "%1" "uninstall.exe"' !endif ;-------------------------------- @@ -191,7 +191,7 @@ Section "" ; Add files to installer !ifndef WRITE_UNINSTALLER - File /r ..\..\@@SRC@@\*.* + File /r ..\launcher\*.* !endif !insertmacro APP_ASSOCIATE_HKCU "apkg" "anki.apkg" \ @@ -213,8 +213,8 @@ Section "" WriteRegStr HKCU Software\Anki "Install_Dir64" "$INSTDIR" ; Write the uninstall keys for Windows - WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\Anki" "DisplayName" "Anki" - WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\Anki" "DisplayVersion" "@@VERSION@@" + WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\Anki" "DisplayName" "Anki Launcher" + WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\Anki" "DisplayVersion" "1.0.0" WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\Anki" "UninstallString" '"$INSTDIR\uninstall.exe"' WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\Anki" "QuietUninstallString" '"$INSTDIR\uninstall.exe" /S' WriteRegDWORD HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\Anki" "NoModify" 1 @@ -224,10 +224,29 @@ Section "" WriteUninstaller "uninstall.exe" !endif + ; Ensure uv gets re-run + Push "$INSTDIR\pyproject.toml" + Call TouchFile + + ; Launch Anki after installation + Exec "$INSTDIR\anki.exe" + Quit + SectionEnd ; end the section ;-------------------------------- +; Touch file function to update mtime using copy trick +Function TouchFile + Exch $R0 ; file path + + nsExec::Exec 'cmd /c "copy /B "$R0" +,,"' + + Pop $R0 +FunctionEnd + +;-------------------------------- + ; Uninstaller function un.onInit @@ -252,9 +271,15 @@ Section "Uninstall" !insertmacro APP_UNASSOCIATE_HKCU "ankiaddon" "anki.ankiaddon" !insertmacro UPDATEFILEASSOC + ; Schedule uninstaller for deletion on reboot + Delete /REBOOTOK "$INSTDIR\uninstall.exe" + ; try to remove top level folder if empty RMDir "$INSTDIR" + ; Remove AnkiProgramData folder created during runtime + RMDir /r "$LOCALAPPDATA\AnkiProgramFiles" + ; Remove registry keys DeleteRegKey HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\Anki" DeleteRegKey HKCU Software\Anki diff --git a/qt/launcher/win/build.bat b/qt/launcher/win/build.bat new file mode 100644 index 000000000..b21831462 --- /dev/null +++ b/qt/launcher/win/build.bat @@ -0,0 +1,5 @@ +@echo off + +set CODESIGN=1 +REM set NO_COMPRESS=1 +cargo run --bin build_win diff --git a/qt/bundle/win/fileassoc.nsh b/qt/launcher/win/fileassoc.nsh similarity index 100% rename from qt/bundle/win/fileassoc.nsh rename to qt/launcher/win/fileassoc.nsh diff --git a/qt/mac/anki_mac_helper/__init__.py b/qt/mac/anki_mac_helper/__init__.py new file mode 100644 index 000000000..a0adb469e --- /dev/null +++ b/qt/mac/anki_mac_helper/__init__.py @@ -0,0 +1,51 @@ +# Copyright: Ankitects Pty Ltd and contributors +# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +from __future__ import annotations + +import sys +from collections.abc import Callable +from ctypes import CDLL, CFUNCTYPE, c_bool, c_char_p +from pathlib import Path + + +class _MacOSHelper: + def __init__(self) -> None: + # Look for the dylib in the same directory as this module + module_dir = Path(__file__).parent + path = module_dir / "libankihelper.dylib" + + self._dll = CDLL(str(path)) + self._dll.system_is_dark.restype = c_bool + + def system_is_dark(self) -> bool: + return self._dll.system_is_dark() + + def set_darkmode_enabled(self, enabled: bool) -> bool: + return self._dll.set_darkmode_enabled(enabled) + + def start_wav_record(self, path: str, on_error: Callable[[str], None]) -> None: + global _on_audio_error + _on_audio_error = on_error + self._dll.start_wav_record(path.encode("utf8"), _audio_error_callback) + + def end_wav_record(self) -> None: + "On completion, file should be saved if no error has arrived." + self._dll.end_wav_record() + + +# this must not be overwritten or deallocated +@CFUNCTYPE(None, c_char_p) # type: ignore +def _audio_error_callback(msg: str) -> None: + if handler := _on_audio_error: + handler(msg) + + +_on_audio_error: Callable[[str], None] | None = None + +macos_helper: _MacOSHelper | None = None +if sys.platform == "darwin": + try: + macos_helper = _MacOSHelper() + except Exception as e: + print("macos_helper:", e) diff --git a/qt/mac/anki_mac_helper/py.typed b/qt/mac/anki_mac_helper/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/qt/mac/ankihelper.xcodeproj/project.pbxproj b/qt/mac/ankihelper.xcodeproj/project.pbxproj index 8232fba8e..016b03192 100644 --- a/qt/mac/ankihelper.xcodeproj/project.pbxproj +++ b/qt/mac/ankihelper.xcodeproj/project.pbxproj @@ -172,7 +172,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 11.6; + MACOSX_DEPLOYMENT_TARGET = 11; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -225,7 +225,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 11.6; + MACOSX_DEPLOYMENT_TARGET = 11; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = macosx; diff --git a/qt/mac/build.sh b/qt/mac/build.sh new file mode 100755 index 000000000..4c14a13f4 --- /dev/null +++ b/qt/mac/build.sh @@ -0,0 +1,20 @@ +#!/bin/bash +# Copyright: Ankitects Pty Ltd and contributors +# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +set -e + +# Get the project root directory +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJ_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" + +# Build the dylib first +echo "Building macOS helper dylib..." +"$PROJ_ROOT/out/pyenv/bin/python" "$SCRIPT_DIR/helper_build.py" + +# Create the wheel using uv +echo "Creating wheel..." +cd "$SCRIPT_DIR" +"$PROJ_ROOT/out/extracted/uv/uv" build --wheel + +echo "Build complete!" diff --git a/qt/mac/helper_build.py b/qt/mac/helper_build.py index 4edbd05c1..aaf997669 100644 --- a/qt/mac/helper_build.py +++ b/qt/mac/helper_build.py @@ -7,23 +7,52 @@ import subprocess import sys from pathlib import Path -out_dylib, *src_files = sys.argv[1:] -out_dir = Path(out_dylib).parent.resolve() +# If no arguments provided, build for the anki_mac_helper package +if len(sys.argv) == 1: + script_dir = Path(__file__).parent + out_dylib = script_dir / "anki_mac_helper" / "libankihelper.dylib" + src_files = list(script_dir.glob("*.swift")) +else: + out_dylib, *src_files = sys.argv[1:] + +out_dylib = Path(out_dylib) +out_dir = out_dylib.parent.resolve() src_dir = Path(src_files[0]).parent.resolve() -if platform.machine() == "arm64" and not os.environ.get("MAC_X86"): - target = "arm64-apple-macos11" -else: - target = "x86_64-apple-macos10.14" +# Build for both architectures +architectures = ["arm64", "x86_64"] +temp_files = [] -args = [ - "swiftc", - "-target", - target, - "-emit-library", - "-module-name", - "ankihelper", - "-O", +for arch in architectures: + target = f"{arch}-apple-macos11" + temp_out = out_dir / f"temp_{arch}.dylib" + temp_files.append(temp_out) + + args = [ + "swiftc", + "-target", + target, + "-emit-library", + "-module-name", + "ankihelper", + "-O", + ] + if isinstance(src_files[0], Path): + args.extend(src_files) + else: + args.extend(src_dir / Path(file).name for file in src_files) + args.extend(["-o", str(temp_out)]) + subprocess.run(args, check=True, cwd=out_dir) + +# Ensure output directory exists +out_dir.mkdir(parents=True, exist_ok=True) + +# Create universal binary +lipo_args = ["lipo", "-create", "-output", str(out_dylib)] + [ + str(f) for f in temp_files ] -args.extend(src_dir / Path(file).name for file in src_files) -subprocess.run(args, check=True, cwd=out_dir) +subprocess.run(lipo_args, check=True) + +# Clean up temporary files +for temp_file in temp_files: + temp_file.unlink() diff --git a/qt/mac/pyproject.toml b/qt/mac/pyproject.toml new file mode 100644 index 000000000..93f4e939b --- /dev/null +++ b/qt/mac/pyproject.toml @@ -0,0 +1,17 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "anki-mac-helper" +version = "0.1.0" +description = "Small support library for Anki on Macs" +requires-python = ">=3.9" +license = { text = "AGPL-3.0-or-later" } +authors = [ + { name = "Anki Team" }, +] +urls = { Homepage = "https://github.com/ankitects/anki" } + +[tool.hatch.build.targets.wheel] +packages = ["anki_mac_helper"] diff --git a/qt/pyproject.toml b/qt/pyproject.toml new file mode 100644 index 000000000..ed4b372b0 --- /dev/null +++ b/qt/pyproject.toml @@ -0,0 +1,92 @@ +[project] +name = "aqt" +dynamic = ["version"] +requires-python = ">=3.9" +license = "AGPL-3.0-or-later" +dependencies = [ + "beautifulsoup4", + "flask[async]", + "flask_cors", + "jsonschema", + "requests", + "send2trash", + "waitress>=2.0.0", + "psutil; sys.platform == 'win32'", + "pywin32; sys.platform == 'win32'", + "anki-mac-helper; sys.platform == 'darwin'", + "pip-system-certs!=5.1", + "mock", + "types-decorator", + "types-flask", + "types-flask-cors", + "types-markdown", + "types-waitress", + "types-pywin32", + "pyqt6>=6.2", + "pyqt6-webengine>=6.2", + # anki dependency is added dynamically in hatch_build.py with exact version +] + +[project.optional-dependencies] +audio = [ + "anki-audio==0.1.0; sys.platform == 'win32' or sys.platform == 'darwin'", +] +qt66 = [ + "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", +] +qt67 = [ + "pyqt6==6.7.1", + "pyqt6-qt6==6.7.3", + "pyqt6-webengine==6.7.0", + "pyqt6-webengine-qt6==6.7.3", + "pyqt6_sip==13.10.2", +] +qt69 = [ + "pyqt6==6.9.1", + "pyqt6-qt6==6.9.1", + "pyqt6-webengine==6.9.0", + "pyqt6-webengine-qt6==6.9.1", + "pyqt6_sip==13.10.2", +] +qt = [ + "pyqt6==6.8.0", + "pyqt6-qt6==6.8.1", + "pyqt6-webengine==6.8.0", + "pyqt6-webengine-qt6==6.8.1", + "pyqt6_sip==13.10.2", +] + +[tool.uv] +conflicts = [ + [ + { extra = "qt" }, + { extra = "qt66" }, + { extra = "qt67" }, + { extra = "qt69" }, + ], +] + +[tool.uv.sources] +anki = { workspace = true } + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project.scripts] +anki = "aqt:run" + +[tool.hatch.build.targets.wheel] +packages = ["aqt"] +exclude = ["**/*.scss", "**/*.ui"] + +[tool.hatch.version] +source = "code" +path = "../python/version.py" + +[tool.hatch.build.hooks.custom] +path = "hatch_build.py" diff --git a/qt/release/.gitignore b/qt/release/.gitignore new file mode 100644 index 000000000..b09017c0e --- /dev/null +++ b/qt/release/.gitignore @@ -0,0 +1,2 @@ +pyproject.toml +pyproject.toml.old \ No newline at end of file diff --git a/qt/release/publish.sh b/qt/release/publish.sh new file mode 100755 index 000000000..273e34953 --- /dev/null +++ b/qt/release/publish.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +# Get the project root (two levels up from qt/release) +PROJ_ROOT="$(cd "$(dirname "$0")/../.." && pwd)" + +# Use extracted uv binary +UV="$PROJ_ROOT/out/extracted/uv/uv" + +rm -rf dist +"$UV" build --wheel + +#UV_PUBLISH_TOKEN=$(pass show w/pypi-api-test) "$UV" publish --index testpypi +UV_PUBLISH_TOKEN=$(pass show w/pypi-api) "$UV" publish diff --git a/qt/release/update.sh b/qt/release/update.sh new file mode 100755 index 000000000..ddd99c398 --- /dev/null +++ b/qt/release/update.sh @@ -0,0 +1,68 @@ +#!/bin/bash + +set -e + +test -f update.sh || { + echo "run from release folder" + exit 1 +} + +# Get the project root (two levels up from qt/release) +PROJ_ROOT="$(cd "$(dirname "$0")/../.." && pwd)" + +# Use extracted uv binary +UV="$PROJ_ROOT/out/extracted/uv/uv" + +# Read version from .version file +VERSION=$(cat "$PROJ_ROOT/.version" | tr -d '[:space:]') + +# Copy existing pyproject.toml to .old if it exists +if [ -f pyproject.toml ]; then + cp pyproject.toml pyproject.toml.old +fi + +# Export dependencies using uv +echo "Exporting dependencies..." +rm -f pyproject.toml +DEPS=$(cd "$PROJ_ROOT" && "$UV" export --no-hashes --no-annotate --no-header --extra audio --extra qt --all-packages --no-dev --no-emit-workspace) + +# Generate the pyproject.toml file +cat > pyproject.toml << EOF +[project] +name = "anki-release" +version = "$VERSION" +description = "A package to lock Anki's dependencies" +requires-python = ">=3.9" +dependencies = [ + "anki==$VERSION", + "aqt==$VERSION", +EOF + +# Add the exported dependencies to the file +echo "$DEPS" | while IFS= read -r line; do + if [[ -n "$line" ]]; then + echo " \"$line\"," >> pyproject.toml + fi +done + +# Complete the pyproject.toml file +cat >> pyproject.toml << 'EOF' +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +# hatch throws an error if nothing is included +[tool.hatch.build.targets.wheel] +include = ["no-such-file"] +EOF + +echo "Generated pyproject.toml with version $VERSION" + +# Show diff if .old file exists +if [ -f pyproject.toml.old ]; then + echo + echo "Differences from previous version:" + diff -u --color=always pyproject.toml.old pyproject.toml || true +fi diff --git a/qt/tools/build_ui.py b/qt/tools/build_ui.py index 776375598..b87031213 100644 --- a/qt/tools/build_ui.py +++ b/qt/tools/build_ui.py @@ -6,16 +6,10 @@ from __future__ import annotations import io import re import sys +from dataclasses import dataclass from pathlib import Path -try: - from PyQt6.uic import compileUi -except ImportError: - # ARM64 Linux builds may not have access to PyQt6, and may have aliased - # it to PyQt5. We allow fallback, but the _qt6.py files will not be valid. - from PyQt5.uic import compileUi # type: ignore - -from dataclasses import dataclass +from PyQt6.uic import compileUi def compile(ui_file: str | Path) -> str: @@ -53,21 +47,9 @@ def with_fixes_for_qt6(code: str) -> str: return "\n".join(outlines) -def with_fixes_for_qt5(code: str) -> str: - code = code.replace( - "from PyQt5 import QtCore, QtGui, QtWidgets", - "from PyQt5 import QtCore, QtGui, QtWidgets\nfrom aqt.utils import tr\n", - ) - code = code.replace("Qt6", "Qt5") - code = code.replace("QtGui.QAction", "QtWidgets.QAction") - code = code.replace("import icons_rc", "") - return code - - @dataclass class UiFileAndOutputs: ui_file: Path - qt5_file: str qt6_file: str @@ -82,7 +64,6 @@ def get_files() -> list[UiFileAndOutputs]: out.append( UiFileAndOutputs( ui_file=path, - qt5_file=outpath.replace(".ui", "_qt5.py"), qt6_file=outpath.replace(".ui", "_qt6.py"), ) ) @@ -93,8 +74,5 @@ if __name__ == "__main__": for entry in get_files(): stock = compile(entry.ui_file) for_qt6 = with_fixes_for_qt6(stock) - for_qt5 = with_fixes_for_qt5(for_qt6) - with open(entry.qt5_file, "w") as file: - file.write(for_qt5) with open(entry.qt6_file, "w") as file: file.write(for_qt6) diff --git a/rslib/Cargo.toml b/rslib/Cargo.toml index 1c4abf46f..d3c7e215f 100644 --- a/rslib/Cargo.toml +++ b/rslib/Cargo.toml @@ -106,6 +106,5 @@ unicode-normalization.workspace = true zip.workspace = true zstd.workspace = true -[target.'cfg(windows)'.dependencies.windows] -version = "0.56.0" -features = ["Media_SpeechSynthesis", "Foundation_Collections", "Storage_Streams"] +[target.'cfg(windows)'.dependencies] +windows.workspace = true diff --git a/rslib/io/src/lib.rs b/rslib/io/src/lib.rs index c1d4c0205..cb44467e6 100644 --- a/rslib/io/src/lib.rs +++ b/rslib/io/src/lib.rs @@ -152,6 +152,34 @@ pub fn copy_file(src: impl AsRef, dst: impl AsRef) -> Result { }) } +/// Copy a file from src to dst if dst doesn't exist or if src is newer than +/// dst. Preserves the modification time from the source file. +pub fn copy_if_newer(src: impl AsRef, dst: impl AsRef) -> Result { + let src = src.as_ref(); + let dst = dst.as_ref(); + + let should_copy = if !dst.exists() { + true + } else { + let src_time = modified_time(src)?; + let dst_time = modified_time(dst)?; + src_time > dst_time + }; + + if should_copy { + copy_file(src, dst)?; + + // Preserve the modification time from the source file + let src_mtime = modified_time(src)?; + let times = FileTimes::new().set_modified(src_mtime); + set_file_times(dst, times)?; + + Ok(true) + } else { + Ok(false) + } +} + /// Like [read_file], but skips the section that is potentially locked by /// SQLite. pub fn read_locked_db_file(path: impl AsRef) -> Result> { @@ -188,6 +216,14 @@ pub fn metadata(path: impl AsRef) -> Result { }) } +/// Get the modification time of a file. +pub fn modified_time(path: impl AsRef) -> Result { + metadata(&path)?.modified().context(FileIoSnafu { + path: path.as_ref(), + op: FileOp::Metadata, + }) +} + pub fn new_tempfile() -> Result { NamedTempFile::new().context(FileIoSnafu { path: std::env::temp_dir(), diff --git a/rslib/linkchecker/tests/links.rs b/rslib/linkchecker/tests/links.rs index 48656ace4..2f39fbe31 100644 --- a/rslib/linkchecker/tests/links.rs +++ b/rslib/linkchecker/tests/links.rs @@ -52,7 +52,7 @@ impl CheckableUrl { fn anchor(&self) -> Cow { match *self { Self::HelpPage(page) => help_page_link_suffix(page).into(), - Self::String(s) => s.split('#').last().unwrap_or_default().into(), + Self::String(s) => s.split('#').next_back().unwrap_or_default().into(), } } } diff --git a/rslib/process/src/lib.rs b/rslib/process/src/lib.rs index d026dec34..dcf0703f6 100644 --- a/rslib/process/src/lib.rs +++ b/rslib/process/src/lib.rs @@ -57,6 +57,9 @@ pub trait CommandExt { fn ensure_success(&mut self) -> Result<&mut Self>; fn utf8_output(&mut self) -> Result; + fn ensure_spawn(&mut self) -> Result; + #[cfg(unix)] + fn ensure_exec(&mut self) -> Result<()>; } impl CommandExt for Command { @@ -94,6 +97,23 @@ impl CommandExt for Command { })?, }) } + + fn ensure_spawn(&mut self) -> Result { + self.spawn().with_context(|_| DidNotExecuteSnafu { + cmdline: get_cmdline(self), + }) + } + + #[cfg(unix)] + fn ensure_exec(&mut self) -> Result<()> { + use std::os::unix::process::CommandExt as UnixCommandExt; + let cmdline = get_cmdline(self); + let error = self.exec(); + Err(Error::DidNotExecute { + cmdline, + source: error, + }) + } } fn get_cmdline(arg: &mut Command) -> String { diff --git a/rslib/src/card/mod.rs b/rslib/src/card/mod.rs index 8d7821e2c..49d952ecf 100644 --- a/rslib/src/card/mod.rs +++ b/rslib/src/card/mod.rs @@ -189,6 +189,8 @@ impl Card { fn set_deck(&mut self, deck: DeckId) { self.remove_from_filtered_deck_restoring_queue(); self.memory_state = None; + self.desired_retention = None; + self.decay = None; self.deck_id = deck; } diff --git a/rslib/src/card_rendering/tts/windows.rs b/rslib/src/card_rendering/tts/windows.rs index a53994c2e..5afb5cc2d 100644 --- a/rslib/src/card_rendering/tts/windows.rs +++ b/rslib/src/card_rendering/tts/windows.rs @@ -5,12 +5,13 @@ use std::fs::File; use std::io::Write; use anki_proto::card_rendering::all_tts_voices_response::TtsVoice; -use futures::executor::block_on; +use windows::core::Interface; use windows::core::HSTRING; use windows::Media::SpeechSynthesis::SpeechSynthesisStream; use windows::Media::SpeechSynthesis::SpeechSynthesizer; use windows::Media::SpeechSynthesis::VoiceInformation; use windows::Storage::Streams::DataReader; +use windows::Storage::Streams::IRandomAccessStream; use crate::error::windows::WindowsErrorDetails; use crate::error::windows::WindowsSnafu; @@ -45,7 +46,7 @@ fn find_voice(voice_id: &str) -> Result { fn to_hstring(text: &str) -> HSTRING { let utf16: Vec = text.encode_utf16().collect(); - HSTRING::from_wide(&utf16).expect("Strings are valid Unicode") + HSTRING::from_wide(&utf16) } fn synthesize_stream( @@ -64,16 +65,20 @@ fn synthesize_stream( details: WindowsErrorDetails::SettingRate(speed), })?; let async_op = synthesizer.SynthesizeTextToStreamAsync(&to_hstring(text))?; - let stream = block_on(async_op).context(WindowsSnafu { + let stream = async_op.get().context(WindowsSnafu { details: WindowsErrorDetails::Synthesizing, })?; Ok(stream) } fn write_stream_to_path(stream: SpeechSynthesisStream, path: &str) -> Result<()> { - let input_stream = stream.GetInputStreamAt(0)?; + let random_access_stream: IRandomAccessStream = stream.cast()?; + let input_stream = random_access_stream.GetInputStreamAt(0)?; let date_reader = DataReader::CreateDataReader(&input_stream)?; - let stream_size = stream.Size()?.try_into().or_invalid("stream too large")?; + let stream_size = random_access_stream + .Size()? + .try_into() + .or_invalid("stream too large")?; date_reader.LoadAsync(stream_size)?; let mut file = File::create(path)?; write_reader_to_file(date_reader, &mut file, stream_size as usize) diff --git a/rslib/src/config/bool.rs b/rslib/src/config/bool.rs index b430babe4..39273b931 100644 --- a/rslib/src/config/bool.rs +++ b/rslib/src/config/bool.rs @@ -40,6 +40,7 @@ pub enum BoolKey { WithScheduling, WithDeckConfigs, Fsrs, + FsrsHealthCheck, LoadBalancerEnabled, FsrsShortTermWithStepsEnabled, #[strum(to_string = "normalize_note_text")] @@ -76,6 +77,7 @@ impl Collection { | BoolKey::RestorePositionBrowser | BoolKey::RestorePositionReviewer | BoolKey::LoadBalancerEnabled + | BoolKey::FsrsHealthCheck | BoolKey::NormalizeNoteText => self.get_config_optional(key).unwrap_or(true), // other options default to false diff --git a/rslib/src/deckconfig/mod.rs b/rslib/src/deckconfig/mod.rs index c522ea18a..e8fb0c0c0 100644 --- a/rslib/src/deckconfig/mod.rs +++ b/rslib/src/deckconfig/mod.rs @@ -110,9 +110,9 @@ impl DeckConfig { /// Retrieve the FSRS 6.0 params, falling back on 5.0 or 4.x ones. pub fn fsrs_params(&self) -> &Vec { - if self.inner.fsrs_params_6.len() == 21 { + if !self.inner.fsrs_params_6.is_empty() { &self.inner.fsrs_params_6 - } else if self.inner.fsrs_params_5.len() == 19 { + } else if !self.inner.fsrs_params_5.is_empty() { &self.inner.fsrs_params_5 } else { &self.inner.fsrs_params_4 diff --git a/rslib/src/deckconfig/service.rs b/rslib/src/deckconfig/service.rs index 516132763..bc6bce8f4 100644 --- a/rslib/src/deckconfig/service.rs +++ b/rslib/src/deckconfig/service.rs @@ -101,30 +101,64 @@ impl crate::services::DeckConfigService for Collection { &mut self, input: anki_proto::deck_config::GetRetentionWorkloadRequest, ) -> Result { - const LEARN_SPAN: usize = 1000; + const LEARN_SPAN: usize = 100_000_000; + const TERMINATION_PROB: f32 = 0.001; + // the default values are from https://github.com/open-spaced-repetition/Anki-button-usage/blob/881009015c2a85ac911021d76d0aacb124849937/analysis.ipynb + const DEFAULT_LEARN_COST: f32 = 19.4698; + const DEFAULT_PASS_COST: f32 = 7.8454; + const DEFAULT_FAIL_COST: f32 = 23.185; + const DEFAULT_INITIAL_PASS_RATE: f32 = 0.7645; let guard = self.search_cards_into_table(&input.search, crate::search::SortMode::NoOrder)?; - let (pass_cost, fail_cost, learn_cost) = guard.col.storage.get_costs_for_retention()?; + let costs = guard.col.storage.get_costs_for_retention()?; + + fn smoothing(obs: f32, default: f32, count: u32) -> f32 { + let alpha = count as f32 / (50.0 + count as f32); + obs * alpha + default * (1.0 - alpha) + } + + let cost_success = smoothing( + costs.average_pass_time_ms / 1000.0, + DEFAULT_PASS_COST, + costs.pass_count, + ); + let cost_failure = smoothing( + costs.average_fail_time_ms / 1000.0, + DEFAULT_FAIL_COST, + costs.fail_count, + ); + let cost_learn = smoothing( + costs.average_learn_time_ms / 1000.0, + DEFAULT_LEARN_COST, + costs.learn_count, + ); + let initial_pass_rate = smoothing( + costs.initial_pass_rate, + DEFAULT_INITIAL_PASS_RATE, + costs.pass_count, + ); let before = fsrs::expected_workload( &input.w, input.before, LEARN_SPAN, - pass_cost, - fail_cost, - 0., - input.before, - )? + learn_cost; + cost_success, + cost_failure, + cost_learn, + initial_pass_rate, + TERMINATION_PROB, + )?; let after = fsrs::expected_workload( &input.w, input.after, LEARN_SPAN, - pass_cost, - fail_cost, - 0., - input.after, - )? + learn_cost; + cost_success, + cost_failure, + cost_learn, + initial_pass_rate, + TERMINATION_PROB, + )?; Ok(anki_proto::deck_config::GetRetentionWorkloadResponse { factor: after / before, @@ -158,6 +192,7 @@ impl From for UpdateDeckConfi apply_all_parent_limits: c.apply_all_parent_limits, fsrs: c.fsrs, fsrs_reschedule: c.fsrs_reschedule, + fsrs_health_check: c.fsrs_health_check, } } } diff --git a/rslib/src/deckconfig/update.rs b/rslib/src/deckconfig/update.rs index 6d49bc5b1..128e43770 100644 --- a/rslib/src/deckconfig/update.rs +++ b/rslib/src/deckconfig/update.rs @@ -22,6 +22,7 @@ use crate::prelude::*; use crate::scheduler::fsrs::memory_state::UpdateMemoryStateEntry; use crate::scheduler::fsrs::memory_state::UpdateMemoryStateRequest; use crate::scheduler::fsrs::params::ignore_revlogs_before_ms_from_config; +use crate::scheduler::fsrs::params::ComputeParamsRequest; use crate::search::JoinSearches; use crate::search::Negated; use crate::search::SearchNode; @@ -41,6 +42,7 @@ pub struct UpdateDeckConfigsRequest { pub apply_all_parent_limits: bool, pub fsrs: bool, pub fsrs_reschedule: bool, + pub fsrs_health_check: bool, } impl Collection { @@ -71,6 +73,7 @@ impl Collection { new_cards_ignore_review_limit: self.get_config_bool(BoolKey::NewCardsIgnoreReviewLimit), apply_all_parent_limits: self.get_config_bool(BoolKey::ApplyAllParentLimits), fsrs: self.get_config_bool(BoolKey::Fsrs), + fsrs_health_check: self.get_config_bool(BoolKey::FsrsHealthCheck), days_since_last_fsrs_optimize, }) } @@ -300,6 +303,7 @@ impl Collection { req.new_cards_ignore_review_limit, )?; self.set_config_bool_inner(BoolKey::ApplyAllParentLimits, req.apply_all_parent_limits)?; + self.set_config_bool_inner(BoolKey::FsrsHealthCheck, req.fsrs_health_check)?; Ok(()) } @@ -365,14 +369,15 @@ impl Collection { }; let ignore_revlogs_before_ms = ignore_revlogs_before_ms_from_config(config)?; let num_of_relearning_steps = config.inner.relearn_steps.len(); - match self.compute_params( - &search, + match self.compute_params(ComputeParamsRequest { + search: &search, ignore_revlogs_before_ms, - idx as u32 + 1, - config_len, - config.fsrs_params(), + current_preset: idx as u32 + 1, + total_presets: config_len, + current_params: config.fsrs_params(), num_of_relearning_steps, - ) { + health_check: false, + }) { Ok(params) => { println!("{}: {:?}", config.name, params.params); config.inner.fsrs_params_6 = params.params; @@ -452,6 +457,7 @@ mod test { col.set_config_string_inner(StringKey::CardStateCustomizer, "")?; col.set_config_bool_inner(BoolKey::NewCardsIgnoreReviewLimit, false)?; col.set_config_bool_inner(BoolKey::ApplyAllParentLimits, false)?; + col.set_config_bool_inner(BoolKey::FsrsHealthCheck, true)?; // pretend we're in sync let stamps = col.storage.get_collection_timestamps()?; @@ -488,6 +494,7 @@ mod test { apply_all_parent_limits: false, fsrs: false, fsrs_reschedule: false, + fsrs_health_check: true, }; assert!(!col.update_deck_configs(input.clone())?.changes.had_change()); diff --git a/rslib/src/image_occlusion/imageocclusion.rs b/rslib/src/image_occlusion/imageocclusion.rs index 0658b4319..2ba83374f 100644 --- a/rslib/src/image_occlusion/imageocclusion.rs +++ b/rslib/src/image_occlusion/imageocclusion.rs @@ -64,19 +64,9 @@ pub fn get_image_cloze_data(text: &str) -> String { } for property in occlusion.properties { match property.name.as_str() { - "left" => { + "left" | "top" | "angle" | "fill" => { if !property.value.is_empty() { - result.push_str(&format!("data-left=\"{}\" ", property.value)); - } - } - "top" => { - if !property.value.is_empty() { - result.push_str(&format!("data-top=\"{}\" ", property.value)); - } - } - "angle" => { - if !property.value.is_empty() { - result.push_str(&format!("data-angle=\"{}\" ", property.value)); + result.push_str(&format!("data-{}=\"{}\" ", property.name, property.value)); } } "width" => { diff --git a/rslib/src/import_export/package/colpkg/export.rs b/rslib/src/import_export/package/colpkg/export.rs index 9d85c19aa..20f25a1c1 100644 --- a/rslib/src/import_export/package/colpkg/export.rs +++ b/rslib/src/import_export/package/colpkg/export.rs @@ -146,8 +146,12 @@ pub(crate) fn export_collection( Ok(()) } -fn file_options_stored() -> FileOptions { - FileOptions::default().compression_method(CompressionMethod::Stored) +fn file_options_stored() -> FileOptions<'static, ()> { + FileOptions::<'static, ()>::default().compression_method(CompressionMethod::Stored) +} + +fn file_options_default() -> FileOptions<'static, ()> { + FileOptions::<'static, ()>::default() } fn write_collection( @@ -160,7 +164,7 @@ fn write_collection( zip.start_file(meta.collection_filename(), file_options_stored())?; zstd_copy(col, zip, size)?; } else { - zip.start_file(meta.collection_filename(), FileOptions::default())?; + zip.start_file(meta.collection_filename(), file_options_default())?; io::copy(col, zip)?; } Ok(()) diff --git a/rslib/src/import_export/package/colpkg/import.rs b/rslib/src/import_export/package/colpkg/import.rs index b8316c032..5ae8b4c8e 100644 --- a/rslib/src/import_export/package/colpkg/import.rs +++ b/rslib/src/import_export/package/colpkg/import.rs @@ -124,7 +124,7 @@ fn maybe_restore_media_file( Ok(()) } -fn restore_media_file(meta: &Meta, zip_file: &mut ZipFile, path: &Path) -> Result<()> { +fn restore_media_file(meta: &Meta, zip_file: &mut ZipFile, path: &Path) -> Result<()> { let mut tempfile = new_tempfile_in_parent_of(path)?; meta.copy(zip_file, &mut tempfile) .with_context(|_| FileIoSnafu { diff --git a/rslib/src/import_export/package/media.rs b/rslib/src/import_export/package/media.rs index 6b0b4c370..ff5bdf4d7 100644 --- a/rslib/src/import_export/package/media.rs +++ b/rslib/src/import_export/package/media.rs @@ -96,7 +96,10 @@ impl SafeMediaEntry { media_folder.join(&self.name) } - pub(super) fn fetch_file<'a>(&self, archive: &'a mut ZipArchive) -> Result> { + pub(super) fn fetch_file<'a>( + &self, + archive: &'a mut ZipArchive, + ) -> Result> { match archive.by_name(&self.index.to_string()) { Ok(file) => Ok(file), Err(err) => invalid_input!(err, "{} missing from archive", self.index), diff --git a/rslib/src/notetype/cardgen.rs b/rslib/src/notetype/cardgen.rs index 8ee2717ba..8e03d8ee4 100644 --- a/rslib/src/notetype/cardgen.rs +++ b/rslib/src/notetype/cardgen.rs @@ -352,7 +352,7 @@ impl Collection { fn random_position(highest_position: u32) -> u32 { let mut rng = StdRng::seed_from_u64(highest_position as u64); - rng.gen_range(1..highest_position.max(1000)) + rng.random_range(1..highest_position.max(1000)) } #[cfg(test)] diff --git a/rslib/src/notetype/mod.rs b/rslib/src/notetype/mod.rs index 914170bc4..5c8c7b87a 100644 --- a/rslib/src/notetype/mod.rs +++ b/rslib/src/notetype/mod.rs @@ -77,6 +77,7 @@ static SPECIAL_FIELDS: LazyLock> = LazyLock::new(|| { "Subdeck", "Tags", "Type", + "CardID", ]) }); diff --git a/rslib/src/notetype/render.rs b/rslib/src/notetype/render.rs index 8f686b0a5..08c5677b0 100644 --- a/rslib/src/notetype/render.rs +++ b/rslib/src/notetype/render.rs @@ -183,6 +183,8 @@ impl Collection { .or_insert_with(|| flag_name(card.flags).into()); map.entry("Card") .or_insert_with(|| template.name.clone().into()); + map.entry("CardID") + .or_insert_with(|| card.id.to_string().into()); Ok(()) } @@ -212,6 +214,7 @@ fn fill_empty_fields(note: &mut Note, qfmt: &str, nt: &Notetype, tr: &I18n) { mod test { use super::*; use crate::collection::CollectionBuilder; + use crate::notetype::SPECIAL_FIELDS; #[test] fn can_render_fully() -> Result<()> { @@ -233,4 +236,20 @@ mod test { Ok(()) } + + #[test] + fn special_fields_complete() -> Result<()> { + let mut col = CollectionBuilder::default().build()?; + let mut map = HashMap::new(); + + let nt = col.get_notetype_by_name("Basic")?.unwrap(); + let note = Note::new(&nt); + let card = Card::new(0.into(), 0.try_into().unwrap(), 0.into(), 0); + let tmpl = nt.templates[0].clone(); + + col.add_special_fields(&mut map, ¬e, &card, &nt, &tmpl)?; + + assert!(map.iter().all(|val| SPECIAL_FIELDS.contains(val.0))); + Ok(()) + } } diff --git a/rslib/src/notetype/styling.css b/rslib/src/notetype/styling.css index 3872575fb..b14c63653 100644 --- a/rslib/src/notetype/styling.css +++ b/rslib/src/notetype/styling.css @@ -1,6 +1,7 @@ .card { font-family: arial; font-size: 20px; + line-height: 1.5; text-align: center; color: black; background-color: white; diff --git a/rslib/src/scheduler/answering/learning.rs b/rslib/src/scheduler/answering/learning.rs index 80204e13f..3283dc3ee 100644 --- a/rslib/src/scheduler/answering/learning.rs +++ b/rslib/src/scheduler/answering/learning.rs @@ -87,7 +87,7 @@ impl CardStateUpdater { if secs >= upper_exclusive { secs } else { - rng.gen_range(secs..upper_exclusive) + rng.random_range(secs..upper_exclusive) } } else { secs diff --git a/rslib/src/scheduler/answering/mod.rs b/rslib/src/scheduler/answering/mod.rs index 02e5ee8d6..d498f7eaf 100644 --- a/rslib/src/scheduler/answering/mod.rs +++ b/rslib/src/scheduler/answering/mod.rs @@ -630,7 +630,7 @@ fn get_fuzz_seed_for_id_and_reps(card_id: CardId, card_reps: u32) -> Option /// Return a fuzz factor from the range `0.0..1.0`, using the provided seed. /// None if seed is None. fn get_fuzz_factor(seed: Option) -> Option { - seed.map(|s| StdRng::seed_from_u64(s).gen_range(0.0..1.0)) + seed.map(|s| StdRng::seed_from_u64(s).random_range(0.0..1.0)) } #[cfg(test)] diff --git a/rslib/src/scheduler/fsrs/memory_state.rs b/rslib/src/scheduler/fsrs/memory_state.rs index 7d4346d06..f5af97674 100644 --- a/rslib/src/scheduler/fsrs/memory_state.rs +++ b/rslib/src/scheduler/fsrs/memory_state.rs @@ -16,7 +16,6 @@ use super::rescheduler::Rescheduler; use crate::card::CardType; use crate::prelude::*; use crate::revlog::RevlogEntry; -use crate::revlog::RevlogReviewKind; use crate::scheduler::answering::get_fuzz_seed; use crate::scheduler::fsrs::params::reviews_for_fsrs; use crate::scheduler::fsrs::params::Params; @@ -31,6 +30,18 @@ pub struct ComputeMemoryProgress { pub total_cards: u32, } +/// Helper function to determine the appropriate decay value based on FSRS +/// parameters +fn get_decay_from_params(params: &[f32]) -> f32 { + if params.is_empty() { + FSRS6_DEFAULT_DECAY // default decay for FSRS-6 + } else if params.len() < 21 { + FSRS5_DEFAULT_DECAY // default decay for FSRS-4.5 and FSRS-5 + } else { + params[20] + } +} + #[derive(Debug)] pub(crate) struct UpdateMemoryStateRequest { pub params: Params, @@ -78,15 +89,7 @@ impl Collection { .then(|| Rescheduler::new(self)) .transpose()?; let fsrs = FSRS::new(req.as_ref().map(|w| &w.params[..]).or(Some([].as_slice())))?; - let decay = req.as_ref().map(|w| { - if w.params.is_empty() { - FSRS6_DEFAULT_DECAY // default decay for FSRS-6 - } else if w.params.len() < 21 { - FSRS5_DEFAULT_DECAY // default decay for FSRS-4.5 and FSRS-5 - } else { - w.params[20] - } - }); + let decay = req.as_ref().map(|w| get_decay_from_params(&w.params)); let historical_retention = req.as_ref().map(|w| w.historical_retention); let items = fsrs_items_for_memory_states( &fsrs, @@ -163,15 +166,8 @@ impl Collection { ); } *due = new_due; - // Add a rescheduled revlog entry if the last entry wasn't - // rescheduled - if !last_info.last_revlog_is_rescheduled { - self.log_rescheduled_review( - &card, - original_interval, - usn, - )?; - } + // Add a rescheduled revlog entry + self.log_rescheduled_review(&card, original_interval, usn)?; } } } @@ -198,7 +194,9 @@ impl Collection { .or_not_found(conf_id)?; let desired_retention = config.inner.desired_retention; let historical_retention = config.inner.historical_retention; - let fsrs = FSRS::new(Some(config.fsrs_params()))?; + let params = config.fsrs_params(); + let decay = get_decay_from_params(params); + let fsrs = FSRS::new(Some(params))?; let revlog = self.revlog_for_srs(SearchNode::CardIds(card.id.to_string()))?; let item = fsrs_item_for_memory_state( &fsrs, @@ -212,6 +210,7 @@ impl Collection { Ok(ComputeMemoryStateResponse { state: card.memory_state.map(Into::into), desired_retention, + decay, }) } else { card.memory_state = None; @@ -219,6 +218,7 @@ impl Collection { Ok(ComputeMemoryStateResponse { state: None, desired_retention, + decay, }) } } @@ -289,9 +289,6 @@ struct LastRevlogInfo { /// reviewed the card and now, so that we can determine an accurate period /// when the card has subsequently been rescheduled to a different day. last_reviewed_at: Option, - /// If true, the last action on this card was a reschedule, so we - /// can avoid writing an extra revlog entry on another reschedule. - last_revlog_is_rescheduled: bool, } /// Return a map of cards to info about last review/reschedule. @@ -303,20 +300,12 @@ fn get_last_revlog_info(revlogs: &[RevlogEntry]) -> HashMap= 1 { last_reviewed_at = Some(e.id.as_secs()); } - last_revlog_is_rescheduled = e.review_kind == RevlogReviewKind::Rescheduled; } - out.insert( - card_id, - LastRevlogInfo { - last_reviewed_at, - last_revlog_is_rescheduled, - }, - ); + out.insert(card_id, LastRevlogInfo { last_reviewed_at }); }); out } @@ -424,8 +413,8 @@ mod tests { assert_int_eq( item.starting_state.map(Into::into), Some(FsrsMemoryState { - stability: 99.999954, - difficulty: 5.6932373, + stability: 100.0, + difficulty: 5.003576, }), ); let mut card = Card { @@ -436,8 +425,8 @@ mod tests { assert_int_eq( card.memory_state, Some(FsrsMemoryState { - stability: 248.64305, - difficulty: 5.7909784, + stability: 248.9251, + difficulty: 4.9938006, }), ); // cards with a single review-type entry also get memory states from revlog @@ -459,8 +448,8 @@ mod tests { assert_int_eq( card.memory_state, Some(FsrsMemoryState { - stability: 99.999954, - difficulty: 5.840841, + stability: 100.0, + difficulty: 5.003576, }), ); Ok(()) diff --git a/rslib/src/scheduler/fsrs/params.rs b/rslib/src/scheduler/fsrs/params.rs index 6da02776a..76bc206be 100644 --- a/rslib/src/scheduler/fsrs/params.rs +++ b/rslib/src/scheduler/fsrs/params.rs @@ -51,19 +51,46 @@ pub(crate) fn ignore_revlogs_before_ms_from_config(config: &DeckConfig) -> Resul ignore_revlogs_before_date_to_ms(&config.inner.ignore_revlogs_before_date) } +pub struct ComputeParamsRequest<'t> { + pub search: &'t str, + pub ignore_revlogs_before_ms: TimestampMillis, + pub current_preset: u32, + pub total_presets: u32, + pub current_params: &'t Params, + pub num_of_relearning_steps: usize, + pub health_check: bool, +} + +/// r: retention +fn log_loss_adjustment(r: f32) -> f32 { + 0.623 * (4. * r * (1. - r)).powf(0.738) +} + +/// r: retention +/// +/// c: review count +fn rmse_adjustment(r: f32, c: u32) -> f32 { + 0.0135 / (r.powf(0.504) - 1.14) + 0.176 / ((c as f32 / 1000.).powf(0.825) + 2.22) + 0.101 +} + impl Collection { /// Note this does not return an error if there are less than 400 items - /// the caller should instead check the fsrs_items count in the return /// value. pub fn compute_params( &mut self, - search: &str, - ignore_revlogs_before: TimestampMillis, - current_preset: u32, - total_presets: u32, - current_params: &Params, - num_of_relearning_steps: usize, + request: ComputeParamsRequest, ) -> Result { + let ComputeParamsRequest { + search, + ignore_revlogs_before_ms: ignore_revlogs_before, + current_preset, + total_presets, + current_params, + num_of_relearning_steps, + health_check, + } = request; + self.clear_progress(); let timing = self.timing_today()?; let revlogs = self.revlog_for_srs(search)?; @@ -75,6 +102,7 @@ impl Collection { return Ok(ComputeFsrsParamsResponse { params: current_params.to_vec(), fsrs_items, + health_check_passed: None, }); } // adapt the progress handler to our built-in progress handling @@ -108,22 +136,22 @@ impl Collection { let (progress, progress_thread) = create_progress_thread()?; let fsrs = FSRS::new(None)?; - let mut params = fsrs.compute_parameters(ComputeParametersInput { + let input = ComputeParametersInput { train_set: items.clone(), progress: Some(progress.clone()), enable_short_term: true, num_relearning_steps: Some(num_of_relearning_steps), - })?; + }; + let mut params = fsrs.compute_parameters(input.clone())?; progress_thread.join().ok(); - if let Ok(fsrs) = FSRS::new(Some(current_params)) { - let current_log_loss = fsrs.evaluate(items.clone(), |_| true)?.log_loss; + if let Ok(current_fsrs) = FSRS::new(Some(current_params)) { + let current_log_loss = current_fsrs.evaluate(items.clone(), |_| true)?.log_loss; let optimized_fsrs = FSRS::new(Some(¶ms))?; let optimized_log_loss = optimized_fsrs.evaluate(items.clone(), |_| true)?.log_loss; if current_log_loss <= optimized_log_loss { if num_of_relearning_steps <= 1 { params = current_params.to_vec(); } else { - let current_fsrs = FSRS::new(Some(current_params))?; let memory_state = MemoryState { stability: 1.0, difficulty: 1.0, @@ -146,7 +174,34 @@ impl Collection { } } - Ok(ComputeFsrsParamsResponse { params, fsrs_items }) + let health_check_passed = if health_check { + let fsrs = FSRS::new(None)?; + fsrs.evaluate_with_time_series_splits(input, |_| true) + .ok() + .map(|eval| { + let r = items.iter().fold(0, |p, item| { + p + (item + .reviews + .last() + .map(|reviews| reviews.rating) + .unwrap_or(0) + > 1) as u32 + }) as f32 + / fsrs_items as f32; + let adjusted_log_loss = eval.log_loss / log_loss_adjustment(r); + let adjusted_rmse = eval.rmse_bins / rmse_adjustment(r, fsrs_items); + + adjusted_log_loss <= 1.11 || adjusted_rmse <= 1.53 + }) + } else { + None + }; + + Ok(ComputeFsrsParamsResponse { + params, + fsrs_items, + health_check_passed, + }) } pub(crate) fn revlog_for_srs( @@ -218,22 +273,24 @@ impl Collection { pub fn evaluate_params( &mut self, - params: &Params, search: &str, ignore_revlogs_before: TimestampMillis, + num_of_relearning_steps: usize, ) -> Result { let timing = self.timing_today()?; - let mut anki_progress = self.new_progress_handler::(); - let guard = self.search_cards_into_table(search, SortMode::NoOrder)?; - let revlogs: Vec = guard - .col - .storage - .get_revlog_entries_for_searched_cards_in_card_order()?; + let revlogs = self.revlog_for_srs(search)?; let (items, review_count) = fsrs_items_for_training(revlogs, timing.next_day_at, ignore_revlogs_before); + let mut anki_progress = self.new_progress_handler::(); anki_progress.state.reviews = review_count as u32; - let fsrs = FSRS::new(Some(params))?; - Ok(fsrs.evaluate(items, |ip| { + let fsrs = FSRS::new(None)?; + let input = ComputeParametersInput { + train_set: items.clone(), + progress: None, + enable_short_term: true, + num_relearning_steps: Some(num_of_relearning_steps), + }; + Ok(fsrs.evaluate_with_time_series_splits(input, |ip| { anki_progress .update(false, |p| { p.total_iterations = ip.total as u32; diff --git a/rslib/src/scheduler/fsrs/retention.rs b/rslib/src/scheduler/fsrs/retention.rs index c724709fb..4c21623bb 100644 --- a/rslib/src/scheduler/fsrs/retention.rs +++ b/rslib/src/scheduler/fsrs/retention.rs @@ -34,6 +34,7 @@ impl Collection { .is_ok() }, Some(cards), + None, )? .clamp(0.7, 0.95)) } diff --git a/rslib/src/scheduler/fsrs/simulator.rs b/rslib/src/scheduler/fsrs/simulator.rs index 133b3ff2c..34cc925d6 100644 --- a/rslib/src/scheduler/fsrs/simulator.rs +++ b/rslib/src/scheduler/fsrs/simulator.rs @@ -68,7 +68,7 @@ pub(crate) fn apply_load_balance_and_easy_days( sibling_modifier: 1.0, easy_days_modifier: easy_days_modifier[interval_index], }); - let fuzz_seed = rng.gen(); + let fuzz_seed = rng.random(); select_weighted_interval(intervals, Some(fuzz_seed)).unwrap() as f32 } @@ -106,7 +106,7 @@ fn create_review_priority_fn( // Random ordering Random => { - wrap!(move |_c, _w| rand::thread_rng().gen_range(0..deck_size) as i32) + wrap!(move |_c, _w| rand::rng().random_range(0..deck_size) as i32) } // Not implemented yet diff --git a/rslib/src/scheduler/mod.rs b/rslib/src/scheduler/mod.rs index b2cd8049e..93aee3c9b 100644 --- a/rslib/src/scheduler/mod.rs +++ b/rslib/src/scheduler/mod.rs @@ -34,8 +34,9 @@ pub struct SchedulerInfo { impl Collection { pub fn scheduler_info(&mut self) -> Result { let now = TimestampSecs::now(); - if let Some(info) = self.state.scheduler_info { + if let Some(mut info) = self.state.scheduler_info { if now < info.timing.next_day_at { + info.timing.now = now; return Ok(info); } } diff --git a/rslib/src/scheduler/new.rs b/rslib/src/scheduler/new.rs index 73b7abf5f..541d8d55e 100644 --- a/rslib/src/scheduler/new.rs +++ b/rslib/src/scheduler/new.rs @@ -127,7 +127,7 @@ fn nids_in_desired_order(cards: &[Card], order: NewCardDueOrder) -> Vec nids.sort_unstable(); } NewCardDueOrder::Random => { - nids.shuffle(&mut rand::thread_rng()); + nids.shuffle(&mut rand::rng()); } NewCardDueOrder::Preserve => unreachable!(), } diff --git a/rslib/src/scheduler/reviews.rs b/rslib/src/scheduler/reviews.rs index 53802fb9a..7e61447da 100644 --- a/rslib/src/scheduler/reviews.rs +++ b/rslib/src/scheduler/reviews.rs @@ -4,8 +4,8 @@ use std::collections::HashMap; use std::sync::LazyLock; -use rand::distributions::Distribution; -use rand::distributions::Uniform; +use rand::distr::Distribution; +use rand::distr::Uniform; use regex::Regex; use super::answering::CardAnswer; @@ -34,11 +34,12 @@ impl Card { let new_due = (today + days_from_today) as i32; let fsrs_enabled = self.memory_state.is_some(); let new_interval = if fsrs_enabled { - self.interval.saturating_add_signed(new_due - self.due) - } else if force_reset || !matches!(self.ctype, CardType::Review | CardType::Relearn) { - days_from_today - } else { self.interval + .saturating_add_signed(new_due - self.original_or_current_due()) + } else if force_reset || !matches!(self.ctype, CardType::Review | CardType::Relearn) { + days_from_today.max(1) + } else { + self.interval.max(1) }; let ease_factor = (ease_factor * 1000.0).round() as u16; @@ -48,7 +49,7 @@ impl Card { fn schedule_as_review(&mut self, interval: u32, due: i32, ease_factor: u16) { self.original_position = self.last_position(); self.remove_from_filtered_deck_before_reschedule(); - self.interval = interval.max(1); + self.interval = interval; self.due = due; self.ctype = CardType::Review; self.queue = CardQueue::Review; @@ -113,8 +114,8 @@ impl Collection { let spec = parse_due_date_str(days)?; let usn = self.usn()?; let today = self.timing_today()?.days_elapsed; - let mut rng = rand::thread_rng(); - let distribution = Uniform::from(spec.min..=spec.max); + let mut rng = rand::rng(); + let distribution = Uniform::new_inclusive(spec.min, spec.max).unwrap(); let mut decks_initial_ease: HashMap = HashMap::new(); self.transact(Op::SetDueDate, |col| { for mut card in col.all_cards_for_ids(cids, false)? { diff --git a/rslib/src/scheduler/service/mod.rs b/rslib/src/scheduler/service/mod.rs index e7d9a04eb..993fd1dbe 100644 --- a/rslib/src/scheduler/service/mod.rs +++ b/rslib/src/scheduler/service/mod.rs @@ -23,6 +23,7 @@ use fsrs::FSRS; use crate::backend::Backend; use crate::prelude::*; +use crate::scheduler::fsrs::params::ComputeParamsRequest; use crate::scheduler::new::NewCardDueOrder; use crate::scheduler::states::CardState; use crate::scheduler::states::SchedulingStates; @@ -264,14 +265,15 @@ impl crate::services::SchedulerService for Collection { &mut self, input: scheduler::ComputeFsrsParamsRequest, ) -> Result { - self.compute_params( - &input.search, - input.ignore_revlogs_before_ms.into(), - 1, - 1, - &input.current_params, - input.num_of_relearning_steps as usize, - ) + self.compute_params(ComputeParamsRequest { + search: &input.search, + ignore_revlogs_before_ms: input.ignore_revlogs_before_ms.into(), + current_preset: 1, + total_presets: 1, + current_params: &input.current_params, + num_of_relearning_steps: input.num_of_relearning_steps as usize, + health_check: input.health_check, + }) } fn simulate_fsrs_review( @@ -295,9 +297,9 @@ impl crate::services::SchedulerService for Collection { input: scheduler::EvaluateParamsRequest, ) -> Result { let ret = self.evaluate_params( - &input.params, &input.search, input.ignore_revlogs_before_ms.into(), + input.num_of_relearning_steps as usize, )?; Ok(scheduler::EvaluateParamsResponse { log_loss: ret.log_loss, @@ -372,7 +374,11 @@ impl crate::services::BackendSchedulerService for Backend { enable_short_term: true, num_relearning_steps: None, })?; - Ok(ComputeFsrsParamsResponse { params, fsrs_items }) + Ok(ComputeFsrsParamsResponse { + params, + fsrs_items, + health_check_passed: None, + }) } fn fsrs_benchmark( diff --git a/rslib/src/scheduler/states/load_balancer.rs b/rslib/src/scheduler/states/load_balancer.rs index 915b0b8b3..20b6936df 100644 --- a/rslib/src/scheduler/states/load_balancer.rs +++ b/rslib/src/scheduler/states/load_balancer.rs @@ -5,8 +5,8 @@ use std::collections::HashMap; use std::collections::HashSet; use chrono::Datelike; -use rand::distributions::Distribution; -use rand::distributions::WeightedIndex; +use rand::distr::weighted::WeightedIndex; +use rand::distr::Distribution; use rand::rngs::StdRng; use rand::SeedableRng; diff --git a/rslib/src/scheduler/states/review.rs b/rslib/src/scheduler/states/review.rs index e7591bb25..853cdf0a6 100644 --- a/rslib/src/scheduler/states/review.rs +++ b/rslib/src/scheduler/states/review.rs @@ -83,7 +83,7 @@ impl ReviewState { } else { let (minimum, maximum) = ctx.min_and_max_review_intervals(ctx.minimum_lapse_interval); let interval = ctx.with_review_fuzz( - (self.scheduled_days as f32) * ctx.lapse_multiplier, + (self.scheduled_days as f32).max(1.0) * ctx.lapse_multiplier, minimum, maximum, ); @@ -211,7 +211,7 @@ impl ReviewState { } fn passing_nonearly_review_intervals(self, ctx: &StateContext) -> (u32, u32, u32) { - let current_interval = self.scheduled_days as f32; + let current_interval = (self.scheduled_days as f32).max(1.0); let days_late = self.days_late().max(0) as f32; // hard @@ -251,8 +251,8 @@ impl ReviewState { /// FIXME: this needs reworking in the future; it overly penalizes reviews /// done shortly before the due date. fn passing_early_review_intervals(self, ctx: &StateContext) -> (u32, u32, u32) { - let scheduled = self.scheduled_days as f32; - let elapsed = (self.scheduled_days as f32) + (self.days_late() as f32); + let scheduled = (self.scheduled_days as f32).max(1.0); + let elapsed = self.elapsed_days as f32; let hard_interval = { let factor = ctx.hard_multiplier; diff --git a/rslib/src/storage/card/get_costs_for_retention.sql b/rslib/src/storage/card/get_costs_for_retention.sql index 811ca3050..ba21cc3f6 100644 --- a/rslib/src/storage/card/get_costs_for_retention.sql +++ b/rslib/src/storage/card/get_costs_for_retention.sql @@ -1,5 +1,9 @@ WITH searched_revlogs AS ( - SELECT * + SELECT *, + RANK() OVER ( + PARTITION BY cid + ORDER BY id ASC + ) AS rank_num FROM revlog WHERE ease > 0 AND cid IN search_cids @@ -9,6 +13,7 @@ WITH searched_revlogs AS ( SELECT AVG(time) FROM searched_revlogs WHERE ease > 1 + AND type = 1 ), lapse_count AS ( SELECT COUNT(time) AS lapse_count @@ -42,8 +47,39 @@ summed_learns AS ( average_learn AS ( SELECT AVG(total_time) AS avg_learn_time FROM summed_learns +), +initial_pass_rate AS ( + SELECT AVG( + CASE + WHEN ease > 1 THEN 1.0 + ELSE 0.0 + END + ) AS initial_pass_rate + FROM searched_revlogs + WHERE rank_num = 1 +), +pass_cnt AS ( + SELECT COUNT(*) AS cnt + FROM searched_revlogs + WHERE ease > 1 + AND type = 1 +), +fail_cnt AS ( + SELECT COUNT(*) AS cnt + FROM searched_revlogs + WHERE ease = 1 + AND type = 1 +), +learn_cnt AS ( + SELECT COUNT(*) AS cnt + FROM searched_revlogs + WHERE type = 0 ) SELECT * FROM average_pass, average_fail, - average_learn; \ No newline at end of file + average_learn, + initial_pass_rate, + pass_cnt, + fail_cnt, + learn_cnt; \ No newline at end of file diff --git a/rslib/src/storage/card/mod.rs b/rslib/src/storage/card/mod.rs index bef3251de..38cf5ef0f 100644 --- a/rslib/src/storage/card/mod.rs +++ b/rslib/src/storage/card/mod.rs @@ -42,6 +42,17 @@ use crate::timestamp::TimestampMillis; use crate::timestamp::TimestampSecs; use crate::types::Usn; +#[derive(Debug, Clone, Default)] +pub struct RetentionCosts { + pub average_pass_time_ms: f32, + pub average_fail_time_ms: f32, + pub average_learn_time_ms: f32, + pub initial_pass_rate: f32, + pub pass_count: u32, + pub fail_count: u32, + pub learn_count: u32, +} + impl FromSql for CardType { fn column_result(value: ValueRef<'_>) -> result::Result { if let ValueRef::Integer(i) = value { @@ -747,18 +758,22 @@ impl super::SqliteStorage { .get(0)?) } - pub(crate) fn get_costs_for_retention(&self) -> Result<(f32, f32, f32)> { + pub(crate) fn get_costs_for_retention(&self) -> Result { let mut statement = self .db .prepare(include_str!("get_costs_for_retention.sql"))?; let mut query = statement.query(params![])?; let row = query.next()?.unwrap(); - Ok(( - row.get(0).unwrap_or(7000.), - row.get(1).unwrap_or(23_000.), - row.get(2).unwrap_or(30_000.), - )) + Ok(RetentionCosts { + average_pass_time_ms: row.get(0).unwrap_or(7000.), + average_fail_time_ms: row.get(1).unwrap_or(23_000.), + average_learn_time_ms: row.get(2).unwrap_or(30_000.), + initial_pass_rate: row.get(3).unwrap_or(0.5), + pass_count: row.get(4).unwrap_or(0), + fail_count: row.get(5).unwrap_or(0), + learn_count: row.get(6).unwrap_or(0), + }) } #[cfg(test)] diff --git a/rslib/src/storage/sqlite.rs b/rslib/src/storage/sqlite.rs index f882cee32..e31fdd46a 100644 --- a/rslib/src/storage/sqlite.rs +++ b/rslib/src/storage/sqlite.rs @@ -15,6 +15,7 @@ use fsrs::FSRS5_DEFAULT_DECAY; use regex::Regex; use rusqlite::functions::FunctionFlags; use rusqlite::params; +use rusqlite::trace::TraceEvent; use rusqlite::Connection; use serde_json::Value; use unicase::UniCase; @@ -47,10 +48,13 @@ pub struct SqliteStorage { } fn open_or_create_collection_db(path: &Path) -> Result { - let mut db = Connection::open(path)?; + let db = Connection::open(path)?; if std::env::var("TRACESQL").is_ok() { - db.trace(Some(trace)); + db.trace_v2( + rusqlite::trace::TraceEventCodes::SQLITE_TRACE_STMT, + Some(trace), + ); } db.busy_timeout(std::time::Duration::from_secs(0))?; @@ -415,8 +419,10 @@ fn schema_version(db: &Connection) -> Result<(bool, u8)> { )) } -fn trace(s: &str) { - println!("sql: {}", s.trim().replace('\n', " ")); +fn trace(event: TraceEvent) { + if let TraceEvent::Stmt(_, sql) = event { + println!("sql: {}", sql.trim().replace('\n', " ")); + } } impl SqliteStorage { diff --git a/rslib/src/sync/http_server/mod.rs b/rslib/src/sync/http_server/mod.rs index f96209df7..ddac26670 100644 --- a/rslib/src/sync/http_server/mod.rs +++ b/rslib/src/sync/http_server/mod.rs @@ -22,7 +22,7 @@ use anki_io::create_dir_all; use axum::extract::DefaultBodyLimit; use axum::routing::get; use axum::Router; -use axum_client_ip::SecureClientIpSource; +use axum_client_ip::ClientIpSource; use pbkdf2::password_hash::PasswordHash; use pbkdf2::password_hash::PasswordHasher; use pbkdf2::password_hash::PasswordVerifier; @@ -69,7 +69,7 @@ pub struct SyncServerConfig { #[serde(default = "default_base", rename = "base")] pub base_folder: PathBuf, #[serde(default = "default_ip_header")] - pub ip_header: SecureClientIpSource, + pub ip_header: ClientIpSource, } fn default_host() -> IpAddr { @@ -86,8 +86,8 @@ fn default_base() -> PathBuf { .join(".syncserver") } -pub fn default_ip_header() -> SecureClientIpSource { - SecureClientIpSource::ConnectInfo +pub fn default_ip_header() -> ClientIpSource { + ClientIpSource::ConnectInfo } impl SimpleServerInner { diff --git a/rslib/src/sync/http_server/routes.rs b/rslib/src/sync/http_server/routes.rs index dd4c0d3bd..072ad6140 100644 --- a/rslib/src/sync/http_server/routes.rs +++ b/rslib/src/sync/http_server/routes.rs @@ -53,7 +53,7 @@ async fn sync_handler( } pub fn collection_sync_router() -> Router

{ - Router::new().route("/:method", post(sync_handler::

)) + Router::new().route("/{method}", post(sync_handler::

)) } /// The Rust code used to send a GET with query params, which was inconsistent @@ -112,5 +112,5 @@ pub fn media_sync_router() -> Router

{ "/begin", get(media_begin_get::

).post(media_begin_post::

), ) - .route("/:method", post(media_sync_handler::

)) + .route("/{method}", post(media_sync_handler::

)) } diff --git a/rslib/src/sync/media/database/client/mod.rs b/rslib/src/sync/media/database/client/mod.rs index e58cb4fd9..5fe493679 100644 --- a/rslib/src/sync/media/database/client/mod.rs +++ b/rslib/src/sync/media/database/client/mod.rs @@ -283,15 +283,20 @@ fn row_to_name_and_checksum(row: &Row) -> error::Result<(String, Sha1Hash)> { Ok((file_name, sha1)) } -fn trace(s: &str) { - println!("sql: {}", s) +fn trace(event: rusqlite::trace::TraceEvent) { + if let rusqlite::trace::TraceEvent::Stmt(_, sql) = event { + println!("sql: {}", sql); + } } pub(crate) fn open_or_create>(path: P) -> error::Result { let mut db = Connection::open(path)?; if std::env::var("TRACESQL").is_ok() { - db.trace(Some(trace)); + db.trace_v2( + rusqlite::trace::TraceEventCodes::SQLITE_TRACE_STMT, + Some(trace), + ); } db.pragma_update(None, "page_size", 4096)?; diff --git a/rslib/src/sync/media/zip.rs b/rslib/src/sync/media/zip.rs index 0ad558d2d..d9b04f5e3 100644 --- a/rslib/src/sync/media/zip.rs +++ b/rslib/src/sync/media/zip.rs @@ -27,7 +27,8 @@ pub struct ZipFileMetadata { /// The metadata is in a different format to the upload case, since deletions /// don't need to be represented. pub fn zip_files_for_download(files: Vec<(String, Vec)>) -> Result> { - let options = FileOptions::default().compression_method(zip::CompressionMethod::Stored); + let options: FileOptions<'_, ()> = + FileOptions::default().compression_method(zip::CompressionMethod::Stored); let mut zip = ZipWriter::new(io::Cursor::new(vec![])); let mut entries = HashMap::new(); @@ -47,7 +48,8 @@ pub fn zip_files_for_download(files: Vec<(String, Vec)>) -> Result> } pub fn zip_files_for_upload(entries_: Vec<(String, Option>)>) -> Result> { - let options = FileOptions::default().compression_method(zip::CompressionMethod::Stored); + let options: FileOptions<'_, ()> = + FileOptions::default().compression_method(zip::CompressionMethod::Stored); let mut zip = ZipWriter::new(io::Cursor::new(vec![])); let mut entries = vec![]; diff --git a/rslib/src/sync/request/mod.rs b/rslib/src/sync/request/mod.rs index 4678cef9b..329aa44dd 100644 --- a/rslib/src/sync/request/mod.rs +++ b/rslib/src/sync/request/mod.rs @@ -10,14 +10,13 @@ use std::marker::PhantomData; use std::net::IpAddr; use std::sync::LazyLock; -use async_trait::async_trait; use axum::body::Body; use axum::extract::FromRequest; use axum::extract::Multipart; use axum::http::Request; use axum::http::StatusCode; use axum::RequestPartsExt; -use axum_client_ip::SecureClientIp; +use axum_client_ip::ClientIp; use axum_extra::TypedHeader; use header_and_stream::SyncHeader; use serde::de::DeserializeOwned; @@ -101,19 +100,18 @@ where } } -#[async_trait] -impl FromRequest for SyncRequest +impl FromRequest for SyncRequest where S: Send + Sync, T: DeserializeOwned, { type Rejection = HttpError; - async fn from_request(req: Request, state: &S) -> HttpResult { + async fn from_request(req: Request, state: &S) -> Result { let (mut parts, body) = req.into_parts(); let ip = parts - .extract::() + .extract::() .await .map_err(|_| { HttpError::new_without_source(StatusCode::INTERNAL_SERVER_ERROR, "missing ip") diff --git a/run b/run index fd65a70c7..3051345b1 100755 --- a/run +++ b/run @@ -8,6 +8,7 @@ export PYTHONPYCACHEPREFIX=out/pycache export ANKIDEV=${ANKIDEV-1} export QTWEBENGINE_REMOTE_DEBUGGING=${QTWEBENGINE_REMOTE_DEBUGGING-8080} export QTWEBENGINE_CHROMIUM_FLAGS=${QTWEBENGINE_CHROMIUM_FLAGS---remote-allow-origins=http://localhost:$QTWEBENGINE_REMOTE_DEBUGGING} +export PYENV=${PYENV-out/pyenv} # The pages can be accessed by, e.g. surfing to # http://localhost:40000/_anki/pages/deckconfig.html @@ -16,4 +17,4 @@ export ANKI_API_PORT=${ANKI_API_PORT-40000} export ANKI_API_HOST=${ANKI_API_HOST-127.0.0.1} ./ninja pylib qt -./out/pyenv/bin/python tools/run.py $* +${PYENV}/bin/python tools/run.py $* diff --git a/run.bat b/run.bat index 03acf0032..c689dda16 100755 --- a/run.bat +++ b/run.bat @@ -9,6 +9,6 @@ set QTWEBENGINE_CHROMIUM_FLAGS=--remote-allow-origins=http://localhost:8080 set ANKI_API_PORT=40000 set ANKI_API_HOST=127.0.0.1 -call tools\ninja pylib qt extract:win_amd64_audio || exit /b 1 +call tools\ninja pylib qt || exit /b 1 .\out\pyenv\scripts\python tools\run.py %* || exit /b 1 popd diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 608f0e356..fa07f7fa5 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] # older versions may fail to compile; newer versions may fail the clippy tests -channel = "1.85.0" +channel = "1.87.0" diff --git a/tools/build b/tools/build index 4fd78e195..3df9456ed 100755 --- a/tools/build +++ b/tools/build @@ -1,5 +1,6 @@ #!/bin/bash set -e -RELEASE=1 ./ninja wheels +rm -rf out/wheels/* +RELEASE=2 ./ninja wheels echo "wheels are in out/wheels" diff --git a/tools/build-arm-lin b/tools/build-arm-lin new file mode 100755 index 000000000..67fb4e4bf --- /dev/null +++ b/tools/build-arm-lin @@ -0,0 +1,12 @@ +#!/bin/bash + +set -e + +# sudo apt install libc6-dev-arm64-cross gcc-aarch64-linux-gnu +rustup target add aarch64-unknown-linux-gnu + +export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc +export LIN_ARM64=1 + +RELEASE=2 ./ninja wheels:anki +echo "wheels are in out/wheels" diff --git a/tools/build-x64-mac b/tools/build-x64-mac new file mode 100755 index 000000000..050b9c636 --- /dev/null +++ b/tools/build-x64-mac @@ -0,0 +1,10 @@ +#!/bin/bash + +set -e + +rustup target add x86_64-apple-darwin + +export MAC_X86=1 + +RELEASE=2 ./ninja wheels:anki +echo "wheels are in out/wheels" diff --git a/tools/build.bat b/tools/build.bat index 364f0f0f8..3ace08110 100755 --- a/tools/build.bat +++ b/tools/build.bat @@ -1,5 +1,7 @@ @echo off pushd "%~dp0"\.. -set RELEASE=1 +if exist out\wheels rmdir /s /q out\wheels +set RELEASE=2 tools\ninja wheels || exit /b 1 +echo wheels are in out/wheels popd diff --git a/tools/mac-x86 b/tools/mac-x86 deleted file mode 100755 index 312e19320..000000000 --- a/tools/mac-x86 +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/bash -# -# Run a command with an alternative buildroot and Intel architecture target, for building Intel on an ARM Mac. -# Eg ./tools/mac-x86 ./tools/run-qt5.14 -# -# Uses hard-coded paths to Python and build folders. -# - -export PYTHON_BINARY=/usr/local/bin/python3.9-intel64 -export BUILD_ROOT=~/Local/build/anki-x86 -export NORMAL_BUILD_ROOT=~/Local/build/anki -export MAC_X86=1 - - -# run provided command -$* - -BUILD_ROOT=$NORMAL_BUILD_ROOT ./ninja just-to-restore-build-root-and-failure-is-expected diff --git a/tools/minilints/src/main.rs b/tools/minilints/src/main.rs index dfe9bef89..3a3c06f2c 100644 --- a/tools/minilints/src/main.rs +++ b/tools/minilints/src/main.rs @@ -29,8 +29,6 @@ const NONSTANDARD_HEADER: &[&str] = &[ "./python/write_wheel.py", "./qt/aqt/mpv.py", "./qt/aqt/winpaths.py", - "./qt/bundle/build.rs", - "./qt/bundle/src/main.rs", ]; const IGNORED_FOLDERS: &[&str] = &[ @@ -38,7 +36,6 @@ const IGNORED_FOLDERS: &[&str] = &[ "./node_modules", "./qt/aqt/forms", "./tools/workspace-hack", - "./qt/bundle/PyOxidizer", "./target", ".mypy_cache", "./extra", @@ -209,7 +206,7 @@ fn sveltekit_temp_file(path: &str) -> bool { } fn check_cargo_deny() -> Result<()> { - Command::run("cargo install cargo-deny@0.18.2")?; + Command::run("cargo install cargo-deny@0.18.3")?; Command::run("cargo deny check")?; Ok(()) } diff --git a/tools/ninja.bat b/tools/ninja.bat index 7d939846e..6310103c3 100755 --- a/tools/ninja.bat +++ b/tools/ninja.bat @@ -1,5 +1,5 @@ @echo off -set CARGO_TARGET_DIR=%~dp0\..\out\rust +set CARGO_TARGET_DIR=%~dp0..\out\rust REM separate build+run steps so build env doesn't leak into subprocesses cargo build -p runner --release || exit /b 1 out\rust\release\runner build %* || exit /b 1 diff --git a/tools/publish b/tools/publish new file mode 100755 index 000000000..4c625c36d --- /dev/null +++ b/tools/publish @@ -0,0 +1,9 @@ +#!/bin/bash + +set -e + +#export UV_PUBLISH_TOKEN=$(pass show w/pypi-api-test) +#out/extracted/uv/uv publish --index testpypi out/wheels/* + +export UV_PUBLISH_TOKEN=$(pass show w/pypi-api) +out/extracted/uv/uv publish out/wheels/* diff --git a/tools/run-qt5.14 b/tools/run-qt5.14 deleted file mode 100755 index bc7b539f4..000000000 --- a/tools/run-qt5.14 +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash - -set -e - -export PYTHONWARNINGS=default -export PYTHONPYCACHEPREFIX=out/pycache -export ANKIDEV=${ANKIDEV-1} -export QTWEBENGINE_REMOTE_DEBUGGING=${QTWEBENGINE_REMOTE_DEBUGGING-8080} -export QTWEBENGINE_CHROMIUM_FLAGS=${QTWEBENGINE_CHROMIUM_FLAGS---remote-allow-origins=http://localhost:$QTWEBENGINE_REMOTE_DEBUGGING} - -./ninja pylib qt pyenv-qt5.14 -./out/pyenv-qt5.14/bin/python tools/run.py $* diff --git a/tools/run-qt5.15 b/tools/run-qt5.15 deleted file mode 100755 index 67a9b6f32..000000000 --- a/tools/run-qt5.15 +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash - -set -e - -export PYTHONWARNINGS=default -export PYTHONPYCACHEPREFIX=out/pycache -export ANKIDEV=${ANKIDEV-1} -export QTWEBENGINE_REMOTE_DEBUGGING=${QTWEBENGINE_REMOTE_DEBUGGING-8080} -export QTWEBENGINE_CHROMIUM_FLAGS=${QTWEBENGINE_CHROMIUM_FLAGS---remote-allow-origins=http://localhost:$QTWEBENGINE_REMOTE_DEBUGGING} -./ninja pylib qt pyenv-qt5.15 -./out/pyenv-qt5.15/bin/python tools/run.py $* diff --git a/tools/run-qt5.15.bat b/tools/run-qt5.15.bat deleted file mode 100755 index d17d5b534..000000000 --- a/tools/run-qt5.15.bat +++ /dev/null @@ -1,10 +0,0 @@ -@echo off -pushd "%~dp0\.." - -set PYTHONWARNINGS=default -set PYTHONPYCACHEPREFIX=out\pycache -set ANKIDEV=1 - -call tools\ninja pylib qt pyenv-qt5.15 || exit /b 1 -.\out\pyenv-qt5.15\scripts\python tools\run.py %* || exit /b 1 -popd diff --git a/tools/run-qt6.6 b/tools/run-qt6.6 new file mode 100755 index 000000000..dc0461229 --- /dev/null +++ b/tools/run-qt6.6 @@ -0,0 +1,9 @@ +#!/bin/bash + +set -e + +./ninja extract:uv + +export PYENV=./out/pyenv66 +UV_PROJECT_ENVIRONMENT=$PYENV ./out/extracted/uv/uv sync --all-packages --extra qt66 +./run $* diff --git a/tools/run-qt6.7 b/tools/run-qt6.7 new file mode 100755 index 000000000..d01d46cea --- /dev/null +++ b/tools/run-qt6.7 @@ -0,0 +1,9 @@ +#!/bin/bash + +set -e + +./ninja extract:uv + +export PYENV=./out/pyenv67 +UV_PROJECT_ENVIRONMENT=$PYENV ./out/extracted/uv/uv sync --all-packages --extra qt67 +./run $* diff --git a/tools/run-qt6.8 b/tools/run-qt6.8 deleted file mode 100755 index 9083f343e..000000000 --- a/tools/run-qt6.8 +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash - -set -e - -export PYTHONWARNINGS=default -export PYTHONPYCACHEPREFIX=out/pycache -export ANKIDEV=${ANKIDEV-1} -export QTWEBENGINE_REMOTE_DEBUGGING=${QTWEBENGINE_REMOTE_DEBUGGING-8080} -export QTWEBENGINE_CHROMIUM_FLAGS=${QTWEBENGINE_CHROMIUM_FLAGS---remote-allow-origins=http://localhost:$QTWEBENGINE_REMOTE_DEBUGGING} -./ninja pylib qt pyenv-qt6.8 -./out/pyenv-qt6.8/bin/python tools/run.py $* diff --git a/tools/run-qt6.9 b/tools/run-qt6.9 new file mode 100755 index 000000000..6576b6c81 --- /dev/null +++ b/tools/run-qt6.9 @@ -0,0 +1,9 @@ +#!/bin/bash + +set -e + +./ninja extract:uv + +export PYENV=./out/pyenv69 +UV_PROJECT_ENVIRONMENT=$PYENV ./out/extracted/uv/uv sync --all-packages --extra qt69 +./run $* diff --git a/ts/lib/components/icons.ts b/ts/lib/components/icons.ts index 33c6e04cb..ab07cbf17 100644 --- a/ts/lib/components/icons.ts +++ b/ts/lib/components/icons.ts @@ -59,6 +59,8 @@ import FormatAlignCenter_ from "@mdi/svg/svg/format-align-center.svg?component"; import formatAlignCenter_ from "@mdi/svg/svg/format-align-center.svg?url"; import FormatBold_ from "@mdi/svg/svg/format-bold.svg?component"; import formatBold_ from "@mdi/svg/svg/format-bold.svg?url"; +import FormatColorFill_ from "@mdi/svg/svg/format-color-fill.svg?component"; +import formatColorFill_ from "@mdi/svg/svg/format-color-fill.svg?url"; import HighlightColor_ from "@mdi/svg/svg/format-color-highlight.svg?component"; import highlightColor_ from "@mdi/svg/svg/format-color-highlight.svg?url"; import TextColor_ from "@mdi/svg/svg/format-color-text.svg?component"; @@ -264,6 +266,7 @@ export const mdiEllipseOutline = { url: ellipseOutline_, component: EllipseOutli export const mdiEye = { url: eye_, component: Eye_ }; export const mdiFormatAlignCenter = { url: formatAlignCenter_, component: FormatAlignCenter_ }; export const mdiFormatBold = { url: formatBold_, component: FormatBold_ }; +export const mdiFormatColorFill = { url: formatColorFill_, component: FormatColorFill_ }; export const mdiFormatItalic = { url: formatItalic_, component: FormatItalic_ }; export const mdiFormatUnderline = { url: formatUnderline_, component: FormatUnderline_ }; export const mdiGroup = { url: group_, component: Group_ }; diff --git a/ts/lib/tslib/time.ts b/ts/lib/tslib/time.ts index a517e8a39..f40758d8d 100644 --- a/ts/lib/tslib/time.ts +++ b/ts/lib/tslib/time.ts @@ -7,8 +7,8 @@ export const SECOND = 1.0; export const MINUTE = 60.0 * SECOND; export const HOUR = 60.0 * MINUTE; export const DAY = 24.0 * HOUR; -export const MONTH = 30.417 * DAY; // 365/12 ≈ 30.417 export const YEAR = 365.0 * DAY; +export const MONTH = YEAR / 12; export enum TimespanUnit { Seconds, @@ -146,8 +146,13 @@ function i18nFuncForUnit( If precise is true, show to two decimal places, eg eg 70 seconds -> "1.17 minutes" If false, seconds and days are shown without decimals. */ -export function timeSpan(seconds: number, short = false, precise = true): string { - const unit = naturalUnit(seconds); +export function timeSpan( + seconds: number, + short = false, + precise = true, + maxUnit: TimespanUnit = TimespanUnit.Years, +): string { + const unit = Math.min(naturalUnit(seconds), maxUnit); let amount = unitAmount(unit, seconds); if (!precise && unit < TimespanUnit.Months) { amount = Math.round(amount); diff --git a/ts/reviewer/reviewer.scss b/ts/reviewer/reviewer.scss index 3154d99a1..a2c05d3f5 100644 --- a/ts/reviewer/reviewer.scss +++ b/ts/reviewer/reviewer.scss @@ -6,6 +6,9 @@ hr { background-color: vars.palette(darkgray, 0); + margin: 1em 0; + border: none; + height: 1px; } body { @@ -71,6 +74,7 @@ pre { width: 100%; // https://anki.tenderapp.com/discussions/beta-testing/1854-using-margin-auto-causes-horizontal-scrollbar-on-typesomething box-sizing: border-box; + line-height: 1.75; } code#typeans { diff --git a/ts/routes/card-info/CardInfo.svelte b/ts/routes/card-info/CardInfo.svelte index 020037dda..938c7d92a 100644 --- a/ts/routes/card-info/CardInfo.svelte +++ b/ts/routes/card-info/CardInfo.svelte @@ -22,12 +22,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html $: decay = (() => { const paramsLength = stats?.fsrsParams?.length ?? 0; if (paramsLength === 0) { - return 0.2; // default decay for FSRS-6 + return 0.1542; // default decay for FSRS-6 } if (paramsLength < 21) { return 0.5; // default decay for FSRS-4.5 and FSRS-5 } - return stats?.fsrsParams?.[20] ?? 0.2; + return stats?.fsrsParams?.[20] ?? 0.1542; })(); diff --git a/ts/routes/card-info/CardStats.svelte b/ts/routes/card-info/CardStats.svelte index 2214d5481..cb4c2f274 100644 --- a/ts/routes/card-info/CardStats.svelte +++ b/ts/routes/card-info/CardStats.svelte @@ -5,7 +5,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html diff --git a/ts/routes/deck-options/SimulatorModal.svelte b/ts/routes/deck-options/SimulatorModal.svelte index 070bed4fa..64b712560 100644 --- a/ts/routes/deck-options/SimulatorModal.svelte +++ b/ts/routes/deck-options/SimulatorModal.svelte @@ -383,38 +383,34 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html {/if} -

-
- {tr.deckConfigComputeOptimalRetention()} - - - {#if optimalRetention} - {estimatedRetention(optimalRetention)} - {#if optimalRetention - $config.desiredRetention >= 0.01} - - {/if} - {/if} - +
+ {tr.deckConfigComputeOptimalRetention()} +
-
+ + + {#if optimalRetention} + {estimatedRetention(optimalRetention)} + {#if optimalRetention - $config.desiredRetention >= 0.01} + + {/if} + {/if} + + {#if computingRetention} +
{computeRetentionProgressString}
+ {/if} +