commit 886ff6b38763019f12cb7a81e3cbb654e0d2710a
parent f8ad4cf3e380cdffba39b5e8f3bf59fa850578b3
Author: triesap <tyson@radroots.org>
Date: Wed, 8 Apr 2026 23:47:18 +0000
distribution: add runtime artifact resolver
Diffstat:
10 files changed, 756 insertions(+), 0 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -2619,6 +2619,15 @@ dependencies = [
]
[[package]]
+name = "radroots-runtime-distribution"
+version = "0.1.0-alpha.1"
+dependencies = [
+ "serde",
+ "thiserror 1.0.69",
+ "toml",
+]
+
+[[package]]
name = "radroots-runtime-paths"
version = "0.1.0-alpha.1"
dependencies = [
diff --git a/Cargo.toml b/Cargo.toml
@@ -36,6 +36,7 @@ members = [
"crates/replica-db",
"crates/replica-db-wasm",
"crates/runtime-paths",
+ "crates/runtime-distribution",
"crates/trade",
"crates/types",
"crates/protected-store",
@@ -66,6 +67,7 @@ radroots-nostr-signer = { path = "crates/nostr-signer", version = "0.1.0-alpha.1
radroots-nostr-ndb = { path = "crates/nostr-ndb", version = "0.1.0-alpha.1", default-features = false }
radroots-runtime = { path = "crates/runtime", version = "0.1.0-alpha.1", default-features = false }
radroots-runtime-paths = { path = "crates/runtime-paths", version = "0.1.0-alpha.1", default-features = false }
+radroots-runtime-distribution = { path = "crates/runtime-distribution", version = "0.1.0-alpha.1", default-features = false }
radroots-log = { path = "crates/log", version = "0.1.0-alpha.1", default-features = false }
radroots-net = { path = "crates/net", version = "0.1.0-alpha.1", default-features = false }
radroots-net-core = { path = "crates/net-core", version = "0.1.0-alpha.1", default-features = false }
diff --git a/contract/coverage/policy.toml b/contract/coverage/policy.toml
@@ -30,6 +30,7 @@ crates = [
"radroots-protected-store",
"radroots-runtime",
"radroots-runtime-paths",
+ "radroots-runtime-distribution",
"radroots-secret-vault",
"radroots-simplex-chat-proto",
"radroots-simplex-smp-proto",
diff --git a/contract/release/publish-set.toml b/contract/release/publish-set.toml
@@ -10,6 +10,7 @@ crates = [
"radroots-protected-store",
"radroots-runtime",
"radroots-runtime-paths",
+ "radroots-runtime-distribution",
"radroots-secret-vault",
"radroots-simplex-chat-proto",
"radroots-simplex-smp-proto",
@@ -49,6 +50,7 @@ crates = [
"radroots-protected-store",
"radroots-runtime",
"radroots-runtime-paths",
+ "radroots-runtime-distribution",
"radroots-secret-vault",
"radroots-simplex-chat-proto",
"radroots-simplex-smp-proto",
diff --git a/crates/runtime-distribution/Cargo.toml b/crates/runtime-distribution/Cargo.toml
@@ -0,0 +1,19 @@
+[package]
+name = "radroots-runtime-distribution"
+version = "0.1.0-alpha.1"
+edition.workspace = true
+authors = [
+ "Radroots Authors",
+]
+rust-version.workspace = true
+license.workspace = true
+description = "contract parsing and artifact resolution for modular radroots runtime distribution"
+repository.workspace = true
+homepage.workspace = true
+documentation = "https://docs.rs/radroots-runtime-distribution"
+readme = "README.md"
+
+[dependencies]
+serde = { workspace = true, features = ["derive"] }
+thiserror = { workspace = true }
+toml = { workspace = true }
diff --git a/crates/runtime-distribution/README.md b/crates/runtime-distribution/README.md
@@ -0,0 +1,15 @@
+# radroots-runtime-distribution
+
+Contract parsing and artifact resolution for modular Rad Roots runtime distribution.
+
+## Goals
+
+- parse the machine-readable runtime distribution contract
+- resolve installable runtime artifacts by runtime id, channel, version, operating system, and
+ architecture
+- keep installer and runtime-manager selection logic out of ad hoc shell branches
+- give CLI and app clients one shared substrate for runtime release-unit resolution
+
+## License
+
+Licensed under AGPL-3.0. See LICENSE.
diff --git a/crates/runtime-distribution/src/error.rs b/crates/runtime-distribution/src/error.rs
@@ -0,0 +1,51 @@
+use thiserror::Error;
+
+#[derive(Debug, Error, PartialEq, Eq)]
+pub enum RadrootsRuntimeDistributionError {
+ #[error("parse runtime distribution contract: {0}")]
+ Parse(String),
+ #[error("runtime distribution schema `{found}` does not match `{expected}`")]
+ UnexpectedSchema {
+ expected: &'static str,
+ found: String,
+ },
+ #[error("runtime `{0}` not found in distribution contract")]
+ UnknownRuntime(String),
+ #[error("runtime `{0}` is not installable through the local runtime distribution contract")]
+ RuntimeNotInstallable(String),
+ #[error("runtime `{0}` has no target set in the distribution contract")]
+ MissingTargetSet(String),
+ #[error("runtime `{runtime_id}` references unknown artifact adapter `{adapter_id}`")]
+ UnknownArtifactAdapter {
+ runtime_id: String,
+ adapter_id: String,
+ },
+ #[error("channel `{0}` is not defined in the runtime distribution contract")]
+ UnknownChannel(String),
+ #[error("channel `{0}` is defined but not active in the runtime distribution contract")]
+ InactiveChannel(String),
+ #[error(
+ "target set `{target_set_id}` for runtime `{runtime_id}` references unknown target `{target_id}`"
+ )]
+ UnknownTarget {
+ runtime_id: String,
+ target_set_id: String,
+ target_id: String,
+ },
+ #[error("runtime `{runtime_id}` does not support os `{os}` arch `{arch}`")]
+ UnsupportedPlatform {
+ runtime_id: String,
+ os: String,
+ arch: String,
+ },
+ #[error("target `{target_id}` references unknown archive format `{archive_format_id}`")]
+ UnknownArchiveFormat {
+ target_id: String,
+ archive_format_id: String,
+ },
+ #[error("target `{target_id}` for runtime `{runtime_id}` does not define an archive format")]
+ MissingArchiveFormat {
+ runtime_id: String,
+ target_id: String,
+ },
+}
diff --git a/crates/runtime-distribution/src/lib.rs b/crates/runtime-distribution/src/lib.rs
@@ -0,0 +1,346 @@
+#![forbid(unsafe_code)]
+
+pub mod error;
+pub mod model;
+pub mod resolve;
+
+pub use error::RadrootsRuntimeDistributionError;
+pub use model::{
+ ArchiveFormat, ArtifactAdapter, ChannelSet, DistributionFamily,
+ RadrootsRuntimeDistributionContract, RuntimeDistributionEntry, TargetSet, TargetSpec,
+};
+pub use resolve::{
+ RUNTIME_DISTRIBUTION_SCHEMA, RadrootsRuntimeDistributionResolver, ResolvedRuntimeArtifact,
+ RuntimeArtifactRequest,
+};
+
+#[cfg(test)]
+mod tests {
+ use super::{
+ RUNTIME_DISTRIBUTION_SCHEMA, RadrootsRuntimeDistributionError,
+ RadrootsRuntimeDistributionResolver, RuntimeArtifactRequest,
+ };
+
+ const CONTRACT: &str = r#"
+schema = "radroots-runtime-distribution"
+schema_version = 1
+owner_doc = "docs/migration/radroots-modular-runtime-management-bootstrap-rcl.md"
+runtime_registry = "registry.toml"
+
+[family]
+id = "radroots-runtime-family"
+canonical_installer_engine = "single_runtime_selected"
+human_install_facade = "delivery_publication_only"
+tooling_consumption = "shared_distribution_library"
+independent_runtime_versions = true
+version_resolution = "runtime_scoped_channel_latest"
+artifact_verification_required = true
+
+[channels]
+active = ["stable"]
+defined = ["stable", "candidate", "nightly"]
+
+[artifact_adapters.rust_binary_archive]
+kind = "binary_archive"
+supported_archive_formats = ["tar.gz", "zip"]
+layout = "single_binary_plus_supporting_files"
+
+[artifact_adapters.desktop_bundle]
+kind = "desktop_bundle"
+supported_archive_formats = ["tar.gz", "zip", "dmg"]
+layout = "host_native_bundle"
+
+[artifact_adapters.mobile_store_package]
+kind = "mobile_store_package"
+supported_archive_formats = []
+layout = "platform_store_managed"
+
+[artifact_adapters.mojo_workspace_archive]
+kind = "workspace_archive"
+supported_archive_formats = ["tar.gz"]
+layout = "workspace_tree"
+
+[archive_formats.tar_gz]
+extension = ".tar.gz"
+platforms = ["linux", "macos"]
+
+[archive_formats.zip]
+extension = ".zip"
+platforms = ["windows"]
+
+[archive_formats.dmg]
+extension = ".dmg"
+platforms = ["macos"]
+
+[target_sets.server_default]
+targets = [
+ "x86_64-unknown-linux-gnu",
+ "aarch64-unknown-linux-gnu",
+]
+
+[target_sets.cli_default]
+targets = [
+ "x86_64-unknown-linux-gnu",
+ "aarch64-unknown-linux-gnu",
+ "x86_64-apple-darwin",
+ "aarch64-apple-darwin",
+]
+
+[target_sets.desktop_default]
+targets = [
+ "x86_64-apple-darwin",
+ "aarch64-apple-darwin",
+]
+
+[target_sets.mojo_workspace_default]
+targets = [
+ "osx-arm64",
+ "linux-64",
+]
+
+[targets.x86_64-unknown-linux-gnu]
+os = "linux"
+arch = "amd64"
+archive_format = "tar.gz"
+
+[targets.aarch64-unknown-linux-gnu]
+os = "linux"
+arch = "arm64"
+archive_format = "tar.gz"
+
+[targets.x86_64-apple-darwin]
+os = "macos"
+arch = "amd64"
+archive_format = "tar.gz"
+
+[targets.aarch64-apple-darwin]
+os = "macos"
+arch = "arm64"
+archive_format = "tar.gz"
+
+[targets.osx-arm64]
+os = "macos"
+arch = "arm64"
+archive_format = "tar.gz"
+
+[targets.linux-64]
+os = "linux"
+arch = "amd64"
+archive_format = "tar.gz"
+
+[[runtime]]
+id = "cli"
+distribution_state = "active"
+release_unit = "cli"
+package_name = "radroots-cli"
+binary_name = "radroots"
+artifact_adapter = "rust_binary_archive"
+target_set = "cli_default"
+default_channel = "stable"
+human_installable = true
+
+[[runtime]]
+id = "radrootsd"
+distribution_state = "active"
+release_unit = "radrootsd"
+package_name = "radrootsd"
+binary_name = "radrootsd"
+artifact_adapter = "rust_binary_archive"
+target_set = "server_default"
+default_channel = "stable"
+human_installable = true
+
+[[runtime]]
+id = "community-app-desktop"
+distribution_state = "defined"
+release_unit = "community-app-desktop"
+package_name = "radroots-app-desktop"
+binary_name = "radroots-app-desktop"
+artifact_adapter = "desktop_bundle"
+target_set = "desktop_default"
+default_channel = "stable"
+human_installable = true
+
+[[runtime]]
+id = "community-app-ios"
+distribution_state = "external_platform_managed"
+release_unit = "community-app-ios"
+package_name = "radroots-app-ios"
+artifact_adapter = "mobile_store_package"
+default_channel = "stable"
+human_installable = false
+
+[[runtime]]
+id = "hyf"
+distribution_state = "bootstrap_only"
+release_unit = "hyf"
+package_name = "hyf"
+binary_name = "hyf"
+artifact_adapter = "mojo_workspace_archive"
+target_set = "mojo_workspace_default"
+default_channel = "stable"
+human_installable = false
+"#;
+
+ #[test]
+ fn parse_str_accepts_the_expected_schema() {
+ let resolver =
+ RadrootsRuntimeDistributionResolver::parse_str(CONTRACT).expect("parse contract");
+
+ assert_eq!(resolver.contract().schema, RUNTIME_DISTRIBUTION_SCHEMA);
+ assert_eq!(resolver.contract().runtime.len(), 5);
+ }
+
+ #[test]
+ fn resolves_cli_linux_artifact_with_explicit_channel() {
+ let resolver =
+ RadrootsRuntimeDistributionResolver::parse_str(CONTRACT).expect("parse contract");
+
+ let artifact = resolver
+ .resolve_artifact(&RuntimeArtifactRequest {
+ runtime_id: "cli",
+ os: "linux",
+ arch: "amd64",
+ version: "0.1.0-alpha.1",
+ channel: Some("stable"),
+ })
+ .expect("resolve cli artifact");
+
+ assert_eq!(artifact.binary_name.as_deref(), Some("radroots"));
+ assert_eq!(artifact.target_id, "x86_64-unknown-linux-gnu");
+ assert_eq!(artifact.archive_extension, ".tar.gz");
+ assert_eq!(
+ artifact.artifact_file_name,
+ "cli-0.1.0-alpha.1-x86_64-unknown-linux-gnu.tar.gz"
+ );
+ }
+
+ #[test]
+ fn resolves_radrootsd_linux_arm64_using_default_channel() {
+ let resolver =
+ RadrootsRuntimeDistributionResolver::parse_str(CONTRACT).expect("parse contract");
+
+ let artifact = resolver
+ .resolve_artifact(&RuntimeArtifactRequest {
+ runtime_id: "radrootsd",
+ os: "linux",
+ arch: "arm64",
+ version: "0.1.0-alpha.1",
+ channel: None,
+ })
+ .expect("resolve radrootsd artifact");
+
+ assert_eq!(artifact.channel, "stable");
+ assert_eq!(artifact.target_id, "aarch64-unknown-linux-gnu");
+ assert_eq!(artifact.binary_name.as_deref(), Some("radrootsd"));
+ }
+
+ #[test]
+ fn resolves_desktop_bundle_for_macos_arm64() {
+ let resolver =
+ RadrootsRuntimeDistributionResolver::parse_str(CONTRACT).expect("parse contract");
+
+ let artifact = resolver
+ .resolve_artifact(&RuntimeArtifactRequest {
+ runtime_id: "community-app-desktop",
+ os: "macos",
+ arch: "arm64",
+ version: "0.1.0-alpha.1",
+ channel: Some("stable"),
+ })
+ .expect("resolve desktop artifact");
+
+ assert_eq!(artifact.target_id, "aarch64-apple-darwin");
+ assert_eq!(artifact.package_name, "radroots-app-desktop");
+ }
+
+ #[test]
+ fn rejects_non_installable_mobile_runtime() {
+ let resolver =
+ RadrootsRuntimeDistributionResolver::parse_str(CONTRACT).expect("parse contract");
+
+ let err = resolver
+ .resolve_artifact(&RuntimeArtifactRequest {
+ runtime_id: "community-app-ios",
+ os: "macos",
+ arch: "arm64",
+ version: "0.1.0-alpha.1",
+ channel: Some("stable"),
+ })
+ .expect_err("mobile runtime should not be installable");
+
+ assert_eq!(
+ err,
+ RadrootsRuntimeDistributionError::RuntimeNotInstallable(
+ "community-app-ios".to_string()
+ )
+ );
+ }
+
+ #[test]
+ fn rejects_bootstrap_only_runtime() {
+ let resolver =
+ RadrootsRuntimeDistributionResolver::parse_str(CONTRACT).expect("parse contract");
+
+ let err = resolver
+ .resolve_artifact(&RuntimeArtifactRequest {
+ runtime_id: "hyf",
+ os: "macos",
+ arch: "arm64",
+ version: "0.1.0",
+ channel: Some("stable"),
+ })
+ .expect_err("bootstrap runtime should not be installable");
+
+ assert_eq!(
+ err,
+ RadrootsRuntimeDistributionError::RuntimeNotInstallable("hyf".to_string())
+ );
+ }
+
+ #[test]
+ fn rejects_inactive_channel() {
+ let resolver =
+ RadrootsRuntimeDistributionResolver::parse_str(CONTRACT).expect("parse contract");
+
+ let err = resolver
+ .resolve_artifact(&RuntimeArtifactRequest {
+ runtime_id: "cli",
+ os: "linux",
+ arch: "amd64",
+ version: "0.1.0-alpha.1",
+ channel: Some("candidate"),
+ })
+ .expect_err("candidate channel should be inactive");
+
+ assert_eq!(
+ err,
+ RadrootsRuntimeDistributionError::InactiveChannel("candidate".to_string())
+ );
+ }
+
+ #[test]
+ fn rejects_unsupported_platform() {
+ let resolver =
+ RadrootsRuntimeDistributionResolver::parse_str(CONTRACT).expect("parse contract");
+
+ let err = resolver
+ .resolve_artifact(&RuntimeArtifactRequest {
+ runtime_id: "radrootsd",
+ os: "windows",
+ arch: "amd64",
+ version: "0.1.0-alpha.1",
+ channel: Some("stable"),
+ })
+ .expect_err("windows target should be unsupported");
+
+ assert_eq!(
+ err,
+ RadrootsRuntimeDistributionError::UnsupportedPlatform {
+ runtime_id: "radrootsd".to_string(),
+ os: "windows".to_string(),
+ arch: "amd64".to_string(),
+ }
+ );
+ }
+}
diff --git a/crates/runtime-distribution/src/model.rs b/crates/runtime-distribution/src/model.rs
@@ -0,0 +1,84 @@
+use std::collections::BTreeMap;
+
+use serde::Deserialize;
+
+#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
+pub struct RadrootsRuntimeDistributionContract {
+ pub schema: String,
+ pub schema_version: u32,
+ pub owner_doc: String,
+ pub runtime_registry: String,
+ pub family: DistributionFamily,
+ pub channels: ChannelSet,
+ #[serde(default)]
+ pub artifact_adapters: BTreeMap<String, ArtifactAdapter>,
+ #[serde(default)]
+ pub archive_formats: BTreeMap<String, ArchiveFormat>,
+ #[serde(default)]
+ pub target_sets: BTreeMap<String, TargetSet>,
+ #[serde(default)]
+ pub targets: BTreeMap<String, TargetSpec>,
+ #[serde(default)]
+ pub runtime: Vec<RuntimeDistributionEntry>,
+}
+
+#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
+pub struct DistributionFamily {
+ pub id: String,
+ pub canonical_installer_engine: String,
+ pub human_install_facade: String,
+ pub tooling_consumption: String,
+ pub independent_runtime_versions: bool,
+ pub version_resolution: String,
+ pub artifact_verification_required: bool,
+}
+
+#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
+pub struct ChannelSet {
+ #[serde(default)]
+ pub active: Vec<String>,
+ #[serde(default)]
+ pub defined: Vec<String>,
+}
+
+#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
+pub struct ArtifactAdapter {
+ pub kind: String,
+ #[serde(default)]
+ pub supported_archive_formats: Vec<String>,
+ pub layout: String,
+}
+
+#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
+pub struct ArchiveFormat {
+ pub extension: String,
+ #[serde(default)]
+ pub platforms: Vec<String>,
+}
+
+#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
+pub struct TargetSet {
+ #[serde(default)]
+ pub targets: Vec<String>,
+}
+
+#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
+pub struct TargetSpec {
+ pub os: String,
+ pub arch: String,
+ pub archive_format: Option<String>,
+}
+
+#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
+pub struct RuntimeDistributionEntry {
+ pub id: String,
+ pub distribution_state: String,
+ pub release_unit: String,
+ pub package_name: String,
+ pub binary_name: Option<String>,
+ pub artifact_adapter: String,
+ pub target_set: Option<String>,
+ pub default_channel: String,
+ pub human_installable: bool,
+ pub notes: Option<String>,
+}
diff --git a/crates/runtime-distribution/src/resolve.rs b/crates/runtime-distribution/src/resolve.rs
@@ -0,0 +1,227 @@
+use crate::error::RadrootsRuntimeDistributionError;
+use crate::model::{
+ ArtifactAdapter, RadrootsRuntimeDistributionContract, RuntimeDistributionEntry, TargetSpec,
+};
+
+pub const RUNTIME_DISTRIBUTION_SCHEMA: &str = "radroots-runtime-distribution";
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct RuntimeArtifactRequest<'a> {
+ pub runtime_id: &'a str,
+ pub os: &'a str,
+ pub arch: &'a str,
+ pub version: &'a str,
+ pub channel: Option<&'a str>,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct ResolvedRuntimeArtifact {
+ pub runtime_id: String,
+ pub release_unit: String,
+ pub package_name: String,
+ pub binary_name: Option<String>,
+ pub artifact_adapter: String,
+ pub channel: String,
+ pub version: String,
+ pub target_id: String,
+ pub os: String,
+ pub arch: String,
+ pub archive_format: String,
+ pub archive_extension: String,
+ pub artifact_stem: String,
+ pub artifact_file_name: String,
+}
+
+#[derive(Debug, Clone)]
+pub struct RadrootsRuntimeDistributionResolver {
+ contract: RadrootsRuntimeDistributionContract,
+}
+
+impl RadrootsRuntimeDistributionResolver {
+ pub fn parse_str(raw: &str) -> Result<Self, RadrootsRuntimeDistributionError> {
+ let contract = toml::from_str::<RadrootsRuntimeDistributionContract>(raw)
+ .map_err(|err| RadrootsRuntimeDistributionError::Parse(err.to_string()))?;
+ Self::new(contract)
+ }
+
+ pub fn new(
+ contract: RadrootsRuntimeDistributionContract,
+ ) -> Result<Self, RadrootsRuntimeDistributionError> {
+ if contract.schema != RUNTIME_DISTRIBUTION_SCHEMA {
+ return Err(RadrootsRuntimeDistributionError::UnexpectedSchema {
+ expected: RUNTIME_DISTRIBUTION_SCHEMA,
+ found: contract.schema.clone(),
+ });
+ }
+ Ok(Self { contract })
+ }
+
+ pub fn contract(&self) -> &RadrootsRuntimeDistributionContract {
+ &self.contract
+ }
+
+ pub fn resolve_artifact(
+ &self,
+ request: &RuntimeArtifactRequest<'_>,
+ ) -> Result<ResolvedRuntimeArtifact, RadrootsRuntimeDistributionError> {
+ let runtime = self
+ .contract
+ .runtime
+ .iter()
+ .find(|runtime| runtime.id == request.runtime_id)
+ .ok_or_else(|| {
+ RadrootsRuntimeDistributionError::UnknownRuntime(request.runtime_id.to_string())
+ })?;
+
+ if !runtime.human_installable {
+ return Err(RadrootsRuntimeDistributionError::RuntimeNotInstallable(
+ runtime.id.clone(),
+ ));
+ }
+
+ let channel = request.channel.unwrap_or(runtime.default_channel.as_str());
+ self.ensure_channel_is_active(channel)?;
+
+ let target_set_id = runtime.target_set.as_ref().ok_or_else(|| {
+ RadrootsRuntimeDistributionError::MissingTargetSet(runtime.id.clone())
+ })?;
+
+ let adapter = self
+ .contract
+ .artifact_adapters
+ .get(&runtime.artifact_adapter)
+ .ok_or_else(
+ || RadrootsRuntimeDistributionError::UnknownArtifactAdapter {
+ runtime_id: runtime.id.clone(),
+ adapter_id: runtime.artifact_adapter.clone(),
+ },
+ )?;
+
+ let (target_id, target) =
+ self.select_target(runtime, target_set_id, request.os, request.arch)?;
+ let archive_format_id =
+ self.resolve_archive_format_id(runtime, target_id, target, adapter)?;
+ let archive_format = self
+ .contract
+ .archive_formats
+ .get(&normalized_contract_key(archive_format_id))
+ .ok_or_else(|| RadrootsRuntimeDistributionError::UnknownArchiveFormat {
+ target_id: target_id.to_string(),
+ archive_format_id: archive_format_id.to_string(),
+ })?;
+
+ let artifact_stem = format!("{}-{}-{}", runtime.release_unit, request.version, target_id);
+ let artifact_file_name = format!("{artifact_stem}{}", archive_format.extension);
+
+ Ok(ResolvedRuntimeArtifact {
+ runtime_id: runtime.id.clone(),
+ release_unit: runtime.release_unit.clone(),
+ package_name: runtime.package_name.clone(),
+ binary_name: runtime.binary_name.clone(),
+ artifact_adapter: runtime.artifact_adapter.clone(),
+ channel: channel.to_string(),
+ version: request.version.to_string(),
+ target_id: target_id.to_string(),
+ os: request.os.to_string(),
+ arch: request.arch.to_string(),
+ archive_format: archive_format_id.to_string(),
+ archive_extension: archive_format.extension.clone(),
+ artifact_stem,
+ artifact_file_name,
+ })
+ }
+
+ fn ensure_channel_is_active(
+ &self,
+ channel: &str,
+ ) -> Result<(), RadrootsRuntimeDistributionError> {
+ if !self
+ .contract
+ .channels
+ .defined
+ .iter()
+ .any(|entry| entry == channel)
+ {
+ return Err(RadrootsRuntimeDistributionError::UnknownChannel(
+ channel.to_string(),
+ ));
+ }
+ if !self
+ .contract
+ .channels
+ .active
+ .iter()
+ .any(|entry| entry == channel)
+ {
+ return Err(RadrootsRuntimeDistributionError::InactiveChannel(
+ channel.to_string(),
+ ));
+ }
+ Ok(())
+ }
+
+ fn select_target<'a>(
+ &'a self,
+ runtime: &RuntimeDistributionEntry,
+ target_set_id: &str,
+ os: &str,
+ arch: &str,
+ ) -> Result<(&'a str, &'a TargetSpec), RadrootsRuntimeDistributionError> {
+ let target_set = self
+ .contract
+ .target_sets
+ .get(target_set_id)
+ .ok_or_else(|| RadrootsRuntimeDistributionError::UnsupportedPlatform {
+ runtime_id: runtime.id.clone(),
+ os: os.to_string(),
+ arch: arch.to_string(),
+ })?;
+
+ let mut found_match = None;
+ for target_id in &target_set.targets {
+ let target = self.contract.targets.get(target_id).ok_or_else(|| {
+ RadrootsRuntimeDistributionError::UnknownTarget {
+ runtime_id: runtime.id.clone(),
+ target_set_id: target_set_id.to_string(),
+ target_id: target_id.clone(),
+ }
+ })?;
+
+ if target.os == os && target.arch == arch {
+ found_match = Some((target_id.as_str(), target));
+ break;
+ }
+ }
+
+ found_match.ok_or_else(|| RadrootsRuntimeDistributionError::UnsupportedPlatform {
+ runtime_id: runtime.id.clone(),
+ os: os.to_string(),
+ arch: arch.to_string(),
+ })
+ }
+
+ fn resolve_archive_format_id<'a>(
+ &self,
+ runtime: &RuntimeDistributionEntry,
+ target_id: &'a str,
+ target: &'a TargetSpec,
+ adapter: &'a ArtifactAdapter,
+ ) -> Result<&'a str, RadrootsRuntimeDistributionError> {
+ if let Some(format) = target.archive_format.as_deref() {
+ return Ok(format);
+ }
+
+ if adapter.supported_archive_formats.len() == 1 {
+ return Ok(adapter.supported_archive_formats[0].as_str());
+ }
+
+ Err(RadrootsRuntimeDistributionError::MissingArchiveFormat {
+ runtime_id: runtime.id.clone(),
+ target_id: target_id.to_string(),
+ })
+ }
+}
+
+fn normalized_contract_key(value: &str) -> String {
+ value.replace('.', "_")
+}