///|
/// A portable sidecar entry for one planned build edge.
pub(all) struct BuildStateEntry {
edge_key : String
command_hash : String
outputs : Array[String]
} derive(Debug, Eq)
///|
/// A deterministic, text-serializable incremental state cache.
pub(all) struct BuildState {
entries : Array[BuildStateEntry]
} derive(Debug, Eq)
///|
fn state_hash(text : String) -> String {
let mut hash : UInt64 = 14695981039346656037
for ch in text {
hash = (hash ^ ch.to_int().to_uint64()) * 1099511628211
}
hash.to_string()
}
///|
pub fn BuildState::from_plan(plan : MaterializedPlan) -> BuildState {
let entries : Array[BuildStateEntry] = []
for index, edge in plan.edges {
let command = match plan.commands.get(index) {
Some(value) => value
None => ""
}
entries.push({
edge_key: edge.key(),
command_hash: state_hash(command),
outputs: edge.outputs.copy(),
})
}
{ entries, }
}
///|
fn state_escape(text : String) -> String {
let mut result = ""
for ch in text {
if ch == '\t' || ch == '\n' || ch == '\\' {
result += "\\"
}
result += "\{ch}"
}
result
}
///|
fn state_unescape(text : String) -> String {
let mut result = ""
let mut escaped = false
for ch in text {
if escaped {
result += "\{ch}"
escaped = false
} else if ch == '\\' {
escaped = true
} else {
result += "\{ch}"
}
}
if escaped {
result += "\\"
}
result
}
///|
fn state_split(text : String, separator : Char) -> Array[String] {
let result : Array[String] = []
let mut current = ""
let mut escaped = false
for ch in text {
if escaped {
current += "\\"
current += "\{ch}"
escaped = false
} else if ch == '\\' {
escaped = true
} else if ch == separator {
result.push(current)
current = ""
} else {
current += "\{ch}"
}
}
if escaped {
current += "\\"
}
result.push(current)
result
}
///|
pub fn BuildState::to_text(self : BuildState) -> String {
let mut result = "# MoonNinja build state v1\n"
for entry in self.entries {
let output_text : Array[String] = []
for output in entry.outputs {
output_text.push(state_escape(output))
}
result += state_escape(entry.edge_key) +
"\t" +
entry.command_hash +
"\t" +
join_strings(output_text, ",") +
"\n"
}
result
}
///|
fn state_lines(text : String) -> Array[String] {
let lines : Array[String] = []
let mut current = ""
for ch in text {
if ch == '\n' {
lines.push(current)
current = ""
} else if ch != '\r' {
current += "\{ch}"
}
}
if current != "" {
lines.push(current)
}
lines
}
///|
/// Parse the state format and reject malformed records instead of silently
/// treating a corrupt cache as up to date.
pub fn parse_build_state(text : String) -> Result[BuildState, String] {
let entries : Array[BuildStateEntry] = []
for line in state_lines(text) {
if line == "" || line == "# MoonNinja build state v1" {
continue
}
let fields = state_split(line, '\t')
if fields.length() != 3 || fields[0] == "" || fields[1] == "" {
return Err("malformed build state record")
}
let outputs : Array[String] = []
if fields[2] != "" {
for output in state_split(fields[2], ',') {
let value = state_unescape(output)
if value != "" && !outputs.contains(value) {
outputs.push(value)
}
}
}
entries.push({
edge_key: state_unescape(fields[0]),
command_hash: fields[1],
outputs,
})
}
Ok({ entries, })
}
///|
/// Return true when an edge is absent or its command has changed.
pub fn BuildState::needs_rebuild(
self : BuildState,
edge_key : String,
command : String,
) -> Bool {
let expected = state_hash(command)
for entry in self.entries {
if entry.edge_key == edge_key {
return entry.command_hash != expected
}
}
true
}
///|
/// Find a state entry by edge key for hosts that also inspect output paths.
pub fn BuildState::find(
self : BuildState,
edge_key : String,
) -> BuildStateEntry? {
for entry in self.entries {
if entry.edge_key == edge_key {
return Some(entry)
}
}
None
}