From 752479e25049348bb7ebb4c9f0672157f5a8e06b Mon Sep 17 00:00:00 2001 From: John Alanbrook Date: Wed, 10 Dec 2025 01:37:21 -0600 Subject: [PATCH] fix links, fix hot reload --- Makefile | 4 + build.ce | 12 +- build.cm | 29 +++-- cellfs.cm | 6 +- internal/engine.cm | 1 + link.ce | 212 +++++++++++++++++++---------------- link.cm | 219 ++++++++++++++++++++++++++++++++++++ package.cm | 7 ++ shop.cm | 272 ++++++++++++++++++++++++++------------------- source/quickjs.c | 2 +- source/quickjs.h | 2 + 11 files changed, 526 insertions(+), 240 deletions(-) create mode 100644 link.cm diff --git a/Makefile b/Makefile index 36dcb4f2..acad4177 100755 --- a/Makefile +++ b/Makefile @@ -9,6 +9,10 @@ CELL_SHOP = $(HOME)/.cell CELL_CORE_PACKAGE = $(CELL_SHOP)/packages/core +makecell: + cell pack core -o cell + cp cell /opt/homebrew/bin/ + # Install core: symlink this directory to ~/.cell/core install: bootstrap $(CELL_SHOP) @echo "Linking cell core to $(CELL_CORE_PACKAGE)" diff --git a/build.ce b/build.ce index 99501c38..8a378ea4 100644 --- a/build.ce +++ b/build.ce @@ -40,15 +40,6 @@ for (var i = 0; i < args.length; i++) { log.error('-b requires a buildtype (release, debug, minsize)') $_.stop() } - log.console('Usage: cell build [options]') - log.console('') - log.console('Options:') - log.console(' -p, --package Build specific package only') - log.console(' -t, --target Cross-compile for target platform') - log.console(' -b, --buildtype Build type: release, debug, minsize (default: release)') - log.console('') - log.console('Available targets: ' + build.list_targets().join(', ')) - $_.stop() } else if (args[i] == '--list-targets') { log.console('Available targets:') var targets = build.list_targets() @@ -105,8 +96,7 @@ if (target_package) { } } - log.console('') - log.console('Build complete: ' + success + ' libraries built' + (failed > 0 ? ', ' + failed + ' failed' : '')) + log.console(`Build complete: ${success} libraries built${failed > 0 ? `, ${failed} failed` : ''}`) } $_.stop() diff --git a/build.cm b/build.cm index c47cc046..9b992841 100644 --- a/build.cm +++ b/build.cm @@ -194,6 +194,7 @@ Build.build_package = function(pkg, target, exclude_main, buildtype = 'release') // ============================================================================ // Build a dynamic library for a package // Output goes to .cell/lib/. +// Dynamic libraries do NOT link against core; undefined symbols are resolved at dlopen time Build.build_dynamic = function(pkg, target, buildtype = 'release') { target = target || Build.detect_host_target() @@ -222,15 +223,26 @@ Build.build_dynamic = function(pkg, target, buildtype = 'release') { // Build link command var cmd_parts = [cc, '-shared', '-fPIC'] - // Add rpath to find libraries in .cell/local at runtime + // Platform-specific flags for undefined symbols (resolved at dlopen) and size optimization if (tc.system == 'darwin') { + // Allow undefined symbols - they will be resolved when dlopen'd into the main executable + cmd_parts.push('-undefined', 'dynamic_lookup') + // Dead-strip unused code + cmd_parts.push('-Wl,-dead_strip') + // rpath for .cell/local libraries cmd_parts.push('-Wl,-rpath,@loader_path/../local') cmd_parts.push('-Wl,-rpath,' + local_dir) } else if (tc.system == 'linux') { + // Allow undefined symbols at link time + cmd_parts.push('-Wl,--allow-shlib-undefined') + // Garbage collect unused sections + cmd_parts.push('-Wl,--gc-sections') + // rpath for .cell/local libraries cmd_parts.push('-Wl,-rpath,$ORIGIN/../local') cmd_parts.push('-Wl,-rpath,' + local_dir) } else if (tc.system == 'windows') { - // Windows uses PATH or same directory, add -L for link time + // Windows DLLs: use --allow-shlib-undefined for mingw + cmd_parts.push('-Wl,--allow-shlib-undefined') } // Add .cell/local to library search path @@ -240,14 +252,7 @@ Build.build_dynamic = function(pkg, target, buildtype = 'release') { cmd_parts.push('"' + objects[i] + '"') } - // Link against core library (all dynamic libs depend on core) - if (pkg != 'core') { - var core_lib_name = shop.lib_name_for_package('core') - var core_lib_path = lib_dir + '/' + core_lib_name + dylib_ext - if (fd.is_file(core_lib_path)) { - cmd_parts.push('"' + core_lib_path + '"') - } - } + // Do NOT link against core library - symbols resolved at dlopen time // Add LDFLAGS (resolve relative -L paths) for (var i = 0; i < ldflags.length; i++) { @@ -375,7 +380,7 @@ Build.build_all_dynamic = function(target, buildtype = 'release') { // Build core first if (packages.indexOf('core') >= 0) { try { - var lib = Build.build_dynamic('core', target) + var lib = Build.build_dynamic('core', target, buildtype) results.push({ package: 'core', library: lib }) } catch (e) { log.error('Failed to build core: ' + e) @@ -389,7 +394,7 @@ Build.build_all_dynamic = function(target, buildtype = 'release') { if (pkg == 'core') continue try { - var lib = Build.build_dynamic(pkg, target) + var lib = Build.build_dynamic(pkg, target, buildtype) results.push({ package: pkg, library: lib }) } catch (e) { log.error('Failed to build ' + pkg + ': ') diff --git a/cellfs.cm b/cellfs.cm index ab125f6d..99b1383b 100644 --- a/cellfs.cm +++ b/cellfs.cm @@ -279,12 +279,12 @@ function mount_package(name) { } var shop = use('shop') - var dir = shop.get_module_dir(name) - + var dir = shop.get_package_dir(name) + if (!dir) { throw new Error("Package not found: " + name) } - + mount(dir, name) } diff --git a/internal/engine.cm b/internal/engine.cm index fc11d0b0..318255fb 100644 --- a/internal/engine.cm +++ b/internal/engine.cm @@ -215,6 +215,7 @@ var $_ = create_actor() os.use_cache = use_cache os.global_shop_path = shop_path +os.$_ = $_ var shop = use('shop') diff --git a/link.ce b/link.ce index 510520ea..4c4d1f2d 100644 --- a/link.ce +++ b/link.ce @@ -1,15 +1,18 @@ // link [args] // Commands: -// list -// delete -// clear -// [package] +// list List all active links +// sync Ensure all symlinks are in place +// delete Remove a link +// clear Remove all links +// Link the package in to that path +// Link to (path or another package) // // Examples: -// cell link ../cell-steam (Links package defined in ../cell-steam to that path) -// cell link my-pkg ../cell-steam (Links my-pkg to ../cell-steam) -// cell link my-pkg other-pkg (Links my-pkg to other-pkg) +// cell link ../cell-steam (Links package in ../cell-steam) +// cell link gitea.pockle.world/john/prosperon ../prosperon (Links prosperon to local path) +// cell link gitea.pockle.world/john/prosperon github.com/prosperon (Links to another remote) +var link = use('link') var shop = use('shop') var fd = use('fd') var toml = use('toml') @@ -18,10 +21,11 @@ if (args.length < 1) { log.console("Usage: link [args] or link [package] ") log.console("Commands:") log.console(" list List all active links") - log.console(" delete Remove a link") + 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 ") - log.console(" Link to (path or package)") + log.console(" Link the package in to that path") + log.console(" Link to (path or package)") $_.stop() return } @@ -29,38 +33,57 @@ if (args.length < 1) { var cmd = args[0] if (cmd == 'list') { - var links = shop.load_links() + var links = link.load() var count = 0 for (var k in links) { log.console(k + " -> " + links[k]) count++ } if (count == 0) log.console("No links.") + +} else if (cmd == 'sync') { + log.console("Syncing links...") + var result = link.sync_all(shop) + log.console("Synced " + result.synced + " link(s)") + if (result.errors.length > 0) { + log.console("Errors:") + for (var i = 0; i < result.errors.length; i++) { + log.console(" " + result.errors[i]) + } + } + } else if (cmd == 'delete' || cmd == 'rm') { if (args.length < 2) { - log.console("Usage: link delete ") + log.console("Usage: link delete ") $_.stop() return } - var target = args[1] + var pkg = args[1] - // Try to remove directly - if (shop.remove_link(target)) { - // Update to restore original if possible - log.console("Restoring " + target + "...") - shop.update(target) + if (link.remove(pkg)) { + // Try to restore the original package + log.console("Restoring " + pkg + "...") + try { + shop.fetch(pkg) + shop.extract(pkg) + log.console("Restored " + pkg) + } catch (e) { + log.console("Could not restore: " + e.message) + log.console("Run 'cell update " + pkg + "' to restore") + } } else { - log.console("No link found for " + target) + log.console("No link found for " + pkg) } } else if (cmd == 'clear') { - shop.clear_links() - log.console("Links cleared. Run 'cell update' to restore packages if needed.") + link.clear() + log.console("Links cleared. Run 'cell update' to restore packages.") + } else { // Linking logic var pkg_name = null - var target_path = null + var target = null // Check for 'add' compatibility var start_idx = 0 @@ -68,94 +91,89 @@ if (cmd == 'list') { start_idx = 1 } - // Parse arguments - // usage: link [pkg] - // valid inputs: - // link ../foo - // link pkg ../foo - // link pkg remote-pkg - var arg1 = args[start_idx] var arg2 = (args.length > start_idx + 1) ? args[start_idx + 1] : null if (!arg1) { - log.console("Error: specific target or package required") - $_.stop() - return + log.console("Error: target or package required") + $_.stop() + return } if (arg2) { - // Two arguments: explicit package name and target - pkg_name = arg1 - target_path = arg2 + // Two arguments: explicit package name and target + pkg_name = arg1 + target = arg2 + + // Resolve target if it's a local path + if (target == '.' || fd.is_dir(target)) { + target = fd.realpath(target) + } else if (target.startsWith('./') || target.startsWith('../')) { + // Relative path that doesn't exist yet - try to resolve anyway + var cwd = fd.realpath('.') + if (target.startsWith('./')) { + target = cwd + target.substring(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) + } else { - // One argument: assume it's a path, and we need to infer the package name - target_path = arg1 - - // Resolve path to check for cell.toml - var resolved = target_path - if (fd.is_dir(resolved)) resolved = fd.realpath(resolved) - - var toml_path = resolved + '/cell.toml' - if (!fd.is_file(toml_path)) { - log.console("Error: No cell.toml found at " + resolved + ". Cannot infer package name.") - log.console("If you meant to link an alias, provide the target: link ") - $_.stop() - return + // 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 (target.startsWith('./') || target.startsWith('../')) { + target = fd.realpath(target) || target + } + + // Must be a local path with cell.toml + var 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 + try { + var content = toml.decode(text(fd.slurp(toml_path))) + if (content.package) { + pkg_name = content.package + } else { + log.console("Error: cell.toml at " + target + " does not define 'package'") + $_.stop() + return } - - // Read package name - try { - var content = toml.decode(text(fd.slurp(toml_path))) - if (content.package) { - pkg_name = content.package - // If package name has version, strip it? - // Usually package = "name", version = "..." in cell.toml? - // Or package = "name" - // Standard is just name. - } else { - log.console("Error: cell.toml at " + resolved + " does not define a 'package' name.") - $_.stop() - return - } - } catch (e) { - log.console("Error reading cell.toml: " + e) - $_.stop() - return - } - - // Ensure target path is fully resolved since we inferred it - if (fd.is_dir(target_path)) target_path = fd.realpath(target_path) + } catch (e) { + log.console("Error reading cell.toml: " + e) + $_.stop() + return + } } - // Process the link - - // 1. Resolve target if it is a directory - if (target_path != "." && fd.is_dir(target_path)) { - target_path = fd.realpath(target_path) - } else if (target_path == ".") { - target_path = fd.realpath(target_path) - } else if (target_path.startsWith('/') || target_path.startsWith('./') || target_path.startsWith('../')) { - // It looks like a path but doesn't exist? - log.console("Warning: Link target '" + target_path + "' does not exist locally. Linking as alias anyway.") - if (target_path.startsWith('./') || target_path.startsWith('../')) { - // Resolve relative path roughly? - var cwd = fd.realpath('.') - // simple concat (fd.realpath typically converts to abspath, but if file missing it might fail or return same) - // Assuming user wants strict path if they used relative. - // Using fd.realpath on CWD + path is safer if we want absolute. - // But we don't have path manipulation lib easily exposed except implicit logic. - // Leaving as provided string if not existing dir. - } + // Validate: if target is a local path, it must have cell.toml + if (target.startsWith('/')) { + if (!fd.is_file(target + '/cell.toml')) { + log.console("Error: " + target + " is not a valid package (no cell.toml)") + $_.stop() + return + } } - - // 2. Add link - shop.add_link(pkg_name, target_path) - // 3. Update shop - // "Doing this effectively adds another item to the shop" - // We trigger update to ensure the shop recognizes the new link - shop.update(pkg_name) + // Add the link (this also creates the symlink) + try { + link.add(pkg_name, target, shop) + } catch (e) { + log.console("Error: " + e.message) + $_.stop() + return + } } $_.stop() \ No newline at end of file diff --git a/link.cm b/link.cm new file mode 100644 index 00000000..370d18f6 --- /dev/null +++ b/link.cm @@ -0,0 +1,219 @@ +// Link management module for cell packages +// Handles creating, removing, and syncing symlinks for local development + +var toml = use('toml') +var fd = use('fd') +var utf8 = use('utf8') +var os = use('os') + +var global_shop_path = os.global_shop_path + +// Get the links file path (in the global shop) +function get_links_path() { + return global_shop_path + '/link.toml' +} + +// Get the packages directory (in the global shop) +function get_packages_dir() { + return global_shop_path + '/packages' +} + +// return the safe path for the package +function safe_package_path(pkg) { + return pkg.replaceAll('@', '_') +} + +function get_package_abs_dir(package) { + return get_packages_dir() + '/' + package +} + +function ensure_dir(path) { + if (fd.stat(path).isDirectory) return + var parts = path.split('/') + var current = path.startsWith('/') ? '/' : '' + for (var i = 0; i < parts.length; i++) { + if (parts[i] == '') continue + current += parts[i] + '/' + if (!fd.stat(current).isDirectory) { + fd.mkdir(current) + } + } +} + +// Resolve a link target to its actual path +// If target is a local path (starts with /), return it directly +// If target is a package name, return the package directory +function resolve_link_target(target) { + if (target.startsWith('/')) { + return target + } + // Target is another package - resolve to its directory + return get_packages_dir() + '/' + safe_package_path(target) +} + +var Link = {} + +var link_cache = null + +Link.load = function() { + if (link_cache) return link_cache + var path = get_links_path() + if (!fd.is_file(path)) { + link_cache = {} + return link_cache + } + + try { + var content = text(fd.slurp(path)) + var cfg = toml.decode(content) + link_cache = cfg.links || {} + } catch (e) { + log.console("Warning: Failed to load link.toml: " + e) + link_cache = {} + } + return link_cache +} + +Link.save = function(links) { + link_cache = links + var cfg = { links: links } + var path = get_links_path() + fd.slurpwrite(path, utf8.encode(toml.encode(cfg))) +} + +Link.add = function(canonical, target, shop) { + // Validate canonical package exists in shop + var lock = shop.load_lock() + if (!lock[canonical]) { + throw new Error('Package ' + canonical + ' is not installed. Install it first with: cell get ' + canonical) + } + + // Validate target is a valid package + if (target.startsWith('/')) { + // Local path - must have cell.toml + if (!fd.is_file(target + '/cell.toml')) { + throw new Error('Target ' + target + ' is not a valid package (no cell.toml)') + } + } else { + // Remote package target - ensure it's installed + shop.get(target) + } + + var links = Link.load() + links[canonical] = target + Link.save(links) + + // Create the symlink immediately + Link.sync_one(canonical, target, shop) + + log.console("Linked " + canonical + " -> " + target) + return true +} + +Link.remove = function(canonical) { + var links = Link.load() + if (!links[canonical]) return false + + // Remove the symlink if it exists + var target_dir = get_package_abs_dir(canonical) + if (fd.is_link(target_dir)) { + fd.unlink(target_dir) + log.console("Removed symlink at " + target_dir) + } + + delete links[canonical] + Link.save(links) + log.console("Unlinked " + canonical) + return true +} + +Link.clear = function() { + // Remove all symlinks first + var links = Link.load() + for (var canonical in links) { + var target_dir = get_package_abs_dir(canonical) + if (fd.is_link(target_dir)) { + fd.unlink(target_dir) + } + } + + Link.save({}) + log.console("Cleared all links") + return true +} + +// Sync a single link - ensure the symlink is in place +Link.sync_one = function(canonical, target, shop) { + var target_dir = get_package_abs_dir(canonical) + var link_target = resolve_link_target(target) + + // Ensure parent directories exist + var parent = target_dir.substring(0, target_dir.lastIndexOf('/')) + ensure_dir(parent) + + // Check current state + var current_link = null + if (fd.is_link(target_dir)) { + current_link = fd.readlink(target_dir) + } + + // If already correctly linked, nothing to do + if (current_link == link_target) { + return true + } + + // Remove existing file/dir/link + if (fd.is_link(target_dir)) { + fd.unlink(target_dir) + } else if (fd.is_dir(target_dir)) { + fd.rmdir(target_dir, 1) + } + + // Create symlink + fd.symlink(link_target, target_dir) + return true +} + +// Sync all links - ensure all symlinks are in place +Link.sync_all = function(shop) { + var links = Link.load() + var count = 0 + var errors = [] + + for (var canonical in links) { + var target = links[canonical] + try { + // Validate target exists + var link_target = resolve_link_target(target) + if (!fd.is_dir(link_target)) { + errors.push(canonical + ': target ' + link_target + ' does not exist') + continue + } + if (!fd.is_file(link_target + '/cell.toml')) { + errors.push(canonical + ': target ' + link_target + ' is not a valid package') + continue + } + + Link.sync_one(canonical, target, shop) + count++ + } catch (e) { + errors.push(canonical + ': ' + e.message) + } + } + + return { synced: count, errors: errors } +} + +// Check if a package is currently linked +Link.is_linked = function(canonical) { + var links = Link.load() + return canonical in links +} + +// Get the link target for a package (or null if not linked) +Link.get_target = function(canonical) { + var links = Link.load() + return links[canonical] || null +} + +return Link diff --git a/package.cm b/package.cm index 1cf92fed..eb66f06e 100644 --- a/package.cm +++ b/package.cm @@ -41,6 +41,13 @@ package.find_alias = function(name, locator) return null } +package.alias_to_package = function(name, alias) +{ + var config = package.load_config(name) + if (!config.dependencies) return null + return config.dependencies[alias] +} + // alias is optional package.add_dependency = function(name, locator, alias = locator) { diff --git a/shop.cm b/shop.cm index f8820acf..ef6fed3a 100644 --- a/shop.cm +++ b/shop.cm @@ -10,6 +10,7 @@ var utf8 = use('utf8') var blob = use('blob') var pkg_tools = use('package') var os = use('os') +var link = use('link') var core = "core" @@ -58,10 +59,15 @@ var SCOPE_CORE = 2 var MOD_EXT = '.cm' var ACTOR_EXT = '.ce' -var dylib_ext = '.so' // Default extension +var dylib_ext = '.dylib' // Default extension var use_cache = os.use_cache var global_shop_path = os.global_shop_path +var $_ = os.$_ + +Shop.get_package_dir = function(name) { + return global_shop_path + '/packages/' + name +} // Get the packages directory (in the global shop) function get_packages_dir() { @@ -85,8 +91,6 @@ Shop.get_reports_dir = function() { return global_shop_path + '/reports' } -var open_dl = {} - function get_import_package(name) { var parts = name.split('/') if (parts.length > 1) @@ -113,6 +117,16 @@ function abs_path_to_package(package_dir) if (package_in_shop(package_dir)) return package_dir + // For local directories (e.g., linked targets), read the package name from cell.toml + try { + var content = text(fd.slurp(package_dir + '/cell.toml')) + var cfg = toml.decode(content) + if (cfg.package) + return cfg.package + } catch (e) { + // Fall through + } + return null } @@ -138,9 +152,9 @@ Shop.file_info = function(file) { info.name = file.substring(pkg_dir.length + 1) if (info.is_actor) - info.name = info.name.substring(0, info.name.length - ACTOR_EXT.length) + info.name = info.path.substring(0, info.path.length - ACTOR_EXT.length) else if (info.is_module) - info.name = info.name.substring(0, info.name.length - MOD_EXT.length) + info.name = info.path.substring(0, info.path.length - MOD_EXT.length) } return info @@ -212,56 +226,6 @@ Shop.save_lock = function(lock) { fd.slurpwrite(path, utf8.encode(toml.encode(lock))); } -/* links functionality */ -var link_cache = null -Shop.load_links = function() { - if (link_cache) return link_cache - var path = global_shop_path + '/link.toml' - if (!fd.is_file(path)) { - link_cache = {} - return link_cache - } - - try { - var content = text(fd.slurp(path)) - var cfg = toml.decode(content) - link_cache = cfg.links || {} - } catch (e) { - log.console("Warning: Failed to load link.toml: " + e) - link_cache = {} - } - return link_cache -} - -Shop.save_links = function(links) { - link_cache = links - var cfg = { links: links } - var path = global_shop_path + '/link.toml' - fd.slurpwrite(path, utf8.encode(toml.encode(cfg))) -} - -Shop.add_link = function(canonical, target) { - var links = Shop.load_links() - links[canonical] = target - Shop.save_links(links) - log.console("Linked " + canonical + " -> " + target) - return true -} - -Shop.remove_link = function(canonical) { - var links = Shop.load_links() - if (!links[canonical]) return false - delete links[canonical] - Shop.save_links(links) - log.console("Unlinked " + canonical) - return true -} - -Shop.clear_links = function() { - Shop.save_links({}) - log.console("Cleared all links") - return true -} // Get information about how to resolve a package // Local packages always start with / @@ -334,8 +298,8 @@ var open_dls = {} // for script forms, path is the canonical path of the module var script_form = function(path, script, pkg) { var pkg_arg = pkg ? `'${pkg}'` : 'null' - var relative_use_fn = `def use = function(path) { return globalThis.use(path, ${pkg_arg});}` - var fn = `(function setup_module($_, args){ ${relative_use_fn}; ${script}})` + var relative_use_fn = `def PACKAGE = ${pkg_arg}; def use = function(path) { return globalThis.use(path, ${pkg_arg});}` + var fn = `(function setup_module($_, args){ def arg = args; ${relative_use_fn}; ${script}})` return fn } @@ -344,8 +308,10 @@ var script_form = function(path, script, pkg) { function resolve_mod_fn(path, pkg) { if (!fd.is_file(path)) throw new Error(`path ${path} is not a file`) + var file_info = Shop.file_info(path) + var file_pkg = file_info.package var content = text(fd.slurp(path)) - var script = script_form(path, content, pkg); + var script = script_form(path, content, file_pkg); var obj = pull_from_cache(utf8.encode(script)) if (obj) { @@ -415,47 +381,71 @@ function resolve_locator(path, ctx) // Generate symbol name for a C module file // Uses the same format as Shop.c_symbol_for_file +// Resolves linked packages to their actual target first function make_c_symbol(pkg, file) { - var pkg_safe = pkg.replace(/\//g, '_').replace(/\./g, '_').replace(/-/g, '_') + // Check if this package is linked - if so, use the link target for symbol name + var link_target = link.get_target(pkg) + var resolved_pkg = link_target ? link_target : pkg + + var pkg_safe = resolved_pkg.replace(/\//g, '_').replace(/\./g, '_').replace(/-/g, '_') var file_safe = file.replace(/\//g, '_').replace(/\./g, '_').replace(/-/g, '_') return 'js_' + pkg_safe + '_' + file_safe + '_use' } // Get the library path for a package in .cell/lib +// Resolves linked packages to their actual target first function get_lib_path(pkg) { - var lib_name = pkg.replace(/\//g, '_').replace(/\./g, '_').replace(/-/g, '_') + // Check if this package is linked - if so, use the link target + var link_target = link.get_target(pkg) + var resolved_pkg = link_target ? link_target : pkg + + var lib_name = resolved_pkg.replace(/\//g, '_').replace(/\./g, '_').replace(/-/g, '_') return global_shop_path + '/lib/' + lib_name + dylib_ext } +// Resolve a C symbol by searching: +// 1. If package_context is null, only check core internal symbols +// 2. Otherwise: own package (internal then dylib) -> other packages (internal then dylib) -> core (internal only) +// Core is never loaded as a dynamic library via dlopen function resolve_c_symbol(path, package_context) { - var static_only = cell.static_only - - // 1. Check internal symbols (statically linked) in package context - if (package_context) { - var sym = make_c_symbol(package_context, path) - if (os.internal_exists(sym)) { + // If no package context, only check core internal symbols + if (!package_context) { + var core_sym = `js_${path}_use` + if (os.internal_exists(core_sym)) { return { - symbol: function() { return os.load_internal(sym) }, + symbol: function() { return os.load_internal(core_sym) }, + scope: SCOPE_CORE, + path: core_sym + } + } + return null + } + + // 1. Check own package first (internal, then dylib) + var sym = make_c_symbol(package_context, path) + if (os.internal_exists(sym)) { + return { + symbol: function() { return os.load_internal(sym) }, + scope: SCOPE_LOCAL, + path: sym + } + } + + var dl_path = get_lib_path(package_context) + + if (fd.is_file(dl_path)) { + if (!open_dls[dl_path]) open_dls[dl_path] = os.dylib_open(dl_path) + if (open_dls[dl_path] && os.dylib_has_symbol(open_dls[dl_path], sym)) { + return { + symbol: function() { return os.dylib_symbol(open_dls[dl_path], sym) }, scope: SCOPE_LOCAL, path: sym } } - - var dl_path = get_lib_path(package_context) - if (fd.is_file(dl_path)) { - if (!open_dls[dl_path]) open_dls[dl_path] = os.dylib_open(dl_path) - if (open_dls[dl_path] && os.dylib_has_symbol(open_dls[dl_path], sym)) { - return { - symbol: function() { return os.dylib_symbol(open_dls[dl_path], sym) }, - scope: SCOPE_LOCAL, - path: sym - } - } - } } - // 2. Check if valid package import (e.g. 'prosperon/sprite') + // 2. Check aliased package imports (e.g. 'prosperon/sprite') var pkg_alias = get_import_package(path) if (pkg_alias) { var canon_pkg = get_aliased_package(path, package_context) @@ -463,6 +453,7 @@ function resolve_c_symbol(path, package_context) var mod_name = get_import_name(path) var sym = make_c_symbol(canon_pkg, mod_name) + // Check internal first if (os.internal_exists(sym)) { return { symbol: function() { return os.load_internal(sym) }, @@ -472,6 +463,7 @@ function resolve_c_symbol(path, package_context) } } + // Then check dylib var dl_path = get_lib_path(canon_pkg) if (fd.is_file(dl_path)) { if (!open_dls[dl_path]) open_dls[dl_path] = os.dylib_open(dl_path) @@ -487,7 +479,7 @@ function resolve_c_symbol(path, package_context) } } - // 3. Check core fallback + // 3. Check core internal symbols (core is never a dynamic library) var core_sym = `js_${path}_use` if (os.internal_exists(core_sym)) { return { @@ -497,21 +489,6 @@ function resolve_c_symbol(path, package_context) } } - // Try core dynamic library - if (!static_only) { - var core_dl = get_lib_path('core') - if (fd.is_file(core_dl)) { - if (!open_dls[core_dl]) open_dls[core_dl] = os.dylib_open(core_dl) - if (open_dls[core_dl] && os.dylib_has_symbol(open_dls[core_dl], core_sym)) { - return { - symbol: function() { return os.dylib_symbol(open_dls[core_dl], core_sym) }, - scope: SCOPE_CORE, - path: core_sym - } - } - } - } - return null } @@ -523,29 +500,48 @@ function resolve_module_info(path, package_context) { if (min_scope == 999) return null - // Cache key is now package/file format - // e.g., "core/shop", "gitea.pockle.world/john/prosperon/sprite" + // Cache key is based on the realpath of the resolved file + // This ensures linked packages resolve to the same cache entry + // whether accessed via symlink or directly var cache_key - if (min_scope == SCOPE_CORE) { - cache_key = 'core/' + path - } else if (min_scope == SCOPE_LOCAL && package_context) { - cache_key = package_context + '/' + path - } else if (min_scope == SCOPE_PACKAGE) { - // For package imports like 'prosperon/sprite', resolve to canonical path - var pkg_alias = get_import_package(path) - if (pkg_alias) { - var canon_pkg = get_canonical_package(pkg_alias, package_context) - if (canon_pkg) { - var mod_name = get_import_name(path) - cache_key = canon_pkg + '/' + mod_name + if (mod_resolve.scope < 900 && mod_resolve.path) { + // Use realpath to resolve symlinks and get the actual file location + var real_path = fd.realpath(mod_resolve.path) + if (real_path) { + // Derive cache key from the real path's package info + var real_info = Shop.file_info(real_path) + if (real_info.package && real_info.name) { + cache_key = real_info.package + '/' + real_info.name + } else { + // Fallback to the realpath itself + cache_key = real_path + } + } + } + + // Fallback for C-only modules or if realpath failed + if (!cache_key) { + if (min_scope == SCOPE_CORE) { + cache_key = 'core/' + path + } else if (min_scope == SCOPE_LOCAL && package_context) { + cache_key = package_context + '/' + path + } else if (min_scope == SCOPE_PACKAGE) { + // For package imports like 'prosperon/sprite', resolve to canonical path + var pkg_alias = get_import_package(path) + if (pkg_alias) { + var canon_pkg = get_canonical_package(pkg_alias, package_context) + if (canon_pkg) { + var mod_name = get_import_name(path) + cache_key = canon_pkg + '/' + mod_name + } else { + cache_key = path + } } else { cache_key = path } } else { cache_key = path } - } else { - cache_key = path } return { @@ -570,7 +566,6 @@ function execute_module(info) { var c_resolve = info.c_resolve var mod_resolve = info.mod_resolve - var cache_key = info.cache_key var used @@ -607,7 +602,6 @@ Shop.use = function(path, package_context) { if (use_cache[info.cache_key]) return use_cache[info.cache_key] - use_cache[info.cache_key] = execute_module(info) return use_cache[info.cache_key] } @@ -709,12 +703,22 @@ Shop.fetch = function(pkg) { } // Extract: Extract a package to its target directory -// For local packages, creates a symlink +// For linked packages, creates a symlink to the link target +// For local packages, creates a symlink to the local path // For remote packages, extracts from the provided zip blob // Returns true on success Shop.extract = function(pkg) { - var info = Shop.resolve_package_info(pkg) var target_dir = get_package_abs_dir(pkg) + + // Check if this package is linked + var link_target = link.get_target(pkg) + if (link_target) { + // Use the link - create symlink to link target + link.sync_one(pkg, link_target) + return true + } + + var info = Shop.resolve_package_info(pkg) if (info == 'local') { if (fd.is_link(target_dir)) @@ -831,11 +835,47 @@ Shop.remove = function(pkg) { return true } +Shop.get = function(pkg) { + var lock = Shop.load_lock() + + if (!lock[pkg]) { + var info = Shop.resolve_package_info(pkg) + if (!info) { + throw new Error("Invalid package: " + pkg) + } + + var commit = null + if (info != 'local') { + commit = fetch_remote_hash(pkg) + if (!commit) { + throw new Error("Could not resolve commit for " + pkg) + } + } + + lock[pkg] = { + type: info, + commit: commit, + updated: time.number() + } + Shop.save_lock(lock) + } +} + // Compile a module // List all files in a package var debug = use('debug') +Shop.file_reload = function(file) +{ + var info = Shop.file_info(file) + if (!info.is_module) return + + var pkg = info.package + + Shop.module_reload(info.name, pkg) +} + Shop.module_reload = function(path, package) { if (!Shop.is_loaded(path,package)) return var info = resolve_module_info(path, package) diff --git a/source/quickjs.c b/source/quickjs.c index d8222275..ce87fff1 100644 --- a/source/quickjs.c +++ b/source/quickjs.c @@ -900,7 +900,7 @@ static JSValue js_error_toString(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv); static const JSClassExoticMethods js_string_exotic_methods; -static JSClassID js_class_id_alloc = JS_CLASS_INIT_COUNT; +JSClassID js_class_id_alloc = JS_CLASS_INIT_COUNT; static void js_trigger_gc(JSRuntime *rt, size_t size) { diff --git a/source/quickjs.h b/source/quickjs.h index 2d7010dc..72631aba 100644 --- a/source/quickjs.h +++ b/source/quickjs.h @@ -496,6 +496,8 @@ JSClassID JS_GetClassID(JSValue v); int JS_NewClass(JSRuntime *rt, JSClassID class_id, const JSClassDef *class_def); int JS_IsRegisteredClass(JSRuntime *rt, JSClassID class_id); +extern JSClassID js_class_id_alloc; + /* value handling */ static js_force_inline JSValue JS_NewBool(JSContext *ctx, JS_BOOL val)