diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index da065f340..253726362 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,6 +17,7 @@ jobs: - uses: astral-sh/setup-uv@v7 with: python-version: "3.11" + - uses: dtolnay/rust-toolchain@1.95.0 # pinned; keep in sync with rust-toolchain.toml - run: uv sync --frozen - run: PYTHONPATH=. uv run pytest --collect-only -q - run: PYTHONPATH=. uv run pytest -m unit --no-cov -q @@ -34,5 +35,6 @@ jobs: - uses: astral-sh/setup-uv@v7 with: python-version: "3.11" + - uses: dtolnay/rust-toolchain@1.95.0 # pinned; keep in sync with rust-toolchain.toml - run: uv sync --frozen - run: PYTHONPATH=. uv run pytest --network -m "unit or network or slow" --no-cov -q diff --git a/.gitignore b/.gitignore index 36e021879..adbc5b9c8 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,10 @@ docs/sources/*/impact-report/modified-cliques.json .coverage .coverage.* htmlcov/ + +# maturin / Rust build artifacts +rust/target/ +# native extension dropped into src/ by `maturin develop` (don't commit compiled binaries) +src/rs.*.so +src/rs.*.pyd +src/rs.*.dylib diff --git a/Dockerfile b/Dockerfile index ae4fdd4ee..4a5bcb806 100644 --- a/Dockerfile +++ b/Dockerfile @@ -36,13 +36,18 @@ RUN adduser --home ${ROOT} --uid 1000 nru # Set up a $ROOT directory with the source code to work in. RUN mkdir -p ${ROOT} WORKDIR ${ROOT} + +# Rust toolchain — required because the build backend is maturin (compiles a native extension). +RUN apt-get update && apt-get install -y cargo + USER nru COPY --chown=nru . ${ROOT} -# Install and run `uv sync` to install packages. +# Install uv and sync packages. `--frozen` installs exactly the committed uv.lock +# (no re-resolution), keeping the image reproducible and matching CI's `uv sync --frozen`. RUN pipx install uv ENV PATH="${ROOT}/.local/bin:${PATH}" -RUN uv sync +RUN uv sync --frozen # Our default entrypoint is to start the Babel run. ENTRYPOINT ["bash", "-c", "uv run snakemake --cores ${CORES}"] diff --git a/pyproject.toml b/pyproject.toml index 63b17de88..84d610865 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,6 +39,7 @@ Issues = "https://github.com/NCATSTranslator/Babel/issues" [dependency-groups] dev = [ "ipykernel>=7.1.0", + "maturin>=1.0,<2.0", "py-spy>=0.4.2", "ruff>=0.14.14", "rumdl>=0.2.4", @@ -55,12 +56,16 @@ source-impact-report = "src.tools.source_impact_report.cli:main" package = true [build-system] -requires = ["setuptools>=83.0.0"] -build-backend = "setuptools.build_meta" +requires = ["maturin>=1.0,<2.0"] +build-backend = "maturin" -[tool.setuptools.packages.find] -where = ["."] -include = ["src", "src.*"] +[tool.maturin] +# The importable Python package is literally named `src` (flat layout). A dotted module-name +# places the compiled extension at `src/rs.*` and tells maturin the package to bundle is `src` +# (auto-resolved to the repo root since src-layout detection won't fire). +module-name = "src.rs" +# Rust source lives under rust/ so Cargo never collides with the Python `src/` dir. +manifest-path = "rust/Cargo.toml" # Linting/formatting configuration @@ -84,6 +89,13 @@ ignore = [ fixable = ["ALL"] unfixable = [] +[tool.ruff.format] +# Ruff 0.16.0 began formatting Python code fences inside Markdown, which overlaps with the +# dedicated rumdl Markdown check and broke CI when astral-sh/ruff-action@v3 floated to 0.16.0 +# (pyproject pins only a floor, >=0.14.14). Keep ruff's formatter scoped to Python source and +# leave Markdown to rumdl so formatting CI stays deterministic across ruff releases. +exclude = ["*.md"] + [tool.snakefmt] line_length = 120 include = '\.snakefile$|^Snakefile' diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 000000000..c62df1bcc --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,6 @@ +# Pin the Rust toolchain so local builds, CI, and any rustup-based environment all +# compile the maturin/PyO3 extension with the same compiler version. The CI action ref +# (dtolnay/rust-toolchain in .github/workflows/test.yml) is pinned to match this; bump +# both together when upgrading Rust. +[toolchain] +channel = "1.95.0" diff --git a/rust/Cargo.lock b/rust/Cargo.lock new file mode 100644 index 000000000..76ba98f35 --- /dev/null +++ b/rust/Cargo.lock @@ -0,0 +1,180 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "babel-pipeline" +version = "1.17.0" +dependencies = [ + "pyo3", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "indoc" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "portable-atomic" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d20d5497ef88037a52ff98267d066e7f11fcc5e99bbfbd58a42336193aacec3" + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pyo3" +version = "0.23.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7778bffd85cf38175ac1f545509665d0b9b92a198ca7941f131f85f7a4f9a872" +dependencies = [ + "cfg-if", + "indoc", + "libc", + "memoffset", + "once_cell", + "portable-atomic", + "pyo3-build-config", + "pyo3-ffi", + "pyo3-macros", + "unindent", +] + +[[package]] +name = "pyo3-build-config" +version = "0.23.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f6cbe86ef3bf18998d9df6e0f3fc1050a8c5efa409bf712e661a4366e010fb" +dependencies = [ + "once_cell", + "target-lexicon", +] + +[[package]] +name = "pyo3-ffi" +version = "0.23.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9f1b4c431c0bb1c8fb0a338709859eed0d030ff6daa34368d3b152a63dfdd8d" +dependencies = [ + "libc", + "pyo3-build-config", +] + +[[package]] +name = "pyo3-macros" +version = "0.23.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc2201328f63c4710f68abdf653c89d8dbc2858b88c5d88b0ff38a75288a9da" +dependencies = [ + "proc-macro2", + "pyo3-macros-backend", + "quote", + "syn", +] + +[[package]] +name = "pyo3-macros-backend" +version = "0.23.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fca6726ad0f3da9c9de093d6f116a93c1a38e417ed73bf138472cf4064f72028" +dependencies = [ + "heck", + "proc-macro2", + "pyo3-build-config", + "quote", + "syn", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unindent" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" diff --git a/rust/Cargo.toml b/rust/Cargo.toml new file mode 100644 index 000000000..426067dda --- /dev/null +++ b/rust/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "babel-pipeline" +version = "1.17.0" # valid SemVer (3 parts); published version is "1.17" from root pyproject +edition = "2021" + +[lib] +name = "rs" # matches the #[pymodule] fn + module-name's last component +crate-type = ["cdylib"] + +[dependencies] +pyo3 = { version = "0.23", features = ["abi3-py311", "extension-module"] } diff --git a/rust/src/lib.rs b/rust/src/lib.rs new file mode 100644 index 000000000..0db123630 --- /dev/null +++ b/rust/src/lib.rs @@ -0,0 +1,13 @@ +//! Placeholder native extension for babel-pipeline. +//! +//! The build backend is maturin; this empty module exists only so the project builds as a +//! mixed Rust/Python package. Real functionality will land in a follow-up PR. + +use pyo3::prelude::*; + +/// Empty pyo3 module, importable as `from src import rs`. Name must match the last component +/// of `[tool.maturin] module-name` ("src.rs" → `rs`). +#[pymodule] +fn rs(_m: &Bound<'_, PyModule>) -> PyResult<()> { + Ok(()) +} diff --git a/uv.lock b/uv.lock index 1502a8e5c..de33fc912 100644 --- a/uv.lock +++ b/uv.lock @@ -211,6 +211,7 @@ dependencies = [ [package.dev-dependencies] dev = [ { name = "ipykernel" }, + { name = "maturin" }, { name = "py-spy" }, { name = "ruff" }, { name = "rumdl" }, @@ -247,6 +248,7 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ { name = "ipykernel", specifier = ">=7.1.0" }, + { name = "maturin", specifier = ">=1.0,<2.0" }, { name = "py-spy", specifier = ">=0.4.2" }, { name = "ruff", specifier = ">=0.14.14" }, { name = "rumdl", specifier = ">=0.2.4" }, @@ -1239,6 +1241,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/41/09/5b161152e2d90f7b87f781c2e1267494aef9c32498df793f73ad0a0a494a/matplotlib_inline-0.2.2-py3-none-any.whl", hash = "sha256:3c821cf1c209f59fb2d2d64abbf5b23b67bcb2210d663f9918dd851c6da1fcf6", size = 9534, upload-time = "2026-05-08T17:33:32.055Z" }, ] +[[package]] +name = "maturin" +version = "1.14.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/b3/addd877f871fb1860d46d3a4f206ecb10b946c85846805e6367631926fd3/maturin-1.14.1.tar.gz", hash = "sha256:9d6577a62cd08e0ceba7a0db06fb098e0c9b1b3429bad747a4f3a18215a1b3df", size = 369637, upload-time = "2026-06-19T05:19:49.774Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/f0/97c5a5bd9c71653a066c0976a484eaaae50b9369557838a4176b7b0bdaa5/maturin-1.14.1-py3-none-linux_armv6l.whl", hash = "sha256:522292398945442cdafa9daeb2271b2340fbde57027b818f923f88eab04174f8", size = 10207496, upload-time = "2026-06-19T05:19:09.321Z" }, + { url = "https://files.pythonhosted.org/packages/fe/83/294bca639b0e052f1e2f65199b3db258780c7d4e31408b934c9c974a1379/maturin-1.14.1-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:ffe5ad71f21d1e6603c4dd75f7fee34adf5ed5ebcebb692886549888ebb329ed", size = 19680113, upload-time = "2026-06-19T05:19:13.43Z" }, + { url = "https://files.pythonhosted.org/packages/43/b6/79c881410a3b1c187f7eb3d407aecae646c6a4433d630d72200359015e83/maturin-1.14.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f3306078070c1508fd715b9116070cbcaff5959024272a9f1e6f5cb29768b86c", size = 10169205, upload-time = "2026-06-19T05:19:16.615Z" }, + { url = "https://files.pythonhosted.org/packages/93/9d/44b6f26dcb7f7a04c5501ac2dbb6ca1490150682baa525ca5860504f9eab/maturin-1.14.1-py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686.whl", hash = "sha256:cd457cd88961156e26379e1155bd287cc0ec1c8b2f1582b0660fb31b87c8842d", size = 10188098, upload-time = "2026-06-19T05:19:19.736Z" }, + { url = "https://files.pythonhosted.org/packages/1a/bd/9c0d5d6983905ce2c9edaa073a7e89355a9cf7f396988e05d32f1c37785d/maturin-1.14.1-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:dfc54ae32e6fcb18302193ab9a30b0b25eefffba994ae13238974805533ef75e", size = 10627576, upload-time = "2026-06-19T05:19:22.713Z" }, + { url = "https://files.pythonhosted.org/packages/e5/33/b096412bd6a7cb399652b260666f901adf88a687181a6dbd6a3f89f0a94e/maturin-1.14.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:a131d912b5267e640bc96d70f4914e10590aed64082ec9abacba7cea52004224", size = 10085181, upload-time = "2026-06-19T05:19:25.69Z" }, + { url = "https://files.pythonhosted.org/packages/56/8d/08c3bf469c38a23c9e6c877e338193001eb604d010fedc08341974e38528/maturin-1.14.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:be18fc568fb76884c0205456336892a75105ec398e6b667cd777c6268bd06d69", size = 10026363, upload-time = "2026-06-19T05:19:28.904Z" }, + { url = "https://files.pythonhosted.org/packages/3a/a4/c4d1a92839f8745ab4aab988a7db884a79d6d710bd3b286fcf9316dece1a/maturin-1.14.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:994a0c8ba3ad8a92b3a9ee1b02645d200d610216b15cff5102b0fe65e8e08666", size = 13321347, upload-time = "2026-06-19T05:19:32.411Z" }, + { url = "https://files.pythonhosted.org/packages/b3/fa/170f04624d03fd07d2a8b1b67de83a127af93aef9eaa425839553347297b/maturin-1.14.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:be80866363e605d137991b491a741a84cde9ae350183c4c85f49690ca9aaaa65", size = 10877609, upload-time = "2026-06-19T05:19:35.448Z" }, + { url = "https://files.pythonhosted.org/packages/61/ad/1ae2e1d0ded282bf2c55ac13f0811d87deb425e200ae64a15785675dede9/maturin-1.14.1-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:5282dffd4b539d2be245f4e5b1a5ab6bc1033b58f4a4872f5833f9d43c954aa4", size = 10417316, upload-time = "2026-06-19T05:19:38.28Z" }, + { url = "https://files.pythonhosted.org/packages/fb/27/bf677183920718da49cd7982d6a3ffc440aad8919329f571d189f81b7bdf/maturin-1.14.1-py3-none-win32.whl", hash = "sha256:1a04de0a20188f95c721b5702eed18140bdcccb28c386797093eca3f62f4d4e0", size = 8931293, upload-time = "2026-06-19T05:19:41.183Z" }, + { url = "https://files.pythonhosted.org/packages/63/4b/585adeb9167b08d3cdff0032a938b0e72655c92003df4f52c3f696a1bcc2/maturin-1.14.1-py3-none-win_amd64.whl", hash = "sha256:3c9f94640ecc4895e94abaf834a0684430032c865b2748a36c12461fd9252fdd", size = 10314067, upload-time = "2026-06-19T05:19:44.389Z" }, + { url = "https://files.pythonhosted.org/packages/51/d4/dac8c0720ae246be1700afb6fbdbbea20fe35b13f6570b2f70faa005df77/maturin-1.14.1-py3-none-win_arm64.whl", hash = "sha256:15cea8fcb3ba47dd636f50092bb34baea8b04ac777392f23e6bf8a9a61efb894", size = 9718943, upload-time = "2026-06-19T05:19:47.49Z" }, +] + [[package]] name = "more-itertools" version = "11.1.0"