Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 148 additions & 0 deletions .github/workflows/update_bioconda_recipe.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
name: Update Bioconda Recipe

# Opens a version-bump pull request against bioconda/bioconda-recipes when a
# release is published. Bioconda's autobump bot usually does this on its own
# within a day or so; this workflow makes the bump deterministic instead of
# waiting on the bot.
#
# Requires a BIOCONDA_PAT secret: a token with `public_repo` scope on a fork of
# bioconda/bioconda-recipes owned by BIOCONDA_FORK_OWNER.

on:
release:
types: [published]
workflow_dispatch:
inputs:
version:
description: 'Version to update (e.g., 0.33.0)'
required: true
type: string

permissions:
contents: read

env:
BIOCONDA_FORK_OWNER: mwiewior

jobs:
update-bioconda:
runs-on: ubuntu-latest
steps:
- name: Resolve version
id: version
run: |
set -euo pipefail
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
VERSION="${{ github.event.inputs.version }}"
else
VERSION="${GITHUB_REF#refs/tags/}"
fi
# Tags are published without a leading "v", but tolerate one.
VERSION="${VERSION#v}"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "Resolved version: $VERSION"

- name: Wait for the sdist to appear on PyPI
id: sdist
run: |
set -euo pipefail
VERSION="${{ steps.version.outputs.version }}"
URL="https://pypi.org/pypi/polars-bio/${VERSION}/json"

# Publishing to PyPI is a separate workflow, so the release event can
# win the race. Poll rather than fail on a 404.
for attempt in $(seq 1 60); do
SDIST_URL=$(curl -fsSL "$URL" 2>/dev/null \
| jq -r '.urls[] | select(.packagetype=="sdist") | .url' || true)
if [ -n "$SDIST_URL" ]; then
echo "sdist available after ${attempt} attempt(s): $SDIST_URL"
break
fi
echo "Attempt ${attempt}: sdist for ${VERSION} not on PyPI yet; retrying in 30s"
sleep 30
done

if [ -z "${SDIST_URL:-}" ]; then
echo "::error::sdist for polars-bio ${VERSION} never appeared on PyPI"
exit 1
fi

curl -fsSL -o sdist.tar.gz "$SDIST_URL"
SHA256=$(sha256sum sdist.tar.gz | cut -d' ' -f1)
echo "sha256=$SHA256" >> "$GITHUB_OUTPUT"
echo "SHA256: $SHA256"

- name: Checkout bioconda-recipes fork
uses: actions/checkout@v4
with:
repository: ${{ env.BIOCONDA_FORK_OWNER }}/bioconda-recipes
token: ${{ secrets.BIOCONDA_PAT }}
path: bioconda-recipes
fetch-depth: 0

- name: Sync fork with upstream
working-directory: bioconda-recipes
run: |
set -euo pipefail
# Branching off a stale fork produces a PR full of unrelated commits.
git remote add upstream https://github.com/bioconda/bioconda-recipes.git
git fetch upstream master
git checkout -B "update-polars-bio-${{ steps.version.outputs.version }}" upstream/master

- name: Update recipe
working-directory: bioconda-recipes
env:
VERSION: ${{ steps.version.outputs.version }}
SHA256: ${{ steps.sdist.outputs.sha256 }}
run: |
set -euo pipefail
RECIPE=recipes/polars-bio/meta.yaml
test -f "$RECIPE" || { echo "::error::$RECIPE not found — has the recipe been merged yet?"; exit 1; }

# Anchored so these only ever touch the intended lines.
sed -i -E "s|^\{% set version = \".*\" %\}$|{% set version = \"${VERSION}\" %}|" "$RECIPE"
sed -i -E "s|^( sha256: ).*$|\1${SHA256}|" "$RECIPE"
sed -i -E "s|^( number: ).*$|\10|" "$RECIPE"

grep -q "set version = \"${VERSION}\"" "$RECIPE" || { echo "::error::version not updated"; exit 1; }
grep -q "${SHA256}" "$RECIPE" || { echo "::error::sha256 not updated"; exit 1; }

- name: Push and open pull request
working-directory: bioconda-recipes
env:
GH_TOKEN: ${{ secrets.BIOCONDA_PAT }}
VERSION: ${{ steps.version.outputs.version }}
SHA256: ${{ steps.sdist.outputs.sha256 }}
run: |
set -euo pipefail
if git diff --quiet recipes/polars-bio/meta.yaml; then
echo "No changes to push."
exit 0
fi

