Files
cell/internal/shop.cm
2026-01-06 11:17:07 -06:00

1236 lines
33 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 = path.split('/')
var current = path.startsWith('/') ? '/' : ''
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 = name.split('/')
if (parts.length > 1)
return parts[0]
return null
}
function is_internal_path(path)
{
return path && path.startsWith('internal/')
}
function split_explicit_package_import(path)
{
if (!path) return null
var parts = path.split('/')
if (parts.length < 2) return null
var looks_explicit = path.startsWith('/') || (parts[0] && parts[0].includes('.'))
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 = parts.slice(0, i).join('/')
var mod_path = parts.slice(i).join('/')
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 new 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 (package_dir.startsWith(packages_prefix))
return package_dir.substring(packages_prefix.length)
// 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 (file.endsWith(MOD_EXT))
info.is_module = true
else if (file.endsWith(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 = file.substring(pkg_dir.length + 1, file.length - ACTOR_EXT.length)
else if (info.is_module)
info.name = file.substring(pkg_dir.length + 1, file.length - MOD_EXT.length)
else
info.name = file.substring(pkg_dir.length + 1)
}
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 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 && pkg.startsWith('/'))
return pkg.replaceAll('/', '_').replaceAll('@', '_')
return pkg.replaceAll('@', '_')
}
function package_cache_path(pkg)
{
return global_shop_path + '/cache/' + pkg.replaceAll('/', '_').replaceAll('@', '_')
}
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(new blob(toml.encode(lock))));
}
// Get information about how to resolve a package
// Local packages always start with /
Shop.resolve_package_info = function(pkg) {
if (pkg.startsWith('/')) return 'local'
if (pkg.includes('gitea')) return 'gitea'
return null
}
// 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 == 'gitea') {
var parts = pkg.split('/')
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 = pkg.split('/')
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 name.substring(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 ', ' + inject.join(', ')
}
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 new 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(new 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(new 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 (ctx.startsWith('/')) {
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
// Resolves linked packages to their actual target first
function make_c_symbol(pkg, file) {
// Check if this package is linked - if so, use the link target for symbol name
var link_target = link.get_target(pkg)
var resolved_pkg = link_target ? link_target : pkg
var pkg_safe = resolved_pkg.replace(/\//g, '_').replace(/\./g, '_').replace(/-/g, '_')
var file_safe = file.replace(/\//g, '_').replace(/\./g, '_').replace(/-/g, '_')
return 'js_' + pkg_safe + '_' + file_safe + '_use'
}
// Get the library path for a package in .cell/lib
// Resolves linked packages to their actual target first
function get_lib_path(pkg) {
// Check if this package is linked - if so, use the link target
var link_target = link.get_target(pkg)
var resolved_pkg = link_target ? link_target : pkg
var lib_name = resolved_pkg.replace(/\//g, '_').replace(/\./g, '_').replace(/-/g, '_')
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 (resolved_pkg.startsWith('/')) {
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]
Shop.open_package_dylib(dep_pkg)
}
}
} catch (e) {
// Ignore errors reading cell.toml
}
}
var dl_path = get_lib_path(pkg)
if (fd.is_file(dl_path)) {
if (!open_dls[dl_path]) {
open_dls[dl_path] = os.dylib_open(dl_path)
}
}
}
// 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 = path.replace('/', '_')
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_${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 = number.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)
}
}
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 = c_resolve.symbol(null, my$_)
}
// 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 = mod_resolve.symbol.call(context, null, use_fn, ...vals)
} else if (c_resolve.scope < 900) {
// C only
used = c_resolve.symbol(null, my$_)
} else {
throw new Error(`Module ${info.path} could not be found`)
} if (!used)
throw new 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 new 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 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) {
return global_shop_path + '/cache/' + pkg.replaceAll('@','_').replaceAll('/','_') + '_' + 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
}
log.console("Downloading from " + download_url)
try {
var zip_blob = http.fetch(download_url)
log.console(`putting to ${cache_path}`)
fd.slurpwrite(cache_path, zip_blob)
log.console("Cached to " + cache_path)
return zip_blob
} catch (e) {
log.error(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 (returns true)
// For remote packages, downloads the zip if not present or hash mismatch
// Returns true on success
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 null
// No lock entry - can't fetch without knowing what commit
if (!lock_entry || !lock_entry.commit)
throw new Error("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) {
// Verify hash matches
var actual_hash = text(crypto.blake2(zip_blob), 'h')
if (actual_hash == expected_hash)
return true
log.console("Zip hash mismatch for " + pkg + ", re-fetching...")
}
// Download the zip
download_zip(pkg, commit)
return true
}
// 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
}
var zip_blob = get_package_zip(pkg)
if (!zip_blob)
throw new Error("No zip blob available for " + pkg)
// Extract zip for remote package
install_zip(zip_blob, target_dir)
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') return {
updated: time.number()
}
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 (local_commit == remote_commit)
return null
if (!remote_commit) {
log.error("Could not resolve commit for " + pkg)
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 new 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()
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('/')
var full_path = target_dir + '/' + rel_path
var dir_path = full_path.substring(0, full_path.lastIndexOf('/'))
ensure_dir(dir_path)
fd.slurpwrite(full_path, zip.slurp(filename))
}
}
// 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 new Error("Invalid package: " + pkg)
}
var commit = null
if (info != 'local') {
commit = fetch_remote_hash(pkg)
if (!commit) {
throw new 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 (file.endsWith('.cm') || file.endsWith('.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 = pkg.replace(/\//g, '_').replace(/\./g, '_').replace(/-/g, '_')
var file_safe = file.substring(0, file.lastIndexOf('.')).replace(/\//g, '_').replace(/\./g, '_').replace(/-/g, '_')
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 = pkg.replace(/\//g, '_').replace(/\./g, '_').replace(/-/g, '_')
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 pkg.replace(/\//g, '_').replace(/\./g, '_').replace(/-/g, '_')
}
// 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 (locator.includes('@')) {
clean = locator.split('@')[0]
}
var info = Shop.resolve_package_info(clean)
if (!info) return null
// Extract package name (last component of path)
var parts = clean.split('/')
var name = parts[parts.length - 1]
return {
path: clean,
name: name,
type: info
}
}
return Shop