1use std::collections::BTreeMap;
29
30use indexmap::IndexMap;
31use serde::{Deserialize, Serialize};
32
33pub const SCHEMA_VERSION: u32 = 2;
36
37pub mod coarse {
42 use indexmap::IndexMap;
46 use serde::{Deserialize, Serialize};
47
48 pub const SCHEMA_VERSION: u32 = 1;
49
50 #[derive(Clone, Debug, Serialize, Deserialize, gen_macros::SpecShape)]
51 #[spec(
52 args = "PackageArgs",
53 quirk = "crate::quirks::GomodQuirk",
54 args_field = "args",
55 root_field = "root_package",
56 members_field = "workspace_members",
57 crates_field = "packages"
58 )]
59 pub struct BuildSpec {
60 pub version: u32,
61 pub packages: IndexMap<String, PackageSpec>,
62 pub root_package: String,
63 pub workspace_members: Vec<String>,
64 }
65
66 #[derive(Clone, Debug, Serialize, Deserialize)]
67 pub struct PackageSpec {
68 pub name: String,
69 pub version: String,
70 pub args: PackageArgs,
71 #[serde(default, skip_serializing_if = "Vec::is_empty")]
72 pub quirks: Vec<crate::quirks::GomodQuirk>,
73 }
74
75 #[derive(Clone, Debug, Default, Serialize, Deserialize)]
79 pub struct PackageArgs {
80 pub pname: Option<String>,
81 pub version: Option<String>,
82 #[serde(rename = "vendorHash", skip_serializing_if = "Option::is_none")]
83 pub vendor_hash: Option<String>,
84 #[serde(rename = "proxyVendor", skip_serializing_if = "Option::is_none")]
85 pub proxy_vendor: Option<bool>,
86 #[serde(skip_serializing_if = "Vec::is_empty")]
87 pub tags: Vec<String>,
88 #[serde(skip_serializing_if = "Vec::is_empty")]
89 pub ldflags: Vec<String>,
90 #[serde(rename = "subPackages", skip_serializing_if = "Vec::is_empty")]
91 pub sub_packages: Vec<String>,
92 #[serde(rename = "doCheck", skip_serializing_if = "Option::is_none")]
93 pub do_check: Option<bool>,
94 #[serde(skip_serializing_if = "IndexMap::is_empty")]
95 pub env: IndexMap<String, String>,
96 #[serde(rename = "nativeBuildInputs", skip_serializing_if = "Vec::is_empty")]
97 pub native_build_inputs: Vec<String>,
98 #[serde(rename = "buildInputs", skip_serializing_if = "Vec::is_empty")]
99 pub build_inputs: Vec<String>,
100 }
101}
102
103#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
111pub struct TargetTuple {
112 pub goos: String,
113 pub goarch: String,
114 #[serde(default, skip_serializing_if = "Vec::is_empty")]
117 pub tags: Vec<String>,
118}
119
120impl TargetTuple {
121 #[must_use]
122 pub fn new(goos: impl Into<String>, goarch: impl Into<String>, mut tags: Vec<String>) -> Self {
123 tags.sort();
124 tags.dedup();
125 Self { goos: goos.into(), goarch: goarch.into(), tags }
126 }
127
128 #[must_use]
132 pub fn host() -> Self {
133 let goos = match std::env::consts::OS {
134 "macos" => "darwin",
135 other => other, };
137 let goarch = rust_arch_to_go(std::env::consts::ARCH);
138 Self::new(goos, goarch, Vec::new())
139 }
140
141 #[must_use]
146 pub fn from_rust_triple(triple: &str) -> Option<Self> {
147 let parts: Vec<&str> = triple.split('-').collect();
148 let arch = parts.first()?;
149 let goos = if triple.contains("darwin") || triple.contains("apple") {
150 "darwin"
151 } else if triple.contains("linux") {
152 "linux"
153 } else if triple.contains("windows") {
154 "windows"
155 } else {
156 return None;
157 };
158 Some(Self::new(goos, rust_arch_to_go(arch), Vec::new()))
159 }
160
161 #[must_use]
165 pub fn suffix(&self) -> String {
166 if self.tags.is_empty() {
167 format!("#{}-{}", self.goos, self.goarch)
168 } else {
169 format!("#{}-{}+{}", self.goos, self.goarch, self.tags.join(","))
170 }
171 }
172}
173
174fn rust_arch_to_go(arch: &str) -> &str {
177 match arch {
178 "aarch64" => "arm64",
179 "x86_64" => "amd64",
180 "x86" | "i686" => "386",
181 "powerpc64" => "ppc64le",
182 other => other,
183 }
184}
185
186#[must_use]
190pub fn node_key(import_path: &str, kind: PackageKind, tuple: &TargetTuple) -> String {
191 let base = if kind.is_std() {
192 format!("std/{import_path}")
193 } else {
194 import_path.to_string()
195 };
196 format!("{base}{}", tuple.suffix())
197}
198
199#[derive(Clone, Debug, Serialize, Deserialize, gen_macros::SpecShape)]
204#[spec(
205 args = "GoPackageArgs",
206 quirk = "crate::quirks::GomodQuirk",
207 args_field = "args",
208 root_field = "root_package",
209 members_field = "workspace_members",
210 crates_field = "packages"
211)]
212pub struct BuildSpec {
213 pub version: u32,
214 pub renderer: Renderer,
217 pub module: ModuleSpec,
218
219 #[serde(default)]
222 pub packages: BTreeMap<String, PackageSpec>,
223
224 pub root_package: String,
226 #[serde(default)]
228 pub workspace_members: Vec<String>,
229
230 #[serde(default, skip_serializing_if = "Option::is_none")]
234 pub target_resolves: Option<GoCompactTargetResolves>,
235
236 #[serde(default, skip_serializing_if = "Option::is_none")]
241 pub go_sum_sha256: Option<String>,
242}
243
244#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, gen_macros::IsVariant)]
245#[serde(rename_all = "kebab-case")]
246pub enum Renderer {
247 Coarse,
248 Incremental,
249}
250
251#[derive(Clone, Debug, Serialize, Deserialize)]
252pub struct ModuleSpec {
253 pub module_path: String,
255 pub go_version: String,
257 #[serde(default, skip_serializing_if = "Option::is_none")]
259 pub toolchain: Option<String>,
260 pub has_external_deps: bool,
263 pub dep_mode: DepMode,
265 #[serde(rename = "vendorHash", default, skip_serializing_if = "Option::is_none")]
268 pub vendor_hash: Option<String>,
269}
270
271#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, gen_macros::IsVariant)]
272#[serde(rename_all = "kebab-case")]
273pub enum DepMode {
274 Vendored,
275 Proxy,
276}
277
278#[derive(Clone, Debug, Serialize, Deserialize)]
280pub struct PackageSpec {
281 pub import_path: String,
284 pub kind: PackageKind,
286 pub source: PackageSource,
288 #[serde(default)]
293 pub tree: BuildTree,
294 pub go_files: Vec<String>,
298 #[serde(default, skip_serializing_if = "Vec::is_empty")]
301 pub build_tags: Vec<String>,
302 #[serde(default, skip_serializing_if = "EmbedSpec::is_empty")]
304 pub embed: EmbedSpec,
305 #[serde(default)]
308 pub imports: Vec<String>,
309 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
313 pub import_map: BTreeMap<String, String>,
314 #[serde(default, skip_serializing_if = "Option::is_none")]
316 pub module: Option<PackageModuleRef>,
317 pub source_hash: String,
322 pub args: GoPackageArgs,
323 #[serde(default, skip_serializing_if = "Vec::is_empty")]
324 pub quirks: Vec<crate::quirks::GomodQuirk>,
325}
326
327#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, gen_macros::IsVariant)]
328#[serde(rename_all = "kebab-case")]
329pub enum PackageKind {
330 Std,
331 Module,
332 Main,
333 }
335
336#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, gen_macros::IsVariant)]
340#[serde(rename_all = "kebab-case")]
341pub enum BuildTree {
342 #[default]
344 Target,
345 Host,
347}
348
349#[derive(Clone, Debug, Serialize, Deserialize)]
350#[serde(tag = "kind", rename_all = "kebab-case")]
351pub enum PackageSource {
352 Vendored { relative_path: String },
357 Std,
359 }
361
362impl PackageSource {
363 #[must_use]
364 pub fn is_std(&self) -> bool {
365 matches!(self, PackageSource::Std)
366 }
367}
368
369#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
370pub struct EmbedSpec {
371 #[serde(default, skip_serializing_if = "Vec::is_empty")]
372 pub patterns: Vec<String>,
373 #[serde(default, skip_serializing_if = "Vec::is_empty")]
374 pub files: Vec<String>,
375}
376
377impl EmbedSpec {
378 #[must_use]
379 pub fn is_empty(&self) -> bool {
380 self.patterns.is_empty() && self.files.is_empty()
381 }
382}
383
384#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
388pub struct PackageModuleRef {
389 pub path: String,
390 #[serde(default, skip_serializing_if = "Option::is_none")]
391 pub version: Option<String>,
392}
393
394#[derive(Clone, Debug, Default, Serialize, Deserialize)]
397pub struct GoPackageArgs {
398 #[serde(default, skip_serializing_if = "Vec::is_empty")]
400 pub gcflags: Vec<String>,
401 #[serde(default, skip_serializing_if = "Vec::is_empty")]
404 pub ldflags: Vec<String>,
405 #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
407 pub env: IndexMap<String, String>,
408}
409
410#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
419pub struct GoTargetEdges {
420 #[serde(default, skip_serializing_if = "Vec::is_empty")]
421 pub imports: Vec<String>,
422 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
423 pub import_map: BTreeMap<String, String>,
424}
425
426#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
429pub struct GoTargetResolve {
430 #[serde(default)]
431 pub packages: BTreeMap<String, GoTargetEdges>,
432}
433
434#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
444pub struct GoCompactTargetResolves {
445 #[serde(default)]
446 pub base: BTreeMap<String, GoTargetEdges>,
447 #[serde(default)]
448 pub targets: BTreeMap<String, GoTargetOverrides>,
449}
450
451#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
452pub struct GoTargetOverrides {
453 #[serde(default)]
454 pub overrides: BTreeMap<String, GoTargetEdges>,
455}
456
457impl GoCompactTargetResolves {
458 #[must_use]
463 pub fn from_full(full: IndexMap<String, GoTargetResolve>) -> Self {
464 let Some((_first, first_resolve)) = full.iter().next() else {
465 return Self::default();
466 };
467
468 let mut base: BTreeMap<String, GoTargetEdges> = BTreeMap::new();
469 for (key, first_edges) in &first_resolve.packages {
470 let present_in_all = full
471 .values()
472 .all(|resolve| resolve.packages.get(key) == Some(first_edges));
473 if present_in_all {
474 base.insert(key.clone(), first_edges.clone());
475 }
476 }
477
478 let mut targets: BTreeMap<String, GoTargetOverrides> = BTreeMap::new();
479 for (tuple, resolve) in &full {
480 let mut overrides: BTreeMap<String, GoTargetEdges> = BTreeMap::new();
481 for (key, edges) in &resolve.packages {
482 if !base.contains_key(key) {
483 overrides.insert(key.clone(), edges.clone());
484 }
485 }
486 targets.insert(tuple.clone(), GoTargetOverrides { overrides });
487 }
488
489 Self { base, targets }
490 }
491
492 #[must_use]
495 pub fn expand(&self) -> IndexMap<String, GoTargetResolve> {
496 let mut out: IndexMap<String, GoTargetResolve> = IndexMap::new();
497 for (tuple, over) in &self.targets {
498 let mut packages = self.base.clone();
499 for (key, edges) in &over.overrides {
500 packages.insert(key.clone(), edges.clone());
501 }
502 out.insert(tuple.clone(), GoTargetResolve { packages });
503 }
504 out
505 }
506}
507
508#[must_use]
518pub fn source_hash(entries: &[(String, Vec<u8>)]) -> String {
519 let mut sorted: Vec<&(String, Vec<u8>)> = entries.iter().collect();
520 sorted.sort_by(|a, b| a.0.cmp(&b.0));
521 let mut hasher = blake3::Hasher::new();
522 for (path, bytes) in sorted {
523 hasher.update(&(path.len() as u64).to_le_bytes());
524 hasher.update(path.as_bytes());
525 hasher.update(&(bytes.len() as u64).to_le_bytes());
526 hasher.update(bytes);
527 }
528 hasher.finalize().to_hex().to_string()
529}
530
531#[must_use]
535pub fn go_sum_sha256(bytes: &[u8]) -> String {
536 use sha2::{Digest, Sha256};
537 let mut hasher = Sha256::new();
538 hasher.update(bytes);
539 format!("{:x}", hasher.finalize())
540}
541
542#[cfg(test)]
543mod tests {
544 use super::*;
545
546 #[test]
547 fn tuple_suffix_is_canonical_and_tag_sorted() {
548 let t = TargetTuple::new("linux", "amd64", vec!["osusergo".into(), "netgo".into()]);
549 assert_eq!(t.suffix(), "#linux-amd64+netgo,osusergo");
551 let bare = TargetTuple::new("darwin", "arm64", vec![]);
552 assert_eq!(bare.suffix(), "#darwin-arm64");
553 }
554
555 #[test]
556 fn node_key_prefixes_std() {
557 let t = TargetTuple::new("linux", "amd64", vec![]);
558 assert_eq!(node_key("fmt", PackageKind::Std, &t), "std/fmt#linux-amd64");
559 assert_eq!(
560 node_key("example.com/x/cmd/a", PackageKind::Main, &t),
561 "example.com/x/cmd/a#linux-amd64"
562 );
563 }
564
565 #[test]
566 fn source_hash_is_order_independent_and_content_sensitive() {
567 let a = vec![("b.go".to_string(), b"two".to_vec()), ("a.go".to_string(), b"one".to_vec())];
568 let b = vec![("a.go".to_string(), b"one".to_vec()), ("b.go".to_string(), b"two".to_vec())];
569 assert_eq!(source_hash(&a), source_hash(&b), "path order must not change the hash");
570 let c = vec![("a.go".to_string(), b"one!".to_vec()), ("b.go".to_string(), b"two".to_vec())];
571 assert_ne!(source_hash(&a), source_hash(&c), "a one-byte edit must change the hash");
572 }
573
574 #[test]
575 fn empty_go_sum_is_the_canonical_empty_hash() {
576 assert_eq!(
577 go_sum_sha256(b""),
578 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
579 );
580 }
581
582 #[test]
583 fn compact_resolves_round_trip_lossless() {
584 let mut pkgs = BTreeMap::new();
585 pkgs.insert(
586 "example.com/x/a#linux-amd64".to_string(),
587 GoTargetEdges { imports: vec!["std/fmt#linux-amd64".into()], import_map: BTreeMap::new() },
588 );
589 let mut full: IndexMap<String, GoTargetResolve> = IndexMap::new();
590 full.insert("#linux-amd64".to_string(), GoTargetResolve { packages: pkgs.clone() });
591 let compact = GoCompactTargetResolves::from_full(full.clone());
592 assert_eq!(compact.expand(), full, "from_full ∘ expand must be identity");
593 }
594}