1637 lines
45 KiB
Plaintext
1637 lines
45 KiB
Plaintext
var toml = use('toml')
|
|
var json = use('json')
|
|
var fd = use('fd')
|
|
var http = use('http')
|
|
var miniz = use('miniz')
|
|
var time = use('time')
|
|
var js = use('js')
|
|
var crypto = use('crypto')
|
|
var utf8 = use('utf8')
|
|
var blob = use('blob')
|
|
var qop
|
|
var core_qop
|
|
|
|
// a package string is what is used to import a module, like prosperon/sprite
|
|
// in prosperon/sprite, sprite is the module, and prosperon is the package (usually, an alias)
|
|
// a canonical package name relates prosperon to its source, like gitea.pockle.world/john/prosperon
|
|
|
|
var Shop = {}
|
|
|
|
var SCOPE_LOCAL = 0
|
|
var SCOPE_PACKAGE = 1
|
|
var SCOPE_CORE = 2
|
|
|
|
var MOD_EXT = '.cm'
|
|
var ACTOR_EXT = '.ce'
|
|
|
|
var dylib_ext = '.so' // Default extension
|
|
|
|
var os
|
|
var use_cache
|
|
var platform
|
|
var $_
|
|
Shop.set_os = function(o, $guy)
|
|
{
|
|
os = o
|
|
$_ = $guy
|
|
qop = os.load_internal('js_qop_use')
|
|
core_qop = os.core_qop
|
|
use_cache = os.use_cache
|
|
|
|
platform = os.platform()
|
|
if (platform == 'macOS') dylib_ext = '.dylib'
|
|
else if (platform == 'Windows') dylib_ext = '.dll'
|
|
}
|
|
|
|
var config = null
|
|
|
|
var shop_path = '.cell/cell.toml'
|
|
|
|
var open_dl = {}
|
|
|
|
function get_import_package(name) {
|
|
var parts = name.split('/')
|
|
if (parts.length > 1)
|
|
return parts[0]
|
|
|
|
return null
|
|
}
|
|
|
|
Shop.file_info = function(file) {
|
|
var info = {
|
|
path: file,
|
|
is_module: false,
|
|
is_actor: false,
|
|
package: null,
|
|
name: null
|
|
}
|
|
|
|
if (file.endsWith(MOD_EXT)) {
|
|
info.is_module = true
|
|
} else if (file.endsWith(ACTOR_EXT)) {
|
|
info.is_actor = true
|
|
}
|
|
|
|
// Strip extension for name
|
|
var name_without_ext = file
|
|
if (info.is_module) {
|
|
name_without_ext = file.substring(0, file.length - MOD_EXT.length)
|
|
} else if (info.is_actor) {
|
|
name_without_ext = file.substring(0, file.length - ACTOR_EXT.length)
|
|
}
|
|
|
|
// Check if file is in a package
|
|
if (file.startsWith('.cell/modules/')) {
|
|
var rest = file.substring('.cell/modules/'.length)
|
|
|
|
// Get all packages and find which one matches this path
|
|
var packages = Shop.list_packages()
|
|
var matched_pkg = null
|
|
var matched_path = null
|
|
|
|
for (var i = 0; i < packages.length; i++) {
|
|
var pkg = packages[i]
|
|
var parsed = Shop.parse_package(pkg)
|
|
var pkg_path = parsed.path
|
|
|
|
if (rest.startsWith(pkg_path + '/')) {
|
|
// Found matching package - use longest match
|
|
if (!matched_pkg || pkg_path.length > matched_path.length) {
|
|
matched_pkg = pkg
|
|
matched_path = pkg_path
|
|
}
|
|
}
|
|
}
|
|
|
|
if (matched_path) {
|
|
info.package = matched_path
|
|
var import_part = rest.substring(matched_path.length + 1)
|
|
// Strip extension from import name
|
|
if (info.is_module && import_part.endsWith(MOD_EXT)) {
|
|
import_part = import_part.substring(0, import_part.length - MOD_EXT.length)
|
|
} else if (info.is_actor && import_part.endsWith(ACTOR_EXT)) {
|
|
import_part = import_part.substring(0, import_part.length - ACTOR_EXT.length)
|
|
}
|
|
info.name = import_part
|
|
} else {
|
|
// Fallback: use first path component as package
|
|
var slash_idx = rest.indexOf('/')
|
|
if (slash_idx > 0) {
|
|
info.package = rest.substring(0, slash_idx)
|
|
var import_part = rest.substring(slash_idx + 1)
|
|
if (info.is_module && import_part.endsWith(MOD_EXT)) {
|
|
import_part = import_part.substring(0, import_part.length - MOD_EXT.length)
|
|
} else if (info.is_actor && import_part.endsWith(ACTOR_EXT)) {
|
|
import_part = import_part.substring(0, import_part.length - ACTOR_EXT.length)
|
|
}
|
|
info.name = import_part
|
|
}
|
|
}
|
|
} else {
|
|
info.package = 'local'
|
|
info.name = name_without_ext
|
|
}
|
|
|
|
return info
|
|
}
|
|
|
|
function get_import_name(path)
|
|
{
|
|
var parts = path.split('/')
|
|
if (parts.length < 2) return null
|
|
return parts.slice(1).join('/')
|
|
}
|
|
|
|
// given a path, get a full package import
|
|
// ie, 'prosperon/sprite' would return 'gitea.pockle.world/john/prosperon/sprite'
|
|
// if prosperon were a dependency
|
|
function get_path_in_package(path, ctx)
|
|
{
|
|
var pkg = get_import_package(path)
|
|
var mod_name = get_import_name(path)
|
|
if (!pkg) return null
|
|
|
|
var canon_pkg = get_canonical_package(pkg, ctx)
|
|
return canon_pkg + "/" + mod_name
|
|
}
|
|
|
|
function get_normalized_package(path, ctx)
|
|
{
|
|
var pkg = get_import_package(path)
|
|
if (!pkg) return null
|
|
return get_canonical_package(pkg, ctx)
|
|
}
|
|
|
|
// taking the package into account, find the canonical name
|
|
function get_canonical_package(mod, ctx) {
|
|
var cfg = Shop.load_config(ctx)
|
|
|
|
if (!cfg || !cfg.dependencies)
|
|
return null
|
|
|
|
var pkg = cfg.dependencies[mod]
|
|
if (!pkg)
|
|
return null
|
|
|
|
var parsed = Shop.parse_package(pkg)
|
|
if (!parsed)
|
|
return null
|
|
|
|
return parsed.path
|
|
}
|
|
|
|
function get_import_dl(name) {
|
|
var pkg = get_import_package(name)
|
|
if (!pkg) return null
|
|
if (open_dl[pkg]) return open_dl[pkg]
|
|
var dlpath = `.cell/modules/${pkg}/${pkg}${dylib_ext}`
|
|
var dl = os.dylib_open(dlpath)
|
|
if (dl) {
|
|
open_dl[pkg] = dl
|
|
return dl
|
|
}
|
|
return null
|
|
}
|
|
|
|
Shop.get_c_symbol = function get_c_symbol(name) {
|
|
var dl = get_import_dl(name)
|
|
var symname = `js_${name.replace('/', '_')}_use`
|
|
|
|
if (dl)
|
|
return os.dylib_symbol(dl, symname)
|
|
else
|
|
return os.load_internal(symname)
|
|
}
|
|
|
|
function ensure_dir(path) {
|
|
if (fd.stat(path).isDirectory) return true
|
|
|
|
var parts = path.split('/')
|
|
var 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
|
|
}
|
|
|
|
Shop.load_config = function(module) {
|
|
var content
|
|
if (!module) {
|
|
if (!fd.is_file(shop_path))
|
|
return null
|
|
content = fd.slurp(shop_path)
|
|
} else {
|
|
var module_path = `.cell/modules/${module}/.cell/cell.toml`
|
|
if (!fd.stat(module_path).isFile)
|
|
return null
|
|
|
|
content = fd.slurp(module_path)
|
|
}
|
|
|
|
if (!(content instanceof blob)) {
|
|
log.console(`critical error`)
|
|
for (var k in content)
|
|
log.console(k)
|
|
throw new Error("fucked up bad")
|
|
}
|
|
|
|
if (!content.length) return {}
|
|
var cfg = toml.decode(text(content))
|
|
if (cfg.dependencies) {
|
|
var changed = false
|
|
for (var k in cfg.dependencies) {
|
|
if (cfg.dependencies[k].startsWith('https://')) {
|
|
cfg.dependencies[k] = cfg.dependencies[k].substring(8)
|
|
changed = true
|
|
} else if (cfg.dependencies[k].includes('://')) {
|
|
// If it has another protocol, we should probably strip it too or warn
|
|
// But for now assuming mostly https/http
|
|
var parts = cfg.dependencies[k].split('://')
|
|
if (parts.length == 2) {
|
|
cfg.dependencies[k] = parts[1]
|
|
changed = true
|
|
}
|
|
}
|
|
}
|
|
|
|
if (changed && !module) {
|
|
Shop.save_config(cfg)
|
|
}
|
|
}
|
|
return cfg
|
|
}
|
|
|
|
// Save cell.toml configuration
|
|
Shop.save_config = function(config) {
|
|
fd.slurpwrite(shop_path, utf8.encode(toml.encode(config)));
|
|
}
|
|
|
|
// Load lock.toml configuration
|
|
Shop.load_lock = function() {
|
|
var path = '.cell/lock.toml'
|
|
|
|
if (!fd.is_file(path))
|
|
return {}
|
|
|
|
var content = text(fd.slurp(path))
|
|
if (!content.length) return {}
|
|
|
|
var lock = toml.decode(content)
|
|
var changed = false
|
|
|
|
// Clean lock file entries
|
|
for (var key in lock) {
|
|
var entry = lock[key]
|
|
if (entry && entry.package && entry.package.includes('://')) {
|
|
var parts = entry.package.split('://')
|
|
entry.package = parts[1]
|
|
changed = true
|
|
}
|
|
|
|
// Also clean keys if they are locators/packages with protocols
|
|
if (key.includes('://')) {
|
|
var parts = key.split('://')
|
|
var new_key = parts[1]
|
|
lock[new_key] = entry
|
|
delete lock[key]
|
|
changed = true
|
|
}
|
|
}
|
|
|
|
if (changed) {
|
|
Shop.save_lock(lock)
|
|
}
|
|
|
|
return lock
|
|
}
|
|
|
|
// Save lock.toml configuration
|
|
Shop.save_lock = function(lock) {
|
|
fd.slurpwrite('.cell/lock.toml', utf8.encode(toml.encode(lock)));
|
|
}
|
|
|
|
var link_cache = null
|
|
Shop.load_links = function() {
|
|
if (link_cache) return link_cache
|
|
var path = '.cell/link.toml'
|
|
if (!fd.is_file(path)) {
|
|
link_cache = {}
|
|
return link_cache
|
|
}
|
|
|
|
try {
|
|
var content = text(fd.slurp(path))
|
|
var cfg = toml.decode(content)
|
|
link_cache = cfg.links || {}
|
|
} catch (e) {
|
|
log.console("Warning: Failed to load link.toml: " + e)
|
|
link_cache = {}
|
|
}
|
|
return link_cache
|
|
}
|
|
|
|
Shop.save_links = function(links) {
|
|
link_cache = links
|
|
var cfg = { links: links }
|
|
fd.slurpwrite('.cell/link.toml', utf8.encode(toml.encode(cfg)))
|
|
}
|
|
|
|
Shop.add_link = function(canonical, target) {
|
|
var links = Shop.load_links()
|
|
links[canonical] = target
|
|
Shop.save_links(links)
|
|
log.console("Linked " + canonical + " -> " + target)
|
|
return true
|
|
}
|
|
|
|
Shop.remove_link = function(canonical) {
|
|
var links = Shop.load_links()
|
|
if (!links[canonical]) return false
|
|
delete links[canonical]
|
|
Shop.save_links(links)
|
|
log.console("Unlinked " + canonical)
|
|
return true
|
|
}
|
|
|
|
Shop.clear_links = function() {
|
|
Shop.save_links({})
|
|
log.console("Cleared all links")
|
|
return true
|
|
}
|
|
|
|
// Parse module package string (e.g., "git.world/jj/mod@v0.6.3")
|
|
Shop.parse_package = function(pkg) {
|
|
Shop.verify_package_name(pkg)
|
|
var path = pkg
|
|
var version = null
|
|
|
|
// Extract version if present
|
|
if (path.includes('@')) {
|
|
var versionParts = path.split('@')
|
|
path = versionParts[0]
|
|
version = versionParts[1]
|
|
}
|
|
|
|
// Check for links
|
|
var links = Shop.load_links()
|
|
if (links[path]) {
|
|
path = links[path]
|
|
}
|
|
|
|
// Handle absolute paths (local modules)
|
|
// /User/john/mod -> User/john/mod
|
|
if (path.startsWith('/')) {
|
|
path = path.substring(1)
|
|
}
|
|
|
|
// Extract name (last part of path)
|
|
var name = path.split('/').pop()
|
|
|
|
return {
|
|
path,
|
|
name,
|
|
version
|
|
}
|
|
}
|
|
|
|
// Get information about how to resolve a package
|
|
Shop.resolve_package_info = function(pkg) {
|
|
// Check links first
|
|
// We need to check the raw package string against links
|
|
// But we also need to handle the case where pkg is already a local path
|
|
// If pkg is "gitea...", check if it's linked
|
|
|
|
var path = pkg
|
|
if (path.includes('@')) path = path.split('@')[0]
|
|
if (path.startsWith('/')) path = path.substring(1)
|
|
|
|
var links = Shop.load_links()
|
|
if (links[path]) {
|
|
return { type: 'local', path: links[path] }
|
|
}
|
|
|
|
if (pkg.startsWith('/')) {
|
|
return { type: 'local', path: pkg }
|
|
}
|
|
if (pkg.includes('gitea.')) {
|
|
return { type: 'gitea' }
|
|
}
|
|
return { type: 'unknown' }
|
|
}
|
|
|
|
// Verify if a package name is valid and return status
|
|
Shop.verify_package_name = function(pkg) {
|
|
if (!pkg) throw new Error("Empty package name")
|
|
if (pkg == 'local') throw new Error("local is not a valid package name")
|
|
if (pkg == 'core') throw new Error("core is not a valid package name")
|
|
|
|
if (pkg.includes('://'))
|
|
throw new Error(`Invalid package name: ${pkg}; did you mean ${pkg.split('://')[1]}?`)
|
|
}
|
|
|
|
// Convert module package to download URL
|
|
Shop.get_download_url = function(pkg, commit_hash) {
|
|
var info = Shop.resolve_package_info(pkg)
|
|
if (info.type == 'local') return null
|
|
|
|
var parsed = Shop.parse_package(pkg)
|
|
if (!parsed) return null
|
|
|
|
if (parsed.path.includes('gitea.')) {
|
|
var parts = parsed.path.split('/')
|
|
var host = parts[0]
|
|
var user = parts[1]
|
|
var repo = parts[2]
|
|
|
|
if (!commit_hash) {
|
|
log.error("No commit hash available for download URL")
|
|
return null
|
|
}
|
|
|
|
return 'https://' + host + '/' + user + '/' + repo + '/archive/' + commit_hash + '.zip'
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
// Remove a dependency
|
|
Shop.remove_dependency = function(alias) {
|
|
var config = Shop.load_config()
|
|
if (!config) {
|
|
log.error("No cell.toml found")
|
|
return false
|
|
}
|
|
|
|
if (!config.dependencies || !config.dependencies[alias]) {
|
|
return false
|
|
}
|
|
|
|
delete config.dependencies[alias]
|
|
Shop.save_config(config)
|
|
return true
|
|
}
|
|
|
|
// Get the API URL for checking remote git commits
|
|
Shop.get_api_url = function(pkg) {
|
|
var info = Shop.resolve_package_info(pkg)
|
|
if (info.type == 'local') return null
|
|
|
|
var parsed = Shop.parse_package(pkg)
|
|
if (!parsed) return null
|
|
|
|
var parts = parsed.path.split('/')
|
|
// Gitea pattern: gitea.pockle.world/user/repo@branch
|
|
if (parsed.path.includes('gitea.')) {
|
|
var host = parts[0]
|
|
var user = parts[1]
|
|
var repo = parts[2]
|
|
var url = 'https://' + host + '/api/v1/repos/' + user + '/' + repo + '/branches/'
|
|
if (parsed.version) url += parsed.version
|
|
return url
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
// Extract commit hash from API response
|
|
Shop.extract_commit_hash = function(pkg, response) {
|
|
if (!response) return null
|
|
|
|
var data = json.decode(response)
|
|
|
|
if (pkg.includes('gitea.')) {
|
|
// Gitea: response.commit.id
|
|
if (Array.isArray(data))
|
|
data = data[0]
|
|
return data.commit && data.commit.id
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
// Get the module directory for a given alias
|
|
Shop.get_module_dir = function(alias) {
|
|
var config = Shop.load_config()
|
|
if (!config || !config.dependencies || !config.dependencies[alias]) {
|
|
return null
|
|
}
|
|
|
|
var pkg = config.dependencies[alias]
|
|
var parsed = Shop.parse_package(pkg)
|
|
if (!parsed) return null
|
|
|
|
return '.cell/modules/' + parsed.path
|
|
}
|
|
|
|
function lock_package(loc)
|
|
{
|
|
var lock = Shop.load_lock()
|
|
|
|
}
|
|
|
|
Shop.check_cache = function(pkg) {
|
|
var parsed = Shop.parse_package(pkg)
|
|
if (!parsed) return null
|
|
|
|
var cache_path = `.cell/cache/${parsed.path}.zip`
|
|
if (fd.is_file(cache_path)) {
|
|
log.console("Found cached zip: " + cache_path)
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
var open_dls = {}
|
|
|
|
// for script forms, path is the canonical path of the module
|
|
var script_forms = []
|
|
|
|
script_forms['.cm'] = function(path, script, pkg) {
|
|
var pkg_arg = pkg ? `'${pkg}'` : 'null'
|
|
var relative_use_fn = `def use = function(path) { return globalThis.use(path, ${pkg_arg});}`
|
|
var fn = `(function setup_module($_){ ${relative_use_fn}; ${script}})`
|
|
return fn
|
|
}
|
|
|
|
script_forms['.ce'] = function(path, script, pkg) {
|
|
var pkg_arg = pkg ? `'${pkg}'` : 'null'
|
|
var relative_use_fn = `def use = function(path) { return globalThis.use(path, ${pkg_arg});}`
|
|
return `(function start($_, arg) { ${relative_use_fn}; var args = arg; ${script} ; })`
|
|
}
|
|
|
|
// Get flags from config
|
|
function get_flags(config, platform, key) {
|
|
var flags = ''
|
|
if (config.compilation && config.compilation[key]) {
|
|
flags += config.compilation[key]
|
|
}
|
|
if (config.compilation && config.compilation[platform] && config.compilation[platform][key]) {
|
|
if (flags != '') flags += ' '
|
|
flags += config.compilation[platform][key]
|
|
}
|
|
return flags
|
|
}
|
|
|
|
Shop.get_flags = get_flags
|
|
|
|
function get_build_dir(pkg = 'local') {
|
|
return '.cell/build/' + pkg
|
|
}
|
|
|
|
Shop.get_build_dir = get_build_dir
|
|
|
|
function get_rel_path(path, pkg) {
|
|
if (!pkg) return path
|
|
var prefix = '.cell/modules/' + pkg + '/'
|
|
if (path.startsWith(prefix)) {
|
|
return path.substring(prefix.length)
|
|
}
|
|
return path
|
|
}
|
|
|
|
function resolve_mod_fn(path, pkg)
|
|
{
|
|
if (!fd.is_file(path)) throw new Error(`path ${path} is not a file`)
|
|
var rel_path = get_rel_path(path, pkg)
|
|
var build_dir = get_build_dir(pkg)
|
|
var cache_path = build_dir + '/' + rel_path + '.o'
|
|
|
|
if (fd.is_file(cache_path) && fd.stat(path).mtime <= fd.stat(cache_path).mtime) {
|
|
var obj = fd.slurp(cache_path)
|
|
var fn = js.compile_unblob(obj)
|
|
return js.eval_compile(fn)
|
|
}
|
|
|
|
var ext = path.substring(path.lastIndexOf('.'))
|
|
var script_form = script_forms[ext]
|
|
if (!script_form) throw new Error(`No script form for extension ${ext}`)
|
|
|
|
var script = script_form(path, text(fd.slurp(path)), pkg)
|
|
var fn = js.compile(path, script)
|
|
ensure_dir(cache_path.substring(0, cache_path.lastIndexOf('/')))
|
|
fd.slurpwrite(cache_path, js.compile_blob(fn))
|
|
return js.eval_compile(fn)
|
|
}
|
|
|
|
function resolve_locator(path, ext, ctx)
|
|
{
|
|
var local_path
|
|
if (ctx)
|
|
local_path = `.cell/modules/${ctx}/${path}${ext}`
|
|
else
|
|
local_path = path + ext
|
|
|
|
if (fd.is_file(local_path)) {
|
|
var fn = resolve_mod_fn(local_path, ctx)
|
|
return {path: local_path, scope: SCOPE_LOCAL, symbol:fn}
|
|
}
|
|
|
|
var canonical_pkg = get_normalized_package(path, ctx)
|
|
var pkg_path = get_path_in_package(path, ctx)
|
|
var mod_path = `.cell/modules/${pkg_path}${ext}`
|
|
if (fd.is_file(mod_path)) {
|
|
var fn = resolve_mod_fn(mod_path, canonical_pkg)
|
|
return {path: mod_path, scope: SCOPE_PACKAGE, symbol:fn}
|
|
}
|
|
|
|
var core = core_qop.read(path + ext)
|
|
if (core != null) {
|
|
var form = script_forms[ext]
|
|
if (!form) throw new Error(`No script form for extension ${ext}`)
|
|
var script = form(null,text(core))
|
|
var fn = js.compile(path, script)
|
|
return {path, scope: SCOPE_CORE, symbol:js.eval_compile(fn)};
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function c_sym_path(path)
|
|
{
|
|
return path.replace(/\//g, '_').replace(/\\/g, '_').replace(/\./g, '_').replace(/-/g, '_')
|
|
}
|
|
|
|
function resolve_c_symbol(path, package_context)
|
|
{
|
|
var local_path = package_context ? package_context : 'local'
|
|
var local_sym_base = c_sym_path(path)
|
|
var local
|
|
|
|
function symbol_candidates(pkg_path, mod_sym) {
|
|
var variants = []
|
|
var paths = [pkg_path]
|
|
if (!pkg_path.startsWith('/')) paths.push('/' + pkg_path)
|
|
for (var i = 0; i < paths.length; i++) {
|
|
var candidate = `js_${c_sym_path(paths[i])}_${mod_sym}_use`
|
|
if (variants.indexOf(candidate) < 0)
|
|
variants.push(candidate)
|
|
}
|
|
return variants
|
|
}
|
|
|
|
if (!package_context) {
|
|
local = `js_local_${local_sym_base}_use`
|
|
} else {
|
|
local = null // handled via candidates below
|
|
}
|
|
|
|
var build_dir = get_build_dir(package_context)
|
|
var local_dl_name = build_dir + '/cellmod' + dylib_ext
|
|
|
|
if (fd.is_file(local_dl_name)) {
|
|
if (!open_dls[local_dl_name])
|
|
open_dls[local_dl_name] = os.dylib_open(local_dl_name);
|
|
|
|
if (open_dls[local_dl_name]) {
|
|
var locals = package_context ? symbol_candidates(local_path, local_sym_base) : [local]
|
|
for (var i = 0; i < locals.length; i++) {
|
|
var candidate = locals[i]
|
|
if (os.dylib_has_symbol(open_dls[local_dl_name], candidate))
|
|
return {
|
|
symbol: function() { return os.dylib_symbol(open_dls[local_dl_name], candidate); },
|
|
scope: SCOPE_LOCAL,
|
|
path: candidate
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
// Try static linking fallback
|
|
var local_candidates = package_context ? symbol_candidates(local_path, local_sym_base) : [local]
|
|
for (var li = 0; li < local_candidates.length; li++) {
|
|
var lc = local_candidates[li]
|
|
if (os.internal_exists(lc))
|
|
return {
|
|
symbol: function() { return os.load_internal(lc); },
|
|
scope: SCOPE_LOCAL,
|
|
path: lc
|
|
};
|
|
}
|
|
|
|
// If 'path' has a package alias (e.g. 'prosperon/sprite'), try to resolve it
|
|
var pkg_alias = get_import_package(path)
|
|
if (pkg_alias) {
|
|
var canon_pkg = get_normalized_package(path, package_context)
|
|
if (canon_pkg) {
|
|
var build_dir = get_build_dir(canon_pkg)
|
|
var dl_path = build_dir + '/cellmod' + dylib_ext
|
|
var mod_name = get_import_name(path)
|
|
var mod_sym = mod_name.replace(/\//g, '_').replace(/-/g, '_').replace(/\./g, '_')
|
|
var sym_names = symbol_candidates(canon_pkg, mod_sym)
|
|
|
|
if (fd.is_file(dl_path)) {
|
|
if (!open_dls[dl_path]) open_dls[dl_path] = os.dylib_open(dl_path)
|
|
if (open_dls[dl_path]) {
|
|
for (var si = 0; si < sym_names.length; si++) {
|
|
var sym_name = sym_names[si]
|
|
if (os.dylib_has_symbol(open_dls[dl_path], sym_name))
|
|
return {
|
|
symbol: function() { return os.dylib_symbol(open_dls[dl_path], sym_name) },
|
|
scope: SCOPE_PACKAGE,
|
|
package: canon_pkg,
|
|
path: sym_name
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for (var sii = 0; sii < sym_names.length; sii++) {
|
|
var sym_name = sym_names[sii]
|
|
if (os.internal_exists(sym_name))
|
|
return {
|
|
symbol: function() { return os.load_internal(sym_name) },
|
|
scope: SCOPE_PACKAGE,
|
|
package: canon_pkg,
|
|
path: sym_name
|
|
};
|
|
}
|
|
}
|
|
}
|
|
var core_sym = `js_${path.replace(/\//g, '_')}_use`;
|
|
if (os.internal_exists(core_sym))
|
|
return {
|
|
symbol: function() { return os.load_internal(core_sym); },
|
|
scope: SCOPE_CORE,
|
|
path: core_sym
|
|
};
|
|
|
|
return null
|
|
}
|
|
|
|
function resolve_module_info(path, package_context) {
|
|
var c_resolve = resolve_c_symbol(path, package_context) || {scope:999}
|
|
var mod_resolve = resolve_locator(path, '.cm', package_context) || {scope:999}
|
|
var min_scope = Math.min(c_resolve.scope, mod_resolve.scope)
|
|
|
|
if (min_scope == 999)
|
|
return null
|
|
|
|
var resolved_path
|
|
if (mod_resolve.scope != 999) resolved_path = mod_resolve.path
|
|
else resolved_path = c_resolve.path
|
|
|
|
var cache_scope = min_scope == SCOPE_CORE ? 2 : 0
|
|
var cache_key
|
|
if (min_scope == SCOPE_CORE)
|
|
cache_key = `2::${path}`
|
|
else
|
|
cache_key = `${text(cache_scope)}::${resolved_path}`
|
|
|
|
cache_key = cache_key.replace('//', '/')
|
|
|
|
return {
|
|
cache_key: cache_key,
|
|
c_resolve: c_resolve,
|
|
mod_resolve: mod_resolve,
|
|
min_scope: min_scope
|
|
}
|
|
}
|
|
|
|
function get_module_cache_key(path, package_context) {
|
|
var info = resolve_module_info(path, package_context)
|
|
return info ? info.cache_key : null
|
|
}
|
|
|
|
Shop.is_loaded = function(path, package_context) {
|
|
var cache_key = get_module_cache_key(path, package_context)
|
|
return use_cache[cache_key] != null
|
|
}
|
|
|
|
function execute_module(info)
|
|
{
|
|
var c_resolve = info.c_resolve
|
|
var mod_resolve = info.mod_resolve
|
|
var cache_key = info.cache_key
|
|
|
|
var used
|
|
|
|
if (c_resolve.scope < mod_resolve.scope)
|
|
used = c_resolve.symbol(null, $_)
|
|
else if (mod_resolve.scope < c_resolve.scope)
|
|
used = mod_resolve.symbol.call(null, $_)
|
|
else
|
|
used = mod_resolve.symbol.call(c_resolve.symbol(), $_)
|
|
|
|
if (!used)
|
|
throw new Error(`Module ${json.encode(info)} returned null`)
|
|
|
|
return used
|
|
}
|
|
|
|
function get_module(path, package_context) {
|
|
var info = resolve_module_info(path, package_context)
|
|
|
|
if (!info)
|
|
throw new Error(`Module ${path} could not be found in ${package_context}`)
|
|
|
|
return execute_module(info)
|
|
}
|
|
|
|
// first looks in local
|
|
// then in dependencies
|
|
// then in core
|
|
// package_context: optional package context to resolve relative paths within
|
|
Shop.use = function(path, package_context) {
|
|
var info = resolve_module_info(path, package_context)
|
|
if (!info)
|
|
throw new Error(`Module ${path} could not be found in ${package_context}`)
|
|
|
|
if (use_cache[info.cache_key])
|
|
return use_cache[info.cache_key]
|
|
|
|
use_cache[info.cache_key] = execute_module(info)
|
|
return use_cache[info.cache_key]
|
|
}
|
|
|
|
Shop.resolve_locator = resolve_locator
|
|
|
|
// Get cache path for a package and commit
|
|
function get_cache_path(pkg, commit) {
|
|
var parsed = Shop.parse_package(pkg)
|
|
if (!parsed) return null
|
|
|
|
var slug = parsed.path.split('/').join('_')
|
|
return `.cell/cache/${slug}_${commit}.zip`
|
|
}
|
|
|
|
function rm_recursive(path) {
|
|
try {
|
|
fd.rm(path)
|
|
} catch (e) {
|
|
log.error("Failed to remove " + path + ": " + e)
|
|
}
|
|
}
|
|
|
|
function get_all_files(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
|
|
|
|
var full_path = dir + "/" + item
|
|
var rel_path = prefix ? prefix + "/" + item : item
|
|
|
|
var st = fd.stat(full_path)
|
|
if (st.isDirectory) {
|
|
get_all_files(full_path, rel_path, results)
|
|
} else {
|
|
results.push(rel_path)
|
|
}
|
|
}
|
|
return results
|
|
}
|
|
|
|
|
|
// Verify zip contents against target directory
|
|
function verify_zip_contents(zip, target_dir) {
|
|
var count = zip.count()
|
|
var expected_files = {}
|
|
for (var i = 0; i < count; i++) {
|
|
if (zip.is_directory(i)) continue
|
|
var filename = zip.get_filename(i)
|
|
var parts = filename.split('/')
|
|
if (parts.length > 1) {
|
|
parts.shift()
|
|
var rel_path = parts.join('/')
|
|
expected_files[rel_path] = true
|
|
|
|
var full_path = target_dir + '/' + rel_path
|
|
if (!fd.is_file(full_path)) return false
|
|
|
|
var content_zip = zip.slurp(filename)
|
|
var content_disk = fd.slurp(full_path)
|
|
|
|
if (content_zip.length != content_disk.length) return false
|
|
|
|
var hash_zip = text(crypto.blake2(content_zip), 'h')
|
|
var hash_disk = text(crypto.blake2(content_disk), 'h')
|
|
|
|
if (hash_zip != hash_disk) return false
|
|
}
|
|
}
|
|
|
|
// Check for extra files
|
|
var existing_files = get_all_files(target_dir)
|
|
for (var i = 0; i < existing_files.length; i++)
|
|
if (!expected_files[existing_files[i]]) return false
|
|
|
|
return true
|
|
}
|
|
|
|
// High-level: Add a package, install it, and install all transitive dependencies
|
|
// Like `bun add` or `npm install <pkg>`
|
|
Shop.get = function(pkg, alias) {
|
|
if (fd.is_dir(pkg)) {
|
|
log.console("Found directory: " + pkg)
|
|
pkg = fd.realpath(pkg)
|
|
log.console("Resolved to: " + pkg)
|
|
}
|
|
var info = Shop.resolve_package_info(pkg)
|
|
if (info.type == 'unknown') {
|
|
log.error("Could not resolve package: " + pkg)
|
|
return false
|
|
}
|
|
var parsed = Shop.parse_package(pkg)
|
|
if (!alias) alias = parsed.name
|
|
|
|
log.console("Adding dependency: " + alias + " = " + pkg)
|
|
|
|
// Add to config
|
|
var config = Shop.load_config() || { dependencies: {} }
|
|
if (!config.dependencies) config.dependencies = {}
|
|
config.dependencies[alias] = pkg
|
|
Shop.save_config(config)
|
|
return true
|
|
}
|
|
|
|
// Update a specific package
|
|
Shop.update = function(pkg) {
|
|
var config = Shop.load_config()
|
|
var lock = Shop.load_lock()
|
|
var parsed = Shop.parse_package(pkg)
|
|
var info = Shop.resolve_package_info(pkg)
|
|
var target_dir = '.cell/modules/' + parsed.path
|
|
|
|
var result = info.type == 'local'
|
|
? update_local(pkg, info, target_dir)
|
|
: update_remote(pkg, info, target_dir, lock[pkg])
|
|
|
|
if (!result) {
|
|
log.error("Failed to update " + parsed.path)
|
|
return false
|
|
}
|
|
|
|
lock[pkg] = {
|
|
package: pkg,
|
|
commit: result.commit,
|
|
zip_hash: result.zip_hash,
|
|
updated: time.number()
|
|
}
|
|
Shop.save_lock(lock)
|
|
log.console("Updated " + parsed.path + ".")
|
|
return true
|
|
}
|
|
|
|
function update_local(pkg, info, target_dir) {
|
|
if (fd.is_link(target_dir)) {
|
|
if (fd.readlink(target_dir) == info.path)
|
|
return { commit: "local", package: pkg, zip_hash: "local" }
|
|
else
|
|
fd.unlink(target_dir)
|
|
}
|
|
|
|
var parent_dir = target_dir.substring(0, target_dir.lastIndexOf('/'))
|
|
ensure_dir(parent_dir)
|
|
|
|
if (fd.is_dir(target_dir)) fd.rmdir(target_dir)
|
|
|
|
try {
|
|
fd.symlink(info.path, target_dir)
|
|
log.console("Linked " + target_dir + " -> " + info.path)
|
|
return { commit: "local", package: pkg, zip_hash: "local" }
|
|
} catch(e) {
|
|
log.error("Failed to create symlink: " + e)
|
|
return null
|
|
}
|
|
}
|
|
|
|
function update_remote(pkg, info, target_dir, lock_info) {
|
|
var local_hash = lock_info ? lock_info.commit : null
|
|
var remote_hash = fetch_remote_hash(pkg)
|
|
|
|
if (!remote_hash && !local_hash) {
|
|
log.error("Could not resolve commit for " + pkg)
|
|
return null
|
|
}
|
|
|
|
var target_hash = remote_hash || local_hash
|
|
|
|
if (local_hash == target_hash) {
|
|
log.console(pkg + " is already up to date.")
|
|
return lock_info
|
|
}
|
|
|
|
if (local_hash)
|
|
log.console("Updating " + pkg + " " + local_hash.substring(0,8) + " -> " + target_hash.substring(0,8))
|
|
else
|
|
log.console("Installing " + pkg + "...")
|
|
|
|
var zip_blob = get_or_download_zip(pkg, target_hash)
|
|
if (!zip_blob) return null
|
|
|
|
var zip_hash = text(crypto.blake2(zip_blob), 'h')
|
|
install_zip(zip_blob, target_dir)
|
|
|
|
return { commit: target_hash, package: pkg, zip_hash: zip_hash }
|
|
}
|
|
|
|
function fetch_remote_hash(pkg) {
|
|
var api_url = Shop.get_api_url(pkg)
|
|
if (!api_url) return null
|
|
|
|
try {
|
|
var resp = http.fetch(api_url)
|
|
return Shop.extract_commit_hash(pkg, text(resp))
|
|
} catch (e) {
|
|
log.console("Warning: Could not check for updates for " + pkg)
|
|
return null
|
|
}
|
|
}
|
|
|
|
function get_or_download_zip(pkg, commit_hash) {
|
|
var cache_path = get_cache_path(pkg, commit_hash)
|
|
|
|
if (fd.is_file(cache_path)) {
|
|
log.console("Found cached zip: " + cache_path)
|
|
try {
|
|
return fd.slurp(cache_path)
|
|
} catch (e) {
|
|
log.error("Failed to read cache: " + e)
|
|
}
|
|
}
|
|
|
|
var download_url = Shop.get_download_url(pkg, commit_hash)
|
|
if (!download_url) {
|
|
log.error("Could not determine download URL for " + pkg)
|
|
return null
|
|
}
|
|
|
|
log.console("Downloading from " + download_url)
|
|
try {
|
|
var zip_blob = http.fetch(download_url)
|
|
ensure_dir(cache_path.substring(0, cache_path.lastIndexOf('/')))
|
|
fd.slurpwrite(cache_path, zip_blob)
|
|
log.console("Cached to " + cache_path)
|
|
return zip_blob
|
|
} catch (e) {
|
|
log.error(e)
|
|
return null
|
|
}
|
|
}
|
|
|
|
function install_zip(zip_blob, target_dir) {
|
|
var zip = miniz.read(zip_blob)
|
|
if (!zip) throw new Error("Failed to read zip archive")
|
|
|
|
if (fd.is_link(target_dir)) fd.unlink(target_dir)
|
|
|
|
log.console("Syncing to " + target_dir)
|
|
ensure_dir(target_dir)
|
|
|
|
var zip_files = {}
|
|
var count = zip.count()
|
|
|
|
for (var i = 0; i < count; i++) {
|
|
if (zip.is_directory(i)) continue
|
|
var filename = zip.get_filename(i)
|
|
var parts = filename.split('/')
|
|
if (parts.length <= 1) continue
|
|
|
|
parts.shift()
|
|
var rel_path = parts.join('/')
|
|
zip_files[rel_path] = { index: i, filename: filename }
|
|
}
|
|
|
|
var existing_files = fd.is_dir(target_dir) ? get_all_files(target_dir) : []
|
|
|
|
for (var i = 0; i < existing_files.length; i++) {
|
|
var rel_path = existing_files[i]
|
|
if (!zip_files[rel_path]) {
|
|
var full_path = target_dir + '/' + rel_path
|
|
log.console("Removing " + rel_path)
|
|
fd.rm(full_path)
|
|
}
|
|
}
|
|
|
|
for (var rel_path in zip_files) {
|
|
var zip_info = zip_files[rel_path]
|
|
var full_path = target_dir + '/' + rel_path
|
|
var dir_path = full_path.substring(0, full_path.lastIndexOf('/'))
|
|
|
|
var zip_content = zip.slurp(zip_info.filename)
|
|
var needs_write = true
|
|
|
|
if (fd.is_file(full_path)) {
|
|
var disk_content = fd.slurp(full_path)
|
|
if (disk_content.length == zip_content.length && disk_content.length != 0) {
|
|
var hash_zip = text(crypto.blake2(zip_content), 'h')
|
|
var hash_disk = text(crypto.blake2(disk_content), 'h')
|
|
if (hash_zip == hash_disk) {
|
|
needs_write = false
|
|
}
|
|
}
|
|
}
|
|
|
|
if (needs_write) {
|
|
ensure_dir(dir_path)
|
|
log.console("Writing " + rel_path)
|
|
fd.slurpwrite(full_path, zip_content)
|
|
}
|
|
}
|
|
}
|
|
|
|
// High-level: Remove a package and clean up
|
|
// Like `bun remove`
|
|
Shop.remove = function(alias) {
|
|
var config = Shop.load_config()
|
|
if (!config || !config.dependencies || !config.dependencies[alias]) {
|
|
log.error("Dependency not found: " + alias)
|
|
return false
|
|
}
|
|
|
|
var locator = config.dependencies[alias]
|
|
var parsed = Shop.parse_package(locator)
|
|
var target_dir = '.cell/modules/' + parsed.path
|
|
|
|
// Remove from config
|
|
delete config.dependencies[alias]
|
|
Shop.save_config(config)
|
|
|
|
// Remove from lock
|
|
var lock = Shop.load_lock()
|
|
if (lock[locator]) delete lock[locator]
|
|
if (lock[alias]) delete lock[alias] // Cleanup old format
|
|
Shop.save_lock(lock)
|
|
|
|
// Remove directory
|
|
if (fd.is_dir(target_dir)) {
|
|
log.console("Removing " + target_dir)
|
|
try {
|
|
fd.rmdir(target_dir)
|
|
} catch (e) {
|
|
log.error("Failed to remove directory: " + e)
|
|
}
|
|
}
|
|
|
|
log.console("Removed " + alias)
|
|
return true
|
|
}
|
|
|
|
Shop.add_replacement = function(alias, replacement) {
|
|
var config = Shop.load_config()
|
|
if (!config) config = {}
|
|
if (!config.replace) config.replace = {}
|
|
|
|
config.replace[alias] = replacement
|
|
Shop.save_config(config)
|
|
|
|
log.console("Added replacement: " + alias + " = " + replacement)
|
|
return true
|
|
}
|
|
|
|
Shop.remove_replacement = function(alias) {
|
|
var config = Shop.load_config()
|
|
if (!config || !config.replace || !config.replace[alias]) {
|
|
log.error("No replacement found for " + alias)
|
|
return false
|
|
}
|
|
|
|
delete config.replace[alias]
|
|
Shop.save_config(config)
|
|
|
|
log.console("Removed replacement for " + alias)
|
|
log.console("Run 'cell update " + alias + "' to restore the package.")
|
|
return true
|
|
}
|
|
|
|
// Compile a module
|
|
// List all files in a package
|
|
Shop.list_files = function(pkg) {
|
|
var dir
|
|
if (!pkg) dir = '.'
|
|
else dir = '.cell/modules/' + pkg
|
|
|
|
var files = []
|
|
|
|
var walk = function(current_dir, current_prefix) {
|
|
var list = fd.readdir(current_dir)
|
|
if (!list) return
|
|
|
|
for (var i = 0; i < list.length; i++) {
|
|
var item = list[i]
|
|
if (item == '.' || item == '..') continue
|
|
if (item.startsWith('.')) continue
|
|
|
|
// Skip build directories in root
|
|
if (!pkg && (item == 'build' || item == 'build_dbg' || item == 'build_release' || item == 'build_web' || item == 'build_fast')) continue
|
|
if (!pkg && item == 'cell_modules') continue // Just in case
|
|
|
|
var full_path = current_dir + "/" + item
|
|
var rel_path = current_prefix ? current_prefix + "/" + item : item
|
|
|
|
var st = fd.stat(full_path)
|
|
if (st.isDirectory) {
|
|
walk(full_path, rel_path)
|
|
} else {
|
|
files.push(rel_path)
|
|
}
|
|
}
|
|
}
|
|
|
|
if (fd.is_dir(dir)) {
|
|
walk(dir, "")
|
|
}
|
|
return files
|
|
}
|
|
|
|
Shop.get_c_objects = function(pkg) {
|
|
var files = Shop.list_files(pkg)
|
|
var objects = []
|
|
var build_dir = get_build_dir(pkg)
|
|
|
|
for (var i=0; i<files.length; i++) {
|
|
var file = files[i]
|
|
if (file.endsWith('.c') || file.endsWith('.cpp')) {
|
|
objects.push(build_dir + '/' + file + '.o')
|
|
}
|
|
}
|
|
return objects
|
|
}
|
|
|
|
Shop.compile_module = function(alias) {
|
|
var module_dir = Shop.get_module_dir(alias)
|
|
if (!module_dir) {
|
|
log.error("Module not found: " + alias)
|
|
return false
|
|
}
|
|
|
|
log.console("Would compile module: " + alias + " from " + module_dir)
|
|
return true
|
|
}
|
|
|
|
var debug = use('debug')
|
|
|
|
Shop.module_reload = function(path, package) {
|
|
if (!Shop.is_loaded(path,package)) return
|
|
var info = resolve_module_info(path, package)
|
|
if (!info) return
|
|
|
|
var cache_key = info.cache_key
|
|
var old = use_cache[cache_key]
|
|
var newmod = get_module(path, package)
|
|
|
|
for (var i in newmod)
|
|
old[i] = newmod[i]
|
|
|
|
for (var i in old) {
|
|
if (!(i in newmod))
|
|
old[i] = null
|
|
}
|
|
}
|
|
|
|
Shop.build_package = function(package)
|
|
{
|
|
if (package == 'local') package = null
|
|
if (package) package = Shop.parse_package(package).path
|
|
var files = Shop.list_files(package)
|
|
var build_dir = get_build_dir(package)
|
|
ensure_dir(build_dir)
|
|
|
|
var module_dir = package ? '.cell/modules/' + package : '.'
|
|
|
|
log.console(`Building package ${package ? package : 'local'} to ${build_dir}`)
|
|
|
|
// For C compilation
|
|
var config = Shop.load_config(package) || {}
|
|
var cflags = get_flags(config, platform, 'CFLAGS')
|
|
|
|
// Determine usage prefix for C symbols
|
|
var use_prefix
|
|
if (!package) use_prefix = 'js_local_'
|
|
else use_prefix = 'js_' + package.replace(/\//g, '_').replace(/\./g, '_').replace(/-/g, '_') + '_'
|
|
|
|
var c_objects = []
|
|
|
|
function get_hash(str) {
|
|
return text(crypto.blake2(utf8.encode(str)), 'h')
|
|
}
|
|
|
|
for (var i=0; i<files.length; i++) {
|
|
var file = files[i]
|
|
var src_path
|
|
if (!package) src_path = file
|
|
else src_path = '.cell/modules/' + package + '/' + file
|
|
|
|
if (file.endsWith('.cm') || file.endsWith('.ce')) {
|
|
// Compile module
|
|
try {
|
|
resolve_mod_fn(src_path, package)
|
|
} catch (e) {
|
|
log.error(`Failed to compile ${src_path}: ${e}`)
|
|
log.error(e)
|
|
return false
|
|
}
|
|
} else if (file.endsWith('.c') || file.endsWith('.cpp')) {
|
|
// Compile C
|
|
var obj_path = build_dir + '/' + file + '.o'
|
|
var meta_path = obj_path + '.meta'
|
|
ensure_dir(obj_path.substring(0, obj_path.lastIndexOf('/')))
|
|
|
|
var safe_path = c_sym_path(file.substring(0, file.lastIndexOf('.')))
|
|
var use_name = use_prefix + safe_path + '_use'
|
|
var comp_src = file
|
|
var comp_obj = file + '.o'
|
|
var base_cmd = 'cc -fPIC '
|
|
var compile_flags = '-c ' + comp_src + ' -O3 -DCELL_USE_NAME=' + use_name
|
|
if (cflags != '') compile_flags += ' ' + cflags
|
|
|
|
var full_compile_cmd = 'cd ' + module_dir + ' && ' + base_cmd + compile_flags + ' -o ' + comp_obj
|
|
var cmd_hash = get_hash(full_compile_cmd)
|
|
|
|
var needs_compile = true
|
|
var meta = null
|
|
|
|
if (fd.is_file(obj_path) && fd.is_file(meta_path)) {
|
|
try {
|
|
meta = json.decode(text(fd.slurp(meta_path)))
|
|
} catch(e) {}
|
|
|
|
if (meta && meta.cmd_hash == cmd_hash) {
|
|
var st_src = fd.stat(src_path)
|
|
var st_obj = fd.stat(obj_path)
|
|
|
|
if (st_src && st_obj && st_src.mtime <= st_obj.mtime) {
|
|
needs_compile = false
|
|
|
|
// Check headers
|
|
if (meta.headers) {
|
|
for (var h = 0; h < meta.headers.length; h++) {
|
|
var header_rel = meta.headers[h]
|
|
var header_full
|
|
if (header_rel.startsWith('/'))
|
|
header_full = header_rel
|
|
else
|
|
header_full = module_dir + '/' + header_rel
|
|
if (!fd.is_file(header_full)) {
|
|
log.console(`coulnd't find header ${header_full}`)
|
|
needs_compile = true; break;
|
|
}
|
|
var st_h = fd.stat(header_full)
|
|
if (st_h.mtime > st_obj.mtime) {
|
|
log.console(`${header_full} out of date`)
|
|
needs_compile = true; break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (needs_compile) {
|
|
log.console("Compiling " + src_path + " -> " + obj_path)
|
|
|
|
// 1. Generate dependencies
|
|
var deps_file = comp_obj + '.d'
|
|
// Use same flags but with -M
|
|
var deps_cmd = 'cd ' + module_dir + ' && ' + base_cmd + '-MM ' + compile_flags + ' > ' + deps_file
|
|
os.system(deps_cmd)
|
|
|
|
var headers = []
|
|
var deps_full_path = module_dir + '/' + deps_file
|
|
|
|
if (fd.is_file(deps_full_path)) {
|
|
var deps_content = text(fd.slurp(deps_full_path))
|
|
deps_content = deps_content.replace(/\\\n/g, ' ').replace(/\\\r\n/g, ' ').replace(/\n/g, ' ')
|
|
|
|
var parts = deps_content.split(' ')
|
|
for (var p=0; p<parts.length; p++) {
|
|
var part = parts[p].trim()
|
|
if (part == '' || part.endsWith(':')) continue
|
|
if (part == comp_src) continue
|
|
headers.push(part)
|
|
}
|
|
fd.rm(deps_full_path)
|
|
}
|
|
|
|
// 2. Compile
|
|
var ret = os.system(full_compile_cmd)
|
|
if (ret != 0) {
|
|
log.error("Compilation failed for " + src_path)
|
|
return false
|
|
}
|
|
|
|
// 3. Move object
|
|
os.system('mv ' + module_dir + '/' + comp_obj + ' ' + obj_path)
|
|
|
|
// 4. Write meta
|
|
var new_meta = {
|
|
cmd_hash: cmd_hash,
|
|
headers: headers
|
|
}
|
|
fd.slurpwrite(meta_path, utf8.encode(json.encode(new_meta)))
|
|
}
|
|
c_objects.push(obj_path)
|
|
}
|
|
}
|
|
|
|
// Link if there are C objects
|
|
if (c_objects.length > 0) {
|
|
var lib_name = build_dir + '/cellmod' + dylib_ext
|
|
var lib_meta_path = lib_name + '.meta'
|
|
|
|
var link_flags = '-fPIC -shared'
|
|
|
|
var ldflags = get_flags(config, platform, 'LDFLAGS')
|
|
if (ldflags != '') link_flags += ' ' + ldflags
|
|
|
|
var temp_lib = 'cellmod' + dylib_ext
|
|
var objs_str = ''
|
|
for (var i=0; i<c_objects.length; i++) {
|
|
objs_str += '"$HERE/' + c_objects[i] + '" '
|
|
}
|
|
|
|
var link_cmd = 'HERE=$(pwd); cd ' + module_dir + ' && cc ' + link_flags + ' ' + objs_str + ' -lcell_runtime -lc -lc++ -o ' + temp_lib
|
|
var link_cmd_hash = get_hash(link_cmd)
|
|
|
|
// Check if we need to relink
|
|
var needs_link = true
|
|
|
|
if (fd.is_file(lib_name) && fd.is_file(lib_meta_path)) {
|
|
var meta = null
|
|
try {
|
|
meta = json.decode(text(fd.slurp(lib_meta_path)))
|
|
} catch(e) {}
|
|
|
|
if (meta && meta.cmd_hash == link_cmd_hash) {
|
|
var lib_time = fd.stat(lib_name).mtime
|
|
needs_link = false
|
|
for (var i=0; i<c_objects.length; i++) {
|
|
if (fd.stat(c_objects[i]).mtime > lib_time) {
|
|
needs_link = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (needs_link) {
|
|
log.console("Linking " + lib_name)
|
|
|
|
var ret = os.system(link_cmd)
|
|
if (ret != 0) {
|
|
log.error("Linking failed")
|
|
return false
|
|
}
|
|
os.system('mv ' + module_dir + '/' + temp_lib + ' ' + lib_name)
|
|
|
|
fd.slurpwrite(lib_meta_path, utf8.encode(json.encode({ cmd_hash: link_cmd_hash })))
|
|
}
|
|
log.console("Built " + lib_name)
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// Get dependencies for a specific context (package canonical path)
|
|
// If ctx is null, returns dependencies for the local project
|
|
Shop.dependencies = function(ctx) {
|
|
var config = Shop.load_config(ctx)
|
|
if (!config || !config.dependencies) {
|
|
return {}
|
|
}
|
|
return config.dependencies
|
|
}
|
|
|
|
Shop.list_packages = function(root)
|
|
{
|
|
var queue = []
|
|
var processed = {}
|
|
var result = []
|
|
|
|
var deps = Shop.dependencies(root)
|
|
for (var alias in deps) {
|
|
var pkg = deps[alias]
|
|
if (!processed[pkg]) {
|
|
queue.push(pkg)
|
|
}
|
|
}
|
|
|
|
while (queue.length > 0) {
|
|
var pkg = queue.shift()
|
|
if (processed[pkg]) continue
|
|
processed[pkg] = true
|
|
|
|
result.push(pkg)
|
|
|
|
var parsed = Shop.parse_package(pkg)
|
|
var pkg_config = Shop.load_config(parsed.path)
|
|
|
|
if (pkg_config && pkg_config.dependencies) {
|
|
for (var alias in pkg_config.dependencies) {
|
|
var dep_pkg = pkg_config.dependencies[alias]
|
|
if (!processed[dep_pkg]) {
|
|
queue.push(dep_pkg)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// List all .cm and .ce files in a package
|
|
// If ctx is null, lists local files
|
|
// If ctx is a canonical path, lists files in that module
|
|
Shop.list_modules = function(ctx) {
|
|
var files = Shop.list_files(ctx)
|
|
var modules = []
|
|
for (var i=0; i<files.length; i++) {
|
|
var f = files[i]
|
|
if (f.endsWith('.cm') || f.endsWith('.ce')) {
|
|
modules.push(f)
|
|
}
|
|
}
|
|
return modules
|
|
}
|
|
|
|
// Resolve a module path given a package context
|
|
// Returns { path, package_name } or null if not found
|
|
// Resolution order:
|
|
// 1. Local to the current package (if package_name is set)
|
|
// 2. Declared dependencies (by alias)
|
|
// 3. Core modules (handled by caller)
|
|
Shop.resolve_module = function(module_name, package_name, is_file_fn) {
|
|
var config = Shop.load_config()
|
|
var dependencies = (config && config.dependencies) || {}
|
|
|
|
// If we're in a package context, check the package first
|
|
if (package_name) {
|
|
var pkg_path = '.cell/modules/' + package_name + '/' + module_name + '.cm'
|
|
if (is_file_fn(pkg_path)) {
|
|
return { path: pkg_path, package_name: package_name }
|
|
}
|
|
}
|
|
|
|
// Check if module_name contains a slash (explicit package reference)
|
|
if (module_name.includes('/')) {
|
|
var parts = module_name.split('/')
|
|
var pkg_alias = parts[0]
|
|
var sub_module = parts.slice(1).join('/')
|
|
|
|
// Check if it's a declared dependency
|
|
if (dependencies[pkg_alias]) {
|
|
// Need to resolve alias to canonical path
|
|
var pkg = dependencies[pkg_alias]
|
|
var parsed = Shop.parse_package(pkg)
|
|
var canonical_path = parsed.path
|
|
|
|
var dep_path = '.cell/modules/' + canonical_path + '/' + sub_module + '.cm'
|
|
if (is_file_fn(dep_path)) {
|
|
return { path: dep_path, package_name: pkg_alias }
|
|
}
|
|
}
|
|
|
|
// Check local path (relative to project root)
|
|
var local_path = module_name + '.cm'
|
|
if (is_file_fn(local_path)) {
|
|
return { path: local_path, package_name: null }
|
|
}
|
|
} else {
|
|
// Simple module name - check local first, then dependencies
|
|
var local_path = module_name + '.cm'
|
|
if (is_file_fn(local_path)) {
|
|
return { path: local_path, package_name: null }
|
|
}
|
|
|
|
// Check each declared dependency for this module
|
|
for (var alias in dependencies) {
|
|
var pkg = dependencies[alias]
|
|
var parsed = Shop.parse_package(pkg)
|
|
var canonical_path = parsed.path
|
|
|
|
var dep_path = '.cell/modules/' + canonical_path + '/' + module_name + '.cm'
|
|
if (is_file_fn(dep_path)) {
|
|
return { path: dep_path, package_name: alias }
|
|
}
|
|
}
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
Shop.resolve_alias = function(name, ctx) {
|
|
var deps = Shop.dependencies(ctx)
|
|
if (deps && deps[name]) return { alias: name, pkg: Shop.get_canonical_package(name, ctx) }
|
|
|
|
for (var alias in deps) {
|
|
var pkg = deps[alias]
|
|
var parsed = Shop.parse_package(pkg)
|
|
if (parsed && (parsed.name == name || parsed.path == name)) {
|
|
return { alias: alias, pkg: parsed.path }
|
|
}
|
|
}
|
|
return null
|
|
}
|
|
|
|
Shop.get_canonical_package = get_canonical_package
|
|
return Shop |