Skip to content

Commit 1f3b9fc

Browse files
committed
build: derive .dev0 from latest_released_version
- `main` always carries the version planned for the *next* release with a `.dev0` suffix, e.g. `0.2.2.dev0`; `.dev0` never appears in a built wheel - `_about.py` now stores `latest_released_version` (the last tagged release) as the source of truth, and computes `__version__` from it (last number + 1, plus `.dev0`) — version math always derives from the last *release*, never from OSS's own in-progress `__version__`, so a downstream build can never look like a release that hasn't shipped - `make build` builds the release `__version__` implies; `make build-dev` builds the same release with a unique `.dev<timestamp>+<sha>` suffix, useful for comparing which of two local wheels is newer - both targets route through `build.py`, which writes the resolved version into `_about.py` before `uv build`, then restores the file; `make version` reads `_about.py` as plain text, no venv needed - adds `COREAI_OPT_VERSION_EXTENSION` so a repo that vendors this one as a submodule can add its own release-counter segment (e.g. `0.2.1.2`) and keep independent version numbering, with `make build`/`make build-dev`/`make version` unchanged - adds a `check-about-version` pre-commit hook that fetches the latest release tag and verifies `latest_released_version`/`__version__` stay consistent with it
1 parent 04acaee commit 1f3b9fc

11 files changed

Lines changed: 856 additions & 79 deletions

File tree

.pre-commit-config.yaml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,20 @@ repos:
319319
types: [python]
320320
exclude: ^tests/
321321

322+
- repo: local
323+
hooks:
324+
- id: check-about-version
325+
name: Check _about.py version fields
326+
description: |
327+
Check that _about.py's latest_released_version matches the repo's
328+
latest release tag, and that __version__ is its last number plus
329+
one, plus .dev0. Catches a release candidate that looks like a
330+
release has already shipped when it hasn't.
331+
entry: python scripts/pre_commit/check_about_version.py
332+
language: system
333+
files: (^|/)_about\.py$
334+
pass_filenames: false
335+
322336
- repo: local
323337
hooks:
324338
- id: towncrier-check

Makefile

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -217,18 +217,23 @@ env-all: _maybe_patch_pyproject
217217
# Build
218218
# =============================================================================
219219

220-
# Build the canonical, publishable distribution (wheel + sdist) via the uv build
221-
# frontend. `--no-sources` ignores [tool.uv.sources], so the artifact doesn't
222-
# depend on uv-specific index overrides — the recommended way to build for
223-
# publication. This is what the release workflow runs.
220+
# Build the canonical, publishable distribution (wheel + sdist): the on-tree
221+
# version with any `.dev` suffix stripped (e.g. 0.2.2.dev0 -> 0.2.2), via
222+
# `uv build --no-sources`. `--no-sources` ignores [tool.uv.sources], so the
223+
# artifact doesn't depend on uv-specific index overrides — the recommended way
224+
# to build for publication. This is what the release workflow runs. Routed
225+
# through build.py (like build-dev) so both targets share one code path; set
226+
# COREAI_OPT_VERSION_EXTENSION to insert an extra release segment (see
227+
# RELEASE.md).
224228
build:
225-
@uv build --no-sources
229+
@$(call use_env,VENV) && uv run --no-sync --active python $(SCRIPTS)/make/build.py --no-sources
226230

227-
# Build the development distribution with build.py (standard version; build.py
228-
# also supports a PEP 440 .dev version via --dev). Used by contributors and the
229-
# smoke tests.
231+
# Build a development distribution with build.py: the release base with a
232+
# unique, timestamped PEP 440 dev suffix (e.g. 0.2.2.dev202607231430+abc1234).
233+
# Used by contributors, the smoke tests, and the nightly pipeline. Set
234+
# DEV_VERSION=... to use an exact version instead.
230235
build-dev:
231-
@$(call use_env,VENV) && uv run --no-sync --active python $(SCRIPTS)/make/build.py
236+
@$(call use_env,VENV) && uv run --no-sync --active python $(SCRIPTS)/make/build.py --dev
232237

233238
# =============================================================================
234239
# Code Quality
@@ -326,9 +331,12 @@ distclean-all:
326331
set-auto-venv:
327332
@$(SCRIPTS)/make/set_auto_venv.sh $(DEFAULT_VENV) $(SHELL_RC)
328333

329-
# Show current version
334+
# Show the development version carried on the tree (e.g. 0.2.2.dev0). Reads
335+
# _about.py as plain text (no torch import needed), so no venv is required —
336+
# stdlib-only, like _maybe_patch_pyproject. Works for a repo that vendors this
337+
# one via COREAI_OPT_VERSION_EXTENSION (e.g. printing 0.2.2.1.dev0).
330338
version:
331-
@python -c "exec(open('$(MAKEFILE_DIR)src/coreai_opt/_about.py').read()); print(__version__)"
339+
@python3 $(SCRIPTS)/make/print_version.py
332340

