///|
/// Small utilities: paths, filesystem helpers, process spawning and hashing.
/// Everything here is written against libc via `native_stubs.mbt` and
/// `moonbitlang/x`.
///|
/// Read an environment variable, returning `default` when unset or empty.
pub fn env_or(name : String, default : String) -> String {
match @env.get_env_var(name) {
Some(v) => if v == "" { default } else { v }
None => default
}
}
///|
pub fn path_join(a : String, b : String) -> String {
if a == "" {
b
} else if a.has_suffix("/") {
a + b
} else {
a + "/" + b
}
}
///|
/// Join any number of path segments with `/`.
pub fn path_join_all(parts : Array[String]) -> String {
let mut out = ""
for p in parts {
out = path_join(out, p)
}
out
}
///|
pub fn home_dir() -> String raise MbError {
let home = env_or("HOME", "")
if home == "" {
raise MbError("HOME is not set; cannot determine the user home directory")
} else {
home
}
}
///|
/// Root of `mb`'s own data directory (installed toolchains and tools).
///
/// Resolution order: `MB_HOME`, then `$XDG_DATA_HOME/mb`, then
/// `~/.local/share/mb`.
pub fn mb_home() -> String raise MbError {
let explicit = env_or("MB_HOME", "")
if explicit != "" {
return explicit
}
let xdg = env_or("XDG_DATA_HOME", "")
if xdg != "" {
return path_join(xdg, "mb")
}
path_join(path_join(home_dir(), ".local/share"), "mb")
}
///|
/// Directory that receives executables. Defaults to `~/.local/bin`, matching
/// the requirement that every install lands there.
pub fn bin_dir() -> String raise MbError {
let explicit = env_or("MB_BIN_DIR", "")
if explicit != "" {
explicit
} else {
path_join(home_dir(), ".local/bin")
}
}
///|
pub fn dir_exists(path : String) -> Bool {
@fs.is_dir(path) catch {
_ => false
}
}
///|
pub fn file_exists(path : String) -> Bool {
@fs.is_file(path) catch {
_ => false
}
}
///|
pub fn path_exists(path : String) -> Bool {
@fs.path_exists(path)
}
///|
/// `mkdir -p` for a single path.
pub fn mkdir_p(path : String) -> Unit raise MbError {
if path == "" || path == "/" {
return
}
if dir_exists(path) {
return
}
let mut cur = if path.has_prefix("/") { "/" } else { "" }
for part in path.split("/") {
let seg = part.to_owned()
if seg == "" {
continue
}
if cur == "" {
cur = seg
} else if cur == "/" {
cur = "/" + seg
} else {
cur = cur + "/" + seg
}
if !path_exists(cur) {
@fs.create_dir(cur) catch {
_ => raise MbError("failed to create directory: " + cur)
}
}
}
}
///|
/// `rm -rf`. Delegates to the system tool so symlinks, read-only entries and
/// large trees are all handled correctly.
pub fn rm_rf(path : String) -> Unit raise MbError {
if path == "" || !path_exists(path) {
return
}
if path == "/" {
raise MbError("refusing to remove the filesystem root")
}
if !run_ok("rm -rf -- " + shell_quote(path)) {
raise MbError("failed to remove: " + path)
}
}
///|
/// Names (without the directory prefix) of entries in `path`. Returns an empty
/// array when the directory cannot be read.
pub fn list_dir_unsafe(path : String) -> Array[String] {
let entries = @fs.read_dir(path) catch { _ => return [] }
let out : Array[String] = []
for e in entries {
out.push(e)
}
out
}
///|
/// Like `list_dir_unsafe` but reports failures.
pub fn list_dir(path : String) -> Array[String] raise MbError {
let entries = @fs.read_dir(path) catch {
_ => raise MbError("failed to read directory: " + path)
}
let out : Array[String] = []
for e in entries {
out.push(e)
}
out
}
///|
pub fn read_bytes(path : String) -> Bytes raise MbError {
@fs.read_file_to_bytes(path) catch {
_ => raise MbError("failed to read file: " + path)
}
}
///|
pub fn read_text(path : String) -> String raise MbError {
@fs.read_file_to_string(path) catch {
_ => raise MbError("failed to read file: " + path)
}
}
///|
pub fn write_bytes(path : String, data : Bytes) -> Unit raise MbError {
let parent = parent_dir(path)
if parent != "" {
mkdir_p(parent)
}
@fs.write_bytes_to_file(path, data) catch {
_ => raise MbError("failed to write file: " + path)
}
}
///|
pub fn write_text(path : String, text : String) -> Unit raise MbError {
let parent = parent_dir(path)
if parent != "" {
mkdir_p(parent)
}
@fs.write_string_to_file(path, text) catch {
_ => raise MbError("failed to write file: " + path)
}
}
///|
pub fn parent_dir(path : String) -> String {
match path.rev_find("/") {
Some(i) if i == 0 => "/"
Some(i) => path[:i].to_owned()
None => ""
}
}
///|
pub fn base_name(path : String) -> String {
match path.rev_find("/") {
Some(i) => path[i + 1:].to_owned()
None => path
}
}
///|
/// Recursively collect regular files below `dir` (absolute or relative to
/// `dir`), returned with `dir`-relative prefixes.
pub fn walk_files(dir : String) -> Array[String] raise MbError {
let out : Array[String] = []
walk_into(dir, "", out)
out
}
///|
fn walk_into(
root : String,
rel : String,
out : Array[String],
) -> Unit raise MbError {
let here = if rel == "" { root } else { path_join(root, rel) }
for name in list_dir(here) {
let child_rel = if rel == "" { name } else { rel + "/" + name }
let child_abs = path_join(root, child_rel)
if dir_exists(child_abs) {
walk_into(root, child_rel, out)
} else if file_exists(child_abs) {
out.push(child_rel)
}
}
}
///|
/// Copy a regular file, creating parent directories as needed.
pub fn copy_file(src : String, dst : String) -> Unit raise MbError {
write_bytes(dst, read_bytes(src))
chmod_exec_if(dst, is_executable_file(src))
}
///|
fn chmod_exec_if(path : String, yes : Bool) -> Unit {
if yes {
chmod_exec(path)
}
}
///|
pub fn chmod_exec(path : String) -> Unit {
ignore(mb_chmod_exec(cstring(path)))
}
///|
/// True when the file is a native *executable* (ELF or Mach-O), as opposed
/// to a relocatable object file which shares the same leading magic.
pub fn is_executable_file(path : String) -> Bool {
let data = read_bytes(path) catch { _ => return false }
if data.length() < 20 {
return false
}
let b0 = data[0].to_int() & 0xff
let b1 = data[1].to_int() & 0xff
let b2 = data[2].to_int() & 0xff
let b3 = data[3].to_int() & 0xff
// ELF: 0x7f 'E' 'L' 'F'; e_type at offset 16 (2: ET_EXEC, 3: ET_DYN).
if b0 == 0x7f && b1 == 0x45 && b2 == 0x4c && b3 == 0x46 {
let little = (data[5].to_int() & 0xff) == 1
let e_type = if little {
(data[16].to_int() & 0xff) | ((data[17].to_int() & 0xff) << 8)
} else {
((data[16].to_int() & 0xff) << 8) | (data[17].to_int() & 0xff)
}
return e_type == 2 || e_type == 3
}
// Mach-O: filetype (MH_EXECUTE = 2) is a u32 at offset 12.
let ft_le = (data[12].to_int() & 0xff) |
((data[13].to_int() & 0xff) << 8) |
((data[14].to_int() & 0xff) << 16) |
((data[15].to_int() & 0xff) << 24)
let ft_be = ((data[12].to_int() & 0xff) << 24) |
((data[13].to_int() & 0xff) << 16) |
((data[14].to_int() & 0xff) << 8) |
(data[15].to_int() & 0xff)
let mach_o_le_32 = b0 == 0xce && b1 == 0xfa && b2 == 0xed && b3 == 0xfe
let mach_o_le_64 = b0 == 0xcf && b1 == 0xfa && b2 == 0xed && b3 == 0xfe
let mach_o_be_32 = b0 == 0xfe && b1 == 0xed && b2 == 0xfa && b3 == 0xce
let mach_o_be_64 = b0 == 0xfe && b1 == 0xed && b2 == 0xfa && b3 == 0xcf
if mach_o_le_32 || mach_o_le_64 {
return ft_le == 2
}
if mach_o_be_32 || mach_o_be_64 {
return ft_be == 2
}
// Universal (fat) binaries are also acceptable.
b0 == 0xca && b1 == 0xfe && b2 == 0xba && b3 == 0xbe
}
///|
/// NUL-terminated byte view for C entry points.
pub fn cstring(s : String) -> Bytes {
@utf8.encode(s) + Bytes::make(1, (0).to_byte())
}
///|
/// Run a shell command, inheriting stdio. Returns the exit code.
pub fn run_status(cmd : String) -> Int {
mb_system(cstring(cmd))
}
///|
/// Run a shell command and capture its combined output.
pub fn run_capture(cmd : String) -> String {
@utf8.decode_lossy(mb_popen(cstring(cmd)))
}
///|
/// Run a shell command, raising when it exits non-zero.
pub fn run_checked(cmd : String) -> Unit raise MbError {
let code = run_status(cmd)
if code != 0 {
raise MbError("command failed (exit \{code}): " + cmd)
}
}
///|
/// Run a shell command, returning whether it succeeded.
pub fn run_ok(cmd : String) -> Bool {
run_status(cmd) == 0
}
///|
/// Single-quote a string for safe interpolation into a `/bin/sh` command.
pub fn shell_quote(s : String) -> String {
"'" + s.replace(old="'", new="'\\''") + "'"
}
///|
/// SHA-256 of `data` as a lowercase hex string.
pub fn sha256_hex(data : Bytes) -> String {
@crypto.bytes_to_hex_string(@crypto.sha256(data))
}
///|
/// The platform triple used to select MoonBit toolchain archives.
pub fn platform() -> String raise MbError {
let p = @utf8.decode_lossy(mb_platform())
if p == "" {
raise MbError("unsupported platform: mb only supports Linux and macOS")
} else {
p
}
}
///|
pub fn is_tty() -> Bool {
mb_isatty_stdout() != 0
}