// 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) // ── Cross-module forward declarations ───────────────────────────────────────── // Defined in sibling epm modules; resolved at link time. The `extern fn` decls // give elc the C prototypes so generated update.c compiles cleanly under strict // compilers (gcc>=14 / clang) that reject implicit function declarations. extern fn read_installed() -> String // install.el extern fn installed_version(name: String) -> String // install.el extern fn install_vessel(name: String, version: String) -> Bool // install.el extern fn registry_latest_version(name: String) -> String // registry.el // ── 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)") } }