// 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 camino::Utf8Path; 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::command::RunCommand; use crate::hash::simple_hash; use crate::input::BuildInput; use crate::inputs; use crate::Build; // 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/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/astral-sh/uv/releases/download/0.7.13/uv-aarch64-unknown-linux-gnu.tar.gz", sha256: "0b2ad9fe4295881615295add8cc5daa02549d29cc9a61f0578e397efcf12f08f", } }, Platform::MacX64 => { OnlineArchive { 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/astral-sh/uv/releases/download/0.7.13/uv-aarch64-apple-darwin.tar.gz", sha256: "721f532b73171586574298d4311a91d5ea2c802ef4db3ebafc434239330090c6", } }, Platform::WindowsX64 => { OnlineArchive { 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", } } } } 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(), "UV_BINARY must be absolute" ); path.into() } Err(_) => { download_and_extract( build, "uv", uv_archive(platform), hashmap! { "bin" => [ with_exe("uv") ] }, )?; inputs![":extract:uv:bin"] } }; build.add_dependency("uv_binary", uv_binary); // Our macOS packaging needs access to the x86 binary on ARM. download_and_extract( build, "uv_mac_x86", uv_archive(Platform::MacX64), hashmap! { "bin" => [ with_exe("uv") ] }, )?; Ok(()) } pub struct PythonEnvironment { 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 $uv_binary $builddir/$pyenv_folder -- $extra_args" } else { "echo 'OFFLINE_BUILD is set. Using the existing PythonEnvironment.'" } } fn files(&mut self, build: &mut impl crate::build::FilesHandle) { let bin_path = |binary: &str| -> Vec { let folder = self.venv_folder; let path = if cfg!(windows) { format!("{folder}/scripts/{binary}.exe") } else { format!("{folder}/bin/{binary}") }; 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("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); } build.add_output_stamp(format!("{}/.stamp", self.venv_folder)); } } pub struct PythonTypecheck { pub folders: &'static [&'static str], pub deps: BuildInput, } impl BuildAction for PythonTypecheck { fn command(&self) -> &str { "$mypy $folders" } fn files(&mut self, build: &mut impl crate::build::FilesHandle) { build.add_inputs("", &self.deps); build.add_inputs("mypy", inputs![":pyenv:mypy"]); build.add_inputs("", inputs![".mypy.ini"]); build.add_variable("folders", self.folders.join(" ")); let hash = simple_hash(self.folders); build.add_output_stamp(format!("tests/python_typecheck.{hash}")); } fn hide_progress(&self) -> bool { true } } struct PythonFormat<'a> { pub inputs: &'a BuildInput, pub check_only: bool, pub isort_ini: &'a BuildInput, } impl BuildAction for PythonFormat<'_> { fn command(&self) -> &str { "$black -t py39 -q $check --color $in && $ $isort --color --settings-path $isort_ini $check $in" } fn files(&mut self, build: &mut impl crate::build::FilesHandle) { build.add_inputs("in", self.inputs); build.add_inputs("black", inputs![":pyenv:black"]); build.add_inputs("isort", inputs![":pyenv:isort"]); let hash = simple_hash(self.inputs); build.add_env_var("BLACK_CACHE_DIR", "out/python/black.cache.{hash}"); build.add_inputs("isort_ini", self.isort_ini); build.add_variable( "check", if self.check_only { "--diff --check" } else { "" }, ); build.add_output_stamp(format!( "tests/python_format.{}.{hash}", if self.check_only { "check" } else { "fix" } )); } } pub fn python_format(build: &mut Build, group: &str, inputs: BuildInput) -> Result<()> { let isort_ini = &inputs![".isort.cfg"]; build.add_action( format!("check:format:python:{group}"), PythonFormat { inputs: &inputs, check_only: true, isort_ini, }, )?; build.add_action( format!("format:python:{group}"), PythonFormat { inputs: &inputs, check_only: false, isort_ini, }, )?; Ok(()) } pub struct PythonLint { pub folders: &'static [&'static str], pub pylint_ini: BuildInput, pub deps: BuildInput, } impl BuildAction for PythonLint { fn command(&self) -> &str { "$pylint --rcfile $pylint_ini -sn -j $cpus $folders" } fn files(&mut self, build: &mut impl crate::build::FilesHandle) { build.add_inputs("", &self.deps); build.add_inputs("pylint", inputs![":pyenv:pylint"]); build.add_inputs("pylint_ini", &self.pylint_ini); build.add_variable("folders", self.folders.join(" ")); // On a 16 core system, values above 10 do not improve wall clock time, // but waste extra cores that could be working on other tests. build.add_variable("cpus", num_cpus::get().min(10).to_string()); let hash = simple_hash(&self.deps); build.add_output_stamp(format!("tests/python_lint.{hash}")); } } pub struct PythonTest { pub folder: &'static str, pub python_path: &'static [&'static str], pub deps: BuildInput, } impl BuildAction for PythonTest { fn command(&self) -> &str { "$pytest -p no:cacheprovider $folder" } fn files(&mut self, build: &mut impl crate::build::FilesHandle) { build.add_inputs("", &self.deps); build.add_inputs("pytest", inputs![":pyenv:pytest"]); build.add_variable("folder", self.folder); build.add_variable( "pythonpath", self.python_path.join(if cfg!(windows) { ";" } else { ":" }), ); build.add_env_var("PYTHONPATH", "$pythonpath"); build.add_env_var("ANKI_TEST_MODE", "1"); let hash = simple_hash(self.folder); build.add_output_stamp(format!("tests/python_pytest.{hash}")); } fn hide_progress(&self) -> bool { true } } pub fn setup_uv_universal(build: &mut Build) -> Result<()> { build.add_action( "bundle: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!["bundle/uv"], }, }, ) }