From 37f2cff6eca324f7d8e3903cac96aac0681ac231 Mon Sep 17 00:00:00 2001 From: John Alanbrook Date: Tue, 9 Dec 2025 23:19:25 -0600 Subject: [PATCH] fix --- Makefile | 21 +- archive/miniz.c | 4 + benchmarks/js_perf.ce | 12 +- build.ce | 877 ++----------------- build.cm | 1016 ++++++++-------------- cell.toml | 2 +- config.ce | 8 +- fd.c | 4 +- fetch.ce | 86 ++ help/cellstructure.md | 2 + internal/engine.cm | 95 +-- list.ce | 18 +- ls.ce | 32 +- pack.ce | 123 +++ package.cm | 305 +++++++ replace.ce | 29 - shop.cm | 1896 +++++++++-------------------------------- toolchains.cm | 234 +++++ update.ce | 88 +- why.ce | 3 +- 20 files changed, 1716 insertions(+), 3139 deletions(-) create mode 100644 fetch.ce create mode 100644 pack.ce create mode 100644 package.cm delete mode 100644 replace.ce create mode 100644 toolchains.cm diff --git a/Makefile b/Makefile index 00543904..36dcb4f2 100755 --- a/Makefile +++ b/Makefile @@ -7,9 +7,17 @@ # The cell shop is at ~/.cell and core scripts are installed to ~/.cell/core CELL_SHOP = $(HOME)/.cell -CELL_CORE = $(CELL_SHOP)/core CELL_CORE_PACKAGE = $(CELL_SHOP)/packages/core +# Install core: symlink this directory to ~/.cell/core +install: bootstrap $(CELL_SHOP) + @echo "Linking cell core to $(CELL_CORE_PACKAGE)" + rm -rf $(CELL_CORE_PACKAGE) + ln -s $(PWD) $(CELL_CORE_PACKAGE) + cp cell /opt/homebrew/bin/ + cp libcell_runtime.dylib /opt/homebrew/lib/ + @echo "Core installed." + cell: libcell_runtime.dylib cell_main cp cell_main cell chmod +x cell @@ -26,17 +34,6 @@ libcell_runtime.dylib: $(CELL_SHOP)/build/dynamic cell_main: source/main.c libcell_runtime.dylib cc -o cell_main source/main.c -L. -lcell_runtime -Wl,-rpath,@loader_path -Wl,-rpath,/opt/homebrew/lib -# Install core: symlink this directory to ~/.cell/core -install: bootstrap $(CELL_SHOP) - @echo "Linking cell core to $(CELL_CORE)" - rm -rf $(CELL_CORE) - rm -rf $(CELL_CORE_PACKAGE) - ln -s $(PWD) $(CELL_CORE) - ln -s $(PWD) $(CELL_CORE_PACKAGE) - cp cell /opt/homebrew/bin/ - cp libcell_runtime.dylib /opt/homebrew/lib/ - @echo "Core installed." - # Create the cell shop directories $(CELL_SHOP): mkdir -p $(CELL_SHOP) diff --git a/archive/miniz.c b/archive/miniz.c index 650eb46b..98163704 100644 --- a/archive/miniz.c +++ b/archive/miniz.c @@ -217,6 +217,7 @@ static const JSCFunctionListEntry js_writer_funcs[] = { JSValue js_reader_mod(JSContext *js, JSValue self, int argc, JSValue *argv) { +#ifndef MINIZ_NO_TIME const char *file = JS_ToCString(js,argv[0]); if (!file) return JS_EXCEPTION; @@ -243,6 +244,9 @@ JSValue js_reader_mod(JSContext *js, JSValue self, int argc, JSValue *argv) } return JS_NewFloat64(js, pstat.m_time); +#else + return JS_ThrowInternalError(js, "MINIZ_NO_TIME is defined"); +#endif } JSValue js_reader_exists(JSContext *js, JSValue self, int argc, JSValue *argv) diff --git a/benchmarks/js_perf.ce b/benchmarks/js_perf.ce index 2e0d7326..8c28febf 100644 --- a/benchmarks/js_perf.ce +++ b/benchmarks/js_perf.ce @@ -6,7 +6,7 @@ var time = use('time') //////////////////////////////////////////////////////////////////////////////// // Test configurations -const iterations = { +def iterations = { simple: 10000000, medium: 1000000, complex: 100000 @@ -154,7 +154,7 @@ function benchObjectCreation() { this.y = y; } - var constructorTime = measureTime(function() { + var defructorTime = measureTime(function() { for (var i = 0; i < iterations.medium; i++) { var p = new Point(i, i * 2); } @@ -179,7 +179,7 @@ function benchObjectCreation() { return { literalTime: literalTime, - constructorTime: constructorTime, + defructorTime: defructorTime, prototypeTime: prototypeTime }; } @@ -342,9 +342,9 @@ var objResults = benchObjectCreation(); log.console(" Literal: " + objResults.literalTime.toFixed(3) + "s => " + (iterations.medium / objResults.literalTime).toFixed(1) + " creates/sec [" + (objResults.literalTime / iterations.medium * 1e9).toFixed(1) + " ns/op]"); -log.console(" Constructor: " + objResults.constructorTime.toFixed(3) + "s => " + - (iterations.medium / objResults.constructorTime).toFixed(1) + " creates/sec [" + - (objResults.constructorTime / iterations.medium * 1e9).toFixed(1) + " ns/op]"); +log.console(" Constructor: " + objResults.defructorTime.toFixed(3) + "s => " + + (iterations.medium / objResults.defructorTime).toFixed(1) + " creates/sec [" + + (objResults.defructorTime / iterations.medium * 1e9).toFixed(1) + " ns/op]"); log.console(" Prototype: " + objResults.prototypeTime.toFixed(3) + "s => " + (iterations.medium / objResults.prototypeTime).toFixed(1) + " creates/sec [" + (objResults.prototypeTime / iterations.medium * 1e9).toFixed(1) + " ns/op]"); diff --git a/build.ce b/build.ce index e5027fde..99501c38 100644 --- a/build.ce +++ b/build.ce @@ -1,845 +1,112 @@ -// cell build [options] [actor] - Build cell binary -// -// Modes: -// cell build -d Build dynamic libraries for all packages -// cell build -d --target Build dynamic libraries for a specific target -// cell build -o Build static binary for current project -// cell build --target -o Build static binary for target platform +// cell build [options] - Build dynamic libraries locally for the current machine // -// The actor argument specifies the entry point (e.g., "accio" for accio.ce) -// If no actor is specified for static builds, builds cell itself. +// Usage: +// cell build Build dynamic libraries for all packages +// cell build -p Build dynamic library for specific package +// cell build -t Cross-compile dynamic libraries for target platform var build = use('build') var shop = use('shop') +var pkg_tools = use('package') var fd = use('fd') -var os = use('os') -var qop = use('qop') -var utf8 = use('utf8') -var json = use('json') -var targets = [ - "macos_arm64", - "macos_x86_64", - "linux", - "linux_arm64", - "windows", - "windows_i686", - "playdate", - "emscripten" -] - -// Parse arguments var target = null -var dynamic_mode = false -var static_only = false // -s flag: don't look outside pack for scripts -var actor = null -var output_name = null var target_package = null +var buildtype = 'release' for (var i = 0; i < args.length; i++) { - if (args[i] == '--target' || args[i] == '-t') { + if (args[i] == '-t' || args[i] == '--target') { if (i + 1 < args.length) { - target = args[i + 1] - i++ + target = args[++i] } else { - log.error("--target requires an argument") - log.console("Available targets: " + targets.join(', ')) + log.error('-t requires a target') $_.stop() } } else if (args[i] == '-p' || args[i] == '--package') { if (i + 1 < args.length) { - target_package = args[i + 1] - dynamic_mode = true - i++ + target_package = args[++i] } else { - log.error("-p requires a package name") + log.error('-p requires a package name') $_.stop() } - } else if (args[i] == '-d' || args[i] == '--dynamic') { - dynamic_mode = true - } else if (args[i] == '-s' || args[i] == '--static-only') { - static_only = true - } else if (args[i] == '-o' || args[i] == '--output') { + } else if (args[i] == '-b' || args[i] == '--buildtype') { if (i + 1 < args.length) { - output_name = args[i + 1] - i++ + buildtype = args[++i] + if (buildtype != 'release' && buildtype != 'debug' && buildtype != 'minsize') { + log.error('Invalid buildtype: ' + buildtype + '. Must be release, debug, or minsize') + $_.stop() + } } else { - log.error("-o requires an output name") + log.error('-b requires a buildtype (release, debug, minsize)') $_.stop() } - } else if (args[i] == '--help' || args[i] == '-h') { - log.console("Usage: cell build [options] [actor]") - log.console("Build cell binaries or dynamic libraries.") - log.console("") - log.console("Options:") - log.console(" -d, --dynamic Build dynamic libraries for all packages") - log.console(" -p, --package Build dynamic library for specific package") - log.console(" -o, --output Output name for static binary") - log.console(" -s, --static-only Build fully static (no external scripts)") - log.console(" --target, -t Cross-compile for target platform") - log.console("") - log.console("Arguments:") - log.console(" actor Entry point actor (e.g., 'accio' for accio.ce)") - log.console("") - log.console("Examples:") - log.console(" cell build -d Build dynamic libs for host") - log.console(" cell build -d --target windows Build dynamic libs for Windows") - log.console(" cell build -o myapp Build static binary 'myapp'") - log.console(" cell build --target windows -o myapp Cross-compile static binary") - log.console("") - log.console("Available targets: " + targets.join(', ')) + 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:") + log.console('Available targets:') + var targets = build.list_targets() for (var t = 0; t < targets.length; t++) { - log.console(" " + targets[t]) + log.console(' ' + targets[t]) } $_.stop() - } else if (!args[i].startsWith('-')) { - actor = args[i] } } -// Resolve target - if not specified for dynamic mode, detect host +// Detect target if not specified if (!target) { - var host_platform = os.platform() - var host_arch = os.arch ? os.arch() : 'arm64' // Default to arm64 if not available - if (host_platform == 'macOS' || host_platform == 'darwin') { - target = host_arch == 'x86_64' ? 'macos_x86_64' : 'macos_arm64' - } else if (host_platform == 'Linux') { - target = host_arch == 'x86_64' ? 'linux' : 'linux_arm64' - } else if (host_platform == 'Windows') { - target = 'windows' - } - if (target) log.console("Detected host target: " + target) + target = build.detect_host_target() + if (target) log.console('Target: ' + target) } if (target && !build.has_target(target)) { - log.console("Available targets: " + targets.join(', ')) - throw new Error("Invalid target: " + target) -} - -var target_system = build.get_target_system(target) -var platform = target_system || os.platform() -var dylib_ext = build.get_dylib_ext(target) - -// Get build directory for a target (with subdirectory for cross-compilation) -function get_target_build_dir() { - var base = shop.get_shop_path() + '/build' - if (target) return base + '/' + target - return base + '/host' -} - -// Filter out main*.c files (only for static builds) -function filter_main_files(files) { - return files.filter(function(f) { - var basename = f - var slash = f.lastIndexOf('/') - if (slash >= 0) basename = f.substring(slash + 1) - // Exclude main.c and main_*.c files - if (basename == 'main.c') return false - if (basename.startsWith('main_') && basename.endsWith('.c')) return false - return true - }) -} - -// Get the appropriate main file for a target (for static builds) -function get_main_file(cell_dir, target_name) { - // Check for target-specific main first - if (target_name) { - var target_main = cell_dir + '/source/main_' + target_name + '.c' - if (fd.is_file(target_main)) return target_main - } - // Fall back to generic main.c - var generic_main = cell_dir + '/source/main.c' - if (fd.is_file(generic_main)) return generic_main - return null -} - -// Resolve relative paths in LDFLAGS to absolute paths relative to a base directory -// This handles -L and -Wl,-rpath flags with relative paths -function resolve_ldflags(flags, base_dir) { - if (!flags || !base_dir) return flags - - var parts = flags.split(' ') - var result = [] - - for (var i = 0; i < parts.length; i++) { - var part = parts[i] - if (!part) continue - - // Handle -L - if (part.startsWith('-L') && part.length > 2) { - var path = part.substring(2) - if (!path.startsWith('/') && !path.startsWith('$')) { - part = '-L' + base_dir + '/' + path - } - } - // Handle -L (space separated) - else if (part == '-L' && i + 1 < parts.length) { - result.push(part) - i++ - var path = parts[i] - if (!path.startsWith('/') && !path.startsWith('$')) { - path = base_dir + '/' + path - } - result.push(path) - continue - } - - result.push(part) - } - - return result.join(' ') -} - -// Find cell package (core) -function find_cell_dir() { - // Check Shop Core Dir first (this is where cell is linked/installed) - var core_dir = shop.get_core_dir() - if (fd.is_file(core_dir + '/source/cell.c')) { - return core_dir - } - - // Check if we're in the cell directory itself - if (fd.is_file('source/cell.c') && fd.is_file('source/quickjs.c')) { - return fd.realpath('.') - } - - // Check for cell as a dependency - var config = shop.load_config() - if (config && config.dependencies && config.dependencies.cell) { - var pkg = config.dependencies.cell - var parsed = shop.parse_package(pkg) - var pkg_dir = shop.get_shop_path() + '/packages/' + parsed.path - if (fd.is_file(pkg_dir + '/source/cell.c')) { - return pkg_dir - } - } - - // Fallback: try ~/work/cell - var home_cell = os.getenv('HOME') + '/work/cell' - if (fd.is_file(home_cell + '/source/cell.c')) { - return home_cell - } - - return null -} - -// Build core cellmod.dylib (must be done first for dynamic builds) -function build_core_cellmod(cell_dir, build_dir) { - log.console("Building core cellmod" + dylib_ext + "...") - - var source_files = build.list_files(cell_dir + '/source') - var script_files = build.list_files(cell_dir + '/scripts') - - var all_files = [] - for (var i = 0; i < source_files.length; i++) { - all_files.push('source/' + source_files[i]) - } - for (var i = 0; i < script_files.length; i++) { - all_files.push('scripts/' + script_files[i]) - } - - // Select C files for target, then filter out main*.c - var c_files = build.select_c_files(all_files, target) - c_files = filter_main_files(c_files) - - log.console("Found " + text(c_files.length) + " C files for core") - - var cell_config = build.load_config(cell_dir) - var cflags = '-fPIC ' + build.get_flags(cell_config, platform, 'CFLAGS', target) - var ldflags = build.get_flags(cell_config, platform, 'LDFLAGS', target) - - var compile_options = { - target: target, - cflags: cflags, - includes: [cell_dir, cell_dir + '/source'], - defines: {}, - module_dir: cell_dir - } - - if (target == 'playdate') { - compile_options.defines.TARGET_PLAYDATE = true - } - - var objects = [] - for (var i = 0; i < c_files.length; i++) { - var src_rel = c_files[i] - var src_abs = cell_dir + '/' + src_rel - var obj = build_dir + '/core/' + src_rel + '.o' - - var needs_compile = true - if (fd.is_file(obj)) { - var src_stat = fd.stat(src_abs) - var obj_stat = fd.stat(obj) - if (src_stat && obj_stat && src_stat.mtime <= obj_stat.mtime) { - needs_compile = false - } - } - - if (needs_compile) { - var result = build.compile_file(src_abs, obj, compile_options) - if (!result) { - log.error("Failed to compile " + src_abs) - return null - } - } - objects.push(obj) - } - - // Link into shared library - var lib_name = build_dir + '/core/libcellmod' + dylib_ext - build.ensure_dir(build_dir + '/core') - - var link_flags = '-shared -fPIC' - if (ldflags) link_flags += ' ' + ldflags - - var objs_str = objects.join(' ') - var cc = build.get_cpp(target) - var cmd = cc + ' ' + link_flags + ' ' + objs_str + ' -o ' + lib_name - log.console(cmd) - - log.console("Linking " + lib_name) - var ret = os.system(cmd) - if (ret != 0) { - log.error("Failed to link core cellmod") - return null - } - - log.console("Built " + lib_name) - return lib_name -} - -// Build a package's cellmod.dylib -function build_package_cellmod(pkg_path, pkg_dir, build_dir, core_lib) { - var pkg_files = build.list_files(pkg_dir) - var c_files = build.select_c_files(pkg_files, target) - c_files = filter_main_files(c_files) - - if (c_files.length == 0) { - return null // No C files to compile - } - - log.console("Building " + pkg_path + " (" + text(c_files.length) + " C files)...") - - var pkg_config = build.load_config(pkg_dir) - var cflags = '-fPIC ' + build.get_flags(pkg_config, platform, 'CFLAGS', target) - var ldflags = build.get_flags(pkg_config, platform, 'LDFLAGS', target) - - var use_prefix = 'js_' + pkg_path.replace(/\//g, '_').replace(/\./g, '_').replace(/-/g, '_') + '_' - - var objects = [] - for (var i = 0; i < c_files.length; i++) { - var src_rel = c_files[i] - var src_abs = pkg_dir + '/' + src_rel - var obj = build_dir + '/packages/' + pkg_path + '/' + src_rel + '.o' - - var safe_name = src_rel.substring(0, src_rel.lastIndexOf('.')).replace(/\//g, '_').replace(/-/g, '_') - var use_name = use_prefix + safe_name + '_use' - - var compile_options = { - target: target, - cflags: cflags, - includes: [pkg_dir], - defines: { CELL_USE_NAME: use_name }, - module_dir: pkg_dir - } - - var needs_compile = true - if (fd.is_file(obj)) { - var src_stat = fd.stat(src_abs) - var obj_stat = fd.stat(obj) - if (src_stat && obj_stat && src_stat.mtime <= obj_stat.mtime) { - needs_compile = false - } - } - - if (needs_compile) { - var result = build.compile_file(src_abs, obj, compile_options) - if (!result) { - log.error("Failed to compile " + src_abs) - return false - } - } - objects.push(obj) - } - - // Link into shared library, linking against core cellmod - var lib_dir = build_dir + '/packages/' + pkg_path - build.ensure_dir(lib_dir) - var lib_name = lib_dir + '/libcellmod' + dylib_ext - - var link_flags = '-shared -fPIC' - if (platform == 'macOS' || platform == 'darwin') { - // Link against core cellmod - link_flags += ' -L' + build_dir + '/core -Wl,-rpath,@loader_path/../../core' - } else if (platform == 'Linux' || platform == 'linux') { - link_flags += ' -L' + build_dir + '/core -Wl,-rpath,$ORIGIN/../../core' - } else if (platform == 'Windows' || platform == 'windows') { - link_flags += ' -L' + build_dir + '/core' - } - link_flags += ' -lcellmod' - - if (ldflags) link_flags += ' ' + ldflags - - var objs_str = objects.join(' ') - var cc = build.get_cpp(target) - var cmd = cc + ' ' + link_flags + ' ' + objs_str + ' -o ' + lib_name - - log.console("Linking " + lib_name) - // Run from package directory so relative paths in LDFLAGS work - var ret = os.system('cd "' + pkg_dir + '" && ' + cmd) - if (ret != 0) { - log.error("Failed to link " + pkg_path) - return false - } - - return lib_name -} - -// ============================================================================ -// MAIN BUILD LOGIC -// ============================================================================ - -var cell_dir = find_cell_dir() -if (!cell_dir) { - log.error("Could not find cell source. Link cell to core or add as dependency.") + log.error('Invalid target: ' + target) + log.console('Available targets: ' + build.list_targets().join(', ')) $_.stop() } -log.console("Using cell from: " + cell_dir) -if (dynamic_mode) { - // ======================================================================== - // DYNAMIC MODE: Build cellmod.dylib for all packages - // ======================================================================== - var build_dir = get_target_build_dir() - build.ensure_dir(build_dir) - - // 1. Build core cellmod first (everything else links to it) - var core_lib = build_core_cellmod(cell_dir, build_dir) - if (!core_lib) { - log.error("Failed to build core cellmod") - $_.stop() - } - - // 2. Build cellmod for each installed package - var packages = shop.list_shop_packages() - var built_count = 1 // core - - for (var i = 0; i < packages.length; i++) { - var info = packages[i] - var pkg = info.package - if (!pkg || pkg == 'core') continue - if (target_package && pkg != target_package) continue - - var parsed = shop.parse_package(pkg) - var pkg_dir = shop.get_shop_path() + '/packages/' + parsed.path - - if (!fd.is_dir(pkg_dir)) { - log.console("Skipping " + pkg + " (not found)") - continue - } - - var result = build_package_cellmod(parsed.path, pkg_dir, build_dir, core_lib) - if (result) built_count++ - } - - // 3. Also build local project if we're in one - var local_config = shop.load_config() - if (!target_package && local_config && cell_dir != fd.realpath('.')) { - var local_files = build.list_files('.') - var local_c_files = build.select_c_files(local_files, target) - local_c_files = filter_main_files(local_c_files) - - if (local_c_files.length > 0) { - log.console("Building local project...") - var local_cflags = '-fPIC ' + build.get_flags(local_config, platform, 'CFLAGS', target) - var local_ldflags = build.get_flags(local_config, platform, 'LDFLAGS', target) - - var objects = [] - for (var i = 0; i < local_c_files.length; i++) { - var src = local_c_files[i] - var obj = build_dir + '/local/' + src + '.o' - - var safe_name = src.substring(0, src.lastIndexOf('.')).replace(/\//g, '_').replace(/-/g, '_') - var use_name = 'js_local_' + safe_name + '_use' - - var compile_options = { - target: target, - cflags: local_cflags, - includes: ['.'], - defines: { CELL_USE_NAME: use_name } - } - - var needs_compile = true - if (fd.is_file(obj)) { - var src_stat = fd.stat(src) - var obj_stat = fd.stat(obj) - if (src_stat && obj_stat && src_stat.mtime <= obj_stat.mtime) { - needs_compile = false - } - } - - if (needs_compile) { - var result = build.compile_file(src, obj, compile_options) - if (!result) { - log.error("Failed to compile " + src) - $_.stop() - } - } - objects.push(obj) - } - - // Link local cellmod - var local_lib = build_dir + '/local/libcellmod' + dylib_ext - build.ensure_dir(build_dir + '/local') - - var link_flags = '-shared -fPIC' - if (platform == 'macOS' || platform == 'darwin') { - link_flags += ' -L' + build_dir + '/core -Wl,-rpath,@loader_path/../core' - } else { - link_flags += ' -L' + build_dir + '/core -Wl,-rpath,$ORIGIN/../core' - } - link_flags += ' -lcellmod' - if (local_ldflags) link_flags += ' ' + local_ldflags - - var objs_str = objects.join(' ') - var cc = build.get_cc(target) - var cmd = cc + ' ' + link_flags + ' ' + objs_str + ' -o ' + local_lib - - log.console("Linking " + local_lib) - var ret = os.system(cmd) - if (ret != 0) { - log.error("Failed to link local cellmod") - $_.stop() - } - built_count++ - } - } - - // Copy libraries to standard build location for runtime loading - // The cell binary expects libraries at ~/.cell/build/packages/... not ~/.cell/build//packages/... - var standard_build_dir = shop.get_shop_path() + '/build' - - // Copy core - var core_src = build_dir + '/core/libcellmod' + dylib_ext - var core_dst_dir = standard_build_dir + '/core' - build.ensure_dir(core_dst_dir) - var core_dst = core_dst_dir + '/libcellmod' + dylib_ext - if (fd.is_file(core_src)) { - os.system('cp "' + core_src + '" "' + core_dst + '"') - } - - // Copy package libraries - for (var i = 0; i < packages.length; i++) { - var info = packages[i] - var pkg = info.package - if (!pkg || pkg == 'core') continue - - var parsed = shop.parse_package(pkg) - var pkg_src = build_dir + '/packages/' + parsed.path + '/libcellmod' + dylib_ext - if (fd.is_file(pkg_src)) { - var pkg_dst_dir = standard_build_dir + '/packages/' + parsed.path - build.ensure_dir(pkg_dst_dir) - var pkg_dst = pkg_dst_dir + '/libcellmod' + dylib_ext - os.system('cp "' + pkg_src + '" "' + pkg_dst + '"') - } - } - - // Copy local library if built - var local_src = build_dir + '/local/libcellmod' + dylib_ext - if (fd.is_file(local_src)) { - var local_dst_dir = standard_build_dir + '/local' - build.ensure_dir(local_dst_dir) - var local_dst = local_dst_dir + '/libcellmod' + dylib_ext - os.system('cp "' + local_src + '" "' + local_dst + '"') - } - - if (target_package && built_count == 1) { - log.error("Package '" + target_package + "' not found in shop or failed to build.") - } - - log.console("") - log.console("Dynamic build complete: " + text(built_count) + " libraries built") - log.console("Build directory: " + build_dir) - log.console("Libraries copied to: " + standard_build_dir) - -} else { - // ======================================================================== - // STATIC MODE: Build a static executable - // ======================================================================== - - // Must be run from a directory with cell.toml - var local_config = shop.load_config() - if (!local_config && cell_dir != fd.realpath('.')) { - log.error("Static builds must be run from a project directory with cell.toml") - log.error("Or run from the cell source directory itself") - $_.stop() - } - - var build_dir = get_target_build_dir() + '/static' - build.ensure_dir(build_dir) - - // Collect all C files from cell (including main.c for static builds) - var source_files = build.list_files(cell_dir + '/source') - var script_files = build.list_files(cell_dir + '/scripts') - - var all_files = [] - for (var i = 0; i < source_files.length; i++) { - all_files.push('source/' + source_files[i]) - } - for (var i = 0; i < script_files.length; i++) { - all_files.push('scripts/' + script_files[i]) - } - - var c_files = build.select_c_files(all_files, target) - // For static builds, we keep main.c (or target-specific main) - // But we need to filter out wrong main files - c_files = c_files.filter(function(f) { - var basename = f - var slash = f.lastIndexOf('/') - if (slash >= 0) basename = f.substring(slash + 1) - // Keep main.c, filter out main_*.c unless it matches our target - if (basename.startsWith('main_') && basename.endsWith('.c')) { - var main_target = basename.substring(5, basename.length - 2) - return target && target.indexOf(main_target) >= 0 - } - return true - }) - - var cell_config = build.load_config(cell_dir) - var cflags = build.get_flags(cell_config, platform, 'CFLAGS', target) - var ldflags = build.get_flags(cell_config, platform, 'LDFLAGS', target) - - var compile_options = { - target: target, - cflags: cflags, - includes: [cell_dir, cell_dir + '/source'], - defines: {}, - module_dir: cell_dir - } - - if (target == 'playdate') { - compile_options.defines.TARGET_PLAYDATE = true - } - - log.console("Compiling " + text(c_files.length) + " cell C files...") - - var objects = [] - for (var i = 0; i < c_files.length; i++) { - var src_rel = c_files[i] - var src_abs = cell_dir + '/' + src_rel - var obj = build_dir + '/cell/' + src_rel + '.o' - - var needs_compile = true - if (fd.is_file(obj)) { - var src_stat = fd.stat(src_abs) - var obj_stat = fd.stat(obj) - if (src_stat && obj_stat && src_stat.mtime <= obj_stat.mtime) { - needs_compile = false - } - } - - if (needs_compile) { - var result = build.compile_file(src_abs, obj, compile_options) - if (!result) { - log.error("Build failed") - $_.stop() - } - } - objects.push(obj) - } - - // Compile package C files - var packages = shop.list_packages() - for (var p = 0; p < packages.length; p++) { - var pkg = packages[p] - var parsed = shop.parse_package(pkg) - var pkg_dir = shop.get_shop_path() + '/packages/' + parsed.path - - if (!fd.is_dir(pkg_dir)) continue - - var pkg_files = build.list_files(pkg_dir) - var pkg_c_files = build.select_c_files(pkg_files, target) - pkg_c_files = filter_main_files(pkg_c_files) - - if (pkg_c_files.length == 0) continue - - log.console("Compiling " + text(pkg_c_files.length) + " C files from " + parsed.path) - - var pkg_config = build.load_config(pkg_dir) - var pkg_cflags = build.get_flags(pkg_config, platform, 'CFLAGS', target) - var pkg_ldflags = build.get_flags(pkg_config, platform, 'LDFLAGS', target) - if (pkg_ldflags) { - // Resolve relative paths in LDFLAGS to absolute paths - pkg_ldflags = resolve_ldflags(pkg_ldflags, pkg_dir) - if (ldflags != '') ldflags += ' ' - ldflags += pkg_ldflags - } - - var use_prefix = 'js_' + parsed.path.replace(/\//g, '_').replace(/\./g, '_').replace(/-/g, '_') + '_' - - for (var f = 0; f < pkg_c_files.length; f++) { - var src = pkg_dir + '/' + pkg_c_files[f] - var obj = build_dir + '/packages/' + parsed.path + '/' + pkg_c_files[f] + '.o' - - var safe_name = pkg_c_files[f].substring(0, pkg_c_files[f].lastIndexOf('.')).replace(/\//g, '_').replace(/-/g, '_') - var use_name = use_prefix + safe_name + '_use' - - var pkg_options = { - target: target, - cflags: pkg_cflags, - includes: [pkg_dir], - defines: { CELL_USE_NAME: use_name }, - module_dir: pkg_dir - } - - var needs_compile = true - if (fd.is_file(obj)) { - var src_stat = fd.stat(src) - var obj_stat = fd.stat(obj) - if (src_stat && obj_stat && src_stat.mtime <= obj_stat.mtime) { - needs_compile = false - } - } - - if (needs_compile) { - var result = build.compile_file(src, obj, pkg_options) - if (!result) { - log.error("Build failed") - $_.stop() - } - } - objects.push(obj) - } - } - - // Compile local C files (if not building from cell directory) - if (cell_dir != fd.realpath('.')) { - var local_files = build.list_files('.') - var local_c_files = build.select_c_files(local_files, target) - local_c_files = filter_main_files(local_c_files) - - if (local_c_files.length > 0) { - log.console("Compiling " + text(local_c_files.length) + " local C files") - - var local_cflags = build.get_flags(local_config, platform, 'CFLAGS', target) - var local_ldflags = build.get_flags(local_config, platform, 'LDFLAGS', target) - if (local_ldflags) { - // Resolve relative paths - local project is in current directory - local_ldflags = resolve_ldflags(local_ldflags, fd.realpath('.')) - if (ldflags != '') ldflags += ' ' - ldflags += local_ldflags - } - - for (var f = 0; f < local_c_files.length; f++) { - var src = local_c_files[f] - var obj = build_dir + '/local/' + src + '.o' - - var safe_name = src.substring(0, src.lastIndexOf('.')).replace(/\//g, '_').replace(/-/g, '_') - var use_name = 'js_local_' + safe_name + '_use' - - var local_options = { - target: target, - cflags: local_cflags, - includes: ['.'], - defines: { CELL_USE_NAME: use_name } - } - - var needs_compile = true - if (fd.is_file(obj)) { - var src_stat = fd.stat(src) - var obj_stat = fd.stat(obj) - if (src_stat && obj_stat && src_stat.mtime <= obj_stat.mtime) { - needs_compile = false - } - } - - if (needs_compile) { - var result = build.compile_file(src, obj, local_options) - if (!result) { - log.error("Build failed") - $_.stop() - } - } - objects.push(obj) - } - } - } - - log.console("Compiled " + text(objects.length) + " object files") - - // Link into executable - var exe_ext = build.get_exe_ext(target) - // Use 'cell_bin' to avoid conflict with 'cell' directory containing object files - var exe_name = build_dir + '/cell_bin' + exe_ext - - var link_options = { - target: target, - ldflags: ldflags, - libs: [] - } - - if (!target || platform == 'macOS' || platform == 'darwin') { - link_options.ldflags += ' -framework CoreFoundation -framework CFNetwork' - } else if (target == 'windows') { - link_options.libs.push('ws2_32') - link_options.libs.push('winmm') - } - - var result = build.link_executable(objects, exe_name, link_options) - if (!result) { - log.error("Linking failed") - $_.stop() - } - - // Create the pack (scripts) - var pack_path = build_dir + '/pack.qop' - var pack_files = [] - - // Include core scripts - var core_scripts = fd.readdir(cell_dir + '/scripts') - for (var i = 0; i < core_scripts.length; i++) { - var f = core_scripts[i] - if (f.endsWith('.cm') || f.endsWith('.ce')) { - pack_files.push({ path: f, source: cell_dir + '/scripts/' + f }) - } - } - - // Include package scripts - for (var p = 0; p < packages.length; p++) { - var pkg = packages[p] - var parsed = shop.parse_package(pkg) - var pkg_dir = shop.get_shop_path() + '/packages/' + parsed.path - var pkg_scripts = shop.list_modules(parsed.path) - for (var i = 0; i < pkg_scripts.length; i++) { - var pack_name = parsed.path + '/' + pkg_scripts[i] - pack_files.push({ path: pack_name, source: pkg_dir + '/' + pkg_scripts[i] }) - } - } - - // Include local scripts - if (cell_dir != fd.realpath('.')) { - var local_scripts = shop.list_modules() - for (var i = 0; i < local_scripts.length; i++) { - pack_files.push({ path: local_scripts[i], source: local_scripts[i] }) - } - } - - - var final_name = output_name || 'app' + exe_ext - - os.system('cp ' + exe_name + ' ' + final_name) - - log.console("") - log.console("Build complete: " + final_name) - - if (actor) { - log.console("Entry point: " + actor + ".ce") - } else { - log.console("Note: Run with an actor argument, e.g.: ./" + final_name + " main.ce") - } +var packages = shop.list_packages() +log.console('Preparing packages...') +for (var package of packages) { + if (package == 'core') continue + shop.extract(package) } -$_.stop() \ No newline at end of file +if (target_package) { + // Build single package + log.console('Building ' + target_package + '...') + try { + var lib = build.build_dynamic(target_package, target, buildtype) + if (lib) { + log.console('Built: ' + lib) + } + } catch (e) { + log.error('Build failed: ' + e) + $_.stop() + } +} else { + // Build all packages + log.console('Building all packages...') + var results = build.build_all_dynamic(target, buildtype) + + var success = 0 + var failed = 0 + for (var i = 0; i < results.length; i++) { + if (results[i].library) { + success++ + } else if (results[i].error) { + failed++ + } + } + + log.console('') + log.console('Build complete: ' + success + ' libraries built' + (failed > 0 ? ', ' + failed + ' failed' : '')) +} + +$_.stop() diff --git a/build.cm b/build.cm index c1994169..c47cc046 100644 --- a/build.cm +++ b/build.cm @@ -1,400 +1,89 @@ -// build.cm - Build utilities for compiling Cell projects and binaries +// build.cm - Simplified build utilities for Cell +// +// Key functions: +// Build.compile_file(pkg, file, target) - Compile a C file, returns object path +// Build.build_package(pkg, target) - Build all C files for a package +// Build.build_dynamic(pkg, target) - Build dynamic library for a package +// Build.build_static(packages, target, output) - Build static binary + var fd = use('fd') -var toml = use('toml') -var json = use('json') var crypto = use('crypto') var utf8 = use('utf8') -var os_mod = use('os') +var os = use('os') +var toolchains = use('toolchains') +var shop = use('shop') +var pkg_tools = use('package') var Build = {} -// Embedded cross-compilation toolchain configurations -// Note: build.cm uses shop to resolve paths -Build.toolchains = { - playdate: { - binaries: { - c: 'arm-none-eabi-gcc', - cpp: 'arm-none-eabi-g++', - ar: 'arm-none-eabi-ar', - strip: 'arm-none-eabi-strip', - objcopy: 'arm-none-eabi-objcopy', - ld: 'arm-none-eabi-gcc' - }, - host_machine: { - system: 'playdate', - cpu_family: 'arm', - cpu: 'cortex-m7', - endian: 'little' - }, - c_args: ['-mcpu=cortex-m7', '-mthumb', '-mfloat-abi=hard', '-mfpu=fpv5-sp-d16', '-fno-exceptions'], - c_link_args: ['-mcpu=cortex-m7', '-mthumb', '-mfloat-abi=hard', '-mfpu=fpv5-sp-d16', '-nostartfiles', "-T/Users/john/Developer/PlaydateSDK/C_API/buildsupport/link_map.ld"] - }, - windows: { - binaries: { - c: 'x86_64-w64-mingw32-gcc', - cpp: 'x86_64-w64-mingw32-g++', - ar: 'x86_64-w64-mingw32-ar', - windres: 'x86_64-w64-mingw32-windres', - strip: 'x86_64-w64-mingw32-strip' - }, - host_machine: { - system: 'windows', - cpu_family: 'x86_64', - cpu: 'x86_64', - endian: 'little' - }, - c_args: [], - c_link_args: [] - }, - windows_i686: { - binaries: { - c: 'i686-w64-mingw32-gcc', - cpp: 'i686-w64-mingw32-g++', - ar: 'i686-w64-mingw32-ar', - windres: 'i686-w64-mingw32-windres', - strip: 'i686-w64-mingw32-strip' - }, - host_machine: { - system: 'windows', - cpu_family: 'x86', - cpu: 'i686', - endian: 'little' - }, - c_args: [], - c_link_args: [] - }, - linux: { - binaries: { - c: 'zig cc -target x86_64-linux-musl', - cpp: 'zig c++ -target x86_64-linux-musl', - ar: 'zig ar', - strip: 'strip' - }, - host_machine: { - system: 'linux', - cpu_family: 'x86_64', - cpu: 'x86_64', - endian: 'little' - }, - c_args: [], - c_link_args: [] - }, - linux_arm64: { - binaries: { - c: 'zig cc -target aarch64-linux-musl', - cpp: 'zig c++ -target aarch64-linux-musl', - ar: 'zig ar', - strip: 'strip' - }, - host_machine: { - system: 'linux', - cpu_family: 'aarch64', - cpu: 'aarch64', - endian: 'little' - }, - c_args: [], - c_link_args: [] - }, - macos_arm64: { - binaries: { - c: 'clang -target arm64-apple-macos11', - cpp: 'clang++ -target arm64-apple-macos11', - ar: 'ar', - strip: 'strip' - }, - host_machine: { - system: 'darwin', - cpu_family: 'aarch64', - cpu: 'aarch64', - endian: 'little' - }, - c_args: ['-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk'], - c_link_args: ['-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk'] - }, - macos_x86_64: { - binaries: { - c: 'clang -target x86_64-apple-macos10.12', - cpp: 'clang++ -target x86_64-apple-macos10.12', - ar: 'ar', - strip: 'strip' - }, - host_machine: { - system: 'darwin', - cpu_family: 'x86_64', - cpu: 'x86_64', - endian: 'little' - }, - c_args: ['-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk'], - c_link_args: ['-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk'] - }, - ios_arm64: { - binaries: { - c: 'clang -target arm64-apple-ios12.0', - cpp: 'clang++ -target arm64-apple-ios12.0', - ar: 'ar', - strip: 'strip' - }, - host_machine: { - system: 'ios', - cpu_family: 'aarch64', - cpu: 'aarch64', - endian: 'little' - }, - c_args: ['-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk', '-fembed-bitcode'], - c_link_args: ['-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk', '-fembed-bitcode'] - }, - ios_simulator_arm64: { - binaries: { - c: 'clang -target arm64-apple-ios12.0-simulator', - cpp: 'clang++ -target arm64-apple-ios12.0-simulator', - ar: 'ar', - strip: 'strip' - }, - host_machine: { - system: 'ios', - cpu_family: 'aarch64', - cpu: 'aarch64', - endian: 'little' - }, - c_args: ['-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk'], - c_link_args: ['-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk'] - }, - ios_simulator_x86_64: { - binaries: { - c: 'clang -target x86_64-apple-ios12.0-simulator', - cpp: 'clang++ -target x86_64-apple-ios12.0-simulator', - ar: 'ar', - strip: 'strip' - }, - host_machine: { - system: 'ios', - cpu_family: 'x86_64', - cpu: 'x86_64', - endian: 'little' - }, - c_args: ['-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk'], - c_link_args: ['-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk'] - }, - tvos_arm64: { - binaries: { - c: 'clang -target arm64-apple-tvos12.0', - cpp: 'clang++ -target arm64-apple-tvos12.0', - ar: 'ar', - strip: 'strip' - }, - host_machine: { - system: 'tvos', - cpu_family: 'aarch64', - cpu: 'aarch64', - endian: 'little' - }, - c_args: ['-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS.sdk', '-fembed-bitcode'], - c_link_args: ['-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS.sdk', '-fembed-bitcode'] - }, - tvos_simulator_arm64: { - binaries: { - c: 'clang -target arm64-apple-tvos12.0-simulator', - cpp: 'clang++ -target arm64-apple-tvos12.0-simulator', - ar: 'ar', - strip: 'strip' - }, - host_machine: { - system: 'tvos', - cpu_family: 'aarch64', - cpu: 'aarch64', - endian: 'little' - }, - c_args: ['-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/AppleTVSimulator.platform/Developer/SDKs/AppleTVSimulator.sdk'], - c_link_args: ['-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/AppleTVSimulator.platform/Developer/SDKs/AppleTVSimulator.sdk'] - }, - tvos_simulator_x86_64: { - binaries: { - c: 'clang -target x86_64-apple-tvos12.0-simulator', - cpp: 'clang++ -target x86_64-apple-tvos12.0-simulator', - ar: 'ar', - strip: 'strip' - }, - host_machine: { - system: 'tvos', - cpu_family: 'x86_64', - cpu: 'x86_64', - endian: 'little' - }, - c_args: ['-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/AppleTVSimulator.platform/Developer/SDKs/AppleTVSimulator.sdk'], - c_link_args: ['-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/AppleTVSimulator.platform/Developer/SDKs/AppleTVSimulator.sdk'] - }, - watchos_arm64: { - binaries: { - c: 'clang -target arm64_32-apple-watchos5.0', - cpp: 'clang++ -target arm64_32-apple-watchos5.0', - ar: 'ar', - strip: 'strip' - }, - host_machine: { - system: 'watchos', - cpu_family: 'aarch64', - cpu: 'arm64_32', - endian: 'little' - }, - c_args: ['-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/WatchOS.platform/Developer/SDKs/WatchOS.sdk', '-fembed-bitcode'], - c_link_args: ['-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/WatchOS.platform/Developer/SDKs/WatchOS.sdk', '-fembed-bitcode'] - }, - watchos_simulator_arm64: { - binaries: { - c: 'clang -target arm64-apple-watchos5.0-simulator', - cpp: 'clang++ -target arm64-apple-watchos5.0-simulator', - ar: 'ar', - strip: 'strip' - }, - host_machine: { - system: 'watchos', - cpu_family: 'aarch64', - cpu: 'aarch64', - endian: 'little' - }, - c_args: ['-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/WatchSimulator.platform/Developer/SDKs/WatchSimulator.sdk'], - c_link_args: ['-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/WatchSimulator.platform/Developer/SDKs/WatchSimulator.sdk'] - }, - watchos_simulator_x86_64: { - binaries: { - c: 'clang -target x86_64-apple-watchos5.0-simulator', - cpp: 'clang++ -target x86_64-apple-watchos5.0-simulator', - ar: 'ar', - strip: 'strip' - }, - host_machine: { - system: 'watchos', - cpu_family: 'x86_64', - cpu: 'x86_64', - endian: 'little' - }, - c_args: ['-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/WatchSimulator.platform/Developer/SDKs/WatchSimulator.sdk'], - c_link_args: ['-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/WatchSimulator.platform/Developer/SDKs/WatchSimulator.sdk'] - }, - visionos_arm64: { - binaries: { - c: 'clang -target arm64-apple-xros1.0', - cpp: 'clang++ -target arm64-apple-xros1.0', - ar: 'ar', - strip: 'strip' - }, - host_machine: { - system: 'visionos', - cpu_family: 'aarch64', - cpu: 'aarch64', - endian: 'little' - }, - c_args: ['-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/XROS.platform/Developer/SDKs/XROS.sdk'], - c_link_args: ['-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/XROS.platform/Developer/SDKs/XROS.sdk'] - }, - visionos_simulator_arm64: { - binaries: { - c: 'clang -target arm64-apple-xros1.0-simulator', - cpp: 'clang++ -target arm64-apple-xros1.0-simulator', - ar: 'ar', - strip: 'strip' - }, - host_machine: { - system: 'visionos', - cpu_family: 'aarch64', - cpu: 'aarch64', - endian: 'little' - }, - c_args: ['-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/XRSimulator.platform/Developer/SDKs/XRSimulator.sdk'], - c_link_args: ['-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/XRSimulator.platform/Developer/SDKs/XRSimulator.sdk'] - }, - emscripten: { - binaries: { - c: 'emcc', - cpp: 'em++', - ar: 'emar', - strip: 'emstrip' - }, - host_machine: { - system: 'emscripten', - cpu_family: 'wasm32', - cpu: 'wasm32', - endian: 'little' - }, - c_args: [], - c_link_args: [] + +// ============================================================================ +// Sigil replacement +// ============================================================================ + +// Get the local directory for prebuilt libraries +function get_local_dir() { + return shop.get_local_dir() +} + +// Replace sigils in a string +// Currently supports: $LOCAL -> .cell/local full path +function replace_sigils(str) { + return str.replaceAll('$LOCAL', get_local_dir()) +} + +// Replace sigils in an array of flags +function replace_sigils_array(flags) { + var result = [] + for (var i = 0; i < flags.length; i++) { + result.push(replace_sigils(flags[i])) } + return result +} + +Build.get_local_dir = get_local_dir + +// ============================================================================ +// Toolchain helpers +// ============================================================================ + +Build.list_targets = function() { + return Object.keys(toolchains) } Build.has_target = function(target) { - return Build.toolchains[target] != null + return toolchains[target] != null } -// Get toolchain for a target (null = host) -Build.get_toolchain = function(target) { - if (!target) return null - return Build.toolchains[target] -} - -// Get compiler command for target -Build.get_cc = function(target) { - var tc = Build.get_toolchain(target) - if (tc && tc.binaries && tc.binaries.c) return tc.binaries.c - return 'cc' -} - -Build.get_cpp = function(target) { - var tc = Build.get_toolchain(target) - if (tc && tc.binaries && tc.binaries.cpp) return tc.binaries.cpp - return 'cpp' -} - -// Get archiver command for target -Build.get_ar = function(target) { - var tc = Build.get_toolchain(target) - if (tc && tc.binaries && tc.binaries.ar) return tc.binaries.ar - return 'ar' -} - -// Get extra C flags for target -Build.get_target_cflags = function(target) { - var tc = Build.get_toolchain(target) - if (tc && tc.c_args) return tc.c_args.join(' ') - return '' -} - -// Get extra link flags for target -Build.get_target_ldflags = function(target) { - var tc = Build.get_toolchain(target) - if (tc && tc.c_link_args) return tc.c_link_args.join(' ') - return '' -} - -// Get target system name -Build.get_target_system = function(target) { - var tc = Build.get_toolchain(target) - if (tc && tc.host_machine && tc.host_machine.system) return tc.host_machine.system - return os_mod.platform() -} - -// Get executable extension for target -Build.get_exe_ext = function(target) { - var sys = Build.get_target_system(target) - if (sys == 'windows') return '.exe' - return '' -} - -// Get shared library extension for target -Build.get_dylib_ext = function(target) { - var sys = Build.get_target_system(target) - if (sys == 'windows') return '.dll' - if (sys == 'darwin' || sys == 'macOS') return '.dylib' - return '.so' -} - -// Ensure directory exists -function ensure_dir(path) { - if (fd.stat(path).isDirectory) return true - var parts = path.split('/') - var current = '' - // Handle absolute paths (leading /) - if (path.startsWith('/')) { - current = '/' +Build.detect_host_target = function() { + var platform = os.platform() + var arch = os.arch ? os.arch() : 'arm64' + + if (platform == 'macOS' || platform == 'darwin') { + return arch == 'x86_64' ? 'macos_x86_64' : 'macos_arm64' + } else if (platform == 'Linux' || platform == 'linux') { + return arch == 'x86_64' ? 'linux' : 'linux_arm64' + } else if (platform == 'Windows' || platform == 'windows') { + return 'windows' } + return null +} + +// ============================================================================ +// Content-addressed build cache +// ============================================================================ + +function content_hash(str) { + return text(crypto.blake2(utf8.encode(str)), 'h') +} + +function get_build_dir() { + return shop.get_build_dir() +} + +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] + '/' @@ -402,347 +91,314 @@ function ensure_dir(path) { fd.mkdir(current) } } - return true } Build.ensure_dir = ensure_dir -// Get hash of a string -function get_hash(str) { - return text(crypto.blake2(utf8.encode(str)), 'h') -} +// ============================================================================ +// Compilation +// ============================================================================ -Build.get_hash = get_hash - -// List all files in a directory recursively -function list_files_recursive(dir, prefix, results) { - prefix = prefix || "" - results = results || [] +// Compile a single C file for a package +// Returns the object file path (content-addressed in .cell/build) +Build.compile_file = function(pkg, file, target, buildtype = 'release') { + var pkg_dir = shop.get_package_dir(pkg) + var src_path = pkg_dir + '/' + file - var list = fd.readdir(dir) - if (!list) return results - - for (var i = 0; i < list.length; i++) { - var item = list[i] - if (item == '.' || item == '..') continue - if (item.startsWith('.')) continue - - var full_path = dir + "/" + item - var rel_path = prefix ? prefix + "/" + item : item - - var st = fd.stat(full_path) - if (st.isDirectory) { - // Skip build directories - if (item == 'build' || item.startsWith('build_')) continue - list_files_recursive(full_path, rel_path, results) - } else { - results.push(rel_path) - } - } - return results -} - -Build.list_files = list_files_recursive - -// Select C files for a target, handling target-specific variants -// e.g., if target is 'playdate', prefer fd_playdate.c over fd.c -// Platform-specific files can be in any directory (source/ or scripts/) -Build.select_c_files = function(files, target) { - var c_files = [] - var target_suffix = target ? '_' + target : null - - // Known target suffixes for platform-specific files - var known_targets = ['playdate', 'windows', 'linux', 'macos', 'emscripten'] - - // First pass: collect all files and identify platform-specific ones - // Group by generic name (ignoring directory) to find cross-directory variants - var name_groups = {} // generic_name+ext -> { generics: [], variants: { target: file } } - - for (var i = 0; i < files.length; i++) { - var file = files[i] - if (!file.endsWith('.c') && !file.endsWith('.cpp')) continue - - var ext = file.endsWith('.cpp') ? '.cpp' : '.c' - var base = file.substring(0, file.length - ext.length) - var dir = '' - var name = base - var slash = base.lastIndexOf('/') - if (slash >= 0) { - dir = base.substring(0, slash + 1) - name = base.substring(slash + 1) - } - - // Check if this is a target-specific file - var is_target_specific = false - var target_name = null - var generic_name = name - - for (var t = 0; t < known_targets.length; t++) { - var suffix = '_' + known_targets[t] - if (name.endsWith(suffix)) { - is_target_specific = true - target_name = known_targets[t] - generic_name = name.substring(0, name.length - suffix.length) - break - } - } - - // Group key must include directory to avoid cross-directory collisions - // But still allow same-directory variants (e.g. source/scheduler.c vs source/scheduler_playdate.c) - var group_key = dir + generic_name + ext - if (!name_groups[group_key]) { - name_groups[group_key] = { generics: [], variants: {} } - } - - if (is_target_specific) { - // Platform-specific file - store by target name - name_groups[group_key].variants[target_name] = file - } else { - // Generic file - could have multiple in different directories - name_groups[group_key].generics.push(file) - } + if (!fd.is_file(src_path)) { + throw new Error('Source file not found: ' + src_path) } - // Second pass: select appropriate file from each group - for (var key in name_groups) { - var group = name_groups[key] - var selected = null - - // If we have a target, check for target-specific variant first - if (target && group.variants[target]) { - selected = group.variants[target] - } else if (group.generics.length > 0) { - // Use generic if no target-specific variant - // If multiple generics exist (shouldn't happen normally), use first one - selected = group.generics[0] - } else { - // No generic, only variants exist - // This handles cases like scheduler_threaded.c vs scheduler_playdate.c - // where there's no generic scheduler.c - if (target) { - // Only include if it's for our target - if (group.variants[target]) { - selected = group.variants[target] - } - } else { - // No target specified, prefer 'threaded' variant as default - if (group.variants['threaded']) { - selected = group.variants['threaded'] - } - } - } - - if (selected) { - c_files.push(selected) - } + // Get flags (with sigil replacement) + var cflags = replace_sigils_array(pkg_tools.get_flags(pkg, 'CFLAGS', target)) + var target_cflags = toolchains[target].c_args || [] + var cc = toolchains[target].c + + // Symbol name for this file + var sym_name = shop.c_symbol_for_file(pkg, file) + + // Build command + var cmd_parts = [cc, '-c', '-fPIC'] + + // Add buildtype-specific flags + if (buildtype == 'release') { + cmd_parts.push('-O3', '-DNDEBUG') + } else if (buildtype == 'debug') { + cmd_parts.push('-O2', '-g') + } else if (buildtype == 'minsize') { + cmd_parts.push('-Os', '-DNDEBUG') } - return c_files -} - -// Get build directory for a target -// Uses shop module to get the global shop path -Build.get_build_dir = function(target) { - var shop = use('shop') - var shop_path = shop.get_shop_path() - if (!target) return shop_path + '/build/static' - return shop_path + '/build/' + target -} - -// Compile a single C file -// Returns object path on success, null on failure -Build.compile_file = function(src_path, obj_path, options) { - options = options || {} - var target = options.target - var cflags = options.cflags || '' - var includes = options.includes || [] - var defines = options.defines || {} - var module_dir = options.module_dir || '.' + cmd_parts.push('-DCELL_USE_NAME=' + sym_name) + cmd_parts.push('-I"' + pkg_dir + '"') - var cc = Build.get_cc(target) - var target_cflags = Build.get_target_cflags(target) - - ensure_dir(obj_path.substring(0, obj_path.lastIndexOf('/'))) - - var full_cmd - - if (module_dir != '.') { - // If compiling in module dir, we need to adjust paths - - // Adjust includes - prefix with $HERE if relative - var include_str = '' - for (var i = 0; i < includes.length; i++) { - var inc = includes[i] - if (!inc.startsWith('/')) { - inc = '"$HERE/' + inc + '"' - } - include_str += ' -I' + inc + // Add package CFLAGS (resolve relative -I paths) + for (var i = 0; i < cflags.length; i++) { + var flag = cflags[i] + if (flag.startsWith('-I') && !flag.startsWith('-I/')) { + flag = '-I"' + pkg_dir + '/' + flag.substring(2) + '"' } - - var define_str = '' - for (var k in defines) { - if (defines[k] == true) { - define_str += ' -D' + k - } else { - define_str += ' -D' + k + '=' + defines[k] - } - } - - var compile_flags = ' -O3' + include_str + define_str - if (target_cflags) compile_flags += ' ' + target_cflags - if (cflags) compile_flags += ' ' + cflags - - // Adjust source path relative to module dir - var src_file = src_path - // If src_path is absolute, checking if it is inside module_dir - if (src_path.startsWith(module_dir + '/')) { - src_file = '"' + src_path.substring(module_dir.length + 1) + '"' - } else if (!src_path.startsWith('/')) { - src_file = '"$HERE/' + src_path + '"' - } else { - // It's absolute and outside module dir, use as is (quoted) - src_file = '"' + src_path + '"' - } - - // Adjust output path to be absolute/relative to HERE - var out_file = obj_path - if (!out_file.startsWith('/')) { - out_file = '"$HERE/' + out_file + '"' - } else { - out_file = '"' + out_file + '"' - } - - // If we're changing CWD to module_dir, we need to make sure out_file (if absolute) is still valid? - // Yes absolute paths work anywhere. - // Issue reported was: /Users/john/work/accio//Users/john/.cell/build/... - // This happens if we constructed a path that was "module_dir + '/' + absolute_path" somewhere. - // Likely in build.ce or shop.cm (build_package). - - var cc_cmd = cc + ' -c' + compile_flags + ' ' + src_file + ' -o ' + out_file - full_cmd = 'HERE=$(pwd); cd "' + module_dir + '" && ' + cc_cmd - } else { - // Standard compilation from current dir - var include_str = '' - for (var i = 0; i < includes.length; i++) { - include_str += ' -I' + includes[i] - } - - var define_str = '' - for (var k in defines) { - if (defines[k] == true) { - define_str += ' -D' + k - } else { - define_str += ' -D' + k + '=' + defines[k] - } - } - - var base_cmd = cc + ' -c' - var compile_flags = ' -O3' + include_str + define_str - if (target_cflags) compile_flags += ' ' + target_cflags - if (cflags) compile_flags += ' ' + cflags - - full_cmd = base_cmd + compile_flags + ' ' + src_path + ' -o ' + obj_path + cmd_parts.push(flag) } - log.console("Compiling " + src_path) - var ret = os_mod.system(full_cmd) + // Add target CFLAGS + for (var i = 0; i < target_cflags.length; i++) { + cmd_parts.push(target_cflags[i]) + } + + cmd_parts.push('"' + src_path + '"') + + var cmd_str = cmd_parts.join(' ') + + // Content hash: command + file content + var file_content = fd.slurp(src_path) + var hash_input = cmd_str + '\n' + text(file_content) + var hash = content_hash(hash_input) + + var build_dir = get_build_dir() + ensure_dir(build_dir) + var obj_path = build_dir + '/' + hash + + // Check if already compiled + if (fd.is_file(obj_path)) { + return obj_path + } + + // Compile + var full_cmd = cmd_str + ' -o "' + obj_path + '"' + log.console('Compiling ' + file) + var ret = os.system(full_cmd) if (ret != 0) { - log.error("Compilation failed: " + src_path) - return null + throw new Error('Compilation failed: ' + file) } return obj_path } -// Link object files into a static library -Build.link_static = function(objects, output, options) { - options = options || {} - var target = options.target +// Build all C files for a package +// Returns array of object file paths +Build.build_package = function(pkg, target, exclude_main, buildtype = 'release') { + var c_files = pkg_tools.get_c_files(pkg, target, exclude_main) + var objects = [] - var ar = Build.get_ar(target) + for (var i = 0; i < c_files.length; i++) { + var obj = Build.compile_file(pkg, c_files[i], target, buildtype) + objects.push(obj) + } - ensure_dir(output.substring(0, output.lastIndexOf('/'))) + return objects +} + +// ============================================================================ +// Dynamic library building +// ============================================================================ +// Build a dynamic library for a package +// Output goes to .cell/lib/. +Build.build_dynamic = function(pkg, target, buildtype = 'release') { + target = target || Build.detect_host_target() - var objs_str = objects.join(' ') - var cmd = ar + ' rcs ' + output + ' ' + objs_str + var objects = Build.build_package(pkg, target, true, buildtype) // exclude main.c - log.console("Creating static library " + output) - var ret = os_mod.system(cmd) - if (ret != 0) { - log.error("Archiving failed") + if (objects.length == 0) { + log.console('No C files in ' + pkg) return null } - return output + var lib_dir = shop.get_lib_dir() + ensure_dir(lib_dir) + + var lib_name = shop.lib_name_for_package(pkg) + var dylib_ext = toolchains[target].system == 'windows' ? '.dll' : (toolchains[target].system == 'darwin' ? '.dylib' : '.so') + var lib_path = lib_dir + '/' + lib_name + dylib_ext + + // Get link flags (with sigil replacement) + var ldflags = replace_sigils_array(pkg_tools.get_flags(pkg, 'LDFLAGS', target)) + var target_ldflags = toolchains[target].c_link_args || [] + var cc = toolchains[target].cpp || toolchains[target].c + var pkg_dir = shop.get_package_dir(pkg) + var local_dir = get_local_dir() + var tc = toolchains[target] + + // Build link command + var cmd_parts = [cc, '-shared', '-fPIC'] + + // Add rpath to find libraries in .cell/local at runtime + if (tc.system == 'darwin') { + cmd_parts.push('-Wl,-rpath,@loader_path/../local') + cmd_parts.push('-Wl,-rpath,' + local_dir) + } else if (tc.system == 'linux') { + 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 + } + + // Add .cell/local to library search path + cmd_parts.push('-L"' + local_dir + '"') + + for (var i = 0; i < objects.length; i++) { + 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 + '"') + } + } + + // Add LDFLAGS (resolve relative -L paths) + for (var i = 0; i < ldflags.length; i++) { + var flag = ldflags[i] + if (flag.startsWith('-L') && !flag.startsWith('-L/')) { + flag = '-L"' + pkg_dir + '/' + flag.substring(2) + '"' + } + cmd_parts.push(flag) + } + + for (var i = 0; i < target_ldflags.length; i++) { + cmd_parts.push(target_ldflags[i]) + } + + cmd_parts.push('-o', '"' + lib_path + '"') + + var cmd_str = cmd_parts.join(' ') + + log.console('Linking ' + lib_path) + var ret = os.system(cmd_str) + if (ret != 0) { + throw new Error('Linking failed: ' + pkg) + } + + return lib_path } -// Link object files into an executable -Build.link_executable = function(objects, output, options) { - options = options || {} - var target = options.target - var ldflags = options.ldflags || '' - var libs = options.libs || [] +// ============================================================================ +// Static binary building +// ============================================================================ + +// Build a static binary from multiple packages +// packages: array of package names +// output: output binary path +Build.build_static = function(packages, target, output, buildtype = 'release') { + target = target || Build.detect_host_target() - var cc = Build.get_cc(target) - var target_ldflags = Build.get_target_ldflags(target) - var exe_ext = Build.get_exe_ext(target) + var all_objects = [] + var all_ldflags = [] + var seen_flags = {} - if (!output.endsWith(exe_ext)) { + // Compile all packages + for (var i = 0; i < packages.length; i++) { + var pkg = packages[i] + var is_core = (pkg == 'core') + + // For core, include main.c; for others, exclude it + var objects = Build.build_package(pkg, target, !is_core, buildtype) + + for (var j = 0; j < objects.length; j++) { + all_objects.push(objects[j]) + } + + // Collect LDFLAGS (with sigil replacement) + var ldflags = replace_sigils_array(pkg_tools.get_flags(pkg, 'LDFLAGS', target)) + var pkg_dir = shop.get_package_dir(pkg) + + // Deduplicate based on the entire LDFLAGS string for this package + var ldflags_key = pkg + ':' + ldflags.join(' ') + if (!seen_flags[ldflags_key]) { + seen_flags[ldflags_key] = true + for (var j = 0; j < ldflags.length; j++) { + var flag = ldflags[j] + // Resolve relative -L paths + if (flag.startsWith('-L') && !flag.startsWith('-L/')) { + flag = '-L"' + pkg_dir + '/' + flag.substring(2) + '"' + } + all_ldflags.push(flag) + } + } + } + + if (all_objects.length == 0) { + throw new Error('No object files to link') + } + + // Link + var cc = toolchains[target].c + var target_ldflags = toolchains[target].c_link_args || [] + var exe_ext = toolchains[target].system == 'windows' ? '.exe' : '' + + if (!output.endsWith(exe_ext) && exe_ext) { output = output + exe_ext } - ensure_dir(output.substring(0, output.lastIndexOf('/'))) + var cmd_parts = [cc] - var objs_str = objects.join(' ') - var libs_str = '' - for (var i = 0; i < libs.length; i++) { - libs_str += ' -l' + libs[i] + for (var i = 0; i < all_objects.length; i++) { + cmd_parts.push('"' + all_objects[i] + '"') } - var link_flags = '' - if (target_ldflags) link_flags += ' ' + target_ldflags - if (ldflags) link_flags += ' ' + ldflags + for (var i = 0; i < all_ldflags.length; i++) { + cmd_parts.push(all_ldflags[i]) + } - var cmd = cc + ' ' + objs_str + libs_str + link_flags + ' -o ' + output + for (var i = 0; i < target_ldflags.length; i++) { + cmd_parts.push(target_ldflags[i]) + } - log.console("Linking " + output) - var ret = os_mod.system(cmd) + cmd_parts.push('-o', '"' + output + '"') + + var cmd_str = cmd_parts.join(' ') + + log.console('Linking ' + output) + var ret = os.system(cmd_str) if (ret != 0) { - log.error("Linking failed") - return null + throw new Error('Linking failed with command: ' + cmd_str) } + log.console('Built ' + output) return output } -// Get flags from config for a platform/target -// Checks: compilation[key], compilation[platform][key], compilation[target][key] -Build.get_flags = function(config, platform, key, target) { - var flags = '' - if (config.compilation && config.compilation[key]) { - flags += config.compilation[key] +// ============================================================================ +// Convenience functions +// ============================================================================ + +// Build dynamic libraries for all installed packages +Build.build_all_dynamic = function(target, buildtype = 'release') { + target = target || Build.detect_host_target() + + var packages = shop.list_packages() + var results = [] + + // Build core first + if (packages.indexOf('core') >= 0) { + try { + var lib = Build.build_dynamic('core', target) + results.push({ package: 'core', library: lib }) + } catch (e) { + log.error('Failed to build core: ' + e) + results.push({ package: 'core', error: e }) + } } - // Check platform (e.g., 'macOS', 'darwin', 'Linux') - if (config.compilation && config.compilation[platform] && config.compilation[platform][key]) { - if (flags != '') flags += ' ' - flags += config.compilation[platform][key] + + // Build other packages + for (var i = 0; i < packages.length; i++) { + var pkg = packages[i] + if (pkg == 'core') continue + + try { + var lib = Build.build_dynamic(pkg, target) + results.push({ package: pkg, library: lib }) + } catch (e) { + log.error('Failed to build ' + pkg + ': ') + log.error(e) + results.push({ package: pkg, error: e }) + } } - // Check target (e.g., 'macos_arm64', 'linux', 'windows') - if (target && target != platform && config.compilation && config.compilation[target] && config.compilation[target][key]) { - if (flags != '') flags += ' ' - flags += config.compilation[target][key] - } - return flags + + return results } -// Load config from a directory -// Config is now at /cell.toml (package root), not /.cell/cell.toml -Build.load_config = function(dir) { - var path = dir + '/cell.toml' - if (!fd.is_file(path)) return {} - var content = fd.slurp(path) - if (!content || !content.length) return {} - return toml.decode(text(content)) -} - -return Build \ No newline at end of file +return Build diff --git a/cell.toml b/cell.toml index d4134cbf..56273e16 100644 --- a/cell.toml +++ b/cell.toml @@ -7,7 +7,7 @@ CFLAGS = "-x objective-c" LDFLAGS = "-framework CoreFoundation -framework CFNetwork" [compilation.playdate] -CFLAGS = "-DMINIZ_NO_TIME -DTARGET_EXTENSION -DTARGET_PLAYDATE -I$PLAYDATE_SDK_PATH/C_API" +CFLAGS = "-DMINIZ_NO_TIME -DTARGET_EXTENSION -DTARGET_PLAYDATE -I$LOCAL/PlaydateSDK/C_API" [compilation.windows] LDFLAGS = "-lws2_32 -lwinmm -liphlpapi -lbcrypt -lwinhttp -static-libgcc -static-libstdc++" diff --git a/config.ce b/config.ce index 2c9c5ec4..7bbe26a2 100644 --- a/config.ce +++ b/config.ce @@ -1,7 +1,7 @@ // cell config - Manage system and actor configurations var toml = use('toml') -var shop = use('shop') +var pkg = use('package') var text = use('text') function print_help() { @@ -104,7 +104,7 @@ if (args.length == 0) { return } -var config = shop.load_config() +var config = pkg.load_config() if (!config) { log.error("Failed to load cell.toml") $_.stop() @@ -171,7 +171,7 @@ switch (command) { } set_nested(config, path, value) - shop.save_config(config) + pkg.save_config(config) log.console("Set " + key + " = " + format_value(value)) break @@ -230,7 +230,7 @@ switch (command) { var value = parse_value(value_str) set_nested(config.actors[actor_name], path, value) - shop.save_config(config) + pkg.save_config(config) log.console("Set actors." + actor_name + "." + key + " = " + format_value(value)) break diff --git a/fd.c b/fd.c index f6e2a4dd..373e36bc 100644 --- a/fd.c +++ b/fd.c @@ -568,13 +568,13 @@ JSC_CCALL(fd_slurpwrite, int fd = open(str, O_WRONLY | O_CREAT | O_TRUNC, 0644); JS_FreeCString(js, str); if (fd < 0) - return JS_ThrowInternalError(js, "open failed: %s", strerror(errno)); + return JS_ThrowInternalError(js, "open failed for %s: %s", str, strerror(errno)); ssize_t written = write(fd, data, len); close(fd); if (written != (ssize_t)len) - return JS_ThrowInternalError(js, "write failed: %s", strerror(errno)); + return JS_ThrowInternalError(js, "write failed for %s: %s", str, strerror(errno)); return JS_NULL; ) diff --git a/fetch.ce b/fetch.ce new file mode 100644 index 00000000..51c0316e --- /dev/null +++ b/fetch.ce @@ -0,0 +1,86 @@ +// cell fetch - Fetch package zips 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. +// +// Usage: +// cell fetch - Fetch all packages +// cell fetch - Fetch a specific package + +var shop = use('shop') + +// Parse arguments +var target_pkg = null + +for (var i = 0; i < args.length; 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 (!args[i].startsWith('-')) { + target_pkg = args[i] + } +} + +var all_packages = shop.list_packages() +var lock = shop.load_lock() +var packages_to_fetch = [] + +if (target_pkg) { + // Fetch specific package + if (!all_packages.includes(target_pkg)) { + log.error("Package not found: " + target_pkg) + $_.stop() + } + packages_to_fetch.push(target_pkg) +} else { + // Fetch all packages + packages_to_fetch = all_packages +} + +log.console("Fetching " + text(packages_to_fetch.length) + " package(s)...") + +var success_count = 0 +var skip_count = 0 +var fail_count = 0 + +for (var pkg of packages_to_fetch) { + var entry = lock[pkg] + + // Skip local packages + if (entry && entry.type == 'local') { + skip_count++ + continue + } + + // Skip core (handled separately) + if (pkg == 'core') { + skip_count++ + continue + } + + var result = shop.fetch(pkg) + if (result.ok) { + if (result.zip_blob) { + log.console("Fetched: " + pkg) + success_count++ + } else { + skip_count++ + } + } else { + log.error("Failed to fetch: " + pkg) + fail_count++ + } +} + +log.console("") +log.console("Fetch complete: " + text(success_count) + " fetched, " + text(skip_count) + " skipped, " + text(fail_count) + " failed") + +$_.stop() diff --git a/help/cellstructure.md b/help/cellstructure.md index 301bcc5d..a8b44cda 100644 --- a/help/cellstructure.md +++ b/help/cellstructure.md @@ -50,6 +50,8 @@ the cell shop looks like this: ... link.toml - temporary links for this cell shop lock.toml - manifest of installed packages + lib + cache build - content addressed hash diff --git a/internal/engine.cm b/internal/engine.cm index a2f995a9..fc11d0b0 100644 --- a/internal/engine.cm +++ b/internal/engine.cm @@ -43,36 +43,44 @@ var utf8 = use_embed('utf8') var js = use_embed('js') var fd = use_embed('fd') -// Get the core path from C runtime -var core_path = hidden.core_path -if (!core_path) { - throw new Error('Core path not set - cell shop not properly initialized') +// Get the shop path from HOME environment +var home = os.getenv('HOME') || os.getenv('USERPROFILE') +if (!home) { + throw new Error('Could not determine home directory') +} +var shop_path = home + '/.cell' +var packages_path = shop_path + '/packages' +var core_path = packages_path + '/core' + +if (!fd.is_dir(core_path)) { + throw new Error('Cell shop not found at ' + shop_path + '. Run "cell install" to set up.') } var use_cache = {} +use_cache['core/os'] = os // Load a core module from the file system function use_core(path) { - var cache_path = `2::${path}`; - if (use_cache[cache_path]) - return use_cache[cache_path]; + var cache_key = 'core/' + path + if (use_cache[cache_key]) + return use_cache[cache_key]; var sym = use_embed(path.replace('/','_')) - // Core scripts are now in .cell/core/scripts + // Core scripts are in packages/core/ var file_path = core_path + '/' + path + MOD_EXT if (fd.is_file(file_path)) { var script_blob = fd.slurp(file_path) var script = utf8.decode(script_blob) var mod = `(function setup_module($_){${script}})` - var fn = js.eval(path, mod) + var fn = js.eval('core:' + path, mod) var result = fn.call(sym); - use_cache[cache_path] = result; + use_cache[cache_key] = result; return result; } - use_cache[cache_path] = sym; + use_cache[cache_key] = sym; return sym; } @@ -205,47 +213,27 @@ function create_actor(desc = {id:guid()}) { var $_ = create_actor() -var shop = use('shop') os.use_cache = use_cache -shop.set_os(os, $_) +os.global_shop_path = shop_path + +var shop = use('shop') globalThis.use = shop.use - -var config = shop.load_config() - -// Get package name from a resolved path -function get_package_from_path(path) { - if (!path) return null - var modules_prefix = shop.get_shop_path() + '/modules/' - if (path.startsWith(modules_prefix)) { - var rest = path.substring(modules_prefix.length) - var slash_idx = rest.indexOf('/') - if (slash_idx > 0) { - return rest.substring(0, slash_idx) - } - } - return null -} - globalThis.json = use('json') var time = use('time') -var default_config = { +var config = { ar_timer: 60, actor_memory:0, net_service:0.1, reply_timeout:60, - main: false, + main: true } -config ??= {} -config.system ??= {} -config.system.__proto__ = default_config - cell.config = config -ENETSERVICE = config.system.net_service -REPLYTIMEOUT = config.system.reply_timeout +ENETSERVICE = config.net_service +REPLYTIMEOUT = config.reply_timeout /* When handling a message, the message appears like this: @@ -609,12 +597,12 @@ function turn(msg) //log.console(`FIXME: need to get main from config, not just set to true`) //log.console(`FIXME: remove global access (ie globalThis.use)`) //log.console(`FIXME: add freeze/unfreeze at this level, so we can do it (but scripts cannot)`) -actor_mod.register_actor(cell.id, turn, true, config.system.ar_timer) +actor_mod.register_actor(cell.id, turn, true, config.ar_timer) -if (config.system.actor_memory) - js.mem_limit(config.system.actor_memory) +if (config.actor_memory) + js.mem_limit(config.actor_memory) -if (config.system.stack_max) +if (config.stack_max) js.max_stacksize(config.system.stack_max); overling = cell.args.overling @@ -648,21 +636,6 @@ if (!program) { os.exit(1) } -// Find the package containing the program -// The program path is resolved relative to current directory -// Find the package containing the program -// The program path is resolved relative to current directory -// var package_dir = shop.set_current_package(program) -// if (package_dir) { -// // Reload config from the package -// config = shop.load_config() -// if (config) { -// config.system = config.system || {} -// config.system.__proto__ = default_config -// cell.config = config -// } -// } - function handle_actor_disconnect(id) { var greeter = greeters[id] if (greeter) { @@ -751,9 +724,15 @@ actor_mod.setname(cell.args.program) var prog = cell.args.program -// Resolve the main program path +var package = use('package') + var locator = shop.resolve_locator(cell.args.program + ".ce", null) +if (!locator) { + var pkg = package.find_package_dir(cell.args.program + ".ce") + locator = shop.resolve_locator(cell.args.program + ".ce", pkg) +} + if (!locator) throw new Error(`Main program ${cell.args.program} could not be found`) diff --git a/list.ce b/list.ce index 28dfb3ac..afbc461f 100644 --- a/list.ce +++ b/list.ce @@ -4,6 +4,7 @@ // cell list package -> list the packages for the package var shop = use('shop') +var pkg = use('package') var mode = 'local' var target_pkg = null @@ -56,23 +57,16 @@ if (mode == 'local') { if (all.length == 0) log.console(" (none)") } else if (mode == 'shop') { log.console("Shop Packages:") - var all = shop.list_shop_packages() - // Sort by package name or something + var all = shop.list_packages() - if (all.length == 0) { + if (all.length == 0) log.console(" (none)") - } else { - for (var i = 0; i < all.length; i++) { - var item = all[i] - var name = item.package || "unknown" - var ver = item.commit || item.type || "unknown" - log.console(" " + name + " [" + ver + "]") - } - } + else + all.forEach(package => log.console(" " + package)) } function print_deps(ctx) { - var deps = shop.dependencies(ctx) + var deps = pkg.dependencies(ctx) var aliases = [] for (var k in deps) aliases.push(k) aliases.sort() diff --git a/ls.ce b/ls.ce index d2752a57..4d47efdd 100644 --- a/ls.ce +++ b/ls.ce @@ -3,26 +3,15 @@ // otherwise, list the local one var shop = use('shop') +var package = use('package') var ctx = null -var pkg_name = "Local" - -if (args && args.length > 0) { - var alias = args[0] - ctx = shop.get_normalized_module(alias, null) - if (!ctx) { - log.console("Package '" + alias + "' not found in dependencies.") - $_.stop() - return - } - pkg_name = alias + " (" + ctx + ")" -} - -var modules = shop.list_modules(ctx) -log.console("Modules in " + pkg_name + ":") +var pkg = args[0] || package.find_package_dir('.') +var modules = package.list_modules(pkg) +var programs = package.list_programs(pkg) +log.console("Modules in " + pkg + ":") modules.sort() - if (modules.length == 0) { log.console(" (none)") } else { @@ -31,4 +20,15 @@ if (modules.length == 0) { } } +log.console("") +log.console("Programs in " + pkg + ":") +programs.sort() +if (programs.length == 0) { + log.console(" (none)") +} else { + for (var i = 0; i < programs.length; i++) { + log.console(" " + programs[i]) + } +} + $_.stop() diff --git a/pack.ce b/pack.ce new file mode 100644 index 00000000..20780ca7 --- /dev/null +++ b/pack.ce @@ -0,0 +1,123 @@ +// cell pack [options] - Build static binary for a package and its dependencies +// +// Usage: +// cell pack Build static binary for package (output: app) +// cell pack -o Specify output name +// cell pack -t Cross-compile for target platform + +var build = use('build') +var shop = use('shop') +var pkg_tools = use('package') + +var target = null +var output_name = 'app' +var target_package = null +var buildtype = 'release' + +if (args.length < 1) { + log.error('Usage: cell pack [options]') + log.error('') + log.error('Options:') + log.error(' -o, --output Output name for binary (default: app)') + log.error(' -t, --target Cross-compile for target platform') + log.error(' -b, --buildtype Build type: release, debug, minsize (default: release)') + log.error('') + log.error('Available targets: ' + build.list_targets().join(', ')) + $_.stop() + return +} + +target_package = args[0] + +for (var i = 1; i < args.length; i++) { + if (args[i] == '-t' || args[i] == '--target') { + if (i + 1 < args.length) { + target = args[++i] + } else { + log.error('-t requires a target') + $_.stop() + } + } else if (args[i] == '-o' || args[i] == '--output') { + if (i + 1 < args.length) { + output_name = args[++i] + } else { + log.error('-o requires an output name') + $_.stop() + } + } else if (args[i] == '-b' || args[i] == '--buildtype') { + if (i + 1 < args.length) { + buildtype = args[++i] + if (buildtype != 'release' && buildtype != 'debug' && buildtype != 'minsize') { + log.error('Invalid buildtype: ' + buildtype + '. Must be release, debug, or minsize') + $_.stop() + } + } else { + log.error('-b requires a buildtype (release, debug, minsize)') + $_.stop() + } + } else if (args[i] == '-h' || args[i] == '--help') { + log.console('Usage: cell pack [options]') + log.console('') + log.console('Options:') + log.console(' -o, --output Output name for binary (default: app)') + 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 { + log.error('Unknown option: ' + args[i]) + $_.stop() + } +} + +// Detect target if not specified +if (!target) { + target = build.detect_host_target() + if (target) log.console('Target: ' + target) +} + +if (target && !build.has_target(target)) { + log.error('Invalid target: ' + target) + log.console('Available targets: ' + build.list_targets().join(', ')) + $_.stop() +} + +// Prepare packages: core + dependencies + target package +var packages = ['core'] +var deps = pkg_tools.gather_dependencies(target_package) + +for (var i = 0; i < deps.length; i++) { + packages.push(deps[i]) +} +packages.push(target_package) + +// Remove duplicates +var unique_packages = [] +var seen = {} +for (var i = 0; i < packages.length; i++) { + if (!seen[packages[i]]) { + seen[packages[i]] = true + unique_packages.push(packages[i]) + } +} +packages = unique_packages + +log.console('Preparing packages...') +for (var package of packages) { + if (package == 'core') continue + shop.extract(package) +} + +log.console('Building static binary from ' + text(packages.length) + ' packages: ' + packages.join(', ')) + +try { + var result = build.build_static(packages, target, output_name, buildtype) + log.console('Build complete: ' + result) +} catch (e) { + log.error('Build failed: ') + log.error(e) + $_.stop() +} + +$_.stop() diff --git a/package.cm b/package.cm new file mode 100644 index 00000000..1cf92fed --- /dev/null +++ b/package.cm @@ -0,0 +1,305 @@ +var package = {} + +var fd = use('fd') +var toml = use('toml') +var os = use('os') + +function get_path(name) +{ + return os.global_shop_path + '/packages/' + name +} + +package.load_config = function(name) +{ + var config_path = get_path(name) + '/cell.toml' + if (!fd.is_file(config_path)) + throw new Error(`${config_path} isn't a path`) + + return toml.decode(text(fd.slurp(config_path))) +} + +package.save_config = function(name, config) +{ + var config_path = get_path(name) + '/cell.toml' + fd.slurpwrite(config_path, utf8.encode(toml.encode(config))) +} + +package.dependencies = function(name) +{ + return package.load_config(name).dependencies +} + +package.find_alias = function(name, locator) +{ + var config = package.load_config(name) + if (!config.dependencies) return null + + for (var alias in config.dependencies) + if (config.dependencies[alias] == locator) + return alias + + return null +} + +// alias is optional +package.add_dependency = function(name, locator, alias = locator) +{ + var config = package.load_config(name) + if (!config.dependencies) config.dependencies = {} + config.dependencies[alias] = locator + package.save_config(name, config) +} + +// locator can be a locator or alias +package.remove_dependency = function(name, locator) +{ + var config = package.load_config(name) + if (!config.dependencies) return + + if (config.dependencies[locator]) + delete config.dependencies[locator] + else { + var alias = package.find_alias(name, locator) + if (alias) + delete config.dependencies[alias] + } + package.save_config(name, config) +} + +package.find_package_dir = function(file) +{ + var absolute = fd.realpath(file) + + var dir = absolute + if (fd.is_file(dir)) { + var last_slash = dir.lastIndexOf('/') + if (last_slash > 0) dir = dir.substring(0, last_slash) + } + + while (dir && dir.length > 0) { + var toml_path = dir + '/cell.toml' + if (fd.is_file(toml_path)) { + return dir + } + var last_slash = dir.lastIndexOf('/') + if (last_slash <= 0) break + dir = dir.substring(0, last_slash) + } + + return null +} + +// For a given package, +// checks for an alias in path, and returns +// { package, path } +// so package + path is the full path +// Returns null if no alias is found for the given path +package.split_alias = function(name, path) +{ + if (!path || path.length == 0) { + return null + } + + var parts = path.split('/') + var first_part = parts[0] + + var config = package.load_config(name) + if (config.dependencies && config.dependencies[first_part]) { + var dep_locator = config.dependencies[first_part] + var remaining_path = parts.slice(1).join('/') + return { package: dep_locator, path: remaining_path } + } + + return null +} + +package.gather_dependencies = function(name) +{ + var all_deps = {} + var visited = {} + + function gather_recursive(pkg_name) { + if (visited[pkg_name]) return + visited[pkg_name] = true + + var deps = package.dependencies(pkg_name) + if (!deps) return + + for (var alias in deps) { + var locator = deps[alias] + if (!all_deps[locator]) { + all_deps[locator] = true + gather_recursive(locator) + } + } + } + + gather_recursive(name) + return Object.keys(all_deps) +} + +package.list_files = function(pkg) { + var dir = get_path(pkg) + + var files = [] + + var walk = function(current_dir, current_prefix) { + var list = fd.readdir(current_dir) + if (!list) return + + for (var i = 0; i < list.length; i++) { + var item = list[i] + if (item == '.' || item == '..') continue + if (item.startsWith('.')) continue + + // Skip build directories in root + + var full_path = current_dir + "/" + item + var rel_path = current_prefix ? current_prefix + "/" + item : item + + var st = fd.stat(full_path) + if (st.isDirectory) { + walk(full_path, rel_path) + } else { + files.push(rel_path) + } + } + } + + if (fd.is_dir(dir)) { + walk(dir, "") + } + return files +} + +package.list_modules = function(name) { + var files = package.list_files(name) + var modules = [] + for (var i = 0; i < files.length; i++) { + if (files[i].endsWith('.cm')) { + modules.push(files[i].substring(0, files[i].length - 3)) + } + } + return modules +} + +package.list_programs = function(name) { + var files = package.list_files(name) + var programs = [] + for (var i = 0; i < files.length; i++) { + if (files[i].endsWith('.ce')) { + programs.push(files[i].substring(0, files[i].length - 3)) + } + } + return programs +} + +// Get flags from cell.toml for a package +// flag_type is 'CFLAGS' or 'LDFLAGS' +// target is optional (e.g., 'macos_arm64', 'playdate') +// Returns an array of flag strings +package.get_flags = function(name, flag_type, target) { + var config = package.load_config(name) + var flags = [] + + // Base flags + if (config.compilation && config.compilation[flag_type]) { + var base = config.compilation[flag_type] + flags = flags.concat(base.split(/\s+/).filter(function(f) { return f.length > 0 })) + } + + // Target-specific flags + if (target && config.compilation && config.compilation[target] && config.compilation[target][flag_type]) { + var target_flags = config.compilation[target][flag_type] + flags = flags.concat(target_flags.split(/\s+/).filter(function(f) { return f.length > 0 })) + } + + return flags +} + +// Get all C files for a package, handling target-specific variants +// Excludes main.c for dynamic builds (when exclude_main is true) +// Handles patterns like fd.c vs fd_playdate.c +package.get_c_files = function(name, target, exclude_main) { + var toolchains = use('toolchains') + var known_targets = Object.keys(toolchains) + var files = package.list_files(name) + + // Group files by their base name (without target suffix) + var groups = {} // base_key -> { generic: file, variants: { target: file } } + + for (var i = 0; i < files.length; i++) { + var file = files[i] + if (!file.endsWith('.c') && !file.endsWith('.cpp')) continue + + var ext = file.endsWith('.cpp') ? '.cpp' : '.c' + var base = file.substring(0, file.length - ext.length) + var dir = '' + var name_part = base + var slash = base.lastIndexOf('/') + if (slash >= 0) { + dir = base.substring(0, slash + 1) + name_part = base.substring(slash + 1) + } + + // Check for target suffix + var is_variant = false + var variant_target = null + var generic_name = name_part + + for (var t = 0; t < known_targets.length; t++) { + var suffix = '_' + known_targets[t] + if (name_part.endsWith(suffix)) { + is_variant = true + variant_target = known_targets[t] + generic_name = name_part.substring(0, name_part.length - suffix.length) + break + } + } + + var group_key = dir + generic_name + ext + if (!groups[group_key]) { + groups[group_key] = { generic: null, variants: {} } + } + + if (is_variant) { + groups[group_key].variants[variant_target] = file + } else { + groups[group_key].generic = file + } + } + + // Select appropriate file from each group + var result = [] + for (var key in groups) { + var group = groups[key] + var selected = null + + // Prefer target-specific variant if available + if (target && group.variants[target]) { + selected = group.variants[target] + } else if (group.generic) { + selected = group.generic + } + + if (selected) { + // Skip main.c if requested + if (exclude_main) { + var basename = selected + var s = selected.lastIndexOf('/') + if (s >= 0) basename = selected.substring(s + 1) + if (basename == 'main.c' || basename.startsWith('main_')) continue + } + result.push(selected) + } + } + + return result +} + +// Get the absolute path for a package +package.get_dir = function(name) { + return get_path(name) +} + +return package diff --git a/replace.ce b/replace.ce deleted file mode 100644 index 96330f3f..00000000 --- a/replace.ce +++ /dev/null @@ -1,29 +0,0 @@ -// cell replace [package] [path] -// replace a package with a local directory - -var shop = use('shop') - -if (args.length < 2) { - log.console("Usage: cell replace ") - log.console(" cell replace --remove") - $_.stop() -} - -var pkg = args[0] -var path = args[1] - -if (path == '--remove') { - if (shop.remove_replacement(pkg)) { - log.console("Replacement removed.") - } else { - log.console("Failed to remove replacement.") - } -} else { - if (shop.add_replacement(pkg, path)) { - log.console("Replacement added.") - } else { - log.console("Failed to add replacement.") - } -} - -$_.stop() \ No newline at end of file diff --git a/shop.cm b/shop.cm index 94525bc5..f8820acf 100644 --- a/shop.cm +++ b/shop.cm @@ -8,7 +8,8 @@ var js = use('js') var crypto = use('crypto') var utf8 = use('utf8') var blob = use('blob') -var build_utils = use('build') +var pkg_tools = use('package') +var os = use('os') var core = "core" @@ -25,6 +26,19 @@ function put_into_cache(content, obj) fd.slurpwrite(path, obj) } +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) + } + } +} + function content_hash(content) { return text(crypto.blake2(content), 'h') @@ -32,15 +46,11 @@ function content_hash(content) function hash_path(content) { - return get_global_build_dir() + '/' + content_hash(content) + return global_shop_path + '/build' + '/' + content_hash(content) } var Shop = {} -// Global shop path - this is where all packages, builds, and cache are stored -// Located at ~/.cell on the user's machine -var global_shop_path = null - var SCOPE_LOCAL = 0 var SCOPE_PACKAGE = 1 var SCOPE_CORE = 2 @@ -50,185 +60,14 @@ var ACTOR_EXT = '.ce' var dylib_ext = '.so' // Default extension -var os -var use_cache -var platform -var $_ - -// Initialize the global shop path -function init_global_shop() { - if (global_shop_path) return global_shop_path - - var home = os.getenv('HOME') - if (!home) { - // Try Windows-style - home = os.getenv('USERPROFILE') - } - if (!home) { - throw new Error('Could not determine home directory. Set HOME environment variable.') - } - - global_shop_path = home + '/.cell' - - // Verify the shop exists - if (!fd.is_dir(global_shop_path)) { - throw new Error('Cell shop not found at ' + global_shop_path + '. Run "cell install" to set up.') - } - - return global_shop_path -} - -// Get the global shop path -Shop.get_shop_path = function() { - return init_global_shop() -} - -// Find the package directory containing a file by looking for cell.toml -// Returns the absolute path to the package directory, or null if not in a package -Shop.find_package_dir = function(file_path) { - if (!file_path) return null - - // Walk up directories looking for cell.toml - var dir = file_path - if (fd.is_file(dir)) { - // if it's a file, start from parent - var last_slash = dir.lastIndexOf('/') - if (last_slash > 0) dir = dir.substring(0, last_slash) - } - - while (dir && dir.length > 0) { - var toml_path = dir + '/cell.toml' - if (fd.is_file(toml_path)) { - return fd.realpath(dir) - } - var last_slash = dir.lastIndexOf('/') - if (last_slash <= 0) break - dir = dir.substring(0, last_slash) - } - - // Check current directory as fallback - if (fd.is_file('cell.toml')) { - return fd.realpath('.') - } - - return null -} - -function slurp_package_file(file, package) -{ - return fd.slurp(get_packages_dir() + '/' + package + '/' + file) -} - -// Link a local package into the shop -function ensure_package_link(abs_path) { - if (!abs_path || !abs_path.startsWith('/')) return false - - var packages_dir = get_packages_dir() - var target_link = packages_dir + abs_path - - // If link already exists and points to correct place, we are good - if (fd.is_link(target_link)) { - var points_to = fd.readlink(target_link) - if (points_to == abs_path) { - update_local_lock(abs_path) - return true - } - // Incorrect link, remove it - fd.unlink(target_link) - } else if (fd.is_dir(target_link)) { - // If it's a real directory, that's weird for a local package mirror, but let's assume it might be a copy? - // User wants symlinks. - // If it's not a link, maybe we should leave it or warn? - // Use user instructions: "shop should ... ensure it's there ... symlink local dirs" - // safely assuming we can replace if it's not the right thing might be dangerous if user put real files there. - // For now, if it's a dir, check if it's the package itself? - // simpler: proceed to link logic - } - - // Create parent dirs - var parent = target_link.substring(0, target_link.lastIndexOf('/')) - ensure_dir(parent) - - try { - fd.symlink(abs_path, target_link) - // log.console("Linked " + abs_path + " -> " + target_link) - update_local_lock(abs_path) - return true - } catch (e) { - log.error("Failed to link package " + abs_path + ": " + e) - return false - } -} - -function update_local_lock(abs_path) { - var lock = Shop.load_lock() - var name = abs_path.split('/').pop() - - // Check if we can find a better name from cell.toml - var toml_path = abs_path + '/cell.toml' - if (fd.is_file(toml_path)) { - // We could parse it, but for now directory name is usually the package alias - } - - lock[name] = { - package: abs_path, - type: 'local', - updated: time.number() - } - Shop.save_lock(lock) -} - -Shop.ensure_package_link = ensure_package_link - -// Set the current package context from a program path - - -Shop.set_os = function(o, $guy) -{ - os = o - $_ = $guy - use_cache = os.use_cache - - platform = os.platform() - if (platform == 'macOS') dylib_ext = '.dylib' - else if (platform == 'Windows') dylib_ext = '.dll' - - // Initialize the global shop - init_global_shop() -} - -var config = null - -// Get the config path for a package -// If package_path is null, uses current_package_path -function get_config_path(package_path) { - if (package_path) { - return package_path + '/cell.toml' - } - // Fallback to current directory - return 'cell.toml' -} - -// Get the lock file path (in the global shop) -function get_lock_path() { - return global_shop_path + '/lock.toml' -} +var use_cache = os.use_cache +var global_shop_path = os.global_shop_path // Get the packages directory (in the global shop) function get_packages_dir() { return global_shop_path + '/packages' } -// Get the cache directory (in the global shop) -function get_cache_dir() { - return global_shop_path + '/cache' -} - -// Get the build directory (in the global shop) -function get_global_build_dir() { - return global_shop_path + '/build' -} - // Get the core directory (in the global shop) Shop.get_core_dir = function() { return get_packages_dir() + '/' + core_package @@ -256,78 +95,53 @@ function get_import_package(name) { return null } +function package_in_shop(package) { + var lock = Shop.load_lock() + return package in lock +} + +function abs_path_to_package(package_dir) +{ + if (!fd.is_file(package_dir + '/cell.toml')) + throw new Error('Not a valid package directory (no cell.toml): ' + package_dir) + + var packages_prefix = get_packages_dir() + '/' + if (package_dir.startsWith(packages_prefix)) + return package_dir.substring(packages_prefix.length) + + // in this case, the dir is the package + if (package_in_shop(package_dir)) + return package_dir + + return null +} + +// given a file, find the absolute path, package name, and import name Shop.file_info = function(file) { var info = { - path: file, + path: file, is_module: false, is_actor: false, package: null, name: null } - if (file.endsWith(MOD_EXT)) { + if (file.endsWith(MOD_EXT)) info.is_module = true - } else if (file.endsWith(ACTOR_EXT)) { + else if (file.endsWith(ACTOR_EXT)) info.is_actor = true - } - // Strip extension for name - var name_without_ext = file - if (info.is_module) { - name_without_ext = file.substring(0, file.length - MOD_EXT.length) - } else if (info.is_actor) { - name_without_ext = file.substring(0, file.length - ACTOR_EXT.length) - } - - // Check if file is in a package (in global shop packages dir) - var packages_prefix = get_packages_dir() + '/' - if (file.startsWith(packages_prefix)) { - var rest = file.substring(packages_prefix.length) - - // Get all packages and find which one matches this path - // With the new structure, everything in packages/ is a package - // gitea.pockle.world/user/repo/file.cm - - var parts = rest.split('/') - // Heuristic: packages form is host/user/repo - if (parts.length >= 3) { - var pkg_path = parts.slice(0, 3).join('/') - info.package = pkg_path - if (rest.length > pkg_path.length) { - info.name = rest.substring(pkg_path.length + 1) - } else { - info.name = "" // root? - } - } else { - // fallback - info.package = rest - } - - if (info.name) { - if (info.is_module && info.name.endsWith(MOD_EXT)) - info.name = info.name.substring(0, info.name.length - MOD_EXT.length) - if (info.is_actor && info.name.endsWith(ACTOR_EXT)) - info.name = info.name.substring(0, info.name.length - ACTOR_EXT.length) - } + // Find package directory and determine package name + var pkg_dir = pkg_tools.find_package_dir(file) + if (pkg_dir) { + info.package = abs_path_to_package(pkg_dir) - } else { - // Check if it's in any other package via cell.toml lookup - var pkg_dir = Shop.find_package_dir(file) - if (pkg_dir) { - info.package = pkg_dir - if (file.startsWith(pkg_dir + '/')) { - var rel = file.substring(pkg_dir.length + 1) - info.name = rel - if (info.is_module && info.name.endsWith(MOD_EXT)) - info.name = info.name.substring(0, info.name.length - MOD_EXT.length) - if (info.is_actor && info.name.endsWith(ACTOR_EXT)) - info.name = info.name.substring(0, info.name.length - ACTOR_EXT.length) - } - } else { - info.package = 'local' // Should we keep 'local' for truly orphan files? - info.name = name_without_ext - } - } + info.name = file.substring(pkg_dir.length + 1) + if (info.is_actor) + info.name = info.name.substring(0, info.name.length - ACTOR_EXT.length) + else if (info.is_module) + info.name = info.name.substring(0, info.name.length - MOD_EXT.length) + } return info } @@ -339,43 +153,33 @@ function get_import_name(path) return parts.slice(1).join('/') } -function get_aliased_path(path, pkg) -{ - +// Given a path like 'prosperon/sprite' and a package context, +// resolve the alias 'prosperon' to its canonical package name +function get_aliased_package(path, package_context) { + if (!package_context) return null + var alias = pkg_tools.split_alias(package_context, path) + if (alias) return alias.package + return null } -function validate_package_name(pkg) -{ - if (!pkg) return false - - // Check for unsafe filesystem characters (except @ and . which are allowed) - // Disallow: : \ ? * " < > | and control characters - if (/[:\\\?\*"<>\|]/.test(pkg)) return false - - // Disallow control characters (0-31) - for (var i = 0; i < pkg.length; i++) { - var code = pkg.charCodeAt(i) - if (code < 32) return false - } - - // Disallow empty segments or trailing/leading slashes - if (pkg.startsWith('/') || pkg.endsWith('/')) return false - if (pkg.includes('//')) return false - - // Disallow . and .. as path segments - var parts = pkg.split('/') - for (var i = 0; i < parts.length; i++) { - if (parts[i] == '' || parts[i] == '.' || parts[i] == '..') return false - } - - return true +// Same as get_aliased_package but just returns the package for the alias part +function get_canonical_package(alias, package_context) { + if (!package_context) return null + var result = pkg_tools.split_alias(package_context, alias + '/dummy') + if (result) return result.package + return null } // return the safe path for the package // guaranteed to be validated -function get_package_path(pkg) +function safe_package_path(pkg) { - return pkg.replace('@', '_') + return pkg.replaceAll('@', '_') +} + +function package_cache_path(pkg) +{ + return global_shop_path + '/cache/' + pkg.replaceAll('/', '_').replaceAll('@', '_') } function get_shared_lib_path() @@ -383,122 +187,13 @@ function get_shared_lib_path() return get_global_build_dir() + '/' + 'lib' } -function get_aliased_package(alias, pkg) -{ - if (alias.split('/').length > 1) - throw new Error(`alias ${alias} is invalid, as it contains slashes`) - - var cfg = Shop.load_config(pkg) - - if (!cfg.dependencies) - return null - - return cfg.dependencies[alias] -} - -// taking the package ctx into account, find the canonical name -function get_canonical_package(alias, ctx) { - var cfg = Shop.load_config(ctx) - - if (!cfg || !cfg.dependencies) - return null - - var pkg = cfg.dependencies[alias] - if (!pkg) - return null - - var parsed = Shop.parse_package(pkg) - if (!parsed) - return null - - return parsed.path -} - -function get_import_dl(name) { - var pkg = get_import_package(name) - if (!pkg) return null - if (open_dl[pkg]) return open_dl[pkg] - var dlpath = get_packages_dir() + '/' + pkg + '/' + pkg + dylib_ext - var dl = os.dylib_open(dlpath) - if (dl) { - open_dl[pkg] = dl - return dl - } - return null -} - -Shop.get_c_symbol = function get_c_symbol(name) { - var dl = get_import_dl(name) - var symname = `js_${name.replace('/', '_')}_use` - - if (dl) - return os.dylib_symbol(dl, symname) - else - return os.load_internal(symname) -} - -// Use ensure_dir from build_utils -var ensure_dir = build_utils.ensure_dir - -Shop.load_config = function(module) { - var content - var config_path - if (!module) { - config_path = get_config_path() - if (!fd.is_file(config_path)) - return null - content = fd.slurp(config_path) - } else { - // Module config is at //cell.toml - var module_path = get_packages_dir() + '/' + module + '/cell.toml' - if (!fd.is_file(module_path)) - return null - - content = fd.slurp(module_path) - } - - if (!(content instanceof blob)) { - log.console(`critical error`) - for (var k in content) - log.console(k) - throw new Error("fucked up bad") - } - - if (!content.length) return {} - var cfg = toml.decode(text(content)) - if (cfg.dependencies) { - var changed = false - for (var k in cfg.dependencies) { - if (cfg.dependencies[k].startsWith('https://')) { - cfg.dependencies[k] = cfg.dependencies[k].substring(8) - changed = true - } else if (cfg.dependencies[k].includes('://')) { - // If it has another protocol, we should probably strip it too or warn - // But for now assuming mostly https/http - var parts = cfg.dependencies[k].split('://') - if (parts.length == 2) { - cfg.dependencies[k] = parts[1] - changed = true - } - } - } - - if (changed && !module) { - Shop.save_config(cfg) - } - } - return cfg -} - -// Save cell.toml configuration -Shop.save_config = function(config) { - var config_path = get_config_path() - fd.slurpwrite(config_path, utf8.encode(toml.encode(config))); -} - // Load lock.toml configuration (from global shop) +var _lock = null Shop.load_lock = function() { - var path = get_lock_path() + if (_lock) + return _lock + + var path = global_shop_path + '/lock.toml' if (!fd.is_file(path)) return {} @@ -506,22 +201,22 @@ Shop.load_lock = function() { var content = text(fd.slurp(path)) if (!content.length) return {} - var lock = toml.decode(content) + _lock = toml.decode(content) - return lock + return _lock } // Save lock.toml configuration (to global shop) Shop.save_lock = function(lock) { - var path = get_lock_path() - ensure_dir(global_shop_path) + var path = global_shop_path + '/lock.toml' 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 = get_links_path() + var path = global_shop_path + '/link.toml' if (!fd.is_file(path)) { link_cache = {} return link_cache @@ -541,8 +236,7 @@ Shop.load_links = function() { Shop.save_links = function(links) { link_cache = links var cfg = { links: links } - var path = get_links_path() - ensure_dir(global_shop_path) + var path = global_shop_path + '/link.toml' fd.slurpwrite(path, utf8.encode(toml.encode(cfg))) } @@ -569,102 +263,11 @@ Shop.clear_links = function() { return true } -// given a package, return the path in the shop -function package_shop_path(pkg) -{ - -} - -// Parse module package string (e.g., "git.world/jj/mod@v0.6.3") -// returns {path, name, version} -Shop.parse_package = function(pkg) { - Shop.verify_package_name(pkg) - var path = pkg - var version = null - - // Extract version if present - if (path.includes('@')) { - var versionParts = path.split('@') - path = versionParts[0] - version = versionParts[1] - } - - // Check for links - var links = Shop.load_links() - if (links[path]) { - path = links[path] - } - - // Handle absolute paths (local modules) - // /User/john/mod -> User/john/mod - if (path.startsWith('/')) { - path = path.substring(1) - } - - // Extract name (last part of path) - var name = path.split('/').pop() - - return { - path, - name, - version - } -} - // Get information about how to resolve a package +// Local packages always start with / Shop.resolve_package_info = function(pkg) { - // Check links first - // We need to check the raw package string against links - // But we also need to handle the case where pkg is already a local path - // If pkg is "gitea...", check if it's linked - - var path = pkg - if (path.includes('@')) path = path.split('@')[0] - - var links = Shop.load_links() - if (links[path]) { - var resolved = links[path] - if (resolved.startsWith('/')) { - return { type: 'local', path: resolved } - } - return { type: 'package', path: resolved } - } - - if (pkg.startsWith('/')) { - return { type: 'local', path: pkg } - } - if (pkg.includes('gitea.')) { - return { type: 'gitea' } - } - - // If it looks like a path but isn't absolute, might be relative in future, but for now strict - return { type: 'unknown' } -} - -// Resolve a filesystem path to a package identifier in the shop -Shop.resolve_path_to_package = function(path_str) { - var abs = fd.realpath(path_str) - if (!abs) return null - - var modules_dir = get_packages_dir() - - // Case 1: Path is inside the modules directory (e.g. downloaded package) - if (abs.startsWith(modules_dir + '/')) { - var rel = abs.substring(modules_dir.length + 1) - // Remove trailing slash if any - if (rel.endsWith('/')) rel = rel.substring(0, rel.length - 1) - return rel - } - - // Case 2: Path is a local directory linked into the shop - // Local links are stored as modules_dir + abs_path (mirrored) - var target = modules_dir + abs - if (fd.is_dir(target)) { - if (abs.startsWith('/')) return abs.substring(1) - return abs - } - - // Case 3: Path is not linked. Return null. + if (pkg.startsWith('/')) return 'local' + if (pkg.includes('gitea')) return 'gitea' return null } @@ -681,64 +284,31 @@ Shop.verify_package_name = function(pkg) { // Convert module package to download URL Shop.get_download_url = function(pkg, commit_hash) { var info = Shop.resolve_package_info(pkg) - if (info.type == 'local') return null - var parsed = Shop.parse_package(pkg) - if (!parsed) return null - - if (parsed.path.includes('gitea.')) { - var parts = parsed.path.split('/') + if (info == 'gitea') { + var parts = pkg.split('/') var host = parts[0] var user = parts[1] var repo = parts[2] - if (!commit_hash) { - log.error("No commit hash available for download URL") - return null - } - return 'https://' + host + '/' + user + '/' + repo + '/archive/' + commit_hash + '.zip' } return null } -// Remove a dependency -Shop.remove_dependency = function(alias) { - var config = Shop.load_config() - if (!config) { - log.error("No cell.toml found") - return false - } - - if (!config.dependencies || !config.dependencies[alias]) { - return false - } - - delete config.dependencies[alias] - Shop.save_config(config) - return true -} - // Get the API URL for checking remote git commits Shop.get_api_url = function(pkg) { var info = Shop.resolve_package_info(pkg) - if (info.type == 'local') return null - var parsed = Shop.parse_package(pkg) - if (!parsed) return null - - var parts = parsed.path.split('/') - // Gitea pattern: gitea.pockle.world/user/repo@branch - if (parsed.path.includes('gitea.')) { + if (info == 'gitea') { + var parts = pkg.split('/') var host = parts[0] var user = parts[1] var repo = parts[2] - var url = 'https://' + host + '/api/v1/repos/' + user + '/' + repo + '/branches/' - if (parsed.version) url += parsed.version - return url + return 'https://' + host + '/api/v1/repos/' + user + '/' + repo + '/branches/' } - + return null } @@ -746,10 +316,11 @@ Shop.get_api_url = function(pkg) { Shop.extract_commit_hash = function(pkg, response) { if (!response) return null + var info = Shop.resolve_package_info(pkg) + var data = json.decode(response) - if (pkg.includes('gitea.')) { - // Gitea: response.commit.id + if (info == 'gitea') { if (Array.isArray(data)) data = data[0] return data.commit && data.commit.id @@ -758,39 +329,6 @@ Shop.extract_commit_hash = function(pkg, response) { return null } -// Get the module directory for a given alias -Shop.get_module_dir = function(alias) { - var config = Shop.load_config() - if (!config || !config.dependencies || !config.dependencies[alias]) { - return null - } - - var pkg = config.dependencies[alias] - var parsed = Shop.parse_package(pkg) - if (!parsed) return null - - return get_packages_dir() + '/' + parsed.path -} - -function lock_package(loc) -{ - var lock = Shop.load_lock() - -} - -Shop.check_cache = function(pkg) { - var parsed = Shop.parse_package(pkg) - if (!parsed) return null - - var cache_path = get_cache_dir() + '/' + parsed.path + '.zip' - if (fd.is_file(cache_path)) { - log.console("Found cached zip: " + cache_path) - return true - } - - return false -} - var open_dls = {} // for script forms, path is the canonical path of the module @@ -801,22 +339,8 @@ var script_form = function(path, script, pkg) { return fn } -// Get flags from config -function get_flags(config, platform, key) { - var flags = '' - if (config.compilation && config.compilation[key]) { - flags += config.compilation[key] - } - if (config.compilation && config.compilation[platform] && config.compilation[platform][key]) { - if (flags != '') flags += ' ' - flags += config.compilation[platform][key] - } - return flags -} - -Shop.get_flags = get_flags - // Resolve module function, hashing it in the process +// path is the exact path to the script file function resolve_mod_fn(path, pkg) { if (!fd.is_file(path)) throw new Error(`path ${path} is not a file`) @@ -839,7 +363,6 @@ function resolve_mod_fn(path, pkg) { return js.eval_compile(fn) } - // given a path and a package context // return module info about where it was found function resolve_locator(path, ctx) @@ -856,7 +379,7 @@ function resolve_locator(path, ctx) } // check in ctx package - var ctx_path = get_packages_dir() + '/' + get_package_path(ctx) + '/' + path + var ctx_path = get_packages_dir() + '/' + safe_package_path(ctx) + '/' + path if (fd.is_file(ctx_path)) { var fn = resolve_mod_fn(ctx_path, ctx) @@ -864,19 +387,16 @@ function resolve_locator(path, ctx) } // check for aliased dependency - var alias = path.split('/') - if (alias.length > 1) { - var alias_pkg = get_aliased_package(alias[0], ctx) - if (alias_pkg) { - var alias_path = get_packages_dir() + '/' + get_package_path(alias_pkg) + '/' + alias.slice(1).join('/') - if (fd.is_file(alias_path)){ - var fn = resolve_mod_fn(alias_path, ctx) - return {path: alias_path, scope:SCOPE_PACKAGE, symbol:fn} - } + var alias = pkg_tools.split_alias(ctx, path) + if (alias) { + var alias_path = get_packages_dir() + '/' + safe_package_path(alias.package) + '/' + alias.path + if (fd.is_file(alias_path)) { + var fn = resolve_mod_fn(alias_path, ctx) + return {path: alias_path, scope:SCOPE_PACKAGE, symbol:fn} } } - var package_path = get_packages_dir() + '/' + get_package_path(path) + var package_path = get_packages_dir() + '/' + safe_package_path(path) if (fd.is_file(package_path)) { var fn = resolve_mod_fn(package_path, ctx) return {path: package_path, scope: SCOPE_PACKAGE, symbol: fn} @@ -893,147 +413,105 @@ function resolve_locator(path, ctx) return null } -// given a C path like 'foo/bar/baz.c', return 'foo_bar_baz_use' -function c_sym_path(path) -{ - return path.substring(0, path.lastIndexOf('.')).replace(/\//g, '_').replace(/\\/g, '_').replace(/\./g, '_').replace(/-/g, '_') + '_use' +// Generate symbol name for a C module file +// Uses the same format as Shop.c_symbol_for_file +function make_c_symbol(pkg, file) { + var pkg_safe = pkg.replace(/\//g, '_').replace(/\./g, '_').replace(/-/g, '_') + var file_safe = file.replace(/\//g, '_').replace(/\./g, '_').replace(/-/g, '_') + return 'js_' + pkg_safe + '_' + file_safe + '_use' } -function c_sym_prefix(package) -{ - 'js_' + package.replace(/\//g, '_').replace(/\./g, '_').replace(/-/g, '_') + '_' +// Get the library path for a package in .cell/lib +function get_lib_path(pkg) { + var lib_name = pkg.replace(/\//g, '_').replace(/\./g, '_').replace(/-/g, '_') + return global_shop_path + '/lib/' + lib_name + dylib_ext } function resolve_c_symbol(path, package_context) { var static_only = cell.static_only - var local_sym_base = c_sym_path(path) - - // Candidates for symbol names - function symbol_candidates(pkg_path, mod_sym) { - var variants = [] - // if pkg_path is 'gitea.pockle.world/john/prosperon', we want: - // js_gitea_pockle_world_john_prosperon_mod_use - // and maybe with leading slash? - var paths = [pkg_path] - // if (!pkg_path.startsWith('/')) paths.push('/' + pkg_path) // unlikely to need slash variant for standard pkgs - for (var i = 0; i < paths.length; i++) { - var candidate = `js_${c_sym_path(paths[i])}_${mod_sym}_use` - if (variants.indexOf(candidate) < 0) - variants.push(candidate) - } - return variants - } - - // 1. Check internal symbols (statically linked) - // If we have a package context, we check package-prefixed symbols - // If not, we look for 'js_local_...' or 'js_path_use' directly? + // 1. Check internal symbols (statically linked) in package context if (package_context) { - // Check package internal symbols - var variants = symbol_candidates(package_context, local_sym_base) - for (var i = 0; i < variants.length; i++) { - if (os.internal_exists(variants[i])) { - return { - symbol: function() { return os.load_internal(variants[i]); }, - scope: SCOPE_PACKAGE, - path: variants[i] - } - } + 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 } - - // In static_only mode, skip dynamic library lookups - if (!static_only) { - // Then try dynamic library - // Use ./libcellmod for local? - var local_dl_name = './libcellmod' + dylib_ext - // Or check package context dir if it's a local package path? - if (package_context && package_context.startsWith('/')) { - local_dl_name = package_context + '/libcellmod' + dylib_ext } - if (fd.is_file(local_dl_name)) { - if (!open_dls[local_dl_name]) - open_dls[local_dl_name] = os.dylib_open(local_dl_name); - - if (open_dls[local_dl_name]) { - var locals = package_context ? symbol_candidates(package_context, local_sym_base) : [`js_local_${local_sym_base}_use`] - for (var i = 0; i < locals.length; i++) { - var candidate = locals[i] - if (os.dylib_has_symbol(open_dls[local_dl_name], candidate)) - return { - symbol: function() { return os.dylib_symbol(open_dls[local_dl_name], candidate); }, - scope: SCOPE_LOCAL, - path: candidate - }; + 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 } } } } - } else { - // Local context - var local_sym = `js_local_${local_sym_base}_use` - if (os.internal_exists(local_sym)) { - return { - symbol: function() { return os.load_internal(local_sym); }, - scope: SCOPE_LOCAL, - path: local_sym - } - } - } - + // 2. Check if valid package import (e.g. 'prosperon/sprite') var pkg_alias = get_import_package(path) if (pkg_alias) { - var canon_pkg = get_aliased_package(path, package_context) - if (canon_pkg) { - var mod_name = get_import_name(path) - var mod_sym = mod_name.replace(/\//g, '_').replace(/-/g, '_').replace(/\./g, '_') - var sym_names = symbol_candidates(canon_pkg, mod_sym) - - for (var i = 0; i < sym_names.length; i++) { - if (os.internal_exists(sym_names[i])) { - return { - symbol: function() { return os.load_internal(sym_names[i]); }, - scope: SCOPE_PACKAGE, - package: canon_pkg, - path: sym_names[i] - } - } - } - // Then try dynamic library for package (skip in static_only mode) - if (!static_only) { - // Check package dir for libcellmod - var pkg_build_dir = get_packages_dir() + '/' + canon_pkg - var dl_path = pkg_build_dir + '/libcellmod' + dylib_ext - 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]) { - for (var si = 0; si < sym_names.length; si++) { - var sym_name = sym_names[si] - if (os.dylib_has_symbol(open_dls[dl_path], sym_name)) - return { - symbol: function() { return os.dylib_symbol(open_dls[dl_path], sym_name) }, - scope: SCOPE_PACKAGE, - package: canon_pkg, - path: sym_name - } - } - } - } - } + var canon_pkg = get_aliased_package(path, package_context) + if (canon_pkg) { + var mod_name = get_import_name(path) + var sym = make_c_symbol(canon_pkg, mod_name) + + if (os.internal_exists(sym)) { + return { + symbol: function() { return os.load_internal(sym) }, + scope: SCOPE_PACKAGE, + package: canon_pkg, + path: sym + } } + + 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) + 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_PACKAGE, + package: canon_pkg, + path: sym + } + } + } + } } - // 3. Check Core/General fallback (js_path_use) - var core_sym = `js_${path.replace(/\//g, '_')}_use`; - if (os.internal_exists(core_sym)) + // 3. Check core fallback + var core_sym = `js_${path}_use` + if (os.internal_exists(core_sym)) { return { - symbol: function() { return os.load_internal(core_sym); }, + symbol: function() { return os.load_internal(core_sym) }, scope: SCOPE_CORE, path: core_sym - }; - + } + } + + // 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 } @@ -1045,18 +523,30 @@ function resolve_module_info(path, package_context) { if (min_scope == 999) return null - var resolved_path - if (mod_resolve.scope != 999) resolved_path = mod_resolve.path - else resolved_path = c_resolve.path - - var cache_scope = min_scope == SCOPE_CORE ? 2 : 0 + // Cache key is now package/file format + // e.g., "core/shop", "gitea.pockle.world/john/prosperon/sprite" var cache_key - if (min_scope == SCOPE_CORE) - cache_key = `2::${path}` - else - cache_key = `${text(cache_scope)}::${resolved_path}` - - cache_key = cache_key.replace('//', '/') + 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 + } return { cache_key: cache_key, @@ -1084,8 +574,6 @@ function execute_module(info) var used - // If we have a script, it always takes precedence (containing the C symbol if available) - // This supports "Hybrid" modules where JS wraps C. if (mod_resolve.scope < 900) { var context = null if (c_resolve.scope < 900) { @@ -1128,189 +616,16 @@ Shop.resolve_locator = resolve_locator // Get cache path for a package and commit function get_cache_path(pkg, commit) { - var parsed = Shop.parse_package(pkg) - if (!parsed) return null - - var slug = parsed.path.split('/').join('_') - return get_cache_dir() + '/' + slug + '_' + commit + '.zip' + return global_shop_path + '/cache/' + pkg.replaceAll('@','_').replaceAll('/','_') + '_' + commit + '.zip' } -function rm_recursive(path) { - try { - fd.rm(path) - } catch (e) { - log.error("Failed to remove " + path + ": " + e) - } -} - -function get_all_files(dir, prefix, results) { - prefix = prefix || "" - results = results || [] - - var list = fd.readdir(dir) - if (!list) return results - - for (var i = 0; i < list.length; i++) { - var item = list[i] - if (item == '.' || item == '..') continue - - var full_path = dir + "/" + item - var rel_path = prefix ? prefix + "/" + item : item - - var st = fd.stat(full_path) - if (st.isDirectory) { - get_all_files(full_path, rel_path, results) - } else { - results.push(rel_path) - } - } - return results -} - - -// Verify zip contents against target directory -function verify_zip_contents(zip, target_dir) { - var count = zip.count() - var expected_files = {} - for (var i = 0; i < count; i++) { - if (zip.is_directory(i)) continue - var filename = zip.get_filename(i) - var parts = filename.split('/') - if (parts.length > 1) { - parts.shift() - var rel_path = parts.join('/') - expected_files[rel_path] = true - - var full_path = target_dir + '/' + rel_path - if (!fd.is_file(full_path)) return false - - var content_zip = zip.slurp(filename) - var content_disk = fd.slurp(full_path) - - if (content_zip.length != content_disk.length) return false - - var hash_zip = text(crypto.blake2(content_zip), 'h') - var hash_disk = text(crypto.blake2(content_disk), 'h') - - if (hash_zip != hash_disk) return false - } - } - - // Check for extra files - var existing_files = get_all_files(target_dir) - for (var i = 0; i < existing_files.length; i++) - if (!expected_files[existing_files[i]]) return false - - return true -} - -// High-level: Add a package, install it, and install all transitive dependencies -// Like `bun add` or `npm install ` -Shop.get = function(pkg, alias) { - if (fd.is_dir(pkg)) { - log.console("Found directory: " + pkg) - pkg = fd.realpath(pkg) - log.console("Resolved to: " + pkg) - } - var info = Shop.resolve_package_info(pkg) - if (info.type == 'unknown') { - log.error("Could not resolve package: " + pkg) - return false - } - var parsed = Shop.parse_package(pkg) - if (!alias) alias = parsed.name - - log.console("Adding dependency: " + alias + " = " + pkg) - - // Add to config - var config = Shop.load_config() || { dependencies: {} } - if (!config.dependencies) config.dependencies = {} - config.dependencies[alias] = pkg - Shop.save_config(config) - return true -} - -// Update a specific package -Shop.update = function(pkg) { - var config = Shop.load_config() - var lock = Shop.load_lock() - var parsed = Shop.parse_package(pkg) - var info = Shop.resolve_package_info(pkg) - var target_dir = get_packages_dir() + '/' + parsed.path - - var result = info.type == 'local' - ? update_local(pkg, info, target_dir) - : update_remote(pkg, info, target_dir, lock[pkg]) - - if (!result) { - log.error("Failed to update " + parsed.path) - return false - } - - lock[pkg] = { - package: pkg, - commit: result.commit, - zip_hash: result.zip_hash, - updated: time.number() - } - Shop.save_lock(lock) - log.console("Updated " + parsed.path + ".") - return true -} - -function update_local(pkg, info, target_dir) { - if (fd.is_link(target_dir)) { - if (fd.readlink(target_dir) == info.path) - return { commit: "local", package: pkg, zip_hash: "local" } - else - fd.unlink(target_dir) - } - - var parent_dir = target_dir.substring(0, target_dir.lastIndexOf('/')) - ensure_dir(parent_dir) - - if (fd.is_dir(target_dir)) fd.rmdir(target_dir) - - try { - fd.symlink(info.path, target_dir) - log.console("Linked " + target_dir + " -> " + info.path) - return { commit: "local", package: pkg, zip_hash: "local" } - } catch(e) { - log.error("Failed to create symlink: " + e) - return null - } -} - -function update_remote(pkg, info, target_dir, lock_info) { - var local_hash = lock_info ? lock_info.commit : null - var remote_hash = fetch_remote_hash(pkg) - - if (!remote_hash && !local_hash) { - log.error("Could not resolve commit for " + pkg) - return null - } - - var target_hash = remote_hash || local_hash - - if (local_hash == target_hash) { - log.console(pkg + " is already up to date.") - return lock_info - } - - if (local_hash) - log.console("Updating " + pkg + " " + local_hash.substring(0,8) + " -> " + target_hash.substring(0,8)) - else - log.console("Installing " + pkg + "...") - - var zip_blob = get_or_download_zip(pkg, target_hash) - if (!zip_blob) return null - - var zip_hash = text(crypto.blake2(zip_blob), 'h') - install_zip(zip_blob, target_dir) - - return { commit: target_hash, package: pkg, zip_hash: zip_hash } +function get_package_abs_dir(package) +{ + return get_packages_dir() + '/' + package } +// Fetch the latest commit hash from remote for a package +// Returns null for local packages or if fetch fails function fetch_remote_hash(pkg) { var api_url = Shop.get_api_url(pkg) if (!api_url) return null @@ -1324,28 +639,21 @@ function fetch_remote_hash(pkg) { } } -function get_or_download_zip(pkg, commit_hash) { +// Download a zip for a package at a specific commit and cache it +// Returns the zip blob or null on failure +function download_zip(pkg, commit_hash) { var cache_path = get_cache_path(pkg, commit_hash) - if (fd.is_file(cache_path)) { - log.console("Found cached zip: " + cache_path) - try { - return fd.slurp(cache_path) - } catch (e) { - log.error("Failed to read cache: " + e) - } - } - var download_url = Shop.get_download_url(pkg, commit_hash) if (!download_url) { log.error("Could not determine download URL for " + pkg) return null } - + log.console("Downloading from " + download_url) try { var zip_blob = http.fetch(download_url) - ensure_dir(cache_path.substring(0, cache_path.lastIndexOf('/'))) + log.console(`putting to ${cache_path}`) fd.slurpwrite(cache_path, zip_blob) log.console("Cached to " + cache_path) return zip_blob @@ -1355,16 +663,143 @@ function get_or_download_zip(pkg, commit_hash) { } } +// Get zip from cache, returns null if not cached +function get_cached_zip(pkg, commit_hash) { + var cache_path = get_cache_path(pkg, commit_hash) + if (fd.is_file(cache_path)) + return fd.slurp(cache_path) + + return null +} + +// Fetch: Ensure the zip on disk matches what's in the lock file +// For local packages, this is a no-op (returns true) +// For remote packages, downloads the zip if not present or hash mismatch +// Returns true on success +Shop.fetch = function(pkg) { + var lock = Shop.load_lock() + var lock_entry = lock[pkg] + var info = Shop.resolve_package_info(pkg) + + if (info == 'local') return null + + // No lock entry - can't fetch without knowing what commit + if (!lock_entry || !lock_entry.commit) + throw new Error("No lock entry for " + pkg + " - run update first") + + var commit = lock_entry.commit + var expected_hash = lock_entry.zip_hash + + // Check if we have the zip cached + var zip_blob = get_cached_zip(pkg, commit) + + if (zip_blob) { + // Verify hash matches + var actual_hash = text(crypto.blake2(zip_blob), 'h') + if (actual_hash == expected_hash) + return true + + log.console("Zip hash mismatch for " + pkg + ", re-fetching...") + } + + // Download the zip + download_zip(pkg, commit) + + return true +} + +// Extract: Extract a package to its target directory +// For local packages, creates a symlink +// 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) + + if (info == 'local') { + if (fd.is_link(target_dir)) + fd.unlink(target_dir) + if (fd.is_dir(target_dir)) + fd.rmdir(target_dir) + fd.symlink(pkg, target_dir) + + return true + } + + var zip_blob = get_package_zip(pkg) + + if (!zip_blob) + throw new Error("No zip blob available for " + pkg) + + // Extract zip for remote package + install_zip(zip_blob, target_dir) + return true +} + +function get_package_zip(pkg) +{ + var lock = Shop.load_lock() + var lock_entry = lock[pkg] + + if (!lock_entry || !lock_entry.commit) + return null + + var commit = lock_entry.commit + + // Try to get from cache first + var cached = get_cached_zip(pkg, commit) + if (cached) + return cached + + // Not in cache, download it + return download_zip(pkg, commit) +} + +// 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) { + var lock = Shop.load_lock() + var lock_entry = lock[pkg] + var info = Shop.resolve_package_info(pkg) + +log.console(`checking ${pkg}`) + if (info == 'local') return { + updated: time.number() + } + + var local_commit = lock_entry.commit + var remote_commit = fetch_remote_hash(pkg) + + if (local_commit == remote_commit) + return null + + if (!remote_commit) { + log.error("Could not resolve commit for " + pkg) + return null + } + + var new_entry = { + type: info, + commit: remote_commit, + updated: time.number() + } + + lock[pkg] = new_entry + Shop.save_lock(lock) + + return new_entry +} + function install_zip(zip_blob, target_dir) { var zip = miniz.read(zip_blob) if (!zip) throw new Error("Failed to read zip archive") if (fd.is_link(target_dir)) fd.unlink(target_dir) + if (fd.is_dir(target_dir)) fd.rmdir(target_dir, 1) - log.console("Syncing to " + target_dir) + log.console("Extracting to " + target_dir) ensure_dir(target_dir) - var zip_files = {} var count = zip.count() for (var i = 0; i < count; i++) { @@ -1375,210 +810,29 @@ function install_zip(zip_blob, target_dir) { parts.shift() var rel_path = parts.join('/') - zip_files[rel_path] = { index: i, filename: filename } - } - - var existing_files = fd.is_dir(target_dir) ? get_all_files(target_dir) : [] - - for (var i = 0; i < existing_files.length; i++) { - var rel_path = existing_files[i] - if (!zip_files[rel_path]) { - var full_path = target_dir + '/' + rel_path - log.console("Removing " + rel_path) - fd.rm(full_path) - } - } - - for (var rel_path in zip_files) { - var zip_info = zip_files[rel_path] var full_path = target_dir + '/' + rel_path var dir_path = full_path.substring(0, full_path.lastIndexOf('/')) - var zip_content = zip.slurp(zip_info.filename) - var needs_write = true - - if (fd.is_file(full_path)) { - var disk_content = fd.slurp(full_path) - if (disk_content.length == zip_content.length && disk_content.length != 0) { - var hash_zip = text(crypto.blake2(zip_content), 'h') - var hash_disk = text(crypto.blake2(disk_content), 'h') - if (hash_zip == hash_disk) { - needs_write = false - } - } - } - - if (needs_write) { - ensure_dir(dir_path) - log.console("Writing " + rel_path) - fd.slurpwrite(full_path, zip_content) - } + ensure_dir(dir_path) + fd.slurpwrite(full_path, zip.slurp(filename)) } } -// High-level: Remove a package and clean up -// Like `bun remove` -Shop.remove = function(alias_or_path) { - var config = Shop.load_config() - var is_dependency = config && config.dependencies && config.dependencies[alias_or_path] - - var target_pkg = null - var locator = null - - if (is_dependency) { - locator = config.dependencies[alias_or_path] - var parsed = Shop.parse_package(locator) - target_pkg = parsed.path - - // Remove from config - delete config.dependencies[alias_or_path] - Shop.save_config(config) - } else { - // Check if it's a resolvable path - var resolved = Shop.resolve_path_to_package(alias_or_path) - if (resolved) { - target_pkg = resolved - } else { - // Check if alias_or_path exists directly in modules dir? - var direct_path = get_packages_dir() + '/' + alias_or_path - if (fd.exists(direct_path)) { - target_pkg = alias_or_path - } - } - } - - if (!target_pkg) { - log.error("Package/Dependency not found: " + alias_or_path) - return false - } - - var target_dir = get_packages_dir() + '/' + target_pkg - +// High-level: Remove a package from the shop +Shop.remove = function(pkg) { // Remove from lock var lock = Shop.load_lock() - var lock_changed = false - - if (locator && lock[locator]) { - delete lock[locator] - lock_changed = true + if (lock[pkg]) { + delete lock[pkg] + Shop.save_lock(lock) } - if (lock[alias_or_path]) { - delete lock[alias_or_path] - lock_changed = true - } - - // Scan for any entry pointing to this target - for (var k in lock) { - if (lock[k].package == target_pkg || lock[k].package == '/' + target_pkg) { - delete lock[k] - lock_changed = true - } - } - - if (lock_changed) Shop.save_lock(lock) - - // Remove directory - if (fd.is_link(target_dir)) { - log.console("Unlinking " + target_dir) - fd.unlink(target_dir) - } else if (fd.is_dir(target_dir)) { - log.console("Removing " + target_dir) - fd.rmdir(target_dir) - } - - log.console("Removed " + (is_dependency ? alias_or_path : target_pkg)) - return true -} -Shop.add_replacement = function(alias, replacement) { - var config = Shop.load_config() - if (!config) config = {} - if (!config.replace) config.replace = {} - - config.replace[alias] = replacement - Shop.save_config(config) - - log.console("Added replacement: " + alias + " = " + replacement) - return true -} - -Shop.remove_replacement = function(alias) { - var config = Shop.load_config() - if (!config || !config.replace || !config.replace[alias]) { - log.error("No replacement found for " + alias) - return false - } - - delete config.replace[alias] - Shop.save_config(config) - - log.console("Removed replacement for " + alias) - log.console("Run 'cell update " + alias + "' to restore the package.") + log.console("Removed " + pkg) return true } // Compile a module // List all files in a package -Shop.list_files = function(pkg) { - var dir - if (!pkg) dir = '.' - else dir = get_packages_dir() + '/' + pkg - - var files = [] - - var walk = function(current_dir, current_prefix) { - var list = fd.readdir(current_dir) - if (!list) return - - for (var i = 0; i < list.length; i++) { - var item = list[i] - if (item == '.' || item == '..') continue - if (item.startsWith('.')) continue - - // Skip build directories in root - - var full_path = current_dir + "/" + item - var rel_path = current_prefix ? current_prefix + "/" + item : item - - var st = fd.stat(full_path) - if (st.isDirectory) { - walk(full_path, rel_path) - } else { - files.push(rel_path) - } - } - } - - if (fd.is_dir(dir)) { - walk(dir, "") - } - return files -} - -Shop.get_c_objects = function(pkg) { - var files = Shop.list_files(pkg) - var objects = [] - var build_dir = get_build_dir(pkg) - - for (var i=0; i= 0) basename = file.substring(slash + 1) - if (basename == 'main.c') return true - if (basename.startsWith('main_') && basename.endsWith('.c')) return true - return false -} - -// Get the core build directory for linking -function get_core_build_dir() { - return get_global_build_dir() + '/core' -} - function get_package_scripts(package) { - var files = Shop.list_files(package) + var files = pkg_tools.list_files(package) var scripts = [] for (var i = 0; i < files.length; i++) { @@ -1630,390 +869,77 @@ function get_package_scripts(package) return scripts } -function get_package_c_files(package) -{ - var files = Shop.list_files(package) - var c_files = [] - - for (var i = 0; i < files.length; i++) { - var file = files[i] - if (file.endsWith('.c') || file.endsWith('.cpp')) { - c_files.push(file) - } - } - - return c_files -} - Shop.build_package_scripts = function(package) { // compiles all .ce and .cm files in a package var scripts = get_package_scripts(package) + var pkg_dir = get_package_abs_dir(package) + for (var script of scripts) - resolve_mod_fn(script, package) + resolve_mod_fn(pkg_dir + '/' + script, package) } -function get_target_dynamic_files(package, target) +Shop.list_packages = function() { - var files = get_package_c_files(package) - var result = [] - - // Build a map of base names to their variants - var variants = {} - for (var i = 0; i < files.length; i++) { - var file = files[i] - var basename = file.substring(0, file.lastIndexOf('.')) - var ext = file.substring(file.lastIndexOf('.')) - - // Check if this is a target-specific variant (name_target.ext) - var underscore = basename.lastIndexOf('_') - if (underscore >= 0) { - var base = basename.substring(0, underscore) - var suffix = basename.substring(underscore + 1) - if (!variants[base]) variants[base] = {} - variants[base][suffix] = file - } else { - if (!variants[basename]) variants[basename] = {} - variants[basename]['default'] = file - } - } - - // Select appropriate files - for (var base in variants) { - var v = variants[base] - // Skip main files entirely for dynamic builds - if (base == 'main' || base.endsWith('/main')) continue - - if (target && v[target]) { - result.push(v[target]) - } else if (v['default']) { - result.push(v['default']) - } - } - - return result -} - -Shop.build_package = function(package) -{ - Shop.build_package_scripts(package) - - var dynamic_files = get_target_dynamic_files(package, os.platform()) - - var files = Shop.list_files(package) - var build_dir = get_build_dir(package) - ensure_dir(build_dir) - - var module_dir = package ? get_packages_dir() + '/' + package : (current_package_path || '.') - - log.console(`Building package ${package ? package : 'local'} to ${build_dir}`) - - // For C compilation - var config = Shop.load_config(package) || {} - var cflags = get_flags(config, platform, 'CFLAGS') - - // Determine usage prefix for C symbols - var use_prefix = c_sym_prefix(package) - - var c_objects = [] - - // Use get_hash from build_utils - var get_hash = build_utils.get_hash - - for (var i=0; i " + obj_path) - var ret = os.system(full_compile_cmd) - if (ret != 0) { - log.error("Compilation failed for " + src_path) - return false - } - - // 3. Move object - os.system('mv ' + module_dir + '/' + comp_obj + ' ' + obj_path) - } - c_objects.push(obj_path) - } - - - // Link if there are C objects - if (c_objects.length > 0) { - var lib_name = get_shared_lib_path() + '/lib' + get_package_path(package) + dylib_ext - - var link_flags = '-fPIC -shared' - - // Link against core package dylib (unless we ARE the core) - if (package != core_package) { - var lib_dir = get_shared_lib_path() - if (platform == 'macOS') { - link_flags += ' -L' + lib_dir + ' -Wl,-rpath,@loader_path' - } else if (platform == 'Linux' || platform == 'linux') { - link_flags += ' -L' + lib_dir + ' -Wl,-rpath,$ORIGIN' - } else if (platform == 'Windows') { - link_flags += ' -L' + lib_dir - } - link_flags += ' -l' + core_package - } - - var ldflags = get_flags(config, platform, 'LDFLAGS') - if (ldflags != '') link_flags += ' ' + ldflags - - var objs_str = '' - for (var i=0; i 0) { - var pkg = queue.shift() - if (processed[pkg]) continue - processed[pkg] = true - - result.push(pkg) - - var parsed = Shop.parse_package(pkg) - var pkg_config = Shop.load_config(parsed.path) - - if (pkg_config && pkg_config.dependencies) { - for (var alias in pkg_config.dependencies) { - var dep_pkg = pkg_config.dependencies[alias] - if (!processed[dep_pkg]) { - queue.push(dep_pkg) - } - } - } - } - - return result -} - -// List all .cm and .ce files in a package -// If ctx is null, lists local files -// If ctx is a canonical path, lists files in that module -Shop.list_modules = function(ctx) { - var files = Shop.list_files(ctx) - var modules = [] - for (var i=0; i 'js_gitea_pockle_world_john_prosperon_sprite_use' +Shop.c_symbol_for_file = function(pkg, file) { + var pkg_safe = pkg.replace(/\//g, '_').replace(/\./g, '_').replace(/-/g, '_') + var file_safe = file.substring(0, file.lastIndexOf('.')).replace(/\//g, '_').replace(/\./g, '_').replace(/-/g, '_') + return 'js_' + pkg_safe + '_' + file_safe + '_use' +} + +// Generate C symbol prefix for a package +// e.g., c_symbol_prefix('gitea.pockle.world/john/prosperon') -> 'js_gitea_pockle_world_john_prosperon_' +Shop.c_symbol_prefix = function(pkg) { + var pkg_safe = pkg.replace(/\//g, '_').replace(/\./g, '_').replace(/-/g, '_') + return 'js_' + pkg_safe + '_' +} + +// Get the library name for a package (without extension) +// e.g., 'gitea.pockle.world/john/prosperon' -> 'gitea_pockle_world_john_prosperon' +Shop.lib_name_for_package = function(pkg) { + return pkg.replace(/\//g, '_').replace(/\./g, '_').replace(/-/g, '_') +} + +// Returns { ok: bool, results: [{pkg, ok, error}] } +Shop.audit_packages = function() { + var packages = Shop.list_packages() + + var bad = [] + + for (var package of packages) { + if (package == 'core') continue + if (fd.is_dir(package)) continue + if (fetch_remote_hash(package)) continue + bad.push(package) } - Shop.save_lock(lock) - - log.console("Core updated to " + hash) - return true -} -Shop.link_core = function(target) { - if (!fd.is_dir(target)) return false - var core_dir = Shop.get_core_dir() - - if (fd.is_link(core_dir)) fd.unlink(core_dir) - else if (fd.is_dir(core_dir)) { - // Safety check? - // For now, we assume user knows what they are doing. - // We can't easily rm recursive if it's a real dir unless we use system rm or implement it. - // shop.cm has rm_recursive? No, I implemented it? - // I saw `function rm_recursive` at line 1141. internal. - rm_recursive(core_dir) - } - - fd.symlink(fd.realpath(target), core_dir) - - var lock = Shop.load_lock() - lock['core'] = { - package: fd.realpath(target), - type: 'local', - updated: time.number() - } - Shop.save_lock(lock) - return true -} - -Shop.unlink_core = function() { - var core_dir = Shop.get_core_dir() - if (fd.is_link(core_dir)) { - fd.unlink(core_dir) - return true - } - return false -} - -Shop.is_core_linked = function() { - var core_dir = Shop.get_core_dir() - return fd.is_link(core_dir) -} - -Shop.list_shop_packages = function() { - var lock = Shop.load_lock() - var list = [] - for (var k in lock) { - log.console(k) - if (lock[k]) list.push(lock[k]) - } - return list + return bad } return Shop \ No newline at end of file diff --git a/toolchains.cm b/toolchains.cm new file mode 100644 index 00000000..a478b274 --- /dev/null +++ b/toolchains.cm @@ -0,0 +1,234 @@ +return { + playdate: { + c: 'arm-none-eabi-gcc', + cpp: 'arm-none-eabi-g++', + ar: 'arm-none-eabi-ar', + strip: 'arm-none-eabi-strip', + objcopy: 'arm-none-eabi-objcopy', + ld: 'arm-none-eabi-gcc', + system: 'playdate', + cpu_family: 'arm', + cpu: 'cortex-m7', + endian: 'little', + c_args: ['-mcpu=cortex-m7', '-mthumb', '-mfloat-abi=hard', '-mfpu=fpv5-sp-d16', '-fno-exceptions'], + c_link_args: ['-mcpu=cortex-m7', '-mthumb', '-mfloat-abi=hard', '-mfpu=fpv5-sp-d16', '-nostartfiles', "-T/Users/john/Developer/PlaydateSDK/C_API/buildsupport/link_map.ld"] + }, + windows: { + c: 'x86_64-w64-mingw32-gcc', + cpp: 'x86_64-w64-mingw32-g++', + ar: 'x86_64-w64-mingw32-ar', + windres: 'x86_64-w64-mingw32-windres', + strip: 'x86_64-w64-mingw32-strip', + system: 'windows', + cpu_family: 'x86_64', + cpu: 'x86_64', + endian: 'little', + c_args: [], + c_link_args: [] + }, + windows_i686: { + c: 'i686-w64-mingw32-gcc', + cpp: 'i686-w64-mingw32-g++', + ar: 'i686-w64-mingw32-ar', + windres: 'i686-w64-mingw32-windres', + strip: 'i686-w64-mingw32-strip', + system: 'windows', + cpu_family: 'x86', + cpu: 'i686', + endian: 'little', + c_args: [], + c_link_args: [] + }, + linux: { + c: 'zig cc -target x86_64-linux-musl', + cpp: 'zig c++ -target x86_64-linux-musl', + ar: 'zig ar', + strip: 'strip', + system: 'linux', + cpu_family: 'x86_64', + cpu: 'x86_64', + endian: 'little', + c_args: [], + c_link_args: [] + }, + linux_arm64: { + c: 'zig cc -target aarch64-linux-musl', + cpp: 'zig c++ -target aarch64-linux-musl', + ar: 'zig ar', + strip: 'strip', + system: 'linux', + cpu_family: 'aarch64', + cpu: 'aarch64', + endian: 'little', + c_args: [], + c_link_args: [] + }, + macos_arm64: { + c: 'clang -target arm64-apple-macos11', + cpp: 'clang++ -target arm64-apple-macos11', + ar: 'ar', + strip: 'strip', + system: 'darwin', + cpu_family: 'aarch64', + cpu: 'aarch64', + endian: 'little', + c_args: ['-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk'], + c_link_args: ['-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk'] + }, + macos_x86_64: { + c: 'clang -target x86_64-apple-macos10.12', + cpp: 'clang++ -target x86_64-apple-macos10.12', + ar: 'ar', + strip: 'strip', + system: 'darwin', + cpu_family: 'x86_64', + cpu: 'x86_64', + endian: 'little', + c_args: ['-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk'], + c_link_args: ['-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk'] + }, + ios_arm64: { + c: 'clang -target arm64-apple-ios12.0', + cpp: 'clang++ -target arm64-apple-ios12.0', + ar: 'ar', + strip: 'strip', + system: 'ios', + cpu_family: 'aarch64', + cpu: 'aarch64', + endian: 'little', + c_args: ['-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk', '-fembed-bitcode'], + c_link_args: ['-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk', '-fembed-bitcode'] + }, + ios_simulator_arm64: { + c: 'clang -target arm64-apple-ios12.0-simulator', + cpp: 'clang++ -target arm64-apple-ios12.0-simulator', + ar: 'ar', + strip: 'strip', + system: 'ios', + cpu_family: 'aarch64', + cpu: 'aarch64', + endian: 'little', + c_args: ['-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk'], + c_link_args: ['-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk'] + }, + ios_simulator_x86_64: { + c: 'clang -target x86_64-apple-ios12.0-simulator', + cpp: 'clang++ -target x86_64-apple-ios12.0-simulator', + ar: 'ar', + strip: 'strip', + system: 'ios', + cpu_family: 'x86_64', + cpu: 'x86_64', + endian: 'little', + c_args: ['-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk'], + c_link_args: ['-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk'] + }, + tvos_arm64: { + c: 'clang -target arm64-apple-tvos12.0', + cpp: 'clang++ -target arm64-apple-tvos12.0', + ar: 'ar', + strip: 'strip', + system: 'tvos', + cpu_family: 'aarch64', + cpu: 'aarch64', + endian: 'little', + c_args: ['-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS.sdk', '-fembed-bitcode'], + c_link_args: ['-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS.sdk', '-fembed-bitcode'] + }, + tvos_simulator_arm64: { + c: 'clang -target arm64-apple-tvos12.0-simulator', + cpp: 'clang++ -target arm64-apple-tvos12.0-simulator', + ar: 'ar', + strip: 'strip', + system: 'tvos', + cpu_family: 'aarch64', + cpu: 'aarch64', + endian: 'little', + c_args: ['-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/AppleTVSimulator.platform/Developer/SDKs/AppleTVSimulator.sdk'], + c_link_args: ['-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/AppleTVSimulator.platform/Developer/SDKs/AppleTVSimulator.sdk'] + }, + tvos_simulator_x86_64: { + c: 'clang -target x86_64-apple-tvos12.0-simulator', + cpp: 'clang++ -target x86_64-apple-tvos12.0-simulator', + ar: 'ar', + strip: 'strip', + system: 'tvos', + cpu_family: 'x86_64', + cpu: 'x86_64', + endian: 'little', + c_args: ['-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/AppleTVSimulator.platform/Developer/SDKs/AppleTVSimulator.sdk'], + c_link_args: ['-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/AppleTVSimulator.platform/Developer/SDKs/AppleTVSimulator.sdk'] + }, + watchos_arm64: { + c: 'clang -target arm64_32-apple-watchos5.0', + cpp: 'clang++ -target arm64_32-apple-watchos5.0', + ar: 'ar', + strip: 'strip', + system: 'watchos', + cpu_family: 'aarch64', + cpu: 'arm64_32', + endian: 'little', + c_args: ['-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/WatchOS.platform/Developer/SDKs/WatchOS.sdk', '-fembed-bitcode'], + c_link_args: ['-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/WatchOS.platform/Developer/SDKs/WatchOS.sdk', '-fembed-bitcode'] + }, + watchos_simulator_arm64: { + c: 'clang -target arm64-apple-watchos5.0-simulator', + cpp: 'clang++ -target arm64-apple-watchos5.0-simulator', + ar: 'ar', + strip: 'strip', + system: 'watchos', + cpu_family: 'aarch64', + cpu: 'aarch64', + endian: 'little', + c_args: ['-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/WatchSimulator.platform/Developer/SDKs/WatchSimulator.sdk'], + c_link_args: ['-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/WatchSimulator.platform/Developer/SDKs/WatchSimulator.sdk'] + }, + watchos_simulator_x86_64: { + c: 'clang -target x86_64-apple-watchos5.0-simulator', + cpp: 'clang++ -target x86_64-apple-watchos5.0-simulator', + ar: 'ar', + strip: 'strip', + system: 'watchos', + cpu_family: 'x86_64', + cpu: 'x86_64', + endian: 'little', + c_args: ['-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/WatchSimulator.platform/Developer/SDKs/WatchSimulator.sdk'], + c_link_args: ['-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/WatchSimulator.platform/Developer/SDKs/WatchSimulator.sdk'] + }, + visionos_arm64: { + c: 'clang -target arm64-apple-xros1.0', + cpp: 'clang++ -target arm64-apple-xros1.0', + ar: 'ar', + strip: 'strip', + system: 'visionos', + cpu_family: 'aarch64', + cpu: 'aarch64', + endian: 'little', + c_args: ['-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/XROS.platform/Developer/SDKs/XROS.sdk'], + c_link_args: ['-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/XROS.platform/Developer/SDKs/XROS.sdk'] + }, + visionos_simulator_arm64: { + c: 'clang -target arm64-apple-xros1.0-simulator', + cpp: 'clang++ -target arm64-apple-xros1.0-simulator', + ar: 'ar', + strip: 'strip', + system: 'visionos', + cpu_family: 'aarch64', + cpu: 'aarch64', + endian: 'little', + c_args: ['-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/XRSimulator.platform/Developer/SDKs/XRSimulator.sdk'], + c_link_args: ['-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/XRSimulator.platform/Developer/SDKs/XRSimulator.sdk'] + }, + emscripten: { + c: 'emcc', + cpp: 'em++', + ar: 'emar', + strip: 'emstrip', + system: 'emscripten', + cpu_family: 'wasm32', + cpu: 'wasm32', + endian: 'little', + c_args: [], + c_link_args: [] + } +} \ No newline at end of file diff --git a/update.ce b/update.ce index 7eb83b68..a9628015 100644 --- a/update.ce +++ b/update.ce @@ -1,45 +1,77 @@ -// cell update [alias] - Update packages to latest versions and rebuild dynamic libraries +// cell update - Update packages from remote sources // -// This command: -// 1. Updates all packages from their remote sources -// 2. Rebuilds all dynamic libraries via 'cell build -d' +// This command checks for updates to all packages and downloads new versions. +// For local packages, ensures the symlink is correct. +// For remote packages, checks the remote for new commits. +// +// Usage: +// cell update - Update all packages +// cell update - Update a specific package var shop = use('shop') -var os = use('os') -var target = null +var target_pkg = null // Parse arguments for (var i = 0; i < args.length; i++) { - if (args[i] == '--target' || args[i] == '-t') { - if (i + 1 < args.length) { - target = args[i + 1] - i++ - } - } else if (args[i] == '--help' || args[i] == '-h') { - log.console("Usage: cell update [options]") - log.console("Update packages and rebuild dynamic libraries.") + if (args[i] == '--help' || args[i] == '-h') { + log.console("Usage: cell update [package]") + log.console("Update packages from remote sources.") log.console("") - log.console("Options:") - log.console(" --target, -t Build for specific target platform") + log.console("Arguments:") + log.console(" package Optional package name to update. If omitted, updates all.") log.console("") - log.console("This command updates all installed packages from their remote") - log.console("sources, then rebuilds all dynamic libraries.") + log.console("This command checks for updates to all packages and downloads") + log.console("new versions. For local packages, ensures the symlink is correct.") $_.stop() + } else if (!args[i].startsWith('-')) { + target_pkg = args[i] } } -var packages = shop.list_shop_packages() - -log.console("Checking for updates (" + text(packages.length) + " packages)...") - -// 1. Update all packages -for (var info of packages) { - var pack = info.package - if (!pack || pack == 'core') continue +function update_and_fetch(pkg) +{ + var lock = shop.load_lock() + var old_entry = lock[pkg] + var old_commit = old_entry ? old_entry.commit : null - log.console("Updating " + pack) - shop.update(pack) + var new_entry = shop.update(pkg) + + if (new_entry && new_entry.commit) { + log.console(" " + pkg + " " + old_commit.substring(0, 8) + " -> " + new_entry.commit.substring(0, 8)) + shop.fetch(pkg) + shop.build_package_scripts(pkg) + return true + } + return false +} + +if (target_pkg) { + if (update_and_fetch(target_pkg)) + log.console("Updated " + target_pkg + ".") + else + log.console(target_pkg + " is up to date.") +} else { + var packages = shop.list_packages() + var pkg_count = packages.length + log.console("Checking for updates (" + text(pkg_count) + " package" + (pkg_count == 1 ? "" : "s") + ")...") + + var updated_count = 0 + + for (var i = 0; i < packages.length; i++) { + var pkg = packages[i] + if (pkg == 'core') continue + + if (update_and_fetch(pkg)) { + updated_count++ + } + } + + if (updated_count > 0) { + log.console("Updated " + text(updated_count) + " package" + (updated_count == 1 ? "" : "s") + ".") + } else { + log.console("All packages are up to date.") + } } $_.stop() diff --git a/why.ce b/why.ce index 7298e5f7..0d8e78d0 100644 --- a/why.ce +++ b/why.ce @@ -1,4 +1,5 @@ var shop = use('shop') +var pkg = use('package') if (!args || args.length < 1) { log.console("Usage: cell why ") @@ -19,7 +20,7 @@ var found = false // stack: array of {alias, pkg} leading to current_pkg function search(current_pkg, stack) { - var deps = shop.dependencies(current_pkg) + var deps = pkg.dependencies(current_pkg) // Sort for consistent output var aliases = []