git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add recipes/polars-bio/meta.yaml
git commit -m "Update polars-bio to ${VERSION}"
git push --force origin "HEAD:update-polars-bio-${VERSION}"

gh pr create \
--repo bioconda/bioconda-recipes \
--base master \
--head "${{ env.BIOCONDA_FORK_OWNER }}:update-polars-bio-${VERSION}" \
--title "Update polars-bio to ${VERSION}" \
--body "$(cat <<EOF
Automated update of polars-bio to ${VERSION}.

| | |
|---|---|
| Version | \`${VERSION}\` |
| SHA256 | \`${SHA256}\` |
| Build number | reset to 0 |

- PyPI: https://pypi.org/project/polars-bio/${VERSION}/
- Release: https://github.com/biodatageeks/polars-bio/releases/tag/${VERSION}

Opened by the \`update_bioconda_recipe\` workflow in biodatageeks/polars-bio.
EOF
)"
107 changes: 107 additions & 0 deletions bioconda-recipe/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# Conda packaging for polars-bio

Source of truth for the conda recipes. The copies that actually get built live in
the upstream repositories; this directory is where they are edited and reviewed
before being copied there.

## Contents

| Path | Target repository | Purpose |
| --- | --- | --- |
| `meta.yaml`, `build.sh`, `test_overlap.py` | `bioconda/bioconda-recipes` → `recipes/polars-bio/` | The polars-bio package |
| `conda-forge-recipes/polars-config-meta/meta.yaml` | `conda-forge/staged-recipes` → `recipes/polars-config-meta/` | A dependency that is not yet packaged for conda |

## Ordering constraint

`polars-config-meta` must land on conda-forge **before** the bioconda recipe can
build, because it is a hard runtime dependency of polars-bio (imported
unconditionally from `polars_bio/__init__.py`). Everything else polars-bio needs
is already on conda-forge:

| Dependency | Required | conda-forge |
| --- | --- | --- |
| `polars` | `>=1.37.1` | yes |
| `pyarrow` | `>=23.0.1,<25` | yes |
| `datafusion` | `>=53.0.0,<54` | yes (53.0.0) |
| `tqdm` | `>=4.67.0,<5` | yes |
| `polars-config-meta` | `>=0.3.0,<1` | **no — submit first** |

Note that `polars-config-meta` declares `dependencies = []` in its
`pyproject.toml` and lists polars only as an optional extra, but imports polars
at module scope. The conda recipe therefore declares polars as a hard run
dependency; without it the package installs but fails on import.

## Build notes

Two things in the source tree need handling in `build.sh`, which is why the
recipe uses a build script rather than an inline `script:` entry:

- `rust-toolchain.toml` pins an exact toolchain for local development. The conda
build supplies its own `rustc` and has no `rustup` to satisfy the pin with, so
the file is removed.
- Upstream CI builds PyPI wheels with `-Ctarget-cpu=skylake` / `apple-m1` and
`-Dwarnings`. Both are wrong for a redistributable package, so `RUSTFLAGS` is
cleared.

The crate graph is large (662 crates) and six dependencies resolve from git
tags, so the build environment needs `git` and network access. `Cargo.lock` is
shipped in the sdist, so those revisions are pinned. `cargo-bundle-licenses`
records the licences of that crate graph into `THIRDPARTY.yml`, which is listed
in `license_file` alongside `LICENSE`.

One local-only gotcha: running `conda-build` without conda-forge's pinning lets
it choose a macOS deployment target from the host SDK, which can exceed the
running OS version. maturin then tags the wheel with that version and the test
phase fails `pip check` with "not supported on this platform". Pass
`--variants "{MACOSX_DEPLOYMENT_TARGET: ['11.0']}"` to reproduce what CI does.
This does not affect bioconda, which pins the target well below the runner.

## Local verification

```bash
conda create -n cbuild -c conda-forge conda-build
conda activate cbuild

# polars-config-meta (noarch)
conda-build conda-forge-recipes/polars-config-meta \
-c conda-forge --override-channels --variants "{python_min: ['3.10']}"

