From f0c2486a5c0c985625059f8583d8fc071602bd25 Mon Sep 17 00:00:00 2001 From: John Alanbrook Date: Fri, 20 Feb 2026 13:39:26 -0600 Subject: [PATCH 1/2] better path resolution --- add.ce | 27 ++----------- fetch.ce | 20 ++++++++-- install.ce | 37 +++++++----------- internal/engine.cm | 55 +++++++++++++++++---------- internal/os.c | 6 ++- internal/shop.cm | 94 +++++++++++++++++++++++++++++++++++++++++++--- package.cm | 22 +++++++++++ source/runtime.c | 5 +++ 8 files changed, 188 insertions(+), 78 deletions(-) diff --git a/add.ce b/add.ce index 28010596..3749242c 100644 --- a/add.ce +++ b/add.ce @@ -78,29 +78,6 @@ if (!fd.is_file(cwd + '/cell.toml')) { $stop() } -// Recursively find all cell packages in a directory -function find_packages(dir) { - var found = [] - var list = fd.readdir(dir) - if (!list) return found - if (fd.is_file(dir + '/cell.toml')) { - push(found, dir) - } - arrfor(list, function(item) { - if (item == '.' || item == '..' || item == '.cell' || item == '.git') return - var full = dir + '/' + item - var st = fd.stat(full) - var sub = null - if (st && st.isDirectory) { - sub = find_packages(full) - arrfor(sub, function(p) { - push(found, p) - }) - } - }) - return found -} - // If -r flag, find all packages recursively and add each if (recursive) { if (!locator) { @@ -111,7 +88,9 @@ if (recursive) { log.error(`${locator} is not a directory`) $stop() } - locators = find_packages(resolved) + locators = filter(pkg.find_packages(resolved), function(p) { + return p != cwd + }) if (length(locators) == 0) { log.console("No packages found in " + resolved) $stop() diff --git a/fetch.ce b/fetch.ce index a1cd1a35..cc526740 100644 --- a/fetch.ce +++ b/fetch.ce @@ -33,12 +33,26 @@ for (i = 0; i < length(args); i++) { var all_packages = shop.list_packages() var lock = shop.load_lock() var packages_to_fetch = [] +var _update = null if (target_pkg) { - // Fetch specific package + // Fetch specific package - auto-update if not in lock if (find(all_packages, target_pkg) == null) { - log.error("Package not found: " + target_pkg) - $stop() + log.console("Package not in lock, updating: " + target_pkg) + _update = function() { + shop.update(target_pkg) + } disruption { + log.error("Could not update package: " + target_pkg) + $stop() + } + _update() + // Reload after update + all_packages = shop.list_packages() + lock = shop.load_lock() + if (find(all_packages, target_pkg) == null) { + log.error("Package not found: " + target_pkg) + $stop() + } } push(packages_to_fetch, target_pkg) } else { diff --git a/install.ce b/install.ce index 05fb3df5..c5f2c818 100644 --- a/install.ce +++ b/install.ce @@ -79,28 +79,7 @@ if (locator && (locator == '.' || starts_with(locator, './') || starts_with(loca } } -// Recursively find all cell packages in a directory -function find_packages(dir) { - var found = [] - var list = fd.readdir(dir) - if (!list) return found - if (fd.is_file(dir + '/cell.toml')) { - push(found, dir) - } - arrfor(list, function(item) { - if (item == '.' || item == '..' || item == '.cell' || item == '.git') return - var full = dir + '/' + item - var st = fd.stat(full) - var sub = null - if (st && st.isDirectory) { - sub = find_packages(full) - arrfor(sub, function(p) { - push(found, p) - }) - } - }) - return found -} +var cwd = fd.realpath('.') // If -r flag, find all packages recursively and install each if (recursive) { @@ -112,7 +91,9 @@ if (recursive) { log.error(`${locator} is not a directory`) $stop() } - locators = find_packages(resolved) + locators = filter(pkg.find_packages(resolved), function(p) { + return p != cwd + }) if (length(locators) == 0) { log.console("No packages found in " + resolved) $stop() @@ -133,6 +114,16 @@ var visited = {} // Recursive mode: install all found packages and exit if (recursive) { + if (dry_run) { + log.console("Would install:") + arrfor(locators, function(loc) { + var lock = shop.load_lock() + var exists = lock[loc] != null + log.console(" " + loc + (exists ? " (already installed)" : "")) + }) + $stop() + } + arrfor(locators, function(loc) { log.console(" Installing " + loc + "...") var _inst = function() { diff --git a/internal/engine.cm b/internal/engine.cm index 81815b53..7c279d3c 100644 --- a/internal/engine.cm +++ b/internal/engine.cm @@ -1204,23 +1204,23 @@ if (ends_with(prog, '.ce')) prog = text(prog, 0, -3) var package = use_core('package') -// Find the .ce file -var prog_path = prog + ".ce" -var pkg_dir = null -var core_dir = null -if (!fd.is_file(prog_path)) { - pkg_dir = package.find_package_dir(".") - if (pkg_dir) - prog_path = pkg_dir + '/' + prog + '.ce' -} -if (!fd.is_file(prog_path)) { - // Check core packages - core_dir = core_path - prog_path = core_dir + '/' + prog + '.ce' -} -if (!fd.is_file(prog_path)) { - os.print(`Main program ${prog} could not be found\n`) - os.exit(1) +// Find the .ce file using unified resolver +var cwd_package = package.find_package_dir(".") +var prog_info = shop.resolve_program ? shop.resolve_program(prog, cwd_package) : null +var prog_path = null +if (prog_info) { + prog_path = prog_info.path +} else { + // Fallback: check CWD, package dir, and core + prog_path = prog + ".ce" + if (!fd.is_file(prog_path) && cwd_package) + prog_path = cwd_package + '/' + prog + '.ce' + if (!fd.is_file(prog_path)) + prog_path = core_path + '/' + prog + '.ce' + if (!fd.is_file(prog_path)) { + os.print(`Main program ${prog} could not be found\n`) + os.exit(1) + } } $_.clock(_ => { @@ -1244,18 +1244,33 @@ $_.clock(_ => { var pkg = file_info ? file_info.package : null - // Verify all transitive dependency packages are present + // Verify all transitive dependency packages are present, auto-install if missing var _deps = null var _di = 0 var _dep_dir = null + var _auto_install = null if (pkg) { _deps = package.gather_dependencies(pkg) _di = 0 while (_di < length(_deps)) { _dep_dir = package.get_dir(_deps[_di]) if (!fd.is_dir(_dep_dir)) { - log.error('missing dependency package: ' + _deps[_di]) - disrupt + log.console('installing missing dependency: ' + _deps[_di]) + _auto_install = function() { + shop.update(_deps[_di]) + shop.fetch(_deps[_di]) + shop.extract(_deps[_di]) + shop.build_package_scripts(_deps[_di]) + } disruption { + log.error('failed to install dependency: ' + _deps[_di]) + disrupt + } + _auto_install() + _dep_dir = package.get_dir(_deps[_di]) + if (!fd.is_dir(_dep_dir)) { + log.error('missing dependency package: ' + _deps[_di]) + disrupt + } } _di = _di + 1 } diff --git a/internal/os.c b/internal/os.c index 3638c059..4d3c542b 100644 --- a/internal/os.c +++ b/internal/os.c @@ -318,7 +318,9 @@ JSC_SCALL(os_system, ) JSC_CCALL(os_exit, - exit(0); + int code = 0; + if (argc > 0) JS_ToInt32(js, &code, argv[0]); + exit(code); ) static JSValue js_os_dylib_open(JSContext *js, JSValue self, int argc, JSValue *argv) @@ -714,7 +716,7 @@ static const JSCFunctionListEntry js_os_funcs[] = { MIST_FUNC_DEF(os, rusage, 0), MIST_FUNC_DEF(os, mallinfo, 0), MIST_FUNC_DEF(os, system, 1), - MIST_FUNC_DEF(os, exit, 0), + MIST_FUNC_DEF(os, exit, 1), MIST_FUNC_DEF(os, sleep, 1), MIST_FUNC_DEF(os, dylib_open, 1), MIST_FUNC_DEF(os, dylib_preload, 1), diff --git a/internal/shop.cm b/internal/shop.cm index 2d549f85..c9d6b657 100644 --- a/internal/shop.cm +++ b/internal/shop.cm @@ -778,6 +778,11 @@ function resolve_path(path, ctx) var ctx_path = null var alias = null var package_path = null + var lock = null + var best_pkg = null + var best_remainder = null + var shop_dir = null + var shop_file = null if (explicit) { if (is_internal_path(explicit.path) && ctx && explicit.package != ctx) @@ -823,6 +828,25 @@ function resolve_path(path, ctx) if (fd.is_file(package_path)) return {path: package_path, scope: SCOPE_PACKAGE, pkg: ctx} + // Shop package scanning: longest prefix match against lock.toml entries + lock = Shop.load_lock() + best_pkg = null + best_remainder = null + arrfor(array(lock), function(pkg_name) { + if (starts_with(path, pkg_name + '/')) { + if (!best_pkg || length(pkg_name) > length(best_pkg)) { + best_pkg = pkg_name + best_remainder = text(path, length(pkg_name) + 1) + } + } + }) + if (best_pkg && best_remainder) { + shop_dir = get_packages_dir() + '/' + safe_package_path(best_pkg) + shop_file = shop_dir + '/' + best_remainder + if (fd.is_file(shop_file)) + return {path: shop_file, scope: SCOPE_PACKAGE, pkg: best_pkg} + } + core_dir = Shop.get_core_dir() core_file_path = core_dir + '/' + path if (fd.is_file(core_file_path)) @@ -1298,6 +1322,64 @@ Shop.resolve_use_path = function(path, ctx) { return info.path } +// Resolve a program (.ce) path using the unified resolver. +// Returns {path, scope, pkg} or null. +// If the path looks like a remote package locator and is not found locally, +// attempts to auto-fetch and install it. +Shop.resolve_program = function(prog, package_context) { + var info = resolve_path(prog + '.ce', package_context) + if (info) return info + + // Auto-install: if the path matches a recognized remote locator, try fetching + var lock = Shop.load_lock() + var best_pkg = null + var best_remainder = null + var pkg_info = null + var parts = array(prog, '/') + var i = 0 + var candidate = null + var _auto = null + arrfor(array(lock), function(pkg_name) { + if (starts_with(prog, pkg_name + '/')) { + if (!best_pkg || length(pkg_name) > length(best_pkg)) { + best_pkg = pkg_name + best_remainder = text(prog, length(pkg_name) + 1) + } + } + }) + + // If not in lock, check if this looks like a fetchable package + if (!best_pkg) { + for (i = length(parts) - 1; i >= 1; i--) { + candidate = text(array(parts, 0, i), '/') + pkg_info = Shop.resolve_package_info(candidate) + if (pkg_info && pkg_info != 'local') { + best_pkg = candidate + best_remainder = text(array(parts, i), '/') + break + } + } + } + + if (best_pkg && best_remainder) { + log.console('fetching ' + best_pkg + '...') + _auto = function() { + Shop.update(best_pkg) + Shop.fetch(best_pkg) + Shop.extract(best_pkg) + Shop.build_package_scripts(best_pkg) + } disruption { + return null + } + _auto() + // Retry resolution + info = resolve_path(prog + '.ce', package_context) + if (info) return info + } + + return null +} + // Resolve a use() module path to {resolved_path, package, type} without compiling. // type is 'script', 'native', or null. Checks .cm files, C symbols, and aliases. Shop.resolve_import_info = function(path, ctx) { @@ -1410,7 +1492,7 @@ Shop.fetch = function(pkg) { if (actual_hash == expected_hash) { return { status: 'cached' } } - log.console("Zip hash mismatch for " + pkg + ", re-fetching...") + log.shop("Zip hash mismatch for " + pkg + ", re-fetching...") } else { // No hash stored yet - compute and store it actual_hash = text(crypto.blake2(zip_blob), 'h') @@ -1522,13 +1604,13 @@ Shop.update = function(pkg) { var lock_entry = lock[pkg] var info = Shop.resolve_package_info(pkg) - log.console(`checking ${pkg}`) + log.shop(`checking ${pkg}`) var new_entry = null if (info == 'local') { // Check if local path exists if (!fd.is_dir(pkg)) { - log.console(` Local path does not exist: ${pkg}`) + log.shop(` Local path does not exist: ${pkg}`) return null } // Local packages always get a lock entry @@ -1544,8 +1626,8 @@ Shop.update = function(pkg) { var local_commit = lock_entry ? lock_entry.commit : null var remote_commit = fetch_remote_hash(pkg) - log.console(`local commit: ${local_commit}`) - log.console(`remote commit: ${remote_commit}`) + log.shop(`local commit: ${local_commit}`) + log.shop(`remote commit: ${remote_commit}`) if (!remote_commit) { log.error("Could not resolve commit for " + pkg) @@ -1574,7 +1656,7 @@ function install_zip(zip_blob, target_dir) { if (fd.is_link(target_dir)) fd.unlink(target_dir) if (fd.is_dir(target_dir)) fd.rmdir(target_dir, 1) - log.console("Extracting to " + target_dir) + log.shop("Extracting to " + target_dir) ensure_dir(target_dir) var count = zip.count() diff --git a/package.cm b/package.cm index 506006c4..1c21ffc8 100644 --- a/package.cm +++ b/package.cm @@ -202,6 +202,28 @@ package.gather_dependencies = function(name) return array(all_deps) } +// Recursively find all cell packages (dirs with cell.toml) under a directory +package.find_packages = function(dir) { + var found = [] + var list = fd.readdir(dir) + if (!list) return found + if (fd.is_file(dir + '/cell.toml')) + push(found, dir) + arrfor(list, function(item) { + if (item == '.' || item == '..' || item == '.cell' || item == '.git') return + var full = dir + '/' + item + var st = fd.stat(full) + var sub = null + if (st && st.isDirectory) { + sub = package.find_packages(full) + arrfor(sub, function(p) { + push(found, p) + }) + } + }) + return found +} + package.list_files = function(pkg) { var dir = get_path(pkg) if (!fd.is_dir(dir)) return [] diff --git a/source/runtime.c b/source/runtime.c index 920d0754..41fbba95 100644 --- a/source/runtime.c +++ b/source/runtime.c @@ -8057,6 +8057,11 @@ static JSValue js_cell_text_format (JSContext *ctx, JSValue this_val, int argc, } } + if (!made_substitution && JS_IsNull (cv_ref.val)) { + substitution = JS_NewString (ctx, "null"); + made_substitution = 1; + } + if (!made_substitution && !JS_IsNull (cv_ref.val)) { JSValue conv_text_val = JS_ToString (ctx, cv_ref.val); if (JS_IsText (conv_text_val)) { From 601a78b3c7570a6186b643847cdad7a96c3a39cb Mon Sep 17 00:00:00 2001 From: John Alanbrook Date: Fri, 20 Feb 2026 14:10:24 -0600 Subject: [PATCH 2/2] package resolution --- internal/engine.cm | 31 +++++++++++++--- internal/shop.cm | 90 +++++++++++++++++++++++++++++++++++++++------- 2 files changed, 104 insertions(+), 17 deletions(-) diff --git a/internal/engine.cm b/internal/engine.cm index 7c279d3c..853ea881 100644 --- a/internal/engine.cm +++ b/internal/engine.cm @@ -1224,7 +1224,18 @@ if (prog_info) { } $_.clock(_ => { - var file_info = shop.file_info ? shop.file_info(prog_path) : null + var _file_info_ok = false + var file_info = null + var _try_fi = function() { + file_info = shop.file_info ? shop.file_info(prog_path) : null + _file_info_ok = true + } disruption {} + _try_fi() + if (!_file_info_ok || !file_info) + file_info = {path: prog_path, is_module: false, is_actor: true, package: null, name: prog} + // If the unified resolver found the package, use that as the authoritative source + if (prog_info && prog_info.pkg) + file_info.package = prog_info.pkg var inject = shop.script_inject_for ? shop.script_inject_for(file_info) : [] // Build env with runtime functions + capability injections @@ -1278,10 +1289,22 @@ $_.clock(_ => { env.use = function(path) { var ck = 'core/' + path + var _use_core_result = null + var _use_core_ok = false if (use_cache[ck]) return use_cache[ck] - var core_mod = use_core(path) - if (core_mod) return core_mod - return shop.use(path, pkg) + var _try_core = function() { + _use_core_result = use_core(path) + _use_core_ok = true + } disruption {} + _try_core() + if (_use_core_ok && _use_core_result) return _use_core_result + var _shop_use = function() { + return shop.use(path, pkg) + } disruption { + log.error(`use('${path}') failed (package: ${pkg})`) + disrupt + } + return _shop_use() } env.args = _cell.args.arg env.log = log diff --git a/internal/shop.cm b/internal/shop.cm index c9d6b657..ceea81ea 100644 --- a/internal/shop.cm +++ b/internal/shop.cm @@ -137,9 +137,6 @@ function split_explicit_package_import(path) if (package_in_shop(pkg_candidate)) return {package: pkg_candidate, path: mod_path} - - if (Shop.resolve_package_info(pkg_candidate)) - return {package: pkg_candidate, path: mod_path} } return null @@ -158,6 +155,8 @@ function abs_path_to_package(package_dir) } var packages_prefix = get_packages_dir() + '/' + var packages_prefix_abs = fd.realpath(get_packages_dir()) + if (packages_prefix_abs) packages_prefix_abs = packages_prefix_abs + '/' var core_dir = packages_prefix + core_package // Check if this is the core package directory (or its symlink target) @@ -176,6 +175,10 @@ function abs_path_to_package(package_dir) if (starts_with(package_dir, packages_prefix)) return text(package_dir, length(packages_prefix)) + // Also try absolute path comparison (package_dir may be absolute, packages_prefix relative) + if (packages_prefix_abs && starts_with(package_dir, packages_prefix_abs)) + return text(package_dir, length(packages_prefix_abs)) + // Check if this local path is the target of a link // If so, return the canonical package name (link origin) instead var link_origin = link.get_origin(package_dir) @@ -344,9 +347,11 @@ function get_policy() { // Get information about how to resolve a package // Local packages always start with / +// Remote packages must be exactly host/owner/repo (3 components) Shop.resolve_package_info = function(pkg) { if (starts_with(pkg, '/')) return 'local' - if (search(pkg, 'gitea') != null) return 'gitea' + var parts = array(pkg, '/') + if (length(parts) == 3 && search(parts[0], 'gitea') != null) return 'gitea' return null } @@ -1349,25 +1354,78 @@ Shop.resolve_program = function(prog, package_context) { }) // If not in lock, check if this looks like a fetchable package - if (!best_pkg) { - for (i = length(parts) - 1; i >= 1; i--) { - candidate = text(array(parts, 0, i), '/') - pkg_info = Shop.resolve_package_info(candidate) - if (pkg_info && pkg_info != 'local') { - best_pkg = candidate - best_remainder = text(array(parts, i), '/') - break - } + // For gitea-style URLs, the package root is host/owner/repo (3 components) + if (!best_pkg && length(parts) > 3) { + candidate = text(array(parts, 0, 3), '/') + pkg_info = Shop.resolve_package_info(candidate) + if (pkg_info && pkg_info != 'local') { + best_pkg = candidate + best_remainder = text(array(parts, 3), '/') } } if (best_pkg && best_remainder) { log.console('fetching ' + best_pkg + '...') _auto = function() { + // Install the package itself first Shop.update(best_pkg) Shop.fetch(best_pkg) Shop.extract(best_pkg) + // Install dependencies iteratively (each dep must be extracted before reading its deps) + var all_deps = {} + var queue = [best_pkg] + var qi = 0 + var current = null + var direct_deps = null + var dep_locator = null + var dep_dir = null + var build_mod = null + var target = null + var _build_c = null + var _read_deps = null + while (qi < length(queue)) { + current = queue[qi] + qi = qi + 1 + _read_deps = function() { + direct_deps = pkg_tools.dependencies(current) + } disruption { + direct_deps = null + } + _read_deps() + if (direct_deps) { + arrfor(array(direct_deps), function(alias) { + dep_locator = direct_deps[alias] + if (!all_deps[dep_locator]) { + all_deps[dep_locator] = true + dep_dir = pkg_tools.get_dir(dep_locator) + if (!fd.is_dir(dep_dir)) { + log.console(' installing dependency: ' + dep_locator) + Shop.update(dep_locator) + Shop.fetch(dep_locator) + Shop.extract(dep_locator) + } + push(queue, dep_locator) + } + }) + } + } + // Build scripts for all packages Shop.build_package_scripts(best_pkg) + arrfor(array(all_deps), function(dep) { + Shop.build_package_scripts(dep) + }) + // Build C modules + build_mod = use_cache['core/build'] + if (build_mod) { + _build_c = function() { + target = build_mod.detect_host_target() + arrfor(array(all_deps), function(dep) { + build_mod.build_dynamic(dep, target, 'release') + }) + build_mod.build_dynamic(best_pkg, target, 'release') + } disruption {} + _build_c() + } } disruption { return null } @@ -1600,10 +1658,16 @@ function get_package_zip(pkg) // Update: Check for new version, update lock, fetch and extract // Returns the new lock entry if updated, null if already up to date or failed Shop.update = function(pkg) { + Shop.verify_package_name(pkg) var lock = Shop.load_lock() var lock_entry = lock[pkg] var info = Shop.resolve_package_info(pkg) + if (!info) { + log.error("Not a valid package locator: " + pkg) + return null + } + log.shop(`checking ${pkg}`) var new_entry = null