Skip to content

Commit 09cf63d

Browse files
committed
Handle export modifier, hopefully
Signed-off-by: itowlson <ivan.towlson@fermyon.com>
1 parent 0f57b9a commit 09cf63d

3 files changed

Lines changed: 276 additions & 41 deletions

File tree

Cargo.lock

Lines changed: 5 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/dependency-wit/Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,8 @@ tokio = { workspace = true, features = ["rt", "time"] }
1414
wasmparser = { workspace = true }
1515
wit-component = { workspace = true }
1616
wit-parser = { workspace = true }
17+
18+
[dev-dependencies]
19+
tempfile = { workspace = true }
20+
wasm-pkg-common = { workspace = true }
21+
wit-component = { workspace = true, features = ["dummy-module"] }

crates/dependency-wit/src/lib.rs

Lines changed: 266 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -35,16 +35,35 @@ pub async fn extract_wits(
3535

3636
// TODO: figure out what to do if we import two itfs from same dep
3737
for (index, (dependency_name, dependency)) in source.enumerate() {
38-
// TODO: map `export`
39-
let (wasm_path, _export) = loader
38+
let import_name = match dependency_name {
39+
DependencyName::Plain(_) => None,
40+
DependencyName::Package(dependency_package_name) => {
41+
dependency_package_name.interface.as_ref()
42+
// match dependency_package_name.interface.as_ref() {
43+
// Some(itf) => Some(itf),
44+
// None => None,
45+
// }
46+
}
47+
};
48+
49+
let (wasm_path, export) = loader
4050
.load_component_dependency(dependency_name, dependency)
4151
.await?;
4252
let wasm_bytes = tokio::fs::read(&wasm_path).await?;
4353

4454
let decoded = read_wasm(&wasm_bytes)?;
55+
let decoded = match export {
56+
None => decoded,
57+
Some(export) => munge_aliased_export(decoded, &export, dependency_name)?,
58+
};
4559
let impo_world = format!("impo-world{index}");
4660
let importised = importize(decoded, None, Some(&impo_world))?;
4761

62+
let imports = match import_name {
63+
None => all_imports(&importised),
64+
Some(itf) => one_import(&importised, itf.as_ref()),
65+
};
66+
4867
// Capture WITs for all packages used in the importised thing.
4968
// Things like WASI packages may be depended on my multiple packages
5069
// so we index on the package name to avoid emitting them twice.
@@ -68,16 +87,6 @@ pub async fn extract_wits(
6887

6988
// Now add the imports to the aggregating component import world
7089

71-
let imports = match dependency_name {
72-
DependencyName::Plain(_) => all_imports(&importised),
73-
DependencyName::Package(dependency_package_name) => {
74-
match dependency_package_name.interface.as_ref() {
75-
Some(itf) => one_import(&importised, itf),
76-
None => all_imports(&importised),
77-
}
78-
}
79-
};
80-
8190
let remap = aggregating_resolve.merge(importised.resolve().clone())?;
8291
for iid in imports {
8392
let mapped_iid = remap.map_interface(iid, None)?;
@@ -113,6 +122,122 @@ pub async fn extract_wits(
113122
Ok(buf)
114123
}
115124

125+
fn munge_aliased_export(
126+
decoded: DecodedWasm,
127+
export: &str,
128+
new_name: &DependencyName,
129+
) -> anyhow::Result<DecodedWasm> {
130+
// TODO: I am not sure how `export` is meant to work if you are
131+
// depping on a package rather than an itf
132+
133+
let export_qname = spin_serde::DependencyPackageName::try_from(export.to_string())?;
134+
let Some(export_itf_name) = export_qname.interface.as_ref() else {
135+
anyhow::bail!("the export name should be a qualified interface name - missing interface");
136+
};
137+
let export_pkg_name = wit_parser::PackageName {
138+
namespace: export_qname.package.namespace().to_string(),
139+
name: export_qname.package.name().to_string(),
140+
version: export_qname.version,
141+
};
142+
143+
let DependencyName::Package(new_name) = new_name else {
144+
anyhow::bail!("the dependency name should be a qualified interface name - not qualified");
145+
};
146+
let Some(new_itf_name) = new_name.interface.as_ref() else {
147+
anyhow::bail!(
148+
"the dependency name should be a qualified interface name - missing interface"
149+
);
150+
};
151+
let new_pkg_name = wit_parser::PackageName {
152+
namespace: new_name.package.namespace().to_string(),
153+
name: new_name.package.name().to_string(),
154+
version: new_name.version.clone(),
155+
};
156+
157+
let (mut resolve, decode_id) = match decoded {
158+
DecodedWasm::WitPackage(resolve, id) => (resolve, WorldOrPackageId::Package(id)),
159+
DecodedWasm::Component(resolve, id) => (resolve, WorldOrPackageId::World(id)),
160+
};
161+
162+
// Two scenarios:
163+
// 1. The new name is in a package that is already in the Resolve
164+
// 1a. The package already contains an interface with the right name
165+
// 1b. The package does not already contain an interface with the right name
166+
// 2. The new name is in a package that is NOT already in the Resolve
167+
168+
let existing_pkg = resolve
169+
.packages
170+
.iter()
171+
.find(|(_pkg_id, pkg)| pkg.name == new_pkg_name);
172+
173+
// We address the first level by creating the new-name package if it doesn't exist
174+
let (inserting_into_pkg_id, inserting_into_pkg) = match existing_pkg {
175+
Some(tup) => tup,
176+
None => {
177+
// insert the needed package
178+
let package_wit = format!("package {new_pkg_name};");
179+
let pkg_id = resolve
180+
.push_str(std::env::current_dir().unwrap(), &package_wit)
181+
.context("failed at setting up fake pkg")?;
182+
let pkg = resolve.packages.get(pkg_id).unwrap();
183+
(pkg_id, pkg)
184+
}
185+
};
186+
187+
// Second level asks if the package already contains the interface
188+
let existing_itf = inserting_into_pkg.interfaces.get(new_itf_name.as_ref());
189+
if existing_itf.is_some() {
190+
// no rename is needed, but we might need to do some extra work to make sure
191+
// that the export, rather than the import, gets included in the aggregated world
192+
return Ok(decode_id.make_decoded_wasm(resolve));
193+
}
194+
195+
// It does not: we need to slurp the EXPORTED itf into the `inserting_into`
196+
// package under the NEW (importing) interface name
197+
let Some(export_pkg_id) = resolve.package_names.get(&export_pkg_name) else {
198+
anyhow::bail!("export is from a package that doesn't exist");
199+
};
200+
let Some(export_pkg) = resolve.packages.get(*export_pkg_id) else {
201+
anyhow::bail!("export pkg id doesn't point to a package wtf");
202+
};
203+
let Some(export_itf_id) = export_pkg.interfaces.get(export_itf_name.as_ref()) else {
204+
anyhow::bail!("export pkg doesn't contain export itf");
205+
};
206+
let Some(export_itf) = resolve.interfaces.get(*export_itf_id) else {
207+
anyhow::bail!("export pkg doesn't contain export itf part 2");
208+
};
209+
210+
let mut export_itf = export_itf.clone();
211+
export_itf.package = Some(inserting_into_pkg_id);
212+
export_itf.name = Some(new_itf_name.to_string());
213+
let export_itf_id_2 = resolve.interfaces.alloc(export_itf);
214+
215+
// OKAY TIME TO ADD THIS UNDER THE WRONG NAME TO THE THINGY
216+
// oh man there is some nonsense about worlds as well
217+
let inserting_into_pkg_mut = resolve.packages.get_mut(inserting_into_pkg_id).unwrap(); // SHENANIGANS to get around a "mutable borrow at the same time as immutable borrow" woe
218+
inserting_into_pkg_mut
219+
.interfaces
220+
.insert(new_itf_name.to_string(), export_itf_id_2);
221+
222+
let thingy = decode_id.make_decoded_wasm(resolve);
223+
224+
Ok(thingy)
225+
}
226+
227+
enum WorldOrPackageId {
228+
Package(wit_parser::PackageId),
229+
World(wit_parser::WorldId),
230+
}
231+
232+
impl WorldOrPackageId {
233+
pub fn make_decoded_wasm(&self, resolve: wit_parser::Resolve) -> DecodedWasm {
234+
match self {
235+
Self::Package(id) => DecodedWasm::WitPackage(resolve, *id),
236+
Self::World(id) => DecodedWasm::Component(resolve, *id),
237+
}
238+
}
239+
}
240+
116241
fn all_imports(wasm: &DecodedWasm) -> Vec<wit_parser::InterfaceId> {
117242
wasm.resolve()
118243
.worlds
@@ -129,7 +254,7 @@ fn as_interface(wi: &wit_parser::WorldItem) -> Option<wit_parser::InterfaceId> {
129254
}
130255
}
131256

132-
fn one_import(wasm: &DecodedWasm, name: &spin_serde::KebabId) -> Vec<wit_parser::InterfaceId> {
257+
fn one_import(wasm: &DecodedWasm, name: &str) -> Vec<wit_parser::InterfaceId> {
133258
let id = wasm
134259
.resolve()
135260
.interfaces
@@ -182,28 +307,131 @@ fn importize(
182307
Ok(DecodedWasm::Component(resolve, world_id))
183308
}
184309

185-
// fn all_imports(wasm: &DecodedWasm) -> Vec<(wit_parser::PackageName, String)> {
186-
// let mut itfs = vec![];
187-
188-
// for (_pid, pp) in &wasm.resolve().packages {
189-
// for (_w, wid) in &pp.worlds {
190-
// if let Some(world) = wasm.resolve().worlds.get(*wid) {
191-
// for (_wk, witem) in &world.imports {
192-
// if let wit_parser::WorldItem::Interface { id, .. } = witem {
193-
// if let Some(itf) = wasm.resolve().interfaces.get(*id) {
194-
// if let Some(itfp) = itf.package.as_ref() {
195-
// if let Some(ppp) = wasm.resolve().packages.get(*itfp) {
196-
// if let Some(itfname) = itf.name.as_ref() {
197-
// itfs.push((ppp.name.clone(), itfname.clone()));
198-
// }
199-
// }
200-
// }
201-
// }
202-
// }
203-
// }
204-
// }
205-
// }
206-
// }
207-
208-
// itfs
209-
// }
310+
#[cfg(test)]
311+
mod test {
312+
use super::*;
313+
314+
fn parse_wit(wit: &str) -> anyhow::Result<wit_parser::Resolve> {
315+
let mut resolve = wit_parser::Resolve::new();
316+
resolve.push_str("dummy.wit", wit)?;
317+
Ok(resolve)
318+
}
319+
320+
fn generate_dummy_component(wit: &str, world: &str) -> Vec<u8> {
321+
let mut resolve = wit_parser::Resolve::default();
322+
let package_id = resolve.push_str("test", wit).expect("should parse WIT");
323+
let world_id = resolve
324+
.select_world(&[package_id], Some(world))
325+
.expect("should select world");
326+
327+
let mut wasm = wit_component::dummy_module(
328+
&resolve,
329+
world_id,
330+
wit_parser::ManglingAndAbi::Legacy(wit_parser::LiftLowerAbi::Sync),
331+
);
332+
wit_component::embed_component_metadata(
333+
&mut wasm,
334+
&resolve,
335+
world_id,
336+
wit_component::StringEncoding::UTF8,
337+
)
338+
.expect("should embed component metadata");
339+
340+
let mut encoder = wit_component::ComponentEncoder::default()
341+
.validate(true)
342+
.module(&wasm)
343+
.expect("should set module");
344+
encoder.encode().expect("should encode component")
345+
}
346+
347+
#[tokio::test]
348+
async fn if_no_dependencies_then_empty_valid_wit() -> anyhow::Result<()> {
349+
let wit = extract_wits(std::iter::empty(), ".").await?;
350+
351+
let resolve = parse_wit(&wit).expect("should have emitted valid WIT");
352+
353+
assert_eq!(1, resolve.packages.len());
354+
assert_eq!(
355+
"root:component",
356+
resolve.packages.iter().next().unwrap().1.name.to_string()
357+
);
358+
359+
assert_eq!(0, resolve.interfaces.len());
360+
361+
assert_eq!(1, resolve.worlds.len());
362+
363+
let world = resolve.worlds.iter().next().unwrap().1;
364+
assert_eq!("root", world.name);
365+
assert_eq!(0, world.imports.len());
366+
367+
Ok(())
368+
}
369+
370+
#[tokio::test]
371+
async fn single_dep_wit_extracted() -> anyhow::Result<()> {
372+
let tempdir = tempfile::TempDir::new()?;
373+
let dep_file = tempdir.path().join("regex.wasm");
374+
375+
let dep_wit = "package my:regex@1.0.0;\n\ninterface regex {\n matches: func(s: string) -> bool;\n}\nworld matcher {\n export regex;\n}";
376+
let dep_wasm = generate_dummy_component(dep_wit, "matcher");
377+
tokio::fs::write(&dep_file, &dep_wasm).await?;
378+
379+
let dep_name =
380+
DependencyName::Package("my:regex/regex@1.0.0".to_string().try_into().unwrap());
381+
let dep_src = ComponentDependency::Local {
382+
path: dep_file,
383+
export: None,
384+
};
385+
let deps = std::iter::once((&dep_name, &dep_src));
386+
387+
let wit = extract_wits(deps, ".").await?;
388+
389+
let resolve = parse_wit(&wit).expect("should have emitted valid WIT");
390+
391+
assert_eq!(2, resolve.packages.len()); // root:component and my:regex
392+
let (_rc_pkg_id, rc_pkg) = resolve
393+
.packages
394+
.iter()
395+
.find(|(_, p)| p.name.to_string() == "root:component")
396+
.expect("should have had `root:component`");
397+
let (_mr_pkg_id, _mr_pkg) = resolve
398+
.packages
399+
.iter()
400+
.find(|(_, p)| p.name.to_string() == "my:regex@1.0.0")
401+
.expect("should have had `my:regex`");
402+
403+
assert_eq!(1, resolve.interfaces.len());
404+
assert_eq!(
405+
"regex",
406+
resolve
407+
.interfaces
408+
.iter()
409+
.next()
410+
.unwrap()
411+
.1
412+
.name
413+
.as_ref()
414+
.unwrap()
415+
);
416+
let regex_itf_id = resolve.interfaces.iter().next().unwrap().0;
417+
418+
assert_eq!(2, rc_pkg.worlds.len()); // root and synthetic "impo*" wart
419+
let root_world_id = rc_pkg
420+
.worlds
421+
.iter()
422+
.find(|w| w.0 == "root")
423+
.expect("should have had `root` world")
424+
.1;
425+
426+
let world = resolve.worlds.get(*root_world_id).unwrap();
427+
assert_eq!(1, world.imports.len());
428+
let expected_import = wit_parser::WorldItem::Interface {
429+
id: regex_itf_id,
430+
stability: wit_parser::Stability::Unknown,
431+
};
432+
let import = world.imports.values().next().unwrap();
433+
assert_eq!(&expected_import, import);
434+
435+
Ok(())
436+
}
437+
}

0 commit comments

Comments
 (0)