Files
cell/build.cm

471 lines
14 KiB
Plaintext

// 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/<package_name>.<ext>
// 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