Files
cell/scripts/build.ce
2025-12-08 13:01:44 -06:00

499 lines
16 KiB
Plaintext

// cell build [options] [actor] - Build cell binary
//
// Modes:
// cell build -d Build libcell_runtime.dylib (development mode)
// cell build [actor] Build static binary for project (release mode)
// cell build -s [actor] Build fully static binary (no external script lookup)
//
// The actor argument specifies the entry point (e.g., "accio" for accio.ce)
// If no actor is specified, builds cell itself.
var build = use('build')
var shop = use('shop')
var fd = use('fd')
var os = use('os')
var qop = use('qop')
var utf8 = use('utf8')
var json = use('json')
var targets = [
"arm64-macos",
"x86_64-macos",
"x86_64-linux",
"arm64-linux",
"windows",
"playdate"
]
// Parse arguments
var target = null
var dynamic_mode = false
var static_only = false // -s flag: don't look outside pack for scripts
var actor = null
for (var i = 0; i < args.length; i++) {
if (args[i] == '--target' || args[i] == '-t') {
if (i + 1 < args.length) {
target = args[i + 1]
i++
} else {
log.error("--target requires an argument")
log.console("Available targets: " + targets.join(', '))
$_.stop()
}
} else if (args[i] == '-d' || args[i] == '--dynamic') {
dynamic_mode = true
} else if (args[i] == '-s' || args[i] == '--static-only') {
static_only = true
} else if (args[i] == '--help' || args[i] == '-h') {
log.console("Usage: cell build [options] [actor]")
log.console("Build a cell binary for the current project.")
log.console("")
log.console("Options:")
log.console(" -d, --dynamic Build libcell_runtime.dylib (dev mode)")
log.console(" -s, --static-only Build fully static (no external scripts)")
log.console(" --target, -t <target> Cross-compile for target platform")
log.console("")
log.console("Arguments:")
log.console(" actor Entry point actor (e.g., 'accio' for accio.ce)")
log.console("")
log.console("Available targets: " + targets.join(', '))
$_.stop()
} else if (args[i] == '--list-targets') {
log.console("Available targets:")
for (var t = 0; t < targets.length; t++) {
log.console(" " + targets[t])
}
$_.stop()
} else if (!args[i].startsWith('-')) {
actor = args[i]
}
}
// Resolve target
if (target && !build.has_target(target)) {
log.console("Available targets: " + targets.join(', '))
throw new Error("Invalid target: " + target)
}
// Find cell package - it should be at ~/work/cell or a dependency
function find_cell_dir() {
// First check if we're in the cell directory itself
if (fd.is_file('source/cell.c') && fd.is_file('source/quickjs.c')) {
log.console("Building from cell source directory")
return '.'
}
// Check for cell as a local path dependency or linked package
var config = shop.load_config()
if (config && config.dependencies && config.dependencies.cell) {
var pkg = config.dependencies.cell
var parsed = shop.parse_package(pkg)
var pkg_dir = shop.get_shop_path() + '/modules/' + parsed.path
if (fd.is_file(pkg_dir + '/source/cell.c')) {
log.console("Using cell from dependency: " + pkg_dir)
return pkg_dir
}
}
// Check Shop Core Dir
var core_dir = shop.get_core_dir()
if (fd.is_file(core_dir + '/source/cell.c')) {
log.console("Using cell from core: " + core_dir)
return core_dir
}
// Fallback: try ~/work/cell
var home_cell = os.getenv('HOME') + '/work/cell'
if (fd.is_file(home_cell + '/source/cell.c')) {
log.console("Using cell from: " + home_cell)
return home_cell
}
return null
}
var cell_dir = find_cell_dir()
if (!cell_dir) {
log.error("Could not find cell source. Add cell as a dependency or run from cell directory.")
$_.stop()
}
// Collect all C files from cell
var source_files = build.list_files(cell_dir + '/source')
var script_files = build.list_files(cell_dir + '/scripts')
// Prefix with directory
var all_files = []
for (var i = 0; i < source_files.length; i++) {
all_files.push('source/' + source_files[i])
}
for (var i = 0; i < script_files.length; i++) {
all_files.push('scripts/' + script_files[i])
}
// Select C files for target
var c_files = build.select_c_files(all_files, target)
// For dynamic mode, exclude main.c
if (dynamic_mode) {
c_files = c_files.filter(function(f) {
return f.indexOf('main.c') < 0 && f.indexOf('main_') < 0
})
log.console("Dynamic mode: excluding main.c")
}
log.console("Found " + text(c_files.length) + " C files to compile")
// Get build directory
var build_dir = dynamic_mode ? shop.get_shop_path() + '/build/dynamic' : build.get_build_dir(target)
build.ensure_dir(build_dir)
// Load cell config for platform-specific flags
var cell_config = build.load_config(cell_dir)
var target_system = build.get_target_system(target)
var platform = target_system || os.platform()
var cflags = build.get_flags(cell_config, platform, 'CFLAGS')
var ldflags = build.get_flags(cell_config, platform, 'LDFLAGS')
// For dynamic builds, add -fPIC
if (dynamic_mode) {
cflags = '-fPIC ' + cflags
}
// Compile options
var compile_options = {
target: target,
cflags: cflags,
includes: [cell_dir, cell_dir + '/source'], // Add cell root and source for includes
defines: {},
module_dir: cell_dir // Ensure we run compilation from cell dir context
}
// Add target-specific defines
if (target == 'playdate') {
compile_options.defines.TARGET_PLAYDATE = true
}
// Compile all C files
var objects = []
for (var i = 0; i < c_files.length; i++) {
var src = c_files[i] // Relative to module_dir because we set it above?
// No, c_files list was created with full paths (or relative to cell_dir?)
// build.list_files returns relative paths if used with prefix.
// We collected them as 'source/file.c'.
// But compile_file expects src_path. If we use module_dir, src_path should be relative to it.
// Adjusted:
var src_rel = c_files[i]
var obj_base = shop.get_global_build_dir() + '/cell' // Build cell into its own area?
// Wait, existing logic was:
// var build_dir = dynamic_mode ? shop.get_shop_path() + '/build/dynamic' : build.get_build_dir(target)
// Let's stick to existing obj path logic but ensure src is handled right.
// construct object path
var obj = build_dir + (dynamic_mode ? '/' : '/static/') + src_rel + '.o' // Separate static build
// The 'src' var in loop was: var src = cell_dir + '/' + c_files[i]
// But if we pass module_dir = cell_dir, we should pass src relative to it?
// build.cm says: if (src_path.startsWith(module_dir + '/')) src_file = src_path.substring...
// So we can pass absolute path, it should handle it.
var src_abs = cell_dir + '/' + src_rel
// Check if recompilation needed
var needs_compile = true
if (fd.is_file(obj)) {
var src_stat = fd.stat(src_abs)
var obj_stat = fd.stat(obj)
if (src_stat && obj_stat && src_stat.mtime <= obj_stat.mtime) {
needs_compile = false
}
}
if (needs_compile) {
// Ensure specific file includes if needed?
// The global includes should cover it.
var result = build.compile_file(src_abs, obj, compile_options)
if (!result) {
log.error("Build failed")
$_.stop()
}
}
objects.push(obj)
}
// For static builds, also compile package C files
if (!dynamic_mode) {
var packages = shop.list_packages()
for (var p = 0; p < packages.length; p++) {
var pkg = packages[p]
var parsed = shop.parse_package(pkg)
var pkg_dir = shop.get_shop_path() + '/modules/' + parsed.path
if (!fd.is_dir(pkg_dir)) continue
var pkg_files = build.list_files(pkg_dir)
var pkg_c_files = build.select_c_files(pkg_files, target)
var pkg_config = build.load_config(pkg_dir)
var pkg_ldflags = build.get_flags(pkg_config, platform, 'LDFLAGS')
if (pkg_ldflags) {
if (ldflags != '') ldflags += ' '
ldflags += pkg_ldflags
}
if (pkg_c_files.length > 0) {
log.console("Compiling " + text(pkg_c_files.length) + " C files from " + parsed.path)
var pkg_cflags = build.get_flags(pkg_config, platform, 'CFLAGS')
// Create symbol prefix for package
var use_prefix = 'js_' + parsed.path.replace(/\//g, '_').replace(/\./g, '_').replace(/-/g, '_') + '_'
for (var f = 0; f < pkg_c_files.length; f++) {
var src = pkg_dir + '/' + pkg_c_files[f]
// Use shop to determine build directory for this package, instead of manual concat
var pkg_build_base = shop.get_build_dir(parsed.path)
var obj = pkg_build_base + '/' + pkg_c_files[f] + '.o'
var safe_name = pkg_c_files[f].substring(0, pkg_c_files[f].lastIndexOf('.')).replace(/\//g, '_').replace(/-/g, '_')
var use_name = use_prefix + safe_name + '_use'
var pkg_options = {
target: target,
cflags: pkg_cflags,
includes: [pkg_dir],
defines: { CELL_USE_NAME: use_name },
module_dir: pkg_dir
}
var needs_compile = true
if (fd.is_file(obj)) {
var src_stat = fd.stat(src)
var obj_stat = fd.stat(obj)
if (src_stat && obj_stat && src_stat.mtime <= obj_stat.mtime) {
needs_compile = false
}
}
if (needs_compile) {
var result = build.compile_file(src, obj, pkg_options)
if (!result) {
log.error("Build failed")
$_.stop()
}
}
objects.push(obj)
}
}
}
// Collect C files from local project (only if not building from cell source directory)
if (cell_dir != '.') {
var local_config = shop.load_config() || {}
var local_ldflags = build.get_flags(local_config, platform, 'LDFLAGS')
if (local_ldflags) {
if (ldflags != '') ldflags += ' '
ldflags += local_ldflags
}
var local_files = build.list_files('.')
var local_c_files = build.select_c_files(local_files, target)
if (local_c_files.length > 0) {
log.console("Compiling " + text(local_c_files.length) + " local C files")
var local_cflags = build.get_flags(local_config, platform, 'CFLAGS')
for (var f = 0; f < local_c_files.length; f++) {
var src = local_c_files[f]
var obj = build_dir + '/local/' + local_c_files[f] + '.o'
var safe_name = local_c_files[f].substring(0, local_c_files[f].lastIndexOf('.')).replace(/\//g, '_').replace(/-/g, '_')
var use_name = 'js_local_' + safe_name + '_use'
var local_options = {
target: target,
cflags: local_cflags,
includes: ['.'],
defines: { CELL_USE_NAME: use_name }
}
var needs_compile = true
if (fd.is_file(obj)) {
var src_stat = fd.stat(src)
var obj_stat = fd.stat(obj)
if (src_stat && obj_stat && src_stat.mtime <= obj_stat.mtime) {
needs_compile = false
}
}
if (needs_compile) {
var result = build.compile_file(src, obj, local_options)
if (!result) {
log.error("Build failed")
$_.stop()
}
}
objects.push(obj)
}
}
}
}
log.console("Compiled " + text(objects.length) + " object files")
// Link
if (dynamic_mode) {
// Link into shared library
var dylib_ext = build.get_dylib_ext(target)
var lib_name = build_dir + '/libcell_runtime' + dylib_ext
var link_flags = '-shared -fPIC'
if (platform == 'macOS' || platform == 'darwin') {
link_flags += ' -framework CoreFoundation -framework CFNetwork'
}
if (ldflags) link_flags += ' ' + ldflags
var objs_str = objects.join(' ')
var cmd = 'cc ' + link_flags + ' ' + objs_str + ' -o ' + lib_name
log.console("Linking " + lib_name)
var ret = os.system(cmd)
if (ret != 0) {
log.error("Linking failed")
$_.stop()
}
log.console("")
log.console("Build complete: " + lib_name)
log.console("")
log.console("To use: copy to /opt/homebrew/lib/ or set DYLD_LIBRARY_PATH")
} else {
// Link into executable
var exe_ext = build.get_exe_ext(target)
var exe_name = build_dir + '/cell' + exe_ext
var link_options = {
target: target,
ldflags: ldflags,
libs: []
}
// Add platform-specific libraries
if (!target || platform == 'macOS' || platform == 'darwin') {
link_options.ldflags += ' -framework CoreFoundation -framework CFNetwork'
} else if (target == 'windows') {
link_options.libs.push('ws2_32')
link_options.libs.push('winmm')
}
var result = build.link_executable(objects, exe_name, link_options)
if (!result) {
log.error("Linking failed")
$_.stop()
}
// Create the pack (core.qop + project scripts)
var pack_path = build_dir + '/pack.qop'
// Collect scripts to pack
var pack_files = []
// Always include core scripts from cell
var core_scripts = fd.readdir(cell_dir + '/scripts')
for (var i = 0; i < core_scripts.length; i++) {
var f = core_scripts[i]
if (f.endsWith('.cm') || f.endsWith('.ce')) {
pack_files.push({ path: f, source: cell_dir + '/scripts/' + f })
}
}
// If building a project (not cell itself), include project scripts
if (cell_dir != '.') {
// Include local scripts
var local_scripts = shop.list_modules()
for (var i = 0; i < local_scripts.length; i++) {
pack_files.push({ path: local_scripts[i], source: local_scripts[i] })
}
// Include package scripts
var packages = shop.list_packages()
for (var p = 0; p < packages.length; p++) {
var pkg = packages[p]
var parsed = shop.parse_package(pkg)
var pkg_dir = shop.get_shop_path() + '/modules/' + parsed.path
var pkg_scripts = shop.list_modules(parsed.path)
for (var i = 0; i < pkg_scripts.length; i++) {
var pack_name = parsed.path + '/' + pkg_scripts[i]
pack_files.push({ path: pack_name, source: pkg_dir + '/' + pkg_scripts[i] })
}
}
}
// Create the pack
log.console("Creating pack with " + text(pack_files.length) + " scripts")
var writer = qop.write(pack_path)
// Add entry point configuration if actor is specified
if (actor) {
var entry_config = {
entry: actor,
static_only: static_only
}
writer.add_file('__entry__.json', utf8.encode(json.encode(entry_config)))
}
for (var i = 0; i < pack_files.length; i++) {
var pf = pack_files[i]
var data = fd.slurp(pf.source)
if (data) {
writer.add_file(pf.path, data)
}
}
writer.finalize()
// Append pack to executable
var exe_data = fd.slurp(exe_name)
var pack_data = fd.slurp(pack_path)
fd.slurpwrite(exe_name, exe_data)
// Append pack data
var fh = fd.open(exe_name, 'a')
fd.write(fh, pack_data)
fd.close(fh)
// Determine final output name
var final_name
if (actor) {
// Named after the actor
var actor_base = actor
if (actor_base.endsWith('.ce')) actor_base = actor_base.substring(0, actor_base.length - 3)
if (actor_base.indexOf('/') >= 0) actor_base = actor_base.substring(actor_base.lastIndexOf('/') + 1)
final_name = actor_base + exe_ext
} else if (cell_dir != '.') {
// Named after the project directory
var cwd = fd.cwd()
var project_name = cwd.substring(cwd.lastIndexOf('/') + 1)
final_name = project_name + exe_ext
} else {
final_name = 'cell' + exe_ext
}
// Copy to project root
os.system('cp ' + exe_name + ' ' + final_name)
log.console("")
log.console("Build complete: " + final_name)
if (actor) {
log.console("Entry point: " + actor + ".ce")
} else {
log.console("Note: Run with an actor argument, e.g.: ./" + final_name + " main.ce")
}
}
$_.stop()