diff --git a/Cargo.lock b/Cargo.lock index 6389c5f135..42854ff5f4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6332,6 +6332,7 @@ dependencies = [ "ryu", "serde", "serde_json", + "sha2 0.11.0", "socket2", "taffy", "temporal_rs", diff --git a/changelog.d/8816-runtime-library-build-stamp.md b/changelog.d/8816-runtime-library-build-stamp.md new file mode 100644 index 0000000000..4946d68fab --- /dev/null +++ b/changelog.d/8816-runtime-library-build-stamp.md @@ -0,0 +1,4 @@ +Fixed stale or mismatched `libperry_runtime` archives passing `perry doctor` and +then failing during native linking with undefined runtime symbols. Runtime +archives now carry a compiler build identity that `perry doctor` and compile +pipelines verify before linking, with actionable rebuild and reinstall guidance. diff --git a/crates/perry-runtime/Cargo.toml b/crates/perry-runtime/Cargo.toml index f02ee16750..f948f9752f 100644 --- a/crates/perry-runtime/Cargo.toml +++ b/crates/perry-runtime/Cargo.toml @@ -391,6 +391,9 @@ mach2 = "0.6" # See `build.rs` and issue #395 for the rationale. [build-dependencies] perry-dispatch = { path = "../perry-dispatch" } +# Build-time only: fingerprints the compiler/runtime source contract embedded +# in libperry_runtime so the CLI can reject a stale archive before linking. +sha2 = "0.11" # Build-time only (does NOT ship in the runtime binary): generates the WHATWG # single-byte TextDecoder index tables so they are always spec-accurate. Only # the generated `[u16; 128]` arrays land in the binary. Already vetted in the diff --git a/crates/perry-runtime/build.rs b/crates/perry-runtime/build.rs index f33d16410c..122a29e245 100644 --- a/crates/perry-runtime/build.rs +++ b/crates/perry-runtime/build.rs @@ -48,8 +48,172 @@ //! line — see `src/stub_diag.rs` for the env-var policy. use perry_dispatch::{ArgKind, MethodRow, ReturnKind}; +use sha2::{Digest, Sha256}; use std::collections::HashSet; use std::fmt::Write; +use std::path::{Path, PathBuf}; +use std::process::Command; + +/// Source trees that define the compiler <-> runtime contract. A clean git +/// checkout uses the commit as its build id; dirty/source-only builds hash +/// these inputs so rebuilding the compiler without rebuilding the archive is +/// still detected even though the package version did not change. +const RUNTIME_BUILD_INPUTS: &[&str] = &[ + "Cargo.toml", + "Cargo.lock", + "crates/perry-dispatch/Cargo.toml", + "crates/perry-dispatch/src", + "crates/perry/Cargo.toml", + "crates/perry/src", + "crates/perry-codegen/Cargo.toml", + "crates/perry-codegen/src", + "crates/perry-hir/Cargo.toml", + "crates/perry-hir/src", + "crates/perry-transform/Cargo.toml", + "crates/perry-transform/src", + "crates/perry-runtime/Cargo.toml", + "crates/perry-runtime/build.rs", + "crates/perry-runtime/src", +]; + +/// `cargo package` builds the crate from an isolated directory without the +/// workspace siblings above. Hash the packaged runtime itself in that layout; +/// the compiler and static wrapper both consume this same crate artifact. +const PACKAGED_RUNTIME_BUILD_INPUTS: &[&str] = &["Cargo.toml", "build.rs", "src"]; + +fn command_stdout(root: &Path, args: &[&str]) -> Option { + let output = Command::new("git") + .arg("-C") + .arg(root) + .args(args) + .output() + .ok()?; + if !output.status.success() { + return None; + } + Some(String::from_utf8(output.stdout).ok()?.trim().to_string()) +} + +fn sanitize_build_id(value: &str) -> String { + value + .chars() + .take(128) + .map(|c| { + if c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-' | ':') { + c + } else { + '_' + } + }) + .collect() +} + +fn collect_source_files(root: &Path, path: &Path, out: &mut Vec) { + let Ok(metadata) = std::fs::metadata(path) else { + return; + }; + if metadata.is_file() { + out.push(path.to_path_buf()); + return; + } + let Ok(entries) = std::fs::read_dir(path) else { + return; + }; + let mut entries: Vec<_> = entries.flatten().collect(); + entries.sort_by_key(|entry| entry.file_name()); + for entry in entries { + let child = entry.path(); + if child.strip_prefix(root).ok().is_some_and(|relative| { + relative + .components() + .any(|part| part.as_os_str() == "target" || part.as_os_str() == ".git") + }) { + continue; + } + collect_source_files(root, &child, out); + } +} + +fn source_build_id(root: &Path, inputs: &[&str]) -> String { + let mut files = Vec::new(); + for relative in inputs { + collect_source_files(root, &root.join(relative), &mut files); + } + files.sort(); + files.dedup(); + + let mut hasher = Sha256::new(); + hasher.update(b"perry-runtime-build-inputs-v1\0"); + for path in files { + println!("cargo:rerun-if-changed={}", path.display()); + let relative = path.strip_prefix(root).unwrap_or(&path); + hasher.update(relative.to_string_lossy().replace('\\', "/").as_bytes()); + hasher.update(b"\0"); + match std::fs::read(&path) { + Ok(bytes) => { + hasher.update((bytes.len() as u64).to_le_bytes()); + hasher.update(bytes); + } + Err(_) => hasher.update(b"unreadable"), + } + hasher.update(b"\0"); + } + let mut hex = String::with_capacity(64); + for byte in hasher.finalize() { + write!(hex, "{byte:02x}").expect("write source fingerprint"); + } + format!("src:{hex}") +} + +fn emit_runtime_build_id() { + println!("cargo:rerun-if-env-changed=PERRY_BUILD_COMMIT"); + let manifest_dir = + PathBuf::from(std::env::var_os("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR not set")); + let workspace_candidate = manifest_dir.join("../.."); + let workspace_layout = workspace_candidate + .join("crates/perry-runtime/Cargo.toml") + .is_file(); + let (root, inputs) = if workspace_layout { + (workspace_candidate, RUNTIME_BUILD_INPUTS) + } else { + (manifest_dir, PACKAGED_RUNTIME_BUILD_INPUTS) + }; + let root = root.canonicalize().unwrap_or(root); + + // Make branch/commit changes rerun this build script even when the source + // files themselves are byte-identical (for example after a rebase). + if workspace_layout { + if let Some(git_head) = command_stdout(&root, &["rev-parse", "--git-path", "HEAD"]) { + println!("cargo:rerun-if-changed={}", root.join(git_head).display()); + } + if let Some(symbolic_ref) = command_stdout(&root, &["symbolic-ref", "-q", "HEAD"]) { + if let Some(git_ref) = + command_stdout(&root, &["rev-parse", "--git-path", &symbolic_ref]) + { + println!("cargo:rerun-if-changed={}", root.join(git_ref).display()); + } + } + } + + let explicit = std::env::var("PERRY_BUILD_COMMIT") + .ok() + .filter(|value| !value.trim().is_empty()) + .map(|value| format!("git:{}", sanitize_build_id(value.trim()))); + + let source_id = source_build_id(&root, inputs); + let clean_commit = workspace_layout + .then(|| command_stdout(&root, &["rev-parse", "--verify", "HEAD"])) + .flatten() + .filter(|_| { + let mut args = vec!["status", "--porcelain", "--untracked-files=normal", "--"]; + args.extend_from_slice(inputs); + command_stdout(&root, &args).is_some_and(|status| status.is_empty()) + }) + .map(|commit| format!("git:{}", sanitize_build_id(&commit))); + + let build_id = explicit.or(clean_commit).unwrap_or(source_id); + println!("cargo:rustc-env=PERRY_RUNTIME_BUILD_ID={build_id}"); +} fn arg_kind_rust_type(k: ArgKind) -> &'static str { match k { @@ -372,6 +536,7 @@ fn generate_single_byte_encodings(out_dir: &str) { fn main() { println!("cargo:rerun-if-changed=build.rs"); println!("cargo:rerun-if-changed=../perry-dispatch/src/lib.rs"); + emit_runtime_build_id(); let out_dir = std::env::var("OUT_DIR").expect("OUT_DIR not set"); generate_single_byte_encodings(&out_dir); diff --git a/crates/perry-runtime/src/build_stamp.rs b/crates/perry-runtime/src/build_stamp.rs new file mode 100644 index 0000000000..2bcc4e5af2 --- /dev/null +++ b/crates/perry-runtime/src/build_stamp.rs @@ -0,0 +1,30 @@ +//! Build identity embedded in every `libperry_runtime` archive. +//! +//! The compiler reads this marker before linking. Keeping it in the runtime +//! crate (rather than a packaging sidecar) means copied archives, Cargo-built +//! archives, compressed npm archives, and platform-suffixed archives all carry +//! their identity with them. + +/// Revision/fingerprint produced by `build.rs` from the compiler/runtime +/// contract sources. Clean checkouts use `git:`; dirty or source-only +/// builds use `src:`. +pub const PERRY_RUNTIME_BUILD_ID: &str = env!("PERRY_RUNTIME_BUILD_ID"); + +/// NUL-terminated record deliberately stored as plain ASCII so the CLI can +/// find it by streaming over either an ar archive (`.a`) or a COFF library +/// (`.lib`) without invoking platform-specific archive tools. +pub const PERRY_RUNTIME_BUILD_STAMP: &str = concat!( + "PERRY_RUNTIME_BUILD_STAMP_V1|version=", + env!("CARGO_PKG_VERSION"), + "|build=", + env!("PERRY_RUNTIME_BUILD_ID"), + "\0", +); + +// `#[used]` keeps both this reference and its string data in the rlib object +// set copied by perry-runtime-static into libperry_runtime. The symbol stays +// mangled so linking a stdlib archive that also contains perry-runtime cannot +// create a duplicate public C symbol. +#[used] +#[doc(hidden)] +pub static PERRY_RUNTIME_BUILD_STAMP_EMBEDDED: &[u8] = PERRY_RUNTIME_BUILD_STAMP.as_bytes(); diff --git a/crates/perry-runtime/src/lib.rs b/crates/perry-runtime/src/lib.rs index c88131b72f..202d0f25b2 100644 --- a/crates/perry-runtime/src/lib.rs +++ b/crates/perry-runtime/src/lib.rs @@ -51,6 +51,8 @@ pub mod atomics_futex; pub mod bigint; pub mod r#box; pub mod buffer; +mod build_stamp; +pub use build_stamp::{PERRY_RUNTIME_BUILD_ID, PERRY_RUNTIME_BUILD_STAMP}; pub mod builtins; pub mod bun_compat; pub mod bun_ffi; diff --git a/crates/perry/src/commands/compile.rs b/crates/perry/src/commands/compile.rs index 9eeff0f508..3838f24e58 100644 --- a/crates/perry/src/commands/compile.rs +++ b/crates/perry/src/commands/compile.rs @@ -53,6 +53,7 @@ mod windows_target; // reuses the subpath-imports + tsconfig-paths resolvers for `#` specifiers. pub(crate) mod resolve; mod resources; +mod runtime_compat; mod sandbox_buildrs; mod shared_tokio; mod strip_dedup; @@ -112,6 +113,10 @@ use resolve::{ is_recognized_text_asset, parse_native_library_manifest, parse_package_specifier, resolve_import, }; +pub(crate) use runtime_compat::{ + ensure_runtime_library_compatible, runtime_library_diagnostic, runtime_library_status, + RuntimeLibraryStatus, +}; use size_report::emit_size_report; use strip_dedup::{ dedup_native_lib_for_tier3, dedup_runtime_for_tier3, dedup_stdlib_for_tier3, diff --git a/crates/perry/src/commands/compile/library_search.rs b/crates/perry/src/commands/compile/library_search.rs index b0a242c857..7c8a1517ed 100644 --- a/crates/perry/src/commands/compile/library_search.rs +++ b/crates/perry/src/commands/compile/library_search.rs @@ -1196,7 +1196,7 @@ pub(super) fn find_runtime_library(target: Option<&str>) -> Result { "Could not find {lib}{extra}.\n\ Searched:\n{list}\n\n\ Fixes:\n\ - - From the perry workspace: cargo build --release -p perry-runtime{tf}\n\ + - From the perry workspace: cargo build --release -p perry-runtime-static{tf}\n\ - Out-of-tree install: set PERRY_RUNTIME_DIR to the directory containing {lib}\n\ (e.g. export PERRY_RUNTIME_DIR=/path/to/perry/target/release)", lib = lib_name, diff --git a/crates/perry/src/commands/compile/run_pipeline.rs b/crates/perry/src/commands/compile/run_pipeline.rs index 636f22284a..2d26c29371 100644 --- a/crates/perry/src/commands/compile/run_pipeline.rs +++ b/crates/perry/src/commands/compile/run_pipeline.rs @@ -6210,6 +6210,13 @@ pub fn run_with_parse_cache( // emits `perry_module_init` instead of `main` (see is_dylib branch in // codegen/entry.rs, which now also covers `staticlib`). if is_staticlib { + let runtime_lib_for_manifest = optimized_libs + .runtime + .clone() + .or_else(|| find_runtime_library(target.as_deref()).ok()); + if let Some(runtime) = &runtime_lib_for_manifest { + ensure_runtime_library_compatible(runtime)?; + } let windows_target = is_windows_target(target.as_deref()); // Best-effort: drop a stale archive first so `ar` doesn't append to a // previous build's contents. @@ -6283,10 +6290,6 @@ pub fn run_with_parse_cache( "path": abs.display().to_string(), })); }; - let runtime_lib_for_manifest = optimized_libs - .runtime - .clone() - .or_else(|| find_runtime_library(target.as_deref()).ok()); if let Some(p) = &runtime_lib_for_manifest { push_archive(&mut link_archives, "runtime", p); } @@ -6596,6 +6599,12 @@ pub fn run_with_parse_cache( } else { find_runtime_library(target.as_deref())? }; + // #8752: discovery only proves that an archive exists. A runtime copied + // from an older compiler build can be found successfully and then fail at + // the final link with undefined symbols for newly emitted entrypoints. + // Read its embedded build stamp now so the error names the stale archive + // and both builds before invoking the platform linker. + ensure_runtime_library_compatible(&runtime_lib)?; // #1383 — under --enable-geisterhand, prefer the geisterhand-built stdlib // over the auto-optimized one. `build_geisterhand_libs` (already run above // when selecting `runtime_lib`) compiles perry-stdlib into target/geisterhand diff --git a/crates/perry/src/commands/compile/runtime_compat.rs b/crates/perry/src/commands/compile/runtime_compat.rs new file mode 100644 index 0000000000..b9c60df469 --- /dev/null +++ b/crates/perry/src/commands/compile/runtime_compat.rs @@ -0,0 +1,278 @@ +//! Compiler/runtime archive compatibility stamping (#8752). +//! +//! A stale `libperry_runtime` used to pass discovery and fail much later with +//! undefined symbols. The runtime now embeds a small version/build record; +//! this module streams the archive to find it and rejects skew before linking. + +use anyhow::{bail, Result}; +use std::fmt; +use std::fs::File; +use std::io::{self, BufReader, Read}; +use std::path::Path; + +const STAMP_MAGIC: &str = "PERRY_RUNTIME_BUILD_STAMP_V1"; +const STAMP_PREFIX: &[u8] = b"PERRY_RUNTIME_BUILD_STAMP_V1|"; +const MAX_STAMP_LEN: usize = 512; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub(crate) struct RuntimeBuildStamp { + version: String, + build_id: String, +} + +impl RuntimeBuildStamp { + fn current() -> Self { + Self { + version: env!("CARGO_PKG_VERSION").to_string(), + build_id: perry_runtime::PERRY_RUNTIME_BUILD_ID.to_string(), + } + } + + fn parse(bytes: &[u8]) -> std::result::Result { + let text = + std::str::from_utf8(bytes).map_err(|error| format!("stamp is not UTF-8: {error}"))?; + let mut fields = text.split('|'); + if fields.next() != Some(STAMP_MAGIC) { + return Err("unexpected stamp format".to_string()); + } + let version = fields + .next() + .and_then(|field| field.strip_prefix("version=")) + .filter(|value| !value.is_empty()) + .ok_or_else(|| "stamp has no version".to_string())?; + let build_id = fields + .next() + .and_then(|field| field.strip_prefix("build=")) + .filter(|value| !value.is_empty()) + .ok_or_else(|| "stamp has no build id".to_string())?; + if fields.next().is_some() { + return Err("stamp has unexpected fields".to_string()); + } + Ok(Self { + version: version.to_string(), + build_id: build_id.to_string(), + }) + } + + fn short_build_id(&self) -> String { + let (kind, value) = self + .build_id + .split_once(':') + .unwrap_or(("build", self.build_id.as_str())); + let short: String = value.chars().take(12).collect(); + match kind { + "git" => format!("commit {short}"), + "src" => format!("source {short}"), + _ => format!("build {short}"), + } + } +} + +impl fmt::Display for RuntimeBuildStamp { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "v{} ({})", self.version, self.short_build_id()) + } +} + +#[derive(Debug)] +pub(crate) enum RuntimeLibraryStatus { + Compatible(RuntimeBuildStamp), + MissingStamp, + MalformedStamp(String), + Mismatch { + expected: RuntimeBuildStamp, + found: RuntimeBuildStamp, + }, + Unreadable(io::Error), +} + +enum ScannedStamp { + Missing, + Bytes(Vec), + Unterminated, +} + +/// Stream instead of reading the whole archive into memory. Release runtime +/// archives can be tens of megabytes, while the record is at most 512 bytes. +fn scan_stamp(path: &Path) -> io::Result { + let mut reader = BufReader::new(File::open(path)?); + let mut buffer = [0_u8; 64 * 1024]; + let mut prefix_match = 0_usize; + let mut record: Option> = None; + + loop { + let read = reader.read(&mut buffer)?; + if read == 0 { + return Ok(match record { + Some(_) => ScannedStamp::Unterminated, + None => ScannedStamp::Missing, + }); + } + for &byte in &buffer[..read] { + if let Some(bytes) = record.as_mut() { + if byte == 0 { + return Ok(ScannedStamp::Bytes(std::mem::take(bytes))); + } + if bytes.len() >= MAX_STAMP_LEN { + return Ok(ScannedStamp::Unterminated); + } + bytes.push(byte); + continue; + } + + if byte == STAMP_PREFIX[prefix_match] { + prefix_match += 1; + if prefix_match == STAMP_PREFIX.len() { + record = Some(STAMP_PREFIX.to_vec()); + prefix_match = 0; + } + } else { + // The marker has no multi-byte self-overlap; preserving a + // leading `P` is enough to handle a mismatch at a new prefix. + prefix_match = usize::from(byte == STAMP_PREFIX[0]); + } + } + } +} + +pub(crate) fn runtime_library_status(path: &Path) -> RuntimeLibraryStatus { + let found = match scan_stamp(path) { + Ok(ScannedStamp::Missing) => return RuntimeLibraryStatus::MissingStamp, + Ok(ScannedStamp::Unterminated) => { + return RuntimeLibraryStatus::MalformedStamp( + "embedded stamp is unterminated or too long".to_string(), + ) + } + Ok(ScannedStamp::Bytes(bytes)) => match RuntimeBuildStamp::parse(&bytes) { + Ok(stamp) => stamp, + Err(error) => return RuntimeLibraryStatus::MalformedStamp(error), + }, + Err(error) => return RuntimeLibraryStatus::Unreadable(error), + }; + let expected = RuntimeBuildStamp::current(); + if found == expected { + RuntimeLibraryStatus::Compatible(found) + } else { + RuntimeLibraryStatus::Mismatch { expected, found } + } +} + +pub(crate) fn runtime_library_diagnostic(path: &Path, status: &RuntimeLibraryStatus) -> String { + let expected = RuntimeBuildStamp::current(); + let reason = match status { + RuntimeLibraryStatus::Compatible(found) => { + return format!("{} ({found}, matches this Perry)", path.display()) + } + RuntimeLibraryStatus::MissingStamp => format!( + "library build: unknown ({} has no build stamp and predates compatibility checks)\n Perry build: {expected}", + path.display() + ), + RuntimeLibraryStatus::MalformedStamp(error) => format!( + "library build: unknown (invalid stamp in {}: {error})\n Perry build: {expected}", + path.display() + ), + RuntimeLibraryStatus::Mismatch { expected, found } => format!( + "library build: {found}\n Perry build: {expected}\n library: {}", + path.display() + ), + RuntimeLibraryStatus::Unreadable(error) => format!( + "could not inspect {}: {error}\n Perry build: {expected}", + path.display() + ), + }; + + format!( + "runtime library does not match this Perry compiler:\n {reason}\n\ + The archive may be stale. Rebuild it with \ + `cargo build --release -p perry-runtime-static`, then replace {}, \ + or reinstall Perry so the binary and libraries come from the same package.", + path.display() + ) +} + +pub(crate) fn ensure_runtime_library_compatible(path: &Path) -> Result<()> { + let status = runtime_library_status(path); + if matches!(&status, RuntimeLibraryStatus::Compatible(_)) { + return Ok(()); + } + bail!(runtime_library_diagnostic(path, &status)) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Write; + + fn write_archive(bytes: &[u8]) -> tempfile::NamedTempFile { + let mut file = tempfile::NamedTempFile::new().expect("create archive fixture"); + file.write_all(bytes).expect("write archive fixture"); + file + } + + fn encoded(stamp: &RuntimeBuildStamp) -> Vec { + format!( + "noise{STAMP_MAGIC}|version={}|build={}\0trailer", + stamp.version, stamp.build_id + ) + .into_bytes() + } + + #[test] + fn accepts_matching_embedded_stamp() { + let expected = RuntimeBuildStamp::current(); + let archive = write_archive(&encoded(&expected)); + assert!(matches!( + runtime_library_status(archive.path()), + RuntimeLibraryStatus::Compatible(found) if found == expected + )); + } + + #[test] + fn finds_stamp_across_reader_chunk_boundary() { + let expected = RuntimeBuildStamp::current(); + let mut bytes = vec![b'x'; 64 * 1024 - 7]; + bytes.extend(encoded(&expected)); + let archive = write_archive(&bytes); + assert!(matches!( + runtime_library_status(archive.path()), + RuntimeLibraryStatus::Compatible(_) + )); + } + + #[test] + fn rejects_unstamped_legacy_archive_with_refresh_help() { + let archive = write_archive(b"!\nlegacy runtime contents"); + let status = runtime_library_status(archive.path()); + assert!(matches!(&status, RuntimeLibraryStatus::MissingStamp)); + let diagnostic = runtime_library_diagnostic(archive.path(), &status); + assert!(diagnostic.contains("has no build stamp")); + assert!(diagnostic.contains("perry-runtime-static")); + assert!(diagnostic.contains(&archive.path().display().to_string())); + } + + #[test] + fn rejects_mismatched_archive_and_names_both_builds() { + let expected = RuntimeBuildStamp::current(); + let stale = RuntimeBuildStamp { + version: "0.0.1".to_string(), + build_id: "git:1111111111111111111111111111111111111111".to_string(), + }; + let archive = write_archive(&encoded(&stale)); + let status = runtime_library_status(archive.path()); + assert!(matches!(&status, RuntimeLibraryStatus::Mismatch { .. })); + let diagnostic = runtime_library_diagnostic(archive.path(), &status); + assert!(diagnostic.contains(&stale.to_string())); + assert!(diagnostic.contains(&expected.to_string())); + assert!(diagnostic.contains("archive may be stale")); + } + + #[test] + fn rejects_unterminated_stamp() { + let archive = + write_archive(format!("{STAMP_MAGIC}|version=1.0.0|build=git:abc").as_bytes()); + assert!(matches!( + runtime_library_status(archive.path()), + RuntimeLibraryStatus::MalformedStamp(_) + )); + } +} diff --git a/crates/perry/src/commands/doctor.rs b/crates/perry/src/commands/doctor.rs index 4b8d99c3fc..fa178912c9 100644 --- a/crates/perry/src/commands/doctor.rs +++ b/crates/perry/src/commands/doctor.rs @@ -272,16 +272,30 @@ fn check_runtime_library() -> CheckResult { "libperry_runtime.a" }; if let Some(path) = crate::commands::compile::find_library(lib_name, None) { - return CheckResult { - name: "runtime library".to_string(), - status: CheckStatus::Ok, - details: Some(path.display().to_string()), + let library_status = crate::commands::compile::runtime_library_status(&path); + return match &library_status { + crate::commands::compile::RuntimeLibraryStatus::Compatible(_) => CheckResult { + name: "runtime library".to_string(), + status: CheckStatus::Ok, + details: Some(crate::commands::compile::runtime_library_diagnostic( + &path, + &library_status, + )), + }, + _ => CheckResult { + name: "runtime library".to_string(), + status: CheckStatus::Error, + details: Some(crate::commands::compile::runtime_library_diagnostic( + &path, + &library_status, + )), + }, }; } CheckResult { name: "runtime library".to_string(), status: CheckStatus::Warning, - details: Some("not found - run: cargo build --release -p perry-runtime".to_string()), + details: Some("not found - run: cargo build --release -p perry-runtime-static".to_string()), } }