// Dependency files (upstream `typst-cli/src/deps.rs`).
///|
/// Writes dependencies in the given format.
fn write_deps(
world : SystemWorld,
dest : Output,
format : DepsFormat,
outputs : Array[Output]?,
) -> Unit raise @platform.IoError {
match format {
Json => write_deps_json(world, dest, outputs)
Zero => write_deps_zero(world, dest)
Make =>
if outputs is Some(outputs) {
write_deps_make(world, dest, outputs)
}
}
}
///|
/// Writes dependencies in JSON format.
fn write_deps_json(
world : SystemWorld,
dest : Output,
outputs : Array[Output]?,
) -> Unit raise @platform.IoError {
let inputs : Array[Json] = relative_dependencies(world).map(dep => {
Json::string(dep)
})
let outputs : Json = match outputs {
Some(outputs) => {
let list : Array[Json] = []
for output in outputs {
match output {
Path(path) => list.push(Json::string(path))
// Skip stdout
Stdout => ()
}
}
Json::array(list)
}
None => Json::null()
}
let deps : Json = { "inputs": Json::array(inputs), "outputs": outputs }
dest.write(@utf8.encode(json_compact(deps)))
}
///|
/// Serializes JSON like `serde_json::to_string` (keys in insertion order).
fn json_compact(json : Json) -> String {
json.stringify()
}
///|
/// Writes dependencies in the Zero / Text0 format.
fn write_deps_zero(
world : SystemWorld,
dest : Output,
) -> Unit raise @platform.IoError {
let buf = StringBuilder()
for dep in relative_dependencies(world) {
buf.write_string(dep)
buf.write_char('\u{0}')
}
dest.write(@utf8.encode(buf.to_string()))
}
///|
/// Writes dependencies in the Make format.
fn write_deps_make(
world : SystemWorld,
dest : Output,
outputs : Array[Output],
) -> Unit raise @platform.IoError {
let buffer = StringBuilder()
for i, output in outputs {
let path = match output {
Path(path) => path
Stdout =>
raise @platform.IoError(
Other,
"make dependencies contain the output path, but the output was stdout",
)
}
if i != 0 {
buffer.write_char(' ')
}
buffer.write_string(munge(path))
}
// Only create the deps file in case of valid output paths.
buffer.write_char(':')
for dep in relative_dependencies(world) {
buffer.write_char(' ')
buffer.write_string(munge(dep))
}
buffer.write_char('\n')
dest.write(@utf8.encode(buffer.to_string()))
}
///|
/// Based on `munge` in libcpp/mkdeps.cc from the GCC source code. This
/// isn't perfect as some special characters can't be escaped.
fn munge(s : String) -> String {
let res = StringBuilder()
let mut slashes = 0
for c in s {
match c {
'\\' => slashes += 1
'$' => {
res.write_char('$')
slashes = 0
}
':' => {
res.write_char('\\')
slashes = 0
}
' ' | '\t' => {
// `munge`'s source contains a comment here that says: "A space or
// tab preceded by 2N+1 backslashes represents N backslashes
// followed by space..."
for _ in 0..<=slashes {
res.write_char('\\')
}
slashes = 0
}
'#' => {
res.write_char('\\')
slashes = 0
}
_ => slashes = 0
}
res.write_char(c)
}
res.to_string()
}
///|
/// Extracts the current compilation's dependencies as paths relative to the
/// current directory.
fn relative_dependencies(world : SystemWorld) -> Array[String] {
let root = world.root()
let current_dir = world.workdir()
let relative_root = diff_paths(root, current_dir).unwrap_or(root)
world
.dependencies()
.map(dependency => {
match strip_path_prefix(dependency, root) {
Some(rest) => @platform.join(relative_root, rest)
None => dependency
}
})
}
///|
/// Rust's `Path::strip_prefix` (component-wise).
fn strip_path_prefix(path : String, prefix : String) -> String? {
let a = components(path)
let b = components(prefix)
guard a.strip_prefix(b[:]) is Some(rest) else { return None }
Some(rest.to_owned().join(@platform.separator()))
}