Configuration¶
Full reference¶
[tool.just-buildit]
command = "make" # optional — omit for zero-config src/{package}/ build
pure = true # optional — pure-Python: copy src/{package}/ verbatim, compile nothing
package = "my_package" # optional — package dir; defaults to normalized project name
editable_path = "src" # optional — src root for .pth editable installs; auto-detected as src/ if present
repair = "uvx ..." # optional — auto-detected by platform, or false to skip
repair-args = ["--plat", "manylinux_2_28_x86_64"] # optional — extra args appended to the repair command
version-from = "vcs" # optional — derive [project] version; requires dynamic = ["version"]
exclude = [ # optional — glob patterns relative to $JUST_BUILDIT_OUTPUT_DIR
"mypkg/tests/**",
"mypkg/bench/**",
]
__pycache__/, *.pyc, and *.pyo are always excluded.
Package directory name
If package is omitted, the directory name is derived from
[project] name by replacing runs of non-alphanumeric characters with
underscores and lower-casing — the same rule Python uses for import
names. A project named my-lib will look for src/my_lib/.
.c and .h files in the wheel
In a normal (non-pure) build, .c and .h files are excluded from
the wheel — they compile into the extension and have no place in the
distribution.
Set pure = true to keep them as package data (useful when shipping C
source templates or scaffolding examples alongside Python code).
Deriving the version¶
[project] version may be derived at build time instead of written as a
literal. List it in PEP 621's dynamic and name a source:
The point is to remove the version as a tracked file. A package index refuses a duplicate filename, so a re-used version is a rejected upload — with a literal, every PR has to bump it, and any two open PRs conflict on every file that carries a copy, whatever they actually changed.
Where the value comes from¶
version-from = "vcs" is currently the only source. It resolves in two steps:
- A sibling
PKG-INFO. Its presence means the tree is an unpacked sdist, and the version recorded in it is that artifact's identity. git describeagainst the nearestv*tag.
| git state | version |
|---|---|
exactly on tag v1.1.3 |
1.1.3 |
| 5 commits past it | 1.1.4.dev5 |
5 commits past v1.1.3-rc1 |
1.1.3rc2.dev5 |
unpacked sdist, no .git |
whatever PKG-INFO records |
Commits past a tag name the release they are working toward, never the one already made. Each derived version therefore sorts strictly after the tag it followed and strictly before the release it anticipates, which is what makes a stream of dev builds usable on an index.
PKG-INFO is checked first, and that ordering is what closes the loop: an
sdist built from a checkout carries a concrete number, so the wheel built from
that sdist agrees with it without needing a repository that is no longer there.
It also stops an sdist unpacked inside an unrelated checkout from picking up
that checkout's tags.
Asking what the version is¶
Deriving the version removes it from every tracked file, which is the point —
and the reason CI then has nothing to read it out of. There is no
[project] version to grep and no file for a version check to probe, so
there are two questions to ask, and they are different:
just-buildit --current-version # -> 1.1.4.dev5 what this tree builds as
just-buildit --next-version # -> 1.1.4 what a release would tag
--current-version is the one a job usually wants: the number the wheel it
just built actually carries. It answers for every project, literal
version or derived, because the version a tree builds as is a fact about the
tree. It is the same value inspect reports, from the same resolution the
build itself uses — so it cannot disagree with the artifact, which scraping
inspect's human output could.
--next-version answers a different question, and refuses where there is no
answer; the rest of this section is about it.
It prints the version the next release would carry, and nothing else, so it can be captured directly:
This invents no policy. A build five commits past v1.1.3 is already called
1.1.4.dev5, and .devN means "on the way to" — so that build already names
1.1.4 as the next release. The query reports the same number with the .devN
removed, from the same parse of the same tag.
| git state | build version | --next-version |
|---|---|---|
exactly on tag v1.1.3 |
1.1.3 |
1.1.4 |
| 1 commit past it | 1.1.4.dev1 |
1.1.4 |
| 5 commits past it | 1.1.4.dev5 |
1.1.4 |
exactly on v1.1.3rc1 |
1.1.3rc1 |
1.1.3rc2 |
3 commits past v1.1.3rc1 |
1.1.3rc2.dev3 |
1.1.3rc2 |
The answer does not change as commits land — only how far along the dev builds are — so it is stable to read at any point in a release branch's life.
Inside a pre-release phase, it stays there
_bump climbs to the first rung that applies — the pre-release number
before the release component — so once you are past v1.1.4a1 the answer
is 1.1.4a2, and after v1.1.4a2 it is 1.1.4a3. It will never
propose 1.1.4.
That is the same judgement version-from never makes for you, seen from
the other side: the scheme does not invent a pre-release phase, and it
does not leave one either. Deciding "the alphas are done, ship it" is
yours, and you record it by tagging v1.1.4 directly.
So a release job must not tag v$(just-buildit --next-version)
unconditionally while in a pre-release phase — it would cut a2, a3,
a4 forever. Use the query for the routine case, and tag the promotion
by hand.
It refuses rather than guesses in the two cases where there is no answer:
- A literal
[project] version. There is no derivation scheme to report, and which digit the next release bumps is a judgement, not a fact about the repository — the same judgementversion-fromnever makes for you. - No tag yet. There is no next release to name until there is a first
one. Unlike the build's own version, this deliberately does not fall
back to a sibling
PKG-INFO: "what comes next" is a question about a repository's history, and an sdist has neither tags nor a future.
Every diagnostic goes to stderr with a non-zero exit, so a failed query can never be captured and pushed as a tag.
Reaching an alpha, beta or release candidate¶
You tag it. The scheme derives; it never invents a pre-release phase, because deciding "this is now an alpha" is a judgement no tool should make for you. The tag is where that judgement is recorded, and everything after it follows automatically:
| git state | version |
|---|---|
after v1.1.2, 5 commits |
1.1.3.dev5 |
on v1.1.3a1 |
1.1.3a1 |
| 3 commits past it | 1.1.3a2.dev3 |
on v1.1.3rc1 |
1.1.3rc1 |
| 3 commits past it | 1.1.3rc2.dev3 |
on v1.1.3 |
1.1.3 |
Those are strictly increasing under PEP 440, in that order. b1, .post1 and
an epoch (v1!2.3) all behave the same way, and the number is bumped
numerically, so a9 becomes a10 rather than a lexical successor.
Tag spelling does not matter¶
A tag is parsed with PEP 440's grammar, so every spelling the specification makes equivalent gives the same version, and what just-buildit emits is always the canonical form — it becomes a wheel filename and a requirement string, so it must match what every other tool writes.
| tag | version at +3 commits |
|---|---|
v1.1.3-rc1, v1.1.3_rc1, v1.1.3.rc1 |
1.1.3rc2.dev3 |
v1.1.3alpha2, v1.1.3ALPHA2 |
1.1.3a3.dev3 |
v1.1.3c1, v1.1.3preview1 |
1.1.3rc2.dev3 |
v1.1.3.rev1, v1.1.3-1 |
1.1.3.post2.dev3 |
v1.1.3a (numeral omitted, so a0) |
1.1.3a1.dev3 |
v1!2.3 |
1!2.4.dev3 |
A tag that is not a PEP 440 version at all is refused by name rather than guessed at.
Tags that cannot be a base¶
Two tag shapes are refused rather than derived from, because appending a commit distance to them does not produce a usable version:
- one already carrying
.dev—v1.1.3.dev5would give1.1.3.dev6.dev2, which is not a valid PEP 440 version at all. - one carrying a local
+segment —v1.1.3+localwould give1.1.4+local.dev3, which is well-formed and is worse for it: the distance lands inside the local part, so every build past the tag compares equal on its public version, and PyPI refuses local versions outright.
Neither is a release tag, and the error names the offending tag rather than reporting a missing one.
When it cannot be determined¶
PEP 621 requires a back-end to raise an error if a field is listed in dynamic
and cannot be computed, so an untagged repository, a missing git, and a tree
that is neither a checkout nor an sdist are all build failures naming the two
ways out. There is deliberately no 0.0.0 placeholder: that would upload a
wrong version rather than fail.
Three further rules, also PEP 621's:
namemay never appear indynamic.- Giving
versionboth statically and indynamicis an error — pick one. - A project that says nothing about
dynamicbehaves exactly as before, so a literal version is still perfectly good.
Pure-Python packages¶
A zero-config build compiles every .c file it finds under
src/{package}/. That is wrong for a pure-Python package that ships .c
files as data (sample sources, test fixtures, scaffolding templates) — they
must land in the wheel untouched, not be handed to a compiler.
Set pure = true to tell just-buildit the package is pure Python:
With pure = true, just-buildit:
- compiles nothing — the
.cscan is skipped entirely - copies the whole
src/{package}/tree verbatim into the wheel, keeping any.c/.hfiles as package data - tags the wheel
py3-none-any(Root-Is-Purelib: true) - skips the wheel-repair step — a pure wheel has no native binary to repair
Cannot combine pure and command
Setting both is a configuration error:
[tool.just-buildit] sets both 'pure' and 'command'.
'pure' means compile nothing — drop 'command', or drop 'pure'.
pure means "compile nothing and copy the source tree verbatim."
A build command is only needed when there is something to compile.
Wheel repair¶
just-buildit automatically runs the right repair tool for your platform:
| Platform | Auto-detected command |
|---|---|
| Linux | uvx auditwheel repair |
| macOS | uvx --from delocate delocate-wheel |
| Windows / MinGW | uvx delvewheel repair |
Override or disable repair in your config:
[tool.just-buildit]
command = "make"
repair = "uvx auditwheel repair" # override the auto-detected command
# repair = false # skip repair entirely
Pass extra arguments without replacing the whole command using repair-args.
The args are appended after the wheel path:
Accepts either a list of strings or a single space-separated string:
Editable installs¶
pip install -e . installs a single .pth file pointing at your source
tree — no build command is run. Python finds your source directly. The C
extension must be compiled in place once (e.g. make) before importing.
Standard src-layout: zero config needed
If editable_path is not set and a src/ directory exists at the
project root, just-buildit uses it automatically:
Set editable_path explicitly only when your source root differs from src/:
No editable source root found
If neither editable_path is set nor a src/ directory exists,
build_editable raises an error instead of guessing: