wit_component/semver_check.rs
1use crate::{
2 ComponentEncoder, StringEncoding, dummy_module, embed_component_metadata,
3 encoding::encode_world,
4};
5use anyhow::{Context, Result, bail};
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!(
60 "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}",
61 )
62 }
63
64 // Component that will be validated at the end.
65 let mut root_component = ComponentBuilder::default();
66
67 // (1) above - create a dummy component which has the shape of `prev`.
68 let mut prev_as_module = dummy_module(&resolve, prev, ManglingAndAbi::Standard32);
69 embed_component_metadata(&mut prev_as_module, &resolve, prev, StringEncoding::UTF8)
70 .context("failed to embed component metadata")?;
71 let prev_as_component = ComponentEncoder::default()
72 .module(&prev_as_module)
73 .context("failed to register previous world encoded as a module")?
74 .encode()
75 .context("failed to encode previous world as a component")?;
76 let component_to_test_idx = root_component.component_raw(&prev_as_component);
77
78 // (2) above - create a component which imports a component of the shape of
79 // `new`.
80 let test_component_idx = {
81 let component_ty =
82 encode_world(&resolve, new).context("failed to encode the new world as a type")?;
83 let mut component = ComponentBuilder::default();
84 let component_ty_idx = component.type_component(&component_ty);
85 component.import(
86 &resolve.worlds[new].name,
87 ComponentTypeRef::Component(component_ty_idx),
88 );
89 root_component.component(component)
90 };
91
92 // (3) Instantiate the component from (2) with the component to test from (1).
93 root_component.instantiate(
94 test_component_idx,
95 [(
96 resolve.worlds[new].name.clone(),
97 ComponentExportKind::Component,
98 component_to_test_idx,
99 )],
100 );
101 let bytes = root_component.finish();
102
103 // The final step is validating that this component is indeed valid. If any
104 // error message is produced here an attempt is made to make it more
105 // understandable but there's only but so good these errors can be with this
106 // strategy.
107 Validator::new()
108 .validate_all(&bytes)
109 .context("new world is not semver-compatible with the previous world")?;
110
111 Ok(())
112}