1use anyhow::{Context, Result, anyhow, bail};
2use clap::{Args, Subcommand};
3use convert_case::{Case, Casing};
4use once_cell::sync::Lazy;
5use semver::Version;
6use serde::{Deserialize, Serialize};
7use sha2::{Digest, Sha256};
8use std::collections::{BTreeSet, HashMap};
9use std::env;
10use std::fs;
11use std::io::Write;
12use std::path::{Path, PathBuf};
13use std::process::Command;
14use time::OffsetDateTime;
15use time::format_description::well_known::Rfc3339;
16use wit_component::{DecodedWasm, decode as decode_component};
17use wit_parser::{Resolve, WorldId, WorldItem};
18
19static WORKSPACE_ROOT: Lazy<PathBuf> = Lazy::new(|| PathBuf::from(env!("CARGO_MANIFEST_DIR")));
20
21const TEMPLATE_COMPONENT_CARGO: &str = include_str!(concat!(
22 env!("CARGO_MANIFEST_DIR"),
23 "/templates/component/Cargo.toml.in"
24));
25const TEMPLATE_SRC_LIB: &str = include_str!(concat!(
26 env!("CARGO_MANIFEST_DIR"),
27 "/templates/component/src/lib.rs"
28));
29const TEMPLATE_PROVIDER: &str = include_str!(concat!(
30 env!("CARGO_MANIFEST_DIR"),
31 "/templates/component/provider.toml"
32));
33const TEMPLATE_SCHEMA_CONFIG: &str = include_str!(concat!(
34 env!("CARGO_MANIFEST_DIR"),
35 "/templates/component/schemas/v1/config.schema.json"
36));
37const TEMPLATE_README: &str = include_str!(concat!(
38 env!("CARGO_MANIFEST_DIR"),
39 "/templates/component/README.md"
40));
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
43struct ProviderMetadata {
44 name: String,
45 version: String,
46 #[serde(default)]
47 description: Option<String>,
48 #[serde(default)]
49 license: Option<String>,
50 #[serde(default)]
51 homepage: Option<String>,
52 abi: AbiSection,
53 capabilities: CapabilitiesSection,
54 exports: ExportsSection,
55 #[serde(default)]
56 imports: ImportsSection,
57 artifact: ArtifactSection,
58 #[serde(default)]
59 docs: Option<DocsSection>,
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize)]
63struct AbiSection {
64 interfaces_version: String,
65 types_version: String,
66 component_runtime: String,
67 world: String,
68 #[serde(default)]
69 wit_packages: Vec<String>,
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
73struct CapabilitiesSection {
74 #[serde(default)]
75 secrets: bool,
76 #[serde(default)]
77 telemetry: bool,
78 #[serde(default)]
79 network: bool,
80 #[serde(default)]
81 filesystem: bool,
82}
83
84#[derive(Debug, Clone, Serialize, Deserialize)]
85struct ExportsSection {
86 #[serde(default)]
87 provides: Vec<String>,
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize, Default)]
91struct ImportsSection {
92 #[serde(default)]
93 requires: Vec<String>,
94}
95
96#[derive(Debug, Clone, Serialize, Deserialize)]
97struct ArtifactSection {
98 format: String,
99 path: String,
100 #[serde(default)]
101 sha256: String,
102 #[serde(default)]
103 created: String,
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize, Default)]
107struct DocsSection {
108 #[serde(default)]
109 readme: Option<String>,
110 #[serde(default)]
111 schemas: Vec<String>,
112}
113
114#[derive(Debug)]
115struct ValidationReport {
116 provider: ProviderMetadata,
117 component_dir: PathBuf,
118 artifact_path: PathBuf,
119 sha256: String,
120 world: String,
121 packages: Vec<String>,
122}
123
124#[derive(Debug, Clone)]
125struct Versions {
126 interfaces: String,
127 types: String,
128 component_runtime: String,
129 component_wit_version: String,
130 secrets_wit_version: String,
131 state_wit_version: String,
132 http_wit_version: String,
133 telemetry_wit_version: String,
134}
135
136impl Versions {
137 fn load() -> Result<Self> {
138 let interfaces_version = resolved_version("greentic-interfaces")?;
139 let types_version = resolved_version("greentic-types")?;
140 let component_runtime_version = resolved_version("greentic-component")?;
141
142 let interfaces_root = find_crate_source("greentic-interfaces", &interfaces_version)?;
143 let component_wit_version = detect_component_node_world_version(&interfaces_root)?;
144 let secrets_wit_version = detect_wit_package_version(&interfaces_root, "secrets")?;
145 let state_wit_version = detect_wit_package_version(&interfaces_root, "state")?;
146 let http_wit_version = detect_wit_package_version(&interfaces_root, "http")?;
147 let telemetry_wit_version = detect_wit_package_version(&interfaces_root, "telemetry")?;
148
149 Ok(Self {
150 interfaces: interfaces_version,
151 types: types_version,
152 component_runtime: component_runtime_version,
153 component_wit_version,
154 secrets_wit_version,
155 state_wit_version,
156 http_wit_version,
157 telemetry_wit_version,
158 })
159 }
160}
161
162static VERSIONS: Lazy<Versions> =
163 Lazy::new(|| Versions::load().expect("load greentic crate versions"));
164
165pub fn run_component_command(command: ComponentCommands) -> Result<()> {
166 match command {
167 ComponentCommands::New(args) => new_component(args),
168 ComponentCommands::Validate(args) => validate_command(args),
169 ComponentCommands::Pack(args) => pack_command(args),
170 }
171}
172
173#[derive(Subcommand, Debug, Clone)]
174pub enum ComponentCommands {
175 New(NewComponentArgs),
177 Validate(ValidateArgs),
179 Pack(PackArgs),
181}
182
183#[derive(Args, Debug, Clone)]
184pub struct NewComponentArgs {
185 name: String,
187 #[arg(long, value_name = "DIR")]
189 dir: Option<PathBuf>,
190}
191
192#[derive(Args, Debug, Clone)]
193pub struct ValidateArgs {
194 #[arg(long, value_name = "PATH", default_value = ".")]
196 path: PathBuf,
197 #[arg(long)]
199 skip_build: bool,
200}
201
202#[derive(Args, Debug, Clone)]
203pub struct PackArgs {
204 #[arg(long, value_name = "PATH", default_value = ".")]
206 path: PathBuf,
207 #[arg(long, value_name = "DIR")]
209 out_dir: Option<PathBuf>,
210 #[arg(long)]
212 skip_build: bool,
213}
214
215pub fn new_component(args: NewComponentArgs) -> Result<()> {
216 let context = TemplateContext::new(&args.name)?;
217 let base_dir = match args.dir {
218 Some(ref dir) if dir.is_absolute() => dir.clone(),
219 Some(dir) => env::current_dir()
220 .with_context(|| "failed to resolve current directory")?
221 .join(dir),
222 None => env::current_dir().with_context(|| "failed to resolve current directory")?,
223 };
224 fs::create_dir_all(&base_dir)
225 .with_context(|| format!("failed to prepare base directory {}", base_dir.display()))?;
226 let component_dir = base_dir.join(context.component_dir());
227
228 if component_dir.exists() {
229 bail!(
230 "component directory `{}` already exists",
231 component_dir.display()
232 );
233 }
234
235 println!(
236 "Creating new component scaffold at `{}`",
237 component_dir.display()
238 );
239
240 create_dir(component_dir.join("src"))?;
241 create_dir(component_dir.join("schemas/v1"))?;
242
243 write_template(
244 &component_dir.join("Cargo.toml"),
245 TEMPLATE_COMPONENT_CARGO,
246 &context,
247 )?;
248 write_template(&component_dir.join("README.md"), TEMPLATE_README, &context)?;
249 write_template(
250 &component_dir.join("provider.toml"),
251 TEMPLATE_PROVIDER,
252 &context,
253 )?;
254 write_template(
255 &component_dir.join("src/lib.rs"),
256 TEMPLATE_SRC_LIB,
257 &context,
258 )?;
259 write_template(
260 &component_dir.join("schemas/v1/config.schema.json"),
261 TEMPLATE_SCHEMA_CONFIG,
262 &context,
263 )?;
264
265 println!(
266 "Component `{}` scaffolded successfully.",
267 context.component_name
268 );
269
270 Ok(())
271}
272
273pub fn validate_command(args: ValidateArgs) -> Result<()> {
274 let report = validate_component(&args.path, !args.skip_build)?;
275 print_validation_summary(&report);
276 Ok(())
277}
278
279pub fn pack_command(args: PackArgs) -> Result<()> {
280 let report = validate_component(&args.path, !args.skip_build)?;
281 let base_out = match args.out_dir {
282 Some(ref dir) if dir.is_absolute() => dir.clone(),
283 Some(ref dir) => report.component_dir.join(dir),
284 None => report.component_dir.join("packs"),
285 };
286 fs::create_dir_all(&base_out)
287 .with_context(|| format!("failed to create {}", base_out.display()))?;
288
289 let dest_dir = base_out
290 .join(&report.provider.name)
291 .join(&report.provider.version);
292 if dest_dir.exists() {
293 fs::remove_dir_all(&dest_dir)
294 .with_context(|| format!("failed to clear {}", dest_dir.display()))?;
295 }
296 fs::create_dir_all(&dest_dir)
297 .with_context(|| format!("failed to create {}", dest_dir.display()))?;
298
299 let artifact_file = format!("{}-{}.wasm", report.provider.name, report.provider.version);
300 let dest_wasm = dest_dir.join(&artifact_file);
301 fs::copy(&report.artifact_path, &dest_wasm).with_context(|| {
302 format!(
303 "failed to copy {} to {}",
304 report.artifact_path.display(),
305 dest_wasm.display()
306 )
307 })?;
308
309 let mut meta = report.provider.clone();
310 meta.artifact.path = artifact_file.clone();
311 meta.artifact.sha256 = report.sha256.clone();
312 meta.artifact.created = OffsetDateTime::now_utc()
313 .format(&Rfc3339)
314 .context("unable to format timestamp")?;
315 meta.abi.wit_packages = report.packages.clone();
316
317 let meta_path = dest_dir.join("meta.json");
318 let meta_file = fs::File::create(&meta_path)
319 .with_context(|| format!("failed to create {}", meta_path.display()))?;
320 serde_json::to_writer_pretty(meta_file, &meta)
321 .with_context(|| format!("failed to write {}", meta_path.display()))?;
322
323 let mut sums =
324 fs::File::create(dest_dir.join("SHA256SUMS")).context("failed to create SHA256SUMS")?;
325 writeln!(sums, "{} {}", report.sha256, artifact_file).context("failed to write SHA256SUMS")?;
326
327 println!("✓ Packed component at {}", dest_dir.display());
328 Ok(())
329}
330
331fn create_dir(path: PathBuf) -> Result<()> {
332 fs::create_dir_all(&path)
333 .with_context(|| format!("failed to create directory `{}`", path.display()))
334}
335
336fn write_template(path: &Path, template: &str, context: &TemplateContext) -> Result<()> {
337 if path.exists() {
338 bail!("file `{}` already exists", path.display());
339 }
340
341 let rendered = render_template(template, context);
342 fs::write(path, rendered).with_context(|| format!("failed to write `{}`", path.display()))
343}
344
345fn render_template(template: &str, context: &TemplateContext) -> String {
346 let mut output = template.to_owned();
347 for (key, value) in &context.placeholders {
348 let token = format!("{{{{{key}}}}}");
349 output = output.replace(&token, value);
350 }
351 output
352}
353
354fn detect_component_node_world_version(crate_root: &Path) -> Result<String> {
355 let wit_dir = crate_root.join("wit");
356 let namespace_dir = wit_dir.join("greentic");
357 let prefix = "component@";
358 let mut best: Option<(Version, PathBuf)> = None;
359
360 for entry in fs::read_dir(&namespace_dir).with_context(|| {
361 format!(
362 "failed to read namespace directory {}",
363 namespace_dir.display()
364 )
365 })? {
366 let entry = entry?;
367 let path = entry.path();
368 if !path.is_dir() {
369 continue;
370 }
371 let name = entry
372 .file_name()
373 .into_string()
374 .map_err(|_| anyhow!("non-unicode filename under {}", namespace_dir.display()))?;
375 if let Some(rest) = name.strip_prefix(prefix) {
376 let version = Version::parse(rest)
377 .with_context(|| format!("invalid semver `{rest}` for {prefix}"))?;
378 let package_path = path.join("package.wit");
379 let contents = fs::read_to_string(&package_path).with_context(|| {
380 format!("failed to read package file {}", package_path.display())
381 })?;
382 if contents.contains("export node") {
383 match &best {
384 Some((best_ver, _)) if version <= *best_ver => {}
385 _ => best = Some((version, path)),
386 }
387 }
388 }
389 }
390
391 if let Some((version, _)) = best {
392 return Ok(version.to_string());
393 }
394
395 detect_wit_package_version(crate_root, "component")
396}
397
398fn detect_wit_package_version(crate_root: &Path, prefix: &str) -> Result<String> {
399 let wit_dir = crate_root.join("wit");
400 let namespace_dir = wit_dir.join("greentic");
401 let prefix = format!("{prefix}@");
402
403 let mut best: Option<(Version, PathBuf)> = None;
404 for entry in fs::read_dir(&namespace_dir).with_context(|| {
405 format!(
406 "failed to read namespace directory {}",
407 namespace_dir.display()
408 )
409 })? {
410 let entry = entry?;
411 let path = entry.path();
412 if !path.is_dir() {
413 continue;
414 }
415 let name = entry
416 .file_name()
417 .into_string()
418 .map_err(|_| anyhow!("non-unicode filename under {}", namespace_dir.display()))?;
419 if let Some(rest) = name.strip_prefix(&prefix) {
420 let version = Version::parse(rest)
421 .with_context(|| format!("invalid semver `{rest}` for {prefix}"))?;
422 if best.as_ref().is_none_or(|(current, _)| &version > current) {
423 best = Some((version, path));
424 }
425 }
426 }
427
428 match best {
429 Some((version, _)) => Ok(version.to_string()),
430 None => Err(anyhow!(
431 "unable to locate WIT package `{}` under {}",
432 prefix,
433 namespace_dir.display()
434 )),
435 }
436}
437
438#[derive(Deserialize)]
439struct LockPackage {
440 name: String,
441 version: String,
442}
443
444#[derive(Deserialize)]
445struct LockFile {
446 package: Vec<LockPackage>,
447}
448
449fn resolved_version(crate_name: &str) -> Result<String> {
450 let lock_path = WORKSPACE_ROOT.join("Cargo.lock");
451 let contents = fs::read_to_string(&lock_path)
452 .with_context(|| format!("failed to read {}", lock_path.display()))?;
453 let lock: LockFile =
454 toml::from_str(&contents).with_context(|| format!("invalid {}", lock_path.display()))?;
455
456 let mut best: Option<(Version, String)> = None;
457 for pkg in lock
458 .package
459 .into_iter()
460 .filter(|pkg| pkg.name == crate_name)
461 {
462 let version = Version::parse(&pkg.version)
463 .with_context(|| format!("invalid semver `{}` for {}", pkg.version, crate_name))?;
464 if best.as_ref().is_none_or(|(current, _)| &version > current) {
465 best = Some((version, pkg.version));
466 }
467 }
468
469 match best {
470 Some((_, version)) => Ok(version),
471 None => Err(anyhow!(
472 "crate `{}` not found in {}",
473 crate_name,
474 lock_path.display()
475 )),
476 }
477}
478
479fn cargo_home() -> Result<PathBuf> {
480 if let Ok(path) = env::var("CARGO_HOME") {
481 return Ok(PathBuf::from(path));
482 }
483 if let Ok(home) = env::var("HOME") {
484 return Ok(PathBuf::from(home).join(".cargo"));
485 }
486 Err(anyhow!(
487 "unable to determine CARGO_HOME; set the environment variable explicitly"
488 ))
489}
490
491fn find_crate_source(crate_name: &str, version: &str) -> Result<PathBuf> {
492 let home = cargo_home()?;
493 let registry_src = home.join("registry/src");
494 if !registry_src.exists() {
495 return Err(anyhow!(
496 "cargo registry src directory not found at {}",
497 registry_src.display()
498 ));
499 }
500
501 for index in fs::read_dir(®istry_src)? {
502 let index_path = index?.path();
503 if !index_path.is_dir() {
504 continue;
505 }
506 let candidate = index_path.join(format!("{crate_name}-{version}"));
507 if candidate.exists() {
508 return Ok(candidate);
509 }
510 }
511
512 Err(anyhow!(
513 "crate `{}` version `{}` not found under {}",
514 crate_name,
515 version,
516 registry_src.display()
517 ))
518}
519
520struct TemplateContext {
521 component_name: String,
522 component_kebab: String,
523 placeholders: HashMap<String, String>,
524}
525
526impl TemplateContext {
527 fn new(raw: &str) -> Result<Self> {
528 let trimmed = raw.trim();
529 if trimmed.is_empty() {
530 bail!("component name cannot be empty");
531 }
532
533 let component_kebab = trimmed.to_case(Case::Kebab);
534 let component_snake = trimmed.to_case(Case::Snake);
535 let component_pascal = trimmed.to_case(Case::Pascal);
536 let component_name = component_kebab.clone();
537 let versions = VERSIONS.clone();
538
539 let mut placeholders = HashMap::new();
540 placeholders.insert("component_name".into(), component_name.clone());
541 placeholders.insert("component_kebab".into(), component_kebab.clone());
542 placeholders.insert("component_snake".into(), component_snake.clone());
543 placeholders.insert("component_pascal".into(), component_pascal.clone());
544 placeholders.insert("component_crate".into(), component_kebab.clone());
545 placeholders.insert(
546 "component_dir".into(),
547 format!("component-{component_kebab}"),
548 );
549 placeholders.insert("interfaces_version".into(), versions.interfaces.clone());
550 placeholders.insert("types_version".into(), versions.types.clone());
551 placeholders.insert(
552 "component_runtime_version".into(),
553 versions.component_runtime.clone(),
554 );
555 placeholders.insert(
556 "component_world_version".into(),
557 versions.component_wit_version.clone(),
558 );
559 placeholders.insert(
560 "interfaces_guest_version".into(),
561 versions.interfaces.clone(),
562 );
563 placeholders.insert(
564 "secrets_wit_version".into(),
565 versions.secrets_wit_version.clone(),
566 );
567 placeholders.insert(
568 "state_wit_version".into(),
569 versions.state_wit_version.clone(),
570 );
571 placeholders.insert("http_wit_version".into(), versions.http_wit_version.clone());
572 placeholders.insert(
573 "telemetry_wit_version".into(),
574 versions.telemetry_wit_version.clone(),
575 );
576
577 Ok(Self {
578 component_name,
579 component_kebab,
580 placeholders,
581 })
582 }
583
584 fn component_dir(&self) -> String {
585 format!("component-{}", self.component_kebab)
586 }
587}
588
589fn print_validation_summary(report: &ValidationReport) {
590 println!(
591 "✓ Validated {} {}",
592 report.provider.name, report.provider.version
593 );
594 println!(" artifact: {}", report.artifact_path.display());
595 println!(" sha256 : {}", report.sha256);
596 println!(" world : {}", report.world);
597 println!(" packages:");
598 for pkg in &report.packages {
599 println!(" - {pkg}");
600 }
601}
602
603fn validate_component(path: &Path, build: bool) -> Result<ValidationReport> {
604 let component_dir = resolve_component_dir(path)?;
605
606 if build {
607 ensure_cargo_component_installed()?;
608 run_cargo_component_build(&component_dir)?;
609 }
610
611 let provider_path = component_dir.join("provider.toml");
612 let provider = load_provider(&provider_path)?;
613
614 let versions = Versions::load()?;
615 ensure_version_alignment(&provider, &versions)?;
616
617 let mut attempted = Vec::new();
618 let mut artifact_path = None;
619 for candidate in candidate_artifact_paths(&provider.artifact.path) {
620 let resolved = resolve_path(&component_dir, Path::new(&candidate));
621 attempted.push(resolved.clone());
622 if resolved.exists() {
623 artifact_path = Some(resolved);
624 break;
625 }
626 }
627 let artifact_path = match artifact_path {
628 Some(path) => path,
629 None => {
630 let paths = attempted
631 .into_iter()
632 .map(|p| p.display().to_string())
633 .collect::<Vec<_>>()
634 .join(", ");
635 bail!("artifact path not found; checked {paths}");
636 }
637 };
638
639 let wasm_bytes = fs::read(&artifact_path)
640 .with_context(|| format!("failed to read {}", artifact_path.display()))?;
641 let sha256 = format!("{:x}", Sha256::digest(&wasm_bytes));
642
643 let decoded = decode_component(&wasm_bytes).context("failed to decode component")?;
644 let (resolve, world_id) = match decoded {
645 DecodedWasm::Component(resolve, world) => (resolve, world),
646 DecodedWasm::WitPackage(_, _) => {
647 bail!("expected a component artifact but found a WIT package bundle")
648 }
649 };
650 let (packages, world, export_package) = extract_wit_metadata(&resolve, world_id)?;
651
652 if packages.is_empty() {
653 bail!("no WIT packages embedded in component artifact");
654 }
655
656 if provider.abi.world != world {
657 if let Some(expected_pkg) = world_to_package_id(&provider.abi.world) {
658 if let Some(actual_pkg) = export_package {
659 if actual_pkg != expected_pkg {
660 bail!(
661 "provider world `{}` expects package '{}', but embedded exports use '{}'",
662 provider.abi.world,
663 expected_pkg,
664 actual_pkg
665 );
666 }
667 } else if !packages.iter().any(|pkg| pkg == &expected_pkg) {
668 bail!(
669 "provider world `{}` expects package '{}', which was not embedded (found {:?})",
670 provider.abi.world,
671 expected_pkg,
672 packages
673 );
674 }
675 } else {
676 bail!(
677 "provider world `{}` is not formatted as <namespace>:<package>/<world>@<version>",
678 provider.abi.world
679 );
680 }
681 }
682
683 let expected_packages: BTreeSet<_> = provider.abi.wit_packages.iter().cloned().collect();
684 if !expected_packages.is_empty() {
685 let actual_greentic: BTreeSet<_> = packages
686 .iter()
687 .filter(|pkg| pkg.starts_with("greentic:"))
688 .cloned()
689 .collect();
690 if !expected_packages.is_subset(&actual_greentic) {
691 bail!(
692 "provider wit_packages {expected_packages:?} not satisfied by embedded packages \
693 {actual_greentic:?}"
694 );
695 }
696 }
697
698 Ok(ValidationReport {
699 provider,
700 component_dir,
701 artifact_path,
702 sha256,
703 world,
704 packages,
705 })
706}
707
708fn resolve_component_dir(path: &Path) -> Result<PathBuf> {
709 let dir = if path.is_absolute() {
710 path.to_path_buf()
711 } else {
712 env::current_dir()
713 .context("unable to determine current directory")?
714 .join(path)
715 };
716 dir.canonicalize()
717 .with_context(|| format!("failed to canonicalize {}", dir.display()))
718}
719
720fn resolve_path(base: &Path, raw: impl AsRef<Path>) -> PathBuf {
721 let raw_path = raw.as_ref();
722 if raw_path.is_absolute() {
723 raw_path.to_path_buf()
724 } else {
725 base.join(raw_path)
726 }
727}
728
729fn candidate_artifact_paths(original: &str) -> Vec<String> {
730 let mut paths = Vec::new();
731 paths.push(original.to_string());
732
733 for (from, to) in [
734 ("wasm32-wasip2", "wasm32-wasip1"),
735 ("wasm32-wasip2", "wasm32-wasi"),
736 ("wasm32-wasip1", "wasm32-wasip2"),
737 ("wasm32-wasip1", "wasm32-wasi"),
738 ("wasm32-wasi", "wasm32-wasip2"),
739 ("wasm32-wasi", "wasm32-wasip1"),
740 ] {
741 if original.contains(from) {
742 let candidate = original.replace(from, to);
743 if candidate != original && !paths.contains(&candidate) {
744 paths.push(candidate);
745 }
746 }
747 }
748
749 paths
750}
751
752fn ensure_cargo_component_installed() -> Result<()> {
753 let status = Command::new("cargo")
754 .arg("component")
755 .arg("--version")
756 .status();
757 match status {
758 Ok(status) if status.success() => Ok(()),
759 Ok(_) => bail!(
760 "cargo-component is required. Install with `cargo install cargo-component --locked`."
761 ),
762 Err(err) => Err(anyhow!(
763 "failed to execute `cargo component --version`: {err}. Install cargo-component with `cargo install cargo-component --locked`."
764 )),
765 }
766}
767
768fn run_cargo_component_build(component_dir: &Path) -> Result<()> {
769 let cache_dir = component_dir.join("target").join(".component-cache");
770 let status = Command::new("cargo")
771 .current_dir(component_dir)
772 .arg("component")
773 .arg("build")
774 .arg("--release")
775 .arg("--target")
776 .arg("wasm32-wasip2")
777 .env("CARGO_COMPONENT_CACHE_DIR", cache_dir.as_os_str())
778 .env("CARGO_NET_OFFLINE", "true")
779 .status()
780 .with_context(|| {
781 format!(
782 "failed to run `cargo component build` in {}",
783 component_dir.display()
784 )
785 })?;
786 if status.success() {
787 Ok(())
788 } else {
789 bail!("cargo component build failed")
790 }
791}
792
793fn load_provider(path: &Path) -> Result<ProviderMetadata> {
794 let contents = fs::read_to_string(path)
795 .with_context(|| format!("failed to read provider metadata {}", path.display()))?;
796 let provider: ProviderMetadata =
797 toml::from_str(&contents).context("provider.toml is not valid TOML")?;
798 if provider.artifact.format != "wasm-component" {
799 bail!(
800 "artifact.format must be `wasm-component`, found `{}`",
801 provider.artifact.format
802 );
803 }
804 Ok(provider)
805}
806
807fn ensure_version_alignment(provider: &ProviderMetadata, versions: &Versions) -> Result<()> {
808 if provider.abi.interfaces_version != versions.interfaces {
809 bail!(
810 "provider abi.interfaces_version `{}` does not match pinned `{}`",
811 provider.abi.interfaces_version,
812 versions.interfaces
813 );
814 }
815 if provider.abi.types_version != versions.types {
816 bail!(
817 "provider abi.types_version `{}` does not match pinned `{}`",
818 provider.abi.types_version,
819 versions.types
820 );
821 }
822 Ok(())
823}
824
825fn extract_wit_metadata(
826 resolve: &Resolve,
827 world_id: WorldId,
828) -> Result<(Vec<String>, String, Option<String>)> {
829 let mut packages = Vec::new();
830 for (_, package) in resolve.packages.iter() {
831 let name = &package.name;
832 if name.namespace == "root" {
833 continue;
834 }
835 if let Some(version) = &name.version {
836 packages.push(format!("{}:{}@{}", name.namespace, name.name, version));
837 } else {
838 packages.push(format!("{}:{}", name.namespace, name.name));
839 }
840 }
841 packages.sort();
842 packages.dedup();
843
844 let world = &resolve.worlds[world_id];
845 let mut export_package = None;
846 for item in world.exports.values() {
847 if let WorldItem::Interface { id, .. } = item {
848 let iface = &resolve.interfaces[*id];
849 if let Some(pkg_id) = iface.package {
850 let pkg = &resolve.packages[pkg_id].name;
851 if pkg.namespace != "root" {
852 let mut ident = format!("{}:{}", pkg.namespace, pkg.name);
853 if let Some(version) = &pkg.version {
854 ident.push('@');
855 ident.push_str(&version.to_string());
856 }
857 export_package.get_or_insert(ident);
858 }
859 }
860 }
861 }
862
863 let world_string = if let Some(pkg_id) = world.package {
864 let pkg = &resolve.packages[pkg_id];
865 if let Some(version) = &pkg.name.version {
866 format!(
867 "{}:{}/{}@{}",
868 pkg.name.namespace, pkg.name.name, world.name, version
869 )
870 } else {
871 format!("{}:{}/{}", pkg.name.namespace, pkg.name.name, world.name)
872 }
873 } else {
874 world.name.clone()
875 };
876
877 Ok((packages, world_string, export_package))
878}
879
880fn world_to_package_id(world: &str) -> Option<String> {
881 let (pkg_part, rest) = world.split_once('/')?;
882 let (_, version) = rest.rsplit_once('@')?;
883 Some(format!("{pkg_part}@{version}"))
884}