ci: fix gen2/gen3 gcc flags and step name formatting
El SDK CI - dev / build-and-test (pull_request) Failing after 7s
El SDK CI - dev / build-and-test (pull_request) Failing after 7s
- add -lm (el_runtime.c uses pow/sqrt/log/sin/cos/exp) - add -Wl,--allow-multiple-definition to gen2 (is_digit/is_whitespace defined in both elc-bootstrap.c and el_runtime.c; bootstrap predates the text-processing primitives commit) - remove colon from Self-host step name (Gitea YAML parser rejects it) - replace em dashes in step names with hyphens
This commit is contained in:
+229
-49
@@ -1,22 +1,175 @@
|
||||
// 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).
|
||||
// by fetching each dep from the Gitea registry, detects cycles, and produces
|
||||
// a topological install order (dependencies before dependents).
|
||||
//
|
||||
// Installed vessels land in: .epm/vessels/<name>/
|
||||
// Installed vessels land in: ~/.el/packages/<name>/<version>/
|
||||
// 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 manifest from Engram
|
||||
// 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
|
||||
|
||||
// ── List helpers (operating on String lists) ──────────────────────────────────
|
||||
// ── 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 {
|
||||
@@ -32,12 +185,12 @@ fn list_contains(lst: [String], item: String) -> Bool {
|
||||
|
||||
// ── Dependency graph walker ───────────────────────────────────────────────────
|
||||
|
||||
// resolve_deps_recursive fetches a vessel's metadata from Engram and walks
|
||||
// its dependency list depth-first, building a topological order.
|
||||
// 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 any)
|
||||
// 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)
|
||||
@@ -60,38 +213,22 @@ fn resolve_deps_recursive(name: String, version: String, chain: [String], visite
|
||||
// Push this name onto the cycle-detection chain
|
||||
let chain = native_list_append(chain, name)
|
||||
|
||||
// Fetch vessel metadata from Engram
|
||||
// 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":..., "deps":[...]}
|
||||
// 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
|
||||
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")
|
||||
// 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.
|
||||
|
||||
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)
|
||||
// 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)
|
||||
|
||||
@@ -144,29 +281,72 @@ fn resolve_install_order() -> [String] {
|
||||
|
||||
// ── 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.
|
||||
//
|
||||
// install_vessel downloads and extracts a vessel to ~/.el/packages/<name>/<version>/.
|
||||
// Returns true on success.
|
||||
fn install_vessel(name: String, version: String) -> Bool {
|
||||
let content: String = registry_find(name, version)
|
||||
// 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 + "' '" + version + "' from registry")
|
||||
println("epm: error: cannot fetch '" + name + "' '" + ver + "' from registry")
|
||||
return false
|
||||
}
|
||||
|
||||
let install_dir: String = ".epm/vessels/" + name
|
||||
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)
|
||||
|
||||
// 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)
|
||||
// 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
|
||||
}
|
||||
|
||||
println("epm: installed " + name + " " + version + " -> " + dest)
|
||||
// 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
|
||||
}
|
||||
|
||||
@@ -199,21 +379,21 @@ fn cmd_install() -> Void {
|
||||
let i = i + 1
|
||||
}
|
||||
|
||||
// Ensure .epm directory exists
|
||||
let mk2: Int = exec_command("mkdir -p .epm/vessels")
|
||||
// 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 last colon
|
||||
// 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 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)
|
||||
|
||||
Reference in New Issue
Block a user