deps: adopt libcma alpha.10 / libcmt 0.18.0, and fix the riscv64 link so applications can actually use the crate - #3
Merged
Conversation
libcma.a archives only libcma's own objects. Its parser_impl.o references
libcmt's C ABI helpers (cmt_abi_*, cmt_buf_*), and those symbols are not in
the archive, but build.rs emitted only `-lcma -lstdc++`.
Static archive members are pulled lazily and nothing in this crate calls the
C parser -- parser.rs is pure Rust over alloy -- so parser_impl.o was never
pulled and the gap stayed invisible. Any downstream consumer calling
cma_parser_decode_advance/inspect or cma_parser_encode_voucher hit a dozen
`undefined symbol: cmt_abi_*` at link time.
Link libcmt, respecting how upstream's Makefile treats it per arch:
- host-real: build build/host/libcmt.a from the vendored guest-tools
sources (mock io backend) and link it statically;
- riscv64: emit a plain `-lcmt`, since upstream deliberately does not build
it there (its real io backend needs the cartesi kernel headers) and the
machine-guest-tools package in the app image supplies it.
Also self-heal a partially-staged third-party/libcmt. That make target is a
DIRECTORY, whose mtime is trivially newer than the tarball it came from, so
make treats any existing copy as up to date and never repairs it. A checkout
staged by an older revision keeps a headers-only directory forever and the
libcmt compile dies on `third-party/libcmt/src/buf.c: No such file or
directory`.
Guard both halves with tests that force parser_impl.o into the link:
tests/c_parser_link.rs on the host (also checks the C parser agrees with the
Rust port on the canonical ether-deposit vector), and a real riscv64 binary
link in the riscv-link-check CI job, which until now only ar-inspected the
archive and could never have caught this.
…ange build.rs cached the C++ build on `if !lib_path.exists()`. Nothing about libcma.a records which revision of machine-asset-tools it was compiled from, so bumping the submodule left a stale archive in place and silently linked yesterday's libcma against today's headers. The bindgen layout assertions cannot catch that -- they only ever see the headers. Stamp the object directory with the inputs that determine its contents (both submodule revisions, the target arch, and the compiler overrides) and rebuild when they change. On a mismatch the whole object dir is removed first: make cannot be trusted to notice, because a `git checkout` of a submodule can leave sources with OLDER mtimes than objects built from the previous revision, so an incremental make would consider them up to date. The stamp is written only after the archives exist, so an interrupted build leaves none behind and the next run rebuilds instead of trusting a half-built directory.
machine-asset-tools alpha.10 is a single commit whose only substance is
bumping the libcmt it builds against, 0.17.2 -> 0.18.0. libcma's own API and
implementation are untouched: the tag-to-tag diff has zero changes under
include/, ledger/ or parser/. It also adds ioctl.h to the extracted libcmt
header set (0.18.0 vendors the cmio kernel UAPI structs so libcmt
cross-compiles without the cartesi kernel-headers package) and makes
`make docker-image` pass MACHINE_GUEST_TOOLS_VERSION as a build-arg, so the
image's libcmt can no longer drift from the staged headers.
Both submodules move together on purpose. bindgen reads its headers from our
machine-guest-tools submodule while libcma compiles against the tarball
machine-asset-tools downloads itself, so bumping one alone puts 0.17.2 headers
behind 0.18.0 objects.
The libcmt changes reaching bindgen are renames only, with no size, alignment
or field-offset change:
cmt_rollup_t.finish_root_hash -> finish_outputs_merkle_root
struct tag cmt_rollup_finish -> cmt_rollup_finish_s (_t typedef stable)
HTIF_YIELD_REASON_{ADVANCE,INSPECT} -> ..._{ADVANCE,INSPECT}_STATE
Regenerated bindings differ by 54 lines, all renames; the 50 bindgen layout
assertions and the full 100-test host-real suite pass with no source change.
The Cartesi SDK 12 application templates also ship guest-tools 0.18.0, so this
keeps the crate aligned with the runtime its applications execute in.
DEPENDENCIES.lock now records BOTH submodules and is asserted against the
actual pins in CI. It had drifted: it claimed machine-guest-tools v0.17.2
(3d838a2) while the submodule sat on a later main commit (20eba47).
Also corrects two CHANGELOG claims that did not match the code: build.rs
checksums the nlohmann/json header, not "the vendored/built libcma source",
and the uint96 entry cited the e4bfc24 feature branch rather than the
v0.1.0-alpha.9 tag actually pinned.
libcma.a contains only libcma's own objects, so build.rs emits `-lcmt` for the riscv64 backend and libcmt.a must be on the linker's search path in whichever stage cross-links the application binary. Upstream does not build libcmt for riscv64 (its real io backend needs the cartesi kernel headers), so it has to come from the released machine-guest-tools package. Installing machine-guest-tools in the runtime stage -- which the stock Cartesi templates already do -- does NOT satisfy this: by then the binary is linked. Document staging it into the cross sysroot of the build stage instead, at the same version the runtime stage installs, along with the GCC >= 14 cross compiler overrides libcma's C++ needs. Derived from wiring the crate into a real `cartesi create` application; both are the steps that application actually required.
The stamp check as first written deleted any object directory whose stamp did
not match -- including one with NO stamp at all. That would have broken a
documented workflow: cross-building build/riscv64/libcma.a out of band (e.g.
`make docker`) and then running cargo, which is how anyone without a local
RISC-V toolchain uses the riscv64 backend. build.rs would have deleted the
archive and tried to rebuild it natively.
Distinguish the three cases instead:
- stamp present and DIFFERENT -> we built it and the inputs moved: rebuild;
- no stamp, archives present -> supplied from elsewhere: reuse, but warn
that its provenance cannot be checked against the current pins;
- no archives -> build.
This keeps the actual fix (our own stale rebuilds are caught) without taking
away the prebuilt-archive path.
The Cartesi Rust application template sets `-C target-feature=+crt-static`, and
that flag IS honoured for riscv64gc-unknown-linux-gnu: glibc gets linked in
statically. build.rs, however, always requested libstdc++ as a dylib. The
resulting application had a static libc and exactly one dynamic dependency,
libstdc++.so.6 -- and with it an ELF interpreter of /lib/ld.so.1, which does
not exist in the machine's rootfs (Ubuntu riscv64 installs the loader as
/lib/ld-linux-riscv64-lp64d.so.1).
The machine could not exec the application at all:
WARN rollup_http_server::dapp_process] throwing exception because dapp
failed to start with No such file or directory (os error 2)
an error naming neither the loader nor libstdc++, followed by the machine
aborting with "no snapshot to rollback to". So the crate was effectively
unusable in a stock Cartesi Rust application, and nothing in the existing test
suite could see it: the host backend does not set crt-static, and CI never
linked a riscv64 binary.
Under +crt-static, bind libstdc++ and libcmt statically as well. Both archives
live in the gcc-cross directories and the cross sysroot, which the linker
searches but rustc does not, so `static=` alone fails with "could not find
native static library". Each is located with `<compiler> -print-file-name=`, so
the paths come from the toolchain that built libcma rather than being guessed,
and the directory is added to rustc's search path.
Note `-l:libfoo.a` as a raw link-arg does NOT work here: rustc emits link-args
BEFORE the native libraries, so the archive is already past by the time
libcma's undefined __cxa_* references are seen.
Verified by building a `cartesi create --template rust` application against
this crate and booting it on a real Cartesi machine: the binary now has no
PT_INTERP and no NEEDED entries, and the in-machine self-test passes the real
C++ ledger (deposit/transfer/withdraw arithmetic and the records-prefix
layout), the C parser through libcmt, and the Rust parser.
Nothing in CI ever linked a riscv64 EXECUTABLE, which is why the crt-static / libstdc++ defect fixed in 3117a4c reached a real application untouched: `cargo build` on a library only emits an rlib, and rlib members are pulled lazily, so a library build cannot see a missing or wrongly-bound native dependency. Add examples/link_probe.rs, a small binary that touches every native symbol group the crate links against -- the C++ ledger (pulling ledger*.o and libstdc++) and the C parser (pulling parser_impl.o and libcmt's cmt_abi_*). It is meant to be LINKED rather than to be useful. CI now cross-builds it for riscv64gc-unknown-linux-gnu with +crt-static, staging libcmt.a from the machine-guest-tools package into the cross sysroot exactly as an application Dockerfile must, and asserts the result has neither PT_INTERP nor any NEEDED entry. That is precisely the property the Cartesi machine requires and precisely what regressed. Also stop bind-mounting the C probe from /tmp in the riscv link check: a second `-v /tmp/...` mount is refused outright by Docker Desktop, which does not share /tmp. Writing the probe into the already-mounted working directory keeps the step runnable locally as well as on a runner.
The riscv64 link probe itself passed on the first CI run -- g++ linked cmt_link_probe.c against libcma + libcmt cleanly, proving the C parser resolves. The step then failed on its own assertion: `file` is not installed in the libcma-builder image. The idiom was lifted from the neighbouring artifact check, which gets away with it because that one runs on the runner rather than inside the container. Use readelf, which the image does have (it ships binutils alongside g++). That also let the "Cross-link a fully static riscv64 Rust binary" step be skipped, so it has still never executed; this unblocks it.
The fully-static cross-link assertion was riding along in riscv-link-check, which runs a riscv64 container under QEMU. None of it needs emulation: build.rs cross-compiles libcma natively with the RISC-V GCC 14 toolchain, and the link and ELF assertions are ordinary host-side work. Running it as its own job makes the two run in parallel, so the wall clock is the slower of the pair rather than their sum. It also isolates the failure modes -- a broken QEMU container build no longer skips the crt-static guard, which is exactly what happened on the first CI run here. Cache the machine-asset-tools download directory too, since build.rs fetches Boost (~210 MB) through it on a runner that has no prebuilt archive.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adopts
machine-asset-tools v0.1.0-alpha.10/machine-guest-tools (libcmt) v0.18.0, and fixes three build defects that checking the update exposed.The dependency bump itself is mechanically free — libcma's own API and implementation are unchanged between
alpha.9andalpha.10(the tag-to-tag diff has zero changes underinclude/,ledger/,parser/); it only bumps the libcmt it builds against. The libcmt changes reaching bindgen are renames with no size, alignment, or field-offset change, so the regenerated bindings differ by 54 lines and no crate source needed changing.The substance of this PR is what verifying that turned up: the crate could not actually be used by a real Cartesi application. None of the three defects was reachable from the test suite as it stood, because nothing ever linked a riscv64 executable.
The three defects
1. libcmt was never linked (
67e0c06)libcma.aarchives only libcma's own objects. Itsparser_impl.oreferences libcmt'scmt_abi_*/cmt_buf_*, which are not in the archive, butbuild.rsemitted just-lcma -lstdc++. Archive members are pulled lazily and nothing in this crate calls the C parser —parser.rsis pure Rust overalloy— so the gap was invisible here and only broke downstream consumers, with a dozenundefined symbol: cmt_abi_*at link time.Fixed per-arch, matching how upstream's Makefile treats libcmt: built and statically linked on
host-real; a plain-lcmtonriscv64, where upstream deliberately does not build it (its real io backend needs the Cartesi kernel headers) and the machine-guest-tools package supplies it.2. Stale archives survived submodule bumps (
44236aa,7d56db1)build.rscached the C++ build onif !lib_path.exists(). Nothing records which revision producedlibcma.a, so bumping the submodule silently linked the old archive against new headers — a skew the bindgen layout assertions cannot catch, since they only ever see headers. The object dir is now stamped with both submodule revisions, target arch, and compiler overrides. Externally-supplied archives (out-of-bandmake dockercross-builds) are still reused, with a warning that their provenance is uncheckable.3.
+crt-staticproduced an unrunnable binary (427198f)The Cartesi Rust template sets
-C target-feature=+crt-static, and it is honoured forriscv64gc-unknown-linux-gnu— glibc links in statically. Butbuild.rsrequested libstdc++ as a dylib, leaving a static libc, a loneNEEDED libstdc++.so.6, and thereforeINTERP /lib/ld.so.1— which does not exist in the machine rootfs (Ubuntu riscv64 ships the loader as/lib/ld-linux-riscv64-lp64d.so.1). The machine could not exec the application at all:an error naming neither the loader nor libstdc++. Under
+crt-static, libstdc++ and libcmt are now bound statically, each located via<compiler> -print-file-name=so paths come from the toolchain rather than being guessed.Verification
Built a
cartesi create --template rustapplication against this branch and booted it on a real Cartesi machine:exercising the real C++ ledger (deposit/transfer/withdraw arithmetic plus the records-prefix layout), the C parser through libcmt, and the Rust parser.
Also: 100 host-real tests, 91 mock tests, all three stamp cases,
fmt/clippy/doc.CI
examples/link_probe.rs+ a newriscv-rust-linkjob cross-links a fully static riscv64 executable and asserts noPT_INTERPand noNEEDED— the guard for defect 3, which nothing could previously catch. Deliberately QEMU-free and parallel toriscv-link-check.riscv-link-checknow links a real riscv64 binary calling the C parser, instead of onlyar-inspecting the archive.mock-testsassertsDEPENDENCIES.lockmatches the actual submodule pins. It had drifted — it named guest-toolsv0.17.2 (3d838a2)while the submodule sat on a latermaincommit.