Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions tools/js-runtimes/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ pub const ORDER: [Runtime; 4] = [Runtime::Bun, Runtime::Deno, Runtime::Pnpm, Run
pub const LOCKED: [Runtime; 3] = [Runtime::Bun, Runtime::Deno, Runtime::Pnpm];

impl Runtime {
/// The runtime's command-line binary name.
pub fn name(self) -> &'static str {
match self {
Runtime::Bun => "bun",
Expand All @@ -41,6 +42,7 @@ impl Runtime {
}
}

/// Looks up a runtime by its binary name, e.g. `"bun"`.
pub fn parse(s: &str) -> Option<Runtime> {
ORDER.iter().copied().find(|r| r.name() == s)
}
Expand All @@ -67,6 +69,7 @@ impl Runtime {
}

impl fmt::Display for Runtime {
/// Displays the runtime using its binary name.
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.name())
}
Expand Down Expand Up @@ -131,6 +134,7 @@ pub enum EnvKind {
}

impl EnvKind {
/// The short label used in output, e.g. `"mise"`.
pub fn name(self) -> &'static str {
match self {
EnvKind::Mise => "mise",
Expand Down Expand Up @@ -158,6 +162,7 @@ pub struct EnvDef {
}

impl EnvDef {
/// The most-preferred runtime this definition provisions, if any.
pub fn best(&self) -> Option<Runtime> {
pick(|r| self.provides.contains(&r))
}
Expand Down Expand Up @@ -424,6 +429,7 @@ pub enum Verdict {
None,
}

/// Classifies one environment definition against the preference order.
pub fn verdict(def: &EnvDef) -> Verdict {
match def.best() {
Some(Runtime::Bun) => Verdict::Best,
Expand All @@ -437,6 +443,7 @@ pub fn verdict(def: &EnvDef) -> Verdict {
mod tests {
use super::*;

/// The preference order is Bun, Deno, pnpm, npm, and `pick` honours it.
#[test]
fn order_is_bun_deno_pnpm_npm() {
assert_eq!(ORDER.map(Runtime::name), ["bun", "deno", "pnpm", "npm"]);
Expand All @@ -450,12 +457,14 @@ mod tests {
assert_eq!(pick(|_| false), None);
}

/// npm has no tracked lock; every `LOCKED` runtime does.
#[test]
fn npm_keeps_no_lock() {
assert_eq!(Runtime::Npm.lockfile(), None);
assert!(LOCKED.iter().all(|r| r.lockfile().is_some()));
}

/// `canon` classifies documents the same way `ijson-jcs check` does.
#[test]
fn canon_matches_ijson_jcs_check() {
assert_eq!(canon(b"{\"a\":1,\"b\":[true,null]}\n"), Canon::Canonical);
Expand All @@ -472,6 +481,7 @@ mod tests {
assert!(matches!(canon(b"not json"), Canon::Invalid(_)));
}

/// `has_test_script` finds a string `scripts.test`, and only that.
#[test]
fn test_script_lookup() {
assert!(has_test_script(br#"{"scripts":{"test":"echo ok"}}"#));
Expand All @@ -480,6 +490,7 @@ mod tests {
assert!(!has_test_script(br#"{}"#));
}

/// Only the `[tools]` table of a mise.toml is read for runtimes.
#[test]
fn mise_tools_table_only() {
let t = "[settings]\nbun = \"no\"\n[tools]\nzig = \"latest\"\nbun = \"1.4.2\" # js\n\"npm:pnpm\" = \"10\"\n[env]\ndeno = \"x\"\n";
Expand All @@ -491,6 +502,7 @@ mod tests {
assert!(mise_provides("[tools]\njust = \"latest\"\n").is_empty());
}

/// Provisioning requires a base/copied image or install step, not mere use.
#[test]
fn container_provisioning_not_mere_use() {
assert_eq!(
Expand Down Expand Up @@ -523,6 +535,7 @@ mod tests {
assert!(container_provides("# FROM oven/bun\nFROM alpine\n").is_empty());
}

/// Guix package names are matched ignoring commented-out lines.
#[test]
fn guix_names_packages_ignoring_comments() {
assert_eq!(
Expand All @@ -535,6 +548,7 @@ mod tests {
);
}

/// `verdict` covers the Best, Below, Fallback and None outcomes.
#[test]
fn verdicts() {
let d = |kind, provides: Vec<Runtime>| EnvDef {
Expand Down
19 changes: 19 additions & 0 deletions tools/js-runtimes/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,20 @@ const USAGE: &str = "usage: js-runtimes regen [DIR] | check bun|deno|pnpm [DIR]

struct Fail(u8, String);

/// An exit-code-1 failure: a finding was reported.
fn finding(msg: impl Into<String>) -> Fail {
Fail(1, msg.into())
}
/// An exit-code-2 failure: the check could not run at all.
fn cannot(msg: impl Into<String>) -> Fail {
Fail(2, msg.into())
}
/// Prints an informational line prefixed with the tool's name.
fn note(msg: &str) {
println!("js-runtimes: {msg}");
}

/// Whether `bin` is on `PATH` and executable.
fn on_path(bin: &str) -> bool {
let Some(paths) = env::var_os("PATH") else {
return false;
Expand All @@ -53,16 +57,19 @@ fn on_path(bin: &str) -> bool {
})
}

/// Whether the Unix permission bits mark this file executable.
#[cfg(unix)]
fn is_executable(m: &fs::Metadata) -> bool {
use std::os::unix::fs::PermissionsExt;
m.permissions().mode() & 0o111 != 0
}
/// Non-Unix platforms have no executable bit to check.
#[cfg(not(unix))]
fn is_executable(_: &fs::Metadata) -> bool {
true
}

/// Requires `bin` to be on `PATH`, else a cannot-run failure.
fn need(bin: &str) -> Result<(), Fail> {
if on_path(bin) {
Ok(())
Expand All @@ -88,6 +95,7 @@ fn quiet(dir: &Path, argv: &[&str]) -> Result<bool, Fail> {
Ok(out.status.success())
}

/// Run with inherited stdout/stderr, e.g. for the `test` script.
fn loud(dir: &Path, argv: &[&str]) -> Result<bool, Fail> {
let st = Command::new(argv[0])
.args(&argv[1..])
Expand All @@ -98,6 +106,7 @@ fn loud(dir: &Path, argv: &[&str]) -> Result<bool, Fail> {
Ok(st.success())
}

/// Reads `package.json` from `dir`, or a cannot-run failure.
fn manifest(dir: &Path) -> Result<(PathBuf, Vec<u8>), Fail> {
let p = dir.join("package.json");
match fs::read(&p) {
Expand All @@ -106,6 +115,7 @@ fn manifest(dir: &Path) -> Result<(PathBuf, Vec<u8>), Fail> {
}
}

/// Rewrites `path` to its canonical JCS form if it is not already.
fn canonicalise(path: &Path, bytes: &[u8]) -> Result<(), Fail> {
match canon(bytes) {
Canon::Canonical => Ok(()),
Expand All @@ -115,6 +125,8 @@ fn canonicalise(path: &Path, bytes: &[u8]) -> Result<(), Fail> {
}
}

/// The only writer: canonicalises `package.json`, then regenerates each
/// runtime's lock.
fn regen(dir: &Path) -> Result<(), Fail> {
let (path, bytes) = manifest(dir)?;
for r in LOCKED {
Expand Down Expand Up @@ -148,6 +160,8 @@ fn regen(dir: &Path) -> Result<(), Fail> {
Ok(())
}

/// The gate for one runtime: JCS, lock present, frozen install, then the
/// `test` script.
fn check(rt: Runtime, dir: &Path) -> Result<(), Fail> {
let Some(lock) = rt.lockfile() else {
return Err(cannot(format!(
Expand Down Expand Up @@ -192,6 +206,7 @@ fn check(rt: Runtime, dir: &Path) -> Result<(), Fail> {
Ok(())
}

/// The most-preferred runtime on `PATH`, or a cannot-run failure.
fn picked() -> Result<Runtime, Fail> {
pick(|r| on_path(r.name())).ok_or_else(|| {
cannot(format!(
Expand All @@ -201,6 +216,8 @@ fn picked() -> Result<Runtime, Fail> {
})
}

/// Checks that every environment definition under `dir` provisions Bun, or
/// the best runtime it can.
fn provision(dir: &Path) -> Result<(), Fail> {
if !dir.join("package.json").is_file() {
note("no package.json: nothing here needs a JavaScript runtime (0 environment definitions examined)");
Expand Down Expand Up @@ -277,6 +294,7 @@ fn provision(dir: &Path) -> Result<(), Fail> {
Ok(())
}

/// Dispatches the subcommand named by `args[0]`.
fn real_main(args: &[String]) -> Result<(), Fail> {
let dir_at = |i: usize| PathBuf::from(args.get(i).map(String::as_str).unwrap_or("."));
match args.first().map(String::as_str) {
Expand Down Expand Up @@ -321,6 +339,7 @@ fn real_main(args: &[String]) -> Result<(), Fail> {
}
}

/// Entry point: runs `real_main` and maps the result to an exit code.
fn main() -> ExitCode {
let args: Vec<String> = env::args().skip(1).collect();
match real_main(&args) {
Expand Down
Loading