471 lines
13 KiB
Plaintext
471 lines
13 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
|
|
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: []
|
|
},
|
|
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: []
|
|
}
|
|
}
|
|
|
|
// Target aliases for convenience
|
|
Build.target_aliases = {
|
|
'win': 'windows',
|
|
'win64': 'windows',
|
|
'mingw': 'windows',
|
|
'pd': 'playdate'
|
|
}
|
|
|
|
// Resolve target name (handle aliases)
|
|
Build.resolve_target = function(target) {
|
|
if (!target) return null
|
|
target = target.toLowerCase()
|
|
return Build.target_aliases[target] || target
|
|
}
|
|
|
|
// Get toolchain for a target (null = host)
|
|
Build.get_toolchain = function(target) {
|
|
if (!target) return null
|
|
target = Build.resolve_target(target)
|
|
return Build.toolchains[target] || null
|
|
}
|
|
|
|
// 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'
|
|
}
|
|
|
|
// 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 null
|
|
}
|
|
|
|
// 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 = ''
|
|
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', 'threaded', 'single']
|
|
|
|
// 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 is just the generic name + extension (ignoring directory)
|
|
// This allows source/fd_playdate.c to override scripts/fd.c
|
|
var group_key = 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
|
|
Build.get_build_dir = function(target) {
|
|
if (!target) return '.cell/build/static'
|
|
return '.cell/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.startsWith(module_dir + '/')) {
|
|
src_file = src_path.substring(module_dir.length + 1)
|
|
} else if (!src_path.startsWith('/')) {
|
|
src_file = '"$HERE/' + 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 + '"'
|
|
}
|
|
|
|
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")
|
|
log.error(cmd)
|
|
return null
|
|
}
|
|
|
|
return output
|
|
}
|
|
|
|
// Get flags from config for a platform
|
|
Build.get_flags = function(config, platform, key) {
|
|
var flags = ''
|
|
if (config.compilation && config.compilation[key]) {
|
|
flags += config.compilation[key]
|
|
}
|
|
if (config.compilation && config.compilation[platform] && config.compilation[platform][key]) {
|
|
if (flags != '') flags += ' '
|
|
flags += config.compilation[platform][key]
|
|
}
|
|
return flags
|
|
}
|
|
|
|
// Load config from a directory
|
|
Build.load_config = function(dir) {
|
|
var path = dir + '/.cell/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 |