From f15f0cd6739f73e0dd377ad1f8e957d2c7620d30 Mon Sep 17 00:00:00 2001 From: Pavel Zwerschke Date: Fri, 10 Jul 2026 10:08:37 +0200 Subject: [PATCH 1/5] add maturin abi3 example recipe --- docs/_sidebar.json | 3 +- .../example_recipes/maturin-abi3.md | 135 ++++++++++++++++++ 2 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 docs/maintainer/example_recipes/maturin-abi3.md diff --git a/docs/_sidebar.json b/docs/_sidebar.json index d1ea19a62b4..934fdfbdefb 100644 --- a/docs/_sidebar.json +++ b/docs/_sidebar.json @@ -57,7 +57,8 @@ "items": [ "maintainer/example_recipes/go", "maintainer/example_recipes/rust", - "maintainer/example_recipes/pure-python" + "maintainer/example_recipes/pure-python", + "maintainer/example_recipes/maturin-abi3" ] } ] diff --git a/docs/maintainer/example_recipes/maturin-abi3.md b/docs/maintainer/example_recipes/maturin-abi3.md new file mode 100644 index 00000000000..52ea935f43f --- /dev/null +++ b/docs/maintainer/example_recipes/maturin-abi3.md @@ -0,0 +1,135 @@ +--- +title: 'maturin abi3 packages' +--- + +This guide shows you how to create a conda-forge recipe for a Python package with a +compiled Rust extension built with [maturin](https://maturin.rs/) using CPython's +stable [`abi3` ABI](https://docs.python.org/3/c-api/stable.html). + +Building against the stable ABI means a single compiled artifact works across many Python +versions, so the package can be shipped as a +[Python version-independent](https://docs.conda.io/projects/conda-build/en/stable/resources/define-metadata.html#python-version-independent-packages) +package. Instead of one build per Python version, you build once against the minimum +supported Python and reuse it everywhere. + +For more general information on abi3 packages, see the +[knowledge base](../knowledge_base.mdx#abi3). + +## Recipe template + +```yaml title="recipe.yaml" +context: + version: "1.2.3" + +package: + name: example-package + version: ${{ version }} + +source: + url: https://pypi.org/packages/source/e/example-package/example_package-${{ version }}.tar.gz + sha256: 9899b001e26a6d9c0930cd4138a7c7c9d23629dc949b39ae42c25e2d05c2145d + +build: + number: 1 + skip: is_abi3 and not is_python_min + python: + version_independent: ${{ is_abi3 }} + script: + env: + CARGO_PROFILE_RELEASE_STRIP: symbols + CARGO_PROFILE_RELEASE_LTO: fat + content: + # Remove this wrapper once https://github.com/conda-forge/rust-activation-feedstock/pull/79 is merged + - if: unix + then: + - mkdir -p "${BUILD_PREFIX}/bin" + - cp "${RECIPE_DIR}/cargo-auditable-wrapper.sh" "${BUILD_PREFIX}/bin/cargo-auditable-wrapper" + - export CARGO="cargo-auditable-wrapper" + else: + - copy "%RECIPE_DIR%\cargo-auditable-wrapper.bat" "%BUILD_PREFIX%\Library\bin\cargo-auditable-wrapper.bat" || exit 1 + - set CARGO=cargo-auditable-wrapper.bat + - cargo-bundle-licenses --format yaml --output THIRDPARTY.yml + - python -m pip install . --no-deps --ignore-installed -vv --no-build-isolation --disable-pip-version-check + +requirements: + build: + - ${{ compiler('rust') }} + - ${{ stdlib('c') }} + - cargo-bundle-licenses + - cargo-auditable + host: + - python + - if: is_abi3 + then: python-abi3 + - pip + - maturin + run: + - python + +tests: + - python: + python_version: + - ${{ python_min }}.* + - '*' + imports: + - example_package + pip_check: true + - if: is_abi3 + then: + script: + - if: win + then: abi3audit %PREFIX%/Lib/site-packages/example_package/_native.pyd -s -v --assume-minimum-abi3 ${{ python_min }} + else: abi3audit $SP_DIR/example_package/_native.abi3.so -s -v --assume-minimum-abi3 ${{ python_min }} + requirements: + run: + - abi3audit + +about: + homepage: https://github.com/example/example-package + summary: Single-line summary of the package. + license: MIT + license_file: + - LICENSE + - THIRDPARTY.yml + documentation: https://example.com/example-package-docs/ + repository: https://github.com/example/example-package + +extra: + recipe-maintainers: + - LandoCalrissian +``` + +Both `is_abi3` and `is_python_min` are provided by conda-forge's build matrix. This lets the +same recipe fall back to a regular per-Python build if abi3 is ever disabled, without +further changes. + +`python-abi3` (when `is_abi3`): Ensures the extension is built and linked against the stable ABI. +This package pins the abi3 toolchain so the resulting artifact is compatible across Python versions. + +:::note + +If your package doesn't support abi3, remove all abi3-related things from the recipe to build it. + +### The `cargo-auditable-wrapper` + +`cargo-auditable` embeds a dependency manifest into the compiled artifact so it can later be +audited. The plain `cargo auditable install` used for standalone binaries does not fit the +maturin flow, because maturin invokes `cargo` itself. Instead, point the `CARGO` environment +variable at a small wrapper that transparently forwards every invocation through +`cargo auditable`. Add both wrapper scripts next to `recipe.yaml`: + +```sh title="cargo-auditable-wrapper.sh" +#!/bin/sh +exec cargo auditable $* +``` + +```bat title="cargo-auditable-wrapper.bat" +@echo off +cargo auditable %* +``` + +:::note + +These wrappers are a temporary workaround. Once +[rust-activation-feedstock#79](https://github.com/conda-forge/rust-activation-feedstock/pull/79) +is merged, `cargo auditable` will be wired up automatically and the wrappers can be removed. From 11e31e2737479c62bf44daeea25aaee66e7dacfc Mon Sep 17 00:00:00 2001 From: Pavel Zwerschke Date: Fri, 10 Jul 2026 10:16:10 +0200 Subject: [PATCH 2/5] fix admonition --- docs/maintainer/example_recipes/maturin-abi3.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/maintainer/example_recipes/maturin-abi3.md b/docs/maintainer/example_recipes/maturin-abi3.md index 52ea935f43f..3cabf56e729 100644 --- a/docs/maintainer/example_recipes/maturin-abi3.md +++ b/docs/maintainer/example_recipes/maturin-abi3.md @@ -110,6 +110,8 @@ This package pins the abi3 toolchain so the resulting artifact is compatible acr If your package doesn't support abi3, remove all abi3-related things from the recipe to build it. +::: + ### The `cargo-auditable-wrapper` `cargo-auditable` embeds a dependency manifest into the compiled artifact so it can later be @@ -133,3 +135,5 @@ cargo auditable %* These wrappers are a temporary workaround. Once [rust-activation-feedstock#79](https://github.com/conda-forge/rust-activation-feedstock/pull/79) is merged, `cargo auditable` will be wired up automatically and the wrappers can be removed. + +::: From 6f1dfe4889d2aa5c904129be83e78fb73adbf13c Mon Sep 17 00:00:00 2001 From: Pavel Zwerschke Date: Fri, 10 Jul 2026 10:21:30 +0200 Subject: [PATCH 3/5] fix broken anchor link to knowledge base --- docs/maintainer/example_recipes/maturin-abi3.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/maintainer/example_recipes/maturin-abi3.md b/docs/maintainer/example_recipes/maturin-abi3.md index 3cabf56e729..f00aa156e29 100644 --- a/docs/maintainer/example_recipes/maturin-abi3.md +++ b/docs/maintainer/example_recipes/maturin-abi3.md @@ -13,7 +13,7 @@ package. Instead of one build per Python version, you build once against the min supported Python and reuse it everywhere. For more general information on abi3 packages, see the -[knowledge base](../knowledge_base.mdx#abi3). +[knowledge base](../knowledge_base.mdx#packages-with-abi3-extensions). ## Recipe template From 368ea82cf144149ce26b536d0a6c0f8ee1edde1f Mon Sep 17 00:00:00 2001 From: Pavel Zwerschke Date: Wed, 15 Jul 2026 18:02:53 +0200 Subject: [PATCH 4/5] merge abi3.md and maturin-abi3.md --- docs/_sidebar.json | 3 +-- docs/maintainer/example_recipes/abi3.md | 10 ---------- .../maintainer/example_recipes/maturin-abi3.md | 18 +++++++++++++++--- 3 files changed, 16 insertions(+), 15 deletions(-) delete mode 100644 docs/maintainer/example_recipes/abi3.md diff --git a/docs/_sidebar.json b/docs/_sidebar.json index a02d7f5ae90..cd7980ce1cb 100644 --- a/docs/_sidebar.json +++ b/docs/_sidebar.json @@ -68,8 +68,7 @@ "maintainer/example_recipes/go", "maintainer/example_recipes/rust", "maintainer/example_recipes/pure-python", - "maintainer/example_recipes/maturin-abi3", - "maintainer/example_recipes/abi3" + "maintainer/example_recipes/maturin-abi3" ] } ] diff --git a/docs/maintainer/example_recipes/abi3.md b/docs/maintainer/example_recipes/abi3.md deleted file mode 100644 index 51a11898368..00000000000 --- a/docs/maintainer/example_recipes/abi3.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -title: 'Python abi3 packages' ---- - -Packages built against Python's [stable ABI (`abi3`)](https://docs.python.org/3/c-api/stable.html) are compiled once and run on every supported Python version, so a single build covers them all. - -conda-forge keeps full example recipes in the [`python-abi3-feedstock`](https://github.com/conda-forge/python-abi3-feedstock) repository: - -- [`example-recipe.yaml`](https://github.com/conda-forge/python-abi3-feedstock/blob/main/recipe/example-recipe.yaml) (v1 format) -- [`example-meta.yaml`](https://github.com/conda-forge/python-abi3-feedstock/blob/main/recipe/example-meta.yaml) (v0 format) diff --git a/docs/maintainer/example_recipes/maturin-abi3.md b/docs/maintainer/example_recipes/maturin-abi3.md index f00aa156e29..6b5c620aa0a 100644 --- a/docs/maintainer/example_recipes/maturin-abi3.md +++ b/docs/maintainer/example_recipes/maturin-abi3.md @@ -15,6 +15,12 @@ supported Python and reuse it everywhere. For more general information on abi3 packages, see the [knowledge base](../knowledge_base.mdx#packages-with-abi3-extensions). +conda-forge also keeps full example recipes in the +[`python-abi3-feedstock`](https://github.com/conda-forge/python-abi3-feedstock) repository: + +- [`example-recipe.yaml`](https://github.com/conda-forge/python-abi3-feedstock/blob/main/recipe/example-recipe.yaml) (v1 format) +- [`example-meta.yaml`](https://github.com/conda-forge/python-abi3-feedstock/blob/main/recipe/example-meta.yaml) (v0 format) + ## Recipe template ```yaml title="recipe.yaml" @@ -99,9 +105,15 @@ extra: - LandoCalrissian ``` -Both `is_abi3` and `is_python_min` are provided by conda-forge's build matrix. This lets the -same recipe fall back to a regular per-Python build if abi3 is ever disabled, without -further changes. +Both `is_abi3` and `is_python_min` are provided by conda-forge's build matrix. + +`is_abi3` is not really about the package itself but about the Python *variant* being built +against, and whether that variant supports the stable ABI. Regular CPython does, so +`is_abi3` is `true` there and the extension is built once against the minimum supported +Python. Other variants — such as free-threading CPython or PyPy — don't provide a stable +ABI, so `is_abi3` is `false` and the recipe falls back to a normal per-Python build. The +`if: is_abi3` selectors are what let the same recipe handle both cases without further +changes. `python-abi3` (when `is_abi3`): Ensures the extension is built and linked against the stable ABI. This package pins the abi3 toolchain so the resulting artifact is compatible across Python versions. From 2829233f85610a2aea7d8ac4681815e64aaa269d Mon Sep 17 00:00:00 2001 From: Pavel Zwerschke Date: Wed, 15 Jul 2026 18:33:10 +0200 Subject: [PATCH 5/5] fix --- docs/maintainer/example_recipes/maturin-abi3.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/maintainer/example_recipes/maturin-abi3.md b/docs/maintainer/example_recipes/maturin-abi3.md index 6b5c620aa0a..c3366560b4d 100644 --- a/docs/maintainer/example_recipes/maturin-abi3.md +++ b/docs/maintainer/example_recipes/maturin-abi3.md @@ -107,7 +107,7 @@ extra: Both `is_abi3` and `is_python_min` are provided by conda-forge's build matrix. -`is_abi3` is not really about the package itself but about the Python *variant* being built +`is_abi3` is not really about the package itself but about the Python _variant_ being built against, and whether that variant supports the stable ABI. Regular CPython does, so `is_abi3` is `true` there and the extension is built once against the minimum supported Python. Other variants — such as free-threading CPython or PyPy — don't provide a stable