From aae267a6e9cbc9e5ac897e949b60eed75b25cf5b Mon Sep 17 00:00:00 2001 From: John Alanbrook Date: Mon, 8 Dec 2025 15:27:20 -0600 Subject: [PATCH] dynamic works; static works for non cell builds --- cell.toml | 4 +- scripts/build.ce | 903 ++++++++++++++++++++++++++++++++-------------- scripts/build.cm | 27 +- scripts/shop.cm | 41 ++- scripts/update.ce | 31 +- 5 files changed, 718 insertions(+), 288 deletions(-) diff --git a/cell.toml b/cell.toml index 5d702746..d4134cbf 100644 --- a/cell.toml +++ b/cell.toml @@ -2,7 +2,7 @@ CFLAGS = "-Isource -Wno-incompatible-pointer-types -Wno-missing-braces -Wno-strict-prototypes -Wno-unused-function -Wno-int-conversion" LDFLAGS = "-lstdc++ -lm" -[compilation.macOS] +[compilation.macos_arm64] CFLAGS = "-x objective-c" LDFLAGS = "-framework CoreFoundation -framework CFNetwork" @@ -10,4 +10,4 @@ LDFLAGS = "-framework CoreFoundation -framework CFNetwork" CFLAGS = "-DMINIZ_NO_TIME -DTARGET_EXTENSION -DTARGET_PLAYDATE -I$PLAYDATE_SDK_PATH/C_API" [compilation.windows] -LDFLAGS = "-lbcrypt -lwinhttp -static-libgcc -static-libstdc++" +LDFLAGS = "-lws2_32 -lwinmm -liphlpapi -lbcrypt -lwinhttp -static-libgcc -static-libstdc++" diff --git a/scripts/build.ce b/scripts/build.ce index f137d342..e5027fde 100644 --- a/scripts/build.ce +++ b/scripts/build.ce @@ -1,12 +1,13 @@ // cell build [options] [actor] - Build cell binary // // Modes: -// cell build -d Build libcell_runtime.dylib (development mode) -// cell build [actor] Build static binary for project (release mode) -// cell build -s [actor] Build fully static binary (no external script lookup) +// 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 // // The actor argument specifies the entry point (e.g., "accio" for accio.ce) -// If no actor is specified, builds cell itself. +// If no actor is specified for static builds, builds cell itself. var build = use('build') var shop = use('shop') @@ -17,12 +18,14 @@ var utf8 = use('utf8') var json = use('json') var targets = [ - "arm64-macos", - "x86_64-macos", - "x86_64-linux", - "arm64-linux", + "macos_arm64", + "macos_x86_64", + "linux", + "linux_arm64", "windows", - "playdate" + "windows_i686", + "playdate", + "emscripten" ] // Parse arguments @@ -30,6 +33,8 @@ 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 for (var i = 0; i < args.length; i++) { if (args[i] == '--target' || args[i] == '-t') { @@ -41,22 +46,47 @@ for (var i = 0; i < args.length; i++) { log.console("Available targets: " + targets.join(', ')) $_.stop() } + } else if (args[i] == '-p' || args[i] == '--package') { + if (i + 1 < args.length) { + target_package = args[i + 1] + dynamic_mode = true + i++ + } else { + 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') { + if (i + 1 < args.length) { + output_name = args[i + 1] + i++ + } else { + log.error("-o requires an output name") + $_.stop() + } } else if (args[i] == '--help' || args[i] == '-h') { log.console("Usage: cell build [options] [actor]") - log.console("Build a cell binary for the current project.") + log.console("Build cell binaries or dynamic libraries.") log.console("") log.console("Options:") - log.console(" -d, --dynamic Build libcell_runtime.dylib (dev mode)") + 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(', ')) $_.stop() } else if (args[i] == '--list-targets') { @@ -70,191 +100,376 @@ for (var i = 0; i < args.length; i++) { } } -// Resolve target +// Resolve target - if not specified for dynamic mode, detect host +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) +} + if (target && !build.has_target(target)) { log.console("Available targets: " + targets.join(', ')) throw new Error("Invalid target: " + target) } -// Find cell package - it should be at ~/work/cell or a dependency -function find_cell_dir() { - // First check if we're in the cell directory itself - if (fd.is_file('source/cell.c') && fd.is_file('source/quickjs.c')) { - log.console("Building from cell source directory") - return '.' +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) } - // Check for cell as a local path dependency or linked package + 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() + '/modules/' + parsed.path + var pkg_dir = shop.get_shop_path() + '/packages/' + parsed.path if (fd.is_file(pkg_dir + '/source/cell.c')) { - log.console("Using cell from dependency: " + pkg_dir) return pkg_dir } } - - // Check Shop Core Dir - var core_dir = shop.get_core_dir() - if (fd.is_file(core_dir + '/source/cell.c')) { - log.console("Using cell from core: " + core_dir) - return core_dir - } // Fallback: try ~/work/cell var home_cell = os.getenv('HOME') + '/work/cell' if (fd.is_file(home_cell + '/source/cell.c')) { - log.console("Using cell from: " + home_cell) 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. Add cell as a dependency or run from cell directory.") + log.error("Could not find cell source. Link cell to core or add as dependency.") $_.stop() } +log.console("Using cell from: " + cell_dir) -// Collect all C files from cell -var source_files = build.list_files(cell_dir + '/source') -var script_files = build.list_files(cell_dir + '/scripts') - -// Prefix with directory -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 -var c_files = build.select_c_files(all_files, target) - -// For dynamic mode, exclude main.c if (dynamic_mode) { - c_files = c_files.filter(function(f) { - return f.indexOf('main.c') < 0 && f.indexOf('main_') < 0 - }) - log.console("Dynamic mode: excluding main.c") -} - -log.console("Found " + text(c_files.length) + " C files to compile") - -// Get build directory -var build_dir = dynamic_mode ? shop.get_shop_path() + '/build/dynamic' : build.get_build_dir(target) -build.ensure_dir(build_dir) - -// Load cell config for platform-specific flags -var cell_config = build.load_config(cell_dir) -var target_system = build.get_target_system(target) -var platform = target_system || os.platform() - -var cflags = build.get_flags(cell_config, platform, 'CFLAGS') -var ldflags = build.get_flags(cell_config, platform, 'LDFLAGS') - -// For dynamic builds, add -fPIC -if (dynamic_mode) { - cflags = '-fPIC ' + cflags -} - -// Compile options -var compile_options = { - target: target, - cflags: cflags, - includes: [cell_dir, cell_dir + '/source'], // Add cell root and source for includes - defines: {}, - module_dir: cell_dir // Ensure we run compilation from cell dir context -} - -// Add target-specific defines -if (target == 'playdate') { - compile_options.defines.TARGET_PLAYDATE = true -} - -// Compile all C files -var objects = [] -for (var i = 0; i < c_files.length; i++) { - var src = c_files[i] // Relative to module_dir because we set it above? - - // Adjusted: - var src_rel = c_files[i] - var obj_base = shop.get_global_build_dir() + '/cell' // Build cell into its own area? - - var obj = build_dir + (dynamic_mode ? '/' : '/static/') + src_rel + '.o' // Separate static build + // ======================================================================== + // DYNAMIC MODE: Build cellmod.dylib for all packages + // ======================================================================== + var build_dir = get_target_build_dir() + build.ensure_dir(build_dir) - var src_abs = cell_dir + '/' + src_rel - - // Check if recompilation needed - 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 - } + // 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() } - if (needs_compile) { - // Ensure specific file includes if needed? - // The global includes should cover it. - var result = build.compile_file(src_abs, obj, compile_options) - if (!result) { - log.error("Build failed") - $_.stop() - } - } - objects.push(obj) -} - -// For static builds, also compile package C files -if (!dynamic_mode) { - var packages = shop.list_packages() - for (var p = 0; p < packages.length; p++) { - var pkg = packages[p] + // 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() + '/modules/' + parsed.path + 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) - - var pkg_config = build.load_config(pkg_dir) - var pkg_ldflags = build.get_flags(pkg_config, platform, 'LDFLAGS') - if (pkg_ldflags) { - if (ldflags != '') ldflags += ' ' - ldflags += pkg_ldflags + if (!fd.is_dir(pkg_dir)) { + log.console("Skipping " + pkg + " (not found)") + continue } - if (pkg_c_files.length > 0) { - log.console("Compiling " + text(pkg_c_files.length) + " C files from " + parsed.path) + 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 pkg_cflags = build.get_flags(pkg_config, platform, 'CFLAGS') - - // Create symbol prefix for package - 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 objects = [] + for (var i = 0; i < local_c_files.length; i++) { + var src = local_c_files[i] + var obj = build_dir + '/local/' + src + '.o' - // Use shop to determine build directory for this package, instead of manual concat - var pkg_build_base = shop.get_build_dir(parsed.path) - var obj = pkg_build_base + '/' + pkg_c_files[f] + '.o' + var safe_name = src.substring(0, src.lastIndexOf('.')).replace(/\//g, '_').replace(/-/g, '_') + var use_name = 'js_local_' + safe_name + '_use' - 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 = { + var compile_options = { target: target, - cflags: pkg_cflags, - includes: [pkg_dir], - defines: { CELL_USE_NAME: use_name }, - module_dir: pkg_dir + cflags: local_cflags, + includes: ['.'], + defines: { CELL_USE_NAME: use_name } } var needs_compile = true @@ -267,39 +482,262 @@ if (!dynamic_mode) { } if (needs_compile) { - var result = build.compile_file(src, obj, pkg_options) + var result = build.compile_file(src, obj, compile_options) if (!result) { - log.error("Build failed") + 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++ } } - - // Collect C files from local project (only if not building from cell source directory) - if (cell_dir != '.') { - var local_config = shop.load_config() || {} - var local_ldflags = build.get_flags(local_config, platform, 'LDFLAGS') - if (local_ldflags) { - if (ldflags != '') ldflags += ' ' - ldflags += local_ldflags + + // 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') + 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/' + local_c_files[f] + '.o' + var obj = build_dir + '/local/' + src + '.o' - var safe_name = local_c_files[f].substring(0, local_c_files[f].lastIndexOf('.')).replace(/\//g, '_').replace(/-/g, '_') + var safe_name = src.substring(0, src.lastIndexOf('.')).replace(/\//g, '_').replace(/-/g, '_') var use_name = 'js_local_' + safe_name + '_use' var local_options = { @@ -329,68 +767,38 @@ if (!dynamic_mode) { } } } -} - -log.console("Compiled " + text(objects.length) + " object files") - -// Link -if (dynamic_mode) { - // Link into shared library - var dylib_ext = build.get_dylib_ext(target) - var lib_name = build_dir + '/libcell_runtime' + dylib_ext - var link_flags = '-shared -fPIC' - if (platform == 'macOS' || platform == 'darwin') { - link_flags += ' -framework CoreFoundation -framework CFNetwork' - } - if (ldflags) link_flags += ' ' + ldflags + log.console("Compiled " + text(objects.length) + " object files") - var objs_str = objects.join(' ') - var cmd = 'cc ' + link_flags + ' ' + objs_str + ' -o ' + lib_name - - log.console("Linking " + lib_name) - var ret = os.system(cmd) - if (ret != 0) { - log.error("Linking failed") - $_.stop() - } - - log.console("") - log.console("Build complete: " + lib_name) - log.console("") - log.console("To use: copy to /opt/homebrew/lib/ or set DYLD_LIBRARY_PATH") -} else { // Link into executable var exe_ext = build.get_exe_ext(target) - var exe_name = build_dir + '/cell' + exe_ext - + // 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: [] } - - // Add platform-specific libraries + 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 (core.qop + project scripts) - var pack_path = build_dir + '/pack.qop' - // Collect scripts to pack + // Create the pack (scripts) + var pack_path = build_dir + '/pack.qop' var pack_files = [] - // Always include core scripts from cell + // 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] @@ -399,78 +807,29 @@ if (dynamic_mode) { } } - // If building a project (not cell itself), include project scripts - if (cell_dir != '.') { - // Include local scripts + // 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] }) } - - // Include package scripts - 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() + '/modules/' + 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] }) - } - } } - // Create the pack - log.console("Creating pack with " + text(pack_files.length) + " scripts") - var writer = qop.write(pack_path) - // Add entry point configuration if actor is specified - if (actor) { - var entry_config = { - entry: actor, - static_only: static_only - } - writer.add_file('__entry__.json', utf8.encode(json.encode(entry_config))) - } + var final_name = output_name || 'app' + exe_ext - for (var i = 0; i < pack_files.length; i++) { - var pf = pack_files[i] - var data = fd.slurp(pf.source) - if (data) { - writer.add_file(pf.path, data) - } - } - writer.finalize() - - // Append pack to executable - var exe_data = fd.slurp(exe_name) - var pack_data = fd.slurp(pack_path) - fd.slurpwrite(exe_name, exe_data) - - // Append pack data - var fh = fd.open(exe_name, 'a') - fd.write(fh, pack_data) - fd.close(fh) - - // Determine final output name - var final_name - if (actor) { - // Named after the actor - var actor_base = actor - if (actor_base.endsWith('.ce')) actor_base = actor_base.substring(0, actor_base.length - 3) - if (actor_base.indexOf('/') >= 0) actor_base = actor_base.substring(actor_base.lastIndexOf('/') + 1) - final_name = actor_base + exe_ext - } else if (cell_dir != '.') { - // Named after the project directory - var cwd = fd.cwd() - var project_name = cwd.substring(cwd.lastIndexOf('/') + 1) - final_name = project_name + exe_ext - } else { - final_name = 'cell' + exe_ext - } - - // Copy to project root os.system('cp ' + exe_name + ' ' + final_name) log.console("") diff --git a/scripts/build.cm b/scripts/build.cm index 0900faf2..c1994169 100644 --- a/scripts/build.cm +++ b/scripts/build.cm @@ -337,6 +337,12 @@ Build.get_cc = function(target) { 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) @@ -590,18 +596,20 @@ Build.compile_file = function(src_path, obj_path, options) { 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) + 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 (or relative to root?) - // If we use absolute path, it should be fine + // 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? @@ -611,7 +619,7 @@ Build.compile_file = function(src_path, obj_path, options) { // 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 + full_cmd = 'HERE=$(pwd); cd "' + module_dir + '" && ' + cc_cmd } else { // Standard compilation from current dir var include_str = '' @@ -707,16 +715,23 @@ Build.link_executable = function(objects, output, options) { return output } -// Get flags from config for a platform -Build.get_flags = function(config, platform, key) { +// 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] } + // 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] } + // 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 } diff --git a/scripts/shop.cm b/scripts/shop.cm index 63b20802..64ebabb1 100644 --- a/scripts/shop.cm +++ b/scripts/shop.cm @@ -826,6 +826,7 @@ function get_build_dir(pkg) { } Shop.get_build_dir = get_build_dir +Shop.get_global_build_dir = get_global_build_dir function get_rel_path(path, pkg) { if (!pkg) { @@ -1019,7 +1020,7 @@ function resolve_c_symbol(path, package_context) if (!static_only) { // Then try dynamic library var build_dir = get_build_dir(package_context) - var local_dl_name = build_dir + '/cellmod' + dylib_ext + var local_dl_name = build_dir + '/libcellmod' + dylib_ext if (fd.is_file(local_dl_name)) { if (!open_dls[local_dl_name]) @@ -1064,7 +1065,7 @@ function resolve_c_symbol(path, package_context) // Then try dynamic library for package (skip in static_only mode) if (!static_only) { var pkg_build_dir = get_build_dir(canon_pkg) - var dl_path = pkg_build_dir + '/cellmod' + dylib_ext + 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]) { @@ -1658,6 +1659,21 @@ Shop.module_reload = function(path, package) { } } +// Filter out main*.c files (they're only for static builds) +function is_main_file(file) { + var basename = file + var slash = file.lastIndexOf('/') + if (slash >= 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' +} + Shop.build_package = function(package) { if (package == 'local') package = null @@ -1686,6 +1702,10 @@ Shop.build_package = function(package) for (var i=0; i 0) { - var lib_name = build_dir + '/cellmod' + dylib_ext + var lib_name = build_dir + '/libcellmod' + dylib_ext var lib_meta_path = lib_name + '.meta' var link_flags = '-fPIC -shared' + // Link against core cellmod.dylib + var core_build = get_core_build_dir() + if (platform == 'macOS') { + link_flags += ' -L' + core_build + ' -Wl,-rpath,@loader_path/../../core' + } else if (platform == 'Linux' || platform == 'linux') { + link_flags += ' -L' + core_build + ' -Wl,-rpath,$ORIGIN/../../core' + } else if (platform == 'Windows') { + link_flags += ' -L' + core_build + } + link_flags += ' -lcellmod' + var ldflags = get_flags(config, platform, 'LDFLAGS') if (ldflags != '') link_flags += ' ' + ldflags - var temp_lib = 'cellmod' + dylib_ext + var temp_lib = 'libcellmod' + dylib_ext var objs_str = '' for (var i=0; i 0 ? args[0] : null +var target = 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.") + log.console("") + log.console("Options:") + log.console(" --target, -t Build for specific target platform") + log.console("") + log.console("This command updates all installed packages from their remote") + log.console("sources, then rebuilds all dynamic libraries.") + $_.stop() + } +} var packages = shop.list_shop_packages() log.console("Checking for updates (" + packages.length + " packages)...") +// 1. Update all packages for (var info of packages) { var pack = info.package if (!pack || pack == 'core') continue log.console("Updating " + pack) shop.update(pack) - shop.build_package(pack) } $_.stop()