Skip to content

fix: a partial caret or tilde requirement treats unwritten segments as wildcards - #866

Merged
Sunrisepeak merged 1 commit into
mcpp-community:mainfrom
NopeCharon:fix/version-req-partial-caret-tilde
Oct 30, 2026
Merged

Sunrisepeak merged 1 commit into
mcpp-community:mainfrom
NopeCharon:fix/version-req-partial-caret-tilde

Conversation

@NopeCharon

Copy link
Copy Markdown
Contributor

Summary

A caret or tilde requirement whose version leaves segments unwritten read them as zeros: ^0 and ^0.0 were bounded at <0.0.1, ~1 at <1.1 and ~0 at <0.1. A dependency "^0" against an index with 0.3.0 and 0.4.0 failed with constraint '^0' matches none of: [0.3.0, 0.4.0], and "~1" never reached 1.1.0.

matches now follows Cargo's partial requirements, which mcpp.xpkg_version already follows for the caret. A segment that is not written is a wildcard:

  • caret: the bound bumps the leftmost non-zero segment, looking no further than the patch and stopping at the last segment written. ^0 → <1.0.0, ^0.0 → <0.1.0; ^0.0.0 stays <0.0.1.
  • tilde: with no minor written, the bound holds the major. ~1 → <2.0.0; ~1.2 and ~1.2.3 stay <1.3.0.

The grammar notes at the top of version_req.cppm and the constraint list in docs/05-dependencies.md (both languages) name the two forms.

Closes #864.

Criteria

  • VersionReq.PartialCaretBoundsFollowTheWrittenSegments and VersionReq.PartialTildeWithOnlyAMajorHoldsTheMajor (tests/unit/test_version_req.cpp). On aarch64 Linux with the bootstrap 2026.10.29.1 binary, mcpp test test_version_req fails both before the change (matches("^0.0", 0.0.7), matches("~1", 1.1.0), matches("~0", 0.4.0) are false) and passes all 31 after it.
  • The full unit suite (mcpp test, 156 passed, 0 failed) and mcpp test -p versioning pass on aarch64 Linux. Other hosts are left to CI.
  • End to end, with the e2e 169 layout (a project [indices] path index publishing acme.gadget 0.3.0 and 0.4.0): the released binary refuses gadget = "^0" and "~0" with "matches none of". The binary built from this branch resolves both to 0.4.0, builds, runs the program and writes version = "0.4.0" to mcpp.lock.

Intersections

New rule or feature Invariant it crosses Test at the crossing
Caret bound stops at the last written segment The leftmost-nonzero rule for fully written versions, including four-segment date versions (^2026.7.27.3 → <2027) MatchCaretBare, MatchCaretZeroMajor, CaretUpperBoundZeroesTheFourthSegment, and the ^0.0.0 / ^0.0.3 / ^0.5 cases in the new test
Tilde with only a major holds the major ~X.Y[.Z] holds the minor; date versions zero the tail MatchTilde, TildeUpperBoundZeroesTheFourthSegment, the ~1.2 case in the new test
Either bound A pre-release is visible only when the requirement names one at the same numbers; the bound is a release RangesDoNotSeePrereleasesUnlessAskedFor, CaretUpperBoundDoesNotLeakIntoTheNextMajorsPrerelease

Compatibility

Only requirements spelled ^0, ^0.0, ~N (one segment) change. Each now admits the versions Cargo admits, a superset of what it admitted before. A manifest that failed to resolve with one of them now resolves. One that resolved now selects the highest match in the wider range, and the lock records it. All other requirement spellings, and bare versions (exact addresses), are unchanged.

Checks before merging

  • bash .github/tools/check_docs_style.sh, check_docs_structure.sh and check_version_pins.sh pass.
  • python3 .github/tools/check_workflow_assertions.py passes (a step asserts what its name says).
  • No commit on the branch carries an attribution trailer:
    git log origin/main..HEAD -i --grep='Co-Authored-By' prints nothing.
  • The squash merge is given an explicit subject and body, so GitHub does not
    compose one from the branch's commits. Suggested subject: fix: a partial caret or tilde requirement treats unwritten segments as wildcards.

…s wildcards

`^0` and `^0.0` bounded their range at 0.0.1, and `~1` and `~0` at 1.1
and 0.1. The caret branch chose the segment to bump through major(),
minor() and patch(), which read a segment that was never written as 0,
so `^0` fell through to bumping the patch; the tilde branch always
bumped the minor. A dependency written `"^0"` against an index with
0.3.0 and 0.4.0 failed with "matches none of", and `"~1"` never
selected 1.1.0 or later.

The grammar is Cargo's, and Cargo reads an unwritten segment as a
wildcard: `^0` is >=0.0.0, <1.0.0; `^0.0` is >=0.0.0, <0.1.0; `~1` is
>=1.0.0, <2.0.0. mcpp.xpkg_version already bounds a partial caret this
way. The caret bound now bumps the leftmost non-zero segment, looking no
further than the patch and stopping at the last segment written; the
tilde bound holds the major alone when no minor is written. Every fully
written requirement (`^0.0.0`, `^0.0.3`, `^1.2.3`, `~1.2`, the
four-segment date versions) keeps its bound.

Unit tests cover the partial caret and tilde forms next to the existing
caret and tilde cases, and docs/05-dependencies.md (both languages)
lists the two forms.

Closes mcpp-community#864
@Sunrisepeak
Sunrisepeak merged commit 9459220 into mcpp-community:main Oct 30, 2026
6 of 21 checks passed
Sunrisepeak added a commit that referenced this pull request Oct 30, 2026
…t; the TOML writer escapes every control character

