1use std::fs;
2use std::path::{Path, PathBuf};
3use std::process::{Command, Stdio};
4
5use anyhow::{Context, Result, bail};
6use tempfile::TempDir;
7
8pub const PACKC_ENV: &str = "GREENTIC_PACK_PLAN_PACKC";
9
10pub fn materialize_pack_path(input: &Path, verbose: bool) -> Result<(Option<TempDir>, PathBuf)> {
11 let metadata =
12 fs::metadata(input).with_context(|| format!("unable to read input {}", input.display()))?;
13 if metadata.is_file() {
14 Ok((None, input.to_path_buf()))
15 } else if metadata.is_dir() {
16 let (temp, path) = build_pack_from_source(input, verbose)?;
17 Ok((Some(temp), path))
18 } else {
19 bail!(
20 "input {} is neither a file nor a directory",
21 input.display()
22 );
23 }
24}
25
26fn build_pack_from_source(source: &Path, verbose: bool) -> Result<(TempDir, PathBuf)> {
27 let packc_bin = std::env::var(PACKC_ENV).unwrap_or_else(|_| "packc".to_string());
28 build_pack_from_source_with_packc(source, verbose, &packc_bin)
29}
30
31fn build_pack_from_source_with_packc(
32 source: &Path,
33 verbose: bool,
34 packc_bin: &str,
35) -> Result<(TempDir, PathBuf)> {
36 let temp = TempDir::new().context("failed to create temporary directory for pack build")?;
37 let gtpack_path = temp.path().join("pack.gtpack");
38 let wasm_path = temp.path().join("pack.wasm");
39 let manifest_path = temp.path().join("manifest.cbor");
40 let sbom_path = temp.path().join("sbom.cdx.json");
41 let component_data = temp.path().join("data.rs");
42
43 let mut cmd = Command::new(packc_bin);
44 cmd.arg("build")
45 .arg("--in")
46 .arg(source)
47 .arg("--out")
48 .arg(&wasm_path)
49 .arg("--manifest")
50 .arg(&manifest_path)
51 .arg("--sbom")
52 .arg(&sbom_path)
53 .arg("--gtpack-out")
54 .arg(>pack_path)
55 .arg("--component-data")
56 .arg(&component_data)
57 .arg("--log")
58 .arg(if verbose { "info" } else { "warn" });
59
60 if !verbose {
61 cmd.stdout(Stdio::null()).stderr(Stdio::null());
62 }
63
64 let status = cmd
65 .status()
66 .context("failed to spawn packc to build temporary .gtpack")?;
67 if !status.success() {
68 bail!("packc build failed with status {}", status);
69 }
70
71 Ok((temp, gtpack_path))
72}
73
74#[cfg(test)]
75mod tests {
76 use super::*;
77 use std::os::unix::fs::PermissionsExt;
78 use tempfile::tempdir;
79
80 #[test]
81 fn materialize_pack_path_keeps_input_files() {
82 let dir = tempdir().expect("tempdir");
83 let file = dir.path().join("demo.gtpack");
84 fs::write(&file, b"pack").expect("write file");
85
86 let (temp, path) = materialize_pack_path(&file, false).expect("file input should work");
87
88 assert!(temp.is_none());
89 assert_eq!(path, file);
90 }
91
92 #[test]
93 fn materialize_pack_path_reports_missing_input() {
94 let dir = tempdir().expect("tempdir");
95 let missing = dir.path().join("missing.gtpack");
96
97 let err = materialize_pack_path(&missing, false).expect_err("missing input should fail");
98 assert!(err.to_string().contains("unable to read input"));
99 }
100
101 #[test]
102 fn build_pack_from_source_invokes_expected_arguments() {
103 let dir = tempdir().expect("tempdir");
104 let source = dir.path().join("pack-src");
105 fs::create_dir_all(&source).expect("source dir");
106
107 let args_log = dir.path().join("args.log");
108 let script = dir.path().join("fake-packc.sh");
109 fs::write(
110 &script,
111 format!(
112 "#!/bin/sh\nprintf '%s\\n' \"$@\" > \"{}\"\nout=''\nwhile [ \"$#\" -gt 0 ]; do\n if [ \"$1\" = '--gtpack-out' ]; then\n shift\n out=\"$1\"\n fi\n shift\ndone\ntouch \"$out\"\n",
113 args_log.display()
114 ),
115 )
116 .expect("script");
117 let mut perms = fs::metadata(&script).expect("metadata").permissions();
118 perms.set_mode(0o755);
119 fs::set_permissions(&script, perms).expect("chmod");
120
121 let (_temp, gtpack) =
122 build_pack_from_source_with_packc(&source, true, script.to_str().expect("script path"))
123 .expect("build should succeed");
124
125 let logged = fs::read_to_string(&args_log).expect("args log");
126 assert!(gtpack.exists(), "gtpack should be created by fake builder");
127 assert!(logged.contains("build"));
128 assert!(logged.contains("--component-data"));
129 assert!(logged.contains("--log"));
130 assert!(logged.contains("info"));
131 }
132
133 #[test]
134 fn build_pack_from_source_surfaces_command_failures() {
135 let dir = tempdir().expect("tempdir");
136 let source = dir.path().join("pack-src");
137 fs::create_dir_all(&source).expect("source dir");
138
139 let script = dir.path().join("fail-packc.sh");
140 fs::write(&script, "#!/bin/sh\nexit 7\n").expect("script");
141 let mut perms = fs::metadata(&script).expect("metadata").permissions();
142 perms.set_mode(0o755);
143 fs::set_permissions(&script, perms).expect("chmod");
144
145 let err = build_pack_from_source_with_packc(
146 &source,
147 false,
148 script.to_str().expect("script path"),
149 )
150 .expect_err("failing build should error");
151 assert!(err.to_string().contains("packc build failed with status"));
152 }
153}