nailo2c opened a new pull request, #65511:
URL: https://github.com/apache/airflow/pull/65511
closes: #54394
<br>
Hi folks, I know this is a big PR. I'll try my best to explain it concise
and clear.
## Summary
Add build constraints support to Airflow's constraints pipeline.
Airflow now generates and publishes `build-constraints-{python}.txt` next to
the existing runtime constraints, and install flows pass them to `uv pip
install --build-constraints` or `pip install --build-constraint` when available.
## How it works
```mermaid
flowchart LR
A["Existing inputs:<br/>uv.lock + workspace pyproject.toml"]
B["Existing runtime constraints"]
C["Existing constraints branch"]
D["Existing install commands (uv pip install / pip install)"]
X["NEW build constraints generation"]
Y["NEW build-constraints-3.x.txt"]
Z["NEW --build-constraints flags"]
A --> B --> C --> D
A --> X --> Y --> C
C --> Z --> D
classDef existing fill:#ffffff,stroke:#777777,color:#111111;
classDef added
fill:#e7f5ff,stroke:#1971c2,stroke-width:2px,color:#111111;
class A,B,C,D existing;
class X,Y,Z added;
```
**Key design decisions:**
- Build constraints are a single file per Python version, shared across all
3 constraint modes.
- `uv sync` paths are excluded because `uv sync` does not support
`--build-constraints`.
- Fallback no-constraints paths never include build constraints, matching
runtime constraints behavior.
- Auto-inferred missing build constraints are skipped for backward
compatibility with old constraint branches
- Explicit `AIRFLOW_BUILD_CONSTRAINTS_LOCATION` values fail fast if the file
or URL is missing.
- The generator targets build requirements relevant to Airflow's default
wheel-preferred install path, rather than forcing source-build coverage for
every locked package.
<details>
<summary>Source-scan scope validation</summary>
A separate full-scan script scans **all** ~749 packages with sdist URLs
(no wheel filtering, no cache). Running both against the same `uv.lock`:
```
Production (cold): 24.4s (110 sdists) → 24 unique build deps
Production (warm cache): 0.4s (0 downloads)
Full scan: 35.9s (749 sdists) → 61 unique build deps
Only in full scan (production skipped): 37
poetry, poetry-core, pdm-backend, mypy, jupyterlab, twine, ...
Only in production (should be 0): 0
```
All 37 extra deps come from packages with universal wheels. Under the
default wheel-preferred install path, those packages are not expected to enter
build isolation, so the production scanner covers the build deps relevant to
normal Airflow installs while keeping generation fast.
</details>
## Review guide
Suggested review order follows the data flow:
| Area | Main files | What to review |
|------|------------|----------------|
| Generator | scripts/in_container/run_generate_constraints.py | Collect
workspace build requirements and upstream build requirements from uv.lock, then
resolve pinned build dependency versions with uv pip compile. |
| Install helpers | scripts/docker/common.sh | Resolve/download build
constraints and emit the correct uv/pip install flag. |
| Install consumers |
scripts/docker/*.sh<br/>scripts/in_container/install_airflow_and_providers.py<br/>scripts/in_container/install_development_dependencies.py
| Pass build constraints to relevant install commands and omit them from
fallback paths. |
| Breeze plumbing | dev/breeze/src/airflow_breeze/... | Expose
--airflow-build-constraints-location and pass it through env vars / Docker
build args. |
| Publishing |
.github/workflows/generate-constraints.yml<br/>scripts/ci/constraints/*.sh |
Upload, diff, and commit build-constraints-*.txt, including first-time
untracked files. |
| Docs |
installing-from-pypi.rst<br/>MANUALLY_GENERATING_IMAGE_CACHE_AND_CONSTRAINTS.md<br/>09_release_management_tasks.rst
| Document generated files and user-facing uv / pip usage. |
| Tests | scripts/tests/in_container/test_build_constraints.py | Unit
coverage for resolver behavior, helper behavior, command assembly, and fallback
invariants. |
Generated/synced files:
- Dockerfile, Dockerfile.ci were regenerated from the command `prek
update-inlined-dockerfile-scripts --all-files`.
- Breeze command help docs/images were regenerated after adding the new CLI
option.
## Verification
- 64 focused unit tests cover generator, resolver, command assembly, and
fallback behavior.
- Manual Breeze generation produced `build-constraints-*.txt` for all three
constraints modes.
<details>
<summary>Generation: workspace scan, upstream scan, and constraint
resolution</summary>
```console
$ uv run --project scripts python -c "
import sys; sys.path.insert(0, 'scripts/in_container')
from run_generate_constraints import _collect_workspace_build_reqs
from pathlib import Path
reqs = _collect_workspace_build_reqs(Path('.'))
print(f'{len(reqs)} deps: {sorted(reqs)}')
"
10 deps: ['flit-core', 'gitdb', 'gitpython', 'hatchling', 'packaging',
'pathspec', 'pluggy', 'smmap', 'tomli', 'trove-classifiers']
$ uv run --project scripts python -c "
import sys; sys.path.insert(0, 'scripts/in_container')
from run_generate_constraints import _parse_uv_lock
from pathlib import Path
pkgs = _parse_uv_lock(Path('uv.lock'))
targets = [p for p in pkgs if not p.has_universal_wheel and p.sdist_url]
print(f'{len(pkgs)} total, {len(targets)} targets')
"
893 total, 110 targets
$ uv run --project scripts python -c "
import sys; sys.path.insert(0, 'scripts/in_container')
from run_generate_constraints import _stream_build_reqs_from_sdist,
_parse_uv_lock
from pathlib import Path
pkgs = _parse_uv_lock(Path('uv.lock'))
pkg = next(p for p in pkgs if p.name == 'pydantic-core')
reqs = _stream_build_reqs_from_sdist(pkg.sdist_url)
print(f'pydantic-core: {reqs}')
"
pydantic-core: ['maturin>=1.9.4,<2']
```
</details>
<details>
<summary>Integration: 3 constraint modes all produce
build-constraints-*.txt</summary>
```console
$ breeze ci-image build --python 3.12 --upgrade-to-newer-dependencies
--answer yes
$ breeze release-management generate-constraints \
--airflow-constraints-mode constraints --run-in-parallel --answer yes
$ breeze release-management generate-constraints \
--airflow-constraints-mode constraints-source-providers
--run-in-parallel --answer yes
$ breeze release-management generate-constraints \
--airflow-constraints-mode constraints-no-providers --run-in-parallel
--answer yes
$ ls files/constraints-3.12/
build-constraints-3.12.txt constraints-3.12.txt
diff-constraints-3.12.md
build-deps-cache.json constraints-no-providers-3.12.txt
diff-constraints-no-providers-3.12.md
constraints-source-providers-3.12.txt
diff-constraints-source-providers-3.12.md
original-constraints-3.12.txt
original-constraints-no-providers-3.12.txt
original-constraints-source-providers-3.12.txt
$ cat files/constraints-3.12/build-constraints-3.12.txt
# (header omitted)
beniget==0.4.2.post1
flit-core==3.12.0
hatchling==1.29.0
maturin==1.13.1
meson-python==0.18.0
setuptools==82.0.1
wheel==0.46.3
... (31 pinned build deps total)
```
</details>
<details>
<summary>Negative & positive tests: build constraints actually block/allow
installation</summary>
```console
# Create a test package requiring setuptools>=70
$ mkdir -p /tmp/test-pkg && cat > /tmp/test-pkg/pyproject.toml << 'EOF'
[build-system]
requires = ["setuptools>=70"]
build-backend = "setuptools.build_meta"
[project]
name = "test-build-constraint"
version = "0.1.0"
EOF
$ echo "from setuptools import setup; setup()" > /tmp/test-pkg/setup.py
$ cd /tmp/test-pkg && uv build --sdist && cd -
# Negative: conflicting build constraints → correctly fails
$ echo "setuptools==60.0.0" > /tmp/bad-build-constraints.txt
$ uv pip install /tmp/test-pkg/dist/test_build_constraint-0.1.0.tar.gz \
--build-constraints /tmp/bad-build-constraints.txt
× Failed to build `test-build-constraint`
├─▶ Failed to resolve requirements from `build-system.requires`
╰─▶ Because you require setuptools>=70 and setuptools==60.0.0,
we can conclude that your requirements are unsatisfiable.
# Positive: matching build constraints → succeeds
$ echo "setuptools==82.0.1" > /tmp/good-build-constraints.txt
$ uv pip install /tmp/test-pkg/dist/test_build_constraint-0.1.0.tar.gz \
--build-constraints /tmp/good-build-constraints.txt
Installed 1 package in 59ms
+ test-build-constraint==0.1.0
```
</details>
---
##### Was generative AI tooling used to co-author this PR?
<!--
If generative AI tooling has been used in the process of authoring this PR,
please
change below checkbox to `[X]` followed by the name of the tool, uncomment
the "Generated-by".
-->
- [v] Yes (please specify the tool below)
Claude Opus 4.6 + Codex 5.4
<!--
Generated-by: [Tool Name] following [the
guidelines](https://github.com/apache/airflow/blob/main/contributing-docs/05_pull_requests.rst#gen-ai-assisted-contributions)
-->
---
* Read the **[Pull Request
Guidelines](https://github.com/apache/airflow/blob/main/contributing-docs/05_pull_requests.rst#pull-request-guidelines)**
for more information. Note: commit author/co-author name and email in commits
become permanently public when merged.
* For fundamental code changes, an Airflow Improvement Proposal
([AIP](https://cwiki.apache.org/confluence/display/AIRFLOW/Airflow+Improvement+Proposals))
is needed.
* When adding dependency, check compliance with the [ASF 3rd Party License
Policy](https://www.apache.org/legal/resolved.html#category-x).
* For significant user-facing changes create newsfragment:
`{pr_number}.significant.rst`, in
[airflow-core/newsfragments](https://github.com/apache/airflow/tree/main/airflow-core/newsfragments).
You can add this file in a follow-up commit after the PR is created so you
know the PR number.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]