use std::collections::HashMap;
use schemars::JsonSchema;
use serde::{Deserialize, Deserializer, Serialize};
use super::{
ArchiveHooksConfig, SignConfig, StringOrBool, StringOrU32, deserialize_string_or_bool_opt,
};
#[derive(Debug, Clone, JsonSchema)]
pub enum ArchivesConfig {
Disabled,
Configs(Vec<ArchiveConfig>),
}
impl Serialize for ArchivesConfig {
fn serialize<S: serde::Serializer>(
&self,
serializer: S,
) -> std::result::Result<S::Ok, S::Error> {
match self {
ArchivesConfig::Disabled => serializer.serialize_bool(false),
ArchivesConfig::Configs(configs) => configs.serialize(serializer),
}
}
}
impl Default for ArchivesConfig {
fn default() -> Self {
ArchivesConfig::Configs(vec![])
}
}
pub(super) fn deserialize_archives_config<'de, D>(
deserializer: D,
) -> Result<ArchivesConfig, D::Error>
where
D: Deserializer<'de>,
{
use serde::de::{self, Visitor};
struct ArchivesVisitor;
impl<'de> Visitor<'de> for ArchivesVisitor {
type Value = ArchivesConfig;
fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("false or a list of archive configs")
}
fn visit_bool<E: de::Error>(self, v: bool) -> Result<Self::Value, E> {
if !v {
Ok(ArchivesConfig::Disabled)
} else {
Err(E::custom(
"archives: true is not valid; use false or a list",
))
}
}
fn visit_seq<A: de::SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
let mut configs = Vec::new();
while let Some(item) = seq.next_element::<ArchiveConfig>()? {
configs.push(item);
}
Ok(ArchivesConfig::Configs(configs))
}
fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
Ok(ArchivesConfig::Configs(vec![]))
}
fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
Ok(ArchivesConfig::Configs(vec![]))
}
}
deserializer.deserialize_any(ArchivesVisitor)
}
pub(super) fn deserialize_signs<'de, D>(deserializer: D) -> Result<Vec<SignConfig>, D::Error>
where
D: Deserializer<'de>,
{
use serde::de::{self, Visitor};
struct SignsVisitor;
impl<'de> Visitor<'de> for SignsVisitor {
type Value = Vec<SignConfig>;
fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("a sign config object or an array of sign config objects")
}
fn visit_seq<A: de::SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
let mut configs = Vec::new();
while let Some(item) = seq.next_element::<SignConfig>()? {
configs.push(item);
}
Ok(configs)
}
fn visit_map<M: de::MapAccess<'de>>(self, map: M) -> Result<Self::Value, M::Error> {
let config = SignConfig::deserialize(de::value::MapAccessDeserializer::new(map))?;
Ok(vec![config])
}
fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
Ok(Vec::new())
}
fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
Ok(Vec::new())
}
}
deserializer.deserialize_any(SignsVisitor)
}
pub(super) fn deserialize_binary_signs<'de, D>(deserializer: D) -> Result<Vec<SignConfig>, D::Error>
where
D: Deserializer<'de>,
{
let configs = deserialize_signs(deserializer)?;
for (idx, cfg) in configs.iter().enumerate() {
if let Some(art) = cfg.artifacts.as_deref()
&& art != "binary"
&& art != "none"
{
return Err(serde::de::Error::custom(format!(
"binary_signs[{idx}].artifacts: '{art}' is not allowed; \
binary_signs accepts only 'binary' or 'none' (use top-level \
`signs:` for broader artifact filters)"
)));
}
}
Ok(configs)
}
#[derive(Debug, Clone, PartialEq, Serialize, JsonSchema)]
#[serde(untagged)]
pub enum WrapInDirectory {
Bool(bool),
Name(String),
}
impl<'de> serde::Deserialize<'de> for WrapInDirectory {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let value = serde_yaml_ng::Value::deserialize(deserializer)?;
match value {
serde_yaml_ng::Value::Bool(b) => Ok(WrapInDirectory::Bool(b)),
serde_yaml_ng::Value::String(s) => Ok(WrapInDirectory::Name(s)),
_ => Err(serde::de::Error::custom("expected bool or string")),
}
}
}
impl WrapInDirectory {
pub fn directory_name(&self, default_name: &str) -> Option<String> {
match self {
WrapInDirectory::Bool(true) => Some(default_name.to_string()),
WrapInDirectory::Bool(false) => None,
WrapInDirectory::Name(s) if s.is_empty() => None,
WrapInDirectory::Name(s) => Some(s.clone()),
}
}
}
#[derive(Debug, Clone, Serialize, Default, JsonSchema)]
pub struct ArchiveConfig {
pub id: Option<String>,
pub name_template: Option<String>,
pub formats: Option<Vec<String>>,
pub format_overrides: Option<Vec<FormatOverride>>,
pub files: Option<Vec<ArchiveFileSpec>>,
pub binaries: Option<Vec<String>>,
pub wrap_in_directory: Option<WrapInDirectory>,
pub ids: Option<Vec<String>>,
pub meta: Option<bool>,
pub builds_info: Option<ArchiveFileInfo>,
pub strip_binary_directory: Option<bool>,
pub allow_different_binary_count: Option<bool>,
pub hooks: Option<ArchiveHooksConfig>,
}
fn fold_format_into_formats(
context_label: &str,
context_kind: &str,
formats: Option<Vec<String>>,
legacy: Option<String>,
) -> Option<Vec<String>> {
let mut formats = formats;
if let Some(legacy) = legacy {
tracing::warn!(
"DEPRECATION: {}[{}]: 'format: {}' is deprecated; \
use 'formats: [{}]' instead.",
context_kind,
context_label,
legacy,
legacy
);
formats.get_or_insert_with(Vec::new).push(legacy);
}
formats
}
impl<'de> Deserialize<'de> for ArchiveConfig {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize, Default)]
#[serde(default)]
struct Raw {
id: Option<String>,
name_template: Option<String>,
formats: Option<Vec<String>>,
format: Option<String>,
format_overrides: Option<Vec<FormatOverride>>,
files: Option<Vec<ArchiveFileSpec>>,
binaries: Option<Vec<String>>,
wrap_in_directory: Option<WrapInDirectory>,
ids: Option<Vec<String>>,
builds: Option<Vec<String>>,
meta: Option<bool>,
builds_info: Option<ArchiveFileInfo>,
strip_binary_directory: Option<bool>,
allow_different_binary_count: Option<bool>,
hooks: Option<ArchiveHooksConfig>,
}
let raw = Raw::deserialize(deserializer)?;
let id_label = raw.id.clone().unwrap_or_else(|| "default".to_string());
let formats = fold_format_into_formats(
&format!("id={}", id_label),
"archives",
raw.formats,
raw.format,
);
let mut ids = raw.ids;
if let Some(legacy) = raw.builds {
tracing::warn!(
"DEPRECATION: archives[id={}]: 'builds: {:?}' is deprecated; \
use 'ids: [...]' instead.",
id_label,
legacy
);
let target = ids.get_or_insert_with(Vec::new);
target.extend(legacy);
}
Ok(ArchiveConfig {
id: raw.id.or_else(|| Some("default".to_string())),
name_template: raw.name_template,
formats,
format_overrides: raw.format_overrides,
files: raw.files,
binaries: raw.binaries,
wrap_in_directory: raw.wrap_in_directory,
ids,
meta: raw.meta,
builds_info: raw.builds_info,
strip_binary_directory: raw.strip_binary_directory,
allow_different_binary_count: raw.allow_different_binary_count,
hooks: raw.hooks,
})
}
}
#[derive(Debug, Clone, Serialize, JsonSchema)]
pub struct FormatOverride {
pub os: String,
pub formats: Option<Vec<String>>,
}
impl<'de> Deserialize<'de> for FormatOverride {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize, Default)]
#[serde(default)]
struct Raw {
os: String,
formats: Option<Vec<String>>,
format: Option<String>,
}
let raw = Raw::deserialize(deserializer)?;
let formats = fold_format_into_formats(
&format!("os={}", raw.os),
"archives.format_overrides",
raw.formats,
raw.format,
);
Ok(FormatOverride {
os: raw.os,
formats,
})
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(untagged)]
pub enum ArchiveFileSpec {
Glob(String),
Detailed {
src: String,
dst: Option<String>,
info: Option<ArchiveFileInfo>,
strip_parent: Option<bool>,
},
}
impl PartialEq<&str> for ArchiveFileSpec {
fn eq(&self, other: &&str) -> bool {
match self {
ArchiveFileSpec::Glob(s) => s.as_str() == *other,
_ => false,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct FileInfo {
pub owner: Option<String>,
pub group: Option<String>,
pub mode: Option<StringOrU32>,
pub mtime: Option<String>,
}
pub type ArchiveFileInfo = FileInfo;
pub fn parse_octal_mode(s: &str) -> Option<u32> {
let cleaned = s
.strip_prefix("0o")
.or_else(|| s.strip_prefix("0O"))
.unwrap_or(s);
let cleaned = if cleaned.is_empty() { "0" } else { cleaned };
u32::from_str_radix(cleaned, 8).ok()
}
pub const VALID_ARCHIVE_FORMATS: &[&str] = &[
"tar.gz", "tgz", "tar.xz", "txz", "tar.zst", "tzst", "tar", "zip", "gz", "xz", "binary", "none",
];
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(untagged)]
pub enum ExtraFileSpec {
Glob(String),
Detailed {
glob: String,
#[serde(default)]
name_template: Option<String>,
#[serde(default)]
allow_empty: bool,
},
}
impl ExtraFileSpec {
pub fn glob(&self) -> &str {
match self {
ExtraFileSpec::Glob(s) => s,
ExtraFileSpec::Detailed { glob, .. } => glob,
}
}
pub fn name_template(&self) -> Option<&str> {
match self {
ExtraFileSpec::Glob(_) => None,
ExtraFileSpec::Detailed { name_template, .. } => name_template.as_deref(),
}
}
pub fn allow_empty(&self) -> bool {
match self {
ExtraFileSpec::Glob(_) => false,
ExtraFileSpec::Detailed { allow_empty, .. } => *allow_empty,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema, PartialEq)]
#[serde(default)]
pub struct TemplatedExtraFile {
pub src: String,
pub dst: Option<String>,
pub mode: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default)]
pub struct ChecksumConfig {
pub name_template: Option<String>,
pub algorithm: Option<String>,
#[serde(deserialize_with = "deserialize_string_or_bool_opt", default)]
pub skip: Option<StringOrBool>,
pub extra_files: Option<Vec<ExtraFileSpec>>,
pub templated_extra_files: Option<Vec<TemplatedExtraFile>>,
pub ids: Option<Vec<String>>,
pub split: Option<bool>,
}
impl ChecksumConfig {
pub const DEFAULT_NAME_TEMPLATE: &'static str = "{{ ProjectName }}_{{ Version }}_checksums.txt";
pub const DEFAULT_ALGORITHM: &'static str = "sha256";
pub fn resolved_algorithm(&self) -> &str {
self.algorithm.as_deref().unwrap_or(Self::DEFAULT_ALGORITHM)
}
pub fn resolved_split(&self) -> bool {
self.split.unwrap_or(false)
}
pub fn resolved_combined_name_template(&self) -> &str {
self.name_template
.as_deref()
.unwrap_or(Self::DEFAULT_NAME_TEMPLATE)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(untagged)]
pub enum ContentSource {
Inline(String),
FromFile {
from_file: String,
},
FromUrl {
from_url: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
headers: Option<HashMap<String, String>>,
},
}
impl PartialEq for ContentSource {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Inline(a), Self::Inline(b)) => a == b,
(Self::FromFile { from_file: a }, Self::FromFile { from_file: b }) => a == b,
(
Self::FromUrl {
from_url: a,
headers: ha,
},
Self::FromUrl {
from_url: b,
headers: hb,
},
) => a == b && ha == hb,
_ => false,
}
}
}