1use anyhow::{Context, anyhow};
2use camino::{Utf8Path, Utf8PathBuf};
3use camino_tempfile::Utf8TempDir;
4use heck::{ToLowerCamelCase, ToSnakeCase};
5use include_dir::{Dir, include_dir};
6use lazy_static::lazy_static;
7use log::debug;
8use log::info;
9use serde::Deserialize;
10use std::collections::{BTreeMap, HashSet};
11use std::fmt::Display;
12use std::ops::Range;
13use std::sync::atomic::{AtomicBool, Ordering};
14use topologic::AcyclicDependencyGraph;
15use wit_component::{ComponentEncoder, StringEncoding};
16use wit_parser::{PackageId, PackageName, Resolve, WorldId};
17
18mod moonc_wasm;
19
20#[cfg(feature = "get-script")]
22pub mod get_script;
23
24#[cfg(feature = "typed-config")]
26pub mod typed_config;
27
28static MOONBIT_CORE: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/bundled-core");
29
30#[derive(Default)]
31struct MoonC {
32 initialized: AtomicBool,
33}
34
35impl MoonC {
36 pub fn run(&self, mut args: Vec<String>) -> anyhow::Result<()> {
37 self.ensure_initialized()?;
38 debug!("Running the MoonBit compiler with args: {}", args.join(" "));
39 args.insert(0, "moonc".to_string());
40 moonc_wasm::run_wasmoo(args).context("Running the MoonBit compiler")?;
41 Ok(())
42 }
43
44 fn ensure_initialized(&self) -> anyhow::Result<()> {
45 if !self.initialized.load(Ordering::Acquire) {
46 debug!("Initializing V8...");
47 moonc_wasm::initialize_v8()?;
48 self.initialized.store(true, Ordering::Release);
49 }
50 Ok(())
51 }
52}
53
54lazy_static! {
55 static ref MOONC: MoonC = MoonC::default();
56}
57
58pub struct MoonBitComponent {
59 dir: Utf8PathBuf,
60 temp: Option<Utf8TempDir>,
61 packages: BTreeMap<String, MoonBitPackage>,
62 resolve: Option<Resolve>,
63 world_id: Option<WorldId>,
64 root_package_id: Option<PackageId>,
65}
66
67impl MoonBitComponent {
68 pub fn empty_from_wit(
72 wit: impl AsRef<str>,
73 selected_world: Option<&str>,
74 ) -> anyhow::Result<Self> {
75 let temp_dir = Utf8TempDir::new().context("Creating temporary directory")?;
76 let dir = temp_dir.path().to_path_buf();
77
78 info!("Creating MoonBit component in temporary directory: {dir}");
79
80 let mut component = MoonBitComponent {
81 dir,
82 temp: Some(temp_dir),
83 packages: BTreeMap::new(),
84 resolve: None,
85 world_id: None,
86 root_package_id: None,
87 };
88
89 info!("Saving WIT package to {}/package.wit", component.wit_dir());
90 std::fs::create_dir_all(component.wit_dir()).context("Creating WIT package directory")?;
91 std::fs::write(
92 component.wit_dir().join("package.wit"),
93 wit.as_ref().as_bytes(),
94 )?;
95
96 info!("Resolving WIT package");
97 let mut resolve = Resolve::default();
98 let (root_package_id, _) = resolve
99 .push_dir(component.wit_dir())
100 .context("Resolving WIT packages")?;
101 let world_id = resolve
102 .select_world(root_package_id, selected_world)
103 .context("Selecting the WIT world")?;
104
105 info!("Generating MoonBit WIT bindings");
106 let mut wit_bindgen = wit_bindgen_moonbit::Opts {
107 gen_dir: "gen".to_string(),
108 derive_eq: true,
109 derive_show: true,
110 ..Default::default()
111 }
112 .build();
113 let mut bindgen_files = wit_bindgen_core::Files::default();
114 wit_bindgen
115 .generate(&resolve, world_id, &mut bindgen_files)
116 .context("Generating MoonBit WIT bindings")?;
117
118 for (name, contents) in bindgen_files.iter() {
119 let dst = if let Some(stripped_name) = name.strip_prefix('/') {
120 component.dir.join(stripped_name)
121 } else {
122 component.dir.join(name)
123 };
124 debug!("Writing binding file {dst}");
125
126 if let Some(parent) = dst.parent() {
127 std::fs::create_dir_all(parent)
128 .context("Creating directory for generated MoonBit WIT bindings")?;
129 }
130 std::fs::write(&dst, contents).context("Writing generated MoonBit WIT bindings")?;
131 }
132
133 component.extract_core()?;
134
135 component.resolve = Some(resolve);
136 component.world_id = Some(world_id);
137 component.root_package_id = Some(root_package_id);
138 Ok(component)
139 }
140
141 pub fn disable_cleanup(&mut self) {
143 if let Some(temp) = &mut self.temp {
144 temp.disable_cleanup(true);
145 }
146 }
147
148 pub fn existing(path: &Utf8Path, selected_world: Option<&str>) -> anyhow::Result<Self> {
152 let mut component = MoonBitComponent {
153 dir: path.to_path_buf(),
154 temp: None,
155 packages: BTreeMap::new(),
156 resolve: None,
157 world_id: None,
158 root_package_id: None,
159 };
160 component.extract_core()?;
161
162 info!("Resolving WIT package");
163 let mut resolve = Resolve::default();
164 let (root_package_id, _) = resolve
165 .push_dir(component.wit_dir())
166 .context("Resolving WIT package")?;
167 let world_id = resolve
168 .select_world(root_package_id, selected_world)
169 .context("Selecting WIT world")?;
170
171 component.resolve = Some(resolve);
172 component.world_id = Some(world_id);
173 component.root_package_id = Some(root_package_id);
174
175 Ok(component)
176 }
177
178 pub fn define_bindgen_packages(&mut self) -> anyhow::Result<()> {
180 let moonbit_root_package = self.moonbit_root_package()?;
181 let world_name = self.world_name()?;
182 let world_snake = to_moonbit_ident(&world_name);
183
184 let imported_interfaces = self.get_imported_interfaces()?;
185 let exported_interfaces = self.get_exported_interfaces()?;
186
187 debug!("Imported interfaces: {imported_interfaces:?}");
188 debug!("Exported interfaces: {exported_interfaces:?}");
189
190 let mut gen_dependencies = Vec::new();
191 let mut gen_mbt_files = Vec::new();
192
193 self.define_package(MoonBitPackage {
194 name: format!("{moonbit_root_package}/ffi"),
195 mbt_files: vec![Utf8Path::new("ffi").join("top.mbt")],
196 warning_control: vec![WarningControl::Disable(Warning::Specific(44))],
197 alert_control: vec![],
198 output: Utf8Path::new("target")
199 .join("wasm")
200 .join("release")
201 .join("build")
202 .join("ffi")
203 .join("ffi.core"),
204 dependencies: vec![],
205 package_sources: vec![(
206 format!("{moonbit_root_package}/ffi"),
207 Utf8Path::new("ffi").to_path_buf(),
208 )],
209 });
210 let ffi_dep = (
211 Utf8Path::new("target")
212 .join("wasm")
213 .join("release")
214 .join("build")
215 .join("ffi")
216 .join("ffi.mi"),
217 "ffi".to_string(),
218 );
219 gen_dependencies.push(ffi_dep.clone());
220
221 self.define_package(MoonBitPackage {
222 name: format!("{moonbit_root_package}/gen/world/{world_name}"),
223 mbt_files: vec![
224 Utf8Path::new("gen")
225 .join("world")
226 .join(&world_name)
227 .join("stub.mbt"),
228 ],
229 warning_control: vec![],
230 alert_control: vec![],
231 output: Utf8Path::new("target")
232 .join("wasm")
233 .join("release")
234 .join("build")
235 .join("gen")
236 .join("world")
237 .join(&world_name)
238 .join(format!("{world_name}.core")),
239
240 dependencies: vec![],
241 package_sources: vec![(
242 format!("{moonbit_root_package}/gen/world/{world_name}"),
243 Utf8Path::new("gen").join("world").join(&world_name),
244 )],
245 });
246 gen_dependencies.push((
247 Utf8Path::new("target")
248 .join("wasm")
249 .join("release")
250 .join("build")
251 .join("gen")
252 .join("world")
253 .join(&world_name)
254 .join(format!("{world_name}.mi")),
255 world_name.clone(),
256 ));
257 gen_mbt_files.push(Utf8Path::new("gen").join(format!("world_{world_snake}_export.mbt")));
258
259 for (package_name, interface_name) in &imported_interfaces {
260 let pkg_namespace = to_moonbit_ident(&package_name.namespace);
261 let pkg_name = to_moonbit_ident(&package_name.name);
262 let interface_name = interface_name.to_lower_camel_case();
263
264 let name = format!(
265 "{moonbit_root_package}/interface/{pkg_namespace}/{pkg_name}/{interface_name}"
266 );
267 let src = Utf8Path::new("interface")
268 .join(&pkg_namespace)
269 .join(&pkg_name)
270 .join(&interface_name);
271 let output = Utf8Path::new("target")
272 .join("wasm")
273 .join("release")
274 .join("build")
275 .join("interface")
276 .join(&pkg_namespace)
277 .join(&pkg_name)
278 .join(&interface_name);
279 self.define_package(MoonBitPackage {
280 name: name.clone(),
281 mbt_files: vec![src.join("top.mbt"), src.join("ffi.mbt")],
282 warning_control: vec![],
283 alert_control: vec![],
284 output: output.join(format!("{interface_name}.core")),
285 dependencies: vec![ffi_dep.clone()],
286 package_sources: vec![(name, src)],
287 });
288 }
289
290 for (package_name, interface_name) in &exported_interfaces {
291 let pkg_namespace = to_moonbit_ident(&package_name.namespace);
292 let unescaped_pkg_namespace = package_name.namespace.to_snake_case();
293 let pkg_name = to_moonbit_ident(&package_name.name);
294 let unescaped_pkg_name = package_name.name.to_snake_case();
295 let interface_name = interface_name.to_lower_camel_case();
296 let snake_interface_name = interface_name.to_snake_case();
297
298 let name = format!(
299 "{moonbit_root_package}/gen/interface/{pkg_namespace}/{pkg_name}/{interface_name}"
300 );
301 let src = Utf8Path::new("gen")
302 .join("interface")
303 .join(&pkg_namespace)
304 .join(&pkg_name)
305 .join(&interface_name);
306 let output = Utf8Path::new("target")
307 .join("wasm")
308 .join("release")
309 .join("build")
310 .join("gen")
311 .join("interface")
312 .join(&pkg_namespace)
313 .join(&pkg_name)
314 .join(&interface_name);
315 self.define_package(MoonBitPackage {
316 name: name.clone(),
317 mbt_files: vec![src.join("top.mbt"), src.join("stub.mbt")],
318 warning_control: vec![],
319 alert_control: vec![],
320 output: output.join(format!("{interface_name}.core")),
321
322 dependencies: vec![],
323 package_sources: vec![(name, src)],
324 });
325 gen_dependencies.push((
326 output.join(format!("{interface_name}.mi")),
327 interface_name.clone(),
328 ));
329 gen_mbt_files.push(Utf8Path::new("gen").join(format!(
330 "gen_interface_{unescaped_pkg_namespace}_{unescaped_pkg_name}_{snake_interface_name}_export.mbt"
331 )));
332 }
333
334 gen_mbt_files.push(Utf8Path::new("gen").join("ffi.mbt"));
335 self.define_package(MoonBitPackage {
336 name: format!("{moonbit_root_package}/gen"),
337 mbt_files: gen_mbt_files,
338 warning_control: vec![],
339 alert_control: vec![],
340 output: Utf8Path::new("target")
341 .join("wasm")
342 .join("release")
343 .join("build")
344 .join("gen")
345 .join("gen.core"),
346 dependencies: gen_dependencies,
347 package_sources: vec![(
348 format!("{moonbit_root_package}/gen"),
349 Utf8Path::new("gen").to_path_buf(),
350 )],
351 });
352
353 Ok(())
354 }
355
356 pub fn set_warning_control(
357 &mut self,
358 package_name: &str,
359 warning_control: Vec<WarningControl>,
360 ) -> anyhow::Result<()> {
361 let package = self
362 .packages
363 .get_mut(package_name)
364 .ok_or_else(|| anyhow::anyhow!("Package '{package_name}' not found"))?;
365 package.warning_control = warning_control;
366 Ok(())
367 }
368
369 pub fn set_alert_control(
370 &mut self,
371 package_name: &str,
372 alert_control: Vec<WarningControl>,
373 ) -> anyhow::Result<()> {
374 let package = self
375 .packages
376 .get_mut(package_name)
377 .ok_or_else(|| anyhow::anyhow!("Package '{package_name}' not found"))?;
378 package.alert_control = alert_control;
379 Ok(())
380 }
381
382 pub fn define_package(&mut self, package: MoonBitPackage) {
384 debug!("Adding MoonBit package: {}", package.name);
385 self.packages.insert(package.name.clone(), package);
386 }
387
388 pub fn add_dependency(
390 &mut self,
391 package_name: &str,
392 mi_path: &Utf8Path,
393 alias: &str,
394 ) -> anyhow::Result<()> {
395 debug!("Adding dependency: {package_name} ({mi_path}) as {alias}");
396 let package = self
397 .packages
398 .get_mut(package_name)
399 .ok_or_else(|| anyhow::anyhow!("Package '{package_name}' not found"))?;
400 package
401 .dependencies
402 .push((Utf8PathBuf::from(mi_path), alias.to_string()));
403 Ok(())
404 }
405
406 pub fn write_file(&self, relative_path: &Utf8Path, contents: &str) -> anyhow::Result<()> {
407 let path = self.dir.join(relative_path);
408 info!("Writing file: {path:?}");
409 if let Some(parent) = path.parent() {
410 std::fs::create_dir_all(parent).context("Creating directory for generated file")?;
411 }
412 std::fs::write(path, contents)?;
413 Ok(())
414 }
415
416 pub fn write_world_stub(&self, moonbit_source: &str) -> anyhow::Result<()> {
418 let world_name = self.world_name()?;
419 let path = self
420 .dir
421 .join("gen")
422 .join("world")
423 .join(world_name)
424 .join("stub.mbt");
425 info!("Writing world stub to {path}");
426 std::fs::write(path, moonbit_source)?;
427 Ok(())
428 }
429
430 pub fn write_interface_stub(
432 &self,
433 package_name: &PackageName,
434 interface_name: &str,
435 moonbit_source: &str,
436 ) -> anyhow::Result<()> {
437 let package_name_snake = to_moonbit_ident(&package_name.name);
438 let package_namespace_snake = to_moonbit_ident(&package_name.namespace);
439
440 let path = self
441 .dir
442 .join("gen")
443 .join("interface")
444 .join(package_namespace_snake)
445 .join(package_name_snake)
446 .join(interface_name.to_lower_camel_case())
447 .join("stub.mbt");
448 info!("Writing interface stub to {path}");
449 std::fs::create_dir_all(path.parent().unwrap())
450 .context("Creating directory for interface stub")?;
451 std::fs::write(path, moonbit_source).context("Writing interface stub")?;
452 Ok(())
453 }
454
455 pub fn write_interface_package_json(
457 &self,
458 package_name: &PackageName,
459 interface_name: &str,
460 json: serde_json::Value,
461 ) -> anyhow::Result<()> {
462 let package_name_snake = to_moonbit_ident(&package_name.name);
463 let package_namespace_snake = to_moonbit_ident(&package_name.namespace);
464 let path = self
465 .dir
466 .join("gen")
467 .join("interface")
468 .join(package_namespace_snake)
469 .join(package_name_snake)
470 .join(interface_name.to_lower_camel_case())
471 .join("moon.pkg.json");
472 info!("Writing interface definition to {path}");
473 std::fs::create_dir_all(path.parent().unwrap())
474 .context("Creating directory for interface definition")?;
475 std::fs::write(
476 path,
477 serde_json::to_string_pretty(&json).context("Writing interface definition")?,
478 )?;
479 Ok(())
480 }
481
482 pub fn build(&self, main_package_name: Option<&str>, target: &Utf8Path) -> anyhow::Result<()> {
487 let main_package_name = match main_package_name {
488 Some(name) => name.to_string(),
489 None => {
490 let root_package = self.moonbit_root_package()?;
491 format!("{root_package}/gen")
492 }
493 };
494
495 let sorted_packages = self.sorted_packages()?;
496 debug!(
497 "Package build order: {}",
498 sorted_packages
499 .iter()
500 .map(|p| p.name.clone())
501 .collect::<Vec<_>>()
502 .join(", ")
503 );
504
505 for package in &sorted_packages {
506 self.build_package(
507 &package.mbt_files,
508 &package.warning_control,
509 &package.alert_control,
510 &package.output,
511 &package.name,
512 &package.dependencies,
513 &package.package_sources,
514 )
515 .context(format!("Building package {}", package.name))?;
516 }
517
518 let mut core_files = vec![
519 self.core_bundle_dir().join("abort").join("abort.core"),
520 self.core_bundle_dir().join("core.core"),
521 ];
522 let mut package_sources = BTreeMap::new();
523
524 for package in &sorted_packages {
525 core_files.push(package.output.clone());
526 for (name, source) in &package.package_sources {
527 package_sources.insert(name.clone(), source.clone());
528 }
529 }
530 package_sources.insert("moonbitlang/core".to_string(), self.core_dir());
531 let package_sources: Vec<(String, Utf8PathBuf)> = package_sources.into_iter().collect();
532
533 let main_package = self
534 .packages
535 .get(&main_package_name)
536 .ok_or_else(|| anyhow!(format!("Main package '{main_package_name}' not found")))?;
537 let (_, main_package_source) = main_package
538 .package_sources
539 .iter()
540 .find(|(name, _)| name == &main_package_name)
541 .ok_or_else(|| {
542 anyhow!(format!(
543 "Main package sources '{main_package_name}' not found"
544 ))
545 })?;
546
547 let main_package_json = self.dir.join(main_package_source).join("moon.pkg.json");
548 let linker_config = Self::extract_wasm_linker_config(&main_package_json)
549 .context("Extracting linker config")?;
550
551 self.link_core(
552 &core_files,
553 &main_package_name,
554 &main_package_json,
555 &package_sources,
556 &linker_config.export_memory_name,
557 &linker_config.exports,
558 linker_config.heap_start_address,
559 )
560 .context("Linking")?;
561
562 self.embed_wit().context("Embedding WIT")?;
563 self.create_component(target)
564 .context("Creating component")?;
565
566 Ok(())
567 }
568
569 fn extract_core(&self) -> anyhow::Result<()> {
570 let core_dir = self.core_dir();
571 info!("Extracting MoonBit core to {core_dir}");
572 std::fs::create_dir_all(&core_dir)?;
573 MOONBIT_CORE.extract(&core_dir)?;
574 Ok(())
575 }
576
577 #[allow(clippy::too_many_arguments)]
578 fn build_package(
579 &self,
580 mbt_files: &[Utf8PathBuf],
581 warning_control: &[WarningControl],
582 alert_control: &[WarningControl],
583 output: &Utf8Path,
584 package: &str,
585 dependencies: &[(Utf8PathBuf, String)],
586 package_sources: &[(String, Utf8PathBuf)],
587 ) -> anyhow::Result<()> {
588 info!("Building MoonBit package: {package}");
589
590 for mbt_file in mbt_files {
591 let abs = self.dir.join(mbt_file);
592 if !abs.exists() {
593 return Err(anyhow!("MBT file does not exist at {abs}"));
594 }
595 }
596 for (path, name) in dependencies {
597 let abs = self.dir.join(path);
598 if !abs.exists() {
599 return Err(anyhow!("Dependency {name} does not exist at {abs}"));
600 }
601 }
602 for (source_name, source_path) in package_sources {
603 let abs = self.dir.join(source_path);
604 if !abs.exists() {
605 return Err(anyhow!(
606 "Package source {source_name} does not exist at {abs}"
607 ));
608 }
609 }
610
611 let mut args = vec!["build-package".to_string()];
612 for file in mbt_files {
613 let full_path = self.dir.join(file);
614 args.push(full_path.to_string());
615 }
616 for w in warning_control {
617 args.push("-w".to_string());
618 args.push(w.to_string());
619 }
620 for a in alert_control {
621 args.push("-alert".to_string());
622 args.push(a.to_string());
623 }
624 args.push("-o".to_string());
625 args.push(self.dir.join(output).to_string());
626 args.push("-pkg".to_string());
627 args.push(package.to_string());
628 args.push("-std-path".to_string());
629 args.push(self.core_bundle_dir().to_string());
630 for (dep_path, dep_name) in dependencies {
631 args.push("-i".to_string());
632 let full_path = self.dir.join(dep_path);
633 args.push(format!("{full_path}:{dep_name}"));
634 }
635 self.add_package_sources(&mut args, package_sources);
636 args.push("-target".to_string());
637 args.push("wasm".to_string());
638
639 MOONC.run(args)?;
640
641 let abs = self.dir.join(output);
642 if !abs.exists() {
643 return Err(anyhow!("Output does not exist at {abs}"));
644 }
645
646 Ok(())
647 }
648
649 #[allow(clippy::too_many_arguments)]
650 fn link_core(
651 &self,
652 core_files: &[Utf8PathBuf],
653 main_package_name: &str,
654 main_package_json: &Utf8Path,
655 package_sources: &[(String, Utf8PathBuf)],
656 exported_memory_name: &str,
657 exported_functions: &[String],
658 heap_start_address: usize,
659 ) -> anyhow::Result<()> {
660 info!("Linking MoonBit component");
661 let mut args = vec!["link-core".to_string()];
662
663 for file in core_files {
664 let full_path = self.dir.join(file);
665 args.push(full_path.to_string());
666 }
667 args.push("-main".to_string());
668 args.push(main_package_name.to_string());
669 args.push("-o".to_string());
670 args.push(self.module_wasm().to_string());
671 args.push("-pkg-config-path".to_string());
672 args.push(self.dir.join(main_package_json).to_string());
673 self.add_package_sources(&mut args, package_sources);
674 args.push("-pkg-sources".to_string());
675 args.push(format!("moonbitlang/core:{}", self.core_dir()));
676 args.push("-target".to_string());
677 args.push("wasm".to_string());
678 args.push(format!(
679 "-exported_functions={}",
680 exported_functions.join(",")
681 ));
682 args.push("-export-memory-name".to_string());
683 args.push(exported_memory_name.to_string());
684 args.push("-heap-start-address".to_string());
685 args.push(heap_start_address.to_string());
686
687 MOONC.run(args)?;
688 Ok(())
689 }
690
691 fn add_package_sources(
692 &self,
693 args: &mut Vec<String>,
694 package_sources: &[(String, Utf8PathBuf)],
695 ) {
696 for (source_name, source_path) in package_sources {
697 args.push("-pkg-sources".to_string());
698 let full_path = self.dir.join(source_path);
699 args.push(format!("{source_name}:{full_path}"));
700 }
701 }
702
703 fn embed_wit(&self) -> anyhow::Result<()> {
704 info!("Embedding WIT in the compiled MoonBit WASM module");
705
706 let resolve = self.resolve.as_ref().unwrap();
708 let world = &self.world_id.unwrap();
709
710 let module_wasm = self.module_wasm();
711 let mut wasm = std::fs::read(&module_wasm)
712 .context(format!("Failed to read module WASM from {module_wasm}"))?;
713
714 wit_component::embed_component_metadata(&mut wasm, resolve, *world, StringEncoding::UTF16)
715 .context("Embedding component metadata")?;
716
717 std::fs::write(self.module_with_embed_wasm(), wasm)
718 .context("Writing WASM with embedded metadata")?;
719
720 Ok(())
721 }
722
723 fn create_component(&self, target: &Utf8Path) -> anyhow::Result<()> {
724 info!("Creating the final WASM component at {target}");
725
726 let wasm = std::fs::read(self.module_with_embed_wasm())
727 .context("Reading WASM with embedded metadata")?;
728 let mut encoder = ComponentEncoder::default()
729 .validate(true)
730 .reject_legacy_names(false)
731 .merge_imports_based_on_semver(true)
732 .realloc_via_memory_grow(false)
733 .module(&wasm)?;
734
735 let component = encoder.encode().context("Encoding WASM component")?;
736
737 if let Some(parent) = target.parent() {
738 std::fs::create_dir_all(parent).context("Creating directory for WASM component")?;
739 }
740 std::fs::write(target, component).context("Writing WASM component")?;
741
742 Ok(())
743 }
744
745 fn sorted_packages(&self) -> anyhow::Result<Vec<&MoonBitPackage>> {
746 let mut graph = AcyclicDependencyGraph::new();
747 let root_package = self.moonbit_root_package()?;
748
749 for package in self.packages.values() {
750 for (path, dep) in &package.dependencies {
751 let path_components = path.components().map(|c| c.to_string()).collect::<Vec<_>>();
752 let full_dep = if path_components.starts_with(&[
753 "target".to_string(),
754 "wasm".to_string(),
755 "release".to_string(),
756 "build".to_string(),
757 ]) {
758 let relevant_path = &path_components[4..path_components.len() - 1];
759 format!("{}/{}", root_package, relevant_path.join("/"))
760 } else {
761 format!("{root_package}/{dep}")
762 };
763
764 graph.depend_on(package.name.clone(), full_dep)?;
765 }
766 }
767
768 let mut sorted = Vec::new();
769 let mut names = HashSet::new();
770 for layer in graph.get_forward_dependency_topological_layers() {
771 for package_name in layer {
772 sorted.push(&self.packages[&package_name]);
773 names.insert(package_name);
774 }
775 }
776
777 for package in self.packages.values() {
778 if !names.contains(&package.name) {
779 sorted.push(package);
780 }
781 }
782
783 Ok(sorted)
784 }
785
786 fn wit_dir(&self) -> Utf8PathBuf {
787 self.dir.join("wit")
788 }
789
790 fn core_dir(&self) -> Utf8PathBuf {
791 self.dir.join("core")
792 }
793
794 fn core_bundle_dir(&self) -> Utf8PathBuf {
795 self.dir
796 .join("core")
797 .join("target")
798 .join("wasm")
799 .join("release")
800 .join("bundle")
801 }
802
803 fn module_wasm(&self) -> Utf8PathBuf {
804 self.dir.join("target").join("module.wasm")
805 }
806
807 fn module_with_embed_wasm(&self) -> Utf8PathBuf {
808 self.dir.join("target").join("module.embed.wasm")
809 }
810
811 fn extract_wasm_linker_config(
812 package_json_path: &Utf8Path,
813 ) -> anyhow::Result<WasmLinkerConfig> {
814 debug!("Extracting Wasm linker config from {package_json_path}");
815 let json_str = std::fs::read_to_string(package_json_path)?;
816 let pkg: PackageJsonWithWasmLinkerConfig = serde_json::from_str(&json_str)?;
817
818 Ok(pkg.link.wasm)
819 }
820
821 pub fn moonbit_root_package(&self) -> anyhow::Result<String> {
822 Ok(format!(
823 "{}/{}",
824 self.root_pkg_namespace()?,
825 self.root_pkg_name()?
826 ))
827 }
828
829 pub fn root_pkg_namespace(&self) -> anyhow::Result<String> {
830 let root_package_id = self.root_package_id.as_ref().unwrap();
831 let resolve = self.resolve.as_ref().unwrap();
832
833 let root_package = resolve
834 .packages
835 .get(*root_package_id)
836 .ok_or_else(|| anyhow!("Root package not found"))?;
837 Ok(root_package.name.namespace.to_string())
838 }
839
840 pub fn root_pkg_name(&self) -> anyhow::Result<String> {
841 let root_package_id = self.root_package_id.as_ref().unwrap();
842 let resolve = self.resolve.as_ref().unwrap();
843
844 let root_package = resolve
845 .packages
846 .get(*root_package_id)
847 .ok_or_else(|| anyhow!("Root package not found"))?;
848 Ok(root_package.name.name.to_string())
849 }
850
851 fn world_name(&self) -> anyhow::Result<String> {
852 Ok(self
853 .resolve
854 .as_ref()
855 .and_then(|r| r.worlds.get(self.world_id?))
856 .map(|w| w.name.to_string())
857 .ok_or_else(|| anyhow::anyhow!("Could not find world"))?
858 .to_lower_camel_case())
859 }
860
861 fn get_imported_interfaces(&self) -> anyhow::Result<Vec<(PackageName, String)>> {
862 let world = self
863 .resolve
864 .as_ref()
865 .and_then(|r| r.worlds.get(self.world_id?))
866 .ok_or_else(|| anyhow::anyhow!("Could not find world"))?;
867 let mut imported_interfaces = Vec::new();
868 for (_, item) in &world.imports {
869 if let wit_parser::WorldItem::Interface { id, .. } = item
870 && let Some(interface) = self.resolve.as_ref().and_then(|r| r.interfaces.get(*id))
871 {
872 if let Some(interface_name) = interface.name.as_ref() {
873 let owner_package = interface.package.ok_or_else(|| {
874 anyhow::anyhow!("Interface '{}' does not have a package", interface_name)
875 })?;
876 let package = self
877 .resolve
878 .as_ref()
879 .and_then(|r| r.packages.get(owner_package))
880 .ok_or_else(|| {
881 anyhow::anyhow!("Package for interface '{}' not found", interface_name)
882 })?;
883
884 imported_interfaces.push((package.name.clone(), interface_name.to_string()));
885 } else {
886 return Err(anyhow::anyhow!(
887 "Anonymous imported interfaces are not supported"
888 ));
889 }
890 }
891 }
892 Ok(imported_interfaces)
893 }
894
895 fn get_exported_interfaces(&self) -> anyhow::Result<Vec<(PackageName, String)>> {
896 let world = self
897 .resolve
898 .as_ref()
899 .and_then(|r| r.worlds.get(self.world_id?))
900 .ok_or_else(|| anyhow::anyhow!("Could not find world"))?;
901 let mut exported_interfaces = Vec::new();
902 for (_, item) in &world.exports {
903 if let wit_parser::WorldItem::Interface { id, .. } = item
904 && let Some(interface) = self.resolve.as_ref().and_then(|r| r.interfaces.get(*id))
905 {
906 if let Some(interface_name) = interface.name.as_ref() {
907 let owner_package = interface.package.ok_or_else(|| {
908 anyhow::anyhow!("Interface '{}' does not have a package", interface_name)
909 })?;
910 let package = self
911 .resolve
912 .as_ref()
913 .and_then(|r| r.packages.get(owner_package))
914 .ok_or_else(|| {
915 anyhow::anyhow!("Package for interface '{}' not found", interface_name)
916 })?;
917
918 exported_interfaces.push((package.name.clone(), interface_name.to_string()));
919 } else {
920 return Err(anyhow::anyhow!(
921 "Anonymous exported interfaces are not supported"
922 ));
923 }
924 }
925 }
926 Ok(exported_interfaces)
927 }
928}
929
930#[derive(Debug, Deserialize)]
931struct PackageJsonWithWasmLinkerConfig {
932 link: LinkConfig,
933}
934
935#[derive(Debug, Deserialize)]
936struct LinkConfig {
937 wasm: WasmLinkerConfig,
938}
939
940#[derive(Debug, Deserialize)]
941#[serde(rename_all = "kebab-case")]
942struct WasmLinkerConfig {
943 export_memory_name: String,
944 exports: Vec<String>,
945 heap_start_address: usize,
946}
947
948pub struct MoonBitPackage {
949 pub name: String,
950 pub mbt_files: Vec<Utf8PathBuf>,
951 pub warning_control: Vec<WarningControl>,
952 pub alert_control: Vec<WarningControl>,
953 pub output: Utf8PathBuf,
954 pub dependencies: Vec<(Utf8PathBuf, String)>,
955 pub package_sources: Vec<(String, Utf8PathBuf)>,
956}
957
958pub enum Warning {
959 Specific(u16),
960 Range(Range<u16>),
961}
962
963impl Display for Warning {
964 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
965 match self {
966 Warning::Specific(code) => write!(f, "{code}"),
967 Warning::Range(range) => write!(f, "{}..{}", range.start, range.end),
968 }
969 }
970}
971
972pub enum WarningControl {
973 Enable(Warning),
974 Disable(Warning),
975 EnableAsError(Warning),
976}
977
978impl Display for WarningControl {
979 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
980 match self {
981 WarningControl::Enable(code) => write!(f, "+{code}"),
982 WarningControl::Disable(code) => write!(f, "-{code}"),
983 WarningControl::EnableAsError(code) => write!(f, "@{code}"),
984 }
985 }
986}
987
988pub fn to_moonbit_ident(name: impl AsRef<str>) -> String {
989 let name = name.as_ref();
991 match name {
992 "as" | "else" | "extern" | "fn" | "fnalias" | "if" | "let" | "const" | "match" | "using"
994 | "mut" | "type" | "typealias" | "struct" | "enum" | "trait" | "traitalias" | "derive"
995 | "while" | "break" | "continue" | "import" | "return" | "throw" | "raise" | "try" | "catch"
996 | "pub" | "priv" | "readonly" | "true" | "false" | "_" | "test" | "loop" | "for" | "in" | "impl"
997 | "with" | "guard" | "async" | "is" | "suberror" | "and" | "letrec" | "enumview" | "noraise"
998 | "defer" | "init" | "main"
999 | "module" | "move" | "ref" | "static" | "super" | "unsafe" | "use" | "where" | "await"
1001 | "dyn" | "abstract" | "do" | "final" | "macro" | "override" | "typeof" | "virtual" | "yield"
1002 | "local" | "method" | "alias" | "assert" | "recur" | "isnot" | "define" | "downcast"
1003 | "inherit" | "member" | "namespace" | "upcast" | "void" | "lazy" | "include" | "mixin"
1004 | "protected" | "sealed" | "constructor" | "atomic" | "volatile" | "anyframe" | "anytype"
1005 | "asm" | "comptime" | "errdefer" | "export" | "opaque" | "orelse" | "resume" | "threadlocal"
1006 | "unreachable" | "dynclass" | "dynobj" | "dynrec" | "var" | "finally" | "noasync" => {
1007 format ! ("{name}_")
1008 }
1009 _ => name.to_snake_case(),
1010 }
1011}
1012
1013#[cfg(test)]
1014test_r::enable!();
1015
1016#[cfg(test)]
1017struct Trace;
1018
1019#[cfg(test)]
1020#[test_r::test_dep]
1021fn initialize_trace() -> Trace {
1022 pretty_env_logger::formatted_builder()
1023 .filter_level(log::LevelFilter::Debug)
1024 .write_style(pretty_env_logger::env_logger::WriteStyle::Always)
1025 .init();
1026 Trace
1027}