add epm — El Package Manager
Introduces epm/, a new component written entirely in native El.
epm manages vessels (El's deployable package format): publish to Engram,
install with full dependency resolution, list registry contents, and
inspect vessel metadata.
- epm/manifest.el — package manifest
- epm/src/manifest.el — vessel/package manifest parser (line-by-line,
same approach as elb.el)
- epm/src/registry.el — Engram-backed vessel registry (POST /api/nodes,
GET /api/search); vessels stored as Entity nodes
with label "vessel:<name>:<version>"
- epm/src/install.el — topological dependency resolver with cycle
detection; installs to .epm/vessels/<name>/
- epm/src/epm.el — main entry point: publish / install / list / info
This commit is contained in:
@@ -0,0 +1,226 @@
|
||||
// epm/src/install.el — dependency resolution and vessel installation
|
||||
//
|
||||
// Reads the current project's manifest.el, walks the full dependency graph
|
||||
// by fetching each dep from Engram, detects cycles, and produces a
|
||||
// topological install order (dependencies before dependents).
|
||||
//
|
||||
// Installed vessels land in: .epm/vessels/<name>/
|
||||
//
|
||||
// Cycle detection: each recursion carries a chain of names currently on
|
||||
// the call stack. If a dep already appears in the chain it's a cycle.
|
||||
//
|
||||
// Algorithm:
|
||||
// 1. Parse manifest.el to get direct deps
|
||||
// 2. For each dep, fetch its manifest from Engram
|
||||
// 3. Recurse into that dep's own deps (depth-first)
|
||||
// 4. Append dep to order after all its transitive deps
|
||||
// 5. Deduplicate: skip already-ordered vessels
|
||||
|
||||
// ── List helpers (operating on String lists) ──────────────────────────────────
|
||||
|
||||
// list_contains returns true if item appears in lst.
|
||||
fn list_contains(lst: [String], item: String) -> Bool {
|
||||
let n: Int = native_list_len(lst)
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let v: String = native_list_get(lst, i)
|
||||
if str_eq(v, item) { return true }
|
||||
let i = i + 1
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ── Dependency graph walker ───────────────────────────────────────────────────
|
||||
|
||||
// resolve_deps_recursive fetches a vessel's metadata from Engram and walks
|
||||
// its dependency list depth-first, building a topological order.
|
||||
//
|
||||
// Parameters:
|
||||
// name — vessel name to resolve
|
||||
// version — version string (may be "" for any)
|
||||
// chain — names currently on the recursion stack (cycle detection)
|
||||
// visited — names already appended to order (dedup)
|
||||
// order — accumulated result (list of "name:version" strings)
|
||||
//
|
||||
// Returns a Map with keys "visited", "order", and "ok" (Bool as String).
|
||||
fn resolve_deps_recursive(name: String, version: String, chain: [String], visited: [String], order: [String]) -> Map<String, Any> {
|
||||
let key: String = name + ":" + version
|
||||
|
||||
// Skip if already ordered
|
||||
if list_contains(visited, key) {
|
||||
return { "visited": visited, "order": order, "ok": "true" }
|
||||
}
|
||||
|
||||
// Cycle detection: if name is already in the call chain, it's a cycle
|
||||
if list_contains(chain, name) {
|
||||
println("epm: error: dependency cycle detected involving '" + name + "'")
|
||||
return { "visited": visited, "order": order, "ok": "false" }
|
||||
}
|
||||
|
||||
// Push this name onto the cycle-detection chain
|
||||
let chain = native_list_append(chain, name)
|
||||
|
||||
// Fetch vessel metadata from Engram
|
||||
let content: String = registry_find(name, version)
|
||||
if str_eq(content, "") {
|
||||
println("epm: error: vessel '" + name + "' version '" + version + "' not found in registry")
|
||||
return { "visited": visited, "order": order, "ok": "false" }
|
||||
}
|
||||
|
||||
// content is a JSON blob: {"name":..., "version":..., "deps":[...]}
|
||||
let actual_version: String = json_get_string(content, "version")
|
||||
let actual_key: String = name + ":" + actual_version
|
||||
let deps_json: String = json_get_raw(content, "deps")
|
||||
|
||||
// Walk this vessel's dependencies first (depth-first)
|
||||
let dep_count: Int = json_array_len(deps_json)
|
||||
let di: Int = 0
|
||||
while di < dep_count {
|
||||
let dep: String = json_array_get(deps_json, di)
|
||||
let dep_name: String = json_get_string(dep, "name")
|
||||
let dep_ver: String = json_get_string(dep, "version")
|
||||
|
||||
let r = resolve_deps_recursive(dep_name, dep_ver, chain, visited, order)
|
||||
let ok_str: String = r["ok"]
|
||||
if str_eq(ok_str, "false") {
|
||||
return r
|
||||
}
|
||||
let visited = r["visited"]
|
||||
let order = r["order"]
|
||||
|
||||
let di = di + 1
|
||||
}
|
||||
|
||||
// Now append this vessel (after all its deps)
|
||||
let visited = native_list_append(visited, actual_key)
|
||||
let order = native_list_append(order, actual_key)
|
||||
|
||||
return { "visited": visited, "order": order, "ok": "true" }
|
||||
}
|
||||
|
||||
// ── Install order ─────────────────────────────────────────────────────────────
|
||||
|
||||
// resolve_install_order reads manifest.el from the current directory and
|
||||
// returns a list of "name:version" strings in topological install order.
|
||||
//
|
||||
// Returns an empty list on failure (error is printed to stdout).
|
||||
fn resolve_install_order() -> [String] {
|
||||
let src: String = fs_read("manifest.el")
|
||||
if str_eq(src, "") {
|
||||
println("epm: no manifest.el found in current directory")
|
||||
return native_list_empty()
|
||||
}
|
||||
|
||||
let deps_json: String = manifest_deps(src)
|
||||
let dep_count: Int = json_array_len(deps_json)
|
||||
if dep_count == 0 {
|
||||
println("epm: no dependencies declared in manifest.el")
|
||||
return native_list_empty()
|
||||
}
|
||||
|
||||
let chain: [String] = native_list_empty()
|
||||
let visited: [String] = native_list_empty()
|
||||
let order: [String] = native_list_empty()
|
||||
|
||||
let i: Int = 0
|
||||
while i < dep_count {
|
||||
let dep: String = json_array_get(deps_json, i)
|
||||
let dep_name: String = json_get_string(dep, "name")
|
||||
let dep_ver: String = json_get_string(dep, "version")
|
||||
|
||||
let r = resolve_deps_recursive(dep_name, dep_ver, chain, visited, order)
|
||||
let ok_str: String = r["ok"]
|
||||
if str_eq(ok_str, "false") {
|
||||
return native_list_empty()
|
||||
}
|
||||
let visited = r["visited"]
|
||||
let order = r["order"]
|
||||
|
||||
let i = i + 1
|
||||
}
|
||||
|
||||
return order
|
||||
}
|
||||
|
||||
// ── Installation ──────────────────────────────────────────────────────────────
|
||||
|
||||
// install_vessel fetches a vessel's source from Engram and writes it
|
||||
// to .epm/vessels/<name>/.
|
||||
//
|
||||
// Currently writes the content JSON blob as a manifest record so that
|
||||
// other tools can introspect what is installed. When Engram stores actual
|
||||
// source archives, this function would extract them.
|
||||
//
|
||||
// Returns true on success.
|
||||
fn install_vessel(name: String, version: String) -> Bool {
|
||||
let content: String = registry_find(name, version)
|
||||
if str_eq(content, "") {
|
||||
println("epm: error: cannot fetch '" + name + "' '" + version + "' from registry")
|
||||
return false
|
||||
}
|
||||
|
||||
let install_dir: String = ".epm/vessels/" + name
|
||||
let mkdir_ret: Int = exec_command("mkdir -p " + install_dir)
|
||||
|
||||
// Write the content blob as installed.json so downstream tools know what's there
|
||||
let dest: String = install_dir + "/installed.json"
|
||||
fs_write(dest, content)
|
||||
|
||||
println("epm: installed " + name + " " + version + " -> " + dest)
|
||||
return true
|
||||
}
|
||||
|
||||
// ── Top-level install ─────────────────────────────────────────────────────────
|
||||
|
||||
// cmd_install resolves and installs all dependencies for the current project.
|
||||
fn cmd_install() -> Void {
|
||||
let src: String = fs_read("manifest.el")
|
||||
if str_eq(src, "") {
|
||||
println("epm: no manifest.el found in current directory")
|
||||
exit(1)
|
||||
}
|
||||
|
||||
let pkg_name: String = manifest_name(src)
|
||||
println("epm: resolving dependencies for " + pkg_name)
|
||||
|
||||
let order: [String] = resolve_install_order()
|
||||
let n: Int = native_list_len(order)
|
||||
|
||||
if n == 0 {
|
||||
println("epm: nothing to install")
|
||||
return
|
||||
}
|
||||
|
||||
println("epm: install order (" + native_int_to_str(n) + " vessels):")
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let entry: String = native_list_get(order, i)
|
||||
println(" " + entry)
|
||||
let i = i + 1
|
||||
}
|
||||
|
||||
// Ensure .epm directory exists
|
||||
let mk2: Int = exec_command("mkdir -p .epm/vessels")
|
||||
|
||||
// Install each vessel in topological order
|
||||
let j: Int = 0
|
||||
while j < n {
|
||||
let kv: String = native_list_get(order, j)
|
||||
// Split "name:version" on the last colon
|
||||
let colon: Int = str_index_of(kv, ":")
|
||||
if colon < 0 {
|
||||
println("epm: error: malformed order entry: " + kv)
|
||||
exit(1)
|
||||
}
|
||||
let vname: String = str_slice(kv, 0, colon)
|
||||
let vver: String = str_slice(kv, colon + 1, str_len(kv))
|
||||
let ok: Bool = install_vessel(vname, vver)
|
||||
if !ok {
|
||||
println("epm: install failed at " + kv)
|
||||
exit(1)
|
||||
}
|
||||
let j = j + 1
|
||||
}
|
||||
|
||||
println("epm: all dependencies installed")
|
||||
}
|
||||
Reference in New Issue
Block a user