var package = {} var fd = use('fd') var toml = use('toml') var runtime = use('runtime') var link = use('link') var global_shop_path = runtime.shop_path // Convert package name to a safe directory name // For absolute paths (local packages), replace / with _ // For remote packages, keep slashes as they use nested directories function safe_package_path(pkg) { if (!pkg) return pkg if (starts_with(pkg, '/')) return replace(replace(pkg, '/', '_'), '@', '_') return replace(pkg, '@', '_') } function get_path(name) { // If name is null, return the current project directory if (!name) return fd.realpath('.') // If name is already an absolute path, use it directly if (starts_with(name, '/')) return name // Check if this package is linked - if so, use the link target directly // This avoids symlink-related issues with file reading var link_target = link.get_target(name) if (link_target) { // If link target is a local path, use it directly if (starts_with(link_target, '/')) return link_target // Otherwise it's another package name, resolve that return global_shop_path + '/packages/' + replace(replace(link_target, '/', '_'), '@', '_') } // Remote packages use nested directories, so don't transform slashes return global_shop_path + '/packages/' + replace(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) { var cache_key = name || '_project_' if (config_cache[cache_key]) return config_cache[cache_key] var config_path = get_path(name) + '/cell.toml' if (!fd.is_file(config_path)) { print(`${config_path} does not exist`); disrupt } var content = text(fd.slurp(config_path)) if (!content || length(trim(content)) == 0) return {} var result = toml.decode(content) if (!result) { print(`TOML decode returned null for ${config_path}`) return {} } // 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 return result } package.save_config = function(name, config) { var config_path = get_path(name) + '/cell.toml' fd.slurpwrite(config_path, stone(blob(toml.encode(config)))) } package.dependencies = function(name) { return package.load_config(name).dependencies } package.find_alias = function(name, locator) { var config = package.load_config(name) if (!config.dependencies) return null var found = null arrfor(array(config.dependencies), function(alias) { if (config.dependencies[alias] == locator) found = alias }) return found } package.alias_to_package = function(name, alias) { var config = package.load_config(name) if (!config.dependencies) return null return config.dependencies[alias] } // alias is optional package.add_dependency = function(name, locator, alias) { var _alias = alias == null ? locator : alias var config = package.load_config(name) if (!config.dependencies) config.dependencies = {} config.dependencies[_alias] = locator package.save_config(name, config) } // locator can be a locator or alias package.remove_dependency = function(name, locator) { var config = package.load_config(name) if (!config.dependencies) return var alias = null if (config.dependencies[locator]) delete config.dependencies[locator] else { alias = package.find_alias(name, locator) if (alias) delete config.dependencies[alias] } package.save_config(name, config) } package.find_package_dir = function(file) { var absolute = fd.realpath(file) var dir = absolute if (fd.is_file(dir)) dir = fd.dirname(dir) var toml_path = null while (dir && length(dir) > 0) { toml_path = dir + '/cell.toml' if (fd.is_file(toml_path)) { return dir } dir = fd.dirname(dir) } return null } // For a given package, // checks for an alias in path, and returns // { package, path } // so package + path is the full path // Returns null if no alias is found for the given path package.split_alias = function(name, path) { if (!path || length(path) == 0) { return null } var parts = array(path, '/') var first_part = parts[0] var _split = function() { var config = package.load_config(name) if (!config) return null var deps = config.dependencies var dep_locator = null var remaining_path = null if (deps && deps[first_part]) { dep_locator = deps[first_part] remaining_path = text(array(parts, 1), '/') return { package: dep_locator, path: remaining_path } } return null } disruption { return null } return _split() } package.gather_dependencies = function(name) { var all_deps = {} var visited = {} function gather_recursive(pkg_name) { if (visited[pkg_name]) return visited[pkg_name] = true var deps = package.dependencies(pkg_name) if (!deps) return arrfor(array(deps), function(alias) { var locator = deps[alias] if (!all_deps[locator]) { all_deps[locator] = true gather_recursive(locator) } }) } gather_recursive(name) return array(all_deps) } package.list_files = function(pkg) { var dir = get_path(pkg) var files = [] var walk = function(current_dir, current_prefix) { var list = fd.readdir(current_dir) if (!list) return var i = 0 var item = null var full_path = null var rel_path = null var st = null for (i = 0; i < length(list); i++) { item = list[i] if (item == '.' || item == '..') continue if (starts_with(item, '.')) continue // Skip build directories in root full_path = current_dir + "/" + item rel_path = current_prefix ? current_prefix + "/" + item : item st = fd.stat(full_path) if (st.isDirectory) { walk(full_path, rel_path) } else { push(files, rel_path) } } } if (fd.is_dir(dir)) { walk(dir, "") } return files } package.list_modules = function(name) { var files = package.list_files(name) var modules = [] var i = 0 for (i = 0; i < length(files); i++) { if (ends_with(files[i], '.cm')) { push(modules, text(files[i], 0, -3)) } } return modules } package.list_programs = function(name) { var files = package.list_files(name) var programs = [] var i = 0 for (i = 0; i < length(files); i++) { if (ends_with(files[i], '.ce')) { push(programs, text(files[i], 0, -3)) } } return programs } // Get flags from cell.toml for a package // flag_type is 'CFLAGS' or 'LDFLAGS' // target is optional (e.g., 'macos_arm64', 'playdate') // 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 = [] var base = null var target_flags = null 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 })) } } return flags } // Get all C files for a package, handling target-specific variants // Excludes main.c for dynamic builds (when exclude_main is true) // Handles patterns like fd.c vs fd_playdate.c package.get_c_files = function(name, target, exclude_main) { var toolchains = use('toolchains') var known_targets = array(toolchains) var files = package.list_files(name) // Group files by their base name (without target suffix) var groups = {} // base_key -> { generic: file, variants: { target: file } } var i = 0 var file = null var ext = null var base = null var name_part = null var dir_part = null var dir = null var is_variant = null var variant_target = null var generic_name = null var t = 0 var suffix = null var group_key = null for (i = 0; i < length(files); i++) { file = files[i] if (!ends_with(file, '.c') && !ends_with(file, '.cpp')) continue ext = ends_with(file, '.cpp') ? '.cpp' : '.c' base = text(file, 0, -length(ext)) name_part = fd.basename(base) dir_part = fd.dirname(base) dir = (dir_part && dir_part != '.') ? dir_part + '/' : '' // Check for target suffix is_variant = false variant_target = null generic_name = name_part for (t = 0; t < length(known_targets); t++) { suffix = '_' + known_targets[t] if (ends_with(name_part, suffix)) { is_variant = true variant_target = known_targets[t] generic_name = text(name_part, 0, -length(suffix)) break } } group_key = dir + generic_name + ext if (!groups[group_key]) { groups[group_key] = { generic: null, variants: {} } } if (is_variant) { groups[group_key].variants[variant_target] = file } else { groups[group_key].generic = file } } // Select appropriate file from each group var result = [] arrfor(array(groups), function(key) { var group = groups[key] var selected = null var basename = null // Prefer target-specific variant if available if (target && group.variants[target]) { selected = group.variants[target] } else if (group.generic) { selected = group.generic } if (selected) { // Skip main.c if requested if (exclude_main) { basename = fd.basename(selected) if (basename == 'main.c' || starts_with(basename, 'main_')) return } push(result, selected) } }) // Exclude src/ files (support files, not modules) var sources = package.get_sources(name) if (length(sources) > 0) { result = filter(result, function(f) { return find(sources, function(s) { return s == f }) == null }) } return result } // Get support source files: C files in src/ directories (not modules) package.get_sources = function(name) { var files = package.list_files(name) return filter(files, function(f) { return (ends_with(f, '.c') || ends_with(f, '.cpp')) && starts_with(f, 'src/') }) } // Get the absolute path for a package package.get_dir = function(name) { return get_path(name) } return package