Skip to content

Conversation

itowlson
Copy link
Collaborator

@itowlson itowlson commented Oct 6, 2025

Draft for discussion of the approach.

The idea here is to allow dependencies of the form:

[component.the-main-course.dependencies]
"useless:[email protected]" = { component_id = "yet-another-example-dep" }

where yet-another-example-dep is another component in the manifest (typically, one not attached to a trigger).

I originally took a sane and reasonable approach, but had to abandon it because I had problems passing enough component info through to the composer for it to resolve the ID. The current approach uses or misuses the normalisation step instead, to turn the ID reference into a "proper" dependency source (e.g. local file, package reference, etc.) based on the component.source of the dependency.

This works (by which I mean it has worked for me at least once). However:

  • It makes normalisation fallible, which is a new thing and not one I love.
  • It puts composition logic into normalisation (knowing which fields are allowed in dependencies).
  • It requires ComponentDependency to have a variant that's used in deserialisation but which must never be seen by the composer (because the composer can't process it, it has to be normalised away first).
    • This could in principle be addressed by decoupling the composer from the serialisation objects - I am not yet sure how much of a faff that is.

So I'm putting this up for people to point out the OBVIOUSLY SUPERIOR IN EVERY RESPECT way of doing this that I've completely overlooked.

(or of course to tell me it's a terrible idea and we shouldn't do it anyway)

Thanks!

cc @fibonacci1729 @tschneidereit

@tschneidereit
Copy link
Contributor

This is great, thank you so much!

I can't really comment on the implementation all that much, so instead I'll comment on the color of this bikeshed's cladding and suggest changing component_id to just component. That matches the use in trigger definitions, besides in my eyes being a bit tidier.

Okay, I have a slightly more substantial question, though it still pertains to the paint more than the construction, and is only kinda-sorta related to this particular change:

Would it at all be possible to support the following syntax, by any chance?

[component.the-main-course.dependencies."useless:feature@0.0.1"]
component_id = "yet-another-example-dep"

The reason I ask is that I would quite like to be able to add more "stuff" to the individual dependencies' tables, which is impossible for all reasonable intents and purposes if all I can work with are inline tables.

@lann
Copy link
Collaborator

lann commented Oct 6, 2025

Would it at all be possible to support the following syntax, by any chance?

Not just possible; those are equivalent in TOML.

if surprises.is_empty() {
Ok(())
} else {
anyhow::bail!("Dependencies may not have their own resources or permissions. Component {depended_on_id} cannot be used as a dependency of {depender_id} because it specifies: {}", surprises.join(", "));
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
anyhow::bail!("Dependencies may not have their own resources or permissions. Component {depended_on_id} cannot be used as a dependency of {depender_id} because it specifies: {}", surprises.join(", "));
anyhow::bail!("🎉 Dependencies may not have their own resources or permissions. Component {depended_on_id} cannot be used as a dependency of {depender_id} because it specifies: {} 🥳", surprises.join(", "));

@lann
Copy link
Collaborator

lann commented Oct 6, 2025

Just confirming: is the goal here just to enable nicer spin build for dependencies?

Comment on lines 174 to 197
if !component.ai_models.is_empty() {
surprises.push("ai_models");
}
if !component.allowed_outbound_hosts.is_empty() {
surprises.push("allowed_outbound_hosts");
}
if !component.dependencies.inner.is_empty() {
surprises.push("dependencies");
}
if !component.environment.is_empty() {
surprises.push("environment");
}
if !component.files.is_empty() {
surprises.push("files");
}
if !component.key_value_stores.is_empty() {
surprises.push("key_value_stores");
}
if !component.sqlite_databases.is_empty() {
surprises.push("sqlite_databases");
}
if !component.variables.is_empty() {
surprises.push("variables");
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

This kind of validation always makes me a little nervous where a future field addition could easily slip through the net here.

Two alternative suggestions:

  • Destructure all the fields of component and let the compiler complain if you miss one:
    let Component {
        ai_models,
        // ...
        variables,
    } = component;
  • (Ab)use toml::Value; something vaguely like:
    #[derive(Deserialize)]
    struct DependencyComponent {
        source: serde::de::IgnoredAny,
        #[serde(flatten)]
        surprises: Map<String, serde::de::IgnoredAny>,
    }
    let value = toml::Value::try_from(component).unwrap();
    let dep_component: DependencyComponent = value.try_into()?;
    let surprises = dep_component.surprises.keys();

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 call - I did the first and it showed me I'd forgotten allowed_http_hosts!

@tschneidereit
Copy link
Contributor

Just confirming: is the goal here just to enable nicer spin build for dependencies?

Kinda, in this version?

Where this gets more interesting is once you combine it with profiles. I would expect (and, @itowlson, we should probably make sure that whichever of these PRs lands first makes it so) a dependency defined in this way to automatically use the right profile for the dependency, so you don't have to list the entire [...dependencies] table again for each profile.

@tschneidereit
Copy link
Contributor

tschneidereit commented Oct 6, 2025

t just possible; those are equivalent in TOML.

I could swear I had this failing for me the other day, but I must've done something wrong.

(For posterity: I was, indeed, wrong, and the error I encountered was unrelated.)

@lann
Copy link
Collaborator

lann commented Oct 6, 2025

Another option, with its own warts:

[component.the-main-course.dependencies."useless:feature@0.0.1"]
path = "dep/target/wasm32-wasip2/release/dep.wasm"

[component.the-main-course.dependencies."useless:feature@0.0.1".build]
command = "cargo build"
workdir = "dep"

@tschneidereit
Copy link
Contributor

Another option, with its own warts

So effectively inline definition of dependencies? I don't think I'm necessarily opposed to this as an option, but it's certainly not a replacement for anything: we'll always want to have the ability to use a component as a dependency for multiple other components.

@itowlson
Copy link
Collaborator Author

itowlson commented Oct 6, 2025

Just confirming: is the goal here just to enable nicer spin build for dependencies?

As well as Till's note, for me it is intention-revealing. That is, although we can achieve the same thing by duplicating a file path, that creates a submarine dependency - the reader has to spot "oh that file is the same as this file" - whereas naming the depended-on component makes it obvious "ah that is part of this application and I can go here to look at it."

@itowlson itowlson force-pushed the dependencies-by-component-ref branch 2 times, most recently from 4576c80 to b1a961f Compare October 6, 2025 22:26
@itowlson itowlson force-pushed the dependencies-by-component-ref branch 2 times, most recently from c27efc2 to dc021cc Compare October 8, 2025 17:56
@itowlson itowlson marked this pull request as ready for review October 8, 2025 17:57
@itowlson itowlson force-pushed the dependencies-by-component-ref branch from dc021cc to 3c94860 Compare October 8, 2025 18:45
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.

4 participants