wit_component/
semver_check.rs

1use crate::{
2    dummy_module, embed_component_metadata, encoding::encode_world, ComponentEncoder,
3    StringEncoding,
4};
5use anyhow::{bail, Context, Result};
6use wasm_encoder::{ComponentBuilder, ComponentExportKind, ComponentTypeRef};
7use wasmparser::Validator;
8use wit_parser::{ManglingAndAbi, Resolve, WorldId};
9
10/// Tests whether `new` is a semver-compatible upgrade from the world `prev`.
11///
12/// This function is will test whether a WIT-level semver-compatible predicate
13/// holds. Internally this will ignore all versions associated with packages and
14/// will instead test for structural equality between types and such. For
15/// example `new` is allowed to have more imports and fewer exports, but types
16/// and such must have the exact same structure otherwise (e.g. function params,
17/// record fields, etc).
18//
19// NB: the general implementation strategy here is similar to the `targets`
20// function where components are synthesized and we effectively rely on
21// wasmparser to figure out everything for us. Specifically what happens is:
22//
23// 1. A dummy component representing `prev` is created.
24// 2. A component importing a component of shape `new` is created.
25// 3. The component from (2) is instantiated with the component from (1).
26//
27// If that all type-checks and is valid then the semver compatible predicate
28// holds. Otherwise something has gone wrong.
29//
30// Note that this does not produce great error messages, so this implementation
31// likely wants to be improved in the future.
32pub fn semver_check(mut resolve: Resolve, prev: WorldId, new: WorldId) -> Result<()> {
33    // First up clear out all version information. This is required to ensure
34    // that the strings line up for wasmparser's validation which does exact
35    // string matching.
36    //
37    // This can leave `resolve` in a weird state which is why this function
38    // takes ownership of `resolve`. Specifically the internal maps for package
39    // names won't be updated and additionally this could leave two packages
40    // with the same name (e.g. no version) which would be a bit odd.
41    //
42    // NB: this will probably cause confusing errors below if a world imports
43    // two versions of a package's interfaces. I think that'll result in weird
44    // wasmparser validation errors as there would be two imports of the same
45    // name for example.
46    for (_id, pkg) in resolve.packages.iter_mut() {
47        pkg.name.version = None;
48    }
49
50    let old_pkg_id = resolve.worlds[prev]
51        .package
52        .context("old world not in named package")?;
53    let old_pkg_name = &resolve.packages[old_pkg_id].name;
54    let new_pkg_id = resolve.worlds[new]
55        .package
56        .context("new world not in named package")?;
57    let new_pkg_name = &resolve.packages[new_pkg_id].name;
58    if old_pkg_name != new_pkg_name {
59        bail!("the old world is in package {old_pkg_name}, which is not the same as the new world, which is in package {new_pkg_name}", )
60    }
61
62    // Component that will be validated at the end.
63    let mut root_component = ComponentBuilder::default();
64
65    // (1) above - create a dummy component which has the shape of `prev`.
66    let mut prev_as_module = dummy_module(&resolve, prev, ManglingAndAbi::Standard32);
67    embed_component_metadata(&mut prev_as_module, &resolve, prev, StringEncoding::UTF8)
68        .context("failed to embed component metadata")?;
69    let prev_as_component = ComponentEncoder::default()
70        .module(&prev_as_module)
71        .context("failed to register previous world encoded as a module")?
72        .encode()
73        .context("failed to encode previous world as a component")?;
74    let component_to_test_idx = root_component.component_raw(&prev_as_component);
75
76    // (2) above - create a component which imports a component of the shape of
77    // `new`.
78    let test_component_idx = {
79        let component_ty =
80            encode_world(&resolve, new).context("failed to encode the new world as a type")?;
81        let mut component = ComponentBuilder::default();
82        let component_ty_idx = component.type_component(&component_ty);
83        component.import(
84            &resolve.worlds[new].name,
85            ComponentTypeRef::Component(component_ty_idx),
86        );
87        root_component.component(component)
88    };
89
90    // (3) Instantiate the component from (2) with the component to test from (1).
91    root_component.instantiate(
92        test_component_idx,
93        [(
94            resolve.worlds[new].name.clone(),
95            ComponentExportKind::Component,
96            component_to_test_idx,
97        )],
98    );
99    let bytes = root_component.finish();
100
101    // The final step is validating that this component is indeed valid. If any
102    // error message is produced here an attempt is made to make it more
103    // understandable but there's only but so good these errors can be with this
104    // strategy.
105    Validator::new()
106        .validate_all(&bytes)
107        .context("new world is not semver-compatible with the previous world")?;
108
109    Ok(())
110}