456 lines
13 KiB
Plaintext
456 lines
13 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 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/<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 (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
|