moonbit_component_generator/
lib.rs

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/// An example generator that embeds and exports a script (arbitrary string) into a MoonBit component.
21#[cfg(feature = "get-script")]
22pub mod get_script;
23
24/// An example generator that implements a simple typed configuration interface defined in WIT
25#[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    /// Initializes a new MoonBit component that implements the given WIT interface.
69    ///
70    /// This step will create a temporary directory and generate MoonBit WIT bindings in it.
71    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    /// Disables cleaning up the temporary directory, for debugging purposes.
142    pub fn disable_cleanup(&mut self) {
143        if let Some(temp) = &mut self.temp {
144            temp.disable_cleanup(true);
145        }
146    }
147
148    /// Initializes a new MoonBit component from an existing directory with a valid 'wit' directory in it.
149    ///
150    /// The existing directory is expected to have the WIT bindings already generated.
151    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    /// Defines the MoonBit packages implementing the WIT bindings
179    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 = world_name.to_snake_case();
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            output: Utf8Path::new("target")
198                .join("wasm")
199                .join("release")
200                .join("build")
201                .join("ffi")
202                .join("ffi.core"),
203            dependencies: vec![],
204            package_sources: vec![(
205                format!("{moonbit_root_package}/ffi"),
206                Utf8Path::new("ffi").to_path_buf(),
207            )],
208        });
209        let ffi_dep = (
210            Utf8Path::new("target")
211                .join("wasm")
212                .join("release")
213                .join("build")
214                .join("ffi")
215                .join("ffi.mi"),
216            "ffi".to_string(),
217        );
218        gen_dependencies.push(ffi_dep.clone());
219
220        self.define_package(MoonBitPackage {
221            name: format!("{moonbit_root_package}/gen/world/{world_name}"),
222            mbt_files: vec![
223                Utf8Path::new("gen")
224                    .join("world")
225                    .join(&world_name)
226                    .join("stub.mbt"),
227            ],
228            warning_control: vec![],
229            output: Utf8Path::new("target")
230                .join("wasm")
231                .join("release")
232                .join("build")
233                .join("gen")
234                .join("world")
235                .join(&world_name)
236                .join(format!("{world_name}.core")),
237
238            dependencies: vec![],
239            package_sources: vec![(
240                format!("{moonbit_root_package}/gen/world/{world_name}"),
241                Utf8Path::new("gen").join("world").join(&world_name),
242            )],
243        });
244        gen_dependencies.push((
245            Utf8Path::new("target")
246                .join("wasm")
247                .join("release")
248                .join("build")
249                .join("gen")
250                .join("world")
251                .join(&world_name)
252                .join(format!("{world_name}.mi")),
253            world_name.clone(),
254        ));
255        gen_mbt_files.push(Utf8Path::new("gen").join(format!("world_{world_snake}_export.mbt")));
256
257        for (package_name, interface_name) in &imported_interfaces {
258            let pkg_namespace = package_name.namespace.to_snake_case();
259            let pkg_name = package_name.name.to_snake_case();
260            let interface_name = interface_name.to_lower_camel_case();
261
262            let name = format!(
263                "{moonbit_root_package}/interface/{pkg_namespace}/{pkg_name}/{interface_name}"
264            );
265            let src = Utf8Path::new("interface")
266                .join(&pkg_namespace)
267                .join(&pkg_name)
268                .join(&interface_name);
269            let output = Utf8Path::new("target")
270                .join("wasm")
271                .join("release")
272                .join("build")
273                .join("interface")
274                .join(&pkg_namespace)
275                .join(&pkg_name)
276                .join(&interface_name);
277            self.define_package(MoonBitPackage {
278                name: name.clone(),
279                mbt_files: vec![src.join("top.mbt"), src.join("ffi.mbt")],
280                warning_control: vec![],
281                output: output.join(format!("{interface_name}.core")),
282                dependencies: vec![ffi_dep.clone()],
283                package_sources: vec![(name, src)],
284            });
285        }
286
287        for (package_name, interface_name) in &exported_interfaces {
288            let pkg_namespace = package_name.namespace.to_snake_case();
289            let pkg_name = package_name.name.to_snake_case();
290            let interface_name = interface_name.to_lower_camel_case();
291
292            let name = format!(
293                "{moonbit_root_package}/gen/interface/{pkg_namespace}/{pkg_name}/{interface_name}"
294            );
295            let src = Utf8Path::new("gen")
296                .join("interface")
297                .join(&pkg_namespace)
298                .join(&pkg_name)
299                .join(&interface_name);
300            let output = Utf8Path::new("target")
301                .join("wasm")
302                .join("release")
303                .join("build")
304                .join("gen")
305                .join("interface")
306                .join(&pkg_namespace)
307                .join(&pkg_name)
308                .join(&interface_name);
309            self.define_package(MoonBitPackage {
310                name: name.clone(),
311                mbt_files: vec![src.join("top.mbt"), src.join("stub.mbt")],
312                warning_control: vec![],
313                output: output.join(format!("{interface_name}.core")),
314
315                dependencies: vec![],
316                package_sources: vec![(name, src)],
317            });
318            gen_dependencies.push((
319                output.join(format!("{interface_name}.mi")),
320                interface_name.clone(),
321            ));
322            gen_mbt_files.push(Utf8Path::new("gen").join(format!(
323                "gen_interface_{pkg_namespace}_{pkg_name}_{interface_name}_export.mbt"
324            )));
325        }
326
327        gen_mbt_files.push(Utf8Path::new("gen").join("ffi.mbt"));
328        self.define_package(MoonBitPackage {
329            name: format!("{moonbit_root_package}/gen"),
330            mbt_files: gen_mbt_files,
331            warning_control: vec![],
332            output: Utf8Path::new("target")
333                .join("wasm")
334                .join("release")
335                .join("build")
336                .join("gen")
337                .join("gen.core"),
338            dependencies: gen_dependencies,
339            package_sources: vec![(
340                format!("{moonbit_root_package}/gen"),
341                Utf8Path::new("gen").to_path_buf(),
342            )],
343        });
344
345        Ok(())
346    }
347
348    pub fn set_warning_control(
349        &mut self,
350        package_name: &str,
351        warning_control: Vec<WarningControl>,
352    ) -> anyhow::Result<()> {
353        let package = self
354            .packages
355            .get_mut(package_name)
356            .ok_or_else(|| anyhow::anyhow!("Package '{package_name}' not found"))?;
357        package.warning_control = warning_control;
358        Ok(())
359    }
360
361    /// Defines a custom MoonBit package
362    pub fn define_package(&mut self, package: MoonBitPackage) {
363        debug!("Adding MoonBit package: {}", package.name);
364        self.packages.insert(package.name.clone(), package);
365    }
366
367    /// Defines an additional dependency for a MoonBit package previously added with `define_package`.
368    pub fn add_dependency(
369        &mut self,
370        package_name: &str,
371        mi_path: &Utf8Path,
372        alias: &str,
373    ) -> anyhow::Result<()> {
374        debug!("Adding dependency: {package_name} ({mi_path}) as {alias}");
375        let package = self
376            .packages
377            .get_mut(package_name)
378            .ok_or_else(|| anyhow::anyhow!("Package '{package_name}' not found"))?;
379        package
380            .dependencies
381            .push((Utf8PathBuf::from(mi_path), alias.to_string()));
382        Ok(())
383    }
384
385    pub fn write_file(&self, relative_path: &Utf8Path, contents: &str) -> anyhow::Result<()> {
386        let path = self.dir.join(relative_path);
387        info!("Writing file: {path:?}");
388        if let Some(parent) = path.parent() {
389            std::fs::create_dir_all(parent).context("Creating directory for generated file")?;
390        }
391        std::fs::write(path, contents)?;
392        Ok(())
393    }
394
395    /// Writes the top level export stub for the selected world
396    pub fn write_world_stub(&self, moonbit_source: &str) -> anyhow::Result<()> {
397        let world_name = self.world_name()?;
398        let path = self
399            .dir
400            .join("gen")
401            .join("world")
402            .join(world_name)
403            .join("stub.mbt");
404        info!("Writing world stub to {path}");
405        std::fs::write(path, moonbit_source)?;
406        Ok(())
407    }
408
409    /// Writes the interface stub for a given package and interface name.
410    pub fn write_interface_stub(
411        &self,
412        package_name: &PackageName,
413        interface_name: &str,
414        moonbit_source: &str,
415    ) -> anyhow::Result<()> {
416        let package_name_snake = package_name.name.to_snake_case();
417        let package_namespace_snake = package_name.namespace.to_snake_case();
418        let path = self
419            .dir
420            .join("gen")
421            .join("interface")
422            .join(package_namespace_snake)
423            .join(package_name_snake)
424            .join(interface_name.to_lower_camel_case())
425            .join("stub.mbt");
426        info!("Writing interface stub to {path}");
427        std::fs::create_dir_all(path.parent().unwrap())
428            .context("Creating directory for interface stub")?;
429        std::fs::write(path, moonbit_source).context("Writing interface stub")?;
430        Ok(())
431    }
432
433    /// Writes the MoonBit package JSON file for a given exported package and interface name.
434    pub fn write_interface_package_json(
435        &self,
436        package_name: &PackageName,
437        interface_name: &str,
438        json: serde_json::Value,
439    ) -> anyhow::Result<()> {
440        let package_name_snake = package_name.name.to_snake_case();
441        let package_namespace_snake = package_name.namespace.to_snake_case();
442        let path = self
443            .dir
444            .join("gen")
445            .join("interface")
446            .join(package_namespace_snake)
447            .join(package_name_snake)
448            .join(interface_name.to_lower_camel_case())
449            .join("moon.pkg.json");
450        info!("Writing interface definition to {path}");
451        std::fs::create_dir_all(path.parent().unwrap())
452            .context("Creating directory for interface definition")?;
453        std::fs::write(
454            path,
455            serde_json::to_string_pretty(&json).context("Writing interface definition")?,
456        )?;
457        Ok(())
458    }
459
460    /// Builds the MoonBit component, compiling all packages and linking them together into a
461    /// final WASM component.
462    ///
463    /// The `main_package_name` is optional, if it is not provided, the binding generator's `gen` package will be used as the main package.
464    pub fn build(&self, main_package_name: Option<&str>, target: &Utf8Path) -> anyhow::Result<()> {
465        let main_package_name = match main_package_name {
466            Some(name) => name.to_string(),
467            None => {
468                let root_package = self.moonbit_root_package()?;
469                format!("{root_package}/gen")
470            }
471        };
472
473        let sorted_packages = self.sorted_packages()?;
474        debug!(
475            "Package build order: {}",
476            sorted_packages
477                .iter()
478                .map(|p| p.name.clone())
479                .collect::<Vec<_>>()
480                .join(", ")
481        );
482
483        for package in &sorted_packages {
484            self.build_package(
485                &package.mbt_files,
486                &package.warning_control,
487                &package.output,
488                &package.name,
489                &package.dependencies,
490                &package.package_sources,
491            )
492            .context(format!("Building package {}", package.name))?;
493        }
494
495        let mut core_files = vec![
496            self.core_bundle_dir().join("abort").join("abort.core"),
497            self.core_bundle_dir().join("core.core"),
498        ];
499        let mut package_sources = BTreeMap::new();
500
501        for package in &sorted_packages {
502            core_files.push(package.output.clone());
503            for (name, source) in &package.package_sources {
504                package_sources.insert(name.clone(), source.clone());
505            }
506        }
507        package_sources.insert("moonbitlang/core".to_string(), self.core_dir());
508        let package_sources: Vec<(String, Utf8PathBuf)> = package_sources.into_iter().collect();
509
510        let main_package = self
511            .packages
512            .get(&main_package_name)
513            .ok_or_else(|| anyhow!(format!("Main package '{main_package_name}' not found")))?;
514        let (_, main_package_source) = main_package
515            .package_sources
516            .iter()
517            .find(|(name, _)| name == &main_package_name)
518            .ok_or_else(|| {
519                anyhow!(format!(
520                    "Main package sources '{main_package_name}' not found"
521                ))
522            })?;
523
524        let main_package_json = self.dir.join(main_package_source).join("moon.pkg.json");
525        let linker_config = Self::extract_wasm_linker_config(&main_package_json)
526            .context("Extracting linker config")?;
527
528        self.link_core(
529            &core_files,
530            &main_package_name,
531            &main_package_json,
532            &package_sources,
533            &linker_config.export_memory_name,
534            &linker_config.exports,
535            linker_config.heap_start_address,
536        )
537        .context("Linking")?;
538
539        self.embed_wit().context("Embedding WIT")?;
540        self.create_component(target)
541            .context("Creating component")?;
542
543        Ok(())
544    }
545
546    fn extract_core(&self) -> anyhow::Result<()> {
547        let core_dir = self.core_dir();
548        info!("Extracting MoonBit core to {core_dir}");
549        std::fs::create_dir_all(&core_dir)?;
550        MOONBIT_CORE.extract(&core_dir)?;
551        Ok(())
552    }
553
554    fn build_package(
555        &self,
556        mbt_files: &[Utf8PathBuf],
557        warning_control: &[WarningControl],
558        output: &Utf8Path,
559        package: &str,
560        dependencies: &[(Utf8PathBuf, String)],
561        package_sources: &[(String, Utf8PathBuf)],
562    ) -> anyhow::Result<()> {
563        info!("Building MoonBit package: {package}");
564
565        let mut args = vec!["build-package".to_string()];
566        for file in mbt_files {
567            let full_path = self.dir.join(file);
568            args.push(full_path.to_string());
569        }
570        for w in warning_control {
571            args.push("-w".to_string());
572            args.push(w.to_string());
573        }
574        args.push("-o".to_string());
575        args.push(self.dir.join(output).to_string());
576        args.push("-pkg".to_string());
577        args.push(package.to_string());
578        args.push("-std-path".to_string());
579        args.push(self.core_bundle_dir().to_string());
580        for (dep_path, dep_name) in dependencies {
581            args.push("-i".to_string());
582            let full_path = self.dir.join(dep_path);
583            args.push(format!("{full_path}:{dep_name}"));
584        }
585        self.add_package_sources(&mut args, package_sources);
586        args.push("-target".to_string());
587        args.push("wasm".to_string());
588
589        MOONC.run(args)?;
590        Ok(())
591    }
592
593    #[allow(clippy::too_many_arguments)]
594    fn link_core(
595        &self,
596        core_files: &[Utf8PathBuf],
597        main_package_name: &str,
598        main_package_json: &Utf8Path,
599        package_sources: &[(String, Utf8PathBuf)],
600        exported_memory_name: &str,
601        exported_functions: &[String],
602        heap_start_address: usize,
603    ) -> anyhow::Result<()> {
604        info!("Linking MoonBit component");
605        let mut args = vec!["link-core".to_string()];
606
607        for file in core_files {
608            let full_path = self.dir.join(file);
609            args.push(full_path.to_string());
610        }
611        args.push("-main".to_string());
612        args.push(main_package_name.to_string());
613        args.push("-o".to_string());
614        args.push(self.module_wasm().to_string());
615        args.push("-pkg-config-path".to_string());
616        args.push(self.dir.join(main_package_json).to_string());
617        self.add_package_sources(&mut args, package_sources);
618        args.push("-pkg-sources".to_string());
619        args.push(format!("moonbitlang/core:{}", self.core_dir()));
620        args.push("-target".to_string());
621        args.push("wasm".to_string());
622        args.push(format!(
623            "-exported_functions={}",
624            exported_functions.join(",")
625        ));
626        args.push("-export-memory-name".to_string());
627        args.push(exported_memory_name.to_string());
628        args.push("-heap-start-address".to_string());
629        args.push(heap_start_address.to_string());
630
631        MOONC.run(args)?;
632        Ok(())
633    }
634
635    fn add_package_sources(
636        &self,
637        args: &mut Vec<String>,
638        package_sources: &[(String, Utf8PathBuf)],
639    ) {
640        for (source_name, source_path) in package_sources {
641            args.push("-pkg-sources".to_string());
642            let full_path = self.dir.join(source_path);
643            args.push(format!("{source_name}:{full_path}"));
644        }
645    }
646
647    fn embed_wit(&self) -> anyhow::Result<()> {
648        info!("Embedding WIT in the compiled MoonBit WASM module");
649
650        // based on 'wasm-tools component embed'
651        let resolve = self.resolve.as_ref().unwrap();
652        let world = &self.world_id.unwrap();
653
654        let module_wasm = self.module_wasm();
655        let mut wasm = std::fs::read(&module_wasm)
656            .context(format!("Failed to read module WASM from {module_wasm}"))?;
657
658        wit_component::embed_component_metadata(&mut wasm, resolve, *world, StringEncoding::UTF16)
659            .context("Embedding component metadata")?;
660
661        std::fs::write(self.module_with_embed_wasm(), wasm)
662            .context("Writing WASM with embedded metadata")?;
663
664        Ok(())
665    }
666
667    fn create_component(&self, target: &Utf8Path) -> anyhow::Result<()> {
668        info!("Creating the final WASM component at {target}");
669
670        let wasm = std::fs::read(self.module_with_embed_wasm())
671            .context("Reading WASM with embedded metadata")?;
672        let mut encoder = ComponentEncoder::default()
673            .validate(true)
674            .reject_legacy_names(false)
675            .merge_imports_based_on_semver(true)
676            .realloc_via_memory_grow(false)
677            .module(&wasm)?;
678
679        let component = encoder.encode().context("Encoding WASM component")?;
680
681        if let Some(parent) = target.parent() {
682            std::fs::create_dir_all(parent).context("Creating directory for WASM component")?;
683        }
684        std::fs::write(target, component).context("Writing WASM component")?;
685
686        Ok(())
687    }
688
689    fn sorted_packages(&self) -> anyhow::Result<Vec<&MoonBitPackage>> {
690        let mut graph = AcyclicDependencyGraph::new();
691        let root_package = self.moonbit_root_package()?;
692
693        for package in self.packages.values() {
694            for (path, dep) in &package.dependencies {
695                let path_components = path.components().map(|c| c.to_string()).collect::<Vec<_>>();
696                let full_dep = if path_components.starts_with(&[
697                    "target".to_string(),
698                    "wasm".to_string(),
699                    "release".to_string(),
700                    "build".to_string(),
701                ]) {
702                    let relevant_path = &path_components[4..path_components.len() - 1];
703                    format!("{}/{}", root_package, relevant_path.join("/"))
704                } else {
705                    format!("{root_package}/{dep}")
706                };
707
708                graph.depend_on(package.name.clone(), full_dep)?;
709            }
710        }
711
712        let mut sorted = Vec::new();
713        let mut names = HashSet::new();
714        for layer in graph.get_forward_dependency_topological_layers() {
715            for package_name in layer {
716                sorted.push(&self.packages[&package_name]);
717                names.insert(package_name);
718            }
719        }
720
721        for package in self.packages.values() {
722            if !names.contains(&package.name) {
723                sorted.push(package);
724            }
725        }
726
727        Ok(sorted)
728    }
729
730    fn wit_dir(&self) -> Utf8PathBuf {
731        self.dir.join("wit")
732    }
733
734    fn core_dir(&self) -> Utf8PathBuf {
735        self.dir.join("core")
736    }
737
738    fn core_bundle_dir(&self) -> Utf8PathBuf {
739        self.dir
740            .join("core")
741            .join("target")
742            .join("wasm")
743            .join("release")
744            .join("bundle")
745    }
746
747    fn module_wasm(&self) -> Utf8PathBuf {
748        self.dir.join("target").join("module.wasm")
749    }
750
751    fn module_with_embed_wasm(&self) -> Utf8PathBuf {
752        self.dir.join("target").join("module.embed.wasm")
753    }
754
755    fn extract_wasm_linker_config(
756        package_json_path: &Utf8Path,
757    ) -> anyhow::Result<WasmLinkerConfig> {
758        debug!("Extracting Wasm linker config from {package_json_path}");
759        let json_str = std::fs::read_to_string(package_json_path)?;
760        let pkg: PackageJsonWithWasmLinkerConfig = serde_json::from_str(&json_str)?;
761
762        Ok(pkg.link.wasm)
763    }
764
765    pub fn moonbit_root_package(&self) -> anyhow::Result<String> {
766        Ok(format!(
767            "{}/{}",
768            self.root_pkg_namespace()?,
769            self.root_pkg_name()?
770        ))
771    }
772
773    pub fn root_pkg_namespace(&self) -> anyhow::Result<String> {
774        let root_package_id = self.root_package_id.as_ref().unwrap();
775        let resolve = self.resolve.as_ref().unwrap();
776
777        let root_package = resolve
778            .packages
779            .get(*root_package_id)
780            .ok_or_else(|| anyhow!("Root package not found"))?;
781        Ok(root_package.name.namespace.to_string())
782    }
783
784    pub fn root_pkg_name(&self) -> anyhow::Result<String> {
785        let root_package_id = self.root_package_id.as_ref().unwrap();
786        let resolve = self.resolve.as_ref().unwrap();
787
788        let root_package = resolve
789            .packages
790            .get(*root_package_id)
791            .ok_or_else(|| anyhow!("Root package not found"))?;
792        Ok(root_package.name.name.to_string())
793    }
794
795    fn world_name(&self) -> anyhow::Result<String> {
796        Ok(self
797            .resolve
798            .as_ref()
799            .and_then(|r| r.worlds.get(self.world_id?))
800            .map(|w| w.name.to_string())
801            .ok_or_else(|| anyhow::anyhow!("Could not find world"))?
802            .to_lower_camel_case())
803    }
804
805    fn get_imported_interfaces(&self) -> anyhow::Result<Vec<(PackageName, String)>> {
806        let world = self
807            .resolve
808            .as_ref()
809            .and_then(|r| r.worlds.get(self.world_id?))
810            .ok_or_else(|| anyhow::anyhow!("Could not find world"))?;
811        let mut imported_interfaces = Vec::new();
812        for (_, item) in &world.imports {
813            if let wit_parser::WorldItem::Interface { id, .. } = item {
814                if let Some(interface) = self.resolve.as_ref().and_then(|r| r.interfaces.get(*id)) {
815                    if let Some(interface_name) = interface.name.as_ref() {
816                        let owner_package = interface.package.ok_or_else(|| {
817                            anyhow::anyhow!(
818                                "Interface '{}' does not have a package",
819                                interface_name
820                            )
821                        })?;
822                        let package = self
823                            .resolve
824                            .as_ref()
825                            .and_then(|r| r.packages.get(owner_package))
826                            .ok_or_else(|| {
827                                anyhow::anyhow!(
828                                    "Package for interface '{}' not found",
829                                    interface_name
830                                )
831                            })?;
832
833                        imported_interfaces
834                            .push((package.name.clone(), interface_name.to_string()));
835                    } else {
836                        return Err(anyhow::anyhow!(
837                            "Anonymous imported interfaces are not supported"
838                        ));
839                    }
840                }
841            }
842        }
843        Ok(imported_interfaces)
844    }
845
846    fn get_exported_interfaces(&self) -> anyhow::Result<Vec<(PackageName, String)>> {
847        let world = self
848            .resolve
849            .as_ref()
850            .and_then(|r| r.worlds.get(self.world_id?))
851            .ok_or_else(|| anyhow::anyhow!("Could not find world"))?;
852        let mut exported_interfaces = Vec::new();
853        for (_, item) in &world.exports {
854            if let wit_parser::WorldItem::Interface { id, .. } = item {
855                if let Some(interface) = self.resolve.as_ref().and_then(|r| r.interfaces.get(*id)) {
856                    if let Some(interface_name) = interface.name.as_ref() {
857                        let owner_package = interface.package.ok_or_else(|| {
858                            anyhow::anyhow!(
859                                "Interface '{}' does not have a package",
860                                interface_name
861                            )
862                        })?;
863                        let package = self
864                            .resolve
865                            .as_ref()
866                            .and_then(|r| r.packages.get(owner_package))
867                            .ok_or_else(|| {
868                                anyhow::anyhow!(
869                                    "Package for interface '{}' not found",
870                                    interface_name
871                                )
872                            })?;
873
874                        exported_interfaces
875                            .push((package.name.clone(), interface_name.to_string()));
876                    } else {
877                        return Err(anyhow::anyhow!(
878                            "Anonymous exported interfaces are not supported"
879                        ));
880                    }
881                }
882            }
883        }
884        Ok(exported_interfaces)
885    }
886}
887
888#[derive(Debug, Deserialize)]
889struct PackageJsonWithWasmLinkerConfig {
890    link: LinkConfig,
891}
892
893#[derive(Debug, Deserialize)]
894struct LinkConfig {
895    wasm: WasmLinkerConfig,
896}
897
898#[derive(Debug, Deserialize)]
899#[serde(rename_all = "kebab-case")]
900struct WasmLinkerConfig {
901    export_memory_name: String,
902    exports: Vec<String>,
903    heap_start_address: usize,
904}
905
906pub struct MoonBitPackage {
907    pub name: String,
908    pub mbt_files: Vec<Utf8PathBuf>,
909    pub warning_control: Vec<WarningControl>,
910    pub output: Utf8PathBuf,
911    pub dependencies: Vec<(Utf8PathBuf, String)>,
912    pub package_sources: Vec<(String, Utf8PathBuf)>,
913}
914
915pub enum Warning {
916    Specific(u16),
917    Range(Range<u16>),
918}
919
920impl Display for Warning {
921    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
922        match self {
923            Warning::Specific(code) => write!(f, "{code}"),
924            Warning::Range(range) => write!(f, "{}..{}", range.start, range.end),
925        }
926    }
927}
928
929pub enum WarningControl {
930    Enable(Warning),
931    Disable(Warning),
932    EnableAsError(Warning),
933}
934
935impl Display for WarningControl {
936    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
937        match self {
938            WarningControl::Enable(code) => write!(f, "+{code}"),
939            WarningControl::Disable(code) => write!(f, "-{code}"),
940            WarningControl::EnableAsError(code) => write!(f, "@{code}"),
941        }
942    }
943}
944
945#[cfg(test)]
946test_r::enable!();
947
948#[cfg(test)]
949struct Trace;
950
951#[cfg(test)]
952#[test_r::test_dep]
953fn initialize_trace() -> Trace {
954    pretty_env_logger::formatted_builder()
955        .filter_level(log::LevelFilter::Debug)
956        .write_style(pretty_env_logger::env_logger::WriteStyle::Always)
957        .init();
958    Trace
959}