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 }