use std::collections::BTreeMap;
use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
pub const SCHEMA_VERSION: u32 = 2;
pub mod coarse {
use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
pub const SCHEMA_VERSION: u32 = 1;
#[derive(Clone, Debug, Serialize, Deserialize, gen_macros::SpecShape)]
#[spec(
args = "PackageArgs",
quirk = "crate::quirks::GomodQuirk",
args_field = "args",
root_field = "root_package",
members_field = "workspace_members",
crates_field = "packages"
)]
pub struct BuildSpec {
pub version: u32,
pub packages: IndexMap<String, PackageSpec>,
pub root_package: String,
pub workspace_members: Vec<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct PackageSpec {
pub name: String,
pub version: String,
pub args: PackageArgs,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub quirks: Vec<crate::quirks::GomodQuirk>,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct PackageArgs {
pub pname: Option<String>,
pub version: Option<String>,
#[serde(rename = "vendorHash", skip_serializing_if = "Option::is_none")]
pub vendor_hash: Option<String>,
#[serde(rename = "proxyVendor", skip_serializing_if = "Option::is_none")]
pub proxy_vendor: Option<bool>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub tags: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub ldflags: Vec<String>,
#[serde(rename = "subPackages", skip_serializing_if = "Vec::is_empty")]
pub sub_packages: Vec<String>,
#[serde(rename = "doCheck", skip_serializing_if = "Option::is_none")]
pub do_check: Option<bool>,
#[serde(skip_serializing_if = "IndexMap::is_empty")]
pub env: IndexMap<String, String>,
#[serde(rename = "nativeBuildInputs", skip_serializing_if = "Vec::is_empty")]
pub native_build_inputs: Vec<String>,
#[serde(rename = "buildInputs", skip_serializing_if = "Vec::is_empty")]
pub build_inputs: Vec<String>,
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct TargetTuple {
pub goos: String,
pub goarch: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tags: Vec<String>,
}
impl TargetTuple {
#[must_use]
pub fn new(goos: impl Into<String>, goarch: impl Into<String>, mut tags: Vec<String>) -> Self {
tags.sort();
tags.dedup();
Self { goos: goos.into(), goarch: goarch.into(), tags }
}
#[must_use]
pub fn host() -> Self {
let goos = match std::env::consts::OS {
"macos" => "darwin",
other => other, };
let goarch = rust_arch_to_go(std::env::consts::ARCH);
Self::new(goos, goarch, Vec::new())
}
#[must_use]
pub fn from_rust_triple(triple: &str) -> Option<Self> {
let parts: Vec<&str> = triple.split('-').collect();
let arch = parts.first()?;
let goos = if triple.contains("darwin") || triple.contains("apple") {
"darwin"
} else if triple.contains("linux") {
"linux"
} else if triple.contains("windows") {
"windows"
} else {
return None;
};
Some(Self::new(goos, rust_arch_to_go(arch), Vec::new()))
}
#[must_use]
pub fn suffix(&self) -> String {
if self.tags.is_empty() {
format!("#{}-{}", self.goos, self.goarch)
} else {
format!("#{}-{}+{}", self.goos, self.goarch, self.tags.join(","))
}
}
}
fn rust_arch_to_go(arch: &str) -> &str {
match arch {
"aarch64" => "arm64",
"x86_64" => "amd64",
"x86" | "i686" => "386",
"powerpc64" => "ppc64le",
other => other,
}
}
#[must_use]
pub fn node_key(import_path: &str, kind: PackageKind, tuple: &TargetTuple) -> String {
let base = if kind.is_std() {
format!("std/{import_path}")
} else {
import_path.to_string()
};
format!("{base}{}", tuple.suffix())
}
#[derive(Clone, Debug, Serialize, Deserialize, gen_macros::SpecShape)]
#[spec(
args = "GoPackageArgs",
quirk = "crate::quirks::GomodQuirk",
args_field = "args",
root_field = "root_package",
members_field = "workspace_members",
crates_field = "packages"
)]
pub struct BuildSpec {
pub version: u32,
pub renderer: Renderer,
pub module: ModuleSpec,
#[serde(default)]
pub packages: BTreeMap<String, PackageSpec>,
pub root_package: String,
#[serde(default)]
pub workspace_members: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub target_resolves: Option<GoCompactTargetResolves>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub go_sum_sha256: Option<String>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, gen_macros::IsVariant)]
#[serde(rename_all = "kebab-case")]
pub enum Renderer {
Coarse,
Incremental,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ModuleSpec {
pub module_path: String,
pub go_version: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub toolchain: Option<String>,
pub has_external_deps: bool,
pub dep_mode: DepMode,
#[serde(rename = "vendorHash", default, skip_serializing_if = "Option::is_none")]
pub vendor_hash: Option<String>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, gen_macros::IsVariant)]
#[serde(rename_all = "kebab-case")]
pub enum DepMode {
Vendored,
Proxy,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct PackageSpec {
pub import_path: String,
pub kind: PackageKind,
pub source: PackageSource,
#[serde(default)]
pub tree: BuildTree,
pub go_files: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub build_tags: Vec<String>,
#[serde(default, skip_serializing_if = "EmbedSpec::is_empty")]
pub embed: EmbedSpec,
#[serde(default)]
pub imports: Vec<String>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub import_map: BTreeMap<String, String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub module: Option<PackageModuleRef>,
pub source_hash: String,
pub args: GoPackageArgs,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub quirks: Vec<crate::quirks::GomodQuirk>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, gen_macros::IsVariant)]
#[serde(rename_all = "kebab-case")]
pub enum PackageKind {
Std,
Module,
Main,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, gen_macros::IsVariant)]
#[serde(rename_all = "kebab-case")]
pub enum BuildTree {
#[default]
Target,
Host,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "kebab-case")]
pub enum PackageSource {
Vendored { relative_path: String },
Std,
}
impl PackageSource {
#[must_use]
pub fn is_std(&self) -> bool {
matches!(self, PackageSource::Std)
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct EmbedSpec {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub patterns: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub files: Vec<String>,
}
impl EmbedSpec {
#[must_use]
pub fn is_empty(&self) -> bool {
self.patterns.is_empty() && self.files.is_empty()
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PackageModuleRef {
pub path: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub version: Option<String>,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct GoPackageArgs {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub gcflags: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub ldflags: Vec<String>,
#[serde(default, skip_serializing_if = "IndexMap::is_empty")]
pub env: IndexMap<String, String>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct GoTargetEdges {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub imports: Vec<String>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub import_map: BTreeMap<String, String>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct GoTargetResolve {
#[serde(default)]
pub packages: BTreeMap<String, GoTargetEdges>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct GoCompactTargetResolves {
#[serde(default)]
pub base: BTreeMap<String, GoTargetEdges>,
#[serde(default)]
pub targets: BTreeMap<String, GoTargetOverrides>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct GoTargetOverrides {
#[serde(default)]
pub overrides: BTreeMap<String, GoTargetEdges>,
}
impl GoCompactTargetResolves {
#[must_use]
pub fn from_full(full: IndexMap<String, GoTargetResolve>) -> Self {
let Some((_first, first_resolve)) = full.iter().next() else {
return Self::default();
};
let mut base: BTreeMap<String, GoTargetEdges> = BTreeMap::new();
for (key, first_edges) in &first_resolve.packages {
let present_in_all = full
.values()
.all(|resolve| resolve.packages.get(key) == Some(first_edges));
if present_in_all {
base.insert(key.clone(), first_edges.clone());
}
}
let mut targets: BTreeMap<String, GoTargetOverrides> = BTreeMap::new();
for (tuple, resolve) in &full {
let mut overrides: BTreeMap<String, GoTargetEdges> = BTreeMap::new();
for (key, edges) in &resolve.packages {
if !base.contains_key(key) {
overrides.insert(key.clone(), edges.clone());
}
}
targets.insert(tuple.clone(), GoTargetOverrides { overrides });
}
Self { base, targets }
}
#[must_use]
pub fn expand(&self) -> IndexMap<String, GoTargetResolve> {
let mut out: IndexMap<String, GoTargetResolve> = IndexMap::new();
for (tuple, over) in &self.targets {
let mut packages = self.base.clone();
for (key, edges) in &over.overrides {
packages.insert(key.clone(), edges.clone());
}
out.insert(tuple.clone(), GoTargetResolve { packages });
}
out
}
}
#[must_use]
pub fn source_hash(entries: &[(String, Vec<u8>)]) -> String {
let mut sorted: Vec<&(String, Vec<u8>)> = entries.iter().collect();
sorted.sort_by(|a, b| a.0.cmp(&b.0));
let mut hasher = blake3::Hasher::new();
for (path, bytes) in sorted {
hasher.update(&(path.len() as u64).to_le_bytes());
hasher.update(path.as_bytes());
hasher.update(&(bytes.len() as u64).to_le_bytes());
hasher.update(bytes);
}
hasher.finalize().to_hex().to_string()
}
#[must_use]
pub fn go_sum_sha256(bytes: &[u8]) -> String {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(bytes);
format!("{:x}", hasher.finalize())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tuple_suffix_is_canonical_and_tag_sorted() {
let t = TargetTuple::new("linux", "amd64", vec!["osusergo".into(), "netgo".into()]);
assert_eq!(t.suffix(), "#linux-amd64+netgo,osusergo");
let bare = TargetTuple::new("darwin", "arm64", vec![]);
assert_eq!(bare.suffix(), "#darwin-arm64");
}
#[test]
fn node_key_prefixes_std() {
let t = TargetTuple::new("linux", "amd64", vec![]);
assert_eq!(node_key("fmt", PackageKind::Std, &t), "std/fmt#linux-amd64");
assert_eq!(
node_key("example.com/x/cmd/a", PackageKind::Main, &t),
"example.com/x/cmd/a#linux-amd64"
);
}
#[test]
fn source_hash_is_order_independent_and_content_sensitive() {
let a = vec![("b.go".to_string(), b"two".to_vec()), ("a.go".to_string(), b"one".to_vec())];
let b = vec![("a.go".to_string(), b"one".to_vec()), ("b.go".to_string(), b"two".to_vec())];
assert_eq!(source_hash(&a), source_hash(&b), "path order must not change the hash");
let c = vec![("a.go".to_string(), b"one!".to_vec()), ("b.go".to_string(), b"two".to_vec())];
assert_ne!(source_hash(&a), source_hash(&c), "a one-byte edit must change the hash");
}
#[test]
fn empty_go_sum_is_the_canonical_empty_hash() {
assert_eq!(
go_sum_sha256(b""),
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
);
}
#[test]
fn compact_resolves_round_trip_lossless() {
let mut pkgs = BTreeMap::new();
pkgs.insert(
"example.com/x/a#linux-amd64".to_string(),
GoTargetEdges { imports: vec!["std/fmt#linux-amd64".into()], import_map: BTreeMap::new() },
);
let mut full: IndexMap<String, GoTargetResolve> = IndexMap::new();
full.insert("#linux-amd64".to_string(), GoTargetResolve { packages: pkgs.clone() });
let compact = GoCompactTargetResolves::from_full(full.clone());
assert_eq!(compact.expand(), full, "from_full ∘ expand must be identity");
}
}