// 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 crypto = use('crypto') var blob = use('blob') var os = use('os') var toolchains = use('toolchains') var shop = use('internal/shop') var pkg_tools = use('package') var Build = {} // ============================================================================ // 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 array(toolchains) } Build.has_target = function(target) { return toolchains[target] != null } 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) { var bb = stone(new blob(str)) return text(crypto.blake2(bb, 32), '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] + '/' if (!fd.stat(current).isDirectory) { fd.mkdir(current) } } } Build.ensure_dir = ensure_dir // ============================================================================ // Compilation // ============================================================================ // 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 if (!fd.is_file(src_path)) { throw new Error('Source file not found: ' + src_path) } // 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') } cmd_parts.push('-DCELL_USE_NAME=' + sym_name) cmd_parts.push('-I"' + pkg_dir + '"') // 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) + '"' } cmd_parts.push(flag) } // 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) { throw new Error('Compilation failed: ' + file) } return obj_path } // Build all C files for a package // Returns array of object file paths Build.build_package = function(pkg, target = Build.detect_host_target(), exclude_main, buildtype = 'release') { var c_files = pkg_tools.get_c_files(pkg, target, exclude_main) var objects = [] for (var i = 0; i < c_files.length; i++) { var obj = Build.compile_file(pkg, c_files[i], target, buildtype) objects.push(obj) } return objects } // ============================================================================ // Dynamic library building // ============================================================================ // Compute link key from all inputs that affect the dylib output function compute_link_key(objects, ldflags, target_ldflags, target, cc) { // Sort objects for deterministic hash var sorted_objects = objects.slice().sort() // Build a string representing all link inputs var parts = [] parts.push('target:' + target) parts.push('cc:' + cc) for (var i = 0; i < sorted_objects.length; i++) { // Object paths are content-addressed, so the path itself is the hash parts.push('obj:' + sorted_objects[i]) } for (var i = 0; i < ldflags.length; i++) { parts.push('ldflag:' + ldflags[i]) } for (var i = 0; i < target_ldflags.length; i++) { parts.push('target_ldflag:' + target_ldflags[i]) } return content_hash(parts.join('\n')) } // Build a dynamic library for a package // Output goes to .cell/lib/. // Dynamic libraries do NOT link against core; undefined symbols are resolved at dlopen time // Uses content-addressed store + symlink for caching Build.build_dynamic = function(pkg, target = Build.detect_host_target(), buildtype = 'release') { var objects = Build.build_package(pkg, target, true, buildtype) // exclude main.c if (objects.length == 0) { log.console('No C files in ' + pkg) return null } var lib_dir = shop.get_lib_dir() var store_dir = lib_dir + '/store' ensure_dir(lib_dir) ensure_dir(store_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 stable_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] // Resolve relative -L paths in ldflags for hash computation var resolved_ldflags = [] 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) + '"' } resolved_ldflags.push(flag) } // Compute link key var link_key = compute_link_key(objects, resolved_ldflags, target_ldflags, target, cc) var store_path = store_dir + '/' + lib_name + '-' + link_key + dylib_ext // Check if already linked in store if (fd.is_file(store_path)) { // Ensure symlink points to the store file if (fd.is_link(stable_path)) { var current_target = fd.readlink(stable_path) if (current_target == store_path) { // Already up to date return stable_path } fd.unlink(stable_path) } else if (fd.is_file(stable_path)) { fd.unlink(stable_path) } fd.symlink(store_path, stable_path) return stable_path } // Build link command var cmd_parts = [cc, '-shared', '-fPIC'] // Platform-specific flags for undefined symbols (resolved at dlopen) and size optimization if (tc.system == 'darwin') { // Allow undefined symbols - they will be resolved when dlopen'd into the main executable cmd_parts.push('-undefined', 'dynamic_lookup') // Dead-strip unused code cmd_parts.push('-Wl,-dead_strip') // Set install_name to stable path so runtime finds it correctly cmd_parts.push('-Wl,-install_name,' + stable_path) // rpath for .cell/local libraries cmd_parts.push('-Wl,-rpath,@loader_path/../local') cmd_parts.push('-Wl,-rpath,' + local_dir) } else if (tc.system == 'linux') { // Allow undefined symbols at link time cmd_parts.push('-Wl,--allow-shlib-undefined') // Garbage collect unused sections cmd_parts.push('-Wl,--gc-sections') // rpath for .cell/local libraries cmd_parts.push('-Wl,-rpath,$ORIGIN/../local') cmd_parts.push('-Wl,-rpath,' + local_dir) } else if (tc.system == 'windows') { // Windows DLLs: use --allow-shlib-undefined for mingw cmd_parts.push('-Wl,--allow-shlib-undefined') } // 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] + '"') } // Do NOT link against core library - symbols resolved at dlopen time // Add LDFLAGS for (var i = 0; i < resolved_ldflags.length; i++) { cmd_parts.push(resolved_ldflags[i]) } for (var i = 0; i < target_ldflags.length; i++) { cmd_parts.push(target_ldflags[i]) } cmd_parts.push('-o', '"' + store_path + '"') var cmd_str = cmd_parts.join(' ') log.console('Linking ' + lib_name + dylib_ext) var ret = os.system(cmd_str) if (ret != 0) { throw new Error('Linking failed: ' + pkg) } // Update symlink to point to the new store file if (fd.is_link(stable_path)) { fd.unlink(stable_path) } else if (fd.is_file(stable_path)) { fd.unlink(stable_path) } fd.symlink(store_path, stable_path) return stable_path } // ============================================================================ // 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 = Build.detect_host_target(), output, buildtype = 'release') { var all_objects = [] var all_ldflags = [] var seen_flags = {} // 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 } var cmd_parts = [cc] for (var i = 0; i < all_objects.length; i++) { cmd_parts.push('"' + all_objects[i] + '"') } for (var i = 0; i < all_ldflags.length; i++) { cmd_parts.push(all_ldflags[i]) } for (var i = 0; i < target_ldflags.length; i++) { cmd_parts.push(target_ldflags[i]) } cmd_parts.push('-o', '"' + output + '"') var cmd_str = cmd_parts.join(' ') log.console('Linking ' + output) var ret = os.system(cmd_str) if (ret != 0) { throw new Error('Linking failed with command: ' + cmd_str) } log.console('Built ' + output) return output } // ============================================================================ // 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, buildtype) results.push({ package: 'core', library: lib }) } catch (e) { log.error('Failed to build core: ' + text(e)) results.push({ package: 'core', error: e }) } } // 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, buildtype) results.push({ package: pkg, library: lib }) } catch (e) { log.error('Failed to build ' + pkg + ': ') log.error(e) results.push({ package: pkg, error: e }) } } return results } return Build