lib

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

lib.rs (19206B)


      1 #![forbid(unsafe_code)]
      2 
      3 pub mod error;
      4 pub mod model;
      5 pub mod resolve;
      6 
      7 pub use error::RadrootsRuntimeDistributionError;
      8 pub use model::{
      9     ArchiveFormat, ArtifactAdapter, ChannelSet, DistributionFamily,
     10     RadrootsRuntimeDistributionContract, RuntimeDistributionEntry, TargetSet, TargetSpec,
     11 };
     12 pub use resolve::{
     13     RUNTIME_DISTRIBUTION_SCHEMA, RadrootsRuntimeDistributionResolver, ResolvedRuntimeArtifact,
     14     RuntimeArtifactRequest,
     15 };
     16 
     17 #[cfg(test)]
     18 mod tests {
     19     use toml::Value;
     20 
     21     use super::{
     22         RUNTIME_DISTRIBUTION_SCHEMA, RadrootsRuntimeDistributionError,
     23         RadrootsRuntimeDistributionResolver, RuntimeArtifactRequest,
     24     };
     25 
     26     const CONTRACT: &str = r#"
     27 schema = "radroots-runtime-distribution"
     28 schema_version = 1
     29 owner_doc = "docs/execution/rcl/radroots-modular-runtime-management-bootstrap-rcl.md"
     30 runtime_registry = "registry.toml"
     31 
     32 [family]
     33 id = "radroots_runtime-family"
     34 canonical_installer_engine = "single_runtime_selected"
     35 human_install_facade = "delivery_publication_only"
     36 tooling_consumption = "shared_distribution_library"
     37 independent_runtime_versions = true
     38 version_resolution = "runtime_scoped_channel_latest"
     39 artifact_verification_required = true
     40 
     41 [channels]
     42 active = ["stable"]
     43 defined = ["stable", "candidate", "nightly"]
     44 
     45 [artifact_adapters.rust_binary_archive]
     46 kind = "binary_archive"
     47 supported_archive_formats = ["tar.gz", "zip"]
     48 layout = "single_binary_plus_supporting_files"
     49 
     50 [artifact_adapters.desktop_bundle]
     51 kind = "desktop_bundle"
     52 supported_archive_formats = ["tar.gz", "zip", "dmg"]
     53 layout = "host_native_bundle"
     54 
     55 [artifact_adapters.mobile_store_package]
     56 kind = "mobile_store_package"
     57 supported_archive_formats = []
     58 layout = "platform_store_managed"
     59 
     60 [artifact_adapters.mojo_workspace_archive]
     61 kind = "workspace_archive"
     62 supported_archive_formats = ["tar.gz"]
     63 layout = "workspace_tree"
     64 
     65 [archive_formats.tar_gz]
     66 extension = ".tar.gz"
     67 platforms = ["linux", "macos"]
     68 
     69 [archive_formats.zip]
     70 extension = ".zip"
     71 platforms = ["windows"]
     72 
     73 [archive_formats.dmg]
     74 extension = ".dmg"
     75 platforms = ["macos"]
     76 
     77 [target_sets.server_default]
     78 targets = [
     79   "x86_64-unknown-linux-gnu",
     80   "aarch64-unknown-linux-gnu",
     81   "x86_64-apple-darwin",
     82   "aarch64-apple-darwin",
     83 ]
     84 
     85 [target_sets.cli_default]
     86 targets = [
     87   "x86_64-unknown-linux-gnu",
     88   "aarch64-unknown-linux-gnu",
     89   "x86_64-apple-darwin",
     90   "aarch64-apple-darwin",
     91 ]
     92 
     93 [target_sets.desktop_default]
     94 targets = [
     95   "x86_64-apple-darwin",
     96   "aarch64-apple-darwin",
     97 ]
     98 
     99 [target_sets.mojo_workspace_default]
    100 targets = [
    101   "osx-arm64",
    102   "linux-64",
    103 ]
    104 
    105 [targets.x86_64-unknown-linux-gnu]
    106 os = "linux"
    107 arch = "amd64"
    108 archive_format = "tar.gz"
    109 
    110 [targets.aarch64-unknown-linux-gnu]
    111 os = "linux"
    112 arch = "arm64"
    113 archive_format = "tar.gz"
    114 
    115 [targets.x86_64-apple-darwin]
    116 os = "macos"
    117 arch = "amd64"
    118 archive_format = "tar.gz"
    119 
    120 [targets.aarch64-apple-darwin]
    121 os = "macos"
    122 arch = "arm64"
    123 archive_format = "tar.gz"
    124 
    125 [targets.osx-arm64]
    126 os = "macos"
    127 arch = "arm64"
    128 archive_format = "tar.gz"
    129 
    130 [targets.linux-64]
    131 os = "linux"
    132 arch = "amd64"
    133 archive_format = "tar.gz"
    134 
    135 [[runtime]]
    136 id = "cli"
    137 distribution_state = "active"
    138 release_unit = "cli"
    139 package_name = "radroots_cli"
    140 binary_name = "radroots"
    141 artifact_adapter = "rust_binary_archive"
    142 target_set = "cli_default"
    143 default_channel = "stable"
    144 human_installable = true
    145 
    146 [[runtime]]
    147 id = "radrootsd"
    148 distribution_state = "active"
    149 release_unit = "radrootsd"
    150 package_name = "radrootsd"
    151 binary_name = "radrootsd"
    152 artifact_adapter = "rust_binary_archive"
    153 target_set = "server_default"
    154 default_channel = "stable"
    155 human_installable = true
    156 
    157 [[runtime]]
    158 id = "community-app-desktop"
    159 distribution_state = "defined"
    160 release_unit = "community-app-desktop"
    161 package_name = "radroots_app"
    162 binary_name = "radroots_app"
    163 artifact_adapter = "desktop_bundle"
    164 target_set = "desktop_default"
    165 default_channel = "stable"
    166 human_installable = true
    167 
    168 [[runtime]]
    169 id = "community-app-ios"
    170 distribution_state = "external_platform_managed"
    171 release_unit = "community-app-ios"
    172 package_name = "radroots_app_ios"
    173 artifact_adapter = "mobile_store_package"
    174 default_channel = "stable"
    175 human_installable = false
    176 
    177 [[runtime]]
    178 id = "hyf"
    179 distribution_state = "bootstrap_only"
    180 release_unit = "hyf"
    181 package_name = "hyf"
    182 binary_name = "hyf"
    183 artifact_adapter = "mojo_workspace_archive"
    184 target_set = "mojo_workspace_default"
    185 default_channel = "stable"
    186 human_installable = false
    187 "#;
    188 
    189     fn contract_value() -> Value {
    190         toml::from_str(CONTRACT).expect("parse contract value")
    191     }
    192 
    193     fn resolver_from_value(value: Value) -> RadrootsRuntimeDistributionResolver {
    194         let raw = toml::to_string(&value).expect("serialize contract");
    195         RadrootsRuntimeDistributionResolver::parse_str(&raw).expect("parse resolver")
    196     }
    197 
    198     fn resolve_error(
    199         resolver: &RadrootsRuntimeDistributionResolver,
    200         request: RuntimeArtifactRequest<'_>,
    201     ) -> RadrootsRuntimeDistributionError {
    202         resolver
    203             .resolve_artifact(&request)
    204             .expect_err("request should fail")
    205     }
    206 
    207     #[test]
    208     fn parse_str_accepts_the_expected_schema() {
    209         let resolver =
    210             RadrootsRuntimeDistributionResolver::parse_str(CONTRACT).expect("parse contract");
    211 
    212         assert_eq!(resolver.contract().schema, RUNTIME_DISTRIBUTION_SCHEMA);
    213         assert_eq!(resolver.contract().runtime.len(), 5);
    214     }
    215 
    216     #[test]
    217     fn parse_str_rejects_invalid_toml() {
    218         let err = RadrootsRuntimeDistributionResolver::parse_str("schema = [")
    219             .expect_err("invalid toml should fail");
    220         assert_eq!(
    221             std::mem::discriminant(&err),
    222             std::mem::discriminant(&RadrootsRuntimeDistributionError::Parse(String::new()))
    223         );
    224     }
    225 
    226     #[test]
    227     fn new_rejects_unexpected_schema() {
    228         let mut contract = contract_value();
    229         contract["schema"] = Value::String("wrong-schema".to_string());
    230 
    231         let raw = toml::to_string(&contract).expect("serialize contract");
    232         let err = RadrootsRuntimeDistributionResolver::parse_str(&raw)
    233             .expect_err("unexpected schema should fail");
    234 
    235         assert_eq!(
    236             err,
    237             RadrootsRuntimeDistributionError::UnexpectedSchema {
    238                 expected: RUNTIME_DISTRIBUTION_SCHEMA,
    239                 found: "wrong-schema".to_string(),
    240             }
    241         );
    242     }
    243 
    244     #[test]
    245     fn resolves_cli_linux_artifact_with_explicit_channel() {
    246         let resolver =
    247             RadrootsRuntimeDistributionResolver::parse_str(CONTRACT).expect("parse contract");
    248 
    249         let artifact = resolver
    250             .resolve_artifact(&RuntimeArtifactRequest {
    251                 runtime_id: "cli",
    252                 os: "linux",
    253                 arch: "amd64",
    254                 version: "0.1.0-alpha.2",
    255                 channel: Some("stable"),
    256             })
    257             .expect("resolve cli artifact");
    258 
    259         assert_eq!(artifact.binary_name.as_deref(), Some("radroots"));
    260         assert_eq!(artifact.target_id, "x86_64-unknown-linux-gnu");
    261         assert_eq!(artifact.archive_extension, ".tar.gz");
    262         assert_eq!(
    263             artifact.artifact_file_name,
    264             "cli-0.1.0-alpha.2-x86_64-unknown-linux-gnu.tar.gz"
    265         );
    266     }
    267 
    268     #[test]
    269     fn resolves_radrootsd_linux_arm64_using_default_channel() {
    270         let resolver =
    271             RadrootsRuntimeDistributionResolver::parse_str(CONTRACT).expect("parse contract");
    272 
    273         let artifact = resolver
    274             .resolve_artifact(&RuntimeArtifactRequest {
    275                 runtime_id: "radrootsd",
    276                 os: "linux",
    277                 arch: "arm64",
    278                 version: "0.1.0-alpha.2",
    279                 channel: None,
    280             })
    281             .expect("resolve radrootsd artifact");
    282 
    283         assert_eq!(artifact.channel, "stable");
    284         assert_eq!(artifact.target_id, "aarch64-unknown-linux-gnu");
    285         assert_eq!(artifact.binary_name.as_deref(), Some("radrootsd"));
    286     }
    287 
    288     #[test]
    289     fn resolves_desktop_bundle_for_macos_arm64() {
    290         let resolver =
    291             RadrootsRuntimeDistributionResolver::parse_str(CONTRACT).expect("parse contract");
    292 
    293         let artifact = resolver
    294             .resolve_artifact(&RuntimeArtifactRequest {
    295                 runtime_id: "community-app-desktop",
    296                 os: "macos",
    297                 arch: "arm64",
    298                 version: "0.1.0-alpha.2",
    299                 channel: Some("stable"),
    300             })
    301             .expect("resolve desktop artifact");
    302 
    303         assert_eq!(artifact.target_id, "aarch64-apple-darwin");
    304         assert_eq!(artifact.package_name, "radroots_app");
    305     }
    306 
    307     #[test]
    308     fn rejects_non_installable_mobile_runtime() {
    309         let resolver =
    310             RadrootsRuntimeDistributionResolver::parse_str(CONTRACT).expect("parse contract");
    311 
    312         let err = resolver
    313             .resolve_artifact(&RuntimeArtifactRequest {
    314                 runtime_id: "community-app-ios",
    315                 os: "macos",
    316                 arch: "arm64",
    317                 version: "0.1.0-alpha.2",
    318                 channel: Some("stable"),
    319             })
    320             .expect_err("mobile runtime should not be installable");
    321 
    322         assert_eq!(
    323             err,
    324             RadrootsRuntimeDistributionError::RuntimeNotInstallable(
    325                 "community-app-ios".to_string()
    326             )
    327         );
    328     }
    329 
    330     #[test]
    331     fn rejects_bootstrap_only_runtime() {
    332         let resolver =
    333             RadrootsRuntimeDistributionResolver::parse_str(CONTRACT).expect("parse contract");
    334 
    335         let err = resolver
    336             .resolve_artifact(&RuntimeArtifactRequest {
    337                 runtime_id: "hyf",
    338                 os: "macos",
    339                 arch: "arm64",
    340                 version: "0.1.0",
    341                 channel: Some("stable"),
    342             })
    343             .expect_err("bootstrap runtime should not be installable");
    344 
    345         assert_eq!(
    346             err,
    347             RadrootsRuntimeDistributionError::RuntimeNotInstallable("hyf".to_string())
    348         );
    349     }
    350 
    351     #[test]
    352     fn rejects_inactive_channel() {
    353         let resolver =
    354             RadrootsRuntimeDistributionResolver::parse_str(CONTRACT).expect("parse contract");
    355 
    356         let err = resolver
    357             .resolve_artifact(&RuntimeArtifactRequest {
    358                 runtime_id: "cli",
    359                 os: "linux",
    360                 arch: "amd64",
    361                 version: "0.1.0-alpha.2",
    362                 channel: Some("candidate"),
    363             })
    364             .expect_err("candidate channel should be inactive");
    365 
    366         assert_eq!(
    367             err,
    368             RadrootsRuntimeDistributionError::InactiveChannel("candidate".to_string())
    369         );
    370     }
    371 
    372     #[test]
    373     fn rejects_unknown_runtime() {
    374         let resolver =
    375             RadrootsRuntimeDistributionResolver::parse_str(CONTRACT).expect("parse contract");
    376 
    377         let err = resolve_error(
    378             &resolver,
    379             RuntimeArtifactRequest {
    380                 runtime_id: "missing-runtime",
    381                 os: "linux",
    382                 arch: "amd64",
    383                 version: "0.1.0-alpha.2",
    384                 channel: Some("stable"),
    385             },
    386         );
    387 
    388         assert_eq!(
    389             err,
    390             RadrootsRuntimeDistributionError::UnknownRuntime("missing-runtime".to_string())
    391         );
    392     }
    393 
    394     #[test]
    395     fn rejects_unknown_channel() {
    396         let resolver =
    397             RadrootsRuntimeDistributionResolver::parse_str(CONTRACT).expect("parse contract");
    398 
    399         let err = resolve_error(
    400             &resolver,
    401             RuntimeArtifactRequest {
    402                 runtime_id: "cli",
    403                 os: "linux",
    404                 arch: "amd64",
    405                 version: "0.1.0-alpha.2",
    406                 channel: Some("beta"),
    407             },
    408         );
    409 
    410         assert_eq!(
    411             err,
    412             RadrootsRuntimeDistributionError::UnknownChannel("beta".to_string())
    413         );
    414     }
    415 
    416     #[test]
    417     fn rejects_unsupported_platform() {
    418         let resolver =
    419             RadrootsRuntimeDistributionResolver::parse_str(CONTRACT).expect("parse contract");
    420 
    421         let err = resolver
    422             .resolve_artifact(&RuntimeArtifactRequest {
    423                 runtime_id: "radrootsd",
    424                 os: "windows",
    425                 arch: "amd64",
    426                 version: "0.1.0-alpha.2",
    427                 channel: Some("stable"),
    428             })
    429             .expect_err("windows target should be unsupported");
    430 
    431         assert_eq!(
    432             err,
    433             RadrootsRuntimeDistributionError::UnsupportedPlatform {
    434                 runtime_id: "radrootsd".to_string(),
    435                 os: "windows".to_string(),
    436                 arch: "amd64".to_string(),
    437             }
    438         );
    439     }
    440 
    441     #[test]
    442     fn rejects_runtime_with_missing_target_set() {
    443         let mut contract = contract_value();
    444         let runtime = contract["runtime"]
    445             .as_array_mut()
    446             .expect("runtime array")
    447             .iter_mut()
    448             .find(|runtime| runtime["id"].as_str() == Some("community-app-ios"))
    449             .expect("ios runtime");
    450         runtime["human_installable"] = Value::Boolean(true);
    451 
    452         let resolver = resolver_from_value(contract);
    453         let err = resolve_error(
    454             &resolver,
    455             RuntimeArtifactRequest {
    456                 runtime_id: "community-app-ios",
    457                 os: "ios",
    458                 arch: "arm64",
    459                 version: "0.1.0-alpha.2",
    460                 channel: Some("stable"),
    461             },
    462         );
    463 
    464         assert_eq!(
    465             err,
    466             RadrootsRuntimeDistributionError::MissingTargetSet("community-app-ios".to_string())
    467         );
    468     }
    469 
    470     #[test]
    471     fn rejects_unknown_artifact_adapter() {
    472         let mut contract = contract_value();
    473         let runtime = contract["runtime"]
    474             .as_array_mut()
    475             .expect("runtime array")
    476             .iter_mut()
    477             .find(|runtime| runtime["id"].as_str() == Some("cli"))
    478             .expect("cli runtime");
    479         runtime["artifact_adapter"] = Value::String("missing_adapter".to_string());
    480 
    481         let resolver = resolver_from_value(contract);
    482         let err = resolve_error(
    483             &resolver,
    484             RuntimeArtifactRequest {
    485                 runtime_id: "cli",
    486                 os: "linux",
    487                 arch: "amd64",
    488                 version: "0.1.0-alpha.2",
    489                 channel: Some("stable"),
    490             },
    491         );
    492 
    493         assert_eq!(
    494             err,
    495             RadrootsRuntimeDistributionError::UnknownArtifactAdapter {
    496                 runtime_id: "cli".to_string(),
    497                 adapter_id: "missing_adapter".to_string(),
    498             }
    499         );
    500     }
    501 
    502     #[test]
    503     fn rejects_missing_target_set_definition() {
    504         let mut contract = contract_value();
    505         let runtime = contract["runtime"]
    506             .as_array_mut()
    507             .expect("runtime array")
    508             .iter_mut()
    509             .find(|runtime| runtime["id"].as_str() == Some("cli"))
    510             .expect("cli runtime");
    511         runtime["target_set"] = Value::String("missing-target-set".to_string());
    512 
    513         let resolver = resolver_from_value(contract);
    514         let err = resolve_error(
    515             &resolver,
    516             RuntimeArtifactRequest {
    517                 runtime_id: "cli",
    518                 os: "linux",
    519                 arch: "amd64",
    520                 version: "0.1.0-alpha.2",
    521                 channel: Some("stable"),
    522             },
    523         );
    524 
    525         assert_eq!(
    526             err,
    527             RadrootsRuntimeDistributionError::UnsupportedPlatform {
    528                 runtime_id: "cli".to_string(),
    529                 os: "linux".to_string(),
    530                 arch: "amd64".to_string(),
    531             }
    532         );
    533     }
    534 
    535     #[test]
    536     fn rejects_target_set_with_unknown_target() {
    537         let mut contract = contract_value();
    538         contract["target_sets"]["cli_default"]["targets"] =
    539             Value::Array(vec![Value::String("missing-target".to_string())]);
    540 
    541         let resolver = resolver_from_value(contract);
    542         let err = resolve_error(
    543             &resolver,
    544             RuntimeArtifactRequest {
    545                 runtime_id: "cli",
    546                 os: "linux",
    547                 arch: "amd64",
    548                 version: "0.1.0-alpha.2",
    549                 channel: Some("stable"),
    550             },
    551         );
    552 
    553         assert_eq!(
    554             err,
    555             RadrootsRuntimeDistributionError::UnknownTarget {
    556                 runtime_id: "cli".to_string(),
    557                 target_set_id: "cli_default".to_string(),
    558                 target_id: "missing-target".to_string(),
    559             }
    560         );
    561     }
    562 
    563     #[test]
    564     fn infers_archive_format_from_single_supported_adapter_format() {
    565         let mut contract = contract_value();
    566         contract["targets"]["x86_64-unknown-linux-gnu"]
    567             .as_table_mut()
    568             .expect("target table")
    569             .remove("archive_format");
    570         contract["artifact_adapters"]["rust_binary_archive"]["supported_archive_formats"] =
    571             Value::Array(vec![Value::String("tar.gz".to_string())]);
    572 
    573         let resolver = resolver_from_value(contract);
    574         let artifact = resolver
    575             .resolve_artifact(&RuntimeArtifactRequest {
    576                 runtime_id: "cli",
    577                 os: "linux",
    578                 arch: "amd64",
    579                 version: "0.1.0-alpha.2",
    580                 channel: Some("stable"),
    581             })
    582             .expect("single supported format should be inferred");
    583 
    584         assert_eq!(artifact.archive_format, "tar.gz");
    585         assert_eq!(artifact.archive_extension, ".tar.gz");
    586     }
    587 
    588     #[test]
    589     fn rejects_unknown_archive_format_reference() {
    590         let mut contract = contract_value();
    591         contract["targets"]["x86_64-unknown-linux-gnu"]["archive_format"] =
    592             Value::String("tar.xz".to_string());
    593 
    594         let resolver = resolver_from_value(contract);
    595         let err = resolve_error(
    596             &resolver,
    597             RuntimeArtifactRequest {
    598                 runtime_id: "cli",
    599                 os: "linux",
    600                 arch: "amd64",
    601                 version: "0.1.0-alpha.2",
    602                 channel: Some("stable"),
    603             },
    604         );
    605 
    606         assert_eq!(
    607             err,
    608             RadrootsRuntimeDistributionError::UnknownArchiveFormat {
    609                 target_id: "x86_64-unknown-linux-gnu".to_string(),
    610                 archive_format_id: "tar.xz".to_string(),
    611             }
    612         );
    613     }
    614 
    615     #[test]
    616     fn rejects_missing_archive_format_when_adapter_is_ambiguous() {
    617         let mut contract = contract_value();
    618         contract["targets"]["aarch64-apple-darwin"]
    619             .as_table_mut()
    620             .expect("target table")
    621             .remove("archive_format");
    622 
    623         let resolver = resolver_from_value(contract);
    624         let err = resolve_error(
    625             &resolver,
    626             RuntimeArtifactRequest {
    627                 runtime_id: "community-app-desktop",
    628                 os: "macos",
    629                 arch: "arm64",
    630                 version: "0.1.0-alpha.2",
    631                 channel: Some("stable"),
    632             },
    633         );
    634 
    635         assert_eq!(
    636             err,
    637             RadrootsRuntimeDistributionError::MissingArchiveFormat {
    638                 runtime_id: "community-app-desktop".to_string(),
    639                 target_id: "aarch64-apple-darwin".to_string(),
    640             }
    641         );
    642     }
    643 }