Skip to content

feat(wit): wit_from_component — recover WIT from an adopted component (#626) - #627

Open
avrabe wants to merge 1 commit into
mainfrom
feat/wit-from-component
Open

feat(wit): wit_from_component — recover WIT from an adopted component (#626)#627
avrabe wants to merge 1 commit into
mainfrom
feat/wit-from-component

Conversation

@avrabe

@avrabe avrabe commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Addresses the single-component half of #626.

Problem

wasm_component_import adopts a prebuilt component into the graph but always sets wit_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): runs wasm-tools component wit on an adopted component and returns a WitInfo, so wit_bindgen/wit_library can consume the real, extracted interfaces.
  • wasm_component_import gains an optional wit attr to populate wit_info instead of leaving it None. No cycle: build wit_from_component off a first import, then feed it into a second one (the extraction needs the already-adopted wasm_file to 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, for WitInfo.wit_files) and --out-dir <dir> (for DefaultInfo) — rather than once. Two reasons, both found by actually building the example, not by inspection:

  1. wit_bindgen's directory-detection loop needs wit_files non-empty regardless of code path — an empty depset there hits a real Starlark landmine: 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.
  2. A file-only (no directory) WitInfo routes into wit_bindgen's "no external dependencies" fallback branch — which declares an output file that wit-bindgen never actually writes (it writes into a sibling out_dir; the parallel wit_library_dir branch has a copy step bridging exactly this gap, this one doesn't). Any real invocation of that branch fails with output ... 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-proven wit_library_dir path instead (the one every wit_library consumer already exercises). Worth a follow-up issue for whoever next hits it via a source-WIT wit_library with 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 wit extracting 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_example to chain wasm_component_importwit_from_component → a second wasm_component_import (wit_info now populated) → wit_bindgen (generate_all = True, since the extracted closure explicitly names wasi:io/* imports, 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.

Not addressed here

The union case from #626 — deriving one WitInfo from several components whose individually-extracted WIT is incomplete on its own (a tree-shaken types interface 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

…#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>
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.

1 participant