diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 034c79b..bb11c0e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -730,6 +730,39 @@ jobs: # C++ modules in `namespace kal::kit` and exports no name beginning with # `kal_`; a C++ name is mangled to `_ZN3kal3kit…`, so an implementation # that linked the kit still passes `check-surface.sh --complete`. + # ⭐⭐ A PUBLISHED PACKAGE MAY NOT REACH ANOTHER BY PATH. + # + # kit/mcpp.toml is published as openkal-kit, out of this repository's own + # tarball. While it read `openkal = { path = ".." }` that was true here and + # true inside the tarball, and it made the package unusable with any + # implementation: a consumer naming both openkal-kit and an implementation + # reached the specification two ways --- by path from one, by version from + # the other --- and the build refused, correctly, because two routes to one + # package are two copies of it. + # + # ⚠️ NO CONTINUOUS INTEGRATION HERE COULD HAVE CAUGHT IT. Every workflow in + # this ecosystem substitutes working trees, so both routes become paths and + # agree. The defect appears only where an implementation names the + # specification by VERSION, which is the published shape and exactly what + # no job reproduces. It was found in a sandbox against the published + # packages, after release. + # + # This check is structural rather than behavioural for that reason: it + # cannot reproduce the graph, but it can require the form that avoids it. + - name: The kit names the specification by version + run: | + set -euo pipefail + if grep -nE '^openkal = \{[^}]*path' kit/mcpp.toml; then + echo "::error::kit/mcpp.toml reaches the specification by path." + echo " openkal-kit is published from this tarball, and a path" + echo " there is a second route to a package every consumer" + echo " already reaches by version." + exit 1 + fi + grep -qE '^openkal = "[0-9]' kit/mcpp.toml \ + || { echo "::error::kit/mcpp.toml does not name openkal by version at all"; exit 1; } + echo " the kit reaches the specification the way a consumer does: $(grep -E '^openkal = ' kit/mcpp.toml)" + - name: The kit, composed from the interfaces above if: matrix.implementation == 'openkal-linux' run: | diff --git a/kit/mcpp.toml b/kit/mcpp.toml index 1839cfe..2154f93 100644 --- a/kit/mcpp.toml +++ b/kit/mcpp.toml @@ -1,7 +1,7 @@ [package] namespace = "mcpplibs" name = "openkal-kit" -version = "0.1.0" +version = "0.1.1" description = "Facilities composed from openkal's interfaces. Not part of the specification, and structurally incapable of being mistaken for it." license = "Apache-2.0" authors = ["mcpplibs"] @@ -50,10 +50,32 @@ repo = "https://github.com/mcpplibs/openkal" kind = "lib" [dependencies] -openkal = { path = ".." } +# ⚠️⚠️ BY VERSION, NOT BY PATH, AND THE DIFFERENCE IS NOT COSMETIC. +# +# This line read `{ path = ".." }`, which is true inside this repository and +# inside the tarball this package is published from --- and which made the +# package unusable with any implementation. A consumer naming both openkal-kit +# and an implementation reached the specification two ways, and the build +# refused, correctly: +# +# error: dependency 'mcpplibs.openkal' is requested as both a path dep (by +# 'mcpplibs.openkal-kit@0.1.0') and a version dep (by +# 'mcpplibs.openkal-musl@0.4.0'). Pick one. +# +# Two routes to one package are two copies of it, and two copies of a +# specification are two sets of its modules. Measured in a sandbox against the +# published packages, which is the only place this could be observed: no +# continuous integration in this ecosystem resolves a published package, +# because they all substitute working trees by design. +openkal = "0.8.0" -[build] -include_dirs = ["../include"] +# ⚠️ NO `include_dirs` HERE EITHER. It named `../include`, which reaches out of +# this package into whatever happens to sit above it. With the specification +# named by version that directory is the tarball's own copy while the headers +# actually compiled against come from the resolved dependency --- two header +# sets for one contract. openkal publishes `include` as its own `include_dirs`, +# and a dependency's public directories reach a consumer; measured before this +# line was removed, with the specification placed outside this package. # AN IMPLEMENTATION, FOR THE TESTS AND FOR NOTHING ELSE. # diff --git a/tools/run-kit-tests.sh b/tools/run-kit-tests.sh index a95f133..272a6d5 100755 --- a/tools/run-kit-tests.sh +++ b/tools/run-kit-tests.sh @@ -56,6 +56,16 @@ sed -i.bak -E "s|^${package} = \{ version = \"[^\"]*\"(.*)$|${package} = { path "$kit/mcpp.toml" rm -f "$kit/mcpp.toml.bak" +# ⚠️ AND THE KIT REACHES THE SPECIFICATION BY VERSION TOO, WHICH IT DID NOT USED +# TO. While that line read `path = ".."` it already named this working tree and +# needed no substitution. It now names a published version --- because a path +# there made the package unusable alongside any implementation --- so without +# this line these tests would run against the PUBLISHED specification while +# claiming to test the one written here, and a change to the specification would +# be invisible to them. +sed -i.bak -E "s|^openkal = .*$|openkal = { path = \"$here_native\" }|" "$kit/mcpp.toml" +rm -f "$kit/mcpp.toml.bak" + # ⚠️ ASSERTED RATHER THAN ASSUMED. A substitution that matched nothing leaves the # manifest naming a version, the resolver fetches a published implementation, and # the run reports on that one while appearing to report on this branch. @@ -63,6 +73,8 @@ grep -q "path = \"$impl_native\"" "$kit/mcpp.toml" \ || { echo "the implementation substitution matched nothing in kit/mcpp.toml" >&2; exit 2; } grep -q "path = \"$here_native\"" "$impl/mcpp.toml" \ || { echo "the specification substitution matched nothing in $impl/mcpp.toml" >&2; exit 2; } +grep -q "path = \"$here_native\"" "$kit/mcpp.toml" \ + || { echo "the specification substitution matched nothing in kit/mcpp.toml" >&2; exit 2; } echo "--- the kit's dependencies ---" sed -n '/^\[dependencies\]/,/^$/p;/dev-dependencies/,+2p' "$kit/mcpp.toml"