use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use crate::config::schema::{
AptMirrorDef, CertificatePreflightDef, CheckSpec, EnvironmentMutation, InstallSpec,
NetworkPolicy,
};
use crate::error::ForgeError;
use crate::model::{Agent, InstallKind};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ExecutionPlan {
pub profile: String,
pub target: TargetPlatform,
pub config_hash: String,
pub plan_hash: String,
pub policy: PlanPolicy,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub certificate_preflight: Option<CertificatePreflightDef>,
pub components: Vec<ResolvedComponent>,
#[serde(skip)]
pub unsupported_components: Vec<UnsupportedComponent>,
pub nodes: Vec<ExecutionNode>,
pub environment: Vec<EnvironmentMutation>,
pub apt_mirror: Option<AptMirrorDef>,
pub origins: BTreeMap<String, String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct UnsupportedComponent {
pub id: String,
pub display_name: Option<String>,
pub kind: InstallKind,
pub optional: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PlanPolicy {
pub network: NetworkPolicy,
pub max_parallel: usize,
pub max_downloads: usize,
pub max_memory_mib: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TargetPlatform {
pub os: String,
pub arch: String,
pub abi: String,
}
impl TargetPlatform {
pub fn host() -> Self {
let os = std::env::consts::OS.to_string();
let arch = normalize_arch(std::env::consts::ARCH).to_string();
let abi = if os == "windows" && cfg!(target_env = "gnu") {
"gnu"
} else if os == "windows" {
"msvc"
} else if os == "linux" && cfg!(target_env = "musl") {
"musl"
} else if os == "linux" {
"gnu"
} else {
"native"
};
Self {
os,
arch,
abi: abi.to_string(),
}
}
pub fn selector(&self) -> String {
if self.abi == "native" {
format!("{}-{}", self.os, self.arch)
} else {
format!("{}-{}-{}", self.os, self.arch, self.abi)
}
}
pub fn validate_supported(&self) -> Result<(), ForgeError> {
let supported = matches!(
(self.os.as_str(), self.arch.as_str(), self.abi.as_str()),
("linux", "x86_64" | "aarch64", "gnu")
| ("windows", "x86_64" | "aarch64", "msvc")
| ("macos", "x86_64" | "aarch64", "native")
);
if supported {
Ok(())
} else {
Err(ForgeError::Config(format!(
"this release does not support target platform: {}; supported targets are Linux GNU, macOS, and Windows MSVC on x86_64/aarch64",
self.selector()
)))
}
}
pub(crate) fn matches(&self, selector: &str) -> bool {
selector == "*"
|| selector == format!("{}-*", self.os)
|| selector == self.selector()
|| selector == format!("{}-{}", self.os, self.arch)
}
}
fn normalize_arch(arch: &str) -> &str {
match arch {
"amd64" | "x64" => "x86_64",
"arm64" => "aarch64",
value => value,
}
}
#[cfg(test)]
mod platform_tests {
use crate::planning::TargetPlatform;
#[test]
fn host_abi_matches_the_compilation_target() {
let target = TargetPlatform::host();
if cfg!(target_os = "linux") && cfg!(target_env = "musl") {
assert_eq!(target.abi, "musl");
} else if (cfg!(target_os = "linux") || cfg!(windows)) && cfg!(target_env = "gnu") {
assert_eq!(target.abi, "gnu");
} else if cfg!(windows) {
assert_eq!(target.abi, "msvc");
} else {
assert_eq!(target.abi, "native");
}
}
#[test]
fn release_contract_rejects_unpublished_target_families() {
for target in [
TargetPlatform {
os: "linux".into(),
arch: "x86_64".into(),
abi: "musl".into(),
},
TargetPlatform {
os: "windows".into(),
arch: "x86_64".into(),
abi: "gnu".into(),
},
] {
assert!(target.validate_supported().is_err());
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ResolvedComponent {
pub id: String,
pub display_name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub version: Option<String>,
pub optional: bool,
pub allow_insecure_hosts: Vec<String>,
pub kind: InstallKind,
pub variant: Option<String>,
pub requested_by: Vec<String>,
pub dependencies: Vec<String>,
pub provides: Vec<String>,
pub conflicts: Vec<String>,
pub detect: Option<CheckSpec>,
pub install: Option<InstallSpec>,
pub verify: Option<CheckSpec>,
pub source: Option<String>,
pub revision: Option<String>,
pub agents: Vec<Agent>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ExecutionNode {
pub id: String,
pub component: String,
pub kind: NodeKind,
pub dependencies: Vec<String>,
pub resources: Vec<ResourceClaim>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum NodeKind {
Detect,
Fetch,
Acquire,
Verify,
Store,
Activate,
Record,
Environment,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ResourceClaim {
pub key: String,
pub units: u32,
}