feat(wit): wit_from_component — recover WIT from an adopted component (#626) - #627
Open
avrabe wants to merge 1 commit into
Open
feat(wit): wit_from_component — recover WIT from an adopted component (#626)#627avrabe wants to merge 1 commit into
avrabe wants to merge 1 commit into
Conversation
…#626) wasm_component_import adopts a prebuilt component into the graph but always sets wit_info = None: the WIT is baked into the binary as a real part of the component-model encoding, so recovering it is mechanical, but every existing consumer had to do that extraction out-of-band and paste the result back in -- exactly the kind of copy that drifts from the artifact it was taken from. Adds wit_from_component: runs `wasm-tools component wit <component>` and returns a WitInfo, so wit_bindgen/wit_library can consume an adopted component's real interfaces without a hand-maintained copy. Wires an optional `wit` attr into wasm_component_import so it can populate wit_info instead of leaving it None (no cycle: the caller builds wit_from_component off a first import, then feeds it into a second one, since the extraction needs the already-adopted wasm_file to exist as an input). Implementation note: the rule runs the extraction twice -- once with `-o` (a single self-contained file, for WitInfo.wit_files) and once with `--out-dir` (a directory, for DefaultInfo) -- rather than once. Two reasons: - wit_bindgen's directory-detection loop needs at least one real File in wit_files regardless of which code path it takes: an empty depset there hits a genuine landmine in wit_bindgen.bzl, where `cmd_args[:-len(wit_file_args)]` with `len(wit_file_args) == 0` evaluates as `cmd_args[:-0]` == `cmd_args[:0]` == `[]`, silently discarding the whole argument list instead of leaving it untouched. - Producing only the self-contained file (no directory) routes through wit_bindgen's "no external dependencies" fallback branch, which -- as discovered while building the example below -- declares an output file wit-bindgen never actually writes to (it writes into a sibling `out_dir` instead; the sibling `wit_library_dir` branch has a copy step bridging exactly this gap, this one doesn't), so any real invocation of that branch fails with "output ... was not created". Nothing else in this repo exercises that branch today, so the bug was latent, not something this PR introduces or needs to fix -- producing a directory output routes around it entirely by taking the already-working `wit_library_dir` path instead. Filed as its own follow-up: the bug is real and someone will hit it on a source-WIT-based no-deps wit_library too. Naming caveat, stated in the code and docs: a component binary retains only its structural imports/exports, not the original source WIT's package/world names. `wasm-tools component wit` extracting from a binary synthesizes fixed wrapper identifiers (`root:component` / `root`) -- verified against actual tool output, not guessed -- so this rule reports those rather than inventing something. The real interface names are exactly right in the extracted text; only the synthetic outer wrapper is generic. Verified end-to-end, not just type-checked: extended examples/component_import_example to chain wasm_component_import -> wit_from_component -> a second wasm_component_import (wit_info populated) -> wit_bindgen (generate_all=True, since the extracted closure explicitly imports wasi:io/* by name, unlike a typical hand-written wit_library source). Inspected the generated root.rs directly: it contains the real `hello:interfaces/greeting@0.1.0` export and `pub mod greeting`, not just WASI boilerplate. `bazel test //examples/component_import_example:component_import_test` passes. Does not address the union case from #626 (deriving one WitInfo from several components whose individually-extracted WIT is incomplete on its own, e.g. a tree-shaken `types` interface split across a fused pipeline) -- that needs a semantic merge, not a mechanical extraction. Follow-up comment posted on the issue asking for the referenced jess implementation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.
Addresses the single-component half of #626.
Problem
wasm_component_importadopts a prebuilt component into the graph but always setswit_info = None. The WIT is baked into the binary — a real, non-optional part of the component-model encoding — so recovering it is mechanical, but every consumer had to extract it out-of-band (wasm-tools component wit <artifact>.wasm) and hand-assemble the result back in. That's exactly the kind of copy that drifts from the artifact it was taken from — the one input the whole build depends on was the one input Bazel didn't manage.What this adds
wit_from_component(//wit:defs.bzl): runswasm-tools component witon an adopted component and returns aWitInfo, sowit_bindgen/wit_librarycan consume the real, extracted interfaces.wasm_component_importgains an optionalwitattr to populatewit_infoinstead of leaving itNone. No cycle: buildwit_from_componentoff a first import, then feed it into a second one (the extraction needs the already-adoptedwasm_fileto exist as an input, so it can't be self-referential on one target).A design decision worth flagging, and a bug found (not introduced) along the way
The rule runs the extraction twice —
-o <file>(self-contained, forWitInfo.wit_files) and--out-dir <dir>(forDefaultInfo) — rather than once. Two reasons, both found by actually building the example, not by inspection:wit_bindgen's directory-detection loop needswit_filesnon-empty regardless of code path — an empty depset there hits a real Starlark landmine:cmd_args[:-len(wit_file_args)]withlen(wit_file_args) == 0evaluates ascmd_args[:-0]==cmd_args[:0]==[], silently discarding the whole argument list.WitInforoutes intowit_bindgen's "no external dependencies" fallback branch — which declares an output file thatwit-bindgennever actually writes (it writes into a siblingout_dir; the parallelwit_library_dirbranch has a copy step bridging exactly this gap, this one doesn't). Any real invocation of that branch fails withoutput ... was not created. Nothing else in this repo exercises that branch today, so this is a pre-existing, latent bug — not something this PR causes or needs to fix. Producing a directory output routes around it by taking the already-provenwit_library_dirpath instead (the one everywit_libraryconsumer already exercises). Worth a follow-up issue for whoever next hits it via a source-WITwit_librarywith no deps.Naming caveat, stated in the code: a component binary retains only structural imports/exports, not the original source WIT's package/world names — those aren't part of the component-model encoding.
wasm-tools component witextracting from a binary synthesizes fixed wrapper identifiers (root:component/root), verified against actual tool output rather than assumed, so the rule reports those instead of guessing. The real interface names are exactly right in the extracted text; only the synthetic outer wrapper is generic.Verification (not just type-checked)
Extended
examples/component_import_exampleto chainwasm_component_import→wit_from_component→ a secondwasm_component_import(wit_infonow populated) →wit_bindgen(generate_all = True, since the extracted closure explicitly nameswasi:io/*imports, unlike a typical hand-writtenwit_librarysource). Inspected the generatedroot.rsdirectly — it contains the realhello:interfaces/greeting@0.1.0export andpub mod greeting, not just WASI boilerplate.bazel test //examples/component_import_example:component_import_testpasses.Not addressed here
The union case from #626 — deriving one
WitInfofrom several components whose individually-extracted WIT is incomplete on its own (a tree-shakentypesinterface split across a fused pipeline) — needs a semantic merge, not a mechanical extraction, and the issue says jess already has a working reference implementation. I've asked on the issue for that as a follow-up rather than reverse-engineering the merge semantics blind.🤖 Generated with Claude Code