diff --git a/build.cm b/build.cm index 73c29df2..386b4306 100644 --- a/build.cm +++ b/build.cm @@ -26,9 +26,10 @@ function get_local_dir() { } // Replace sigils in a string -// Supports: $LOCAL -> .cell/local, $PACKAGE -> package dir (if provided) +// Supports: $LOCAL -> absolute path to .cell/local, $PACKAGE -> package dir (if provided) function replace_sigils(str, pkg_dir) { - var r = replace(str, '$LOCAL', get_local_dir()) + var local = fd.realpath('.') + '/' + get_local_dir() + var r = replace(str, '$LOCAL', local) if (pkg_dir) r = replace(r, '$PACKAGE', pkg_dir) return r } @@ -138,6 +139,11 @@ Build.compile_file = function(pkg, file, target, opts) { push(cmd_parts, '-DCELL_USE_NAME=' + sym_name) push(cmd_parts, '-I"' + pkg_dir + '"') + // Auto-discover include/ directory + if (fd.is_dir(pkg_dir + '/include')) { + push(cmd_parts, '-I"' + pkg_dir + '/include"') + } + // External packages need core's source dir for cell.h, quickjs.h, blob.h if (pkg != 'core') { core_dir = shop.get_package_dir('core') @@ -165,7 +171,7 @@ Build.compile_file = function(pkg, file, target, opts) { 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) @@ -180,13 +186,32 @@ Build.compile_file = function(pkg, file, target, opts) { return obj_path } - // Compile - var full_cmd = cmd_str + ' -o "' + obj_path + '"' + // Compile — capture stderr to detect missing-header vs real errors + var err_path = '/tmp/cell_build_err_' + hash + '.log' + var full_cmd = cmd_str + ' -o "' + obj_path + '" 2>"' + err_path + '"' + var err_text = null + var missing = null + var err_lines = null + var first_err = null log.console('Compiling ' + file) var ret = os.system(full_cmd) if (ret != 0) { - print('Compilation failed: ' + file) - print('Command: ' + full_cmd) + if (fd.is_file(err_path)) { + err_text = text(fd.slurp(err_path)) + } + if (err_text) { + missing = search(err_text, /fatal error: [''].*[''] file not found/) + if (missing == null) missing = search(err_text, /fatal error: .*: No such file or directory/) + } + if (missing != null) { + err_lines = array(err_text, "\n") + first_err = length(err_lines) > 0 ? err_lines[0] : err_text + print(file + ': ' + first_err + ' (SDK not installed?)') + } else { + print('Compilation failed: ' + file) + if (err_text) print(err_text) + else print('Command: ' + full_cmd) + } return null } @@ -353,8 +378,6 @@ Build.build_dynamic = function(pkg, target, buildtype) { var pkg_dir = shop.get_package_dir(pkg) var cached_cflags = replace_sigils_array(pkg_tools.get_flags(pkg, 'CFLAGS', _target), pkg_dir) - log.console(`CFLAGS ${pkg}: ${text(cached_cflags, '|')}`) - // Compile support sources to cached objects var sources = pkg_tools.get_sources(pkg) var support_objects = [] @@ -785,6 +808,9 @@ Build.build_all_dynamic = function(target, buildtype) { var packages = shop.list_packages() var results = [] var core_mods = null + var total_files = 0 + var total_ok = 0 + var total_fail = 0 // Build core first if (find(packages, function(p) { return p == 'core' }) != null) { @@ -799,6 +825,24 @@ Build.build_all_dynamic = function(target, buildtype) { push(results, {package: pkg, modules: pkg_mods}) }) + // Print build report + print('\n--- Build Report ---') + arrfor(results, function(r) { + var pkg_dir = shop.get_package_dir(r.package) + var c_files = pkg_tools.get_c_files(r.package, _target, true) + var file_count = length(c_files) + var ok_count = length(r.modules) + var fail_count = file_count - ok_count + total_files = total_files + file_count + total_ok = total_ok + ok_count + total_fail = total_fail + fail_count + if (file_count == 0) return + var status = fail_count == 0 ? 'OK' : `${ok_count}/${file_count}` + print(` ${r.package}: ${status}`) + }) + print(`Total: ${total_ok}/${total_files} compiled, ${total_fail} failed`) + print('--------------------\n') + return results } diff --git a/docs/c-modules.md b/docs/c-modules.md index 17dd9071..98b4b052 100644 --- a/docs/c-modules.md +++ b/docs/c-modules.md @@ -255,6 +255,8 @@ If your package is at `/path/to/mypkg`, this becomes `-I/path/to/mypkg/sdk/publi Absolute paths are passed through unchanged. +The build system also auto-discovers `include/` directories — if your package has an `include/` directory, it is automatically added to the include path. No need to add `-I$PACKAGE/include` in cell.toml. + ### Library paths Relative `-L` paths work the same way: @@ -285,9 +287,13 @@ Available targets: `macos_arm64`, `macos_x86_64`, `linux`, `linux_arm64`, `windo ### Sigils -Use `$LOCAL` in flags to refer to the `.cell/local` directory (for prebuilt libraries): +Use sigils in flags to refer to standard directories: + +- `$LOCAL` — absolute path to `.cell/local` (for prebuilt libraries) +- `$PACKAGE` — absolute path to the package root ```toml +CFLAGS = "-I$PACKAGE/vendor/include" LDFLAGS = "-L$LOCAL -lmyprebuilt" ``` @@ -495,3 +501,32 @@ static int module_state = 0; ``` This prevents symbol conflicts between packages. + +## Troubleshooting + +### Missing header / SDK not installed + +If a package wraps a third-party SDK that isn't installed on your system, the build will show: + +``` +module.c: fatal error: 'sdk/header.h' file not found (SDK not installed?) +``` + +Install the required SDK or skip that package. These warnings are harmless — other packages continue building normally. + +### CFLAGS not applied + +If your `cell.toml` has a `[compilation]` section but flags aren't being picked up, check: + +1. The TOML syntax is valid (strings must be quoted) +2. The section header is exactly `[compilation]` (not `[compile]` etc.) +3. Target-specific sections use valid target names: `macos_arm64`, `macos_x86_64`, `linux`, `linux_arm64`, `windows` + +### API changes from older versions + +If C modules fail with errors about function signatures: + +- `JS_IsArray` takes one argument (the value), not two — remove the context argument +- Use `JS_GetPropertyNumber` / `JS_SetPropertyNumber` instead of `JS_GetPropertyUint32` / `JS_SetPropertyUint32` +- Use `JS_NewString` instead of `JS_NewAtomString` +- There is no `undefined` — use `JS_IsNull` and `JS_NULL` only diff --git a/package.cm b/package.cm index b7c4641b..c83da5a3 100644 --- a/package.cm +++ b/package.cm @@ -41,6 +41,64 @@ function get_path(name) } var config_cache = {} +var compilation_cache = {} + +// Fallback parser for [compilation] sections when toml.decode() is unreliable. +// Stores results as flat keys in compilation_cache: "key|CFLAGS", "key|target|CFLAGS" +function parse_compilation_into_cache(content, cache_key) { + var lines = array(content, "\n") + var current_section = null + var i = 0 + var line = null + var trimmed = null + var eq_pos = null + var key = null + var val_part = null + var val = null + var sub = null + var flat_key = null + for (i = 0; i < length(lines); i++) { + line = lines[i] + trimmed = trim(line) + if (length(trimmed) == 0 || starts_with(trimmed, '#')) continue + + // Detect section headers + if (starts_with(trimmed, '[compilation.') && ends_with(trimmed, ']')) { + sub = text(trimmed, 13, -1) + current_section = sub + continue + } + if (trimmed == '[compilation]') { + current_section = '_base_' + continue + } + if (starts_with(trimmed, '[')) { + current_section = null + continue + } + + // Parse KEY = "VALUE" in a compilation section + if (current_section == null) continue + eq_pos = search(trimmed, '=') + if (eq_pos == null) continue + key = trim(text(trimmed, 0, eq_pos)) + val_part = trim(text(trimmed, eq_pos + 1)) + // Strip surrounding quotes + if (starts_with(val_part, '"') && ends_with(val_part, '"')) { + val = text(val_part, 1, -1) + } else { + val = val_part + } + if (current_section == '_base_') { + flat_key = cache_key + '|' + key + } else { + flat_key = cache_key + '|' + current_section + '|' + key + } + compilation_cache[flat_key] = val + } + // Mark that we parsed this package + compilation_cache[cache_key] = true +} package.load_config = function(name) { @@ -63,25 +121,14 @@ package.load_config = function(name) return {} } - // Validate: if content has [compilation] but decode result doesn't, retry - var has_compilation = search(content, '[compilation') != null - var retry = 0 - var cf = null - if (has_compilation && !result.compilation) { - print(`TOML decode missing compilation for ${config_path}, retrying`) - while (retry < 3 && (!result || !result.compilation)) { - result = toml.decode(content) - retry = retry + 1 - } - if (!result) return {} - } - - if (has_compilation && result.compilation) { - cf = result.compilation.CFLAGS - if (cf == null && search(content, 'CFLAGS') != null) { - print(`TOML has CFLAGS text but decode missing it for ${config_path}`) - print(`compilation keys: ${text(array(result.compilation), ',')}`) - } + // If the raw TOML text has [compilation] sections, always use + // the fallback line parser (the TOML decoder is unreliable for + // nested [compilation.target] sub-tables). + // We store it alongside the result in a separate cache since + // toml.decode returns frozen objects. + var has_compilation = search(content, /\[compilation/) != null + if (has_compilation) { + parse_compilation_into_cache(content, cache_key) } config_cache[cache_key] = result @@ -292,22 +339,35 @@ package.list_programs = function(name) { // Returns an array of flag strings package.get_flags = function(name, flag_type, target) { var config = package.load_config(name) + var cache_key = name || '_project_' var flags = [] - - // Base flags var base = null var target_flags = null - if (config.compilation && config.compilation[flag_type]) { - base = config.compilation[flag_type] - flags = array(flags, filter(array(base, /\s+/), function(f) { return length(f) > 0 })) + + if (compilation_cache[cache_key]) { + // Use flat cache: keys are "cache_key|FLAG_TYPE" and "cache_key|target|FLAG_TYPE" + base = compilation_cache[cache_key + '|' + flag_type] + if (base) { + flags = array(flags, filter(array(base, /\s+/), function(f) { return length(f) > 0 })) + } + if (target) { + target_flags = compilation_cache[cache_key + '|' + target + '|' + flag_type] + if (target_flags) { + flags = array(flags, filter(array(target_flags, /\s+/), function(f) { return length(f) > 0 })) + } + } + } else if (config.compilation) { + // Fall back to toml.decode() result + if (config.compilation[flag_type]) { + base = config.compilation[flag_type] + flags = array(flags, filter(array(base, /\s+/), function(f) { return length(f) > 0 })) + } + if (target && config.compilation[target] && config.compilation[target][flag_type]) { + target_flags = config.compilation[target][flag_type] + flags = array(flags, filter(array(target_flags, /\s+/), function(f) { return length(f) > 0 })) + } } - // Target-specific flags - if (target && config.compilation && config.compilation[target] && config.compilation[target][flag_type]) { - target_flags = config.compilation[target][flag_type] - flags = array(flags, filter(array(target_flags, /\s+/), function(f) { return length(f) > 0 })) - } - return flags }