///|
pub(all) suberror ToolchainError {
ToolchainError(String)
}
///|
/// Effects are supplied by the embedding host. No engine dependency on processes.
pub(all) struct Host {
execute : (String, Array[String], String, String, Int32) -> Json raise
kind : (String) -> Int32 raise
read : (String) -> String raise
write : (String, String) -> Unit raise
list : (String) -> Array[String] raise
mkdir : (String) -> Unit raise
remove_file : (String) -> Unit raise
remove_dir : (String) -> Unit raise
rename : (String, String) -> Unit raise
temporary : () -> String raise
environment : Map[String, String]
cwd : String
system : String
architecture : String
process : Bool
cancelled : () -> Bool
}
///|
pub fn Host::platform() -> Host {
{
execute: @platform.process,
kind: @platform.kind,
read: @platform.read,
write: @platform.write,
list: @platform.list,
mkdir: @platform.mkdir,
remove_file: @platform.remove_file,
remove_dir: @platform.remove_dir,
rename: @platform.rename,
temporary: @platform.temporary,
environment: @platform.environment(),
cwd: @platform.cwd(),
system: @platform.system_type(),
architecture: @platform.architecture(),
process: @platform.process_supported(),
cancelled: fn() { false },
}
}
///|
fn field(value : Json, key : String) -> Json {
match value {
Object(fields) => fields.get(key).unwrap_or(Json::null())
_ => Json::null()
}
}
///|
fn string(value : Json, key : String, fallback~ : String = "") -> String {
match field(value, key) {
String(text) => text
_ => fallback
}
}
///|
fn object(items : Array[(String, Json)]) -> Json {
Json::object(Map::from_array(items))
}
///|
fn check_cancelled(host : Host) -> Unit raise {
if (host.cancelled)() {
raise ToolchainError("Cancelled")
}
}
///|
fn execute(
host : Host,
program : String,
args : Array[String],
cwd : String,
home : String,
timeout : Int32,
) -> Json raise {
check_cancelled(host)
if !host.process {
raise ToolchainError("Host does not provide process execution")
}
if [program, cwd, home].any(s => s.contains("\u0000")) ||
args.any(s => s.contains("\u0000")) {
raise ToolchainError("Process arguments must not contain NUL")
}
let result = (host.execute)(program, args, cwd, home, timeout)
check_cancelled(host)
if !(field(result, "stdout") is String(_)) ||
!(field(result, "stderr") is String(_)) {
raise ToolchainError("Invalid process host response")
}
match field(result, "exitCode") {
Number(n, ..) if n >= -1.0 && n <= 255.0 && n == n.to_int().to_double() =>
()
_ => raise ToolchainError("Invalid process host exit code")
}
result
}
///|
fn successful(result : Json) -> Bool {
match field(result, "exitCode") {
Number(n, ..) => n == 0.0
_ => false
}
}
///|
fn require_success(result : Json, operation : String) -> Unit raise {
if !successful(result) {
raise ToolchainError(
operation + ": " + string(result, "stderr") + string(result, "stdout"),
)
}
}
///|
fn join(a : String, b : String) -> String {
if a.has_suffix("/") || a.has_suffix("\\") {
a + b
} else {
a + "/" + b
}
}
///|
fn executable(host : Host, home : String, name : String) -> String {
join(
join(home, "bin"),
name + (if host.system == "windows" { ".exe" } else { "" }),
)
}
///|
fn make_directories(host : Host, path : String) -> Unit raise {
if (host.kind)(path) == 2 {
return
}
let parts = path
.replace(old="\\", new="/")
.split("/")
.map(StringView::to_owned)
.collect()
if parts.length() > 1 {
let parent = parts[:parts.length() - 1].to_owned().join("/")
if parent != "" {
make_directories(host, parent)
}
}
(host.mkdir)(path)
}
///|
fn remove_tree(host : Host, path : String) -> Unit raise {
match (host.kind)(path) {
0 => ()
2 => {
for child in (host.list)(path) {
if child == "" ||
child == "." ||
child == ".." ||
child.contains("/") ||
child.contains("\\") {
raise ToolchainError("Invalid cleanup entry")
}
remove_tree(host, join(path, child))
}
(host.remove_dir)(path)
}
_ => (host.remove_file)(path)
}
}