/// Detect the installed `moon` CLI version.
pub fn detect_moon_version() -> String {
match popen("moon --version 2>/dev/null") {
Some(v) => trim(v)
None => "unknown"
}
}
/// Parse a semver string "x.y.z" (optionally with leading text or pre-release suffix).
pub fn parse_semver(s : String) -> Option[SemVer] {
let len = s.length()
if len == 0 { return None }
// Find first digit
let mut i = 0
while i < len {
let ch = s[i]
if ch >= zero() && ch <= nine() { break }
i = i + 1
}
if i >= len { return None }
// Parse major
let mut major_val = 0
while i < len {
let ch = s[i]
if ch >= zero() && ch <= nine() {
major_val = major_val * 10 + (ch - zero()).to_int()
i = i + 1
} else {
break
}
}
if i >= len || s[i] != dot() { return None }
i = i + 1
// Parse minor
let mut minor_val = 0
while i < len {
let ch = s[i]
if ch >= zero() && ch <= nine() {
minor_val = minor_val * 10 + (ch - zero()).to_int()
i = i + 1
} else {
break
}
}
if i >= len || s[i] != dot() { return None }
i = i + 1
// Parse patch
let mut patch_val = 0
while i < len {
let ch = s[i]
if ch >= zero() && ch <= nine() {
patch_val = patch_val * 10 + (ch - zero()).to_int()
i = i + 1
} else {
break
}
}
Some({ major: major_val, minor: minor_val, patch: patch_val })
}
fn zero() -> UInt16 { '0'.to_int().to_uint16() }
fn nine() -> UInt16 { '9'.to_int().to_uint16() }
fn dot() -> UInt16 { '.'.to_int().to_uint16() }
/// Compare two SemVers. Returns -1 if a < b, 0 if equal, 1 if a > b.
pub fn compare_semver(a : SemVer, b : SemVer) -> Int {
if a.major != b.major { return if a.major < b.major { -1 } else { 1 } }
if a.minor != b.minor { return if a.minor < b.minor { -1 } else { 1 } }
if a.patch != b.patch { return if a.patch < b.patch { -1 } else { 1 } }
0
}
/// Build a complete version report with optional constraints from env vars.
pub fn make_report(moon_version : String) -> VersionReport {
let semver_val = parse_semver(moon_version)
let check_min_str = getenv("MOONBIT_CHECK_MIN")
let check_max_str = getenv("MOONBIT_CHECK_MAX")
let check_min = match check_min_str {
Some(s) => parse_semver(s)
None => None
}
let check_max = match check_max_str {
Some(s) => parse_semver(s)
None => None
}
let meets_min = match (semver_val, check_min) {
(Some(ver), Some(min)) => compare_semver(ver, min) >= 0
_ => true
}
let meets_max = match (semver_val, check_max) {
(Some(ver), Some(max)) => compare_semver(ver, max) <= 0
_ => true
}
{
moon_version,
semver: semver_val,
check_min,
check_max,
meets_min,
meets_max,
meets_all: meets_min && meets_max,
}
}
/// Serialize version report to JSON.
pub fn report_to_json(report : VersionReport) -> String {
let sb = StringBuilder()
sb.write_string("{\n")
sb.write_string(" \"moon_version\": ")
sb.write_string(json_escape(report.moon_version))
sb.write_string(",\n")
sb.write_string(" \"semver\": ")
match report.semver {
Some(v) => {
sb.write_string("{\n")
sb.write_string(" \"major\": " + v.major.to_string() + ",\n")
sb.write_string(" \"minor\": " + v.minor.to_string() + ",\n")
sb.write_string(" \"patch\": " + v.patch.to_string() + "\n")
sb.write_string(" }")
}
None => { sb.write_string("null") }
}
sb.write_string(",\n")
// Constraints
sb.write_string(" \"check_min\": ")
match report.check_min {
Some(v) => { sb.write_string(semver_to_json(v)) }
None => { sb.write_string("null") }
}
sb.write_string(",\n")
sb.write_string(" \"check_max\": ")
match report.check_max {
Some(v) => { sb.write_string(semver_to_json(v)) }
None => { sb.write_string("null") }
}
sb.write_string(",\n")
sb.write_string(" \"meets_min\": ")
sb.write_string(if report.meets_min { "true" } else { "false" })
sb.write_string(",\n")
sb.write_string(" \"meets_max\": ")
sb.write_string(if report.meets_max { "true" } else { "false" })
sb.write_string(",\n")
sb.write_string(" \"meets_all\": ")
sb.write_string(if report.meets_all { "true" } else { "false" })
sb.write_string("\n}\n")
sb.to_string()
}
/// Format a SemVer as JSON object.
fn semver_to_json(v : SemVer) -> String {
let sb = StringBuilder()
sb.write_string("{\n")
sb.write_string(" \"major\": ")
sb.write_string(v.major.to_string())
sb.write_string(",\n")
sb.write_string(" \"minor\": ")
sb.write_string(v.minor.to_string())
sb.write_string(",\n")
sb.write_string(" \"patch\": ")
sb.write_string(v.patch.to_string())
sb.write_string("\n }")
sb.to_string()
}
/// Trim whitespace from a string.
fn trim(s : String) -> String {
let len = s.length()
if len == 0 { return s }
let mut start = 0
while start < len && is_whitespace(s[start]) {
start = start + 1
}
let mut end = len
while end > start && is_whitespace(s[end - 1]) {
end = end - 1
}
if start >= end {
""
} else {
// Build result without using deprecated substring
let sb = StringBuilder()
for i = start; i < end; i = i + 1 {
sb.write_char(s[i].unsafe_to_char())
}
sb.to_string()
}
}
fn is_whitespace(ch : UInt16) -> Bool {
let sp = ' '.to_int().to_uint16()
let nl = '\n'.to_int().to_uint16()
let cr = '\r'.to_int().to_uint16()
let tab = '\t'.to_int().to_uint16()
ch == sp || ch == nl || ch == cr || ch == tab
}
/// JSON string escape.
fn json_escape(s : String) -> String {
let sb = StringBuilder()
sb.write_char('"')
for i = 0; i < s.length(); i = i + 1 {
let ch = s[i]
if ch == '"'.to_int().to_uint16() {
sb.write_string("\\\"")
} else if ch == '\\'.to_int().to_uint16() {
sb.write_string("\\\\")
} else if ch == '\n'.to_int().to_uint16() {
sb.write_string("\\n")
} else if ch == '\r'.to_int().to_uint16() {
sb.write_string("\\r")
} else if ch == '\t'.to_int().to_uint16() {
sb.write_string("\\t")
} else {
sb.write_char(ch.unsafe_to_char())
}
}
sb.write_char('"')
sb.to_string()
}
// ── C FFI ──
#borrow(cmd)
extern "c" fn popen(cmd : String) -> Option[String] = "moonwasm_probe_ffi_popen"
#borrow(name)
extern "c" fn getenv(name : String) -> Option[String] = "moonwasm_probe_ffi_getenv"
/// Get an environment variable string.
pub fn get_env(name : String) -> Option[String] {
getenv(name)
}
/// Exit the process with a code.
pub fn exit_process(code : Int) -> Unit {
raw_exit(code)
}
extern "c" fn raw_exit(code : Int) = "exit"