#866: a bare requirement is a caret, so "0" and "0.0" take the wildcard
bounds as well (<1.0.0, <0.1.0); a unit test pins bare against caret, docs/05
states it in both languages.

#867: with \b, \f and \uXXXX read, a value may hold any control character;
escape_string escapes U+0000-U+001F and U+007F (short forms where TOML has one,
\uXXXX elsewhere, never the reader's \0), so serialize writes TOML. A round
trip test covers every control character.

Design 2026-10-30 part 2 §17.

Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com>
Sunrisepeak added a commit that referenced this pull request Oct 30, 2026
…866/#867 follow-ups; canary build steps; --pkg-format polyglot; ldflags files as link inputs (#869)

* design 2026-10-30 part 2: mcpp plugins and mcppls -- layers, the project's mcpp, mcppls's contract, entries, test cases, the manifest's rules as LSP, CLion native, distribution; one file for every system (#849): the APE and polyglot probes, the polyglot carrier model

Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com>

* 2026.10.31.1: `features` in --protocol-version and every envelope, `program` in self env, and how a tool runs mcpp for a project (docs/50)

A client gates on a feature (`target-sets`, `pkg-format-list`) rather than a
version number; every envelope's `mcpp` field carries the list beside the
version, so one `self env` gives both and a job's envelope says when either
changed. `self env` names the file that is running, so a tool that ran `mcpp`
through a shim in a project's directory reads which mcpp the project uses.
Design 2026-10-30 part 2 §4.0, §4.3.

Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com>

* a workspace's lock entry is keyed the same way whatever members a command plans (#868)

Two members spelling one dependency two ways (`tinyhttps.workspace = true` and
`[dependencies.mcpplibs] tinyhttps.workspace = true`) made `-p` of each write
its own key and hash. The key is now the workspace's spelling: its
`[dependencies]`, `[workspace.dependencies]`, then every member in the order
`members` lists them, selected or not (docs/07, e2e 990).

Closes #868

Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com>

* the follow-ups of #866 and #867: a bare partial requirement is a caret; the TOML writer escapes every control character

#866: a bare requirement is a caret, so "0" and "0.0" take the wildcard
bounds as well (<1.0.0, <0.1.0); a unit test pins bare against caret, docs/05
states it in both languages.

#867: with \b, \f and \uXXXX read, a value may hold any control character;
escape_string escapes U+0000-U+001F and U+007F (short forms where TOML has one,
\uXXXX elsewhere, never the reader's \0), so serialize writes TOML. A round
trip test covers every control character.

Design 2026-10-30 part 2 §17.

Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com>

* release canaries print the longest build steps of every build they made

A canary's time is mostly its project's build (xlings: 1750 s of 47 min); each
canary now ends with the 20 longest steps read from every .ninja_log beneath
it, and `release_canaries.py steps <dir>` prints them for any tree. Design
2026-10-30 part 2 §11.

Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com>

* a file [build] ldflags names by absolute path is a link input

A replaced prebuilt archive named in [build] ldflags did not relink the
program: the word reached the linker but no link edge depended on it, and the
clangd ok-image probe deleted its image to take a new library. A word that names
an existing file by absolute path is now an implicit input of every non-archive
link that carries it (docs/04, e2e 991). Design 2026-10-30 part 2 §25.3 C1.

Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com>

* one file for several systems: --pkg-format polyglot (SPEC-014 2.1 §5.3, #849)

An ok image's single files of every leg and system (--systems all unless
stated) are assembled into one <name>-<version>.exe. The Windows program leads:
its DOS header is a shell assignment (MZqFpD='...') followed by a two-line
script, the PE header moves down by a multiple of the file alignment and the
offsets it holds move with it; the other systems' programs follow unchanged, and
the POSIX script last. Windows starts the file as the PE it is (Authenticode
signs it); Linux and macOS start it through sh, which copies the system's
program to the single-file cache by SPEC-014 §5.2's rules, checks its sha256 and
executes it. Without Windows the file is a #!/bin/sh script.

The carriers are signed before assembly (the Windows one excepted) and the file
after. polyglot is not in a --pkg-format list, a native target is refused, and
mcpp emit xpkg names the file it refuses. mcpp run --pkg-format single-file and
polyglot run the file directly (polyglot through sh on POSIX); they were refused
as unknown formats; tar, zip and dir are refused as containers.

Design 2026-10-30 part 2 §19.6; e2e 992, unit test_pack_polyglot.

Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com>

* CI: the new e2e tests' durations in the Linux shards; e2e 992 also in the wine job, where its file runs as a Windows program

Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com>

* the binary states 2026.10.31.1; #868 reads each member's manifest as written

The version module still named 2026.10.30.1 (help, version and the subsystem
contract test compare it with mcpp.toml). Loading every member through
load_member_manifest for its spelling of a dependency also ran the inheritance a
planned member receives, whose position-reading notes then reached the root's
own build (e2e 911 R5); the manifests are now read as written, the way
workspace_members reads them.

Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com>

* a tree's POSIX launcher names macOS's quarantine mark on its runner, with the command that removes it

A package a browser downloaded carries com.apple.quarantine on every file, and
macOS refuses to start an unsigned runner so marked; the launcher says so before
it starts the runner (SPEC-014 §4.3; mcpp design 2026-10-30 part 2 §10 D6).

Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com>

* docs: the part 2 design is active and indexed; docs/50's heading for how a tool runs mcpp is a noun phrase

Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com>

---------

Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix: ^0, ^0.0 and ~1 read unwritten version segments as zeros and bound the range far too tightly

2 participants