diff --git a/add.ce b/add.ce index 28010596..1278f0f8 100644 --- a/add.ce +++ b/add.ce @@ -9,188 +9,127 @@ var shop = use('internal/shop') var pkg = use('package') -var build = use('build') var fd = use('fd') var locator = null var alias = null -var resolved = null -var parts = null -var cwd = null -var build_target = null var recursive = false +var cwd = fd.realpath('.') +var parts = null var locators = null var added = 0 var failed = 0 -var summary = null +var _add_dep = null +var _install = null +var i = 0 -array(args, function(arg) { - if (arg == '--help' || arg == '-h') { - log.console("Usage: cell add [alias]") - log.console("") - log.console("Add a dependency to the current package.") - log.console("") - log.console("Examples:") - log.console(" cell add gitea.pockle.world/john/prosperon") - log.console(" cell add gitea.pockle.world/john/cell-image image") - log.console(" cell add ../local-package") - log.console(" cell add -r ../packages") - $stop() - } else if (arg == '-r') { - recursive = true - } else if (!starts_with(arg, '-')) { - if (!locator) { - locator = arg - } else if (!alias) { - alias = arg - } - } -}) - -if (!locator && !recursive) { - log.console("Usage: cell add [alias]") - $stop() -} - -// Resolve relative paths to absolute paths -if (locator && (locator == '.' || starts_with(locator, './') || starts_with(locator, '../') || fd.is_dir(locator))) { - resolved = fd.realpath(locator) - if (resolved) { - locator = resolved - } -} - -// Generate default alias from locator -if (!alias && locator) { - // Use the last component of the locator as alias - parts = array(locator, '/') - alias = parts[length(parts) - 1] - // Remove any version suffix - if (search(alias, '@') != null) { - alias = array(alias, '@')[0] - } -} - -// Check we're in a package directory -cwd = fd.realpath('.') -if (!fd.is_file(cwd + '/cell.toml')) { - log.error("Not in a package directory (no cell.toml found)") - $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) { - locator = '.' - } - resolved = fd.realpath(locator) - if (!resolved || !fd.is_dir(resolved)) { - log.error(`${locator} is not a directory`) - $stop() - } - locators = find_packages(resolved) - if (length(locators) == 0) { - log.console("No packages found in " + resolved) - $stop() - } - log.console(`Found ${text(length(locators))} package(s) in ${resolved}`) - - arrfor(locators, function(loc) { - // Generate alias from directory name - var loc_parts = array(loc, '/') - var loc_alias = loc_parts[length(loc_parts) - 1] - - log.console(" Adding " + loc + " as '" + loc_alias + "'...") - var _add = function() { - pkg.add_dependency(null, loc, loc_alias) - shop.get(loc) - shop.extract(loc) - shop.build_package_scripts(loc) - var _build_c = function() { - build_target = build.detect_host_target() - build.build_dynamic(loc, build_target, 'release') - } disruption { - // Not all packages have C code +var run = function() { + for (i = 0; i < length(args); i++) { + if (args[i] == '--help' || args[i] == '-h') { + log.console("Usage: cell add [alias]") + log.console("") + log.console("Add a dependency to the current package.") + log.console("") + log.console("Examples:") + log.console(" cell add gitea.pockle.world/john/prosperon") + log.console(" cell add gitea.pockle.world/john/cell-image image") + log.console(" cell add ../local-package") + log.console(" cell add -r ../packages") + return + } else if (args[i] == '-r') { + recursive = true + } else if (!starts_with(args[i], '-')) { + if (!locator) { + locator = args[i] + } else if (!alias) { + alias = args[i] } - _build_c() - added++ - } disruption { - log.console(` Warning: Failed to add ${loc}`) - failed++ } - _add() - }) - - summary = "Added " + text(added) + " package(s)." - if (failed > 0) { - summary += " Failed: " + text(failed) + "." - } - log.console(summary) - $stop() -} - -log.console("Adding " + locator + " as '" + alias + "'...") - -// Add to local project's cell.toml -var _add_dep = function() { - pkg.add_dependency(null, locator, alias) - log.console(" Added to cell.toml") -} disruption { - log.error("Failed to update cell.toml") - $stop() -} -_add_dep() - -// Install to shop -var _install = function() { - shop.get(locator) - shop.extract(locator) - - // Build scripts - var script_result = shop.build_package_scripts(locator) - if (length(script_result.errors) > 0) { - log.console(" Warning: " + text(length(script_result.errors)) + " script(s) failed to compile") } - // Build C code if any - var _build_c = function() { - build_target = build.detect_host_target() - build.build_dynamic(locator, build_target, 'release') + if (!locator && !recursive) { + log.console("Usage: cell add [alias]") + return + } + + if (locator) + locator = shop.resolve_locator(locator) + + // Generate default alias from locator + if (!alias && locator) { + parts = array(locator, '/') + alias = parts[length(parts) - 1] + if (search(alias, '@') != null) + alias = array(alias, '@')[0] + } + + // Check we're in a package directory + if (!fd.is_file(cwd + '/cell.toml')) { + log.error("Not in a package directory (no cell.toml found)") + return + } + + // Recursive mode + if (recursive) { + if (!locator) locator = '.' + locator = shop.resolve_locator(locator) + if (!fd.is_dir(locator)) { + log.error(`${locator} is not a directory`) + return + } + locators = filter(pkg.find_packages(locator), function(p) { + return p != cwd + }) + if (length(locators) == 0) { + log.console("No packages found in " + locator) + return + } + log.console(`Found ${text(length(locators))} package(s) in ${locator}`) + + added = 0 + failed = 0 + arrfor(locators, function(loc) { + var loc_parts = array(loc, '/') + var loc_alias = loc_parts[length(loc_parts) - 1] + log.console(" Adding " + loc + " as '" + loc_alias + "'...") + var _add = function() { + pkg.add_dependency(null, loc, loc_alias) + shop.sync(loc) + added = added + 1 + } disruption { + log.console(` Warning: Failed to add ${loc}`) + failed = failed + 1 + } + _add() + }) + + log.console("Added " + text(added) + " package(s)." + (failed > 0 ? " Failed: " + text(failed) + "." : "")) + return + } + + // Single package add + log.console("Adding " + locator + " as '" + alias + "'...") + + _add_dep = function() { + pkg.add_dependency(null, locator, alias) + log.console(" Added to cell.toml") } disruption { - // Not all packages have C code + log.error("Failed to update cell.toml") + return } - _build_c() + _add_dep() - log.console(" Installed to shop") -} disruption { - log.error("Failed to install") - $stop() + _install = function() { + shop.sync_with_deps(locator) + log.console(" Installed to shop") + } disruption { + log.error("Failed to install") + return + } + _install() + + log.console("Added " + alias + " (" + locator + ")") } -_install() - -log.console("Added " + alias + " (" + locator + ")") +run() $stop() diff --git a/audit.ce b/audit.ce index 8b228098..33a5d9f3 100644 --- a/audit.ce +++ b/audit.ce @@ -10,32 +10,26 @@ var shop = use('internal/shop') var pkg = use('package') -var fd = use('fd') var target_package = null var i = 0 -var resolved = null -for (i = 0; i < length(args); i++) { - if (args[i] == '--help' || args[i] == '-h') { - log.console("Usage: cell audit []") - log.console("") - log.console("Test-compile all .ce and .cm scripts in package(s).") - log.console("Reports all errors without stopping at the first failure.") - $stop() - } else if (!starts_with(args[i], '-')) { - target_package = args[i] +var run = function() { + for (i = 0; i < length(args); i++) { + if (args[i] == '--help' || args[i] == '-h') { + log.console("Usage: cell audit []") + log.console("") + log.console("Test-compile all .ce and .cm scripts in package(s).") + log.console("Reports all errors without stopping at the first failure.") + return + } else if (!starts_with(args[i], '-')) { + target_package = args[i] + } } -} // Resolve local paths if (target_package) { - if (target_package == '.' || starts_with(target_package, './') || starts_with(target_package, '../') || fd.is_dir(target_package)) { - resolved = fd.realpath(target_package) - if (resolved) { - target_package = resolved - } - } + target_package = shop.resolve_locator(target_package) } var packages = null @@ -75,5 +69,7 @@ if (length(all_failures) > 0) { } log.console("Audit complete: " + text(total_ok) + "/" + text(total_scripts) + " scripts compiled" + (total_errors > 0 ? ", " + text(total_errors) + " failed" : "")) +} +run() $stop() diff --git a/build.ce b/build.ce index 7d3b445a..3f7aebc4 100644 --- a/build.ce +++ b/build.ce @@ -22,67 +22,60 @@ var dry_run = false var i = 0 var targets = null var t = 0 -var resolved = null var lib = null var results = null var success = 0 var failed = 0 -for (i = 0; i < length(args); i++) { - if (args[i] == '-t' || args[i] == '--target') { - if (i + 1 < length(args)) { - target = args[++i] - } else { - log.error('-t requires a target') - $stop() - } - } else if (args[i] == '-p' || args[i] == '--package') { - // Legacy support for -p flag - if (i + 1 < length(args)) { - target_package = args[++i] - } else { - log.error('-p requires a package name') - $stop() - } - } else if (args[i] == '-b' || args[i] == '--buildtype') { - if (i + 1 < length(args)) { - buildtype = args[++i] - if (buildtype != 'release' && buildtype != 'debug' && buildtype != 'minsize') { - log.error('Invalid buildtype: ' + buildtype + '. Must be release, debug, or minsize') - $stop() +var run = function() { + for (i = 0; i < length(args); i++) { + if (args[i] == '-t' || args[i] == '--target') { + if (i + 1 < length(args)) { + target = args[++i] + } else { + log.error('-t requires a target') + return } - } else { - log.error('-b requires a buildtype (release, debug, minsize)') - $stop() + } else if (args[i] == '-p' || args[i] == '--package') { + // Legacy support for -p flag + if (i + 1 < length(args)) { + target_package = args[++i] + } else { + log.error('-p requires a package name') + return + } + } else if (args[i] == '-b' || args[i] == '--buildtype') { + if (i + 1 < length(args)) { + buildtype = args[++i] + if (buildtype != 'release' && buildtype != 'debug' && buildtype != 'minsize') { + log.error('Invalid buildtype: ' + buildtype + '. Must be release, debug, or minsize') + return + } + } else { + log.error('-b requires a buildtype (release, debug, minsize)') + return + } + } else if (args[i] == '--force') { + force_rebuild = true + } else if (args[i] == '--verbose' || args[i] == '-v') { + verbose = true + } else if (args[i] == '--dry-run') { + dry_run = true + } else if (args[i] == '--list-targets') { + log.console('Available targets:') + targets = build.list_targets() + for (t = 0; t < length(targets); t++) { + log.console(' ' + targets[t]) + } + return + } else if (!starts_with(args[i], '-') && !target_package) { + // Positional argument - treat as package locator + target_package = args[i] } - } else if (args[i] == '--force') { - force_rebuild = true - } else if (args[i] == '--verbose' || args[i] == '-v') { - verbose = true - } else if (args[i] == '--dry-run') { - dry_run = true - } else if (args[i] == '--list-targets') { - log.console('Available targets:') - targets = build.list_targets() - for (t = 0; t < length(targets); t++) { - log.console(' ' + targets[t]) - } - $stop() - } else if (!starts_with(args[i], '-') && !target_package) { - // Positional argument - treat as package locator - target_package = args[i] } -} -// Resolve local paths to absolute paths -if (target_package) { - if (target_package == '.' || starts_with(target_package, './') || starts_with(target_package, '../') || fd.is_dir(target_package)) { - resolved = fd.realpath(target_package) - if (resolved) { - target_package = resolved - } - } -} +if (target_package) + target_package = shop.resolve_locator(target_package) // Detect target if not specified if (!target) { @@ -90,17 +83,16 @@ if (!target) { if (target) log.console('Target: ' + target) } -if (target && !build.has_target(target)) { - log.error('Invalid target: ' + target) - log.console('Available targets: ' + text(build.list_targets(), ', ')) - $stop() -} + if (target && !build.has_target(target)) { + log.error('Invalid target: ' + target) + log.console('Available targets: ' + text(build.list_targets(), ', ')) + return + } var packages = shop.list_packages() -log.console('Preparing packages...') arrfor(packages, function(package) { if (package == 'core') return - shop.extract(package) + shop.sync(package, {no_build: true}) }) var _build = null @@ -132,5 +124,7 @@ if (target_package) { log.console(`Build complete: ${success} libraries built${failed > 0 ? `, ${failed} failed` : ''}`) } +} +run() $stop() diff --git a/build.cm b/build.cm index e8b5cc88..676f23dd 100644 --- a/build.cm +++ b/build.cm @@ -118,19 +118,7 @@ function get_build_dir() { return shop.get_build_dir() } -function ensure_dir(path) { - if (fd.stat(path).isDirectory) return - var parts = array(path, '/') - var current = starts_with(path, '/') ? '/' : '' - var i = 0 - for (i = 0; i < length(parts); i++) { - if (parts[i] == '') continue - current += parts[i] + '/' - if (!fd.stat(current).isDirectory) fd.mkdir(current) - } -} - -Build.ensure_dir = ensure_dir +Build.ensure_dir = fd.ensure_dir // ============================================================================ // Dependency scanning helpers @@ -267,7 +255,7 @@ Build.compile_file = function(pkg, file, target, opts) { var fail_path = cache_path(quick_content, SALT_FAIL) var build_dir = get_build_dir() - ensure_dir(build_dir) + fd.ensure_dir(build_dir) // Check for cached failure (skip files that previously failed to compile) if (fd.is_file(fail_path)) { @@ -429,7 +417,7 @@ Build.build_module_dylib = function(pkg, file, target, opts) { all_objects = array(all_objects, _extra) var link_content = compute_link_content(all_objects, resolved_ldflags, target_ldflags, {target: _target, cc: cc}) var build_dir = get_build_dir() - ensure_dir(build_dir) + fd.ensure_dir(build_dir) var dylib_path = cache_path(link_content, SALT_DYLIB) var cmd_parts = null var cmd_str = null @@ -703,7 +691,7 @@ Build.compile_native = function(src_path, target, buildtype, pkg) { var src = text(fd.slurp(src_path)) var native_key = native_cache_content(src, _target, san_flags) var build_dir = get_build_dir() - ensure_dir(build_dir) + fd.ensure_dir(build_dir) var dylib_path = cache_path(native_key, SALT_NATIVE) if (fd.is_file(dylib_path)) @@ -775,7 +763,7 @@ Build.compile_native_ir = function(optimized, src_path, opts) { var src = text(fd.slurp(src_path)) var native_key = native_cache_content(src, _target, san_flags) var build_dir = get_build_dir() - ensure_dir(build_dir) + fd.ensure_dir(build_dir) var dylib_path = cache_path(native_key, SALT_NATIVE) if (fd.is_file(dylib_path)) diff --git a/clean.ce b/clean.ce index 89c80bc7..83b7c260 100644 --- a/clean.ce +++ b/clean.ce @@ -24,42 +24,42 @@ var clean_fetch = false var deep = false var dry_run = false var i = 0 -var resolved = null var deps = null -for (i = 0; i < length(args); i++) { - if (args[i] == '--build') { - clean_build = true - } else if (args[i] == '--fetch') { - clean_fetch = true - } else if (args[i] == '--all') { - clean_build = true - clean_fetch = true - } else if (args[i] == '--deep') { - deep = true - } else if (args[i] == '--dry-run') { - dry_run = true - } else if (args[i] == '--help' || args[i] == '-h') { - log.console("Usage: cell clean [] [options]") - log.console("") - log.console("Remove cached material to force refetch/rebuild.") - log.console("") - log.console("Scopes:") - log.console(" Clean specific package") - log.console(" shop Clean entire shop") - log.console(" world Clean all world packages") - log.console("") - log.console("Options:") - log.console(" --build Remove build outputs only (default)") - log.console(" --fetch Remove fetched sources only") - log.console(" --all Remove both build outputs and fetched sources") - log.console(" --deep Apply to full dependency closure") - log.console(" --dry-run Show what would be deleted") - $stop() - } else if (!starts_with(args[i], '-')) { - scope = args[i] +var run = function() { + for (i = 0; i < length(args); i++) { + if (args[i] == '--build') { + clean_build = true + } else if (args[i] == '--fetch') { + clean_fetch = true + } else if (args[i] == '--all') { + clean_build = true + clean_fetch = true + } else if (args[i] == '--deep') { + deep = true + } else if (args[i] == '--dry-run') { + dry_run = true + } else if (args[i] == '--help' || args[i] == '-h') { + log.console("Usage: cell clean [] [options]") + log.console("") + log.console("Remove cached material to force refetch/rebuild.") + log.console("") + log.console("Scopes:") + log.console(" Clean specific package") + log.console(" shop Clean entire shop") + log.console(" world Clean all world packages") + log.console("") + log.console("Options:") + log.console(" --build Remove build outputs only (default)") + log.console(" --fetch Remove fetched sources only") + log.console(" --all Remove both build outputs and fetched sources") + log.console(" --deep Apply to full dependency closure") + log.console(" --dry-run Show what would be deleted") + return + } else if (!starts_with(args[i], '-')) { + scope = args[i] + } } -} // Default to --build if nothing specified if (!clean_build && !clean_fetch) { @@ -76,12 +76,7 @@ var is_shop_scope = (scope == 'shop') var is_world_scope = (scope == 'world') if (!is_shop_scope && !is_world_scope) { - if (scope == '.' || starts_with(scope, './') || starts_with(scope, '../') || fd.is_dir(scope)) { - resolved = fd.realpath(scope) - if (resolved) { - scope = resolved - } - } + scope = shop.resolve_locator(scope) } var files_to_delete = [] @@ -196,5 +191,7 @@ if (dry_run) { log.console("Clean complete: " + text(deleted_count) + " item(s) deleted.") } } +} +run() $stop() diff --git a/clone.ce b/clone.ce index 9f9b1afe..c3a10294 100644 --- a/clone.ce +++ b/clone.ce @@ -5,115 +5,56 @@ var shop = use('internal/shop') var link = use('link') var fd = use('fd') var http = use('http') -var miniz = use('miniz') -var resolved = null -var cwd = null -var parent = null - -if (length(args) < 2) { - log.console("Usage: cell clone ") - log.console("Clones a cell package to a local path and links it.") - $stop() -} +var run = function() { + if (length(args) < 2) { + log.console("Usage: cell clone ") + log.console("Clones a cell package to a local path and links it.") + return + } var origin = args[0] var target_path = args[1] // Resolve target path to absolute -if (target_path == '.' || starts_with(target_path, './') || starts_with(target_path, '../')) { - resolved = fd.realpath(target_path) - if (resolved) { - target_path = resolved - } else { - // Path doesn't exist yet, resolve relative to cwd - cwd = fd.realpath('.') - if (target_path == '.') { - target_path = cwd - } else if (starts_with(target_path, './')) { - target_path = cwd + text(target_path, 1) - } else if (starts_with(target_path, '../')) { - // Go up one directory from cwd - parent = fd.dirname(cwd) - target_path = parent + text(target_path, 2) - } - } -} +target_path = shop.resolve_locator(target_path) // Check if target already exists -if (fd.is_dir(target_path)) { - log.console("Error: " + target_path + " already exists") - $stop() -} + if (fd.is_dir(target_path)) { + log.console("Error: " + target_path + " already exists") + return + } log.console("Cloning " + origin + " to " + target_path + "...") // Get the latest commit var info = shop.resolve_package_info(origin) -if (!info || info == 'local') { - log.console("Error: " + origin + " is not a remote package") - $stop() -} + if (!info || info == 'local') { + log.console("Error: " + origin + " is not a remote package") + return + } // Update to get the commit hash var update_result = shop.update(origin) -if (!update_result) { - log.console("Error: Could not fetch " + origin) - $stop() -} + if (!update_result) { + log.console("Error: Could not fetch " + origin) + return + } // Fetch and extract to the target path var lock = shop.load_lock() var entry = lock[origin] -if (!entry || !entry.commit) { - log.console("Error: No commit found for " + origin) - $stop() -} + if (!entry || !entry.commit) { + log.console("Error: No commit found for " + origin) + return + } var download_url = shop.get_download_url(origin, entry.commit) log.console("Downloading from " + download_url) -var zip_blob = null -var zip = null -var count = 0 -var i = 0 -var filename = null -var first_slash = null -var rel_path = null -var full_path = null -var dir_path = null - var _clone = function() { - zip_blob = http.fetch(download_url) - - // Extract zip to target path - zip = miniz.read(zip_blob) - if (!zip) { - log.console("Error: Failed to read zip archive") - $stop() - } - - // Create target directory - fd.mkdir(target_path) - - count = zip.count() - for (i = 0; i < count; i++) { - if (zip.is_directory(i)) continue - filename = zip.get_filename(i) - first_slash = search(filename, '/') - if (first_slash == null) continue - if (first_slash + 1 >= length(filename)) continue - - rel_path = text(filename, first_slash + 1) - full_path = target_path + '/' + rel_path - dir_path = fd.dirname(full_path) - - // Ensure directory exists - if (!fd.is_dir(dir_path)) { - fd.mkdir(dir_path) - } - fd.slurpwrite(full_path, zip.slurp(filename)) - } + var zip_blob = http.fetch(download_url) + shop.install_zip(zip_blob, target_path) log.console("Extracted to " + target_path) @@ -123,6 +64,8 @@ var _clone = function() { } disruption { log.console("Error during clone") } -_clone() + _clone() +} +run() $stop() diff --git a/diff.ce b/diff.ce index 4fe58b2f..f10bf111 100644 --- a/diff.ce +++ b/diff.ce @@ -8,6 +8,7 @@ var shop = use('internal/shop') var pkg = use('package') var fd = use('fd') var time = use('time') +var testlib = use('internal/testlib') var _args = args == null ? [] : args @@ -27,10 +28,7 @@ if (length(_args) > 0) { target_test = _args[0] } -function is_valid_package(dir) { - var _dir = dir == null ? '.' : dir - return fd.is_file(_dir + '/cell.toml') -} +var is_valid_package = testlib.is_valid_package if (!is_valid_package('.')) { log.console('No cell.toml found in current directory') @@ -63,47 +61,8 @@ function collect_tests(specific_test) { return test_files } -// Deep comparison of two values -function values_equal(a, b) { - var i = 0 - var ka = null - var kb = null - if (a == b) return true - if (is_null(a) && is_null(b)) return true - if (is_null(a) || is_null(b)) return false - if (is_array(a) && is_array(b)) { - if (length(a) != length(b)) return false - i = 0 - while (i < length(a)) { - if (!values_equal(a[i], b[i])) return false - i = i + 1 - } - return true - } - if (is_object(a) && is_object(b)) { - ka = array(a) - kb = array(b) - if (length(ka) != length(kb)) return false - i = 0 - while (i < length(ka)) { - if (!values_equal(a[ka[i]], b[ka[i]])) return false - i = i + 1 - } - return true - } - return false -} - -function describe(val) { - if (is_null(val)) return "null" - if (is_text(val)) return `"${val}"` - if (is_number(val)) return text(val) - if (is_logical(val)) return text(val) - if (is_function(val)) return "" - if (is_array(val)) return `[array length=${text(length(val))}]` - if (is_object(val)) return `{record keys=${text(length(array(val)))}}` - return "" -} +var values_equal = testlib.values_equal +var describe = testlib.describe // Run a single test file through both paths function diff_test_file(file_path) { diff --git a/docs/cli.md b/docs/cli.md index 907b1d6d..af10caf8 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -317,11 +317,13 @@ Options: - `--format=pretty|bare|json` — output format (default: `pretty` for console, `json` for file) - `--channels=ch1,ch2` — channels to subscribe (default: `console,error,system`). Use `'*'` for all channels (quote to prevent shell glob expansion). - `--exclude=ch1,ch2` — channels to exclude (useful with `'*'`) +- `--stack=ch1,ch2` — channels that capture a full stack trace (default: `error`) ```bash pit log add terminal console --format=bare --channels=console pit log add errors file .cell/logs/errors.jsonl --channels=error pit log add dump file .cell/logs/dump.jsonl '--channels=*' --exclude=console +pit log add debug console --channels=error,debug --stack=error,debug ``` ### pit log remove diff --git a/docs/logging.md b/docs/logging.md index 2aea9e42..b7b0d838 100644 --- a/docs/logging.md +++ b/docs/logging.md @@ -29,14 +29,16 @@ Non-text values are JSON-encoded automatically. ## Default Behavior -With no configuration, a default sink routes `console`, `error`, and `system` to the terminal in pretty format: +With no configuration, a default sink routes `console`, `error`, and `system` to the terminal in pretty format. The `error` channel includes a stack trace by default: ``` -[a3f12] [console] main.ce:5 server started on port 8080 -[a3f12] [error] main.ce:12 connection refused +[a3f12] [console] server started on port 8080 +[a3f12] [error] connection refused + at handle_request (server.ce:42:3) + at main (main.ce:5:1) ``` -The format is `[actor_id] [channel] file:line message`. +The format is `[actor_id] [channel] message`. Error stack traces are always on unless you explicitly configure a sink without them. ## Configuration @@ -114,14 +116,22 @@ File sinks write one JSON-encoded record per line. Console sinks format the reco ## Stack Traces -Add a `stack` field to a sink to capture a full call stack for specific channels. The value is an array of channel names. +The `error` channel captures stack traces by default. To enable stack traces for other channels, add a `stack` field to a sink — an array of channel names that should include a call stack. + +Via the CLI: + +```bash +pit log add terminal console --channels=console,error,debug --stack=error,debug +``` + +Or in `log.toml`: ```toml [sink.terminal] type = "console" format = "bare" -channels = ["console", "error"] -stack = ["error"] +channels = ["console", "error", "debug"] +stack = ["error", "debug"] ``` Only channels listed in `stack` get stack traces. Other channels on the same sink print without one: @@ -150,6 +160,7 @@ The `pit log` command manages sinks and reads log files. See [CLI — pit log](/ pit log list # show sinks pit log add terminal console --format=bare --channels=console pit log add dump file .cell/logs/dump.jsonl '--channels=*' --exclude=console +pit log add debug console --channels=error,debug --stack=error,debug pit log remove terminal pit log read dump --lines=20 --channel=error pit log tail dump diff --git a/fd.cm b/fd.cm index ea5fbfd6..dc9c056a 100644 --- a/fd.cm +++ b/fd.cm @@ -97,4 +97,24 @@ fd.globfs = function(globs, dir) { return results } +fd.ensure_dir = function ensure_dir(path) { + if (fd.is_dir(path)) return true + var parts = array(path, '/') + var current = starts_with(path, '/') ? '/' : '' + var i = 0 + for (i = 0; i < length(parts); i++) { + if (parts[i] == '') continue + current = current + parts[i] + '/' + if (!fd.is_dir(current)) + fd.mkdir(current) + } + return true +} + +fd.safe_package_path = function safe_package_path(pkg) { + if (pkg && starts_with(pkg, '/')) + return replace(replace(pkg, '/', '_'), '@', '_') + return replace(pkg, '@', '_') +} + return fd diff --git a/fetch.ce b/fetch.ce index a1cd1a35..89f653b7 100644 --- a/fetch.ce +++ b/fetch.ce @@ -1,90 +1,49 @@ -// cell fetch - Fetch package zips from remote sources +// cell fetch - Sync packages from remote sources // -// This command ensures that the zip files on disk match what's in the lock file. -// For local packages, this is a no-op. -// For remote packages, downloads the zip if not present or hash mismatch. +// Ensures all packages are fetched, extracted, compiled, and ready to use. +// For local packages, this is a no-op (symlinks only). // // Usage: -// cell fetch - Fetch all packages -// cell fetch - Fetch a specific package +// cell fetch - Sync all packages +// cell fetch - Sync a specific package var shop = use('internal/shop') -// Parse arguments var target_pkg = null var i = 0 +var packages = null +var count = 0 -for (i = 0; i < length(args); i++) { - if (args[i] == '--help' || args[i] == '-h') { - log.console("Usage: cell fetch [package]") - log.console("Fetch package zips from remote sources.") - log.console("") - log.console("Arguments:") - log.console(" package Optional package name to fetch. If omitted, fetches all.") - log.console("") - log.console("This command ensures that the zip files on disk match what's in") - log.console("the lock file. For local packages, this is a no-op.") - $stop() - } else if (!starts_with(args[i], '-')) { - target_pkg = args[i] +var run = function() { + for (i = 0; i < length(args); i++) { + if (args[i] == '--help' || args[i] == '-h') { + log.console("Usage: cell fetch [package]") + log.console("Sync packages from remote sources.") + log.console("") + log.console("Arguments:") + log.console(" package Optional package to sync. If omitted, syncs all.") + return + } else if (!starts_with(args[i], '-')) { + target_pkg = args[i] + } + } + + if (target_pkg) { + target_pkg = shop.resolve_locator(target_pkg) + log.console("Syncing " + target_pkg + "...") + shop.sync(target_pkg) + log.console("Done.") + } else { + packages = shop.list_packages() + count = 0 + arrfor(packages, function(pkg) { + if (pkg == 'core') return + shop.sync(pkg) + count = count + 1 + }) + log.console("Synced " + text(count) + " package(s).") } } - -var all_packages = shop.list_packages() -var lock = shop.load_lock() -var packages_to_fetch = [] - -if (target_pkg) { - // Fetch specific package - if (find(all_packages, target_pkg) == null) { - log.error("Package not found: " + target_pkg) - $stop() - } - push(packages_to_fetch, target_pkg) -} else { - // Fetch all packages - packages_to_fetch = all_packages -} - -var remote_count = 0 -arrfor(packages_to_fetch, function(pkg) { - var entry = lock[pkg] - if (pkg != 'core' && (!entry || entry.type != 'local')) - remote_count++ -}, null, null) - -if (remote_count > 0) - log.console(`Fetching ${text(remote_count)} remote package(s)...`) - -var downloaded_count = 0 -var cached_count = 0 -var fail_count = 0 - -arrfor(packages_to_fetch, function(pkg) { - // Skip core (handled separately) - if (pkg == 'core') return - - var result = shop.fetch(pkg) - if (result.status == 'local') { - // Local packages are just symlinks, nothing to fetch - return - } else if (result.status == 'cached') { - cached_count++ - } else if (result.status == 'downloaded') { - log.console(" Downloaded: " + pkg) - downloaded_count++ - } else if (result.status == 'error') { - log.error(" Failed: " + pkg + (result.message ? " - " + result.message : "")) - fail_count++ - } -}, null, null) - -log.console("") -var parts = [] -if (downloaded_count > 0) push(parts, `${text(downloaded_count)} downloaded`) -if (cached_count > 0) push(parts, `${text(cached_count)} cached`) -if (fail_count > 0) push(parts, `${text(fail_count)} failed`) -if (length(parts) == 0) push(parts, "nothing to fetch") -log.console("Fetch complete: " + text(parts, ", ")) +run() $stop() diff --git a/fuzz.ce b/fuzz.ce index 832410a2..7b5c68c5 100644 --- a/fuzz.ce +++ b/fuzz.ce @@ -12,6 +12,7 @@ var fd = use('fd') var time = use('time') var json = use('json') +var testlib = use('internal/testlib') var os_ref = use('internal/os') var analyze = os_ref.analyze @@ -54,48 +55,9 @@ if (!run_ast_noopt_fn) { // Ensure failures directory exists var failures_dir = "tests/fuzz_failures" -function ensure_dir(path) { - if (fd.is_dir(path)) return - var parts = array(path, '/') - var current = '' - var j = 0 - while (j < length(parts)) { - if (parts[j] != '') { - current = current + parts[j] + '/' - if (!fd.is_dir(current)) { - fd.mkdir(current) - } - } - j = j + 1 - } -} - -// Deep comparison -function values_equal(a, b) { - var j = 0 - if (a == b) return true - if (is_null(a) && is_null(b)) return true - if (is_null(a) || is_null(b)) return false - if (is_array(a) && is_array(b)) { - if (length(a) != length(b)) return false - j = 0 - while (j < length(a)) { - if (!values_equal(a[j], b[j])) return false - j = j + 1 - } - return true - } - return false -} - -function describe(val) { - if (is_null(val)) return "null" - if (is_text(val)) return `"${val}"` - if (is_number(val)) return text(val) - if (is_logical(val)) return text(val) - if (is_function(val)) return "" - return "" -} +var ensure_dir = fd.ensure_dir +var values_equal = testlib.values_equal +var describe = testlib.describe // Run a single fuzz iteration function run_fuzz(seed_val) { diff --git a/graph.ce b/graph.ce index 07d24566..7bc87971 100644 --- a/graph.ce +++ b/graph.ce @@ -15,7 +15,6 @@ var shop = use('internal/shop') var pkg = use('package') var link = use('link') -var fd = use('fd') var json = use('json') var target_locator = null @@ -23,41 +22,41 @@ var format = 'tree' var show_locked = false var show_world = false var i = 0 -var resolved = null -for (i = 0; i < length(args); i++) { - if (args[i] == '--format' || args[i] == '-f') { - if (i + 1 < length(args)) { - format = args[++i] - if (format != 'tree' && format != 'dot' && format != 'json') { - log.error('Invalid format: ' + format + '. Must be tree, dot, or json') - $stop() +var run = function() { + for (i = 0; i < length(args); i++) { + if (args[i] == '--format' || args[i] == '-f') { + if (i + 1 < length(args)) { + format = args[++i] + if (format != 'tree' && format != 'dot' && format != 'json') { + log.error('Invalid format: ' + format + '. Must be tree, dot, or json') + return + } + } else { + log.error('--format requires a format type') + return } - } else { - log.error('--format requires a format type') - $stop() + } else if (args[i] == '--resolved') { + show_locked = false + } else if (args[i] == '--locked') { + show_locked = true + } else if (args[i] == '--world') { + show_world = true + } else if (args[i] == '--help' || args[i] == '-h') { + log.console("Usage: cell graph [] [options]") + log.console("") + log.console("Emit the dependency graph.") + log.console("") + log.console("Options:") + log.console(" --format Output format: tree (default), dot, json") + log.console(" --resolved Show resolved view with links applied (default)") + log.console(" --locked Show lock view without links") + log.console(" --world Graph all packages in shop") + return + } else if (!starts_with(args[i], '-')) { + target_locator = args[i] } - } else if (args[i] == '--resolved') { - show_locked = false - } else if (args[i] == '--locked') { - show_locked = true - } else if (args[i] == '--world') { - show_world = true - } else if (args[i] == '--help' || args[i] == '-h') { - log.console("Usage: cell graph [] [options]") - log.console("") - log.console("Emit the dependency graph.") - log.console("") - log.console("Options:") - log.console(" --format Output format: tree (default), dot, json") - log.console(" --resolved Show resolved view with links applied (default)") - log.console(" --locked Show lock view without links") - log.console(" --world Graph all packages in shop") - $stop() - } else if (!starts_with(args[i], '-')) { - target_locator = args[i] } -} var links = show_locked ? {} : link.load() @@ -127,13 +126,7 @@ if (show_world) { target_locator = '.' } - // Resolve local paths - if (target_locator == '.' || starts_with(target_locator, './') || starts_with(target_locator, '../') || fd.is_dir(target_locator)) { - resolved = fd.realpath(target_locator) - if (resolved) { - target_locator = resolved - } - } + target_locator = shop.resolve_locator(target_locator) push(roots, target_locator) } @@ -244,5 +237,7 @@ if (format == 'tree') { log.console(json.encode(output)) } +} +run() $stop() diff --git a/install.ce b/install.ce index 05fb3df5..2e76fc0b 100644 --- a/install.ce +++ b/install.ce @@ -1,4 +1,4 @@ -// cell install - Install a package to the shop +// cell install - Install a package and its dependencies // // Usage: // cell install Install a package and its dependencies @@ -6,266 +6,113 @@ // // Options: // --target Build for target platform -// --refresh Refresh floating refs before locking // --dry-run Show what would be installed // -r Recursively find and install all packages in directory var shop = use('internal/shop') -var build = use('build') var pkg = use('package') var fd = use('fd') -if (length(args) < 1) { - log.console("Usage: cell install [options]") - log.console("") - log.console("Options:") - log.console(" --target Build for target platform") - log.console(" --refresh Refresh floating refs before locking") - log.console(" --dry-run Show what would be installed") - log.console(" -r Recursively find and install all packages in directory") - $stop() -} - var locator = null var target_triple = null -var refresh = false var dry_run = false var recursive = false var i = 0 -var resolved = null var locators = null +var cwd = fd.realpath('.') +var lock = null +var installed = 0 +var failed = 0 -for (i = 0; i < length(args); i++) { - if (args[i] == '--target' || args[i] == '-t') { - if (i + 1 < length(args)) { - target_triple = args[++i] - } else { - log.error('--target requires a triple') - $stop() - } - } else if (args[i] == '--refresh') { - refresh = true - } else if (args[i] == '--dry-run') { - dry_run = true - } else if (args[i] == '-r') { - recursive = true - } else if (args[i] == '--help' || args[i] == '-h') { - log.console("Usage: cell install [options]") - log.console("") - log.console("Install a package and its dependencies to the shop.") - log.console("") - log.console("Options:") - log.console(" --target Build for target platform") - log.console(" --refresh Refresh floating refs before locking") - log.console(" --dry-run Show what would be installed") - log.console(" -r Recursively find and install all packages in directory") - $stop() - } else if (!starts_with(args[i], '-')) { - locator = args[i] - } -} - -if (!locator && !recursive) { - log.console("Usage: cell install ") - $stop() -} - -// Resolve relative paths to absolute paths -// Local paths like '.' or '../foo' need to be converted to absolute paths -if (locator && (locator == '.' || starts_with(locator, './') || starts_with(locator, '../') || fd.is_dir(locator))) { - resolved = fd.realpath(locator) - if (resolved) { - locator = resolved - } -} - -// 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 install each -if (recursive) { - if (!locator) { - locator = '.' - } - resolved = fd.realpath(locator) - if (!resolved || !fd.is_dir(resolved)) { - log.error(`${locator} is not a directory`) - $stop() - } - locators = find_packages(resolved) - if (length(locators) == 0) { - log.console("No packages found in " + resolved) - $stop() - } - log.console(`Found ${text(length(locators))} package(s) in ${resolved}`) -} - -// Default target -if (!target_triple) { - target_triple = build.detect_host_target() -} - -// Gather all packages that will be installed -var packages_to_install = [] -var skipped_packages = [] -var summary = null -var visited = {} - -// Recursive mode: install all found packages and exit -if (recursive) { - arrfor(locators, function(loc) { - log.console(" Installing " + loc + "...") - var _inst = function() { - shop.update(loc) - shop.extract(loc) - shop.build_package_scripts(loc) - var _build_c = function() { - build.build_dynamic(loc, target_triple, 'release') - } disruption { - // Not all packages have C code +var run = function() { + for (i = 0; i < length(args); i++) { + if (args[i] == '--target' || args[i] == '-t') { + if (i + 1 < length(args)) { + target_triple = args[++i] + } else { + log.error('--target requires a triple') + return } - _build_c() - push(packages_to_install, loc) - } disruption { - push(skipped_packages, loc) - log.console(` Warning: Failed to install ${loc}`) + } else if (args[i] == '--dry-run') { + dry_run = true + } else if (args[i] == '-r') { + recursive = true + } else if (args[i] == '--help' || args[i] == '-h') { + log.console("Usage: cell install [options]") + log.console("") + log.console("Install a package and its dependencies.") + log.console("") + log.console("Options:") + log.console(" --target Build for target platform") + log.console(" --dry-run Show what would be installed") + log.console(" -r Recursively find and install all packages in directory") + return + } else if (!starts_with(args[i], '-')) { + locator = args[i] } - _inst() - }) - - summary = "Installed " + text(length(packages_to_install)) + " package(s)." - if (length(skipped_packages) > 0) { - summary += " Failed: " + text(length(skipped_packages)) + "." } - log.console(summary) - $stop() -} -log.console("Installing " + locator + "...") - -function gather_packages(pkg_locator) { - var lock = null - var update_result = null - var deps = null - if (visited[pkg_locator]) return - visited[pkg_locator] = true - - // Check if this is a local path that doesn't exist - if (starts_with(pkg_locator, '/') && !fd.is_dir(pkg_locator)) { - push(skipped_packages, pkg_locator) - log.console(" Skipping missing local package: " + pkg_locator) + if (!locator && !recursive) { + log.console("Usage: cell install [options]") return } - push(packages_to_install, pkg_locator) +if (locator) + locator = shop.resolve_locator(locator) - // Try to read dependencies - var _gather = function() { - // For packages not yet extracted, we need to update and extract first to read deps - lock = shop.load_lock() - if (!lock[pkg_locator]) { - if (!dry_run) { - update_result = shop.update(pkg_locator) - if (update_result) { - shop.extract(pkg_locator) - } else { - // Update failed - package might not be fetchable - log.console("Warning: Could not fetch " + pkg_locator) - return - } - } - } else { - // Package is in lock, ensure it's extracted - if (!dry_run) { - shop.extract(pkg_locator) - } - } - - deps = pkg.dependencies(pkg_locator) - if (deps) { - arrfor(array(deps), function(alias) { - var dep_locator = deps[alias] - gather_packages(dep_locator) - }) - } - } disruption { - // Package might not have dependencies or cell.toml issue - if (!dry_run) { - log.console(`Warning: Could not read dependencies for ${pkg_locator}`) - } +// Recursive mode: find all packages in directory and install each +if (recursive) { + if (!locator) locator = '.' + locator = shop.resolve_locator(locator) + if (!fd.is_dir(locator)) { + log.error(`${locator} is not a directory`) + return } - _gather() -} - -// Gather all packages -gather_packages(locator) - -if (dry_run) { - log.console("Would install:") - arrfor(packages_to_install, function(p) { - var lock = shop.load_lock() - var exists = lock[p] != null - log.console(" " + p + (exists ? " (already installed)" : "")) + locators = filter(pkg.find_packages(locator), function(p) { + return p != cwd }) - if (length(skipped_packages) > 0) { - log.console("") - log.console("Would skip (missing local paths):") - arrfor(skipped_packages, function(p) { - log.console(" " + p) + if (length(locators) == 0) { + log.console("No packages found in " + locator) + return + } + log.console(`Found ${text(length(locators))} package(s) in ${locator}`) + + if (dry_run) { + log.console("Would install:") + arrfor(locators, function(loc) { + lock = shop.load_lock() + log.console(" " + loc + (lock[loc] ? " (already installed)" : "")) }) + } else { + installed = 0 + failed = 0 + arrfor(locators, function(loc) { + log.console(" Installing " + loc + "...") + var _inst = function() { + shop.sync(loc, {target: target_triple}) + installed = installed + 1 + } disruption { + failed = failed + 1 + log.console(` Warning: Failed to install ${loc}`) + } + _inst() + }) + + log.console("Installed " + text(installed) + " package(s)." + (failed > 0 ? " Failed: " + text(failed) + "." : "")) } - $stop() + return } -// Install each package -function install_package(pkg_locator) { - // Update lock entry - shop.update(pkg_locator) - - // Extract/symlink the package - shop.extract(pkg_locator) - - // Build scripts - shop.build_package_scripts(pkg_locator) - - // Build C code - var _build_c = function() { - build.build_dynamic(pkg_locator, target_triple, 'release') - } disruption { - // Not all packages have C code + // Single package install with dependencies + if (dry_run) { + log.console("Would install: " + locator + " (and dependencies)") + return } - _build_c() -} -arrfor(packages_to_install, function(p) { - log.console(" Installing " + p + "...") - install_package(p) -}) - -summary = "Installed " + text(length(packages_to_install)) + " package(s)." -if (length(skipped_packages) > 0) { - summary += " Skipped " + text(length(skipped_packages)) + " missing local path(s)." + log.console("Installing " + locator + "...") + shop.sync_with_deps(locator, {refresh: true, target: target_triple}) + log.console("Done.") } -log.console(summary) +run() $stop() diff --git a/internal/engine.cm b/internal/engine.cm index eaf62fb7..0211c8f1 100644 --- a/internal/engine.cm +++ b/internal/engine.cm @@ -506,20 +506,37 @@ var REPLYTIMEOUT = 60 // seconds before replies are ignored // --- Logging system (bootstrap phase) --- // Early log: prints to console before toml/time/json are loaded. // Upgraded to full sink-based system after config loads (see load_log_config below). +// The bootstrap log forwards to _log_full once the full system is ready, so that +// modules loaded early (like shop.cm) get full logging even though they captured +// the bootstrap function reference. var log_config = null var channel_sinks = {} var wildcard_sinks = [] var warned_channels = {} var stack_channels = {} +var _log_full = null var log_quiet_channels = { shop: true } function log(name, args) { + if (_log_full) return _log_full(name, args) if (log_quiet_channels[name]) return var msg = args[0] + var stk = null + var i = 0 + var fr = null if (msg == null) msg = "" os.print(`[${text(_cell.id, 0, 5)}] [${name}]: ${msg}\n`) + if (name == "error") { + stk = os.stack(2) + if (stk && length(stk) > 0) { + for (i = 0; i < length(stk); i = i + 1) { + fr = stk[i] + os.print(` at ${fr.fn} (${fr.file}:${text(fr.line)}:${text(fr.col)})\n`) + } + } + } } function actor_die(err) @@ -648,6 +665,7 @@ function build_sink_routing() { var names = array(log_config.sink) arrfor(names, function(name) { var sink = log_config.sink[name] + if (!sink || !is_object(sink)) return sink._name = name if (!is_array(sink.channels)) sink.channels = [] if (is_text(sink.exclude)) sink.exclude = [sink.exclude] @@ -677,7 +695,7 @@ function load_log_config() { log_config = toml.decode(text(fd.slurp(log_path))) } } - if (!log_config || !log_config.sink) { + if (!log_config || !log_config.sink || length(array(log_config.sink)) == 0) { log_config = { sink: { terminal: { @@ -787,6 +805,10 @@ log = function(name, args) { // Wire C-level JS_Log through the ƿit log system actor_mod.set_log(log) +// Let the bootstrap log forward to the full system — modules loaded early +// (before the full log was ready) captured the bootstrap function reference. +_log_full = log + var pronto = use_core('pronto') var fallback = pronto.fallback var parallel = pronto.parallel @@ -1402,27 +1424,38 @@ 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(_ => { - 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 @@ -1442,18 +1475,25 @@ $_.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.sync(_deps[_di]) + } disruption { + log.error('failed to install dependency: ' + _deps[_di]) + disrupt + } + _auto_install() } _di = _di + 1 } @@ -1461,10 +1501,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/os.c b/internal/os.c index 0e48db47..c26b5dcc 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 aa8a539b..15265a7d 100644 --- a/internal/shop.cm +++ b/internal/shop.cm @@ -40,20 +40,6 @@ function put_into_cache(content, obj) fd.slurpwrite(path, obj) } -function ensure_dir(path) { - if (fd.stat(path).isDirectory) return - var parts = array(path, '/') - var current = starts_with(path, '/') ? '/' : '' - var i = 0 - for (i = 0; i < length(parts); i++) { - if (parts[i] == '') continue - current = current + parts[i] + '/' - if (!fd.stat(current).isDirectory) { - fd.mkdir(current) - } - } -} - function hash_path(content, salt) { var s = salt || 'mach' @@ -88,11 +74,6 @@ Shop.get_core_dir = function() { return get_packages_dir() + '/' + core_package } -// Get the links file path (in the global shop) -function get_links_path() { - return global_shop_path + '/link.toml' -} - // Get the reports directory (in the global shop) Shop.get_reports_dir = function() { return global_shop_path + '/reports' @@ -131,15 +112,12 @@ function split_explicit_package_import(path) mod_path = text(array(parts, i), '/') if (!mod_path || length(mod_path) == 0) continue - candidate_dir = get_packages_dir() + '/' + safe_package_path(pkg_candidate) + candidate_dir = get_packages_dir() + '/' + fd.safe_package_path(pkg_candidate) if (fd.is_file(candidate_dir + '/cell.toml')) return {package: pkg_candidate, path: mod_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 +136,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 +156,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) @@ -256,15 +240,15 @@ function get_canonical_package(alias, package_context) { return null } -// return the safe path for the package -// guaranteed to be validated -function safe_package_path(pkg) -{ - // For absolute paths, replace / with _ to create a valid directory name - // Also replace @ with _ - if (pkg && starts_with(pkg, '/')) - return replace(replace(pkg, '/', '_'), '@', '_') - return replace(pkg, '@', '_') +// Resolve a locator string to its canonical form +// Handles '.', './', '../', and existing directory paths +Shop.resolve_locator = function(locator) { + var resolved = null + if (locator == '.' || starts_with(locator, './') || starts_with(locator, '../') || fd.is_dir(locator)) { + resolved = fd.realpath(locator) + if (resolved) return resolved + } + return locator } function package_cache_path(pkg) @@ -294,7 +278,8 @@ Shop.load_lock = function() { // Save lock.toml configuration (to global shop) Shop.save_lock = function(lock) { var path = global_shop_path + '/lock.toml' - fd.slurpwrite(path, stone(blob(toml.encode(lock)))); + fd.slurpwrite(path, stone(blob(toml.encode(lock)))) + _lock = lock } @@ -344,9 +329,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 } @@ -442,7 +429,7 @@ function try_native_mod_dylib(pkg, stem) { var build_mod = use_cache['core/build'] if (!build_mod) return null - var src_path = get_packages_dir() + '/' + safe_package_path(pkg) + '/' + stem + var src_path = get_packages_dir() + '/' + fd.safe_package_path(pkg) + '/' + stem if (!fd.is_file(src_path)) return null var src = text(fd.slurp(src_path)) @@ -660,7 +647,7 @@ Shop.all_script_paths = function() { packages = array(packages, ['core']) } for (i = 0; i < length(packages); i++) { - pkg_dir = starts_with(packages[i], '/') ? packages[i] : get_packages_dir() + '/' + safe_package_path(packages[i]) + pkg_dir = starts_with(packages[i], '/') ? packages[i] : get_packages_dir() + '/' + fd.safe_package_path(packages[i]) scripts = get_package_scripts(packages[i]) for (j = 0; j < length(scripts); j++) { result[] = { @@ -705,7 +692,7 @@ function resolve_mod_fn(path, pkg) { // Compute _pkg_dir and _stem early so all paths can use them if (pkg) { - _pkg_dir = get_packages_dir() + '/' + safe_package_path(pkg) + _pkg_dir = get_packages_dir() + '/' + fd.safe_package_path(pkg) if (starts_with(path, _pkg_dir + '/')) { _stem = text(path, length(_pkg_dir) + 1) } @@ -767,7 +754,7 @@ function resolve_mod_fn(path, pkg) { mcode_json = shop_json.encode(optimized) // Cache mcode (architecture-independent) in content-addressed store - ensure_dir(global_shop_path + '/build') + fd.ensure_dir(global_shop_path + '/build') fd.slurpwrite(cached_mcode_path, stone(blob(mcode_json))) // Cache mach blob @@ -797,13 +784,18 @@ 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) explicit = null } if (explicit) { - explicit_path = get_packages_dir() + '/' + safe_package_path(explicit.package) + '/' + explicit.path + explicit_path = get_packages_dir() + '/' + fd.safe_package_path(explicit.package) + '/' + explicit.path if (fd.is_file(explicit_path)) return {path: explicit_path, scope: SCOPE_PACKAGE, pkg: explicit.package} } @@ -819,7 +811,7 @@ function resolve_path(path, ctx) if (starts_with(ctx, '/')) ctx_dir = ctx else - ctx_dir = get_packages_dir() + '/' + safe_package_path(ctx) + ctx_dir = get_packages_dir() + '/' + fd.safe_package_path(ctx) ctx_path = ctx_dir + '/' + path if (fd.is_file(ctx_path)) { @@ -833,15 +825,34 @@ function resolve_path(path, ctx) alias = pkg_tools.split_alias(ctx, path) if (alias) { - alias_path = get_packages_dir() + '/' + safe_package_path(alias.package) + '/' + alias.path + alias_path = get_packages_dir() + '/' + fd.safe_package_path(alias.package) + '/' + alias.path if (fd.is_file(alias_path)) return {path: alias_path, scope: SCOPE_PACKAGE, pkg: ctx} } - package_path = get_packages_dir() + '/' + safe_package_path(path) + package_path = get_packages_dir() + '/' + fd.safe_package_path(path) 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() + '/' + fd.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)) @@ -1254,7 +1265,7 @@ function get_module(path, package_context) { if (!info) { log.shop(`Module '${path}' could not be found in package '${package_context}'`) - _ctx_dir = package_context ? (starts_with(package_context, '/') ? package_context : get_packages_dir() + '/' + safe_package_path(package_context)) : null + _ctx_dir = package_context ? (starts_with(package_context, '/') ? package_context : get_packages_dir() + '/' + fd.safe_package_path(package_context)) : null if (_ctx_dir) { if (fd.is_file(_ctx_dir + '/' + path + '.c') || fd.is_file(_ctx_dir + '/' + path + '.cpp')) log.shop(`C source exists at ${_ctx_dir}/${path}.c but was not compiled - run 'cell build'`) @@ -1290,7 +1301,7 @@ Shop.use = function use(path, package_context) { var _alias2 = null if (!info) { log.shop(`Module '${path}' could not be found in package '${package_context}'`) - _ctx_dir2 = package_context ? (starts_with(package_context, '/') ? package_context : get_packages_dir() + '/' + safe_package_path(package_context)) : null + _ctx_dir2 = package_context ? (starts_with(package_context, '/') ? package_context : get_packages_dir() + '/' + fd.safe_package_path(package_context)) : null if (_ctx_dir2) { if (fd.is_file(_ctx_dir2 + '/' + path + '.c') || fd.is_file(_ctx_dir2 + '/' + path + '.cpp')) log.shop(`C source exists at ${_ctx_dir2}/${path}.c but was not compiled - run 'cell build'`) @@ -1307,8 +1318,6 @@ Shop.use = function use(path, package_context) { return use_cache[info.cache_key] } -Shop.resolve_locator = resolve_locator - // Resolve a use() module path to a filesystem path without compiling. // Returns the absolute path string, or null if not found. Shop.resolve_use_path = function(path, ctx) { @@ -1317,6 +1326,56 @@ 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 + + // Find best matching package from lock or infer from path + var lock = Shop.load_lock() + var best_pkg = null + var best_remainder = null + var parts = array(prog, '/') + var candidate = null + var pkg_info = 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, try gitea-style 3-component package (host/owner/repo) + 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) return null + + // Auto-install the package and all its dependencies + log.console('fetching ' + best_pkg + '...') + _auto = function() { + Shop.sync_with_deps(best_pkg) + } disruption { + return null + } + _auto() + + info = resolve_path(prog + '.ce', package_context) + return info +} + // 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) { @@ -1346,7 +1405,7 @@ function get_cache_path(pkg, commit) { function get_package_abs_dir(package) { - return get_packages_dir() + '/' + safe_package_path(package) + return get_packages_dir() + '/' + fd.safe_package_path(package) } // Fetch the latest commit hash from remote for a package @@ -1370,7 +1429,7 @@ function fetch_remote_hash(pkg) { // Returns the zip blob or null on failure function download_zip(pkg, commit_hash) { var cache_path = get_cache_path(pkg, commit_hash) - ensure_dir(global_shop_path + '/cache') + fd.ensure_dir(global_shop_path + '/cache') var download_url = Shop.get_download_url(pkg, commit_hash) if (!download_url) { @@ -1429,7 +1488,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') @@ -1537,17 +1596,23 @@ 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) - log.console(`checking ${pkg}`) + if (!info) { + log.error("Not a valid package locator: " + pkg) + return null + } + + 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 @@ -1563,8 +1628,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) @@ -1586,6 +1651,82 @@ Shop.update = function(pkg) { return new_entry } +// Sync a package: ensure it's in lock, fetched, extracted, and compiled +// opts.refresh - check remote for updates even if lock entry exists +// opts.no_build - skip C module build step +// opts.target - explicit build target (auto-detected if not provided) +// opts.buildtype - 'release'|'debug'|'minsize' (default 'release') +Shop.sync = function(pkg, opts) { + var lock = Shop.load_lock() + var info = Shop.resolve_package_info(pkg) + var build_mod = null + var target = null + var _build_c = null + + // Step 1: Ensure lock entry (update if refresh or not in lock) + if ((opts && opts.refresh) || !lock[pkg]) + Shop.update(pkg) + + // Step 2: Fetch zip (no-op for local packages) + if (info && info != 'local') + Shop.fetch(pkg) + + // Step 3: Extract to packages dir + Shop.extract(pkg) + + // Step 4: Compile scripts + Shop.build_package_scripts(pkg) + + // Step 5: Build C modules + if (!opts || !opts.no_build) { + build_mod = use_cache['core/build'] + if (build_mod) { + target = (opts && opts.target) ? opts.target : build_mod.detect_host_target() + _build_c = function() { + build_mod.build_dynamic(pkg, target, (opts && opts.buildtype) ? opts.buildtype : 'release') + } disruption { + // Not all packages have C code + } + _build_c() + } + } +} + +// Sync a package and all its dependencies (BFS) +Shop.sync_with_deps = function(pkg, opts) { + var visited = {} + var queue = [pkg] + var qi = 0 + var current = null + var deps = null + var dep_locator = null + var _read_deps = null + + while (qi < length(queue)) { + current = queue[qi] + qi = qi + 1 + if (visited[current]) continue + visited[current] = true + + Shop.sync(current, opts) + + _read_deps = function() { + deps = pkg_tools.dependencies(current) + } disruption { + deps = null + } + _read_deps() + + if (deps) { + arrfor(array(deps), function(alias) { + dep_locator = deps[alias] + if (!visited[dep_locator]) + push(queue, dep_locator) + }) + } + } +} + function install_zip(zip_blob, target_dir) { var zip = miniz.read(zip_blob) if (!zip) { print("Failed to read zip archive"); disrupt } @@ -1593,8 +1734,8 @@ 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) - ensure_dir(target_dir) + log.shop("Extracting to " + target_dir) + fd.ensure_dir(target_dir) var count = zip.count() var created_dirs = {} @@ -1617,7 +1758,7 @@ function install_zip(zip_blob, target_dir) { dir_path = fd.dirname(full_path) if (!created_dirs[dir_path]) { - ensure_dir(dir_path) + fd.ensure_dir(dir_path) created_dirs[dir_path] = true } file_data = zip.slurp(filename) @@ -1638,7 +1779,7 @@ Shop.remove = function(pkg) { } // Remove package symlink/directory - var pkg_dir = get_packages_dir() + '/' + safe_package_path(pkg) + var pkg_dir = get_packages_dir() + '/' + fd.safe_package_path(pkg) if (fd.is_link(pkg_dir)) { fd.unlink(pkg_dir) } else if (fd.is_dir(pkg_dir)) { @@ -1652,34 +1793,6 @@ Shop.remove = function(pkg) { return true } -Shop.get = function(pkg) { - var lock = Shop.load_lock() - var info = null - var commit = null - - if (!lock[pkg]) { - info = Shop.resolve_package_info(pkg) - if (!info) { - print("Invalid package: " + pkg); disrupt - } - - commit = null - if (info != 'local') { - commit = fetch_remote_hash(pkg) - if (!commit) { - print("Could not resolve commit for " + pkg); disrupt - } - } - - lock[pkg] = { - type: info, - commit: commit, - updated: time.number() - } - Shop.save_lock(lock) - } -} - // Compile a module // List all files in a package @@ -1756,6 +1869,7 @@ Shop.build_package_scripts = function(package) ok = ok + 1 } disruption { push(errors, script) + log.console(" compile error: " + package + '/' + script) } _try() }) @@ -1776,7 +1890,8 @@ Shop.get_lib_dir = function() { return global_shop_path + '/lib' } -Shop.ensure_dir = ensure_dir +Shop.ensure_dir = fd.ensure_dir +Shop.install_zip = install_zip Shop.ensure_package_dylibs = ensure_package_dylibs Shop.get_local_dir = function() { @@ -1790,7 +1905,7 @@ Shop.get_build_dir = function() { // Get the absolute path for a package Shop.get_package_dir = function(pkg) { - return get_packages_dir() + '/' + safe_package_path(pkg) + return get_packages_dir() + '/' + fd.safe_package_path(pkg) } // Generate C symbol name for a file within a package @@ -1811,7 +1926,7 @@ Shop.c_symbol_prefix = function(pkg) { // Get the library directory name for a package Shop.lib_name_for_package = function(pkg) { - return safe_package_path(pkg) + return fd.safe_package_path(pkg) } // Load a module explicitly as mach bytecode, bypassing dylib resolution. @@ -1865,7 +1980,7 @@ Shop.load_as_mach = function(path, pkg) { optimized = _streamline_mod(ir) mcode_json = shop_json.encode(optimized) cached_mcode_path = hash_path(content_key, 'mcode') - ensure_dir(global_shop_path + '/build') + fd.ensure_dir(global_shop_path + '/build') fd.slurpwrite(cached_mcode_path, stone(blob(mcode_json))) compiled = mach_compile_mcode_bin(file_path, mcode_json) put_into_cache(content_key, compiled) @@ -1902,7 +2017,7 @@ Shop.load_as_dylib = function(path, pkg) { } if (!real_pkg) return null - pkg_dir = get_packages_dir() + '/' + safe_package_path(real_pkg) + pkg_dir = get_packages_dir() + '/' + fd.safe_package_path(real_pkg) if (!starts_with(file_path, pkg_dir + '/')) return null stem = text(file_path, length(pkg_dir) + 1) result = try_native_mod_dylib(real_pkg, stem) diff --git a/internal/testlib.cm b/internal/testlib.cm index 743db329..03d1709e 100644 --- a/internal/testlib.cm +++ b/internal/testlib.cm @@ -33,26 +33,54 @@ function get_pkg_dir(package_name) { return shop.get_package_dir(package_name) } -// Ensure directory exists -function ensure_dir(path) { - if (fd.is_dir(path)) return true - - var parts = array(path, '/') - var current = starts_with(path, '/') ? '/' : '' +// Deep comparison of two values (handles arrays and objects) +function values_equal(a, b) { var i = 0 - for (i = 0; i < length(parts); i++) { - if (parts[i] == '') continue - current = current + parts[i] + '/' - if (!fd.is_dir(current)) { - fd.mkdir(current) + var ka = null + var kb = null + if (a == b) return true + if (is_null(a) && is_null(b)) return true + if (is_null(a) || is_null(b)) return false + if (is_array(a) && is_array(b)) { + if (length(a) != length(b)) return false + i = 0 + while (i < length(a)) { + if (!values_equal(a[i], b[i])) return false + i = i + 1 } + return true } - return true + if (is_object(a) && is_object(b)) { + ka = array(a) + kb = array(b) + if (length(ka) != length(kb)) return false + i = 0 + while (i < length(ka)) { + if (!values_equal(a[ka[i]], b[ka[i]])) return false + i = i + 1 + } + return true + } + return false +} + +// Describe a value for error messages +function describe(val) { + if (is_null(val)) return "null" + if (is_text(val)) return `"${val}"` + if (is_number(val)) return text(val) + if (is_logical(val)) return text(val) + if (is_function(val)) return "" + if (is_array(val)) return `[array length=${text(length(val))}]` + if (is_object(val)) return `{record keys=${text(length(array(val)))}}` + return "" } return { is_valid_package: is_valid_package, get_current_package_name: get_current_package_name, get_pkg_dir: get_pkg_dir, - ensure_dir: ensure_dir + ensure_dir: fd.ensure_dir, + values_equal: values_equal, + describe: describe } diff --git a/link.ce b/link.ce index 2c552a5e..0f766e0e 100644 --- a/link.ce +++ b/link.ce @@ -28,24 +28,24 @@ var target = null var start_idx = 0 var arg1 = null var arg2 = null -var cwd = null var toml_path = null var content = null var _restore = null var _read_toml = null var _add_link = null -if (length(args) < 1) { - log.console("Usage: link [args] or link [package] ") - log.console("Commands:") - log.console(" list List all active links") - log.console(" sync Ensure all symlinks are in place") - log.console(" delete Remove a link and restore original") - log.console(" clear Remove all links") - log.console(" Link the package in to that path") - log.console(" Link to (path or package)") - $stop() -} +var run = function() { + if (length(args) < 1) { + log.console("Usage: link [args] or link [package] ") + log.console("Commands:") + log.console(" list List all active links") + log.console(" sync Ensure all symlinks are in place") + log.console(" delete Remove a link and restore original") + log.console(" clear Remove all links") + log.console(" Link the package in to that path") + log.console(" Link to (path or package)") + return + } cmd = args[0] @@ -72,7 +72,7 @@ if (cmd == 'list') { } else if (cmd == 'delete' || cmd == 'rm') { if (length(args) < 2) { log.console("Usage: link delete ") - $stop() + return } pkg = args[1] @@ -114,7 +114,7 @@ if (cmd == 'list') { if (!arg1) { log.console("Error: target or package required") - $stop() + return } if (arg2) { @@ -123,37 +123,21 @@ if (cmd == 'list') { target = arg2 // Resolve target if it's a local path - if (target == '.' || fd.is_dir(target)) { - target = fd.realpath(target) - } else if (starts_with(target, './') || starts_with(target, '../')) { - // Relative path that doesn't exist yet - try to resolve anyway - cwd = fd.realpath('.') - if (starts_with(target, './')) { - target = cwd + text(target, 1) - } else { - // For ../ paths, let fd.realpath handle it if possible - target = fd.realpath(target) || target - } - } - // Otherwise target is a package name (e.g., github.com/prosperon) + target = shop.resolve_locator(target) } else { // One argument: assume it's a local path, infer package name from cell.toml target = arg1 // Resolve path - if (target == '.' || fd.is_dir(target)) { - target = fd.realpath(target) - } else if (starts_with(target, './') || starts_with(target, '../')) { - target = fd.realpath(target) || target - } + target = shop.resolve_locator(target) // Must be a local path with cell.toml toml_path = target + '/cell.toml' if (!fd.is_file(toml_path)) { log.console("Error: No cell.toml found at " + target) log.console("For linking to another package, use: link ") - $stop() + return } // Read package name from cell.toml @@ -176,7 +160,7 @@ if (cmd == 'list') { if (starts_with(target, '/')) { if (!fd.is_file(target + '/cell.toml')) { log.console("Error: " + target + " is not a valid package (no cell.toml)") - $stop() + return } } @@ -189,5 +173,7 @@ if (cmd == 'list') { } _add_link() } +} +run() $stop() diff --git a/link.cm b/link.cm index b3fba589..71a6cf81 100644 --- a/link.cm +++ b/link.cm @@ -20,30 +20,8 @@ function get_packages_dir() { return global_shop_path + '/packages' } -// return the safe path for the package -function safe_package_path(pkg) { - // For absolute paths, replace / with _ to create a valid directory name - if (pkg && starts_with(pkg, '/')) - return replace(replace(pkg, '/', '_'), '@', '_') - return replace(pkg, '@', '_') -} - function get_package_abs_dir(package) { - return get_packages_dir() + '/' + safe_package_path(package) -} - -function ensure_dir(path) { - if (fd.stat(path).isDirectory) return - var parts = array(path, '/') - var current = starts_with(path, '/') ? '/' : '' - var i = 0 - for (i = 0; i < length(parts); i++) { - if (parts[i] == '') continue - current = current + parts[i] + '/' - if (!fd.stat(current).isDirectory) { - fd.mkdir(current) - } - } + return get_packages_dir() + '/' + fd.safe_package_path(package) } // Resolve a link target to its actual path @@ -54,7 +32,7 @@ function resolve_link_target(target) { return target } // Target is another package - resolve to its directory - return get_packages_dir() + '/' + safe_package_path(target) + return get_packages_dir() + '/' + fd.safe_package_path(target) } var Link = {} @@ -194,7 +172,7 @@ Link.sync_one = function(canonical, target, shop) { // Ensure parent directories exist var parent = fd.dirname(target_dir) - ensure_dir(parent) + fd.ensure_dir(parent) // Check current state var current_link = null diff --git a/list.ce b/list.ce index 262f873a..362b95b2 100644 --- a/list.ce +++ b/list.ce @@ -8,11 +8,9 @@ var shop = use('internal/shop') var pkg = use('package') var link = use('link') -var fd = use('fd') var mode = 'local' var target_pkg = null -var resolved = null var i = 0 var deps = null var packages = null @@ -20,37 +18,32 @@ var local_pkgs = null var linked_pkgs = null var remote_pkgs = null -if (args && length(args) > 0) { - if (args[0] == 'shop') { - mode = 'shop' - } else if (args[0] == '--help' || args[0] == '-h') { - log.console("Usage: cell list []") - log.console("") - log.console("List packages and dependencies.") - log.console("") - log.console("Scopes:") - log.console(" (none) List dependencies of current package") - log.console(" shop List all packages in shop with status") - log.console(" List dependency tree for a package") - $stop() - } else { - mode = 'package' - target_pkg = args[0] +var run = function() { + if (args && length(args) > 0) { + if (args[0] == 'shop') { + mode = 'shop' + } else if (args[0] == '--help' || args[0] == '-h') { + log.console("Usage: cell list []") + log.console("") + log.console("List packages and dependencies.") + log.console("") + log.console("Scopes:") + log.console(" (none) List dependencies of current package") + log.console(" shop List all packages in shop with status") + log.console(" List dependency tree for a package") + return + } else { + mode = 'package' + target_pkg = args[0] - // Resolve local paths - if (target_pkg == '.' || starts_with(target_pkg, './') || starts_with(target_pkg, '../') || fd.is_dir(target_pkg)) { - resolved = fd.realpath(target_pkg) - if (resolved) { - target_pkg = resolved - } + target_pkg = shop.resolve_locator(target_pkg) } } -} -var links = link.load() -var lock = shop.load_lock() + var links = link.load() + var lock = shop.load_lock() -function print_deps(ctx, raw_indent) { + function print_deps(ctx, raw_indent) { var aliases = null var indent = raw_indent || "" deps = null @@ -181,5 +174,7 @@ if (mode == 'local') { log.console("Total: " + text(length(packages)) + " package(s)") } } +} +run() $stop() diff --git a/log.ce b/log.ce index 8428a7a5..7caac4f3 100644 --- a/log.ce +++ b/log.ce @@ -1,12 +1,15 @@ // cell log - Manage and read log sinks // // Usage: -// cell log list List configured sinks -// cell log add console [opts] Add a console sink +// cell log list List configured sinks +// cell log add console [opts] Add a console sink // cell log add file [opts] Add a file sink -// cell log remove Remove a sink -// cell log read [opts] Read from a file sink -// cell log tail [--lines=N] Follow a file sink +// cell log remove Remove a sink +// cell log read [opts] Read from a file sink +// cell log tail [--lines=N] Follow a file sink +// +// The --stack option controls which channels capture a stack trace. +// Default: --stack=error (errors always show a stack trace). var toml = use('toml') var fd = use('fd') @@ -53,6 +56,7 @@ function print_help() { log.console(" --format=pretty|bare|json Output format (default: pretty for console, json for file)") log.console(" --channels=ch1,ch2 Channels to subscribe (default: console,error,system)") log.console(" --exclude=ch1,ch2 Channels to exclude (for wildcard sinks)") + log.console(" --stack=ch1,ch2 Channels that capture a stack trace (default: error)") log.console("") log.console("Options for read:") log.console(" --lines=N Show last N lines (default: all)") @@ -80,21 +84,22 @@ function format_entry(entry) { function do_list() { var config = load_config() var names = null - if (!config || !config.sink) { + names = (config && config.sink) ? array(config.sink) : [] + if (length(names) == 0) { log.console("No log sinks configured.") - log.console("Default: console pretty for console/error/system") + log.console("Default: console pretty for console/error/system (stack traces on error)") return } - names = array(config.sink) arrfor(names, function(n) { var s = config.sink[n] var ch = is_array(s.channels) ? text(s.channels, ', ') : '(none)' var ex = is_array(s.exclude) ? " exclude=" + text(s.exclude, ',') : "" + var stk = is_array(s.stack) ? " stack=" + text(s.stack, ',') : "" var fmt = s.format || (s.type == 'file' ? 'json' : 'pretty') if (s.type == 'file') - log.console(" " + n + ": " + s.type + " -> " + s.path + " [" + ch + "] format=" + fmt + ex) + log.console(" " + n + ": " + s.type + " -> " + s.path + " [" + ch + "] format=" + fmt + ex + stk) else - log.console(" " + n + ": " + s.type + " [" + ch + "] format=" + fmt + ex) + log.console(" " + n + ": " + s.type + " [" + ch + "] format=" + fmt + ex + stk) }) } @@ -105,6 +110,7 @@ function do_add() { var format = null var channels = ["console", "error", "system"] var exclude = null + var stack_chs = ["error"] var config = null var val = null var i = 0 @@ -138,13 +144,15 @@ function do_add() { if (val) { channels = array(val, ','); continue } val = parse_opt(args[i], 'exclude') if (val) { exclude = array(val, ','); continue } + val = parse_opt(args[i], 'stack') + if (val) { stack_chs = array(val, ','); continue } } config = load_config() if (!config) config = {} if (!config.sink) config.sink = {} - config.sink[name] = {type: sink_type, format: format, channels: channels} + config.sink[name] = {type: sink_type, format: format, channels: channels, stack: stack_chs} if (path) config.sink[name].path = path if (exclude) config.sink[name].exclude = exclude @@ -165,7 +173,7 @@ function do_remove() { log.error("Sink not found: " + name) return } - config.sink[name] = null + delete config.sink[name] save_config(config) log.console("Removed sink: " + name) } diff --git a/package.cm b/package.cm index 506006c4..6c8f9d9a 100644 --- a/package.cm +++ b/package.cm @@ -6,16 +6,6 @@ var link = use('link') var global_shop_path = runtime.shop_path -// Convert package name to a safe directory name -// For absolute paths (local packages), replace / with _ -// For remote packages, keep slashes as they use nested directories -function safe_package_path(pkg) { - if (!pkg) return pkg - if (starts_with(pkg, '/')) - return replace(replace(pkg, '/', '_'), '@', '_') - return replace(pkg, '@', '_') -} - function get_path(name) { // If name is null, return the current project directory @@ -33,11 +23,11 @@ function get_path(name) if (starts_with(link_target, '/')) return link_target // Otherwise it's another package name, resolve that - return global_shop_path + '/packages/' + replace(replace(link_target, '/', '_'), '@', '_') + return global_shop_path + '/packages/' + fd.safe_package_path(link_target) } // Remote packages use nested directories, so don't transform slashes - return global_shop_path + '/packages/' + replace(name, '@', '_') + return global_shop_path + '/packages/' + fd.safe_package_path(name) } var config_cache = {} @@ -202,6 +192,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/remove.ce b/remove.ce index 09f6d76d..0464871d 100644 --- a/remove.ce +++ b/remove.ce @@ -17,95 +17,91 @@ var target_pkg = null var prune = false var dry_run = false var i = 0 -var resolved = null -for (i = 0; i < length(args); i++) { - if (args[i] == '--prune') { - prune = true - } else if (args[i] == '--dry-run') { - dry_run = true - } else if (args[i] == '--help' || args[i] == '-h') { +var run = function() { + for (i = 0; i < length(args); i++) { + if (args[i] == '--prune') { + prune = true + } else if (args[i] == '--dry-run') { + dry_run = true + } else if (args[i] == '--help' || args[i] == '-h') { + log.console("Usage: cell remove [options]") + log.console("") + log.console("Remove a package from the shop.") + log.console("") + log.console("Options:") + log.console(" --prune Also remove packages no longer needed by any root") + log.console(" --dry-run Show what would be removed") + return + } else if (!starts_with(args[i], '-')) { + target_pkg = args[i] + } + } + + if (!target_pkg) { log.console("Usage: cell remove [options]") - log.console("") - log.console("Remove a package from the shop.") - log.console("") - log.console("Options:") - log.console(" --prune Also remove packages no longer needed by any root") - log.console(" --dry-run Show what would be removed") - $stop() - } else if (!starts_with(args[i], '-')) { - target_pkg = args[i] + return + } + + target_pkg = shop.resolve_locator(target_pkg) + + var packages_to_remove = [target_pkg] + + var lock = null + var all_packages = null + var needed = null + if (prune) { + // Find packages no longer needed + // Get all dependencies of remaining packages + lock = shop.load_lock() + all_packages = shop.list_packages() + + // Build set of all needed packages (excluding target) + needed = {} + arrfor(all_packages, function(p) { + if (p == target_pkg || p == 'core') return + + // Mark this package and its deps as needed + needed[p] = true + var _gather = function() { + var deps = pkg.gather_dependencies(p) + arrfor(deps, function(dep) { + needed[dep] = true + }) + } disruption { + // Skip if can't read deps + } + _gather() + }) + + // Find packages that are NOT needed + arrfor(all_packages, function(p) { + if (p == 'core') return + if (!needed[p] && find(packages_to_remove, p) == null) { + push(packages_to_remove, p) + } + }) + } + + if (dry_run) { + log.console("Would remove:") + arrfor(packages_to_remove, function(p) { + log.console(" " + p) + }) + } else { + arrfor(packages_to_remove, function(p) { + // Remove any link for this package + if (link.is_linked(p)) { + link.remove(p) + } + + // Remove from shop + shop.remove(p) + }) + + log.console("Removed " + text(length(packages_to_remove)) + " package(s).") } } - -if (!target_pkg) { - log.console("Usage: cell remove [options]") - $stop() -} - -// Resolve relative paths to absolute paths -if (target_pkg == '.' || starts_with(target_pkg, './') || starts_with(target_pkg, '../') || fd.is_dir(target_pkg)) { - resolved = fd.realpath(target_pkg) - if (resolved) { - target_pkg = resolved - } -} - -var packages_to_remove = [target_pkg] - -var lock = null -var all_packages = null -var needed = null -if (prune) { - // Find packages no longer needed - // Get all dependencies of remaining packages - lock = shop.load_lock() - all_packages = shop.list_packages() - - // Build set of all needed packages (excluding target) - needed = {} - arrfor(all_packages, function(p) { - if (p == target_pkg || p == 'core') return - - // Mark this package and its deps as needed - needed[p] = true - var _gather = function() { - var deps = pkg.gather_dependencies(p) - arrfor(deps, function(dep) { - needed[dep] = true - }) - } disruption { - // Skip if can't read deps - } - _gather() - }) - - // Find packages that are NOT needed - arrfor(all_packages, function(p) { - if (p == 'core') return - if (!needed[p] && find(packages_to_remove, p) == null) { - push(packages_to_remove, p) - } - }) -} - -if (dry_run) { - log.console("Would remove:") - arrfor(packages_to_remove, function(p) { - log.console(" " + p) - }) -} else { - arrfor(packages_to_remove, function(p) { - // Remove any link for this package - if (link.is_linked(p)) { - link.remove(p) - } - - // Remove from shop - shop.remove(p) - }) - - log.console("Removed " + text(length(packages_to_remove)) + " package(s).") -} +run() $stop() diff --git a/resolve.ce b/resolve.ce index e67dba7f..e2e94a22 100644 --- a/resolve.ce +++ b/resolve.ce @@ -21,34 +21,34 @@ var target_triple = null var show_locked = false var refresh_first = false var i = 0 -var resolved = null -for (i = 0; i < length(args); i++) { - if (args[i] == '--target' || args[i] == '-t') { - if (i + 1 < length(args)) { - target_triple = args[++i] - } else { - log.error('--target requires a triple') - $stop() +var run = function() { + for (i = 0; i < length(args); i++) { + if (args[i] == '--target' || args[i] == '-t') { + if (i + 1 < length(args)) { + target_triple = args[++i] + } else { + log.error('--target requires a triple') + return + } + } else if (args[i] == '--locked') { + show_locked = true + } else if (args[i] == '--refresh') { + refresh_first = true + } else if (args[i] == '--help' || args[i] == '-h') { + log.console("Usage: cell resolve [] [options]") + log.console("") + log.console("Print the fully resolved dependency closure.") + log.console("") + log.console("Options:") + log.console(" --target Annotate builds for target platform") + log.console(" --locked Show lock state without applying links") + log.console(" --refresh Refresh floating refs before printing") + return + } else if (!starts_with(args[i], '-')) { + target_locator = args[i] } - } else if (args[i] == '--locked') { - show_locked = true - } else if (args[i] == '--refresh') { - refresh_first = true - } else if (args[i] == '--help' || args[i] == '-h') { - log.console("Usage: cell resolve [] [options]") - log.console("") - log.console("Print the fully resolved dependency closure.") - log.console("") - log.console("Options:") - log.console(" --target Annotate builds for target platform") - log.console(" --locked Show lock state without applying links") - log.console(" --refresh Refresh floating refs before printing") - $stop() - } else if (!starts_with(args[i], '-')) { - target_locator = args[i] } -} // Default to current directory if (!target_locator) { @@ -56,23 +56,18 @@ if (!target_locator) { } // Resolve local paths -if (target_locator == '.' || starts_with(target_locator, './') || starts_with(target_locator, '../') || fd.is_dir(target_locator)) { - resolved = fd.realpath(target_locator) - if (resolved) { - target_locator = resolved - } -} +target_locator = shop.resolve_locator(target_locator) -// Check if it's a valid package -var pkg_dir = null -if (!fd.is_file(target_locator + '/cell.toml')) { - // Try to find it in the shop - pkg_dir = shop.get_package_dir(target_locator) - if (!fd.is_file(pkg_dir + '/cell.toml')) { - log.error("Not a valid package: " + target_locator) - $stop() + // Check if it's a valid package + var pkg_dir = null + if (!fd.is_file(target_locator + '/cell.toml')) { + // Try to find it in the shop + pkg_dir = shop.get_package_dir(target_locator) + if (!fd.is_file(pkg_dir + '/cell.toml')) { + log.error("Not a valid package: " + target_locator) + return + } } -} // Detect target if not specified if (!target_triple) { @@ -216,7 +211,9 @@ for (i = 0; i < length(sorted); i++) { } } -log.console("") -log.console("Total: " + text(length(sorted)) + " package(s)") + log.console("") + log.console("Total: " + text(length(sorted)) + " package(s)") +} +run() $stop() diff --git a/source/runtime.c b/source/runtime.c index 3dbc3294..fb2b2181 100644 --- a/source/runtime.c +++ b/source/runtime.c @@ -8093,6 +8093,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)) { diff --git a/test.ce b/test.ce index 6cfe1a84..0e2e9842 100644 --- a/test.ce +++ b/test.ce @@ -7,6 +7,7 @@ var time = use('time') var json = use('json') var blob = use('blob') var dbg = use('js') +var testlib = use('internal/testlib') // run gc with dbg.gc() @@ -29,23 +30,8 @@ def ACTOR_TEST_TIMEOUT = 30000 // 30 seconds timeout for actor tests var pending_actor_tests = [] var actor_test_results = [] -// Check if current directory is a valid cell package -function is_valid_package(dir) { - var _dir = dir == null ? '.' : dir - return fd.is_file(_dir + '/cell.toml') -} - -// Get current package name from cell.toml or null -function get_current_package_name() { - if (!is_valid_package('.')) return null - var _load = function() { - var config = pkg.load_config(null) - return config.package || 'local' - } disruption { - return 'local' - } - return _load() -} +var is_valid_package = testlib.is_valid_package +var get_current_package_name = testlib.get_current_package_name // Parse arguments // Usage: @@ -192,32 +178,8 @@ if (diff_mode && !run_ast_noopt_fn) { return } -// Diff mode: deep comparison helper -function values_equal(a, b) { - var i = 0 - if (a == b) return true - if (is_null(a) && is_null(b)) return true - if (is_null(a) || is_null(b)) return false - if (is_array(a) && is_array(b)) { - if (length(a) != length(b)) return false - i = 0 - while (i < length(a)) { - if (!values_equal(a[i], b[i])) return false - i = i + 1 - } - return true - } - return false -} - -function describe(val) { - if (is_null(val)) return "null" - if (is_text(val)) return `"${val}"` - if (is_number(val)) return text(val) - if (is_logical(val)) return text(val) - if (is_function(val)) return "" - return "" -} +var values_equal = testlib.values_equal +var describe = testlib.describe // Diff mode: run a test function through noopt and compare var diff_mismatches = 0 @@ -251,31 +213,8 @@ function diff_check(test_name, file_path, opt_fn, noopt_fn) { } } -function ensure_dir(path) { - if (fd.is_dir(path)) return true - - var parts = array(path, '/') - var current = starts_with(path, '/') ? '/' : '' - var i = 0 - for (i = 0; i < length(parts); i++) { - if (parts[i] == '') continue - current = current + parts[i] + '/' - if (!fd.is_dir(current)) - fd.mkdir(current) - } - return true -} - -// Get the directory for a package -function get_pkg_dir(package_name) { - if (!package_name) { - return fd.realpath('.') - } - if (starts_with(package_name, '/')) { - return package_name - } - return shop.get_package_dir(package_name) -} +var ensure_dir = fd.ensure_dir +var get_pkg_dir = testlib.get_pkg_dir // Collect .ce actor tests from a package function collect_actor_tests(package_name, specific_test) { diff --git a/update.ce b/update.ce index ceb395c5..7a00284a 100644 --- a/update.ce +++ b/update.ce @@ -1,84 +1,69 @@ // cell update [] - Update packages from remote sources // // Usage: -// cell update Update all packages in shop +// cell update Update all packages // cell update . Update current directory package // cell update Update a specific package // // Options: -// --build Run build after updating -// --target Target platform for build (requires --build) +// --target Target platform for build // --follow-links Update link targets instead of origins // --git Run git pull on local packages before updating var shop = use('internal/shop') -var build = use('build') var fd = use('fd') var os = use('internal/os') +var link = use('link') var target_pkg = null -var run_build = false var target_triple = null var follow_links = false var git_pull = false var i = 0 -var resolved = null +var updated = 0 +var packages = null -// Parse arguments -for (i = 0; i < length(args); i++) { - if (args[i] == '--help' || args[i] == '-h') { - log.console("Usage: cell update [] [options]") - log.console("") - log.console("Update packages from remote sources.") - log.console("") - log.console("Options:") - log.console(" --build Run build after updating") - log.console(" --target Target platform for build (requires --build)") - log.console(" --follow-links Update link targets instead of origins") - log.console(" --git Run git pull on local packages") - $stop() - } else if (args[i] == '--build') { - run_build = true - } else if (args[i] == '--target' || args[i] == '-t') { - if (i + 1 < length(args)) { - target_triple = args[++i] - } else { - log.error('--target requires a triple') - $stop() - } - } else if (args[i] == '--follow-links') { +var run = function() { + for (i = 0; i < length(args); i++) { + if (args[i] == '--help' || args[i] == '-h') { + log.console("Usage: cell update [] [options]") + log.console("") + log.console("Update packages from remote sources.") + log.console("") + log.console("Options:") + log.console(" --target Target platform for build") + log.console(" --follow-links Update link targets instead of origins") + log.console(" --git Run git pull on local packages") + return + } else if (args[i] == '--target' || args[i] == '-t') { + if (i + 1 < length(args)) { + target_triple = args[++i] + } else { + log.error('--target requires a triple') + return + } + } else if (args[i] == '--follow-links') { follow_links = true } else if (args[i] == '--git') { git_pull = true } else if (!starts_with(args[i], '-')) { target_pkg = args[i] - // Resolve relative paths to absolute paths - if (target_pkg == '.' || starts_with(target_pkg, './') || starts_with(target_pkg, '../') || fd.is_dir(target_pkg)) { - resolved = fd.realpath(target_pkg) - if (resolved) { - target_pkg = resolved - } - } } } -// Default target if building -if (run_build && !target_triple) { - target_triple = build.detect_host_target() -} +if (target_pkg) + target_pkg = shop.resolve_locator(target_pkg) -var link = use('link') - -function update_and_fetch(pkg) { +function update_one(pkg) { + var effective_pkg = pkg + var link_target = null var lock = shop.load_lock() var old_entry = lock[pkg] var old_commit = old_entry ? old_entry.commit : null - var effective_pkg = pkg - var link_target = null + var info = shop.resolve_package_info(pkg) var new_entry = null var old_str = null - // Handle follow-links option if (follow_links) { link_target = link.get_target(pkg) if (link_target) { @@ -87,80 +72,49 @@ function update_and_fetch(pkg) { } } + // For local packages with --git, pull first + if (git_pull && info == 'local' && fd.is_dir(effective_pkg + '/.git')) { + log.console(" " + effective_pkg + " (git pull)") + os.system('git -C "' + effective_pkg + '" pull') + } + + // Check for update (sets lock entry if changed) new_entry = shop.update(effective_pkg) - if (new_entry) { - if (new_entry.commit) { - old_str = old_commit ? text(old_commit, 0, 8) : "(new)" - log.console(" " + effective_pkg + " " + old_str + " -> " + text(new_entry.commit, 0, 8)) - shop.fetch(effective_pkg) - } else { - // Local package - run git pull if requested - if (git_pull && fd.is_dir(effective_pkg + '/.git')) { - log.console(" " + effective_pkg + " (git pull)") - os.system('git -C "' + effective_pkg + '" pull') - } else { - log.console(" " + effective_pkg + " (local)") - } - } - shop.extract(effective_pkg) - shop.build_package_scripts(effective_pkg) - return effective_pkg + if (new_entry && new_entry.commit) { + old_str = old_commit ? text(old_commit, 0, 8) : "(new)" + log.console(" " + effective_pkg + " " + old_str + " -> " + text(new_entry.commit, 0, 8)) } - return null + + // Sync: fetch, extract, build + shop.sync(effective_pkg, {target: target_triple}) + + return new_entry } -var updated_packages = [] - -var updated = null -var packages = null -var pkg_count = 0 -var pkg = null if (target_pkg) { - updated = update_and_fetch(target_pkg) - if (updated) { - push(updated_packages, updated) + if (update_one(target_pkg)) { log.console("Updated " + target_pkg + ".") } else { log.console(target_pkg + " is up to date.") } } else { packages = shop.list_packages() - pkg_count = length(packages) - log.console("Checking for updates (" + text(pkg_count) + " package" + (pkg_count == 1 ? "" : "s") + ")...") + log.console("Checking for updates (" + text(length(packages)) + " packages)...") - for (i = 0; i < length(packages); i++) { - pkg = packages[i] - if (pkg == 'core') continue + arrfor(packages, function(pkg) { + if (pkg == 'core') return + if (update_one(pkg)) + updated = updated + 1 + }) - updated = update_and_fetch(pkg) - if (updated) { - push(updated_packages, updated) - } - } - - if (length(updated_packages) > 0) { - log.console("Updated " + text(length(updated_packages)) + " package" + (length(updated_packages) == 1 ? "" : "s") + ".") + if (updated > 0) { + log.console("Updated " + text(updated) + " package(s).") } else { log.console("All packages are up to date.") } } - -// Run build if requested -if (run_build && length(updated_packages) > 0) { - log.console("") - log.console("Building updated packages...") - - arrfor(updated_packages, function(pkg) { - var _build = function() { - var lib = build.build_dynamic(pkg, target_triple, 'release') - if (lib) - log.console(" Built: " + lib) - } disruption { - log.error(" Failed to build " + pkg) - } - _build() - }) } +run() $stop() diff --git a/verify.ce b/verify.ce index cda42bba..e08bfe3b 100644 --- a/verify.ce +++ b/verify.ce @@ -21,36 +21,36 @@ var scope = null var deep = false var target_triple = null var i = 0 -var resolved = null -for (i = 0; i < length(args); i++) { - if (args[i] == '--deep') { - deep = true - } else if (args[i] == '--target' || args[i] == '-t') { - if (i + 1 < length(args)) { - target_triple = args[++i] - } else { - log.error('--target requires a triple') - $stop() +var run = function() { + for (i = 0; i < length(args); i++) { + if (args[i] == '--deep') { + deep = true + } else if (args[i] == '--target' || args[i] == '-t') { + if (i + 1 < length(args)) { + target_triple = args[++i] + } else { + log.error('--target requires a triple') + return + } + } else if (args[i] == '--help' || args[i] == '-h') { + log.console("Usage: cell verify [] [options]") + log.console("") + log.console("Verify integrity and consistency.") + log.console("") + log.console("Scopes:") + log.console(" Verify specific package") + log.console(" shop Verify entire shop") + log.console(" world Verify all world roots") + log.console("") + log.console("Options:") + log.console(" --deep Traverse full dependency closure") + log.console(" --target Verify builds for specific target") + return + } else if (!starts_with(args[i], '-')) { + scope = args[i] } - } else if (args[i] == '--help' || args[i] == '-h') { - log.console("Usage: cell verify [] [options]") - log.console("") - log.console("Verify integrity and consistency.") - log.console("") - log.console("Scopes:") - log.console(" Verify specific package") - log.console(" shop Verify entire shop") - log.console(" world Verify all world roots") - log.console("") - log.console("Options:") - log.console(" --deep Traverse full dependency closure") - log.console(" --target Verify builds for specific target") - $stop() - } else if (!starts_with(args[i], '-')) { - scope = args[i] } -} // Default to current directory if (!scope) { @@ -206,13 +206,7 @@ if (scope == 'shop') { // Single package locator = scope - // Resolve local paths - if (locator == '.' || starts_with(locator, './') || starts_with(locator, '../') || fd.is_dir(locator)) { - resolved = fd.realpath(locator) - if (resolved) { - locator = resolved - } - } + locator = shop.resolve_locator(locator) if (deep) { // Gather all dependencies @@ -257,5 +251,7 @@ if (length(errors) > 0) { } else { log.console("Verification PASSED: " + text(checked) + " package(s) checked, " + text(length(warnings)) + " warning(s)") } +} +run() $stop()