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}