///|
/// Versions and paths refer to the tools selected by Moon, not whichever moonc is first on PATH.
priv struct Environment {
host_os : String?
host_arch : String?
zig_cross : String
moon : String?
compiler : String?
zig : String?
toolchain_root : String?
runtime_fingerprint : String?
runtime_mode : String
darwin_sdk : String?
toolchain_overrides : Array[String]
} derive(ToJson)
///|
priv struct NextStep {
kind : String
command : String
prerequisites : String
} derive(ToJson)
///|
priv struct Finding {
code : String
severity : String
targets : Array[String]
message : String
applicability : String
evidence : Array[String]
next_steps : Array[NextStep]
} derive(ToJson)
///|
priv struct Report {
schema_version : Int32
environment : Environment
targets : Array[String]
status : String
findings : Array[Finding]
} derive(ToJson)
///|
let assessed_compiler = "0.10.17+d971670cd"
///|
let stock_runtime_fingerprint = "067e9fd5378d52fea64a265b4a4f5b2e79a8f47c91e5fb987a20a0ce8530a6c7"
///|
/// Stock toolchains with an assessment of the full runtime: compiler, Moon and runtime SHA-256.
/// Only the first one is also assessed for the sync runtime (`sync_eligible`).
let assessed_toolchains : Array[(String, String, String)] = [
(assessed_compiler, "0.1.20261023", stock_runtime_fingerprint),
(
"0.10.18+6c83a7f62", "0.1.20261028", "2f3e28288883bc669aacf961b6e0a04eede59f5ca09d7c838cf70e3651fc5e7f",
),
]
///|
fn requested_mode() -> String {
@env.get_env_var("ZIG_CROSS_RUNTIME").unwrap_or("full")
}
///|
async fn probe(command : String, args : Array[String]) -> String? {
try {
let out = @shell.Cmd(command, args).output(
timeout_ms=3000,
max_output_bytes=65536,
)
if out.exit_code() == 0 {
Some(out.stdout().trim().to_owned())
} else {
None
}
} catch {
_ => None
}
}
///|
fn version_field(output : String, prefix : String) -> String? {
for line in output.split("\n") {
if line.has_prefix(prefix) {
return Some(
line[prefix.length():].split(" ").next().unwrap_or("").to_owned(),
)
}
}
None
}
///|
fn compiler_root(output : String) -> String? {
for line in output.split("\n") {
if line.has_prefix("moonc ") {
let path = line.split(") ").last().unwrap_or("").to_owned()
if path.has_suffix("/bin/moonc") {
let path = if path.has_prefix("~/") {
@env.get_env_var("HOME").unwrap_or("") + path[1:].to_owned()
} else {
path
}
return Some(path[:path.length() - 10].to_owned())
}
}
}
None
}
///|
async fn runtime_fingerprint(root : String) -> String? {
try {
let dir = root + "/lib/runtime"
let names = @fs.readdir(dir)
names.sort()
let mut manifest = ""
for name in names {
if name.has_suffix(".c") {
let data = @fs.read_file(dir + "/" + name).binary()
manifest += name +
":" +
@crypto.bytes_to_hex_string(@crypto.sha256(data)) +
"\n"
}
}
Some(@crypto.bytes_to_hex_string(@crypto.sha256(@utf8.encode(manifest))))
} catch {
_ => None
}
}
///|
async fn detect_environment() -> Environment {
let moon_output = probe("moon", ["version", "--all"]).unwrap_or("")
let host_os = probe("uname", ["-s"])
let sdk = if host_os == Some("Darwin") {
probe("xcrun", ["--show-sdk-path"])
} else {
None
}
let sdk = match sdk {
Some(path) =>
if @fs.exists(path + "/usr/include/sys/vnode.h") {
Some(path)
} else {
None
}
None => None
}
let root = compiler_root(moon_output)
let fingerprint = match root {
Some(path) => runtime_fingerprint(path)
None => None
}
{
host_os,
host_arch: probe("uname", ["-m"]),
zig_cross: version_text(),
moon: version_field(moon_output, "moon "),
compiler: version_field(moon_output, "moonc v"),
zig: probe("zig", ["version"]),
toolchain_root: root,
runtime_fingerprint: fingerprint,
runtime_mode: requested_mode(),
darwin_sdk: sdk,
toolchain_overrides: [
"MOONC_OVERRIDE", "MOON_CORE_OVERRIDE", "MOON_OVERRIDE",
].filter(name => @env.get_env_var(name) is Some(_)),
}
}
///|
fn assessed_toolchain(env : Environment, index : Int32) -> Bool {
let (compiler, moon, fingerprint) = assessed_toolchains[index]
env.toolchain_overrides.is_empty() &&
env.compiler == Some(compiler) &&
env.moon == Some(moon) &&
env.zig == Some("0.17.0") &&
env.runtime_fingerprint == Some(fingerprint) &&
(env.host_os == Some("Linux") || env.host_os == Some("Darwin"))
}
///|
fn sync_eligible(env : Environment) -> Bool {
assessed_toolchain(env, 0)
}
///|
/// The stock full runtime matches an assessment, so doctor knows its header defects.
fn full_runtime_assessed(env : Environment) -> Bool {
for index in 0.. Bool {
env.toolchain_overrides.is_empty() &&
env.compiler == Some("0.10.15+943464bfa") &&
env.moon == Some("0.1.20261012") &&
env.zig == Some("0.17.0") &&
env.runtime_fingerprint ==
Some("0c0a95f4fa7e59715b57ec30962c34f642a5caff0b28535c5933278c691fd23c")
}
///|
fn step(command : String, prerequisites : String) -> NextStep {
{ kind: "command", command, prerequisites, }
}
///|
fn instruction(text : String) -> NextStep {
{ kind: "instruction", command: text, prerequisites: "", }
}
///|
fn finding(
code : String,
severity : String,
targets : Array[String],
message : String,
applicability : String,
evidence : Array[String],
next_steps : Array[NextStep],
) -> Finding {
{ code, severity, targets, message, applicability, evidence, next_steps, }
}
///|
fn report(
env : Environment,
targets : Array[Target],
usage? : String,
) -> Report {
let unique : Array[Target] = []
for target in targets {
if !unique.contains(target) {
unique.push(target)
}
}
let targets = unique
let names = targets.map(t => t.name())
let findings : Array[Finding] = []
if usage is Some(message) {
findings.push(
finding("ZC_USAGE", "error", names, message, "invalid arguments", [], [
step("zig-cross --help", "none"),
]),
)
} else {
for
(name, version) in [
("Moon/compiler", env.compiler),
("Moon build tool", env.moon),
("Zig", env.zig),
] {
if version is None || version == Some("") {
findings.push(
finding(
"ZC_TOOL_MISSING",
"error",
names,
"Cannot probe \{name}; it is missing, failed, or exceeded the 3-second probe limit.",
"tool probe failed",
[],
[
instruction(
"Install the complete MoonBit toolchain and Zig 0.17.0; correct PATH, then rerun doctor.",
),
],
),
)
}
}
if env.host_os != Some("Linux") && env.host_os != Some("Darwin") {
findings.push(
finding(
"ZC_HOST_UNSUPPORTED",
"error",
names,
"zig-cross requires a Linux or macOS host.",
"host probe",
[],
[instruction("Run zig-cross on Linux or macOS.")],
),
)
}
if env.runtime_mode != "full" && env.runtime_mode != "sync" {
findings.push(
finding(
"ZC_RUNTIME_MODE_INVALID",
"error",
names,
"Unknown ZIG_CROSS_RUNTIME value: \{env.runtime_mode}",
"explicit environment selection",
[],
[instruction("Unset ZIG_CROSS_RUNTIME or set it to full or sync.")],
),
)
}
let eligible = sync_eligible(env)
let assessed = full_runtime_assessed(env)
if !assessed && !old_toolchain(env) {
let fixed = targets.filter(t => !header_flags(t, env, "").is_empty())
let message = if fixed.is_empty() {
"This version, host, or runtime fingerprint has not been assessed. Compatibility is unverified."
} else {
"This version, host, or runtime fingerprint has not been assessed. Compatibility is unverified. The wrapper still applies its MoonBit header fix for \{fixed.map(t => t.name()).join(", ")}: limits.h on Linux and a sys/vnode.h shim on macOS, to MoonBit runtime and moonbitlang/async C sources only."
}
findings.push(
finding(
"ZC_TOOLCHAIN_UNVERIFIED",
"unverified",
names,
message,
"no exact stock-toolchain assessment",
[],
[
step(
"zig-cross build \{names.join(" ")} -- ",
"Replace . Unset ZIG_CROSS_RUNTIME or set it to full; an unassessed toolchain cannot use sync. A successful build checks compilation and linking, not runtime behavior.",
),
],
),
)
}
if env.runtime_mode == "sync" {
if eligible {
findings.push(
finding(
"ZC_COMPATIBILITY_MODE",
"warning",
names,
"Experimental sync runtime selected: runtime.c, env.c, sync_io.c, utf.c, backtrace.c, c_buffer.c. Excludes event loop, async filesystem, networking, processes, TLS, thread pools and watchers. Required excluded symbols fail at link time; no stubs are supplied.",
"explicit ZIG_CROSS_RUNTIME=sync; exact version and runtime SHA-256 match",
[stock_runtime_fingerprint],
[
instruction(
"For excluded APIs, unset ZIG_CROSS_RUNTIME and build on the target OS with a native compiler supported by MoonBit; do not route that build through zig-cross.",
),
],
),
)
} else {
findings.push(
finding(
"ZC_SYNC_UNAVAILABLE",
"error",
names,
"Sync runtime refused: requires stock MoonBit \{assessed_compiler}, Moon 0.1.20261023, Zig 0.17.0 and a supported host.",
"explicit sync selection failed eligibility",
[],
[
instruction(
"Install the assessed complete toolchain, or unset ZIG_CROSS_RUNTIME and use a native compiler on the target OS.",
),
],
),
)
}
} else if assessed {
for target in targets {
let os = if target == Host {
env.host_os.unwrap_or("")
} else {
target.name()
}
let (code, message) = if os.has_prefix("linux") || os == "Linux" {
(
"ZC_LINUX_NAME_MAX", "The full runtime dir.c and the moonbitlang/async C stubs use NAME_MAX but do not include limits.h, so they do not compile with Zig's musl headers. The wrapper adds -include limits.h to MoonBit runtime and moonbitlang/async C sources only.",
)
} else if os.has_prefix("darwin") || os == "Darwin" {
(
"ZC_DARWIN_VNODE_HEADER", "The full runtime dir.c and fs.c and the moonbitlang/async C stubs include sys/vnode.h, which Zig's macOS headers do not have. The wrapper writes a minimal sys/vnode.h shim (enum vtype with the macOS values) and adds it with -idirafter to MoonBit runtime and moonbitlang/async C sources only. A real sys/vnode.h comes first. No macOS SDK is necessary, so this also works on a Linux host.",
)
} else {
(
"ZC_WINDOWS_MSVC_RUNTIME", "The full runtime contains MSVC-only Windows sources and additional MinGW incompatibilities. Compiler flags alone do not fix it.",
)
}
let header_fix = header_flags(
target,
env,
"/zig/" + target.name(),
).join(" ")
let active = !header_fix.is_empty()
findings.push(
finding(
code,
if active {
"warning"
} else {
"error"
},
[target.name()],
if active {
"Active header workaround: " + header_fix + ". " + message
} else {
message
},
"fingerprinted stock \{env.compiler.unwrap_or("")} full runtime with Zig 0.17.0; assessed compilation, not reproduced by doctor",
[
"https://github.com/moonbitlang/moon/blob/3778465a252d2838357dddf25dc0ebf3ff3ba40f/crates/moonutil/src/toolchain.rs#L62",
..runtime_header_evidence,
],
if active {
[
step(
"zig-cross build \{target.name()} -- ",
"Replace ; the detected header fix is automatically applied by this wrapper.",
),
]
} else {
[
step(
"ZIG_CROSS_RUNTIME=sync zig-cross doctor \{target.name()}",
"Experimental; only consumers using the synchronous runtime subset.",
),
step(
"ZIG_CROSS_RUNTIME=sync zig-cross build \{target.name()} -- ",
"Replace ; no excluded runtime APIs.",
),
instruction(
"For full runtime APIs, use the target OS and a supported native compiler. Validate your own build.",
),
]
},
),
)
}
}
if targets.contains(WindowsX64) {
findings.push(
finding(
"ZC_WINDOWS_OSSTRING",
"warning",
["windows-x64"],
"Cross-built Windows code using OsString can mishandle arguments, environment variables and paths. A valid PE header does not prove runtime correctness.",
"Moon platform cfg and bundled core follow the Linux/macOS host; remains unverified for unknown versions",
["https://github.com/moonbitlang/moon/issues/2320"],
[
instruction(
"Build Windows binaries that use OsString on Windows with the native toolchain until upstream target-platform support is fixed.",
),
],
),
)
}
}
let status = if findings.any(f => f.severity == "error") {
"error"
} else if findings.any(f => f.severity == "warning") {
"warning"
} else if findings.any(f => f.severity == "unverified") {
"unknown"
} else {
"ok"
}
{ schema_version: 1, environment: env, targets: names, status, findings, }
}
///|
fn report_text(report : Report) -> String {
let env = report.environment
let mut text = "zig-cross doctor: \{report.status}\nHost: \{env.host_os.unwrap_or("unknown")} \{env.host_arch.unwrap_or("unknown")}\nMoon: \{env.moon.unwrap_or("unknown")}\nCompiler: \{env.compiler.unwrap_or("unknown")}\nZig: \{env.zig.unwrap_or("unknown")}\nRuntime: \{env.runtime_mode}\nTargets: \{report.targets.join(", ")}\n"
for f in report.findings {
text += "[\{f.code}] \{f.severity} (\{f.targets.join(", ")}): \{f.message}\n"
text += " Applies: \{f.applicability}\n"
for next in f.next_steps {
text += " Next: \{next.command}\n Requires: \{next.prerequisites}\n"
}
}
text
}
///|
async fn emit_notices(report : Report) -> Unit {
for f in report.findings {
@stdio.stderr.write(
"zig-cross [\{f.code}] \{f.targets.join(", ")}: \{f.message}\n",
)
for next in f.next_steps {
@stdio.stderr.write(" Next: \{next.command}\n")
}
}
}
///|
fn shell_quote(value : String) -> String {
"'" + value.replace(old="'", new="'\"'\"'") + "'"
}
///|
/// The compiler flags of the MoonBit header fix for `target`, or none. The `cc` wrapper adds them
/// only to MoonBit runtime and moonbitlang/async C sources (`moonbit_source_patterns`).
/// `wrapper_dir` is the directory of the wrapper; the macOS shim is in its `include` directory.
///
/// The fix applies to the full runtime of every toolchain except the old 0.10.15 pin, which does not
/// need it. An unassessed toolchain that has the defects then builds, and one without them is not
/// changed: limits.h is a standard header, and a real sys/vnode.h comes before the shim.
fn header_flags(
target : Target,
env : Environment,
wrapper_dir : String,
) -> Array[String] {
if env.runtime_mode != "full" ||
old_toolchain(env) ||
(env.host_os != Some("Linux") && env.host_os != Some("Darwin")) {
return []
}
if target == LinuxX64 ||
target == LinuxArm64 ||
(target == Host && env.host_os == Some("Linux")) {
["-include", "limits.h"]
} else if target == DarwinArm64 ||
target == DarwinX64 ||
(target == Host && env.host_os == Some("Darwin")) {
["-idirafter", wrapper_dir + "/include"]
} else {
[]
}
}
///|
fn execution_allowed(env : Environment) -> Bool {
env.runtime_mode == "full" ||
(env.runtime_mode == "sync" && sync_eligible(env))
}