333341
# =============================================================================
334342
# Documentation

RELEASE.md

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,37 @@
11
# Package Release Guide
22

3-
The OSS release process for Core AI Optimization is being defined. This page will document the workflow for publishing to PyPI once the public release infrastructure is finalized.
3+
The OSS release process for Core AI Optimization is still being defined. This page will document the workflow for publishing to PyPI once the public release infrastructure is finalized.
44

5-
Available locally:
5+
The following commands are available locally:
66

77
```bash
8-
make build # build the package wheel
9-
make version # show current version
8+
make build # build the canonical, publishable wheel + sdist (uv build --no-sources)
9+
make build-dev # build a timestamped dev wheel (e.g. 0.2.2.dev202607231430+abc1234)
10+
make version # show the development version carried on the tree (e.g. 0.2.2.dev0)
1011
make clean # remove build artifacts
1112
```
1213

14+
## Version scheme
15+
16+
`main` always carries the version planned for the _next_ release. This ensures that ongoing development is never mistaken for an already-published version, and that a release can be stabilized, tested, and published on its own branch, independently of later changes on `main`. (The release-branch workflow itself — branch naming, tagging, and backporting fixes to `main` — will be documented separately in the release schedule doc; this section covers only the version-string mechanics.)
17+
18+
`src/coreai_opt/_about.py` stores `latest_released_version` (the last tagged release, e.g. `"0.2.1"`) and computes `__version__` from it by incrementing its last number by one and adding a `.dev0` suffix (e.g. `"0.2.2.dev0"`). A pre-commit hook (`check-about-version`) verifies that `__version__` always follows this rule and that `latest_released_version` matches the repo's latest release tag. As a result, `__version__` can never look as though a release has shipped when it hasn't. The `.dev0` suffix is only a marker on the tree; it never appears in a built wheel.
19+
20+
- `make build` builds the release that `__version__` implies, e.g. `0.2.2`. A release is cut by tagging it (`v0.2.2`); `latest_released_version` is then hard-coded to `"0.2.2"`, which bumps `__version__` to the next candidate (`0.2.3.dev0`).
21+
- `make build-dev` builds that same release but with a unique `.dev<UTC-timestamp>+<short-sha>` suffix instead. It is used by contributors, smoke tests, and the nightly pipeline. `DEV_VERSION=<version>` uses that version exactly instead.
22+
23+
Sorting is preserved: `0.2.2.dev0 < 0.2.2.dev202607231430+abc1234 < 0.2.2`.
24+
25+
### Extending the scheme downstream
26+
27+
A repo that uses this one as a submodule and includes this `Makefile` — building one combined wheel from both trees — can add its own 4th number. Set `COREAI_OPT_VERSION_EXTENSION` to the last value it released for that number (e.g. `"1"` right after releasing `...1`) — **not** the next value, since the `+1` step described below already handles that. Then call `make build`, `make build-dev`, or `make version` unchanged:
28+
29+
- `latest_released_version` `"0.2.1"` + extension `"1"` -> appended: `"0.2.1.1"` -> plus one: `"0.2.1.2"` -> candidate: `0.2.1.2.dev0`
30+
- `make build` -> `0.2.1.2`
31+
- `make build-dev` -> `0.2.1.2.dev<UTC-timestamp>+<short-sha>`
32+
33+
The `+1` step (`scripts/release/release_utils.next_release_base`) is the only place in the entire scheme that adds to a version number, so it is always applied exactly once per build. Setting `COREAI_OPT_VERSION_EXTENSION` to a value that has already been bumped would apply the `+1` twice, so always set it to what is actually on the last built wheel, not what you expect the next one to be. On a repo's very first build from a given `latest_released_version`, there is no prior number to add to, so set the extension to `"0"`, giving `0.2.1.1`.
34+
35+
There is still only one `_about.py` (this package's own); the extra number is a plain string handled entirely in `scripts/release/release_utils.next_release_base` — no other file or package is involved.
36+
1337
<!-- TODO: Document the chosen OSS release workflow (PyPI trusted publishing, twine upload, or uv publish). -->

changelog.d/53.changed

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Carry the next planned release with a `.dev0` suffix on `main` (e.g. `0.2.2.dev0`). `make build` strips the suffix for a clean release and `make build-dev` builds a unique, timestamped dev wheel (`0.2.2.dev<timestamp>+<shortsha>`). A repo that vendors this one can insert one extra release segment via the `COREAI_OPT_VERSION_EXTENSION` environment variable to extend the version scheme.

scripts/make/build.py

Lines changed: 53 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,22 @@
66
"""Build the coreai-opt package.
77
88
Usage:
9-
build.py Build a standard wheel from the current version
10-
build.py --dev Build a dev wheel with a PEP 440 .dev version
9+
build.py --no-sources Build a release: the `.dev` suffix stripped, via
10+
`uv build --no-sources` (ignores [tool.uv.sources];
11+
the recommended way to build the publishable
12+
artifact). Called by `make build`.
13+
build.py --dev Build a dev wheel with a timestamped PEP 440 .dev
14+
version. Called by `make build-dev`.
15+
16+
``_about.py`` stores ``latest_released_version`` (the last tagged release) by
17+
hand; ``__version__`` is computed from it — add one to its last number, add
18+
``.dev0``. This script computes the version to build from
19+
``latest_released_version``, not from the on-tree ``__version__`` (which must
20+
never be treated as already released), writes it into ``_about.py``, builds,
21+
then restores the file. A repo that uses this one as a submodule (building
22+
one combined wheel) can add its own extra number to the version with
23+
``COREAI_OPT_VERSION_EXTENSION``; see
24+
``scripts/release/release_utils.next_release_base``.
1125
"""
1226

1327
from __future__ import annotations
@@ -24,43 +38,64 @@
2438
# internal-only scripts use the explicit `external.scripts.*` form instead.
2539
from scripts._utils import find_repo_root as _find_repo_root
2640
from scripts.release.release_utils import (
27-
get_dev_release_version,
28-
get_package_version,
41+
ENV_VERSION_EXTENSION,
42+
next_release_base,
43+
read_latest_released_version,
44+
resolve_about_path,
45+
resolve_build_version,
2946
write_version,
3047
)
3148

3249

33-
def run_build() -> None:
34-
"""Run ``uv build`` to produce the wheel and sdist."""
50+
def run_build(*, no_sources: bool) -> None:
51+
"""Run ``uv build`` to produce the wheel and sdist.
52+
53+
Args:
54+
no_sources: Pass ``--no-sources`` to ``uv build``, ignoring
55+
``[tool.uv.sources]`` so the artifact doesn't depend on uv-specific
56+
index overrides — used for the publishable release build.
57+
"""
3558
print(f"Building package with python (version: {sys.version})...")
36-
subprocess.run(["uv", "build"], check=True)
59+
command = ["uv", "build", *(["--no-sources"] if no_sources else [])]
60+
subprocess.run(command, check=True)
3761
print("Build complete! Check dist/ directory")
3862

3963

4064
def _build_parser() -> argparse.ArgumentParser:
4165
parser = argparse.ArgumentParser(description="Build the coreai-opt package.")
42-
parser.add_argument(
66+
mode = parser.add_mutually_exclusive_group(required=True)
67+
mode.add_argument(
4368
"--dev",
4469
action="store_true",
45-
help="Build a dev wheel with a PEP 440 .dev version",
70+
help="Build a dev wheel with a timestamped PEP 440 .dev version",
71+
)
72+
mode.add_argument(
73+
"--no-sources",
74+
action="store_true",
75+
help="Build a release via `uv build --no-sources`",
4676
)
4777
return parser
4878

4979

5080
def main() -> None:
5181
args = _build_parser().parse_args()
52-
if not args.dev:
53-
run_build()
54-
return
5582
repo_root = _find_repo_root(Path(__file__))
56-
original_version = get_package_version(repo_root)
57-
dev_version = os.environ.get("DEV_VERSION") or get_dev_release_version(original_version)
83+
84+
about = resolve_about_path(repo_root)
85+
original = about.read_text(encoding="utf-8") # exact bytes to restore afterwards
86+
latest_released = read_latest_released_version(original)
87+
release_base = next_release_base(latest_released, os.environ.get(ENV_VERSION_EXTENSION))
88+
build_version = resolve_build_version(
89+
release_base,
90+
dev=args.dev,
91+
dev_version_override=os.environ.get("DEV_VERSION"),
92+
)
5893
try:
59-
write_version(repo_root, dev_version)
60-
print(f"Dev version: {dev_version}")
61-
run_build()
94+
write_version(about, build_version)
95+
print(f"Version: {build_version}")
96+
run_build(no_sources=args.no_sources)
6297
finally:
63-
write_version(repo_root, original_version)
98+
about.write_text(original, encoding="utf-8", newline="\n") # restore on-tree version
6499

65100

66101
if __name__ == "__main__":

scripts/make/print_version.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
#!/usr/bin/env python3
2+
3+
# Copyright 2026 Apple Inc.
4+
#
5+
# Use of this source code is governed by a BSD-3-Clause license that can
6+
# be found in the LICENSE file or at https://opensource.org/licenses/BSD-3-Clause
7+
8+
"""Print the development version carried on the tree (e.g. 0.2.2.dev0).
9+
10+
Reads ``_about.py`` as plain text (no import, so no extra dependencies) and
11+
computes the same ``.dev0`` version that ``build.py`` builds toward, from
12+
``latest_released_version``. A repo that uses this one as a submodule can add
13+
its own extra number via ``COREAI_OPT_VERSION_EXTENSION`` (e.g. printing
14+
``0.2.1.2.dev0``); see ``scripts/release/release_utils.next_candidate_version``.
15+
"""
16+
17+
import os
18+
from pathlib import Path
19+
20+
from scripts._utils import find_repo_root
21+
from scripts.release.release_utils import (
22+
ENV_VERSION_EXTENSION,
23+
next_candidate_version,
24+
read_latest_released_version,
25+
resolve_about_path,
26+
)
27+
28+
29+
def main() -> None:
30+
about = resolve_about_path(find_repo_root(Path(__file__)))
31+
latest_released = read_latest_released_version(about.read_text(encoding="utf-8"))
32+
print(next_candidate_version(latest_released, os.environ.get(ENV_VERSION_EXTENSION)))
33+
34+
35+
if __name__ == "__main__":
36+
main()
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
#!/usr/bin/env python3
2+
3+
# Copyright 2026 Apple Inc.
4+
#
5+
# Use of this source code is governed by a BSD-3-Clause license that can
6+
# be found in the LICENSE file or at https://opensource.org/licenses/BSD-3-Clause
7+
8+
"""Verify ``_about.py``'s ``latest_released_version`` and ``__version__``.
9+
10+
Two checks:
11+
12+
1. ``latest_released_version`` matches the repo's latest ``vX.Y.Z`` release
13+
tag (fetched fresh from ``origin`` so an out-of-date local tag can't hide a
14+
mismatch). Skipped if there's no such tag yet (e.g. before the first
15+
release).
16+
2. ``__version__`` equals the ``.dev0`` version computed from
17+
``latest_released_version`` (``next_candidate_version``) — its last number
18+
plus one. This stops a release candidate from looking like it already
19+
shipped a release that hasn't happened yet; see
20+
``scripts/release/release_utils.next_candidate_version``.
21+
"""
22+
23+
from __future__ import annotations
24+
25+
import sys
26+
from pathlib import Path
27+
28+
# pre-commit runs this as `entry: python scripts/pre_commit/...`, which puts
29+
# the script's own directory on sys.path[0], not the repo root — so
30+
# `scripts.release.release_utils` can't be imported without help. Adding the
31+
# repo root here makes it importable no matter how the script is run (same
32+
# trick as scripts/patch_pyproject.py).
33+
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
34+
35+
from scripts.release.release_utils import ( # noqa: E402
36+
latest_release_tag,
37+
next_candidate_version,
38+
read_latest_released_version,
39+
read_version,
40+
resolve_about_path,
41+
)
42+
43+
44+
def main() -> int:
45+
"""Check ``latest_released_version`` and the ``__version__`` computed from it."""
46+
repo_root = Path.cwd() # pre-commit runs hooks with cwd set to the repo root
47+
about = resolve_about_path(repo_root)
48+
about_text = about.read_text(encoding="utf-8")
49+
latest_released = read_latest_released_version(about_text)
50+
version = read_version(about_text)
51+
52+
errors = []
53+
54+
latest_tag = latest_release_tag(repo_root)
55+
if latest_tag is not None and latest_tag != latest_released:
56+
errors.append(
57+
f"latest_released_version is {latest_released!r} in {about}, but the "
58+
f"latest release tag is v{latest_tag}. Update latest_released_version "
59+
f"to {latest_tag!r}."
60+
)
61+
62+
expected_version = next_candidate_version(latest_released)
63+
if version != expected_version:
64+
errors.append(
65+
f"__version__ is {version!r} in {about}, but latest_released_version "
66+
f"{latest_released!r} implies {expected_version!r}. __version__ must be "
67+
"latest_released_version with its last segment incremented by one, "
68+
"plus '.dev0'."
69+
)
70+
71+
if errors:
72+
for error in errors:
73+
print(f"_about.py: {error}")
74+
return 1
75+
76+
return 0
77+
78+
79+
if __name__ == "__main__":
80+
sys.exit(main())

0 commit comments

Comments
 (0)