feat: build and publish Nemotron OCR wheels for Python 3.11, 3.12, and 3.13#2362
Conversation
The build_ocr_cuda job in the Hugging Face nightly/release workflow only built Python 3.12 wheels for nemotron-ocr-v2. The C++ extension compiles per-interpreter ABI, so the only blocker to other interpreters was the upstream requires-python metadata. - Add --set-requires-python to nightly_build_publish.py to relax the upstream [project].requires-python so wheels stay pip-installable on all targeted interpreters. - Add --skip-sdist so the interpreter-agnostic sdist is emitted once. - Add a Python 3.11/3.12/3.13 matrix dimension to build_ocr_cuda, drive setup-python from it, relax requires-python to >=3.11,<3.14, build the sdist only on the x86_64+3.12 leg, and make artifact names unique per arch and Python version. Applies to both nightly and stable releases. Co-authored-by: Jeremy Dyer <jdye64@gmail.com>
Greptile SummaryThis PR expands the
|
| Filename | Overview |
|---|---|
| .github/workflows/huggingface-nightly.yml | Adds 3.11/3.12/3.13 Python matrix to build_ocr_cuda job; unique artifact names per arch+tag; apt retry with HTTPS mirror fallback for ARM runners; sdist built exactly once on x86_64+3.12 |
| ci/scripts/nightly_build_publish.py | Adds --set-requires-python, --skip-sdist, license patching functions; venv usability check improved; good test coverage for all new paths |
| ci/tests/test_huggingface_release_workflow.py | New tests for requires-python patching (existing/missing/single-quoted), license metadata, and workflow structure assertions; coverage is thorough |
| nemo_retriever/pyproject.toml | Drops setuptools>=78.1.1 version pin (unrelated to PR goals) and adds packaging without a version constraint; setuptools floor removal could allow old incompatible versions |
| ci/scripts/build_ocr_wheels_local.sh | New local helper script that builds OCR wheels for Python 3.11 and 3.13 (3.12 is the system Python); correctly mirrors CI behaviour with --set-requires-python and --skip-sdist |
| scripts/init_nemotron_ocr_v2.py | New initialisation helper with SPDX header present, updated Python range in docstring (3.11–3.13); straightforward |
| .gitignore | Adds .work*/ and dist* gitignore globs; dist* is broad (previously flagged) but not a blocking issue |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[build_ocr_cuda trigger
nightly / workflow_dispatch] --> B{Matrix
2 arch x 3 Python}
B --> C1[x86_64 + 3.11]
B --> C2[x86_64 + 3.12]
B --> C3[x86_64 + 3.13]
B --> C4[aarch64 + 3.11]
B --> C5[aarch64 + 3.12]
B --> C6[aarch64 + 3.13]
C2 -->|sdist_arg=''| D2[build wheel + sdist]
C1 -->|--skip-sdist| D1[build wheel only]
C3 -->|--skip-sdist| D3[build wheel only]
C4 -->|--skip-sdist| D4[build wheel only]
C5 -->|--skip-sdist| D5[build wheel only]
C6 -->|--skip-sdist| D6[build wheel only]
D1 & D2 & D3 & D4 & D5 & D6 --> E[nightly_build_publish.py]
E --> F[patch requires-python to >=3.11,<3.14]
F --> G[hatch build + auditwheel repair]
G --> H{upload_to}
H -->|nightly| I[TestPyPI]
H -->|stable| J[PyPI]
G --> K[upload-artifact dist-id-arch-cpXYZ]
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
A[build_ocr_cuda trigger
nightly / workflow_dispatch] --> B{Matrix
2 arch x 3 Python}
B --> C1[x86_64 + 3.11]
B --> C2[x86_64 + 3.12]
B --> C3[x86_64 + 3.13]
B --> C4[aarch64 + 3.11]
B --> C5[aarch64 + 3.12]
B --> C6[aarch64 + 3.13]
C2 -->|sdist_arg=''| D2[build wheel + sdist]
C1 -->|--skip-sdist| D1[build wheel only]
C3 -->|--skip-sdist| D3[build wheel only]
C4 -->|--skip-sdist| D4[build wheel only]
C5 -->|--skip-sdist| D5[build wheel only]
C6 -->|--skip-sdist| D6[build wheel only]
D1 & D2 & D3 & D4 & D5 & D6 --> E[nightly_build_publish.py]
E --> F[patch requires-python to >=3.11,<3.14]
F --> G[hatch build + auditwheel repair]
G --> H{upload_to}
H -->|nightly| I[TestPyPI]
H -->|stable| J[PyPI]
G --> K[upload-artifact dist-id-arch-cpXYZ]
Reviews (3): Last reviewed commit: "Merge remote-tracking branch 'origin/mai..." | Re-trigger Greptile
| text = _read_text(setup_cfg) | ||
| changed = False | ||
|
|
||
| if not re.search(r"(?ms)^\[metadata\]\s.*?^\s*license\s*=", text): |
There was a problem hiding this comment.
Dotall regex can match
license = across section boundaries
The (?ms) flags make .*? span newlines and even entire sections. Given a setup.cfg where license lives in [options] (or any section other than [metadata]), the regex ^\[metadata\]\s.*?^\s*license\s*= will still match — the .*? bridge crosses the section boundary. The function will therefore believe [metadata] already contains a license key, skip the insertion, and silently publish the wheel to PyPI with no license metadata in [metadata]. Fixing this requires the pattern to stop at the next section header so it only matches license = within [metadata].
| if not re.search(r"(?ms)^\[metadata\]\s.*?^\s*license\s*=", text): | |
| if not re.search(r"(?m)^\[metadata\][ \t]*(?:#.*)?\n(?:(?!^\[).*\n)*?[ \t]*license\s*=", text): |
Prompt To Fix With AI
This is a comment left during a code review.
Path: ci/scripts/nightly_build_publish.py
Line: 590
Comment:
**Dotall regex can match `license =` across section boundaries**
The `(?ms)` flags make `.*?` span newlines and even entire sections. Given a `setup.cfg` where `license` lives in `[options]` (or any section other than `[metadata]`), the regex `^\[metadata\]\s.*?^\s*license\s*=` will still match — the `.*?` bridge crosses the section boundary. The function will therefore believe `[metadata]` already contains a license key, skip the insertion, and silently publish the wheel to PyPI with no license metadata in `[metadata]`. Fixing this requires the pattern to stop at the next section header so it only matches `license =` within `[metadata]`.
```suggestion
if not re.search(r"(?m)^\[metadata\][ \t]*(?:#.*)?\n(?:(?!^\[).*\n)*?[ \t]*license\s*=", text):
```
How can I resolve this? If you propose a fix, please make it concise.…11-py313-cb4a Co-authored-by: Jeremy Dyer <jdye64@gmail.com>
Description
The Hugging Face nightly/release workflow (
.github/workflows/huggingface-nightly.yml) builds NVIDIA model wheels and publishes them to PyPI. Thebuild_ocr_cudajob — which buildsnemotron-ocr-v2(a package with a compiled C++ extension,nemotron_ocr_cpp) — only produced Python 3.12 wheels becausesetup-pythonwas hardcoded to"3.12".Investigation
cpp_extension(BUILD_CPP_EXTENSION=1) against the active interpreter's ABI, and torch 2.11.0 ships cp311/cp312/cp313 wheels — so per-interpreter compilation works for 3.11–3.13.requires-pythoninherited from the upstream package (the old workflow comment even said 3.12 was chosen "to match upstream package constraints"). pip on 3.11/3.13 refuses to install a wheel whose metadata restricts Python to 3.12.Changes
ci/scripts/nightly_build_publish.py--set-requires-pythonto relax/insert[project].requires-pythonbefore building, so wheels built on a Python matrix stay pip-installable on every targeted interpreter.--skip-sdistso the interpreter-agnostic sdist is produced (and uploaded) exactly once across a matrix, instead of once per leg..github/workflows/huggingface-nightly.yml(build_ocr_cudajob)pythonmatrix dimension (3.11 / 3.12 / 3.13) and drivesetup-pythonfrom it (2 archs × 3 pythons = 6 legs).requires-python = ">=3.11,<3.14".x86_64 + 3.12leg (--skip-sdistelsewhere) to avoid duplicate-upload failures on stable releases.Because both the scheduled nightly and the manual stable release (
workflow_dispatchwithrelease_type=stable) flow through this same job, the new matrix covers both nightly and release-time publishing. This is the only workflow that builds/publishes OCR wheels.Checklist
Testing notes
--set-requires-python(replace existing constraint and insert-when-missing, with no-op re-run) and a workflow assertion that all three Python versions build,requires-pythonis relaxed, the sdist is built once, and artifact names are unique.ci/tests/test_huggingface_release_workflow.pypasses;black(pinned 25.9.0) andflake8are clean; the workflow YAML parses to the expected 6-leg matrix.mainto pick up the Typer 0.27 help-formatting fix (Adopt Typer 0.27 help formatting #2372) that had been failing the unit tests, and the OCR wheel-member validation from Fix OCR v2 revision pinning and validate nightly wheel contents #2375 (which coexists with the new flags). All CI checks are green.