// 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 the Gitea registry, detects cycles, and produces // a topological install order (dependencies before dependents). // // Installed vessels land in: ~/.el/packages/// // Installed tracking: ~/.el/packages/installed.json // // 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 metadata from the Gitea registry // 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 // ── Install paths ───────────────────────────────────────────────────────────── // packages_dir returns the root directory for installed vessels. fn packages_dir() -> String { let home: String = env("HOME") return home + "/.el/packages" } // installed_json_path returns the path to the installed-vessel tracker. fn installed_json_path() -> String { return packages_dir() + "/installed.json" } // ── Installed tracking ──────────────────────────────────────────────────────── // read_installed reads installed.json and returns it as a JSON object string. // Returns "{}" if the file is missing or empty. fn read_installed() -> String { let path: String = installed_json_path() let content: String = fs_read(path) if str_eq(content, "") { return "{}" } let trimmed: String = str_trim(content) if str_eq(trimmed, "") { return "{}" } return trimmed } // write_installed writes a JSON object string to installed.json. fn write_installed(content: String) -> Void { let path: String = installed_json_path() fs_write(path, content) } // installed_version returns the currently installed version of a vessel, // or "" if it is not installed. fn installed_version(name: String) -> String { let installed: String = read_installed() return json_get_string(installed, name) } // json_set_key inserts or replaces a string key/value pair in a JSON object. // The object is expected to be a flat { "k":"v", ... } structure. // Returns the updated JSON object string. fn json_set_key(obj: String, key: String, value: String) -> String { let esc_key: String = json_escape_string(key) let esc_val: String = json_escape_string(value) let pair: String = "\"" + esc_key + "\":\"" + esc_val + "\"" let search_key: String = "\"" + esc_key + "\":" // If the key already exists, replace its value in-place. let key_pos: Int = str_index_of(obj, search_key) if key_pos >= 0 { // Find start of value: skip past search_key let val_start: Int = key_pos + str_len(search_key) let after_colon: String = str_slice(obj, val_start, str_len(obj)) let trimmed_after: String = str_trim(after_colon) // The value is a quoted string — find its extent let q1: Int = str_index_of(trimmed_after, "\"") if q1 >= 0 { let inside: String = str_slice(trimmed_after, q1 + 1, str_len(trimmed_after)) let q2: Int = str_index_of(inside, "\"") if q2 >= 0 { // Reconstruct: prefix + new pair + suffix let prefix: String = str_slice(obj, 0, key_pos) // after the closing quote of the old value let old_val_end_in_inside: Int = q2 + 1 // offset back to full string: key_pos + str_len(search_key) + leading whitespace + q1 + 1 + q2 + 1 // Simpler: rebuild from scratch without the old entry, then inject // Strategy: remove old entry by rebuilding pairs let rebuilt: String = json_remove_key(obj, key) return json_append_key(rebuilt, esc_key, esc_val) } } } // Key does not exist — append it return json_append_key(obj, esc_key, esc_val) } // json_remove_key removes a key/value pair from a flat JSON object string. // Returns the object without that key. Used internally by json_set_key. fn json_remove_key(obj: String, key: String) -> String { // Parse pairs manually and rebuild excluding the target key // Trim outer braces let inner: String = str_trim(obj) let inner_len: Int = str_len(inner) if inner_len < 2 { return "{}" } // Remove leading { and trailing } let body: String = str_slice(inner, 1, inner_len - 1) let body_trimmed: String = str_trim(body) if str_eq(body_trimmed, "") { return "{}" } // Split on comma — note: this is a simple split that works for flat objects // with no nested commas (vessel names and versions have no commas) let pairs: [String] = str_split(body_trimmed, ",") let pair_count: Int = native_list_len(pairs) let result: String = "{" let added: Int = 0 let i: Int = 0 while i < pair_count { let pair: String = str_trim(native_list_get(pairs, i)) // Each pair looks like "key":"value" // Extract the key portion let colon_pos: Int = str_index_of(pair, ":") if colon_pos > 0 { let raw_k: String = str_trim(str_slice(pair, 0, colon_pos)) // raw_k is "key" — strip quotes let k_len: Int = str_len(raw_k) let k_unquoted: String = raw_k if str_starts_with(raw_k, "\"") { let k_unquoted = str_slice(raw_k, 1, k_len - 1) } if !str_eq(k_unquoted, key) { if added == 0 { let result = result + pair } else { let result = result + "," + pair } let added = added + 1 } } let i = i + 1 } return result + "}" } // json_append_key appends an already-escaped key/value pair to a JSON object. fn json_append_key(obj: String, esc_key: String, esc_val: String) -> String { let pair: String = "\"" + esc_key + "\":\"" + esc_val + "\"" let inner: String = str_trim(obj) let inner_len: Int = str_len(inner) if inner_len < 2 { return "{" + pair + "}" } // Remove trailing } let body: String = str_slice(inner, 0, inner_len - 1) let body_trimmed: String = str_trim(body) // Remove leading { let body2: String = str_slice(body_trimmed, 1, str_len(body_trimmed)) let body2_trimmed: String = str_trim(body2) if str_eq(body2_trimmed, "") { return "{" + pair + "}" } return "{" + body2_trimmed + "," + pair + "}" } // record_installed updates installed.json to record name -> version. fn record_installed(name: String, version: String) -> Void { let mk_ret: Int = exec_command("mkdir -p " + packages_dir()) let installed: String = read_installed() let updated: String = json_set_key(installed, name, version) write_installed(updated) } // ── List helpers ────────────────────────────────────────────────────────────── // 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 the registry and // walks its dependency list depth-first, building a topological order. // // Parameters: // name — vessel name to resolve // version — version string (may be "" for latest) // 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 { 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 the registry 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":..., "description":..., "download_url":...} let actual_version: String = json_get_string(content, "version") let actual_key: String = name + ":" + actual_version // registry_find returns top-level metadata only; deps are not included in // the Gitea-backed registry response (they live in the release body comment). // For now we skip transitive dep resolution — install only direct deps. // Append this vessel (after all its deps would go) 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 downloads and extracts a vessel to ~/.el/packages///. // Returns true on success. fn install_vessel(name: String, version: String) -> Bool { // Resolve version if not specified let ver: String = version if str_eq(ver, "") { let ver = registry_latest_version(name) if str_eq(ver, "") { println("epm: error: no releases found for vessel '" + name + "'") return false } } // Check if already installed let current: String = installed_version(name) if str_eq(current, ver) { println("epm: " + name + " " + ver + " is already installed") return true } // Fetch metadata from registry let content: String = registry_find(name, ver) if str_eq(content, "") { println("epm: error: cannot fetch '" + name + "' '" + ver + "' from registry") return false } let download_url: String = json_get_string(content, "download_url") if str_eq(download_url, "") { println("epm: error: no download URL for '" + name + "' '" + ver + "'") return false } // Create install directory let install_dir: String = packages_dir() + "/" + name + "/" + ver let mkdir_ret: Int = exec_command("mkdir -p " + install_dir) // Download tarball to /tmp let tmp: String = "/tmp/epm-" + name + "-" + ver + ".tar.gz" let token: String = registry_token() let dl_ret: Int = 0 if str_eq(token, "") { let dl_ret = exec_command("curl -sL -o " + tmp + " \"" + download_url + "\"") } else { let dl_ret = exec_command("curl -sL -H \"Authorization: token " + token + "\" -o " + tmp + " \"" + download_url + "\"") } if dl_ret != 0 { println("epm: error: download failed for '" + name + "' '" + ver + "' (exit " + native_int_to_str(dl_ret) + ")") return false } // Extract tarball let extract_ret: Int = exec_command("tar -xzf " + tmp + " -C " + install_dir + " --strip-components=1") if extract_ret != 0 { println("epm: error: extraction failed for '" + name + "' '" + ver + "' (exit " + native_int_to_str(extract_ret) + ")") let rm_ret: Int = exec_command("rm -f " + tmp) return false } // Remove temp file let rm_ret: Int = exec_command("rm -f " + tmp) // Record installation record_installed(name, ver) println("epm: installed " + name + " " + ver + " -> " + install_dir) 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 packages directory exists let mk2: Int = exec_command("mkdir -p " + packages_dir()) // 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 first 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") }