Files
cell/package.cm
2026-02-20 13:39:26 -06:00

399 lines
10 KiB
Plaintext

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 = {}
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 {}
}
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)
}
// Recursively find all cell packages (dirs with cell.toml) under a directory
package.find_packages = function(dir) {
var found = []
var list = fd.readdir(dir)
if (!list) return found
if (fd.is_file(dir + '/cell.toml'))
push(found, dir)
arrfor(list, function(item) {
if (item == '.' || item == '..' || item == '.cell' || item == '.git') return
var full = dir + '/' + item
var st = fd.stat(full)
var sub = null
if (st && st.isDirectory) {
sub = package.find_packages(full)
arrfor(sub, function(p) {
push(found, p)
})
}
})
return found
}
package.list_files = function(pkg) {
var dir = get_path(pkg)
if (!fd.is_dir(dir)) return []
return fd.globfs(["**/*", "!.*"], dir)
}
package.list_modules = function(name) {
var files = package.list_files(name)
var modules = []
var i = 0
var stem = null
for (i = 0; i < length(files); i++) {
if (ends_with(files[i], '.cm')) {
push(modules, text(files[i], 0, -3))
}
}
var c_files = package.get_c_files(name, null, true)
for (i = 0; i < length(c_files); i++) {
stem = ends_with(c_files[i], '.cpp') ? text(c_files[i], 0, -4) : text(c_files[i], 0, -2)
if (find(modules, function(m) { return m == stem }) == null)
push(modules, stem)
}
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 comp = config.compilation
var flags = []
var base = null
var target_flags = null
if (!comp) return flags
// Base flags
if (comp[flag_type]) {
base = comp[flag_type]
flags = array(flags, filter(array(base, /\s+/), function(f) { return length(f) > 0 }))
}
// Target-specific flags
if (target && comp[target] && comp[target][flag_type]) {
target_flags = comp[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)
result = filter(result, function(f) {
return !starts_with(f, 'src/')
})
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