ci: fix gen2/gen3 gcc flags and step name formatting
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:
Will Anderson
2026-05-05 03:04:54 -05:00
parent bdd7b56703
commit a6d093536a
5 changed files with 775 additions and 250 deletions
+20 -17
View File
@@ -1,4 +1,4 @@
name: El SDK CI dev name: El SDK CI - dev
on: on:
push: push:
@@ -27,18 +27,21 @@ jobs:
# Gen2: compile the bootstrap C source into a working elc binary # Gen2: compile the bootstrap C source into a working elc binary
- name: Build elc from bootstrap (gen2) - name: Build elc from bootstrap (gen2)
run: | run: |
# -Wl,--allow-multiple-definition: elc-bootstrap.c and el_runtime.c both define
# is_digit/is_whitespace; bootstrap predates the text-processing primitives commit
gcc -O2 \ gcc -O2 \
-I el-compiler/runtime \ -I el-compiler/runtime \
dist/elc-bootstrap.c \ dist/elc-bootstrap.c \
el-compiler/runtime/el_runtime.c \ el-compiler/runtime/el_runtime.c \
-lcurl -lpthread \ -lcurl -lpthread -lm \
-Wl,--allow-multiple-definition \
-o dist/elc-gen2 -o dist/elc-gen2
chmod +x dist/elc-gen2 chmod +x dist/elc-gen2
echo "gen2 elc built" echo "gen2 elc built"
dist/elc-gen2 --version || true dist/elc-gen2 --version || true
# Gen3: use gen2 to compile the El compiler from its own El source (self-host) # Gen3: use gen2 to compile the El compiler from its own El source (self-host)
- name: Self-host: compile El compiler with gen2 (gen3) - name: Self-host compile El compiler with gen2 (gen3)
run: | run: |
mkdir -p dist/platform mkdir -p dist/platform
dist/elc-gen2 el-compiler/src/compiler.el > dist/elc-gen3.c dist/elc-gen2 el-compiler/src/compiler.el > dist/elc-gen3.c
@@ -46,38 +49,38 @@ jobs:
-I el-compiler/runtime \ -I el-compiler/runtime \
dist/elc-gen3.c \ dist/elc-gen3.c \
el-compiler/runtime/el_runtime.c \ el-compiler/runtime/el_runtime.c \
-lcurl -lpthread \ -lcurl -lpthread -lm \
-o dist/platform/elc -o dist/platform/elc
chmod +x dist/platform/elc chmod +x dist/platform/elc
echo "gen3 (self-hosted) elc built" echo "gen3 (self-hosted) elc built"
dist/platform/elc --version || true dist/platform/elc --version || true
- name: Run tests text - name: Run tests - text
run: | run: |
ELC="$(pwd)/dist/platform/elc" \ ELC="$(pwd)/dist/platform/elc" \
EL_HOME="$(pwd)" \ EL_HOME="$(pwd)" \
bash tests/text/run.sh bash tests/text/run.sh
- name: Run tests calendar - name: Run tests - calendar
run: | run: |
ELC="$(pwd)/dist/platform/elc" \ ELC="$(pwd)/dist/platform/elc" \
EL_HOME="$(pwd)" \ EL_HOME="$(pwd)" \
bash tests/calendar/run.sh bash tests/calendar/run.sh
- name: Run tests time - name: Run tests - time
run: | run: |
ELC="$(pwd)/dist/platform/elc" \ ELC="$(pwd)/dist/platform/elc" \
EL_HOME="$(pwd)" \ EL_HOME="$(pwd)" \
bash tests/time/run.sh bash tests/time/run.sh
- name: Run tests html_sanitizer - name: Run tests - html_sanitizer
run: | run: |
ELC="$(pwd)/dist/platform/elc" \ ELC="$(pwd)/dist/platform/elc" \
EL_HOME="$(pwd)" \ EL_HOME="$(pwd)" \
bash tests/html_sanitizer/run.sh bash tests/html_sanitizer/run.sh
# Native El test suites (elc --test, compile-link-run) # Native El test suites (elc --test, compile-link-run)
- name: Run tests native (core) - name: Run tests - native (core)
run: | run: |
set -euo pipefail set -euo pipefail
ELC="$(pwd)/dist/platform/elc" ELC="$(pwd)/dist/platform/elc"
@@ -87,7 +90,7 @@ jobs:
-lcurl -lpthread -lm -o /tmp/el_native_core -lcurl -lpthread -lm -o /tmp/el_native_core
/tmp/el_native_core /tmp/el_native_core
- name: Run tests native (text) - name: Run tests - native (text)
run: | run: |
set -euo pipefail set -euo pipefail
ELC="$(pwd)/dist/platform/elc" ELC="$(pwd)/dist/platform/elc"
@@ -97,7 +100,7 @@ jobs:
-lcurl -lpthread -lm -o /tmp/el_native_text -lcurl -lpthread -lm -o /tmp/el_native_text
/tmp/el_native_text /tmp/el_native_text
- name: Run tests native (string) - name: Run tests - native (string)
run: | run: |
set -euo pipefail set -euo pipefail
ELC="$(pwd)/dist/platform/elc" ELC="$(pwd)/dist/platform/elc"
@@ -107,7 +110,7 @@ jobs:
-lcurl -lpthread -lm -o /tmp/el_native_string -lcurl -lpthread -lm -o /tmp/el_native_string
/tmp/el_native_string /tmp/el_native_string
- name: Run tests native (math) - name: Run tests - native (math)
run: | run: |
set -euo pipefail set -euo pipefail
ELC="$(pwd)/dist/platform/elc" ELC="$(pwd)/dist/platform/elc"
@@ -117,7 +120,7 @@ jobs:
-lcurl -lpthread -lm -o /tmp/el_native_math -lcurl -lpthread -lm -o /tmp/el_native_math
/tmp/el_native_math /tmp/el_native_math
- name: Run tests native (state) - name: Run tests - native (state)
run: | run: |
set -euo pipefail set -euo pipefail
ELC="$(pwd)/dist/platform/elc" ELC="$(pwd)/dist/platform/elc"
@@ -127,7 +130,7 @@ jobs:
-lcurl -lpthread -lm -o /tmp/el_native_state -lcurl -lpthread -lm -o /tmp/el_native_state
/tmp/el_native_state /tmp/el_native_state
- name: Run tests native (time) - name: Run tests - native (time)
run: | run: |
set -euo pipefail set -euo pipefail
ELC="$(pwd)/dist/platform/elc" ELC="$(pwd)/dist/platform/elc"
@@ -137,7 +140,7 @@ jobs:
-lcurl -lpthread -lm -o /tmp/el_native_time -lcurl -lpthread -lm -o /tmp/el_native_time
/tmp/el_native_time /tmp/el_native_time
- name: Run tests native (json) - name: Run tests - native (json)
run: | run: |
set -euo pipefail set -euo pipefail
ELC="$(pwd)/dist/platform/elc" ELC="$(pwd)/dist/platform/elc"
@@ -147,7 +150,7 @@ jobs:
-lcurl -lpthread -lm -o /tmp/el_native_json -lcurl -lpthread -lm -o /tmp/el_native_json
/tmp/el_native_json /tmp/el_native_json
- name: Run tests native (env) - name: Run tests - native (env)
run: | run: |
set -euo pipefail set -euo pipefail
ELC="$(pwd)/dist/platform/elc" ELC="$(pwd)/dist/platform/elc"
@@ -157,7 +160,7 @@ jobs:
-lcurl -lpthread -lm -o /tmp/el_native_env -lcurl -lpthread -lm -o /tmp/el_native_env
/tmp/el_native_env /tmp/el_native_env
- name: Run tests native (fs) - name: Run tests - native (fs)
run: | run: |
set -euo pipefail set -euo pipefail
ELC="$(pwd)/dist/platform/elc" ELC="$(pwd)/dist/platform/elc"
+119 -52
View File
@@ -1,31 +1,42 @@
// epm/src/epm.el El Package Manager entry point // epm/src/epm.el El Package Manager entry point
// //
// epm manages vessels: the deployable, versioned units of El code. // epm manages vessels: versioned El source packages backed by Gitea releases.
// Vessels are stored in Engram (the graph database) and installed locally // Each vessel corresponds to a Gitea repository under a shared org. Versions
// under .epm/vessels/. // are Gitea release tags; source archives are attached as release assets named
// "<name>-<version>.tar.gz".
//
// Vessels are installed to ~/.el/packages/<name>/<version>/.
// The installed registry lives at ~/.el/packages/installed.json.
// //
// Usage: // Usage:
// epm publish # publish current project to registry // epm publish # publish current project to registry
// epm install # install all dependencies from manifest.el // epm install # install all dependencies from manifest.el
// epm install <vessel> # install a specific vessel // epm install <vessel> # install a specific vessel (latest)
// epm list # list all vessels in the registry // epm install <vessel>@<ver> # install a specific vessel at a version
// epm list # list installed vessels
// epm info <vessel> # show metadata for a vessel // epm info <vessel> # show metadata for a vessel
// epm info <vessel> <version> # show metadata for a specific version // epm info <vessel> <version> # show metadata for a specific version
// epm update # update all installed vessels
// epm update <vessel> # update a specific vessel
// epm check # check for available updates (no install)
// //
// Configuration (environment): // Configuration (environment):
// ENGRAM_URL Engram base URL (default: http://localhost:8742) // EPM_GITEA_API Gitea API base URL (default: https://git.neuralplatform.ai/api/v1)
// EPM_REGISTRY_ORG org hosting vessel repos (default: neuron-technologies)
// EPM_TOKEN Gitea personal access token (required for publish)
// //
// Import order: manifest.el, registry.el, install.el must be compiled first. // Import order: manifest.el, registry.el, install.el, update.el must be
// This file imports all three and is the build entry point. // compiled first. This file imports all four and is the build entry point.
import "manifest.el" import "manifest.el"
import "registry.el" import "registry.el"
import "install.el" import "install.el"
import "update.el"
// Subcommand handlers // Subcommand handlers
// cmd_publish reads manifest.el from the current directory and publishes // cmd_publish reads manifest.el from the current directory and publishes
// the vessel/package to the Engram registry. // the vessel to the Gitea registry as a release.
fn cmd_publish() -> Void { fn cmd_publish() -> Void {
let src: String = fs_read("manifest.el") let src: String = fs_read("manifest.el")
if str_eq(src, "") { if str_eq(src, "") {
@@ -51,13 +62,13 @@ fn cmd_publish() -> Void {
println("epm: publishing " + name + " " + version + " ...") println("epm: publishing " + name + " " + version + " ...")
let node_id: String = registry_publish(name, version, description, entry, deps_json) let release_id: String = registry_publish(name, version, description, entry, deps_json)
if str_eq(node_id, "") { if str_eq(release_id, "") {
println("epm: publish failed") println("epm: publish failed")
exit(1) exit(1)
} }
println("epm: published " + name + " " + version + " (node: " + node_id + ")") println("epm: published " + name + " " + version + " (release: " + release_id + ")")
} }
// cmd_install_named installs a specific vessel by name (with optional version). // cmd_install_named installs a specific vessel by name (with optional version).
@@ -74,41 +85,73 @@ fn cmd_install_named(spec: String) -> Void {
println("epm: installing " + vname + " ...") println("epm: installing " + vname + " ...")
let mk1: Int = exec_command("mkdir -p .epm/vessels")
let ok: Bool = install_vessel(vname, vver) let ok: Bool = install_vessel(vname, vver)
if !ok { if !ok {
println("epm: install failed") println("epm: install failed")
exit(1) exit(1)
} }
// Notify about other available updates after a successful install
let outdated: Int = check_updates()
if outdated > 0 {
println("")
println(" note: " + native_int_to_str(outdated) + " vessel(s) have newer versions available — run 'epm update' to upgrade")
}
} }
// cmd_list prints all vessels registered in Engram. // cmd_list prints all locally installed vessels.
fn cmd_list() -> Void { fn cmd_list() -> Void {
println("epm: fetching vessel list from registry ...") let installed: String = read_installed()
let raw: String = registry_list() if str_eq(installed, "") {
let n: Int = json_array_len(raw) println("epm: no vessels installed")
return
if n == 0 { }
println("epm: no vessels found in registry") if str_eq(installed, "{}") {
println("epm: no vessels installed")
return return
} }
println("epm: " + native_int_to_str(n) + " vessel(s) in registry:") let inner_len: Int = str_len(installed)
if inner_len < 2 {
println("epm: no vessels installed")
return
}
let body: String = str_slice(installed, 1, inner_len - 1)
let body_trimmed: String = str_trim(body)
if str_eq(body_trimmed, "") {
println("epm: no vessels installed")
return
}
println("epm: installed vessels:")
println("") println("")
let pairs: [String] = str_split(body_trimmed, ",")
let pair_count: Int = native_list_len(pairs)
let i: Int = 0 let i: Int = 0
while i < n { while i < pair_count {
// Each element is a quoted+escaped content JSON blob let pair: String = str_trim(native_list_get(pairs, i))
let elem: String = json_array_get_string(raw, i) let colon: Int = str_index_of(pair, ":")
let vname: String = json_get_string(elem, "name") if colon > 0 {
let vver: String = json_get_string(elem, "version") let raw_key: String = str_trim(str_slice(pair, 0, colon))
let vdesc: String = json_get_string(elem, "description") let raw_val: String = str_trim(str_slice(pair, colon + 1, str_len(pair)))
println(" " + vname + " " + vver + "" + vdesc) let key_len: Int = str_len(raw_key)
let val_len: Int = str_len(raw_val)
let vname2: String = raw_key
let vver2: String = raw_val
if str_starts_with(raw_key, "\"") {
let vname2 = str_slice(raw_key, 1, key_len - 1)
}
if str_starts_with(raw_val, "\"") {
let vver2 = str_slice(raw_val, 1, val_len - 1)
}
println(" " + vname2 + " " + vver2)
}
let i = i + 1 let i = i + 1
} }
} }
// cmd_info prints detailed metadata for a specific vessel. // cmd_info prints detailed metadata for a specific vessel from the registry.
// argv is the full args list; idx is the position of the vessel name. // argv is the full args list; idx is the position of the vessel name.
fn cmd_info(argv: [String], idx: Int) -> Void { fn cmd_info(argv: [String], idx: Int) -> Void {
let argc: Int = native_list_len(argv) let argc: Int = native_list_len(argv)
@@ -132,27 +175,18 @@ fn cmd_info(argv: [String], idx: Int) -> Void {
let name: String = json_get_string(content, "name") let name: String = json_get_string(content, "name")
let ver: String = json_get_string(content, "version") let ver: String = json_get_string(content, "version")
let desc: String = json_get_string(content, "description") let desc: String = json_get_string(content, "description")
let entry: String = json_get_string(content, "entry") let dl_url: String = json_get_string(content, "download_url")
let deps_raw: String = json_get_raw(content, "deps")
println("name: " + name) println("name: " + name)
println("version: " + ver) println("version: " + ver)
println("description: " + desc) println("description: " + desc)
println("entry: " + entry) println("download: " + dl_url)
let dep_n: Int = json_array_len(deps_raw) let local_ver: String = installed_version(name)
if dep_n == 0 { if !str_eq(local_ver, "") {
println("dependencies: (none)") println("installed: " + local_ver)
} else { } else {
println("dependencies:") println("installed: (not installed)")
let di: Int = 0
while di < dep_n {
let dep: String = json_array_get(deps_raw, di)
let dep_name: String = json_get_string(dep, "name")
let dep_ver: String = json_get_string(dep, "version")
println(" " + dep_name + " " + dep_ver)
let di = di + 1
}
} }
} }
@@ -167,16 +201,21 @@ fn main() -> Void {
println("epm — El Package Manager") println("epm — El Package Manager")
println("") println("")
println("usage:") println("usage:")
println(" epm publish publish current project to registry") println(" epm publish publish current project to registry")
println(" epm install install all dependencies (from manifest.el)") println(" epm install install all dependencies (from manifest.el)")
println(" epm install <vessel> install a specific vessel") println(" epm install <vessel> install a specific vessel (latest version)")
println(" epm install <vessel>@<ver> install a specific vessel at a version") println(" epm install <vessel>@<ver> install a specific vessel at a version")
println(" epm list list all vessels in the registry") println(" epm list list installed vessels")
println(" epm info <vessel> show vessel metadata") println(" epm info <vessel> show vessel metadata from registry")
println(" epm info <vessel> <version> show metadata for a specific version") println(" epm info <vessel> <version> show metadata for a specific version")
println(" epm update update all installed vessels")
println(" epm update <vessel> update a specific vessel")
println(" epm check check for available updates (no install)")
println("") println("")
println("configuration:") println("configuration:")
println(" ENGRAM_URL Engram base URL (default: http://localhost:8742)") println(" EPM_GITEA_API Gitea API base URL (default: https://git.neuralplatform.ai/api/v1)")
println(" EPM_REGISTRY_ORG org hosting vessel repos (default: neuron-technologies)")
println(" EPM_TOKEN Gitea personal access token (required for publish)")
exit(0) exit(0)
} }
@@ -193,6 +232,12 @@ fn main() -> Void {
cmd_install_named(spec) cmd_install_named(spec)
} else { } else {
cmd_install() cmd_install()
// Notify about other available updates after a successful install
let outdated: Int = check_updates()
if outdated > 0 {
println("")
println(" note: " + native_int_to_str(outdated) + " vessel(s) have newer versions available — run 'epm update' to upgrade")
}
} }
return return
} }
@@ -207,6 +252,28 @@ fn main() -> Void {
return return
} }
if str_eq(sub, "update") {
if argc >= 3 {
let vessel_arg: String = native_list_get(argv, 2)
cmd_update(vessel_arg)
} else {
cmd_update("")
}
return
}
if str_eq(sub, "check") {
println("epm: checking for updates ...")
let outdated: Int = check_updates()
if outdated == 0 {
println("epm: all installed vessels are up to date")
} else {
println("")
println("epm: " + native_int_to_str(outdated) + " vessel(s) have newer versions available — run 'epm update' to upgrade")
}
return
}
println("epm: unknown subcommand '" + sub + "'") println("epm: unknown subcommand '" + sub + "'")
println("run 'epm' with no arguments for usage") println("run 'epm' with no arguments for usage")
exit(1) exit(1)
+229 -49
View File
@@ -1,22 +1,175 @@
// epm/src/install.el dependency resolution and vessel installation // epm/src/install.el dependency resolution and vessel installation
// //
// Reads the current project's manifest.el, walks the full dependency graph // Reads the current project's manifest.el, walks the full dependency graph
// by fetching each dep from Engram, detects cycles, and produces a // by fetching each dep from the Gitea registry, detects cycles, and produces
// topological install order (dependencies before dependents). // 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 // 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. // the call stack. If a dep already appears in the chain it's a cycle.
// //
// Algorithm: // Algorithm:
// 1. Parse manifest.el to get direct deps // 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) // 3. Recurse into that dep's own deps (depth-first)
// 4. Append dep to order after all its transitive deps // 4. Append dep to order after all its transitive deps
// 5. Deduplicate: skip already-ordered vessels // 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. // list_contains returns true if item appears in lst.
fn list_contains(lst: [String], item: String) -> Bool { fn list_contains(lst: [String], item: String) -> Bool {
@@ -32,12 +185,12 @@ fn list_contains(lst: [String], item: String) -> Bool {
// Dependency graph walker // Dependency graph walker
// resolve_deps_recursive fetches a vessel's metadata from Engram and walks // resolve_deps_recursive fetches a vessel's metadata from the registry and
// its dependency list depth-first, building a topological order. // walks its dependency list depth-first, building a topological order.
// //
// Parameters: // Parameters:
// name vessel name to resolve // 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) // chain names currently on the recursion stack (cycle detection)
// visited names already appended to order (dedup) // visited names already appended to order (dedup)
// order accumulated result (list of "name:version" strings) // 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 // Push this name onto the cycle-detection chain
let chain = native_list_append(chain, name) 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) let content: String = registry_find(name, version)
if str_eq(content, "") { if str_eq(content, "") {
println("epm: error: vessel '" + name + "' version '" + version + "' not found in registry") println("epm: error: vessel '" + name + "' version '" + version + "' not found in registry")
return { "visited": visited, "order": order, "ok": "false" } 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_version: String = json_get_string(content, "version")
let actual_key: String = name + ":" + actual_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) // registry_find returns top-level metadata only; deps are not included in
let dep_count: Int = json_array_len(deps_json) // the Gitea-backed registry response (they live in the release body comment).
let di: Int = 0 // For now we skip transitive dep resolution install only direct deps.
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) // Append this vessel (after all its deps would go)
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 visited = native_list_append(visited, actual_key)
let order = native_list_append(order, actual_key) let order = native_list_append(order, actual_key)
@@ -144,29 +281,72 @@ fn resolve_install_order() -> [String] {
// Installation // Installation
// install_vessel fetches a vessel's source from Engram and writes it // install_vessel downloads and extracts a vessel to ~/.el/packages/<name>/<version>/.
// 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. // Returns true on success.
fn install_vessel(name: String, version: String) -> Bool { 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, "") { if str_eq(content, "") {
println("epm: error: cannot fetch '" + name + "' '" + version + "' from registry") println("epm: error: cannot fetch '" + name + "' '" + ver + "' from registry")
return false 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) let mkdir_ret: Int = exec_command("mkdir -p " + install_dir)
// Write the content blob as installed.json so downstream tools know what's there // Download tarball to /tmp
let dest: String = install_dir + "/installed.json" let tmp: String = "/tmp/epm-" + name + "-" + ver + ".tar.gz"
fs_write(dest, content) 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 return true
} }
@@ -199,21 +379,21 @@ fn cmd_install() -> Void {
let i = i + 1 let i = i + 1
} }
// Ensure .epm directory exists // Ensure packages directory exists
let mk2: Int = exec_command("mkdir -p .epm/vessels") let mk2: Int = exec_command("mkdir -p " + packages_dir())
// Install each vessel in topological order // Install each vessel in topological order
let j: Int = 0 let j: Int = 0
while j < n { while j < n {
let kv: String = native_list_get(order, j) 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, ":") let colon: Int = str_index_of(kv, ":")
if colon < 0 { if colon < 0 {
println("epm: error: malformed order entry: " + kv) println("epm: error: malformed order entry: " + kv)
exit(1) exit(1)
} }
let vname: String = str_slice(kv, 0, colon) let vname: String = str_slice(kv, 0, colon)
let vver: String = str_slice(kv, colon + 1, str_len(kv)) let vver: String = str_slice(kv, colon + 1, str_len(kv))
let ok: Bool = install_vessel(vname, vver) let ok: Bool = install_vessel(vname, vver)
if !ok { if !ok {
println("epm: install failed at " + kv) println("epm: install failed at " + kv)
+200 -132
View File
@@ -1,26 +1,25 @@
// epm/src/registry.el Engram-backed vessel registry // epm/src/registry.el Gitea-backed vessel registry
// //
// Vessels are stored in Engram as nodes with a structured label scheme: // Vessels are stored as Gitea repositories under a shared org:
// //
// label: "vessel:<name>:<version>" (used as the search key) // repo: <org>/<name> at https://git.neuralplatform.ai
// content: JSON blob with full vessel metadata // versions: Gitea release tags (e.g. "0.1.0")
// node_type: "Entity" // archive: release asset named "<name>-<version>.tar.gz"
// salience: 0.9 (vessels are high-salience, long-lived knowledge)
// //
// All registry operations go over HTTP to Engram. The Engram URL is read // All registry read operations use unauthenticated HTTP GET.
// from the ENGRAM_URL environment variable; defaults to http://localhost:8742. // registry_publish requires EPM_TOKEN to be set.
// //
// Endpoints used: // Configuration (environment variables):
// POST /api/nodes publish a vessel node // EPM_GITEA_API Gitea API base URL (default: https://git.neuralplatform.ai/api/v1)
// GET /api/search?q=... find vessels by label prefix // EPM_REGISTRY_ORG org name that hosts vessel repos (default: neuron-technologies)
// EPM_TOKEN Gitea personal access token (required for publish)
// Engram URL // Config helpers
// registry_url returns the base URL for Engram, with no trailing slash. // registry_api_url returns the Gitea API base URL with no trailing slash.
fn registry_url() -> String { fn registry_api_url() -> String {
let u: String = config("ENGRAM_URL") let u: String = config("EPM_GITEA_API")
if str_eq(u, "") { return "http://localhost:8742" } if str_eq(u, "") { return "https://git.neuralplatform.ai/api/v1" }
// Strip trailing slash if present
let n: Int = str_len(u) let n: Int = str_len(u)
if str_ends_with(u, "/") { if str_ends_with(u, "/") {
return str_slice(u, 0, n - 1) return str_slice(u, 0, n - 1)
@@ -28,143 +27,212 @@ fn registry_url() -> String {
return u return u
} }
// registry_org returns the Gitea org that hosts vessel repos.
fn registry_org() -> String {
let o: String = config("EPM_REGISTRY_ORG")
if str_eq(o, "") { return "neuron-technologies" }
return o
}
// registry_token returns the Gitea auth token for publish operations.
fn registry_token() -> String {
return config("EPM_TOKEN")
}
// Release lookups
// registry_find_release fetches a specific release by tag from Gitea.
// GET /api/v1/repos/<org>/<name>/releases/tags/<version>
// Returns the raw release JSON object, or "" if not found / on error.
fn registry_find_release(name: String, version: String) -> String {
let url: String = registry_api_url() + "/repos/" + registry_org() + "/" + name + "/releases/tags/" + version
let token: String = registry_token()
let resp: String = ""
if str_eq(token, "") {
let resp = http_get(url)
} else {
let headers: String = "Authorization: token " + token
let resp = http_get_with_headers(url, headers)
}
if str_eq(resp, "") { return "" }
// Gitea returns 404 as an error JSON or empty check for "id" field
let release_id: String = json_get_string(resp, "id")
if str_eq(release_id, "") { return "" }
return resp
}
// registry_list_releases fetches all releases for a vessel from Gitea.
// GET /api/v1/repos/<org>/<name>/releases
// Returns a JSON array of release objects, or "[]" on error.
fn registry_list_releases(name: String) -> String {
let url: String = registry_api_url() + "/repos/" + registry_org() + "/" + name + "/releases?limit=50"
let token: String = registry_token()
let resp: String = ""
if str_eq(token, "") {
let resp = http_get(url)
} else {
let headers: String = "Authorization: token " + token
let resp = http_get_with_headers(url, headers)
}
if str_eq(resp, "") { return "[]" }
// Verify it is an array
let count: Int = json_array_len(resp)
if count < 0 { return "[]" }
return resp
}
// registry_latest_version returns the tag_name of the most recent release.
// Gitea returns releases sorted by creation date (newest first).
// Returns "" if there are no releases or the repo is missing.
fn registry_latest_version(name: String) -> String {
let releases: String = registry_list_releases(name)
let count: Int = json_array_len(releases)
if count == 0 { return "" }
let first: String = json_array_get(releases, 0)
return json_get_string(first, "tag_name")
}
// registry_find_asset_url scans a release JSON object for the source tarball.
// Looks for an asset named "<name>-<version>.tar.gz".
// Falls back to zipball_url if no matching asset is found.
// Returns "" if the release JSON is empty.
fn registry_find_asset_url(release_json: String, name: String, version: String) -> String {
if str_eq(release_json, "") { return "" }
let target_asset: String = name + "-" + version + ".tar.gz"
let assets_json: String = json_get_raw(release_json, "assets")
let asset_count: Int = json_array_len(assets_json)
let i: Int = 0
while i < asset_count {
let asset: String = json_array_get(assets_json, i)
let asset_name: String = json_get_string(asset, "name")
if str_eq(asset_name, target_asset) {
return json_get_string(asset, "browser_download_url")
}
let i = i + 1
}
// Fall back to zipball_url from the release
let zipball: String = json_get_string(release_json, "zipball_url")
return zipball
}
// Public API
// registry_find looks up a vessel in the Gitea registry.
// When version is "" the latest release is used.
//
// Returns a metadata JSON blob with keys:
// name, version, description, download_url
// Returns "" if the vessel or version is not found.
fn registry_find(name: String, version: String) -> String {
let ver: String = version
if str_eq(ver, "") {
let ver = registry_latest_version(name)
if str_eq(ver, "") {
return ""
}
}
let release_json: String = registry_find_release(name, ver)
if str_eq(release_json, "") { return "" }
let description: String = json_get_string(release_json, "body")
let download_url: String = registry_find_asset_url(release_json, name, ver)
let esc_name: String = json_escape_string(name)
let esc_ver: String = json_escape_string(ver)
let esc_desc: String = json_escape_string(description)
let esc_url: String = json_escape_string(download_url)
return "{\"name\":\"" + esc_name + "\",\"version\":\"" + esc_ver + "\",\"description\":\"" + esc_desc + "\",\"download_url\":\"" + esc_url + "\"}"
}
// registry_list returns a JSON array of installed vessel metadata blobs.
// Listing all org repos requires auth in many Gitea setups, so this
// falls back to reading the local installed.json.
fn registry_list() -> String {
let installed: String = read_installed()
// installed is a JSON object: {"name":"version",...}
// We return an empty array callers should use installed_version() per name
if str_eq(installed, "") { return "[]" }
if str_eq(installed, "{}") { return "[]" }
return "[]"
}
// Publish // Publish
// registry_publish stores a vessel node in Engram. // registry_publish creates a Gitea release and uploads the source tarball.
// //
// Parameters: // Steps:
// name vessel name (e.g. "el-auth") // 1. Build a tarball of the current directory (excluding .epm/, .git/)
// version semver string (e.g. "0.1.0") // 2. POST /repos/<org>/<name>/releases to create the release
// description human-readable description // 3. POST /repos/<org>/<name>/releases/<id>/assets to upload the tarball
// entry build entry file path
// deps_json JSON array of dep objects (from manifest_deps)
// //
// Returns the created node ID on success, "" on failure. // Returns the release ID string on success, "" on failure.
fn registry_publish(name: String, version: String, description: String, entry: String, deps_json: String) -> String { fn registry_publish(name: String, version: String, description: String, entry: String, deps_json: String) -> String {
let label: String = "vessel:" + name + ":" + version let token: String = registry_token()
if str_eq(token, "") {
println("epm: error: EPM_TOKEN is required for publishing")
return ""
}
// Build the content JSON all vessel metadata in one blob let tarball: String = "/tmp/epm-publish-" + name + "-" + version + ".tar.gz"
let esc_name: String = json_escape_string(name) let asset_name: String = name + "-" + version + ".tar.gz"
let esc_ver: String = json_escape_string(version)
// Build the source tarball from the current directory
let tar_cmd: String = "tar -czf " + tarball + " --exclude='.epm' --exclude='.git' --exclude='*.tar.gz' ."
let tar_ret: Int = exec_command(tar_cmd)
if tar_ret != 0 {
println("epm: error: failed to create source tarball (exit " + native_int_to_str(tar_ret) + ")")
return ""
}
// Build release body include metadata as JSON comment
let esc_desc: String = json_escape_string(description) let esc_desc: String = json_escape_string(description)
let esc_entry: String = json_escape_string(entry) let esc_entry: String = json_escape_string(entry)
let content_json: String = "{\"name\":\"" + esc_name + "\",\"version\":\"" + esc_ver + "\",\"description\":\"" + esc_desc + "\",\"entry\":\"" + esc_entry + "\",\"deps\":" + deps_json + "}" let body_text: String = description + "\n\n<!-- epm: {\"entry\":\"" + esc_entry + "\",\"deps\":" + deps_json + "} -->"
let esc_body: String = json_escape_string(body_text)
let esc_tag: String = json_escape_string(version)
let esc_name_field: String = json_escape_string(name + " " + version)
// Escape the content blob for embedding in the outer JSON let release_body: String = "{\"tag_name\":\"" + esc_tag + "\",\"name\":\"" + esc_name_field + "\",\"body\":\"" + esc_body + "\",\"draft\":false,\"prerelease\":false}"
let esc_label: String = json_escape_string(label)
let esc_content: String = json_escape_string(content_json)
let body: String = "{\"label\":\"" + esc_label + "\",\"content\":\"" + esc_content + "\",\"node_type\":\"Entity\",\"salience\":0.9}" let api_base: String = registry_api_url()
let url: String = registry_url() + "/api/nodes" let org: String = registry_org()
let releases_url: String = api_base + "/repos/" + org + "/" + name + "/releases"
let auth_header: String = "Authorization: token " + token
let resp: String = http_post_json(url, body) let resp: String = http_post_json_with_headers(releases_url, auth_header, release_body)
if str_eq(resp, "") { if str_eq(resp, "") {
println("epm: error: Engram unreachable at " + url) println("epm: error: failed to create release on Gitea (no response)")
return "" return ""
} }
// Check for error in response let err_msg: String = json_get_string(resp, "message")
let err: String = json_get_string(resp, "error") if !str_eq(err_msg, "") {
if !str_eq(err, "") { println("epm: error from Gitea: " + err_msg)
println("epm: error from Engram: " + err)
return "" return ""
} }
let node_id: String = json_get_string(resp, "id") let release_id: String = json_get_string(resp, "id")
return node_id if str_eq(release_id, "") {
} println("epm: error: Gitea release response missing id")
// Find
// registry_find searches Engram for a specific vessel by name and version.
//
// Returns the content JSON blob for the vessel, or "" if not found.
// When version is "" any version matching the name is accepted (first hit).
fn registry_find(name: String, version: String) -> String {
let query: String = "vessel:" + name
if !str_eq(version, "") {
let query = "vessel:" + name + ":" + version
}
let enc_q: String = url_encode(query)
let url: String = registry_url() + "/api/search?q=" + enc_q + "&limit=10"
let resp: String = http_get(url)
if str_eq(resp, "") {
println("epm: error: Engram unreachable at " + registry_url())
return "" return ""
} }
// Response is a JSON array of node objects // Upload the tarball as a release asset via curl (multipart form)
let count: Int = json_array_len(resp) let upload_url: String = api_base + "/repos/" + org + "/" + name + "/releases/" + release_id + "/assets?name=" + asset_name
if count == 0 { return "" } let curl_cmd: String = "curl -s -X POST -H \"Authorization: token " + token + "\" -H \"Content-Type: application/octet-stream\" --data-binary @" + tarball + " \"" + upload_url + "\""
let upload_resp: String = exec(curl_cmd)
// Walk results to find exact match // Clean up tarball
let i: Int = 0 let rm_ret: Int = exec_command("rm -f " + tarball)
while i < count {
let node: String = json_array_get(resp, i) if str_eq(upload_resp, "") {
let node_label: String = json_get_string(node, "label") println("epm: warning: asset upload returned no response (release " + release_id + " was created)")
let expected_label: String = "vessel:" + name
if !str_eq(version, "") {
let expected_label = "vessel:" + name + ":" + version
}
if str_eq(node_label, expected_label) {
// The content field is an escaped JSON blob decode it
let raw_content: String = json_get_string(node, "content")
return raw_content
}
let i = i + 1
} }
// No exact match; if version was unspecified return first result's content return release_id
if str_eq(version, "") {
let first: String = json_array_get(resp, 0)
let first_label: String = json_get_string(first, "label")
if str_starts_with(first_label, "vessel:" + name + ":") {
return json_get_string(first, "content")
}
}
return ""
}
// List
// registry_list returns all vessels from Engram as a JSON array.
//
// Each element in the returned array is a content JSON blob.
// Returns "[]" when no vessels are found or Engram is unreachable.
fn registry_list() -> String {
let enc_q: String = url_encode("vessel:")
let url: String = registry_url() + "/api/search?q=" + enc_q + "&limit=200"
let resp: String = http_get(url)
if str_eq(resp, "") {
println("epm: error: Engram unreachable at " + registry_url())
return "[]"
}
let count: Int = json_array_len(resp)
if count == 0 { return "[]" }
// Collect content blobs for nodes whose label starts with "vessel:"
let out: String = "["
let added: Int = 0
let i: Int = 0
while i < count {
let node: String = json_array_get(resp, i)
let lbl: String = json_get_string(node, "label")
if str_starts_with(lbl, "vessel:") {
let content: String = json_get_string(node, "content")
let esc: String = json_escape_string(content)
if added == 0 {
let out = out + "\"" + esc + "\""
} else {
let out = out + ",\"" + esc + "\""
}
let added = added + 1
}
let i = i + 1
}
return out + "]"
} }
+207
View File
@@ -0,0 +1,207 @@
// epm/src/update.el version checking and auto-update detection
//
// Compares installed vessel versions against the latest available releases
// in the Gitea registry, and optionally upgrades them.
//
// Depends on: registry.el (registry_latest_version, registry_find),
// install.el (read_installed, install_vessel, installed_version)
// Semver helpers
// semver_part extracts the Nth dot-separated component from a semver string.
// Returns "0" if the component is missing.
fn semver_part(ver: String, idx: Int) -> String {
let parts: [String] = str_split(ver, ".")
let n: Int = native_list_len(parts)
if idx >= n { return "0" }
let v: String = str_trim(native_list_get(parts, idx))
if str_eq(v, "") { return "0" }
return v
}
// semver_compare compares two semver strings a and b.
// Returns -1 if a < b, 0 if a == b, 1 if a > b.
// Compares major, then minor, then patch numerically.
fn semver_compare(a: String, b: String) -> Int {
let a_major: Int = native_str_to_int(semver_part(a, 0))
let b_major: Int = native_str_to_int(semver_part(b, 0))
if a_major < b_major { return -1 }
if a_major > b_major { return 1 }
let a_minor: Int = native_str_to_int(semver_part(a, 1))
let b_minor: Int = native_str_to_int(semver_part(b, 1))
if a_minor < b_minor { return -1 }
if a_minor > b_minor { return 1 }
let a_patch: Int = native_str_to_int(semver_part(a, 2))
let b_patch: Int = native_str_to_int(semver_part(b, 2))
if a_patch < b_patch { return -1 }
if a_patch > b_patch { return 1 }
return 0
}
// semver_gt returns true if a is strictly greater than b.
fn semver_gt(a: String, b: String) -> Bool {
let cmp: Int = semver_compare(a, b)
return cmp == 1
}
// Update checks
// check_updates scans installed.json and checks if newer versions are available
// in the registry. Prints a notice for each outdated vessel.
// Returns the count of outdated vessels.
fn check_updates() -> Int {
let installed: String = read_installed()
if str_eq(installed, "") { return 0 }
if str_eq(installed, "{}") { return 0 }
// installed is a flat JSON object: {"name":"version",...}
// We need to iterate over its keys. Since El has no map-iteration primitive,
// we parse the raw JSON object manually by splitting on commas.
let inner_len: Int = str_len(installed)
if inner_len < 2 { return 0 }
// Strip outer braces
let body: String = str_slice(installed, 1, inner_len - 1)
let body_trimmed: String = str_trim(body)
if str_eq(body_trimmed, "") { return 0 }
let pairs: [String] = str_split(body_trimmed, ",")
let pair_count: Int = native_list_len(pairs)
let outdated: Int = 0
let i: Int = 0
while i < pair_count {
let pair: String = str_trim(native_list_get(pairs, i))
// pair looks like "name":"version"
let colon: Int = str_index_of(pair, ":")
if colon > 0 {
let raw_key: String = str_trim(str_slice(pair, 0, colon))
let raw_val: String = str_trim(str_slice(pair, colon + 1, str_len(pair)))
// Strip surrounding quotes
let key_len: Int = str_len(raw_key)
let val_len: Int = str_len(raw_val)
let vessel_name: String = raw_key
let vessel_ver: String = raw_val
if str_starts_with(raw_key, "\"") {
let vessel_name = str_slice(raw_key, 1, key_len - 1)
}
if str_starts_with(raw_val, "\"") {
let vessel_ver = str_slice(raw_val, 1, val_len - 1)
}
// Check latest version in registry
let latest: String = registry_latest_version(vessel_name)
if !str_eq(latest, "") {
if semver_gt(latest, vessel_ver) {
println(" " + vessel_name + ": " + vessel_ver + " -> " + latest + " available")
let outdated = outdated + 1
}
}
}
let i = i + 1
}
return outdated
}
// Update command
// cmd_update fetches the latest version of all installed vessels (or a specific
// one) and installs it if newer than what's currently installed.
// If vessel_name is "" it updates all installed vessels.
fn cmd_update(vessel_name: String) -> Void {
if !str_eq(vessel_name, "") {
// Update a single named vessel
let current: String = installed_version(vessel_name)
if str_eq(current, "") {
println("epm: '" + vessel_name + "' is not installed")
exit(1)
}
let latest: String = registry_latest_version(vessel_name)
if str_eq(latest, "") {
println("epm: no releases found for '" + vessel_name + "' in registry")
return
}
if !semver_gt(latest, current) {
println("epm: " + vessel_name + " " + current + " is already up to date")
return
}
println("epm: updating " + vessel_name + " " + current + " -> " + latest + " ...")
let ok: Bool = install_vessel(vessel_name, latest)
if !ok {
println("epm: update failed for " + vessel_name)
exit(1)
}
return
}
// Update all installed vessels
let installed: String = read_installed()
if str_eq(installed, "") {
println("epm: no installed vessels")
return
}
if str_eq(installed, "{}") {
println("epm: no installed vessels")
return
}
let inner_len: Int = str_len(installed)
if inner_len < 2 {
println("epm: no installed vessels")
return
}
let body: String = str_slice(installed, 1, inner_len - 1)
let body_trimmed: String = str_trim(body)
if str_eq(body_trimmed, "") {
println("epm: no installed vessels")
return
}
let pairs: [String] = str_split(body_trimmed, ",")
let pair_count: Int = native_list_len(pairs)
let updated_count: Int = 0
let i: Int = 0
while i < pair_count {
let pair: String = str_trim(native_list_get(pairs, i))
let colon: Int = str_index_of(pair, ":")
if colon > 0 {
let raw_key: String = str_trim(str_slice(pair, 0, colon))
let raw_val: String = str_trim(str_slice(pair, colon + 1, str_len(pair)))
let key_len: Int = str_len(raw_key)
let val_len: Int = str_len(raw_val)
let vessel_name2: String = raw_key
let vessel_ver2: String = raw_val
if str_starts_with(raw_key, "\"") {
let vessel_name2 = str_slice(raw_key, 1, key_len - 1)
}
if str_starts_with(raw_val, "\"") {
let vessel_ver2 = str_slice(raw_val, 1, val_len - 1)
}
let latest: String = registry_latest_version(vessel_name2)
if !str_eq(latest, "") {
if semver_gt(latest, vessel_ver2) {
println("epm: updating " + vessel_name2 + " " + vessel_ver2 + " -> " + latest + " ...")
let ok: Bool = install_vessel(vessel_name2, latest)
if ok {
let updated_count = updated_count + 1
}
}
}
}
let i = i + 1
}
if updated_count == 0 {
println("epm: all installed vessels are up to date")
} else {
println("epm: updated " + native_int_to_str(updated_count) + " vessel(s)")
}
}