use std::collections::HashMap;
use std::path::PathBuf;
use serde::Serialize;
use super::kind::{ArtifactKind, is_derived_sidecar_kind, is_uploadable};
#[derive(Debug, Clone, Serialize)]
pub struct Artifact {
pub kind: ArtifactKind,
pub path: PathBuf,
pub name: String,
pub target: Option<String>,
pub crate_name: String,
#[serde(serialize_with = "serialize_metadata_sorted")]
pub metadata: HashMap<String, String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub size: Option<u64>,
}
const METADATA_HASH_KEYS: &[&str] = &[
"Checksum", "sha256", "sha512", "sha384", "sha224", "sha1", "md5", "blake2b", "blake3", "crc32",
];
fn serialize_metadata_sorted<S>(map: &HashMap<String, String>, ser: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use serde::ser::SerializeMap as _;
let sorted: std::collections::BTreeMap<&String, &String> = map
.iter()
.filter(|(k, _)| !METADATA_HASH_KEYS.contains(&k.as_str()))
.collect();
let mut m = ser.serialize_map(Some(sorted.len()))?;
for (k, v) in sorted {
m.serialize_entry(k, v)?;
}
m.end()
}
impl Artifact {
pub fn name(&self) -> &str {
&self.name
}
pub fn goos(&self) -> Option<String> {
self.target.as_ref().map(|t| crate::target::map_target(t).0)
}
pub fn goarch(&self) -> Option<String> {
self.target.as_ref().map(|t| crate::target::map_target(t).1)
}
pub fn only_replacing_unibins(&self) -> bool {
self.metadata.get("replaces").is_none_or(|v| v != "false")
}
pub fn extra_binaries(&self) -> Vec<String> {
self.metadata
.get("extra_binaries")
.map(|v| {
v.split(',')
.filter(|s| !s.is_empty())
.map(|s| s.to_string())
.collect()
})
.unwrap_or_default()
}
pub fn extra_binary(&self) -> Option<String> {
self.metadata.get("binary").cloned()
}
pub fn ext(&self) -> String {
if let Some(ext) = self.metadata.get("ext")
&& !ext.is_empty()
{
return ext.clone();
}
crate::template::extract_artifact_ext(&self.name).to_string()
}
}
#[derive(Debug, Default)]
pub struct ArtifactRegistry {
artifacts: Vec<Artifact>,
}
impl ArtifactRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn add(&mut self, mut artifact: Artifact) {
let name = if artifact.name.is_empty() {
let derived = artifact
.path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("artifact")
.trim()
.to_string();
artifact.name = derived.clone();
derived
} else {
artifact.name.clone()
};
artifact.path = resolve_stored_path(&artifact.path, artifact.kind);
if is_derived_sidecar_kind(artifact.kind)
&& let Some(existing) = self
.artifacts
.iter_mut()
.find(|a| a.kind == artifact.kind && a.path == artifact.path)
{
for (key, value) in artifact.metadata {
existing.metadata.insert(key, value);
}
existing.size = existing.size.or(artifact.size);
return;
}
if is_uploadable(artifact.kind)
&& let Some(existing) = self
.artifacts
.iter()
.find(|a| is_uploadable(a.kind) && a.name == name && a.path != artifact.path)
{
tracing::warn!(
"artifact '{}' already registered at '{}' but re-added from '{}'; \
upload may fail with a duplicate error",
name,
existing.path.display(),
artifact.path.display(),
);
}
self.artifacts.push(artifact);
}
pub fn dedupe_targetless_duplicates(&mut self) {
let mut seen: std::collections::HashSet<PathBuf> = std::collections::HashSet::new();
self.artifacts.retain(|a| {
if a.target.is_some() {
return true;
}
seen.insert(a.path.clone())
});
}
pub fn contains_path_kind(&self, path: &std::path::Path, kind: ArtifactKind) -> bool {
let resolved = resolve_stored_path(path, kind);
self.artifacts
.iter()
.any(|a| a.kind == kind && a.path == resolved)
}
pub fn by_kind(&self, kind: ArtifactKind) -> Vec<&Artifact> {
self.artifacts.iter().filter(|a| a.kind == kind).collect()
}
pub fn by_kind_and_crate(&self, kind: ArtifactKind, crate_name: &str) -> Vec<&Artifact> {
self.artifacts
.iter()
.filter(|a| a.kind == kind && a.crate_name == crate_name)
.collect()
}
pub fn by_kinds_and_crate(&self, kinds: &[ArtifactKind], crate_name: &str) -> Vec<&Artifact> {
self.artifacts
.iter()
.filter(|a| kinds.contains(&a.kind) && a.crate_name == crate_name)
.collect()
}
pub fn binary_like_dedup(&self) -> Vec<&Artifact> {
let uploadable_paths: std::collections::HashSet<&std::path::Path> = self
.artifacts
.iter()
.filter(|a| a.kind == ArtifactKind::UploadableBinary)
.map(|a| a.path.as_path())
.collect();
self.artifacts
.iter()
.filter(|a| {
matches!(
a.kind,
ArtifactKind::Binary
| ArtifactKind::UploadableBinary
| ArtifactKind::UniversalBinary
)
})
.filter(|a| {
a.kind == ArtifactKind::UploadableBinary
|| !uploadable_paths.contains(a.path.as_path())
})
.collect()
}
pub fn all(&self) -> &[Artifact] {
&self.artifacts
}
pub fn all_mut(&mut self) -> &mut [Artifact] {
&mut self.artifacts
}
pub fn filter<F: Fn(&Artifact) -> bool>(&self, predicate: F) -> Vec<&Artifact> {
self.artifacts.iter().filter(|a| predicate(a)).collect()
}
pub fn remove_by_paths(&mut self, paths: &[std::path::PathBuf]) {
self.artifacts.retain(|a| !paths.contains(&a.path));
}
pub fn to_artifacts_json(&self) -> anyhow::Result<serde_json::Value> {
let mut sorted: Vec<&Artifact> = self.artifacts.iter().collect();
sorted.sort_by(|a, b| {
(
a.kind.as_str(),
a.target.as_deref().unwrap_or(""),
a.crate_name.as_str(),
a.name.as_str(),
a.path.as_path(),
)
.cmp(&(
b.kind.as_str(),
b.target.as_deref().unwrap_or(""),
b.crate_name.as_str(),
b.name.as_str(),
b.path.as_path(),
))
});
let mut val = serde_json::to_value(&sorted)?;
if let Some(arr) = val.as_array_mut() {
for entry in arr {
if let Some(path) = entry
.get("path")
.and_then(|p| p.as_str())
.map(crate::util::normalize_path_separators)
{
entry["path"] = serde_json::Value::String(path);
}
}
}
Ok(val)
}
}
fn resolve_stored_path(path: &std::path::Path, kind: ArtifactKind) -> PathBuf {
let mut path = path.to_path_buf();
if should_relativize_path(kind)
&& path.is_absolute()
&& let Ok(cwd) = std::env::current_dir()
&& cwd.parent().is_some()
&& let Ok(rel) = path.strip_prefix(&cwd)
{
path = rel.to_path_buf();
}
PathBuf::from(crate::util::normalize_path_separators(
&path.to_string_lossy(),
))
}
fn should_relativize_path(kind: ArtifactKind) -> bool {
!matches!(
kind,
ArtifactKind::DockerImage
| ArtifactKind::DockerImageV2
| ArtifactKind::PublishableDockerImage
| ArtifactKind::DockerManifest
| ArtifactKind::DockerDigest
)
}