// Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html use ninja_gen::action::BuildAction; use ninja_gen::archives::Platform; use ninja_gen::build::FilesHandle; use ninja_gen::command::RunCommand; use ninja_gen::glob; use ninja_gen::hashmap; use ninja_gen::input::BuildInput; use ninja_gen::inputs; use ninja_gen::python::python_format; use ninja_gen::python::PythonEnvironment; use ninja_gen::python::PythonLint; use ninja_gen::python::PythonTypecheck; use ninja_gen::rsync::RsyncFiles; use ninja_gen::Build; use ninja_gen::Result; pub fn setup_venv(build: &mut Build) -> Result<()> { let requirements_txt = if cfg!(windows) { inputs![ "python/requirements.dev.txt", "python/requirements.qt6_4.txt", "python/requirements.win.txt", ] } else if cfg!(all(target_os = "linux", target_arch = "aarch64")) { inputs!["python/requirements.dev.txt"] } else { inputs![ "python/requirements.dev.txt", "python/requirements.qt6_5.txt", ] }; build.add( "pyenv", PythonEnvironment { folder: "pyenv", base_requirements_txt: inputs!["python/requirements.base.txt"], requirements_txt, extra_binary_exports: &[ "pip-compile", "pip-sync", "mypy", "black", "isort", "pylint", "pytest", "protoc-gen-mypy", ], }, )?; // optional venvs for testing with Qt5 let mut reqs_qt5 = inputs!["python/requirements.bundle.txt"]; if cfg!(windows) { reqs_qt5 = inputs![reqs_qt5, "python/requirements.win.txt"]; } build.add( "pyenv-qt5.15", PythonEnvironment { folder: "pyenv-qt5.15", base_requirements_txt: inputs!["python/requirements.base.txt"], requirements_txt: inputs![&reqs_qt5, "python/requirements.qt5_15.txt"], extra_binary_exports: &[], }, )?; build.add( "pyenv-qt5.14", PythonEnvironment { folder: "pyenv-qt5.14", base_requirements_txt: inputs!["python/requirements.base.txt"], requirements_txt: inputs![reqs_qt5, "python/requirements.qt5_14.txt"], extra_binary_exports: &[], }, )?; Ok(()) } pub struct GenPythonProto { pub proto_files: BuildInput, } impl BuildAction for GenPythonProto { fn command(&self) -> &str { "$protoc $ --plugin=protoc-gen-mypy=$protoc-gen-mypy $ --python_out=$builddir/pylib $ --mypy_out=$builddir/pylib $ -Iproto $in" } fn files(&mut self, build: &mut impl FilesHandle) { let proto_inputs = build.expand_inputs(&self.proto_files); let python_outputs: Vec<_> = proto_inputs .iter() .flat_map(|path| { let path = path .replace('\\', "/") .replace("proto/", "pylib/") .replace(".proto", "_pb2"); [format!("{path}.py"), format!("{path}.pyi")] }) .collect(); build.add_inputs("in", &self.proto_files); build.add_inputs("protoc", inputs!["$protoc_binary"]); build.add_inputs("protoc-gen-mypy", inputs![":pyenv:protoc-gen-mypy"]); build.add_outputs("", python_outputs); } } 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" } 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("", &self.deps); build.add_variable("src", self.src_folder); build.add_variable("gen", self.gen_folder); let tag = if let Some(platform) = self.platform { let platform = match platform { Platform::LinuxX64 => "manylinux_2_28_x86_64", Platform::LinuxArm => "manylinux_2_31_aarch64", Platform::MacX64 => "macosx_10_13_x86_64", Platform::MacArm => "macosx_11_0_arm64", Platform::WindowsX64 => "win_amd64", }; format!("cp39-abi3-{platform}") } else { "py3-none-any".into() }; let name = self.name; let version = &self.version; let wheel_path = format!("wheels/{name}-{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( "check:mypy", PythonTypecheck { folders: &[ "pylib", "ts/lib", "qt/aqt", "qt/tools", "out/pylib/anki", "out/qt/_aqt", "ftl", "python", "tools", ], deps: inputs![ glob!["{pylib,ftl,qt}/**/*.{py,pyi}"], ":pylib/anki", ":qt/aqt" ], }, )?; add_pylint(build)?; Ok(()) } fn add_pylint(build: &mut Build) -> Result<()> { // pylint does not support PEP420 implicit namespaces split across import paths, // so we need to merge our pylib sources and generated files before invoking it, // and add a top-level __init__.py build.add( "pylint/anki", RsyncFiles { inputs: inputs![":pylib/anki"], target_folder: "pylint/anki", strip_prefix: "$builddir/pylib/anki", // avoid copying our large rsbridge binary extra_args: "--links", }, )?; build.add( "pylint/anki", RsyncFiles { inputs: inputs![glob!["pylib/anki/**"]], target_folder: "pylint/anki", strip_prefix: "pylib/anki", extra_args: "", }, )?; build.add( "pylint/anki", RunCommand { command: ":pyenv:bin", args: "$script $out", inputs: hashmap! { "script" => inputs!["python/mkempty.py"] }, outputs: hashmap! { "out" => vec!["pylint/anki/__init__.py"] }, }, )?; build.add( "check:pylint", PythonLint { folders: &[ "$builddir/pylint/anki", "qt/aqt", "ftl", "pylib/tools", "tools", "python", ], pylint_ini: inputs![".pylintrc"], deps: inputs![ ":pylint/anki", ":qt/aqt", glob!("{pylib/tools,ftl,qt,python,tools}/**/*.py") ], }, )?; Ok(()) }