lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README | LICENSE

commit 886ff6b38763019f12cb7a81e3cbb654e0d2710a
parent f8ad4cf3e380cdffba39b5e8f3bf59fa850578b3
Author: triesap <tyson@radroots.org>
Date:   Wed,  8 Apr 2026 23:47:18 +0000

distribution: add runtime artifact resolver

Diffstat:
MCargo.lock | 9+++++++++
MCargo.toml | 2++
Mcontract/coverage/policy.toml | 1+
Mcontract/release/publish-set.toml | 2++
Acrates/runtime-distribution/Cargo.toml | 19+++++++++++++++++++
Acrates/runtime-distribution/README.md | 15+++++++++++++++
Acrates/runtime-distribution/src/error.rs | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/runtime-distribution/src/lib.rs | 346+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/runtime-distribution/src/model.rs | 84+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/runtime-distribution/src/resolve.rs | 227+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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('.', "_") +}