Files
cell/build.cm

748 lines
23 KiB
Plaintext

// build.cm - Build utilities for compiling Cell projects and binaries
var fd = use('fd')
var toml = use('toml')
var json = use('json')
var crypto = use('crypto')
var utf8 = use('utf8')
var os_mod = use('os')
var Build = {}
// Embedded cross-compilation toolchain configurations
// Note: build.cm uses shop to resolve paths
Build.toolchains = {
playdate: {
binaries: {
c: 'arm-none-eabi-gcc',
cpp: 'arm-none-eabi-g++',
ar: 'arm-none-eabi-ar',
strip: 'arm-none-eabi-strip',
objcopy: 'arm-none-eabi-objcopy',
ld: 'arm-none-eabi-gcc'
},
host_machine: {
system: 'playdate',
cpu_family: 'arm',
cpu: 'cortex-m7',
endian: 'little'
},
c_args: ['-mcpu=cortex-m7', '-mthumb', '-mfloat-abi=hard', '-mfpu=fpv5-sp-d16', '-fno-exceptions'],
c_link_args: ['-mcpu=cortex-m7', '-mthumb', '-mfloat-abi=hard', '-mfpu=fpv5-sp-d16', '-nostartfiles', "-T/Users/john/Developer/PlaydateSDK/C_API/buildsupport/link_map.ld"]
},
windows: {
binaries: {
c: 'x86_64-w64-mingw32-gcc',
cpp: 'x86_64-w64-mingw32-g++',
ar: 'x86_64-w64-mingw32-ar',
windres: 'x86_64-w64-mingw32-windres',
strip: 'x86_64-w64-mingw32-strip'
},
host_machine: {
system: 'windows',
cpu_family: 'x86_64',
cpu: 'x86_64',
endian: 'little'
},
c_args: [],
c_link_args: []
},
windows_i686: {
binaries: {
c: 'i686-w64-mingw32-gcc',
cpp: 'i686-w64-mingw32-g++',
ar: 'i686-w64-mingw32-ar',
windres: 'i686-w64-mingw32-windres',
strip: 'i686-w64-mingw32-strip'
},
host_machine: {
system: 'windows',
cpu_family: 'x86',
cpu: 'i686',
endian: 'little'
},
c_args: [],
c_link_args: []
},
linux: {
binaries: {
c: 'zig cc -target x86_64-linux-musl',
cpp: 'zig c++ -target x86_64-linux-musl',
ar: 'zig ar',
strip: 'strip'
},
host_machine: {
system: 'linux',
cpu_family: 'x86_64',
cpu: 'x86_64',
endian: 'little'
},
c_args: [],
c_link_args: []
},
linux_arm64: {
binaries: {
c: 'zig cc -target aarch64-linux-musl',
cpp: 'zig c++ -target aarch64-linux-musl',
ar: 'zig ar',
strip: 'strip'
},
host_machine: {
system: 'linux',
cpu_family: 'aarch64',
cpu: 'aarch64',
endian: 'little'
},
c_args: [],
c_link_args: []
},
macos_arm64: {
binaries: {
c: 'clang -target arm64-apple-macos11',
cpp: 'clang++ -target arm64-apple-macos11',
ar: 'ar',
strip: 'strip'
},
host_machine: {
system: 'darwin',
cpu_family: 'aarch64',
cpu: 'aarch64',
endian: 'little'
},
c_args: ['-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk'],
c_link_args: ['-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk']
},
macos_x86_64: {
binaries: {
c: 'clang -target x86_64-apple-macos10.12',
cpp: 'clang++ -target x86_64-apple-macos10.12',
ar: 'ar',
strip: 'strip'
},
host_machine: {
system: 'darwin',
cpu_family: 'x86_64',
cpu: 'x86_64',
endian: 'little'
},
c_args: ['-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk'],
c_link_args: ['-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk']
},
ios_arm64: {
binaries: {
c: 'clang -target arm64-apple-ios12.0',
cpp: 'clang++ -target arm64-apple-ios12.0',
ar: 'ar',
strip: 'strip'
},
host_machine: {
system: 'ios',
cpu_family: 'aarch64',
cpu: 'aarch64',
endian: 'little'
},
c_args: ['-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk', '-fembed-bitcode'],
c_link_args: ['-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk', '-fembed-bitcode']
},
ios_simulator_arm64: {
binaries: {
c: 'clang -target arm64-apple-ios12.0-simulator',
cpp: 'clang++ -target arm64-apple-ios12.0-simulator',
ar: 'ar',
strip: 'strip'
},
host_machine: {
system: 'ios',
cpu_family: 'aarch64',
cpu: 'aarch64',
endian: 'little'
},
c_args: ['-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk'],
c_link_args: ['-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk']
},
ios_simulator_x86_64: {
binaries: {
c: 'clang -target x86_64-apple-ios12.0-simulator',
cpp: 'clang++ -target x86_64-apple-ios12.0-simulator',
ar: 'ar',
strip: 'strip'
},
host_machine: {
system: 'ios',
cpu_family: 'x86_64',
cpu: 'x86_64',
endian: 'little'
},
c_args: ['-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk'],
c_link_args: ['-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk']
},
tvos_arm64: {
binaries: {
c: 'clang -target arm64-apple-tvos12.0',
cpp: 'clang++ -target arm64-apple-tvos12.0',
ar: 'ar',
strip: 'strip'
},
host_machine: {
system: 'tvos',
cpu_family: 'aarch64',
cpu: 'aarch64',
endian: 'little'
},
c_args: ['-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS.sdk', '-fembed-bitcode'],
c_link_args: ['-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS.sdk', '-fembed-bitcode']
},
tvos_simulator_arm64: {
binaries: {
c: 'clang -target arm64-apple-tvos12.0-simulator',
cpp: 'clang++ -target arm64-apple-tvos12.0-simulator',
ar: 'ar',
strip: 'strip'
},
host_machine: {
system: 'tvos',
cpu_family: 'aarch64',
cpu: 'aarch64',
endian: 'little'
},
c_args: ['-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/AppleTVSimulator.platform/Developer/SDKs/AppleTVSimulator.sdk'],
c_link_args: ['-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/AppleTVSimulator.platform/Developer/SDKs/AppleTVSimulator.sdk']
},
tvos_simulator_x86_64: {
binaries: {
c: 'clang -target x86_64-apple-tvos12.0-simulator',
cpp: 'clang++ -target x86_64-apple-tvos12.0-simulator',
ar: 'ar',
strip: 'strip'
},
host_machine: {
system: 'tvos',
cpu_family: 'x86_64',
cpu: 'x86_64',
endian: 'little'
},
c_args: ['-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/AppleTVSimulator.platform/Developer/SDKs/AppleTVSimulator.sdk'],
c_link_args: ['-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/AppleTVSimulator.platform/Developer/SDKs/AppleTVSimulator.sdk']
},
watchos_arm64: {
binaries: {
c: 'clang -target arm64_32-apple-watchos5.0',
cpp: 'clang++ -target arm64_32-apple-watchos5.0',
ar: 'ar',
strip: 'strip'
},
host_machine: {
system: 'watchos',
cpu_family: 'aarch64',
cpu: 'arm64_32',
endian: 'little'
},
c_args: ['-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/WatchOS.platform/Developer/SDKs/WatchOS.sdk', '-fembed-bitcode'],
c_link_args: ['-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/WatchOS.platform/Developer/SDKs/WatchOS.sdk', '-fembed-bitcode']
},
watchos_simulator_arm64: {
binaries: {
c: 'clang -target arm64-apple-watchos5.0-simulator',
cpp: 'clang++ -target arm64-apple-watchos5.0-simulator',
ar: 'ar',
strip: 'strip'
},
host_machine: {
system: 'watchos',
cpu_family: 'aarch64',
cpu: 'aarch64',
endian: 'little'
},
c_args: ['-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/WatchSimulator.platform/Developer/SDKs/WatchSimulator.sdk'],
c_link_args: ['-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/WatchSimulator.platform/Developer/SDKs/WatchSimulator.sdk']
},
watchos_simulator_x86_64: {
binaries: {
c: 'clang -target x86_64-apple-watchos5.0-simulator',
cpp: 'clang++ -target x86_64-apple-watchos5.0-simulator',
ar: 'ar',
strip: 'strip'
},
host_machine: {
system: 'watchos',
cpu_family: 'x86_64',
cpu: 'x86_64',
endian: 'little'
},
c_args: ['-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/WatchSimulator.platform/Developer/SDKs/WatchSimulator.sdk'],
c_link_args: ['-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/WatchSimulator.platform/Developer/SDKs/WatchSimulator.sdk']
},
visionos_arm64: {
binaries: {
c: 'clang -target arm64-apple-xros1.0',
cpp: 'clang++ -target arm64-apple-xros1.0',
ar: 'ar',
strip: 'strip'
},
host_machine: {
system: 'visionos',
cpu_family: 'aarch64',
cpu: 'aarch64',
endian: 'little'
},
c_args: ['-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/XROS.platform/Developer/SDKs/XROS.sdk'],
c_link_args: ['-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/XROS.platform/Developer/SDKs/XROS.sdk']
},
visionos_simulator_arm64: {
binaries: {
c: 'clang -target arm64-apple-xros1.0-simulator',
cpp: 'clang++ -target arm64-apple-xros1.0-simulator',
ar: 'ar',
strip: 'strip'
},
host_machine: {
system: 'visionos',
cpu_family: 'aarch64',
cpu: 'aarch64',
endian: 'little'
},
c_args: ['-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/XRSimulator.platform/Developer/SDKs/XRSimulator.sdk'],
c_link_args: ['-isysroot', '/Applications/Xcode.app/Contents/Developer/Platforms/XRSimulator.platform/Developer/SDKs/XRSimulator.sdk']
},
emscripten: {
binaries: {
c: 'emcc',
cpp: 'em++',
ar: 'emar',
strip: 'emstrip'
},
host_machine: {
system: 'emscripten',
cpu_family: 'wasm32',
cpu: 'wasm32',
endian: 'little'
},
c_args: [],
c_link_args: []
}
}
Build.has_target = function(target) {
return Build.toolchains[target] != null
}
// Get toolchain for a target (null = host)
Build.get_toolchain = function(target) {
if (!target) return null
return Build.toolchains[target]
}
// Get compiler command for target
Build.get_cc = function(target) {
var tc = Build.get_toolchain(target)
if (tc && tc.binaries && tc.binaries.c) return tc.binaries.c
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)
if (tc && tc.binaries && tc.binaries.ar) return tc.binaries.ar
return 'ar'
}
// Get extra C flags for target
Build.get_target_cflags = function(target) {
var tc = Build.get_toolchain(target)
if (tc && tc.c_args) return tc.c_args.join(' ')
return ''
}
// Get extra link flags for target
Build.get_target_ldflags = function(target) {
var tc = Build.get_toolchain(target)
if (tc && tc.c_link_args) return tc.c_link_args.join(' ')
return ''
}
// Get target system name
Build.get_target_system = function(target) {
var tc = Build.get_toolchain(target)
if (tc && tc.host_machine && tc.host_machine.system) return tc.host_machine.system
return os_mod.platform()
}
// Get executable extension for target
Build.get_exe_ext = function(target) {
var sys = Build.get_target_system(target)
if (sys == 'windows') return '.exe'
return ''
}
// Get shared library extension for target
Build.get_dylib_ext = function(target) {
var sys = Build.get_target_system(target)
if (sys == 'windows') return '.dll'
if (sys == 'darwin' || sys == 'macOS') return '.dylib'
return '.so'
}
// Ensure directory exists
function ensure_dir(path) {
if (fd.stat(path).isDirectory) return true
var parts = path.split('/')
var current = ''
// Handle absolute paths (leading /)
if (path.startsWith('/')) {
current = '/'
}
for (var i = 0; i < parts.length; i++) {
if (parts[i] == '') continue
current += parts[i] + '/'
if (!fd.stat(current).isDirectory) {
fd.mkdir(current)
}
}
return true
}
Build.ensure_dir = ensure_dir
// Get hash of a string
function get_hash(str) {
return text(crypto.blake2(utf8.encode(str)), 'h')
}
Build.get_hash = get_hash
// List all files in a directory recursively
function list_files_recursive(dir, prefix, results) {
prefix = prefix || ""
results = results || []
var list = fd.readdir(dir)
if (!list) return results
for (var i = 0; i < list.length; i++) {
var item = list[i]
if (item == '.' || item == '..') continue
if (item.startsWith('.')) continue
var full_path = dir + "/" + item
var rel_path = prefix ? prefix + "/" + item : item
var st = fd.stat(full_path)
if (st.isDirectory) {
// Skip build directories
if (item == 'build' || item.startsWith('build_')) continue
list_files_recursive(full_path, rel_path, results)
} else {
results.push(rel_path)
}
}
return results
}
Build.list_files = list_files_recursive
// Select C files for a target, handling target-specific variants
// e.g., if target is 'playdate', prefer fd_playdate.c over fd.c
// Platform-specific files can be in any directory (source/ or scripts/)
Build.select_c_files = function(files, target) {
var c_files = []
var target_suffix = target ? '_' + target : null
// Known target suffixes for platform-specific files
var known_targets = ['playdate', 'windows', 'linux', 'macos', 'emscripten']
// First pass: collect all files and identify platform-specific ones
// Group by generic name (ignoring directory) to find cross-directory variants
var name_groups = {} // generic_name+ext -> { generics: [], variants: { target: file } }
for (var i = 0; i < files.length; i++) {
var file = files[i]
if (!file.endsWith('.c') && !file.endsWith('.cpp')) continue
var ext = file.endsWith('.cpp') ? '.cpp' : '.c'
var base = file.substring(0, file.length - ext.length)
var dir = ''
var name = base
var slash = base.lastIndexOf('/')
if (slash >= 0) {
dir = base.substring(0, slash + 1)
name = base.substring(slash + 1)
}
// Check if this is a target-specific file
var is_target_specific = false
var target_name = null
var generic_name = name
for (var t = 0; t < known_targets.length; t++) {
var suffix = '_' + known_targets[t]
if (name.endsWith(suffix)) {
is_target_specific = true
target_name = known_targets[t]
generic_name = name.substring(0, name.length - suffix.length)
break
}
}
// Group key must include directory to avoid cross-directory collisions
// But still allow same-directory variants (e.g. source/scheduler.c vs source/scheduler_playdate.c)
var group_key = dir + generic_name + ext
if (!name_groups[group_key]) {
name_groups[group_key] = { generics: [], variants: {} }
}
if (is_target_specific) {
// Platform-specific file - store by target name
name_groups[group_key].variants[target_name] = file
} else {
// Generic file - could have multiple in different directories
name_groups[group_key].generics.push(file)
}
}
// Second pass: select appropriate file from each group
for (var key in name_groups) {
var group = name_groups[key]
var selected = null
// If we have a target, check for target-specific variant first
if (target && group.variants[target]) {
selected = group.variants[target]
} else if (group.generics.length > 0) {
// Use generic if no target-specific variant
// If multiple generics exist (shouldn't happen normally), use first one
selected = group.generics[0]
} else {
// No generic, only variants exist
// This handles cases like scheduler_threaded.c vs scheduler_playdate.c
// where there's no generic scheduler.c
if (target) {
// Only include if it's for our target
if (group.variants[target]) {
selected = group.variants[target]
}
} else {
// No target specified, prefer 'threaded' variant as default
if (group.variants['threaded']) {
selected = group.variants['threaded']
}
}
}
if (selected) {
c_files.push(selected)
}
}
return c_files
}
// Get build directory for a target
// Uses shop module to get the global shop path
Build.get_build_dir = function(target) {
var shop = use('shop')
var shop_path = shop.get_shop_path()
if (!target) return shop_path + '/build/static'
return shop_path + '/build/' + target
}
// Compile a single C file
// Returns object path on success, null on failure
Build.compile_file = function(src_path, obj_path, options) {
options = options || {}
var target = options.target
var cflags = options.cflags || ''
var includes = options.includes || []
var defines = options.defines || {}
var module_dir = options.module_dir || '.'
var cc = Build.get_cc(target)
var target_cflags = Build.get_target_cflags(target)
ensure_dir(obj_path.substring(0, obj_path.lastIndexOf('/')))
var full_cmd
if (module_dir != '.') {
// If compiling in module dir, we need to adjust paths
// Adjust includes - prefix with $HERE if relative
var include_str = ''
for (var i = 0; i < includes.length; i++) {
var inc = includes[i]
if (!inc.startsWith('/')) {
inc = '"$HERE/' + inc + '"'
}
include_str += ' -I' + inc
}
var define_str = ''
for (var k in defines) {
if (defines[k] == true) {
define_str += ' -D' + k
} else {
define_str += ' -D' + k + '=' + defines[k]
}
}
var compile_flags = ' -O3' + include_str + define_str
if (target_cflags) compile_flags += ' ' + target_cflags
if (cflags) compile_flags += ' ' + cflags
// Adjust source path relative to module dir
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) + '"'
} else if (!src_path.startsWith('/')) {
src_file = '"$HERE/' + src_path + '"'
} else {
// 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?
// Yes absolute paths work anywhere.
// Issue reported was: /Users/john/work/accio//Users/john/.cell/build/...
// This happens if we constructed a path that was "module_dir + '/' + absolute_path" somewhere.
// 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
} else {
// Standard compilation from current dir
var include_str = ''
for (var i = 0; i < includes.length; i++) {
include_str += ' -I' + includes[i]
}
var define_str = ''
for (var k in defines) {
if (defines[k] == true) {
define_str += ' -D' + k
} else {
define_str += ' -D' + k + '=' + defines[k]
}
}
var base_cmd = cc + ' -c'
var compile_flags = ' -O3' + include_str + define_str
if (target_cflags) compile_flags += ' ' + target_cflags
if (cflags) compile_flags += ' ' + cflags
full_cmd = base_cmd + compile_flags + ' ' + src_path + ' -o ' + obj_path
}
log.console("Compiling " + src_path)
var ret = os_mod.system(full_cmd)
if (ret != 0) {
log.error("Compilation failed: " + src_path)
return null
}
return obj_path
}
// Link object files into a static library
Build.link_static = function(objects, output, options) {
options = options || {}
var target = options.target
var ar = Build.get_ar(target)
ensure_dir(output.substring(0, output.lastIndexOf('/')))
var objs_str = objects.join(' ')
var cmd = ar + ' rcs ' + output + ' ' + objs_str
log.console("Creating static library " + output)
var ret = os_mod.system(cmd)
if (ret != 0) {
log.error("Archiving failed")
return null
}
return output
}
// Link object files into an executable
Build.link_executable = function(objects, output, options) {
options = options || {}
var target = options.target
var ldflags = options.ldflags || ''
var libs = options.libs || []
var cc = Build.get_cc(target)
var target_ldflags = Build.get_target_ldflags(target)
var exe_ext = Build.get_exe_ext(target)
if (!output.endsWith(exe_ext)) {
output = output + exe_ext
}
ensure_dir(output.substring(0, output.lastIndexOf('/')))
var objs_str = objects.join(' ')
var libs_str = ''
for (var i = 0; i < libs.length; i++) {
libs_str += ' -l' + libs[i]
}
var link_flags = ''
if (target_ldflags) link_flags += ' ' + target_ldflags
if (ldflags) link_flags += ' ' + ldflags
var cmd = cc + ' ' + objs_str + libs_str + link_flags + ' -o ' + output
log.console("Linking " + output)
var ret = os_mod.system(cmd)
if (ret != 0) {
log.error("Linking failed")
return null
}
return output
}
// 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
}
// Load config from a directory
// Config is now at <dir>/cell.toml (package root), not <dir>/.cell/cell.toml
Build.load_config = function(dir) {
var path = dir + '/cell.toml'
if (!fd.is_file(path)) return {}
var content = fd.slurp(path)
if (!content || !content.length) return {}
return toml.decode(text(content))
}
return Build