Skip to content

Extract dependency WITs during build#3378

Open
itowlson wants to merge 6 commits intospinframework:mainfrom
itowlson:deps-extract-wit
Open

Extract dependency WITs during build#3378
itowlson wants to merge 6 commits intospinframework:mainfrom
itowlson:deps-extract-wit

Conversation

@itowlson
Copy link
Collaborator

@itowlson itowlson commented Jan 21, 2026

Fixes #3376.

WIP. Current status: 1. contains gross, gross hacks replaced with BEAUTIFUL AND ELEGANT hacks, and 2. incomplete.

Work needed:

  • Handle export
  • Test that include is handled correctly I think this works but a second tester would be very welcome
  • Unit tests (and e2e tests and examples)
  • Provide a way to invoke it outside of build (can be deferred if naming is hard, which it always is)

@itowlson
Copy link
Collaborator Author

I think include might be a non-issue because Spin dependencies are on interfaces or packages, not on worlds, and interfaces don't include as far as I can tell (only worlds do).

I do determine the exported interfaces by looking at worlds, but I believe it's safe to ignore include directives for this, because an interface can be exported through an include if there's a world that exports it, and because we iterate the worlds we will pick it up from there.

I am not sure if there is some edge case when a component exports interfaces from other packages but that seems like a potential issue regardless of include. Bother.

@itowlson itowlson force-pushed the deps-extract-wit branch 3 times, most recently from de01fea to 09cf63d Compare February 10, 2026 02:25
@fibonacci1729 fibonacci1729 moved this to Backlog in Spin 4.0 Feb 10, 2026
@itowlson itowlson force-pushed the deps-extract-wit branch 2 times, most recently from 4dbe62d to ed40fa4 Compare February 11, 2026 03:03
@itowlson itowlson marked this pull request as ready for review February 11, 2026 21:10
@fibonacci1729 fibonacci1729 self-requested a review February 25, 2026 20:06
Comment on lines +42 to +46
terminal::warn!("One or more components specified dependencies for which Spin couldn't generate import bindings.");
eprintln!(
"If these components rely on Spin-generated bindings they may fail to build."
);
eprintln!("Otherwise, to skip binding generation, use the --skip-generate-wits flag.");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it accurate to call it bindngs? I am not really sure how to articulate it but to me it feels like bindings are what the language toolchain will generate based on the importized wit. This is a very loosely held opinion, so feel free to ignore.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I was trying to find a wording that didn't presume intimate knowledge of Component Model internals. "Bindings" was the least worst one I could think of (because the effect in your program is that yay you have bindings or boo you don't) but I agree it's technically incorrect (the worst kind of incorrect). I'm very open to suggestions for this!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I squint bindings feels ok to me. I might just replace "bindings" with "interfaces". I'm sympathetic to not wanting to presuppose too much CM knowledge but interfaces in this context, at least to me, transcends the WIT specifics.

@fibonacci1729 fibonacci1729 moved this from Backlog to In progress in Spin 4.0 Mar 2, 2026
itowlson added 5 commits March 4, 2026 08:30
Signed-off-by: itowlson <ivan.towlson@fermyon.com>
Signed-off-by: itowlson <ivan.towlson@fermyon.com>
Signed-off-by: itowlson <ivan.towlson@fermyon.com>
Signed-off-by: itowlson <ivan.towlson@fermyon.com>
Signed-off-by: itowlson <ivan.towlson@fermyon.com>
(DecodedWasm::Component(resolve, world), None) => (resolve, world),
(DecodedWasm::Component(..), Some(_)) => {
anyhow::bail!(
"the `--importize-world` flag is not compatible with a \
Copy link
Collaborator

@fibonacci1729 fibonacci1729 Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably don't want to surface this error back to the user.

I think what we can do here IIUC is what is done in the following case-block:

(DecodedWasm::Component(resolve, id), world) => {
  let world = resolve.select_world(&[id], world)?;
  (resolve, world)
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch - I had to copy a bunch of stuff from wasm-tools because it was in the CLI but not the library, and was just too darn diligent about preserving it. Well, that's my story and I'm sticking to it.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fibonacci1729 The proposed fix doesn't work because id is a WorldId and select_world requires a PackageId, and I'm not sure how to address that. Can you expand on your intention here?

Copy link
Collaborator

@fibonacci1729 fibonacci1729 Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah definitely confusing.

I might rewrite importize here to be as minimal for what we actually need at this time. AFAICT importize is only ever called with world=None.

fn importize(
    decoded: DecodedWasm,
    out_world_name: Option<&String>,
) -> anyhow::Result<DecodedWasm> {
    let (mut resolve, world_id) = match decoded {
        DecodedWasm::Component(resolve, world) => (resolve, world),
        DecodedWasm::WitPackage(resolve, id) => {
            let world = resolve.select_world(&[id], None)?;
            (resolve, world)
        }
    };
    resolve
        .importize(world_id, out_world_name.cloned())
        .context("failed to move world exports to imports")?;

    Ok(DecodedWasm::Component(resolve, world_id))
}

wit-bindgen has a few knobs i don't fully understand.

if wasm.is_none() {
anyhow::bail!(
"input is a core wasm module with no `component-type*` \
custom sections meaning that there is not WIT information; \
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
custom sections meaning that there is not WIT information; \
custom sections meaning that there is no WIT information; \

};

// Capture WITs for all packages used in the importised thing.
// Things like WASI packages may be depended on my multiple packages
Copy link
Collaborator

@fibonacci1729 fibonacci1729 Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
// Things like WASI packages may be depended on my multiple packages
// Things like WASI packages may be depended on by multiple packages

stability: wit_parser::Stability::Unknown,
span: Span::default(),
};
aggregating_resolve
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If two different dependencies import the same interface (e.g. both depend on wasi:http/types), this code inserts into [aggregating_resolve.worlds[...].imports with the same WorldKey. If the map already contains that key, the second insert silently overwrites it. This works by coincidence (both values should be equivalent), but i think it would be wise to debug-assert that the existing value matches the new one which would guard against subtle breakage.

}
}

fn one_import(wasm: &DecodedWasm, name: &str) -> Vec<wit_parser::InterfaceId> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't quite convince myself this can't happen but if the interface name here doesn't match anything, the dependency is silently skipped. I think we should consider returning an error or logging a warning here that no import was added since this seems to indicate a misconfiguration at the manifest level.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed that it can happen (e.g. "foo:bar/interface-that-doesnt-exist@1.0.0" = { ... }) and turned it into an error.

Crossing my fingers for no false positives...

export: &str,
new_name: &DependencyName,
) -> anyhow::Result<DecodedWasm> {
// TODO: I am not sure how `export` is meant to work if you are
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can't actually happen given the validation here:

fn ensure_package_names_no_export(&self) -> anyhow::Result<()> {

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VINDICATI-- uh I mean thanks for the pointer (and for having already dealt to it)


[dev-dependencies]
tempfile = { workspace = true }
wasm-pkg-common = { workspace = true }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a dead dependency

Signed-off-by: itowlson <ivan.towlson@fermyon.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

Status: In progress

Development

Successfully merging this pull request may close these issues.

Emit WITs during build

3 participants