# polars-bio, picking the dependency up from the local channel
conda-build . -c local -c conda-forge --override-channels --python 3.12
```

For a build that matches bioconda CI more closely:

```bash
conda create -n bioconda -c conda-forge -c bioconda bioconda-utils
conda activate bioconda
bioconda-utils lint --packages polars-bio
bioconda-utils build --docker --mulled-test --packages polars-bio
```

## Platforms

Bioconda builds `linux-64` and `osx-64` by default; `osx-arm64` and
`linux-aarch64` require an explicit `extra: additional-platforms:` entry. The
recipe currently targets the defaults only — cross-compiling a Rust tree this
large is worth adding once the package is green, not as part of the initial
submission. Bioconda does not build Windows packages at all; Windows users
continue to install from PyPI.

## Release automation

`.github/workflows/update_bioconda_recipe.yml` opens a version-bump pull request
against `bioconda/bioconda-recipes` when a release is published. Bioconda's own
autobump bot usually does this unprompted once the package exists; the workflow
makes the bump deterministic rather than waiting on the bot. It requires a
`BIOCONDA_PAT` secret with `public_repo` scope.

## Maintainers

- @mwiewior

## References

- [Bioconda contributor guidelines](https://bioconda.github.io/contributor/guidelines.html)
- [Building locally](https://bioconda.github.io/contributor/building-locally.html)
- [conda-forge Rust knowledge base](https://conda-forge.org/docs/maintainer/knowledge_base.html#rust)
25 changes: 25 additions & 0 deletions bioconda-recipe/build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#!/bin/bash
set -euxo pipefail

# The source tree pins an exact toolchain for local development. The conda build
# supplies its own rustc and has no rustup to honour the pin with, so drop it.
rm -f rust-toolchain.toml

# Upstream CI builds wheels with `-Ctarget-cpu=skylake` / `apple-m1` and
# `-Dwarnings`. Neither is appropriate for a redistributable package: the first
# emits instructions that fault on older hardware, the second turns any new
# compiler lint into a build failure. Build for the baseline architecture.
unset RUSTFLAGS || true

export CARGO_PROFILE_RELEASE_DEBUG=false
export CARGO_PROFILE_RELEASE_STRIP=symbols
# Several dependencies resolve from git tags; the CLI honours the build
# environment's proxy and credential configuration where cargo's built-in
# fetcher does not.
export CARGO_NET_GIT_FETCH_WITH_CLI=true
export CARGO_NET_RETRY=5

# Record the licences of the vendored crate graph; referenced by license_file.
cargo-bundle-licenses --format yaml --output THIRDPARTY.yml

$PYTHON -m pip install . -vv --no-deps --no-build-isolation
57 changes: 57 additions & 0 deletions bioconda-recipe/conda-forge-recipes/polars-config-meta/meta.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
{% set name = "polars-config-meta" %}
{% set version = "0.3.4" %}

package:
name: {{ name|lower }}
version: {{ version }}

source:
# The PyPI sdist omits the LICENSE file, which conda-forge requires, so the
# GitHub tag archive is used instead.
url: https://github.com/lmmx/{{ name }}/archive/refs/tags/{{ version }}.tar.gz
sha256: 175f3b981eb0ed1d9f387e811628f24edc6dbdc67aedf9e5fa7e4f160523771c

build:
noarch: python
number: 0
script: {{ PYTHON }} -m pip install . -vv --no-deps --no-build-isolation

requirements:
host:
- python {{ python_min }}
- uv-build >=0.11.6,<0.12
- pip
run:
- python >={{ python_min }}
# Upstream lists polars only as an optional extra, but
# polars_config_meta/__init__.py imports it unconditionally, so it is a
# hard runtime requirement in practice. The lower bound matches the
# `polars` extra in pyproject.toml.
- polars >=1.30

test:
imports:
- polars_config_meta
commands:
- pip check
requires:
- pip
- python {{ python_min }}

about:
home: https://github.com/lmmx/polars-config-meta
summary: A Polars plugin for persistent DataFrame-level metadata
description: |
polars-config-meta is a Polars plugin that attaches persistent,
DataFrame-level metadata to Polars DataFrames and LazyFrames. Metadata
survives operations that return new frames, which plain attribute
assignment cannot do.
license: MIT
license_family: MIT
license_file: LICENSE
doc_url: https://github.com/lmmx/polars-config-meta
dev_url: https://github.com/lmmx/polars-config-meta

extra:
recipe-maintainers:
- mwiewior
Loading
Loading