// 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 replace(str, '$LOCAL', get_local_dir()) } // Replace sigils in an array of flags function replace_sigils_array(flags) { var result = [] arrfor(flags, function(flag) { push(result, replace_sigils(flag)) }) 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(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 = array(path, '/') var current = starts_with(path, '/') ? '/' : '' for (var i = 0; i < length(parts); i++) { if (parts[i] == '') continue current += parts[i] + '/' if (!fd.stat(current).isDirectory) fd.mkdir(current) } } Build.ensure_dir = ensure_dir // ============================================================================ // 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 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 = array(cmd_parts, ['-O3', '-DNDEBUG']) } else if (buildtype == 'debug') { cmd_parts = array(cmd_parts, ['-O2', '-g']) } else if (buildtype == 'minsize') { cmd_parts = array(cmd_parts, ['-Os', '-DNDEBUG']) } push(cmd_parts, '-DCELL_USE_NAME=' + sym_name) push(cmd_parts, '-I"' + pkg_dir + '"') // Add package CFLAGS (resolve relative -I paths) arrfor(cflags, function(flag) { if (starts_with(flag, '-I') && !starts_with(flag, '-I/')) { flag = '-I"' + pkg_dir + '/' + text(flag, 2) + '"' } push(cmd_parts, flag) }) // Add target CFLAGS arrfor(target_cflags, function(flag) { push(cmd_parts, flag) }) push(cmd_parts, '"' + src_path + '"') var cmd_str = text(cmd_parts, ' ') // 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 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 = [] arrfor(c_files, function(file) { var obj = Build.compile_file(pkg, file, target, buildtype) push(objects, 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 = sort(objects) // Build a string representing all link inputs var parts = [] push(parts, 'target:' + target) push(parts, 'cc:' + cc) arrfor(sorted_objects, function(obj) { // Object paths are content-addressed, so the path itself is the hash push(parts, 'obj:' + obj) }) arrfor(ldflags, function(flag) { push(parts, 'ldflag:' + flag) }) arrfor(target_ldflags, function(flag) { push(parts, 'target_ldflag:' + flag) }) return content_hash(text(parts, '\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 (length(objects) == 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 = [] arrfor(ldflags, function(flag) { if (starts_with(flag, '-L') && !starts_with(flag, '-L/')) { flag = '-L"' + pkg_dir + '/' + text(flag, 2) + '"' } push(resolved_ldflags, 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') { cmd_parts = array(cmd_parts, [ '-undefined', 'dynamic_lookup', '-Wl,-dead_strip', '-Wl,-install_name,' + stable_path, '-Wl,-rpath,@loader_path/../local', '-Wl,-rpath,' + local_dir ]) } else if (tc.system == 'linux') { cmd_parts = array(cmd_parts, [ '-Wl,--allow-shlib-undefined', '-Wl,--gc-sections', '-Wl,-rpath,$ORIGIN/../local', '-Wl,-rpath,' + local_dir ]) } else if (tc.system == 'windows') { // Windows DLLs: use --allow-shlib-undefined for mingw push(cmd_parts, '-Wl,--allow-shlib-undefined') } // Add .cell/local to library search path push(cmd_parts, '-L"' + local_dir + '"') arrfor(objects, function(obj) { push(cmd_parts, '"' + obj + '"') }) // Do NOT link against core library - symbols resolved at dlopen time cmd_parts = array(cmd_parts, resolved_ldflags) cmd_parts = array(cmd_parts, target_ldflags) push(cmd_parts, '-o') push(cmd_parts, '"' + store_path + '"') var cmd_str = text(cmd_parts, ' ') log.console('Linking ' + lib_name + dylib_ext) var ret = os.system(cmd_str) if (ret != 0) { throw 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 arrfor(packages, function(pkg) { var is_core = (pkg == 'core') // For core, include main.c; for others, exclude it var objects = Build.build_package(pkg, target, !is_core, buildtype) arrfor(objects, function(obj) { push(all_objects, obj) }) // 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 + ':' + text(ldflags, ' ') if (!seen_flags[ldflags_key]) { seen_flags[ldflags_key] = true arrfor(ldflags, function(flag) { // Resolve relative -L paths if (starts_with(flag, '-L') && !starts_with(flag, '-L/')) { flag = '-L"' + pkg_dir + '/' + text(flag, 2) + '"' } push(all_ldflags, flag) }) } }) if (length(all_objects) == 0) { throw 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 (!ends_with(output, exe_ext) && exe_ext) { output = output + exe_ext } var cmd_parts = [cc] arrfor(all_objects, function(obj) { push(cmd_parts, '"' + obj + '"') }) arrfor(all_ldflags, function(flag) { push(cmd_parts, flag) }) arrfor(target_ldflags, function(flag) { push(cmd_parts, flag) }) push(cmd_parts, '-o', '"' + output + '"') var cmd_str = text(cmd_parts, ' ') log.console('Linking ' + output) var ret = os.system(cmd_str) if (ret != 0) { throw 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 (find(packages, 'core') != null) { try { var lib = Build.build_dynamic('core', target, buildtype) push(results, { package: 'core', library: lib }) } catch (e) { log.error('Failed to build core: ' + text(e)) push(results, { package: 'core', error: e }) } } // Build other packages arrfor(packages, function(pkg) { if (pkg == 'core') return try { var lib = Build.build_dynamic(pkg, target, buildtype) push(results, { package: pkg, library: lib }) } catch (e) { log.error('Failed to build ' + pkg + ': ') log.console(e.message) log.console(e.stack) push(results, { package: pkg, error: e }) } }) return results } return Build