3. Packages and system interfaces
Split a package by responsibility: model, parsing, operations, and entry-point
policy. root imports the current package; lib::<alias> imports a dependency
selected by the manifest. Keep the dependency graph visible and pin application
resolutions in longbow.lock.
Filesystem, process, and network APIs return explicit errors and own host resources. Close or defer cleanup at the scope that acquired them. Normalize paths before policy decisions, pass process arguments as values rather than a shell string, and bound network reads, waits, and retries. A deterministic diagnostic should identify the operation without leaking platform-specific ordering into tests.
Read filesystem, child processes, and networking only for the interface you are using.
Tested example
Keep argument capture and environment/process failures at the CLI boundary of a multi-module package.
Excerpt in focus
curriculum/main-evidence/projects/workspace_guard/src/main.fl:1–45
module main;
@imports :: root::commands :: root::model :: lib::std::fmt :: lib::std::os::arguments :: lib::std::os::environment;
const stringv ROOT_ENVIRONMENT = "FLECHA_GUARD_ROOT";
i32 main() { values, captureError := arguments::capture(); if captureError != none { fmt::eprintln("workspace_guard: cannot capture command-line arguments"); return 3; } if values.len() < 2 { print_usage(); return 2; }
command := values[1].view(); error result = none; u32 childStatus = 0;
if command == "snapshot" or command == "verify" { result = snapshot_or_verify(values[:], command == "snapshot"); } else if command == "diff" and values.len() == 4 { result = commands::diff(values[2].view(), values[3].view()); } else if command == "run" and values.len() >= 6 and values[4] == "--" { childStatus, result = commands::run( values[2].view(), values[3].view(), values[5:] ); if result == none { return childStatus.cast<i32>; } } else if command == "help" or command == "--help" or command == "-h" { print_usage(); return 0; } else { result = model::GuardError.Usage; }This excerpt depends on the other package files. The complete compiled source is available below.
Complete package source (7 files)
longbow.toml
[package]name = "workspace_guard"version = "0.1.0"kind = "program"
[dependencies]longbow.lock
{ "schema": 1, "root": "root", "packages": [ { "id": "root", "identity": "local/workspace_guard", "source": { "kind": "workspace", "path": "." }, "version": "0.1.0", "dependencies": { "std": "toolchain-std" } }, { "id": "toolchain-std", "identity": "flecha/std", "source": { "kind": "toolchain" }, "dependencies": {} } ]}src/commands.fl
module commands;
@imports :: root::manifest :: root::model :: root::scanner :: lib::std::containers::collections :: lib::std::fmt :: lib::std::process::command;
export error snapshot(stringv rootPath, stringv manifestPath) { entries, scanError := scanner::scan(rootPath); if scanError != none { return scanError; } writeError := manifest::write(manifestPath, &entries); if writeError != none { return writeError; } fmt::print("snapshot: "); fmt::print(entries.len()); fmt::println(" files"); return none;}
export error verify(stringv rootPath, stringv manifestPath, bool quiet = false) { expected, manifestError := manifest::read(manifestPath); if manifestError != none { return manifestError; } actual, scanError := scanner::scan(rootPath); if scanError != none { return scanError; }
usize differences = compare(&expected, &actual, false); if differences != 0 { fmt::eprint("workspace_guard: "); fmt::eprint(differences); fmt::eprintln(" workspace differences"); return model::GuardError.Mismatch; } if !quiet { fmt::print("verified: "); fmt::print(actual.len()); fmt::println(" files"); } return none;}
export error diff(stringv oldManifest, stringv newManifest) { oldEntries, oldError := manifest::read(oldManifest); if oldError != none { return oldError; } newEntries, newError := manifest::read(newManifest); if newError != none { return newError; }
usize differences = compare(&oldEntries, &newEntries, true); fmt::print("diff: "); fmt::print(differences); fmt::println(" differences"); return none;}
usize compare( &const collections::Array<model::Entry> expected, &const collections::Array<model::Entry> actual, bool standardOutput) { expectedValues := expected->view(); actualValues := actual->view(); usize expectedIndex = 0; usize actualIndex = 0; usize differences = 0;
while expectedIndex < expectedValues.len() or actualIndex < actualValues.len() { if expectedIndex == expectedValues.len() { report("added", actualValues[actualIndex].path(), standardOutput); actualIndex++; differences++; proceed; } if actualIndex == actualValues.len() { report("removed", expectedValues[expectedIndex].path(), standardOutput); expectedIndex++; differences++; proceed; }
expectedPath := expectedValues[expectedIndex].path(); actualPath := actualValues[actualIndex].path(); if model::text_less(expectedPath, actualPath) { report("removed", expectedPath, standardOutput); expectedIndex++; differences++; proceed; } if model::text_less(actualPath, expectedPath) { report("added", actualPath, standardOutput); actualIndex++; differences++; proceed; }
if !model::same_content( &expectedValues[expectedIndex], &actualValues[actualIndex] ) { report("modified", expectedPath, standardOutput); differences++; } expectedIndex++; actualIndex++; }
return differences;}
void report(stringv kind, stringv path, bool standardOutput) { if standardOutput { fmt::print(kind); fmt::print(" "); fmt::println(path); } else { fmt::eprint(kind); fmt::eprint(" "); fmt::eprintln(path); }}
export (u32, error) run( stringv rootPath, stringv manifestPath, []const string arguments) { verifyError := verify(rootPath, manifestPath, true); if verifyError != none { return (none, verifyError); } if arguments.len() == 0 { return (none, model::GuardError.Usage); }
executable, copyError := model::copy_text(arguments[0].view()); if copyError != none { return (none, copyError); } childCommand := command::Command::init(executable); loop usize index = 1; index < arguments.len(); index++ { argument, argumentError := model::copy_text(arguments[index].view()); if argumentError != none { return (none, argumentError); } if childCommand.arg(argument) != none { return (none, AllocationError.OutOfMemory); } }
child, spawnError := childCommand.spawn(); if spawnError != none { return (none, spawnError); } status, waitError := child.wait(); if waitError != none { return (none, waitError); } if !status.exited() { return (none, model::GuardError.ProcessFailure); } return (status.detail(), none);}src/main.fl
module main;
@imports :: root::commands :: root::model :: lib::std::fmt :: lib::std::os::arguments :: lib::std::os::environment;
const stringv ROOT_ENVIRONMENT = "FLECHA_GUARD_ROOT";
i32 main() { values, captureError := arguments::capture(); if captureError != none { fmt::eprintln("workspace_guard: cannot capture command-line arguments"); return 3; } if values.len() < 2 { print_usage(); return 2; }
command := values[1].view(); error result = none; u32 childStatus = 0;
if command == "snapshot" or command == "verify" { result = snapshot_or_verify(values[:], command == "snapshot"); } else if command == "diff" and values.len() == 4 { result = commands::diff(values[2].view(), values[3].view()); } else if command == "run" and values.len() >= 6 and values[4] == "--" { childStatus, result = commands::run( values[2].view(), values[3].view(), values[5:] ); if result == none { return childStatus.cast<i32>; } } else if command == "help" or command == "--help" or command == "-h" { print_usage(); return 0; } else { result = model::GuardError.Usage; }
if result == none { return 0; } if result == model::GuardError.Usage { print_usage(); return 2; } if result == model::GuardError.InvalidManifest { fmt::eprintln("workspace_guard: invalid manifest"); return 4; } if result == model::GuardError.Mismatch { return 5; } if result == model::GuardError.UnsupportedEntry { return 6; } if result == model::GuardError.ProcessFailure { fmt::eprintln("workspace_guard: child process terminated abnormally"); return 7; }
fmt::eprintln("workspace_guard: operation failed"); return 3;}
error snapshot_or_verify([]const string values, bool createSnapshot) { string root = none; stringv manifestPath = ""; if values.len() == 4 { error copyError; root, copyError = model::copy_text(values[2].view()); if copyError != none { return copyError; } manifestPath = values[3].view(); } else if values.len() == 3 { bool present; error environmentError; root, present, environmentError = environment::get(ROOT_ENVIRONMENT); if environmentError != none { return environmentError; } if !present or root.len() == 0 { fmt::eprint("workspace_guard: set "); fmt::eprintln(ROOT_ENVIRONMENT); return model::GuardError.Usage; } manifestPath = values[2].view(); } else { return model::GuardError.Usage; }
if createSnapshot { return commands::snapshot(root.view(), manifestPath); } return commands::verify(root.view(), manifestPath);}
void print_usage() { fmt::println("workspace_guard - deterministic workspace snapshots"); fmt::println(""); fmt::println("usage:"); fmt::println(" workspace_guard snapshot <root> <manifest>"); fmt::println(" workspace_guard snapshot <manifest> # root from FLECHA_GUARD_ROOT"); fmt::println(" workspace_guard verify <root> <manifest>"); fmt::println(" workspace_guard verify <manifest> # root from FLECHA_GUARD_ROOT"); fmt::println(" workspace_guard diff <old-manifest> <new-manifest>"); fmt::println(" workspace_guard run <root> <manifest> -- <program> [arguments...]");}src/manifest.fl
module manifest;
@imports :: root::model :: lib::std::containers::collections :: lib::std::encoding::hex :: lib::std::io::buffered :: lib::std::io::errors as io_errors :: lib::std::io::file :: lib::std::io::filesystem :: lib::std::text::strings :: lib::std::text::utf8;
const stringv HEADER = "FLECHA-WORKSPACE-SNAPSHOT\t1\n";const usize WRITE_CAPACITY = 8192;
export error write( stringv destination, &const collections::Array<model::Entry> entries) { present, existsError := filesystem::exists(destination); if existsError != none { return existsError; } if present { return io_errors::IoError.AlreadyExists; }
options := build file::OpenOptions { self.write = true; self.createNew = true; }; output, openError := file::File::open(destination, options); if openError != none { return openError; }
error result = none; { adapter := output.writer(); writer, writerError := buffered::BufferedWriter::withCapacity(adapter, WRITE_CAPACITY); if writerError != none { result = writerError; } else { result = write_contents(&writer, entries); } }
if result == none { result = output.flush(); } closeError := output.close(); if result == none { result = closeError; }
if result != none { error _ = filesystem::remove_file(destination); } return result;}
error write_contents( &buffered::BufferedWriter writer, &const collections::Array<model::Entry> entries) { error writeError = writer->writeAll(HEADER.cast<[]const u8>); if writeError != none { return writeError; }
values := entries->view(); loop usize index = 0; index < values.len(); index++ { pathHex, pathError := hex::encode(values[index].path()); if pathError != none { return pathError; } sizeText, sizeError := strings::int_to_string<u64>(values[index].byteSize()); if sizeError != none { return sizeError; }
writeError = writer->writeAll( values[index].hash().cast<[]const u8> ); if writeError == none { writeError = writer->writeAll("\t".cast<[]const u8>); } if writeError == none { writeError = writer->writeAll(sizeText.view().cast<[]const u8>); } if writeError == none { writeError = writer->writeAll("\t".cast<[]const u8>); } if writeError == none { writeError = writer->writeAll(pathHex.view().cast<[]const u8>); } if writeError == none { writeError = writer->writeAll("\n".cast<[]const u8>); } if writeError != none { return writeError; } }
return writer->flush();}
export (collections::Array<model::Entry>, error) read(stringv sourcePath) { contents, readError := filesystem::read_file(sourcePath); if readError != none { return (collections::Array<model::Entry>::init(), readError); } return parse(contents.view());}
(collections::Array<model::Entry>, error) parse(stringv contents) { entries := collections::Array<model::Entry>::init(); if contents.len() < HEADER.len() or contents.substr(0, HEADER.len()) != HEADER { return (entries, model::GuardError.InvalidManifest); }
u32 offset = HEADER.len(); while offset < contents.len() { u32 end = offset; while end < contents.len() and contents[end] != '\n' { end++; } if end == contents.len() or end == offset { return (entries, model::GuardError.InvalidManifest); }
entry, entryError := parse_line(contents.substr(offset, end - offset)); if entryError != none { return (entries, entryError); } if !entries.empty() { { values := entries.view(); if !model::text_less( values[values.len() - 1].path(), entry.path() ) { return (entries, model::GuardError.InvalidManifest); } } } pushError := entries.push(entry); if pushError != none { return (entries, pushError); } offset = end + 1; }
return (entries, none);}
(model::Entry, error) parse_line(stringv line) { u32 first = line.len(); u32 second = line.len(); loop u32 index = 0; index < line.len(); index++ { if line[index] != '\t' { proceed; } if first == line.len() { first = index; } else if second == line.len() { second = index; } else { return (none, model::GuardError.InvalidManifest); } } if first == 0 or second <= first + 1 or second + 1 >= line.len() { return (none, model::GuardError.InvalidManifest); }
digestText := line.substr(0, first); sizeText := line.substr(first + 1, second - first - 1); pathText := line.substr(second + 1, line.len() - second - 1);
digestBytes, digestDecodeError := hex::decode_to_bytes(digestText); if digestDecodeError != none or digestBytes.len() != 32 { return (none, model::GuardError.InvalidManifest); } canonicalDigest, digestEncodeError := hex::encode_bytes(digestBytes[:]); if digestEncodeError != none { return (none, digestEncodeError); } if canonicalDigest.view() != digestText { return (none, model::GuardError.InvalidManifest); }
size, sizeParseError := strings::stringv_to_int<u64>(sizeText); if sizeParseError != none { return (none, model::GuardError.InvalidManifest); } canonicalSize, sizeFormatError := strings::int_to_string<u64>(size); if sizeFormatError != none { return (none, sizeFormatError); } if canonicalSize.view() != sizeText { return (none, model::GuardError.InvalidManifest); }
pathValue, pathDecodeError := hex::decode(pathText); if pathDecodeError != none or utf8::validate(pathValue.view()) != none or !model::valid_relative_path(pathValue.view()) { return (none, model::GuardError.InvalidManifest); } canonicalPath, pathEncodeError := hex::encode(pathValue.view()); if pathEncodeError != none { return (none, pathEncodeError); } if canonicalPath.view() != pathText { return (none, model::GuardError.InvalidManifest); }
return (model::Entry::init(pathValue, canonicalDigest, size), none);}src/model.fl
module model;
@imports :: lib::std::text::strings;
export error GuardError { Usage, InvalidManifest, UnsupportedEntry, Mismatch, ProcessFailure}
export structure Entry { string relativePath; string digest; u64 size;};
Entry Entry::init(string relativePath, string digest, u64 size) override { return build Entry { self.relativePath = relativePath; self.digest = digest; self.size = size; };}
stringv Entry::path(&const self) <-self { return self->relativePath.view();}
stringv Entry::hash(&const self) <-self { return self->digest.view();}
u64 Entry::byteSize(&const self) { return self->size;}
export bool text_less(stringv left, stringv right) { u32 common = left.len(); if right.len() < common { common = right.len(); }
loop u32 index = 0; index < common; index++ { if left[index] < right[index] { return true; } if left[index] > right[index] { return false; } }
return left.len() < right.len();}
export bool entry_less(&const Entry left, &const Entry right) { return text_less(left->path(), right->path());}
export bool same_content(&const Entry left, &const Entry right) { return left->byteSize() == right->byteSize() and left->hash() == right->hash();}
export (string, error) copy_text(stringv value) { return strings::string_from_bytes(value.cast<[]const u8>);}
export (string, error) child_path(stringv parent, stringv name) { if parent.len() == 0 { return copy_text(name); }
usize capacity = parent.len().cast<usize> + name.len().cast<usize> + 1; if capacity <= parent.len().cast<usize> { return (none, AllocationError.OutOfMemory); }
builder, allocationError := strings::StringBuilder::withCapacity(capacity); if allocationError != none { return (none, allocationError); } if builder.append(parent) != none or builder.appendChar('/') != none or builder.append(name) != none { return (none, AllocationError.OutOfMemory); }
return builder.toString();}
export bool valid_relative_path(stringv value) { if value.len() == 0 or value[0] == '/' or value[value.len() - 1] == '/' { return false; }
u32 segmentStart = 0; loop u32 index = 0; index <= value.len(); index++ { if index < value.len() { const char BYTE = value[index]; const u8 BYTE_VALUE = BYTE.cast<u8>; if BYTE_VALUE < 32 or BYTE_VALUE == 127 or BYTE == '\\' or BYTE == ':' or BYTE == '*' or BYTE == '?' or BYTE == '"' or BYTE == '<' or BYTE == '>' or BYTE == '|' { return false; } if BYTE != '/' { proceed; } }
const u32 LENGTH = index - segmentStart; if LENGTH == 0 { return false; } segment := value.substr(segmentStart, LENGTH); if segment == "." or segment == ".." { return false; } segmentStart = index + 1; }
return true;}src/scanner.fl
module scanner;
@imports :: root::model :: lib::std::containers::collections :: lib::std::cryptography::sha2 :: lib::std::encoding::hex :: lib::std::fmt :: lib::std::io::buffered :: lib::std::io::file :: lib::std::io::path :: lib::std::text::utf8;
const usize READ_CAPACITY = 32768;
export (collections::Array<model::Entry>, error) scan(stringv rootPath) { entries := collections::Array<model::Entry>::init(); scanError := collect(rootPath, "", &entries); if scanError != none { return (entries, scanError); }
entries.sortBy(model::entry_less); return (entries, none);}
error collect( stringv rootPath, stringv relativePath, &collections::Array<model::Entry> entries) { string directoryPath = none; if relativePath.len() == 0 { error copyError; directoryPath, copyError = model::copy_text(rootPath); if copyError != none { return copyError; } } else { error joinError; directoryPath, joinError = path::join(rootPath, relativePath); if joinError != none { return joinError; } }
directory, openError := file::Directory::open(directoryPath.view()); if openError != none { fmt::eprint("workspace_guard: cannot open directory: "); fmt::eprintln(directoryPath.view()); return openError; }
while true { entry, present, readError := directory.next(); if readError != none { return readError; } if !present { break; } if utf8::validate(entry.name()) != none { fmt::eprintln("workspace_guard: non-UTF-8 path is unsupported"); return model::GuardError.UnsupportedEntry; }
childRelative, relativeError := model::child_path(relativePath, entry.name()); if relativeError != none { return relativeError; } if !model::valid_relative_path(childRelative.view()) { fmt::eprint("workspace_guard: non-portable path: "); fmt::eprintln(childRelative.view()); return model::GuardError.UnsupportedEntry; }
if entry.kind() == file::EntryType.Directory { recurseError := collect(rootPath, childRelative.view(), entries); if recurseError != none { return recurseError; } proceed; } if entry.kind() != file::EntryType.File { fmt::eprint("workspace_guard: unsupported entry: "); fmt::eprintln(childRelative.view()); return model::GuardError.UnsupportedEntry; }
actualPath, joinError := path::join(rootPath, childRelative.view()); if joinError != none { return joinError; } digest, size, hashError := hash_file(actualPath.view()); if hashError != none { fmt::eprint("workspace_guard: cannot hash file: "); fmt::eprintln(childRelative.view()); return hashError; }
pushError := entries->push( model::Entry::init(childRelative, digest, size) ); if pushError != none { return pushError; } }
return directory.close();}
(string, u64, error) hash_file(stringv filePath) { input, openError := file::File::open(filePath, file::OpenOptions::readOnly()); if openError != none { return (none, none, openError); }
hasher := sha2::Sha256::init(); u64 total = 0; error result = none; { adapter := input.reader(); reader, readerError := buffered::BufferedReader::withCapacity(adapter, READ_CAPACITY); if readerError != none { return (none, none, readerError); }
[32768]u8 chunk = none; while true { count, readError := reader.read(chunk[:]); if count > 0 { const u64 NEXT = total + count.cast<u64>; if NEXT < total { return (none, none, AllocationError.OutOfMemory); } total = NEXT; result = hasher.update(chunk[:count]); if result != none { return (none, none, result); } } if readError != none { return (none, none, readError); } if count == 0 { break; } } }
closeError := input.close(); if closeError != none { return (none, none, closeError); } digest, digestError := hasher.finalize(); if digestError != none { return (none, none, digestError); } encoded, encodeError := hex::encode_bytes(digest.bytes()); return (encoded, total, encodeError);}The main module owns CLI parsing and maps host failures to exit codes. Filesystem scanning and manifest behavior stay in sibling modules, which keeps operating-system effects at an explicit package boundary.
Commands
longbow check curriculum/main-evidence/projects/workspace_guardlongbow build curriculum/main-evidence/projects/workspace_guardlongbow run curriculum/main-evidence/projects/workspace_guard -- helpExpected output
workspace_guard - deterministic workspace snapshots
usage: workspace_guard snapshot <root> <manifest> workspace_guard snapshot <manifest> # root from FLECHA_GUARD_ROOT workspace_guard verify <root> <manifest> workspace_guard verify <manifest> # root from FLECHA_GUARD_ROOT workspace_guard diff <old-manifest> <new-manifest> workspace_guard run <root> <manifest> -- <program> [arguments...]exit status: 0Related reference: /examples/workspace_guard/src/main/.