1use std::collections::HashMap;
21use std::path::{Path, PathBuf};
22use std::process::Command;
23
24use indexmap::IndexMap;
25
26use crate::build_spec::{
27 self, BuildSpec, BuildTree, DepMode, EmbedSpec, GoCompactTargetResolves, GoPackageArgs,
28 GoTargetEdges, GoTargetResolve, ModuleSpec, PackageKind, PackageModuleRef, PackageSource,
29 PackageSpec, Renderer, TargetTuple, SCHEMA_VERSION,
30};
31use crate::error::GomodError;
32use crate::golist::parse_stream;
33use crate::gomod::GoMod;
34
35pub trait GoBuildEnv {
38 fn go_list(&self, root: &Path, tuple: &TargetTuple) -> Result<String, GomodError>;
42
43 fn read_file(&self, path: &Path) -> Result<Vec<u8>, GomodError>;
46}
47
48pub struct RealGoBuildEnv {
54 pub mod_mode: String,
55}
56
57impl Default for RealGoBuildEnv {
58 fn default() -> Self {
59 Self { mod_mode: "vendor".to_string() }
60 }
61}
62
63impl GoBuildEnv for RealGoBuildEnv {
64 fn go_list(&self, root: &Path, tuple: &TargetTuple) -> Result<String, GomodError> {
65 let mut cmd = Command::new("go");
66 cmd.arg("list").arg("-deps").arg("-json");
67 if !tuple.tags.is_empty() {
68 cmd.arg("-tags").arg(tuple.tags.join(","));
69 }
70 cmd.arg("./...");
71 cmd.current_dir(root)
72 .env("GOOS", &tuple.goos)
73 .env("GOARCH", &tuple.goarch)
74 .env("CGO_ENABLED", "0")
75 .env("GOFLAGS", format!("-mod={}", self.mod_mode))
76 .env("GOPROXY", "off")
77 .env("GIT_TERMINAL_PROMPT", "0");
79 let out = cmd
80 .output()
81 .map_err(|e| GomodError::GoList(format!("spawn `go list`: {e}")))?;
82 if !out.status.success() {
83 return Err(GomodError::GoList(format!(
84 "`go list` exited {}: {}",
85 out.status,
86 String::from_utf8_lossy(&out.stderr).trim()
87 )));
88 }
89 Ok(String::from_utf8_lossy(&out.stdout).into_owned())
90 }
91
92 fn read_file(&self, path: &Path) -> Result<Vec<u8>, GomodError> {
93 std::fs::read(path).map_err(|source| GomodError::Io { path: path.to_path_buf(), source })
94 }
95}
96
97#[derive(Clone, Debug)]
99pub struct EncodeCtx {
100 pub root: PathBuf,
101 pub tuple: TargetTuple,
102}
103
104pub fn apply(env: &dyn GoBuildEnv, ctx: &EncodeCtx) -> Result<BuildSpec, GomodError> {
108 let gomod_path = ctx.root.join("go.mod");
110 let gomod_bytes = env
111 .read_file(&gomod_path)
112 .map_err(|_| GomodError::ManifestNotFound(gomod_path.clone()))?;
113 let gomod_text = String::from_utf8_lossy(&gomod_bytes);
114 let module: ModuleSpec = GoMod::parse(&gomod_text).to_module_spec(DepMode::Vendored, None);
115
116 let json = env.go_list(&ctx.root, &ctx.tuple)?;
118 let listed = parse_stream(&json)?;
119
120 for p in &listed {
122 if !p.standard && !p.cgo_files.is_empty() {
123 return Err(GomodError::Interp {
124 phase: "reject-cgo",
125 detail: format!(
126 "package `{}` has cgo sources ({} file(s)); cgo nodes are deferred to M-cgo — \
127 an M1 (incremental, vendored) build must be a cgo-free subgraph",
128 p.import_path,
129 p.cgo_files.len()
130 ),
131 });
132 }
133 if !p.standard && !p.s_files.is_empty() {
134 return Err(GomodError::Interp {
135 phase: "reject-asm",
136 detail: format!(
137 "package `{}` has assembly sources; per-node asm is deferred to M-asm \
138 (std asm lives inside the opaque std-tree)",
139 p.import_path
140 ),
141 });
142 }
143 }
144
145 let kind_of: HashMap<&str, PackageKind> =
147 listed.iter().map(|p| (p.import_path.as_str(), p.kind())).collect();
148
149 let mut packages: std::collections::BTreeMap<String, PackageSpec> =
150 std::collections::BTreeMap::new();
151 let mut resolve: std::collections::BTreeMap<String, GoTargetEdges> =
152 std::collections::BTreeMap::new();
153 let mut mains: Vec<String> = Vec::new();
154
155 for p in &listed {
156 let kind = p.kind();
157 let key = build_spec::node_key(&p.import_path, kind, &ctx.tuple);
158
159 let mut import_keys: Vec<String> = Vec::with_capacity(p.imports.len());
163 for imp in &p.imports {
164 let actual = p.import_map.get(imp).map_or(imp.as_str(), String::as_str);
165 let ik = kind_of.get(actual).copied().unwrap_or(PackageKind::Module);
169 import_keys.push(build_spec::node_key(actual, ik, &ctx.tuple));
170 }
171 import_keys.sort();
172 import_keys.dedup();
173
174 let (source, source_hash) = if kind.is_std() {
176 (PackageSource::Std, String::new())
179 } else {
180 let rel = relative_under(&p.dir, &p.root)
181 .or_else(|| relative_under(&p.dir, &ctx.root.to_string_lossy()))
182 .ok_or_else(|| GomodError::Interp {
183 phase: "relative-path",
184 detail: format!(
185 "package dir `{}` is not under module root `{}`",
186 p.dir, p.root
187 ),
188 })?;
189 let mut entries: Vec<(String, Vec<u8>)> = Vec::new();
190 for f in p.go_files.iter().chain(p.embed_files.iter()) {
191 let abs = Path::new(&p.dir).join(f);
192 let bytes = env.read_file(&abs).map_err(|e| GomodError::Interp {
193 phase: "read-source",
194 detail: format!("{}: {e}", abs.display()),
195 })?;
196 entries.push((f.clone(), bytes));
197 }
198 (
199 PackageSource::Vendored { relative_path: rel },
200 build_spec::source_hash(&entries),
201 )
202 };
203
204 let mut env_map: IndexMap<String, String> = IndexMap::new();
205 env_map.insert("GOOS".to_string(), ctx.tuple.goos.clone());
206 env_map.insert("GOARCH".to_string(), ctx.tuple.goarch.clone());
207 env_map.insert("CGO_ENABLED".to_string(), "0".to_string());
208
209 let spec = PackageSpec {
210 import_path: p.import_path.clone(),
211 kind,
212 source,
213 tree: tree_for(kind),
214 go_files: p.go_files.clone(),
215 build_tags: ctx.tuple.tags.clone(),
216 embed: EmbedSpec {
217 patterns: p.embed_patterns.clone(),
218 files: p.embed_files.clone(),
219 },
220 imports: import_keys.clone(),
221 import_map: p.import_map.clone(),
222 module: p.module.as_ref().map(|m| PackageModuleRef {
223 path: m.path.clone(),
224 version: m.version.clone(),
225 }),
226 source_hash,
227 args: GoPackageArgs { gcflags: Vec::new(), ldflags: Vec::new(), env: env_map },
228 quirks: Vec::new(),
229 };
230
231 resolve.insert(
232 key.clone(),
233 GoTargetEdges { imports: import_keys, import_map: p.import_map.clone() },
234 );
235 if matches!(kind, PackageKind::Main) && !p.dep_only {
236 mains.push(key.clone());
237 }
238 packages.insert(key, spec);
239 }
240
241 mains.sort();
244 mains.dedup();
245 let root_package = mains
246 .first()
247 .cloned()
248 .or_else(|| packages.keys().next().cloned())
249 .unwrap_or_default();
250 let workspace_members = mains;
251
252 let mut full: IndexMap<String, GoTargetResolve> = IndexMap::new();
254 full.insert(ctx.tuple.suffix(), GoTargetResolve { packages: resolve });
255 let target_resolves = Some(GoCompactTargetResolves::from_full(full));
256
257 let go_sum_bytes = env.read_file(&ctx.root.join("go.sum")).unwrap_or_default();
260 let go_sum_sha256 = Some(build_spec::go_sum_sha256(&go_sum_bytes));
261
262 Ok(BuildSpec {
263 version: SCHEMA_VERSION,
264 renderer: Renderer::Incremental,
265 module,
266 packages,
267 root_package,
268 workspace_members,
269 target_resolves,
270 go_sum_sha256,
271 })
272}
273
274fn tree_for(kind: PackageKind) -> BuildTree {
278 match kind {
279 PackageKind::Std | PackageKind::Module | PackageKind::Main => BuildTree::Target,
280 }
281}
282
283fn relative_under(dir: &str, root: &str) -> Option<String> {
286 let root = root.trim_end_matches('/');
287 if dir == root {
288 return Some(".".to_string());
289 }
290 dir.strip_prefix(&format!("{root}/")).map(str::to_string)
291}