// 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