Anki/build/configure/src/python.rs
Damien Elmes bac05039a7 Move protobuf generation into a separate crate; write .py interface in Rust
A couple of motivations for this:

- genbackend.py was somewhat messy, and difficult to change with the
lack of types. The mobile clients used it as a base for their generation,
so improving it will make life easier for them too, once they're ported.
- It will make it easier to write a .ts generator in the future
- We currently implement a bunch of helper methods on protobuf types
which don't allow us to compile the protobuf types until we compile
the Anki crate. If we change this in the future, we will be able to
do more of the compilation up-front.

We no longer need to record the services in the proto file, as we can
extract the service order from the compiled protos. Support for map types
has also been added.
2023-06-12 09:52:00 +10:00

242 lines
7.3 KiB
Rust

// 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);
// not a direct dependency, but we include the output interface in our declared
// outputs
build.add_inputs("", inputs!["rslib/proto"]);
build.add_outputs("", vec!["pylib/anki/_backend_generated.py"]);
}
}
pub struct BuildWheel {
pub name: &'static str,
pub version: String,
pub src_folder: &'static str,
pub gen_folder: &'static str,
pub platform: Option<Platform>,
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(())
}