1312 lines
35 KiB
Plaintext
1312 lines
35 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 blob = use('blob')
|
|
var pkg_tools = use('package')
|
|
var os = use('os')
|
|
var link = use('link')
|
|
|
|
var core = "core"
|
|
|
|
function pull_from_cache(content)
|
|
{
|
|
var path = hash_path(content)
|
|
if (fd.is_file(path))
|
|
return fd.slurp(path)
|
|
}
|
|
|
|
function put_into_cache(content, obj)
|
|
{
|
|
var path = hash_path(content)
|
|
fd.slurpwrite(path, obj)
|
|
}
|
|
|
|
function ensure_dir(path) {
|
|
if (fd.stat(path).isDirectory) return
|
|
var parts = array(path, '/')
|
|
var current = starts_with(path, '/') ? '/' : ''
|
|
for (var i = 0; i < parts.length; i++) {
|
|
if (parts[i] == '') continue
|
|
current += parts[i] + '/'
|
|
if (!fd.stat(current).isDirectory) {
|
|
fd.mkdir(current)
|
|
}
|
|
}
|
|
}
|
|
|
|
function content_hash(content)
|
|
{
|
|
return text(crypto.blake2(content), 'h')
|
|
}
|
|
|
|
function hash_path(content)
|
|
{
|
|
return global_shop_path + '/build' + '/' + content_hash(content)
|
|
}
|
|
|
|
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 = '.dylib' // Default extension
|
|
|
|
var use_cache = os.use_cache
|
|
var global_shop_path = os.global_shop_path
|
|
var my$_ = os.$_
|
|
|
|
Shop.get_package_dir = function(name) {
|
|
return global_shop_path + '/packages/' + name
|
|
}
|
|
|
|
// Get the packages directory (in the global shop)
|
|
function get_packages_dir() {
|
|
return global_shop_path + '/packages'
|
|
}
|
|
|
|
// Get the core directory (in the global shop)
|
|
Shop.get_core_dir = function() {
|
|
return get_packages_dir() + '/' + core_package
|
|
}
|
|
|
|
var core_package = 'core'
|
|
|
|
// Get the links file path (in the global shop)
|
|
function get_links_path() {
|
|
return global_shop_path + '/link.toml'
|
|
}
|
|
|
|
// Get the reports directory (in the global shop)
|
|
Shop.get_reports_dir = function() {
|
|
return global_shop_path + '/reports'
|
|
}
|
|
|
|
function get_import_package(name) {
|
|
var parts = array(name, '/')
|
|
if (parts.length > 1)
|
|
return parts[0]
|
|
|
|
return null
|
|
}
|
|
|
|
function is_internal_path(path)
|
|
{
|
|
return path && starts_with(path, 'internal/')
|
|
}
|
|
|
|
function split_explicit_package_import(path)
|
|
{
|
|
if (!path) return null
|
|
var parts = array(path, '/')
|
|
|
|
if (parts.length < 2) return null
|
|
|
|
var looks_explicit = starts_with(path, '/') || (parts[0] && search(parts[0], '.') != null)
|
|
if (!looks_explicit) return null
|
|
|
|
// Find the longest prefix that is an installed package
|
|
for (var i = parts.length - 1; i >= 1; i--) {
|
|
var pkg_candidate = text(array(parts, 0, i), '/')
|
|
var mod_path = text(array(parts, i), '/')
|
|
if (!mod_path || mod_path.length == 0) continue
|
|
|
|
var candidate_dir = get_packages_dir() + '/' + safe_package_path(pkg_candidate)
|
|
if (fd.is_file(candidate_dir + '/cell.toml'))
|
|
return {package: pkg_candidate, path: mod_path}
|
|
|
|
if (package_in_shop(pkg_candidate))
|
|
return {package: pkg_candidate, path: mod_path}
|
|
|
|
if (Shop.resolve_package_info(pkg_candidate))
|
|
return {package: pkg_candidate, path: mod_path}
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
function package_in_shop(package) {
|
|
var lock = Shop.load_lock()
|
|
return package in lock
|
|
}
|
|
|
|
function abs_path_to_package(package_dir)
|
|
{
|
|
if (!fd.is_file(package_dir + '/cell.toml'))
|
|
throw Error('Not a valid package directory (no cell.toml): ' + package_dir)
|
|
|
|
var packages_prefix = get_packages_dir() + '/'
|
|
var core_dir = packages_prefix + core_package
|
|
|
|
// Check if this is the core package directory (or its symlink target)
|
|
if (package_dir == core_dir) {
|
|
return 'core'
|
|
}
|
|
// Also check if core_dir is a symlink pointing to package_dir
|
|
if (fd.is_link(core_dir)) {
|
|
var core_target = fd.readlink(core_dir)
|
|
if (core_target == package_dir || fd.realpath(core_dir) == package_dir) {
|
|
return 'core'
|
|
}
|
|
}
|
|
|
|
if (starts_with(package_dir, packages_prefix))
|
|
return text(package_dir, packages_prefix.length)
|
|
|
|
// Check if this local path is the target of a link
|
|
// If so, return the canonical package name (link origin) instead
|
|
var link_origin = link.get_origin(package_dir)
|
|
if (link_origin) {
|
|
return link_origin
|
|
}
|
|
|
|
// in this case, the dir is the package
|
|
if (package_in_shop(package_dir))
|
|
return package_dir
|
|
|
|
// For local directories (e.g., linked targets), read the package name from cell.toml
|
|
try {
|
|
var content = text(fd.slurp(package_dir + '/cell.toml'))
|
|
var cfg = toml.decode(content)
|
|
if (cfg.package)
|
|
return cfg.package
|
|
} catch (e) {
|
|
// Fall through
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
// given a file, find the absolute path, package name, and import name
|
|
Shop.file_info = function(file) {
|
|
var info = {
|
|
path: file,
|
|
is_module: false,
|
|
is_actor: false,
|
|
package: null,
|
|
name: null
|
|
}
|
|
|
|
if (ends_with(file, MOD_EXT))
|
|
info.is_module = true
|
|
else if (ends_with(file, ACTOR_EXT))
|
|
info.is_actor = true
|
|
|
|
// Find package directory and determine package name
|
|
var pkg_dir = pkg_tools.find_package_dir(file)
|
|
if (pkg_dir) {
|
|
info.package = abs_path_to_package(pkg_dir)
|
|
|
|
if (info.is_actor)
|
|
info.name = text(file, pkg_dir.length + 1, file.length - ACTOR_EXT.length)
|
|
else if (info.is_module)
|
|
info.name = text(file, pkg_dir.length + 1, file.length - MOD_EXT.length)
|
|
else
|
|
info.name = text(file, pkg_dir.length + 1)
|
|
}
|
|
|
|
return info
|
|
}
|
|
|
|
function get_import_name(path)
|
|
{
|
|
var parts = array(path, '/')
|
|
if (parts.length < 2) return null
|
|
return text(array(parts, 1), '/')
|
|
}
|
|
|
|
// Given a path like 'prosperon/sprite' and a package context,
|
|
// resolve the alias 'prosperon' to its canonical package name
|
|
function get_aliased_package(path, package_context) {
|
|
if (!package_context) return null
|
|
var alias = pkg_tools.split_alias(package_context, path)
|
|
if (alias) return alias.package
|
|
return null
|
|
}
|
|
|
|
// Same as get_aliased_package but just returns the package for the alias part
|
|
function get_canonical_package(alias, package_context) {
|
|
if (!package_context) return null
|
|
var result = pkg_tools.split_alias(package_context, alias + '/dummy')
|
|
if (result) return result.package
|
|
return null
|
|
}
|
|
|
|
// return the safe path for the package
|
|
// guaranteed to be validated
|
|
function safe_package_path(pkg)
|
|
{
|
|
// For absolute paths, replace / with _ to create a valid directory name
|
|
// Also replace @ with _
|
|
if (pkg && starts_with(pkg, '/'))
|
|
return replace(replace(pkg, '/', '_'), '@', '_')
|
|
return replace(pkg, '@', '_')
|
|
}
|
|
|
|
function package_cache_path(pkg)
|
|
{
|
|
return global_shop_path + '/cache/' + replace(replace(pkg, '/', '_'), '@', '_')
|
|
}
|
|
|
|
function get_shared_lib_path()
|
|
{
|
|
return get_global_build_dir() + '/' + 'lib'
|
|
}
|
|
|
|
// Load lock.toml configuration (from global shop)
|
|
var _lock = null
|
|
Shop.load_lock = function() {
|
|
if (_lock)
|
|
return _lock
|
|
|
|
var path = global_shop_path + '/lock.toml'
|
|
|
|
if (!fd.is_file(path))
|
|
return {}
|
|
|
|
var content = text(fd.slurp(path))
|
|
if (!content.length) return {}
|
|
|
|
_lock = toml.decode(content)
|
|
|
|
return _lock
|
|
}
|
|
|
|
// Save lock.toml configuration (to global shop)
|
|
Shop.save_lock = function(lock) {
|
|
var path = global_shop_path + '/lock.toml'
|
|
fd.slurpwrite(path, stone(blob(toml.encode(lock))));
|
|
}
|
|
|
|
|
|
// Get information about how to resolve a package
|
|
// Local packages always start with /
|
|
Shop.resolve_package_info = function(pkg) {
|
|
if (starts_with(pkg, '/')) return 'local'
|
|
if (search(pkg, 'gitea') != null) return 'gitea'
|
|
return null
|
|
}
|
|
|
|
// Verify if a package name is valid and return status
|
|
Shop.verify_package_name = function(pkg) {
|
|
if (!pkg) throw Error("Empty package name")
|
|
if (pkg == 'local') throw Error("local is not a valid package name")
|
|
if (pkg == 'core') throw Error("core is not a valid package name")
|
|
|
|
if (search(pkg, '://') != null)
|
|
throw Error(`Invalid package name: ${pkg}; did you mean ${array(pkg, '://')[1]}?`)
|
|
}
|
|
|
|
// Convert module package to download URL
|
|
Shop.get_download_url = function(pkg, commit_hash) {
|
|
var info = Shop.resolve_package_info(pkg)
|
|
|
|
if (info == 'gitea') {
|
|
var parts = array(pkg, '/')
|
|
var host = parts[0]
|
|
var user = parts[1]
|
|
var repo = parts[2]
|
|
|
|
return 'https://' + host + '/' + user + '/' + repo + '/archive/' + commit_hash + '.zip'
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
// Get the API URL for checking remote git commits
|
|
Shop.get_api_url = function(pkg) {
|
|
var info = Shop.resolve_package_info(pkg)
|
|
|
|
if (info == 'gitea') {
|
|
var parts = array(pkg, '/')
|
|
var host = parts[0]
|
|
var user = parts[1]
|
|
var repo = parts[2]
|
|
return 'https://' + host + '/api/v1/repos/' + user + '/' + repo + '/branches/'
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
// Extract commit hash from API response
|
|
Shop.extract_commit_hash = function(pkg, response) {
|
|
if (!response) return null
|
|
|
|
var info = Shop.resolve_package_info(pkg)
|
|
|
|
var data = json.decode(response)
|
|
|
|
if (info == 'gitea') {
|
|
if (is_array(data))
|
|
data = data[0]
|
|
return data.commit && data.commit.id
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
var dylib_visited = {}
|
|
var open_dls = {}
|
|
|
|
// Default capabilities injected into scripts
|
|
// These map to $_ properties in engine.cm
|
|
var SHOP_DEFAULT_INJECT = ['$self', '$overling', '$clock', '$delay', '$start', '$receiver', '$contact', '$portal', '$time_limit', '$couple', '$stop', '$unneeded', '$connection', '$fd']
|
|
|
|
function strip_dollar(name) {
|
|
if (name && name[0] == '$') return text(name, 1)
|
|
return name
|
|
}
|
|
|
|
// Decide what a given module is allowed to see.
|
|
// This is the capability gate - tweak as needed.
|
|
Shop.script_inject_for = function(file_info) {
|
|
if (!file_info) return []
|
|
|
|
// For now, grant everything to all scripts
|
|
// Later this can be tuned per package/script
|
|
return array(SHOP_DEFAULT_INJECT)
|
|
}
|
|
|
|
// Get capabilities for a script path (public API)
|
|
Shop.get_script_capabilities = function(path) {
|
|
var file_info = Shop.file_info(path)
|
|
return Shop.script_inject_for(file_info)
|
|
}
|
|
|
|
function inject_params(inject) {
|
|
if (!inject || !inject.length) return ''
|
|
return ', ' + text(inject, ', ')
|
|
}
|
|
|
|
function inject_values(inject) {
|
|
var vals = []
|
|
for (var i = 0; i < inject.length; i++) {
|
|
var key = strip_dollar(inject[i])
|
|
if (key == 'fd') vals.push(fd)
|
|
else vals.push(my$_[key])
|
|
}
|
|
return vals
|
|
}
|
|
|
|
// Build the use function for a specific package context
|
|
function make_use_fn_code(pkg_arg) {
|
|
return `function(path) { return globalThis.use(path, ${pkg_arg}); }`
|
|
}
|
|
|
|
// for script forms, path is the canonical path of the module
|
|
var script_form = function(path, script, pkg, inject) {
|
|
var pkg_arg = pkg ? `'${pkg}'` : 'null'
|
|
var params = inject_params(inject)
|
|
|
|
var fn = `(function setup_module(args, use${params}){ def arg = args; def PACKAGE = ${pkg_arg}; ${script}})`
|
|
return fn
|
|
}
|
|
|
|
// Resolve module function, hashing it in the process
|
|
// path is the exact path to the script file
|
|
function resolve_mod_fn(path, pkg) {
|
|
if (!fd.is_file(path)) throw Error(`path ${path} is not a file`)
|
|
|
|
var file_info = Shop.file_info(path)
|
|
var file_pkg = file_info.package
|
|
var inject = Shop.script_inject_for(file_info)
|
|
var content = text(fd.slurp(path))
|
|
var script = script_form(path, content, file_pkg, inject);
|
|
|
|
var obj = pull_from_cache(stone(blob(script)))
|
|
if (obj) {
|
|
var fn = js.compile_unblob(obj)
|
|
return js.eval_compile(fn)
|
|
}
|
|
|
|
// Compile name is just for debug/stack traces
|
|
// var compile_name = pkg ? pkg + ':' + path : 'local:' + path
|
|
var compile_name = path
|
|
|
|
var fn = js.compile(compile_name, script)
|
|
|
|
put_into_cache(stone(blob(script)), js.compile_blob(fn))
|
|
|
|
return js.eval_compile(fn)
|
|
}
|
|
|
|
// given a path and a package context
|
|
// return module info about where it was found
|
|
function resolve_locator(path, ctx)
|
|
{
|
|
var explicit = split_explicit_package_import(path)
|
|
if (explicit) {
|
|
if (is_internal_path(explicit.path) && ctx && explicit.package != ctx)
|
|
explicit = null
|
|
}
|
|
if (explicit) {
|
|
var explicit_path = get_packages_dir() + '/' + safe_package_path(explicit.package) + '/' + explicit.path
|
|
if (fd.is_file(explicit_path)) {
|
|
var fn = resolve_mod_fn(explicit_path, explicit.package)
|
|
return {path: explicit_path, scope: SCOPE_PACKAGE, symbol: fn}
|
|
}
|
|
}
|
|
|
|
// 1. If no context, resolve from core only
|
|
if (!ctx) {
|
|
var core_dir = Shop.get_core_dir()
|
|
var core_file_path = core_dir + '/' + path
|
|
if (fd.is_file(core_file_path)) {
|
|
var fn = resolve_mod_fn(core_file_path, 'core')
|
|
return {path: core_file_path, scope: SCOPE_CORE, symbol: fn}
|
|
}
|
|
return null
|
|
}
|
|
|
|
// check in ctx package
|
|
// If ctx is an absolute path (starts with /), use it directly
|
|
// Otherwise, look it up in the packages directory
|
|
var ctx_dir
|
|
if (starts_with(ctx, '/')) {
|
|
ctx_dir = ctx
|
|
} else {
|
|
ctx_dir = get_packages_dir() + '/' + safe_package_path(ctx)
|
|
}
|
|
var ctx_path = ctx_dir + '/' + path
|
|
|
|
if (fd.is_file(ctx_path)) {
|
|
var fn = resolve_mod_fn(ctx_path, ctx)
|
|
// Check if ctx is the core package (either by name or by path)
|
|
var is_core = (ctx == 'core') || (ctx_dir == Shop.get_core_dir())
|
|
var scope = is_core ? SCOPE_CORE : SCOPE_LOCAL
|
|
return {path: ctx_path, scope: scope, symbol: fn}
|
|
}
|
|
|
|
if (is_internal_path(path))
|
|
return null
|
|
|
|
// check for aliased dependency
|
|
var alias = pkg_tools.split_alias(ctx, path)
|
|
if (alias) {
|
|
var alias_path = get_packages_dir() + '/' + safe_package_path(alias.package) + '/' + alias.path
|
|
if (fd.is_file(alias_path)) {
|
|
var fn = resolve_mod_fn(alias_path, ctx)
|
|
return {path: alias_path, scope:SCOPE_PACKAGE, symbol:fn}
|
|
}
|
|
}
|
|
|
|
var package_path = get_packages_dir() + '/' + safe_package_path(path)
|
|
if (fd.is_file(package_path)) {
|
|
var fn = resolve_mod_fn(package_path, ctx)
|
|
return {path: package_path, scope: SCOPE_PACKAGE, symbol: fn}
|
|
}
|
|
|
|
// 4. Check core as fallback
|
|
var core_dir = Shop.get_core_dir()
|
|
var core_file_path = core_dir + '/' + path
|
|
if (fd.is_file(core_file_path)) {
|
|
var fn = resolve_mod_fn(core_file_path, 'core')
|
|
return {path: core_file_path, scope: SCOPE_CORE, symbol: fn}
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
// Generate symbol name for a C module file
|
|
// Uses the same format as Shop.c_symbol_for_file
|
|
// Symbol names are based on canonical package names, not link targets
|
|
function make_c_symbol(pkg, file) {
|
|
var pkg_safe = replace(replace(replace(pkg, '/', '_'), '.', '_'), '-', '_')
|
|
var file_safe = replace(replace(replace(file, '/', '_'), '.', '_'), '-', '_')
|
|
return 'js_' + pkg_safe + '_' + file_safe + '_use'
|
|
}
|
|
|
|
// Get the library path for a package in .cell/lib
|
|
// Library names are based on canonical package names, not link targets
|
|
function get_lib_path(pkg) {
|
|
var lib_name = replace(replace(replace(pkg, '/', '_'), '.', '_'), '-', '_')
|
|
return global_shop_path + '/lib/' + lib_name + dylib_ext
|
|
}
|
|
|
|
// Open a package's dynamic library and all its dependencies
|
|
Shop.open_package_dylib = function(pkg) {
|
|
if (pkg == 'core' || !pkg) return
|
|
if (dylib_visited[pkg]) return
|
|
dylib_visited[pkg] = true
|
|
|
|
var link_target = link.get_target(pkg)
|
|
var resolved_pkg = link_target ? link_target : pkg
|
|
|
|
var pkg_dir;
|
|
if (starts_with(resolved_pkg, '/')) {
|
|
pkg_dir = resolved_pkg
|
|
} else {
|
|
pkg_dir = get_packages_dir() + '/' + safe_package_path(resolved_pkg)
|
|
}
|
|
|
|
var toml_path = pkg_dir + '/cell.toml'
|
|
if (fd.is_file(toml_path)) {
|
|
try {
|
|
var content = text(fd.slurp(toml_path))
|
|
var cfg = toml.decode(content)
|
|
if (cfg.dependencies) {
|
|
for (var alias in cfg.dependencies) {
|
|
var dep_pkg = cfg.dependencies[alias]
|
|
try {
|
|
Shop.open_package_dylib(dep_pkg)
|
|
} catch (dep_e) {
|
|
// Dependency dylib load failed, continue with others
|
|
}
|
|
}
|
|
}
|
|
} catch (e) {
|
|
// Error reading toml, continue
|
|
}
|
|
}
|
|
|
|
var dl_path = get_lib_path(pkg)
|
|
if (fd.is_file(dl_path)) {
|
|
if (!open_dls[dl_path]) {
|
|
try {
|
|
open_dls[dl_path] = os.dylib_open(dl_path)
|
|
} catch (e) {
|
|
dylib_visited[pkg] = false
|
|
throw e
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Resolve a C symbol by searching:
|
|
// 1. If package_context is null, only check core internal symbols
|
|
// 2. Otherwise: own package (internal then dylib) -> other packages (internal then dylib) -> core (internal only)
|
|
// Core is never loaded as a dynamic library via dlopen
|
|
function resolve_c_symbol(path, package_context) {
|
|
var explicit = split_explicit_package_import(path)
|
|
if (explicit) {
|
|
if (is_internal_path(explicit.path) && package_context && explicit.package != package_context)
|
|
explicit = null
|
|
}
|
|
if (explicit) {
|
|
var sym = make_c_symbol(explicit.package, explicit.path)
|
|
if (os.internal_exists(sym)) {
|
|
return {
|
|
symbol: function() { return os.load_internal(sym) },
|
|
scope: SCOPE_PACKAGE,
|
|
package: explicit.package,
|
|
path: sym
|
|
}
|
|
}
|
|
|
|
Shop.open_package_dylib(explicit.package)
|
|
var dl_path = get_lib_path(explicit.package)
|
|
if (open_dls[dl_path] && os.dylib_has_symbol(open_dls[dl_path], sym)) {
|
|
return {
|
|
symbol: function() { return os.dylib_symbol(open_dls[dl_path], sym) },
|
|
scope: SCOPE_PACKAGE,
|
|
package: explicit.package,
|
|
path: sym
|
|
}
|
|
}
|
|
}
|
|
|
|
// If no package context, only check core internal symbols
|
|
if (!package_context || package_context == 'core') {
|
|
path = replace(path, '/', '_')
|
|
var core_sym = `js_${path}_use`
|
|
if (os.internal_exists(core_sym)) {
|
|
return {
|
|
symbol: function() { return os.load_internal(core_sym) },
|
|
scope: SCOPE_CORE,
|
|
path: core_sym
|
|
}
|
|
}
|
|
return null
|
|
}
|
|
|
|
// 1. Check own package first (internal, then dylib)
|
|
var sym = make_c_symbol(package_context, path)
|
|
if (os.internal_exists(sym)) {
|
|
return {
|
|
symbol: function() { return os.load_internal(sym) },
|
|
scope: SCOPE_LOCAL,
|
|
path: sym
|
|
}
|
|
}
|
|
|
|
Shop.open_package_dylib(package_context)
|
|
var dl_path = get_lib_path(package_context)
|
|
|
|
if (open_dls[dl_path] && os.dylib_has_symbol(open_dls[dl_path], sym)) {
|
|
return {
|
|
symbol: function() { return os.dylib_symbol(open_dls[dl_path], sym) },
|
|
scope: SCOPE_LOCAL,
|
|
path: sym
|
|
}
|
|
}
|
|
|
|
if (is_internal_path(path))
|
|
return null
|
|
|
|
// 2. Check aliased package imports (e.g. 'prosperon/sprite')
|
|
var pkg_alias = get_import_package(path)
|
|
if (pkg_alias) {
|
|
var canon_pkg = get_aliased_package(path, package_context)
|
|
if (canon_pkg) {
|
|
var mod_name = get_import_name(path)
|
|
var sym = make_c_symbol(canon_pkg, mod_name)
|
|
|
|
// Check internal first
|
|
if (os.internal_exists(sym)) {
|
|
return {
|
|
symbol: function() { return os.load_internal(sym) },
|
|
scope: SCOPE_PACKAGE,
|
|
package: canon_pkg,
|
|
path: sym
|
|
}
|
|
}
|
|
|
|
// Then check dylib
|
|
Shop.open_package_dylib(canon_pkg)
|
|
var dl_path = get_lib_path(canon_pkg)
|
|
if (open_dls[dl_path] && os.dylib_has_symbol(open_dls[dl_path], sym)) {
|
|
return {
|
|
symbol: function() { return os.dylib_symbol(open_dls[dl_path], sym) },
|
|
scope: SCOPE_PACKAGE,
|
|
package: canon_pkg,
|
|
path: sym
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 3. Check core internal symbols (core is never a dynamic library)
|
|
var core_sym = `js_${replace(path, '/', '_')}_use`
|
|
if (os.internal_exists(core_sym)) {
|
|
return {
|
|
symbol: function() { return os.load_internal(core_sym) },
|
|
scope: SCOPE_CORE,
|
|
path: core_sym
|
|
}
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
// Cache for resolved module info
|
|
var module_info_cache = {}
|
|
|
|
function resolve_module_info(path, package_context) {
|
|
var lookup_key = package_context ? package_context + ':' + path : ':' + path
|
|
|
|
if (module_info_cache[lookup_key])
|
|
return module_info_cache[lookup_key]
|
|
|
|
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 = min(c_resolve.scope, mod_resolve.scope)
|
|
|
|
if (min_scope == 999)
|
|
return null
|
|
|
|
var cache_key
|
|
if (mod_resolve.scope == SCOPE_CORE) {
|
|
cache_key = 'core/' + path
|
|
} else if (mod_resolve.scope < 900 && mod_resolve.path) {
|
|
var real_path = fd.realpath(mod_resolve.path)
|
|
if (real_path) {
|
|
var real_info = Shop.file_info(real_path)
|
|
if (real_info.package && real_info.name)
|
|
cache_key = real_info.package + '/' + real_info.name
|
|
else
|
|
cache_key = real_path
|
|
}
|
|
}
|
|
|
|
if (!cache_key) {
|
|
if (min_scope == SCOPE_CORE)
|
|
cache_key = 'core/' + path
|
|
else if (min_scope == SCOPE_LOCAL && package_context)
|
|
cache_key = package_context + '/' + path
|
|
else if (min_scope == SCOPE_PACKAGE) {
|
|
var pkg_alias = get_import_package(path)
|
|
if (pkg_alias) {
|
|
var canon_pkg = get_canonical_package(pkg_alias, package_context)
|
|
if (canon_pkg) {
|
|
var mod_name = get_import_name(path)
|
|
cache_key = canon_pkg + '/' + mod_name
|
|
} else
|
|
cache_key = path
|
|
} else
|
|
cache_key = path
|
|
} else
|
|
cache_key = path
|
|
}
|
|
|
|
var info = {
|
|
cache_key: cache_key,
|
|
c_resolve: c_resolve,
|
|
mod_resolve: mod_resolve,
|
|
min_scope: min_scope
|
|
}
|
|
|
|
module_info_cache[lookup_key] = info
|
|
return info
|
|
}
|
|
|
|
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 is_loaded(path, package_context) {
|
|
var cache_key = get_module_cache_key(path, package_context)
|
|
return use_cache[cache_key] != null
|
|
}
|
|
|
|
// Create a use function bound to a specific package context
|
|
function make_use_fn(pkg) {
|
|
return function(path) {
|
|
return Shop.use(path, pkg)
|
|
}
|
|
}
|
|
|
|
// Call a C module loader and execute the entrypoint
|
|
function call_c_module(c_resolve) {
|
|
var mod = c_resolve.symbol()
|
|
// if (is_function(mod))
|
|
// return mod()
|
|
return mod
|
|
}
|
|
|
|
function execute_module(info)
|
|
{
|
|
var c_resolve = info.c_resolve
|
|
var mod_resolve = info.mod_resolve
|
|
|
|
var used
|
|
|
|
if (mod_resolve.scope < 900) {
|
|
var context = null
|
|
if (c_resolve.scope < 900) {
|
|
context = call_c_module(c_resolve)
|
|
}
|
|
|
|
// Get file info to determine inject list
|
|
var file_info = Shop.file_info(mod_resolve.path)
|
|
var inject = Shop.script_inject_for(file_info)
|
|
var vals = inject_values(inject)
|
|
var pkg = file_info.package
|
|
var use_fn = make_use_fn(pkg)
|
|
|
|
// Call with signature: setup_module(args, use, ...capabilities)
|
|
// args is null for module loading
|
|
used = call(mod_resolve.symbol, context, null, use_fn, ...vals)
|
|
} else if (c_resolve.scope < 900) {
|
|
// C only
|
|
used = call_c_module(c_resolve)
|
|
} else {
|
|
throw Error(`Module ${info.path} could not be found`)
|
|
}
|
|
|
|
// if (is_function(used))
|
|
// throw Error('C module loader returned a function; did you forget to call it?')
|
|
|
|
if (!used)
|
|
throw Error(`Module ${info} returned null`)
|
|
|
|
// stone(used)
|
|
return used
|
|
}
|
|
|
|
function get_module(path, package_context) {
|
|
var info = resolve_module_info(path, package_context)
|
|
|
|
if (!info)
|
|
throw Error(`Module ${path} could not be found in ${package_context}`)
|
|
|
|
return execute_module(info)
|
|
}
|
|
|
|
Shop.use = function use(path, package_context) {
|
|
var info = resolve_module_info(path, package_context)
|
|
if (!info)
|
|
throw 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) {
|
|
return global_shop_path + '/cache/' + replace(replace(pkg, '@','_'), '/','_') + '_' + commit + '.zip'
|
|
}
|
|
|
|
function get_package_abs_dir(package)
|
|
{
|
|
return get_packages_dir() + '/' + safe_package_path(package)
|
|
}
|
|
|
|
// Fetch the latest commit hash from remote for a package
|
|
// Returns null for local packages or if fetch fails
|
|
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
|
|
}
|
|
}
|
|
|
|
// Download a zip for a package at a specific commit and cache it
|
|
// Returns the zip blob or null on failure
|
|
function download_zip(pkg, commit_hash) {
|
|
var cache_path = get_cache_path(pkg, commit_hash)
|
|
|
|
var download_url = Shop.get_download_url(pkg, commit_hash)
|
|
if (!download_url) {
|
|
log.error("Could not determine download URL for " + pkg)
|
|
return null
|
|
}
|
|
|
|
try {
|
|
var zip_blob = http.fetch(download_url)
|
|
fd.slurpwrite(cache_path, zip_blob)
|
|
return zip_blob
|
|
} catch (e) {
|
|
log.error("Download failed for " + pkg + ": " + e)
|
|
return null
|
|
}
|
|
}
|
|
|
|
// Get zip from cache, returns null if not cached
|
|
function get_cached_zip(pkg, commit_hash) {
|
|
var cache_path = get_cache_path(pkg, commit_hash)
|
|
if (fd.is_file(cache_path))
|
|
return fd.slurp(cache_path)
|
|
|
|
return null
|
|
}
|
|
|
|
// Fetch: Ensure the zip on disk matches what's in the lock file
|
|
// For local packages, this is a no-op
|
|
// For remote packages, downloads the zip if not present or hash mismatch
|
|
// Returns: { status: 'local'|'cached'|'downloaded'|'error', message: string }
|
|
Shop.fetch = function(pkg) {
|
|
var lock = Shop.load_lock()
|
|
var lock_entry = lock[pkg]
|
|
var info = Shop.resolve_package_info(pkg)
|
|
|
|
if (info == 'local') {
|
|
return { status: 'local' }
|
|
}
|
|
|
|
// No lock entry - can't fetch without knowing what commit
|
|
if (!lock_entry || !lock_entry.commit) {
|
|
return { status: 'error', message: "No lock entry for " + pkg + " - run update first" }
|
|
}
|
|
|
|
var commit = lock_entry.commit
|
|
var expected_hash = lock_entry.zip_hash
|
|
|
|
// Check if we have the zip cached
|
|
var zip_blob = get_cached_zip(pkg, commit)
|
|
|
|
if (zip_blob) {
|
|
// If we have a hash on record, verify it
|
|
if (expected_hash) {
|
|
var actual_hash = text(crypto.blake2(zip_blob), 'h')
|
|
if (actual_hash == expected_hash) {
|
|
return { status: 'cached' }
|
|
}
|
|
log.console("Zip hash mismatch for " + pkg + ", re-fetching...")
|
|
} else {
|
|
// No hash stored yet - compute and store it
|
|
var actual_hash = text(crypto.blake2(zip_blob), 'h')
|
|
lock_entry.zip_hash = actual_hash
|
|
Shop.save_lock(lock)
|
|
return { status: 'cached' }
|
|
}
|
|
}
|
|
|
|
// Download the zip
|
|
var new_zip = download_zip(pkg, commit)
|
|
if (!new_zip) {
|
|
return { status: 'error', message: "Failed to download " + pkg }
|
|
}
|
|
|
|
// Store the hash
|
|
var new_hash = text(crypto.blake2(new_zip), 'h')
|
|
lock_entry.zip_hash = new_hash
|
|
Shop.save_lock(lock)
|
|
|
|
return { status: 'downloaded' }
|
|
}
|
|
|
|
// Extract: Extract a package to its target directory
|
|
// For linked packages, creates a symlink to the link target
|
|
// For local packages, creates a symlink to the local path
|
|
// For remote packages, extracts from the provided zip blob
|
|
// Returns true on success
|
|
Shop.extract = function(pkg) {
|
|
var target_dir = get_package_abs_dir(pkg)
|
|
|
|
// Check if this package is linked
|
|
var link_target = link.get_target(pkg)
|
|
if (link_target) {
|
|
// Use the link - create symlink to link target
|
|
link.sync_one(pkg, link_target)
|
|
return true
|
|
}
|
|
|
|
var info = Shop.resolve_package_info(pkg)
|
|
|
|
if (info == 'local') {
|
|
if (fd.is_link(target_dir))
|
|
fd.unlink(target_dir)
|
|
if (fd.is_dir(target_dir))
|
|
fd.rmdir(target_dir)
|
|
fd.symlink(pkg, target_dir)
|
|
|
|
return true
|
|
}
|
|
|
|
// Check if already extracted at correct commit
|
|
var lock = Shop.load_lock()
|
|
var lock_entry = lock[pkg]
|
|
if (lock_entry && lock_entry.commit) {
|
|
var extracted_commit_file = target_dir + '/.cell_commit'
|
|
if (fd.is_file(extracted_commit_file)) {
|
|
var extracted_commit = trim(text(fd.slurp(extracted_commit_file)))
|
|
if (extracted_commit == lock_entry.commit) {
|
|
// Already extracted at this commit, skip
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
|
|
var zip_blob = get_package_zip(pkg)
|
|
|
|
if (!zip_blob)
|
|
throw Error("No zip blob available for " + pkg)
|
|
|
|
// Extract zip for remote package
|
|
install_zip(zip_blob, target_dir)
|
|
|
|
// Write marker file with the extracted commit
|
|
if (lock_entry && lock_entry.commit) {
|
|
fd.slurpwrite(target_dir + '/.cell_commit', stone(blob(lock_entry.commit)))
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
function get_package_zip(pkg)
|
|
{
|
|
var lock = Shop.load_lock()
|
|
var lock_entry = lock[pkg]
|
|
|
|
if (!lock_entry || !lock_entry.commit)
|
|
return null
|
|
|
|
var commit = lock_entry.commit
|
|
|
|
// Try to get from cache first
|
|
var cached = get_cached_zip(pkg, commit)
|
|
if (cached)
|
|
return cached
|
|
|
|
// Not in cache, download it
|
|
return download_zip(pkg, commit)
|
|
}
|
|
|
|
// Update: Check for new version, update lock, fetch and extract
|
|
// Returns the new lock entry if updated, null if already up to date or failed
|
|
Shop.update = function(pkg) {
|
|
var lock = Shop.load_lock()
|
|
var lock_entry = lock[pkg]
|
|
var info = Shop.resolve_package_info(pkg)
|
|
|
|
log.console(`checking ${pkg}`)
|
|
|
|
if (info == 'local') {
|
|
// Check if local path exists
|
|
if (!fd.is_dir(pkg)) {
|
|
log.console(` Local path does not exist: ${pkg}`)
|
|
return null
|
|
}
|
|
// Local packages always get a lock entry
|
|
var new_entry = {
|
|
type: 'local',
|
|
updated: time.number()
|
|
}
|
|
lock[pkg] = new_entry
|
|
Shop.save_lock(lock)
|
|
return new_entry
|
|
}
|
|
|
|
var local_commit = lock_entry ? lock_entry.commit : null
|
|
var remote_commit = fetch_remote_hash(pkg)
|
|
|
|
log.console(`local commit: ${local_commit}`)
|
|
log.console(`remote commit: ${remote_commit}`)
|
|
|
|
if (!remote_commit) {
|
|
log.error("Could not resolve commit for " + pkg)
|
|
return null
|
|
}
|
|
|
|
if (local_commit == remote_commit)
|
|
return null
|
|
|
|
var new_entry = {
|
|
type: info,
|
|
commit: remote_commit,
|
|
updated: time.number()
|
|
}
|
|
|
|
lock[pkg] = new_entry
|
|
Shop.save_lock(lock)
|
|
|
|
return new_entry
|
|
}
|
|
|
|
function install_zip(zip_blob, target_dir) {
|
|
var zip = miniz.read(zip_blob)
|
|
if (!zip) throw Error("Failed to read zip archive")
|
|
|
|
if (fd.is_link(target_dir)) fd.unlink(target_dir)
|
|
if (fd.is_dir(target_dir)) fd.rmdir(target_dir, 1)
|
|
|
|
log.console("Extracting to " + target_dir)
|
|
ensure_dir(target_dir)
|
|
|
|
var count = zip.count()
|
|
var created_dirs = {}
|
|
|
|
for (var i = 0; i < count; i++) {
|
|
if (zip.is_directory(i)) continue
|
|
var filename = zip.get_filename(i)
|
|
var parts = array(filename, '/')
|
|
if (parts.length <= 1) continue
|
|
|
|
parts.shift()
|
|
var rel_path = text(parts, '/')
|
|
var full_path = target_dir + '/' + rel_path
|
|
var dir_path = fd.dirname(full_path)
|
|
|
|
if (!created_dirs[dir_path]) {
|
|
ensure_dir(dir_path)
|
|
created_dirs[dir_path] = true
|
|
}
|
|
var file_data = zip.slurp(filename)
|
|
|
|
stone(file_data)
|
|
|
|
fd.slurpwrite(full_path, file_data)
|
|
}
|
|
}
|
|
|
|
// High-level: Remove a package from the shop
|
|
Shop.remove = function(pkg) {
|
|
// Remove from lock
|
|
var lock = Shop.load_lock()
|
|
if (lock[pkg]) {
|
|
delete lock[pkg]
|
|
Shop.save_lock(lock)
|
|
}
|
|
|
|
log.console("Removed " + pkg)
|
|
return true
|
|
}
|
|
|
|
Shop.get = function(pkg) {
|
|
var lock = Shop.load_lock()
|
|
|
|
if (!lock[pkg]) {
|
|
var info = Shop.resolve_package_info(pkg)
|
|
if (!info) {
|
|
throw Error("Invalid package: " + pkg)
|
|
}
|
|
|
|
var commit = null
|
|
if (info != 'local') {
|
|
commit = fetch_remote_hash(pkg)
|
|
if (!commit) {
|
|
throw Error("Could not resolve commit for " + pkg)
|
|
}
|
|
}
|
|
|
|
lock[pkg] = {
|
|
type: info,
|
|
commit: commit,
|
|
updated: time.number()
|
|
}
|
|
Shop.save_lock(lock)
|
|
}
|
|
}
|
|
|
|
// Compile a module
|
|
// List all files in a package
|
|
|
|
var debug = use('debug')
|
|
|
|
Shop.file_reload = function(file)
|
|
{
|
|
var info = Shop.file_info(file)
|
|
if (!info.is_module) return
|
|
|
|
var pkg = info.package
|
|
|
|
Shop.module_reload(info.name, pkg)
|
|
}
|
|
|
|
Shop.module_reload = function(path, package) {
|
|
if (!Shop.is_loaded(path,package)) return
|
|
|
|
// Clear the module info cache for this path
|
|
var lookup_key = package ? package + ':' + path : ':' + path
|
|
module_info_cache[lookup_key] = null
|
|
|
|
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
|
|
}
|
|
|
|
function get_package_scripts(package)
|
|
{
|
|
var files = pkg_tools.list_files(package)
|
|
var scripts = []
|
|
|
|
for (var i = 0; i < files.length; i++) {
|
|
var file = files[i]
|
|
if (ends_with(file, '.cm') || ends_with(file, '.ce')) {
|
|
scripts.push(file)
|
|
}
|
|
}
|
|
|
|
return scripts
|
|
}
|
|
|
|
Shop.build_package_scripts = function(package)
|
|
{
|
|
// compiles all .ce and .cm files in a package
|
|
var scripts = get_package_scripts(package)
|
|
var pkg_dir = get_package_abs_dir(package)
|
|
|
|
for (var script of scripts)
|
|
resolve_mod_fn(pkg_dir + '/' + script, package)
|
|
}
|
|
|
|
Shop.list_packages = function()
|
|
{
|
|
var lock = Shop.load_lock()
|
|
return array(lock)
|
|
}
|
|
|
|
// Get the lib directory for dynamic libraries
|
|
Shop.get_lib_dir = function() {
|
|
return global_shop_path + '/lib'
|
|
}
|
|
|
|
Shop.get_local_dir = function() {
|
|
return global_shop_path + "/local"
|
|
}
|
|
|
|
// Get the build cache directory
|
|
Shop.get_build_dir = function() {
|
|
return global_shop_path + '/build'
|
|
}
|
|
|
|
// Get the absolute path for a package
|
|
Shop.get_package_dir = function(pkg) {
|
|
return get_packages_dir() + '/' + safe_package_path(pkg)
|
|
}
|
|
|
|
// Generate C symbol name for a file within a package
|
|
// e.g., c_symbol_for_file('gitea.pockle.world/john/prosperon', 'sprite.c')
|
|
// -> 'js_gitea_pockle_world_john_prosperon_sprite_use'
|
|
Shop.c_symbol_for_file = function(pkg, file) {
|
|
var pkg_safe = replace(replace(replace(pkg, '/', '_'), '.', '_'), '-', '_')
|
|
var file_safe = replace(replace(fd.stem(file), '/', '_'), '.', '_')
|
|
return 'js_' + pkg_safe + '_' + file_safe + '_use'
|
|
}
|
|
|
|
// Generate C symbol prefix for a package
|
|
// e.g., c_symbol_prefix('gitea.pockle.world/john/prosperon') -> 'js_gitea_pockle_world_john_prosperon_'
|
|
Shop.c_symbol_prefix = function(pkg) {
|
|
var pkg_safe = replace(replace(replace(pkg, '/', '_'), '.', '_'), '-', '_')
|
|
return 'js_' + pkg_safe + '_'
|
|
}
|
|
|
|
// Get the library name for a package (without extension)
|
|
// e.g., 'gitea.pockle.world/john/prosperon' -> 'gitea_pockle_world_john_prosperon'
|
|
Shop.lib_name_for_package = function(pkg) {
|
|
return replace(replace(replace(pkg, '/', '_'), '.', '_'), '-', '_')
|
|
}
|
|
|
|
// Returns { ok: bool, results: [{pkg, ok, error}] }
|
|
Shop.audit_packages = function() {
|
|
var packages = Shop.list_packages()
|
|
|
|
var bad = []
|
|
|
|
for (var package of packages) {
|
|
if (package == 'core') continue
|
|
if (fd.is_dir(package)) continue
|
|
if (fetch_remote_hash(package)) continue
|
|
bad.push(package)
|
|
}
|
|
|
|
return bad
|
|
}
|
|
|
|
// Parse a package locator and return info about it
|
|
// Returns { path: canonical_path, name: package_name, type: 'local'|'gitea'|null }
|
|
Shop.parse_package = function(locator) {
|
|
if (!locator) return null
|
|
|
|
// Strip version suffix if present
|
|
var clean = locator
|
|
if (search(locator, '@') != null) {
|
|
clean = array(locator, '@')[0]
|
|
}
|
|
|
|
var info = Shop.resolve_package_info(clean)
|
|
if (!info) return null
|
|
|
|
// Extract package name (last component of path)
|
|
var parts = array(clean, '/')
|
|
var name = parts[parts.length - 1]
|
|
|
|
return {
|
|
path: clean,
|
|
name: name,
|
|
type: info
|
|
}
|
|
}
|
|
|
|
return Shop |