///|
priv enum DoctorStatus {
Pass
Warn
Fail
Skip
} derive(Eq)
///|
fn DoctorStatus::name(self : DoctorStatus) -> String {
match self {
Pass => "pass"
Warn => "warn"
Fail => "fail"
Skip => "skip"
}
}
///|
priv struct DoctorCheck {
id : String
status : DoctorStatus
required : Bool
summary : String
detail : String
remediation : String
}
///|
priv struct DoctorReport {
schema_version : Int
ok : Bool
host : String
project_root : String
platforms : Array[String]
checks : Array[DoctorCheck]
evidence : Map[String, EvidenceStatus]
}
///|
priv struct DoctorOptions {
project_root : String
platforms : Array[String]
all : Bool
}
///|
fn doctor_check(
id : String,
status : DoctorStatus,
required : Bool,
summary : String,
detail? : String = "",
remediation? : String = "",
) -> DoctorCheck {
{ id, status, required, summary, detail, remediation, }
}
///|
fn skipped_check(id : String, summary : String) -> DoctorCheck {
doctor_check(id, Skip, false, summary)
}
///|
fn path_is_file(path : String) -> Bool {
@fs.is_file(path) catch {
_ => false
}
}
///|
fn path_is_dir(path : String) -> Bool {
@fs.is_dir(path) catch {
_ => false
}
}
///|
fn path_is_nonempty_file(path : String) -> Bool {
if !path_is_file(path) {
return false
}
let bytes = @fs.read_file_to_bytes(path) catch { _ => return false }
!bytes.is_empty()
}
///|
/// Backward-compatible alias for `find_in_path` defined in `cli_path.mbt`.
/// Existing doctor call sites continue to use the `find_executable` name.
fn find_executable(name : String, host : HostPlatform) -> String? {
find_in_path(name, host)
}
///|
/// Backward-compatible alias for `first_existing` defined in `cli_path.mbt`.
fn first_existing_file(paths : Array[String]) -> String? {
first_existing(paths)
}
///|
fn env_directory(names : Array[String]) -> String? {
for name in names {
match @env.get_env_var(name) {
Some(value) if path_is_dir(value) => return Some(value)
_ => ()
}
}
None
}
///|
fn valid_skia_root(root : String) -> Bool {
path_is_dir(root) &&
path_is_nonempty_file(join_path(root, "moon.mod")) &&
path_is_nonempty_file(join_path(root, "build.js")) &&
path_is_nonempty_file(join_path(root, "skia-provider-lock.json")) &&
path_is_nonempty_file(join_path(root, "native/capabilities.json"))
}
///|
fn doctor_platforms(
options : DoctorOptions,
host : HostPlatform,
) -> Result[Array[String], String] {
if options.all {
return Ok([ for platform in platform_order => platform ])
}
if !options.platforms.is_empty() {
let selected : Map[String, Bool] = Map([])
for platform in options.platforms {
let platform = platform.to_lower()
if !is_platform(platform) {
return Err("unsupported doctor platform: " + platform)
}
selected[platform] = true
}
let result : Array[String] = []
for platform in platform_order {
if selected.contains(platform) {
result.push(platform)
}
}
return Ok(result)
}
let manifest_path = join_path(options.project_root, "moui.project.json")
if !@fs.path_exists(manifest_path) {
return Ok(default_platforms(host))
}
match read_project_manifest(options.project_root) {
Ok(manifest) => Ok(manifest.platforms)
Err(message) => Err(message)
}
}
///|
fn moonbit_doctor_check(host : HostPlatform) -> DoctorCheck {
match find_executable("moon", host) {
Some(path) =>
doctor_check(
"moonbit",
Pass,
true,
"MoonBit CLI is available",
detail=path,
)
None =>
doctor_check(
"moonbit",
Fail,
true,
"MoonBit CLI is missing from PATH",
remediation="Install MoonBit and ensure the moon executable is on PATH.",
)
}
}
///|
fn skia_doctor_check(project_root : String, required : Bool) -> DoctorCheck {
if !required {
return skipped_check(
"skia", "Skia is not required by the selected Web-only project",
)
}
let env_root = @env.get_env_var("MOUI_SKIA_ROOT").unwrap_or("")
let candidates = [env_root]
for root in project_search_roots(project_root) {
candidates.push(join_path(root, "moui_skia"))
candidates.push(join_path(root, ".mooncakes/wzzc-dev/moui_skia"))
}
let invalid_root = Ref("")
for path in candidates {
if valid_skia_root(path) {
return doctor_check(
"skia",
Pass,
true,
"MoUI Skia package is available",
detail=path,
)
}
if invalid_root.val == "" && path_is_dir(path) {
invalid_root.val = path
}
}
doctor_check(
"skia",
Fail,
true,
if invalid_root.val == "" {
"No local MoUI Skia package or override was found"
} else {
"The resolved MoUI Skia package is incomplete"
},
detail=invalid_root.val,
remediation="Run moon update/build to resolve wzzc-dev/moui_skia, or set MOUI_SKIA_ROOT to a complete package root.",
)
}
///|
fn valid_msvc_zlib_root(root : String) -> Bool {
path_is_nonempty_file(join_path(root, "include/zlib.h")) &&
(
path_is_nonempty_file(join_path(root, "lib/zlib.lib")) ||
path_is_nonempty_file(join_path(root, "lib/zlibstatic.lib"))
)
}
///|
fn msvc_zlib_root(project_root : String) -> String? {
let vcpkg_root = @env.get_env_var("VCPKG_ROOT").unwrap_or("")
let vcpkg_installation = @env.get_env_var("VCPKG_INSTALLATION_ROOT").unwrap_or(
"",
)
let candidates = [
@env.get_env_var("MOUI_MSVC_ZLIB_TRIPLET_ROOT").unwrap_or(""),
join_path(vcpkg_root, "installed/x64-windows"),
join_path(vcpkg_installation, "installed/x64-windows"),
join_path(project_root, ".tools/vcpkg-msvc/vcpkg_installed/x64-windows"),
join_path(project_root, "vcpkg_installed/x64-windows"),
]
for root in candidates {
if root != "" && valid_msvc_zlib_root(root) {
return Some(root)
}
}
None
}
///|
fn msvc_doctor_check(
project_root : String,
host : HostPlatform,
required : Bool,
) -> DoctorCheck {
if !required {
return skipped_check(
"msvc", "MSVC is not required by the selected platforms",
)
}
let configured = @env.get_env_var("VCToolsInstallDir") is Some(_) ||
@env.get_env_var("VCINSTALLDIR") is Some(_) ||
@env.get_env_var("VSINSTALLDIR") is Some(_)
match find_executable("cl", host) {
Some(path) => {
if find_executable("node", host) is None {
return doctor_check(
"msvc",
Fail,
true,
"MSVC is available, but Node.js is missing from PATH",
detail=path,
remediation="Install Node.js for MoonBit native build hooks.",
)
}
match msvc_zlib_root(project_root) {
Some(zlib) =>
doctor_check(
"msvc",
Pass,
true,
"MSVC and the x64-windows zlib dependency are available",
detail=path + " | " + zlib,
)
None =>
doctor_check(
"msvc",
Fail,
true,
"MSVC is available, but zlib:x64-windows was not detected",
detail=path,
remediation="Run scripts/windows/setup_msvc_deps.ps1 -InstallZlib, then initialize scripts/windows/msvc_env.ps1.",
)
}
}
None if configured =>
doctor_check(
"msvc",
Fail,
true,
"Visual Studio is configured but cl.exe is not on PATH",
remediation="Run moui from a Visual Studio Developer PowerShell, or let `moui build windows` configure the MSVC environment automatically via vswhere/vcvarsall.",
)
None =>
doctor_check(
"msvc",
Fail,
true,
"MSVC Build Tools were not detected",
remediation="Install Visual Studio Build Tools with: winget install --id Microsoft.VisualStudio.2022.BuildTools -e; `moui build windows` then configures the environment automatically.",
)
}
}
///|
/// Web builds and the dev runner need Node.js on PATH.
fn web_doctor_check(required : Bool) -> DoctorCheck {
if !required {
return skipped_check("web", "Web is not required by the selected platforms")
}
let host = detect_host_platform()
match find_executable("node", host) {
Some(path) =>
doctor_check(
"web",
Pass,
true,
"Node.js is available for web build hooks and the dev runner",
detail=path,
)
None =>
doctor_check(
"web",
Fail,
true,
"Node.js was not found on PATH",
remediation="Install Node.js; `moui dev` and web packaging require it.",
)
}
}
///|
fn wayland_doctor_check(host : HostPlatform, required : Bool) -> DoctorCheck {
if !required {
return skipped_check(
"wayland", "Wayland is not required by the selected platforms",
)
}
let scanner = find_executable("wayland-scanner", host)
let pkg_config = find_executable("pkg-config", host)
if scanner is None || pkg_config is None {
return doctor_check(
"wayland",
Fail,
true,
"Wayland build tools are incomplete",
detail="wayland-scanner=" +
scanner.unwrap_or("missing") +
" pkg-config=" +
pkg_config.unwrap_or("missing"),
remediation="Install wayland-scanner, pkg-config, libwayland-dev, and wayland-protocols.",
)
}
let package_probe = run_doctor_probe(pkg_config.unwrap(), WaylandPackages)
if package_probe.status != 0 {
return doctor_check(
"wayland",
Fail,
true,
"Required Wayland pkg-config packages do not resolve",
detail="packages=wayland-client,wayland-cursor,wayland-protocols" +
(if package_probe.output == "" {
""
} else {
" | " + package_probe.output
}),
remediation="Install libwayland-dev and wayland-protocols, then ensure PKG_CONFIG_PATH exposes their .pc files.",
)
}
let detail = scanner.unwrap() +
" | " +
pkg_config.unwrap() +
" | packages=wayland-client,wayland-cursor,wayland-protocols"
if @env.get_env_var("WAYLAND_DISPLAY") is None {
doctor_check(
"wayland",
Warn,
true,
"Wayland build tools are available, but no active Wayland display is set",
detail~,
remediation="Set WAYLAND_DISPLAY when running native window smoke tests.",
)
} else {
doctor_check(
"wayland",
Pass,
true,
"Wayland build and display environment is available",
detail~,
)
}
}
///|
fn project_entrypoint_checks(
project_root : String,
platforms : Array[String],
) -> Array[DoctorCheck] {
let checks : Array[DoctorCheck] = []
for platform in ["android", "ios", "harmonyos"] {
if platforms.contains(platform) {
let generated_root = join_path(project_root, platform + "_window_hosted")
let repository_root = join_path(project_root, platform)
let entrypoint_root = if @fs.path_exists(
join_path(generated_root, "moon.pkg"),
) ||
@fs.path_exists(join_path(generated_root, "main.mbt")) {
generated_root
} else if @fs.path_exists(join_path(repository_root, "moon.pkg")) ||
@fs.path_exists(join_path(repository_root, "main.mbt")) {
repository_root
} else {
generated_root
}
let package_path = join_path(entrypoint_root, "moon.pkg")
let main_path = join_path(entrypoint_root, "main.mbt")
let package_text = @fs.read_file_to_string(package_path) catch { _ => "" }
let main_text = @fs.read_file_to_string(main_path) catch { _ => "" }
let package_markers = [
"wzzc-dev/moui/runtime",
"wzzc-dev/moui/backend/" + platform,
"wzzc-dev/moui_skia_renderer",
]
let main_markers = [
"@runtime.run_app", ".render_all(", ".backend(@platform_host.entry())",
]
let missing : Array[String] = []
for marker in package_markers {
if !package_text.contains(marker) {
missing.push(marker)
}
}
for marker in main_markers {
if !main_text.contains(marker) {
missing.push(marker)
}
}
if path_is_nonempty_file(package_path) &&
path_is_nonempty_file(main_path) &&
missing.is_empty() {
checks.push(
doctor_check(
"project." + platform + ".entrypoint",
Pass,
true,
platform + " MoonBit entrypoint is present",
detail=package_path + " | " + main_path,
),
)
} else {
checks.push(
doctor_check(
"project." + platform + ".entrypoint",
Warn,
true,
platform +
" mobile metadata is present but its MoonBit entrypoint contract is incomplete",
detail=if missing.is_empty() {
"expected nonempty " + package_path + " and " + main_path
} else {
"missing markers: " + missing.join(", ")
},
remediation="Generate or add the platform entrypoint before building the native app.",
),
)
}
}
}
checks
}
///|
fn run_doctor(options : DoctorOptions) -> Result[DoctorReport, String] {
let host_platform = detect_host_platform()
let project_manifest_path = join_path(
options.project_root,
"moui.project.json",
)
if @fs.path_exists(project_manifest_path) {
match read_project_manifest(options.project_root) {
Ok(_) => ()
Err(message) => return Err(message)
}
}
let platforms = match doctor_platforms(options, host_platform) {
Ok(value) => value
Err(message) => return Err(message)
}
let native_selected = platforms.any(platform => platform != "web")
let checks = [
moonbit_doctor_check(host_platform),
skia_doctor_check(options.project_root, native_selected),
msvc_doctor_check(
options.project_root,
host_platform,
platforms.contains("windows"),
),
web_doctor_check(platforms.contains("web")),
wayland_doctor_check(host_platform, platforms.contains("linux")),
android_doctor_check(
options.project_root,
host_platform,
platforms.contains("android"),
),
xcode_doctor_check(
host_platform,
platforms.contains("macos"),
platforms.contains("ios"),
),
harmonyos_doctor_check(
options.project_root,
host_platform,
platforms.contains("harmonyos"),
),
]
for check in project_entrypoint_checks(options.project_root, platforms) {
checks.push(check)
}
let ok = !checks.any(check => check.required && check.status == Fail)
// Scan `artifacts//*/verify-manifest.json` so the doctor can flag
// missing runtime evidence alongside the toolchain checks.
let evidence = check_evidence_ready(
options.project_root,
platforms,
host_platform,
)
Ok({
schema_version: 1,
ok,
host: host_platform.name(),
project_root: options.project_root,
platforms,
checks,
evidence,
})
}
///|
fn doctor_check_json(check : DoctorCheck) -> Json {
(
{
"id": check.id,
"status": check.status.name(),
"required": check.required,
"summary": check.summary,
"detail": check.detail,
"remediation": check.remediation,
} : Json)
}
///|
fn DoctorReport::to_json_text(self : DoctorReport) -> String {
let checks = self.checks.map(doctor_check_json)
let evidence_entries : Array[Json] = []
for key, status in self.evidence {
let entry : Json = {
"key": Json::string(key),
"platform": Json::string(status.platform),
"appId": Json::string(status.app_id),
"manifestPath": Json::string(status.manifest_path),
"passed": Json::boolean(status.passed),
"observationsSummary": Json::string(status.observations_summary),
}
evidence_entries.push(entry)
}
let value : Json = {
"schemaVersion": self.schema_version,
"ok": self.ok,
"host": self.host,
"projectRoot": self.project_root,
"platforms": self.platforms,
"checks": checks,
"evidence": Json::array(evidence_entries),
}
value.stringify(indent=2) + "\n"
}
///|
fn DoctorReport::to_human_text(self : DoctorReport) -> String {
let builder = StringBuilder()
builder.write_string("MoUI doctor host=" + self.host + "\n")
for check in self.checks {
builder.write_string(
"[" + check.status.name() + "] " + check.id + ": " + check.summary + "\n",
)
if check.detail != "" {
builder.write_string(" " + check.detail + "\n")
}
if check.remediation != "" {
builder.write_string(" Fix: " + check.remediation + "\n")
}
}
if !self.evidence.is_empty() {
builder.write_string("Runtime evidence:\n")
builder.write_string(evidence_status_to_text(self.evidence))
}
builder.write_string(
if self.ok {
"Doctor completed without required failures.\n"
} else {
"Doctor found required environment failures.\n"
},
)
builder.to_string()
}