From 58af683ca88b9ca3a0afbc38bd8cb42f192fae3e Mon Sep 17 00:00:00 2001 From: Guillaume ARM Date: Tue, 9 Jun 2026 06:25:32 +0200 Subject: [PATCH] feat(version): resolve versions from packages --- CLAUDE.md | 2 +- apis/eventloop.lua | 2 - apis/libai.lua | 6 -- apis/libccpm.lua | 6 -- apis/libtest.lua | 6 -- apis/libtui.lua | 2 - apis/libversion.lua | 102 ++++++++++++++++++++++++++++++++ apis/net.lua | 2 - manifest.json | 2 +- packages/index.json | 14 ++--- packages/trapos-ai/ccpm.json | 2 +- packages/trapos-boot/ccpm.json | 2 +- packages/trapos-core/ccpm.json | 3 +- packages/trapos-net/ccpm.json | 2 +- packages/trapos-test/ccpm.json | 2 +- packages/trapos-ui/ccpm.json | 2 +- packages/trapos/ccpm.json | 2 +- programs/ai.lua | 5 +- programs/ccpm.lua | 5 +- programs/events.lua | 4 +- programs/ping.lua | 4 +- programs/router.lua | 4 +- programs/runtest.lua | 4 +- programs/tuidemo.lua | 4 +- programs/upgrade.lua | 4 +- servers/ping-server.lua | 5 +- startup/motd.lua | 2 - startup/servers.lua | 2 - tests/libversion.lua | 103 +++++++++++++++++++++++++++++++++ 29 files changed, 238 insertions(+), 67 deletions(-) create mode 100644 apis/libversion.lua create mode 100644 tests/libversion.lua diff --git a/CLAUDE.md b/CLAUDE.md index f2bb18a..0b1fe05 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -41,7 +41,7 @@ Use [`docs/README.md`](docs/README.md) as the entrypoint for CC:Tweaked, CraftOS ## Conventions -- Bump `local _VERSION = '...'` when changing module behavior. +- Bump the owning `packages//ccpm.json` version (and mirror it in `packages/index.json`) when changing module behavior; programs report it at runtime via `require('/apis/libversion')().forSelf()`. `install-ccpm.lua` is the only file that still carries its own `_VERSION` because it is the wget bootstrap and lives outside the package system. - Programs support `-version`/`--version` and `-help`/`--help`; router also supports `-silent`/`--silent`. - French or English comments are fine; match surrounding code. - Commit messages use lightweight conventional style: `topic(scope): description` or `topic: description`. diff --git a/apis/eventloop.lua b/apis/eventloop.lua index dd29f85..88c9a46 100644 --- a/apis/eventloop.lua +++ b/apis/eventloop.lua @@ -1,5 +1,3 @@ -local _VERSION = '2.0.0' - -- Basic event loop library for computer craft -- -- Example usage: diff --git a/apis/libai.lua b/apis/libai.lua index ed4f656..a6f51eb 100644 --- a/apis/libai.lua +++ b/apis/libai.lua @@ -1,5 +1,3 @@ -local _VERSION = '0.4.1'; - local PING_PROMPT = 'reply with exactly: pong'; local DEFAULT_TIMEOUT_SECONDS = 1200; @@ -71,10 +69,6 @@ local function createAi(opts) local api = {}; - function api.version() - return _VERSION; - end - local function resolveTimeout(options) local raw = options.timeoutSeconds; if raw == nil then raw = settingsLib.get('opencc.timeout_seconds'); end diff --git a/apis/libccpm.lua b/apis/libccpm.lua index ac13699..314a1af 100644 --- a/apis/libccpm.lua +++ b/apis/libccpm.lua @@ -1,5 +1,3 @@ -local _VERSION = '0.2.0'; - -- libccpm: the testable core of the TrapOS package manager (ccpm). -- -- A factory: `local createCcpm = require('/apis/libccpm'); local ccpm = createCcpm();` @@ -472,10 +470,6 @@ local function createCcpm(opts) return true; end - function api.version() - return _VERSION; - end - return api; end diff --git a/apis/libtest.lua b/apis/libtest.lua index 1df7211..6d03a82 100644 --- a/apis/libtest.lua +++ b/apis/libtest.lua @@ -1,5 +1,3 @@ -local _VERSION = "1.5.0" - local DEFAULT_TIMEOUT_SECONDS = 3 local function createLibTest(args) @@ -137,10 +135,6 @@ local function createLibTest(args) return true end - function api.version() - return _VERSION - end - return api end diff --git a/apis/libtui.lua b/apis/libtui.lua index 45d0bcc..7b1d48e 100644 --- a/apis/libtui.lua +++ b/apis/libtui.lua @@ -1,4 +1,3 @@ -local _VERSION = '0.1.2'; local utf8 = rawget(_G, 'utf8'); local NODE_TEXT = 'text'; @@ -711,7 +710,6 @@ local function createTui(eventloop) api.List = makeList; api.list = makeList; api.Fragment = makeFragment; - api.version = _VERSION; api.eventloop = eventloop; return api; diff --git a/apis/libversion.lua b/apis/libversion.lua new file mode 100644 index 0000000..ec83b3d --- /dev/null +++ b/apis/libversion.lua @@ -0,0 +1,102 @@ +-- libversion: resolve a file's version from its owning package descriptor. +-- +-- A factory: `local createVersion = require('/apis/libversion'); local v = createVersion();` +-- +-- Resolution order for `api.forFile(path)`: +-- 1. `/ccpm.lock.json` — production lookup against installed packages. +-- 2. `/packages/*/ccpm.json` — dev fallback (e.g. `just trapos`, +-- where the repo is mounted read-only at `/trapos` and ccpm has never run). +-- 3. `'?'` when no descriptor lists the file. +-- +-- `api.forSelf()` returns the version of the currently running program by +-- delegating to `shell.getRunningProgram()`. + +local DEFAULT_STATE_DIR = '/trapos'; +local DEFAULT_REPO_ROOT = '/trapos'; + +local function normalizePath(path) + if type(path) ~= 'string' or path == '' then return nil; end + path = path:gsub('\\', '/'); + path = path:gsub('//+', '/'); + if path:sub(1, 1) ~= '/' then + path = '/' .. path; + end + return path; +end + +local function readJsonFile(fsLib, path) + if not fsLib.exists(path) then return nil; end + local f = fsLib.open(path, 'r'); + if not f then return nil; end + local data = f.readAll(); + f.close(); + if not data or data == '' then return nil; end + return textutils.unserializeJSON(data); +end + +local function fileMatches(files, target) + if type(files) ~= 'table' then return false; end + for _, raw in ipairs(files) do + if normalizePath(raw) == target then + return true; + end + end + return false; +end + +local function createVersion(opts) + opts = opts or {}; + local fsLib = opts.fs or fs; + local shellLib = opts.shell or shell; + local stateDir = opts.stateDir or DEFAULT_STATE_DIR; + local repoRoot = opts.repoRoot or DEFAULT_REPO_ROOT; + + local lockPath = stateDir .. '/ccpm.lock.json'; + local packagesDir = repoRoot .. '/packages'; + + local api = {}; + + local function lookupInLock(target) + local lock = readJsonFile(fsLib, lockPath); + if not lock or type(lock.packages) ~= 'table' then return nil; end + for _, entry in pairs(lock.packages) do + if fileMatches(entry.files, target) then + return entry.version; + end + end + return nil; + end + + local function lookupInRepo(target) + if not fsLib.isDir(packagesDir) then return nil; end + for _, name in ipairs(fsLib.list(packagesDir)) do + local descPath = packagesDir .. '/' .. name .. '/ccpm.json'; + local desc = readJsonFile(fsLib, descPath); + if desc and fileMatches(desc.files, target) then + return desc.version; + end + end + return nil; + end + + function api.forFile(path) + local target = normalizePath(path); + if not target then return '?'; end + local v = lookupInLock(target); + if v then return v; end + v = lookupInRepo(target); + if v then return v; end + return '?'; + end + + function api.forSelf() + if not shellLib or not shellLib.getRunningProgram then return '?'; end + local path = shellLib.getRunningProgram(); + if not path or path == '' then return '?'; end + return api.forFile(path); + end + + return api; +end + +return createVersion; diff --git a/apis/net.lua b/apis/net.lua index 980bfab..d00304c 100644 --- a/apis/net.lua +++ b/apis/net.lua @@ -1,5 +1,3 @@ -local _VERSION = '2.1.2'; - local createEventLoop = require('/apis/eventloop'); local DEFAULT_TIMEOUT_WAIT_MESSAGE = 0.5; -- in seconds diff --git a/manifest.json b/manifest.json index 18a9e3f..10e8b32 100644 --- a/manifest.json +++ b/manifest.json @@ -1,6 +1,6 @@ { "name": "TrapOS", - "version": "0.5.5", + "version": "0.6.0", "branch": "next", "packages": [ "trapos" diff --git a/packages/index.json b/packages/index.json index b77ae39..e307c88 100644 --- a/packages/index.json +++ b/packages/index.json @@ -1,11 +1,11 @@ { "packages": { - "trapos-core": "0.3.0", - "trapos-test": "0.2.0", - "trapos-boot": "0.2.1", - "trapos-net": "0.2.0", - "trapos-ui": "0.2.1", - "trapos-ai": "0.4.1", - "trapos": "0.5.5" + "trapos-core": "0.4.0", + "trapos-test": "0.2.1", + "trapos-boot": "0.2.2", + "trapos-net": "0.2.1", + "trapos-ui": "0.2.2", + "trapos-ai": "0.4.2", + "trapos": "0.6.0" } } diff --git a/packages/trapos-ai/ccpm.json b/packages/trapos-ai/ccpm.json index 15906b7..3d78e8c 100644 --- a/packages/trapos-ai/ccpm.json +++ b/packages/trapos-ai/ccpm.json @@ -1,6 +1,6 @@ { "name": "trapos-ai", - "version": "0.4.1", + "version": "0.4.2", "description": "TrapOS AI client for opencode serve", "dependencies": ["trapos-core"], "files": [ diff --git a/packages/trapos-boot/ccpm.json b/packages/trapos-boot/ccpm.json index bd0756f..28aa4a1 100644 --- a/packages/trapos-boot/ccpm.json +++ b/packages/trapos-boot/ccpm.json @@ -1,6 +1,6 @@ { "name": "trapos-boot", - "version": "0.2.1", + "version": "0.2.2", "description": "TrapOS boot: startup MOTD and autostart server launcher", "dependencies": ["trapos-core"], "files": [ diff --git a/packages/trapos-core/ccpm.json b/packages/trapos-core/ccpm.json index 26781e4..4c812a3 100644 --- a/packages/trapos-core/ccpm.json +++ b/packages/trapos-core/ccpm.json @@ -1,11 +1,12 @@ { "name": "trapos-core", - "version": "0.3.0", + "version": "0.4.0", "description": "TrapOS base: package manager, event loop, upgrade and event tools", "dependencies": [], "files": [ "apis/eventloop.lua", "apis/libccpm.lua", + "apis/libversion.lua", "programs/ccpm.lua", "programs/upgrade.lua", "programs/events.lua" diff --git a/packages/trapos-net/ccpm.json b/packages/trapos-net/ccpm.json index 2fe2303..7eee110 100644 --- a/packages/trapos-net/ccpm.json +++ b/packages/trapos-net/ccpm.json @@ -1,6 +1,6 @@ { "name": "trapos-net", - "version": "0.2.0", + "version": "0.2.1", "description": "TrapOS networking: routed modem messaging, router, ping", "dependencies": ["trapos-core"], "files": [ diff --git a/packages/trapos-test/ccpm.json b/packages/trapos-test/ccpm.json index 2bacf1c..0f857ff 100644 --- a/packages/trapos-test/ccpm.json +++ b/packages/trapos-test/ccpm.json @@ -1,6 +1,6 @@ { "name": "trapos-test", - "version": "0.2.0", + "version": "0.2.1", "description": "TrapOS test framework and CraftOS-PC suite runner", "dependencies": ["trapos-core"], "files": [ diff --git a/packages/trapos-ui/ccpm.json b/packages/trapos-ui/ccpm.json index 1f2b258..dd4bcd3 100644 --- a/packages/trapos-ui/ccpm.json +++ b/packages/trapos-ui/ccpm.json @@ -1,6 +1,6 @@ { "name": "trapos-ui", - "version": "0.2.1", + "version": "0.2.2", "description": "TrapOS terminal UI toolkit and demo", "dependencies": ["trapos-core"], "files": [ diff --git a/packages/trapos/ccpm.json b/packages/trapos/ccpm.json index db334d0..4d8343e 100644 --- a/packages/trapos/ccpm.json +++ b/packages/trapos/ccpm.json @@ -1,6 +1,6 @@ { "name": "trapos", - "version": "0.5.5", + "version": "0.6.0", "description": "TrapOS full install meta-package", "dependencies": ["trapos-boot", "trapos-net", "trapos-ui", "trapos-test", "trapos-ai"], "files": [], diff --git a/programs/ai.lua b/programs/ai.lua index 4f9cd8d..9fbf538 100644 --- a/programs/ai.lua +++ b/programs/ai.lua @@ -1,6 +1,5 @@ -local _VERSION = '0.4.1'; - local createAi = require('/apis/libai'); +local createVersion = require('/apis/libversion'); local args = table.pack(...); @@ -60,7 +59,7 @@ end local command = args[1]; if command == '--version' or command == '-version' or command == 'version' then - print('ai v' .. _VERSION); + print('v' .. createVersion().forSelf()); return; end diff --git a/programs/ccpm.lua b/programs/ccpm.lua index b149e2c..2861f35 100644 --- a/programs/ccpm.lua +++ b/programs/ccpm.lua @@ -1,6 +1,5 @@ -local _VERSION = '0.2.0'; - local createCcpm = require('/apis/libccpm'); +local createVersion = require('/apis/libversion'); local args = table.pack(...); local command = args[1]; @@ -25,7 +24,7 @@ local function printUsage() end if command == 'version' or command == '-version' or command == '--version' then - print('ccpm v' .. _VERSION); + print('v' .. createVersion().forSelf()); return; end diff --git a/programs/events.lua b/programs/events.lua index 9bcc02e..1063706 100644 --- a/programs/events.lua +++ b/programs/events.lua @@ -1,4 +1,4 @@ -local _VERSION = '1.0.2'; +local createVersion = require('/apis/libversion'); local command = ...; @@ -51,7 +51,7 @@ local function valueToString(value) end if command == 'version' or command == '-version' or command == '--version' then - print('events v' .. _VERSION); + print('v' .. createVersion().forSelf()); return; end diff --git a/programs/ping.lua b/programs/ping.lua index 8695986..6b28923 100644 --- a/programs/ping.lua +++ b/programs/ping.lua @@ -1,8 +1,6 @@ -local _VERSION = '2.0.2'; - local firstArg = ...; if firstArg == '-version' or firstArg == '--version' then - print('v' .. _VERSION); + print('v' .. require('/apis/libversion')().forSelf()); return; end diff --git a/programs/router.lua b/programs/router.lua index 4fc6b6b..30fe323 100644 --- a/programs/router.lua +++ b/programs/router.lua @@ -1,9 +1,7 @@ -local _VERSION = '1.3.1'; - local firstArg = ...; if firstArg == '-version' or firstArg == '--version' then - print('v' .. _VERSION); + print('v' .. require('/apis/libversion')().forSelf()); return; end diff --git a/programs/runtest.lua b/programs/runtest.lua index 683c311..84bf20f 100644 --- a/programs/runtest.lua +++ b/programs/runtest.lua @@ -1,4 +1,4 @@ -local _VERSION = "1.1.0" +local createVersion = require("/apis/libversion") local SUCCESS_MARKER = "__TRAPOS_TEST_OK__" local DEFAULT_REPORT_PATH = "/trapos-test-report" @@ -26,7 +26,7 @@ local function parseArgs(args) while i <= #args do local arg = args[i] if arg == "--version" or arg == "-version" then - print("runtest v" .. _VERSION) + print("v" .. createVersion().forSelf()) return nil elseif arg == "--help" or arg == "-help" then printUsage() diff --git a/programs/tuidemo.lua b/programs/tuidemo.lua index 45b6e13..6c93dc8 100644 --- a/programs/tuidemo.lua +++ b/programs/tuidemo.lua @@ -1,4 +1,4 @@ -local _VERSION = '0.1.0'; +local createVersion = require('/apis/libversion'); local command = ...; @@ -11,7 +11,7 @@ local function printUsage() end if command == 'version' or command == '-version' or command == '--version' then - print('tuidemo v' .. _VERSION); + print('v' .. createVersion().forSelf()); return; end diff --git a/programs/upgrade.lua b/programs/upgrade.lua index ec07372..ca9e552 100644 --- a/programs/upgrade.lua +++ b/programs/upgrade.lua @@ -1,4 +1,4 @@ -local _VERSION = '2.0.0'; +local createVersion = require('/apis/libversion'); local function printUsage() print('upgrade usage:'); @@ -11,7 +11,7 @@ end local command = ...; if command == 'version' or command == '-version' or command == '--version' then - print('upgrade v' .. _VERSION); + print('v' .. createVersion().forSelf()); return; end diff --git a/servers/ping-server.lua b/servers/ping-server.lua index 3cf2042..279d28a 100644 --- a/servers/ping-server.lua +++ b/servers/ping-server.lua @@ -1,10 +1,9 @@ -local _VERSION = "2.0.0" - -- -- Example: implementation simple de ping-server local PING_CHANNEL = 9; local MODEM_DETECTION_TIME = 3; -- in seconds local createNet = require('/apis/net'); +local createVersion = require('/apis/libversion'); local modem = peripheral.find('modem'); @@ -27,6 +26,6 @@ net.listenRequest(PING_CHANNEL, 'ping', function(message, reply) end end) -print('ping-server v' .. _VERSION .. ' started.') +print('ping-server v' .. createVersion().forSelf() .. ' started.') net.start(); diff --git a/startup/motd.lua b/startup/motd.lua index d1e2ec6..1cdb51c 100644 --- a/startup/motd.lua +++ b/startup/motd.lua @@ -1,5 +1,3 @@ -local _VERSION = '1.0.0'; - local LOCAL_MANIFEST_PATH = '/trapos/manifest.json'; local function readLocalManifest() diff --git a/startup/servers.lua b/startup/servers.lua index a592c3e..e642396 100644 --- a/startup/servers.lua +++ b/startup/servers.lua @@ -1,5 +1,3 @@ -local _VERSION = '1.3.3' - local LOCAL_MANIFEST_PATH = '/trapos/manifest.json'; local function readLocalManifest() diff --git a/tests/libversion.lua b/tests/libversion.lua new file mode 100644 index 0000000..880cb9c --- /dev/null +++ b/tests/libversion.lua @@ -0,0 +1,103 @@ +local createLibTest = require('/apis/libtest'); +local createVersion = require('/apis/libversion'); + +local testlib = createLibTest({ ... }); + +local counter = 0; + +local function freshDirs() + counter = counter + 1; + local stateDir = '/libversion-test/state-' .. counter; + local repoRoot = '/libversion-test/repo-' .. counter; + fs.delete(stateDir); + fs.delete(repoRoot); + return stateDir, repoRoot; +end + +local function writeJson(path, value) + local dir = path:match('^(.*)/[^/]+$'); + if dir then fs.makeDir(dir); end + local f = fs.open(path, 'w'); + f.write(textutils.serializeJSON(value)); + f.close(); +end + +testlib.test('forFile reads the version from the lock file', function() + local stateDir, repoRoot = freshDirs(); + writeJson(stateDir .. '/ccpm.lock.json', { + packages = { + ['trapos-ai'] = { + version = '0.4.2', + files = { 'apis/libai.lua', 'programs/ai.lua' }, + }, + ['trapos-core'] = { + version = '0.4.0', + files = { 'apis/eventloop.lua' }, + }, + }, + }); + local v = createVersion({ stateDir = stateDir, repoRoot = repoRoot }); + testlib.assertEquals(v.forFile('/programs/ai.lua'), '0.4.2'); + testlib.assertEquals(v.forFile('programs/ai.lua'), '0.4.2'); + testlib.assertEquals(v.forFile('/apis/eventloop.lua'), '0.4.0'); +end); + +testlib.test('forFile falls back to packages//ccpm.json when no lock entry matches', function() + local stateDir, repoRoot = freshDirs(); + writeJson(repoRoot .. '/packages/trapos-net/ccpm.json', { + name = 'trapos-net', + version = '0.2.1', + files = { 'apis/net.lua', 'programs/router.lua' }, + }); + writeJson(repoRoot .. '/packages/trapos-ui/ccpm.json', { + name = 'trapos-ui', + version = '0.2.2', + files = { 'apis/libtui.lua' }, + }); + local v = createVersion({ stateDir = stateDir, repoRoot = repoRoot }); + testlib.assertEquals(v.forFile('/programs/router.lua'), '0.2.1'); + testlib.assertEquals(v.forFile('/apis/libtui.lua'), '0.2.2'); +end); + +testlib.test('forFile prefers the lock entry over the repo descriptor', function() + local stateDir, repoRoot = freshDirs(); + writeJson(stateDir .. '/ccpm.lock.json', { + packages = { + ['trapos-net'] = { + version = '9.9.9', + files = { 'programs/ping.lua' }, + }, + }, + }); + writeJson(repoRoot .. '/packages/trapos-net/ccpm.json', { + name = 'trapos-net', + version = '0.2.1', + files = { 'programs/ping.lua' }, + }); + local v = createVersion({ stateDir = stateDir, repoRoot = repoRoot }); + testlib.assertEquals(v.forFile('/programs/ping.lua'), '9.9.9'); +end); + +testlib.test('forFile returns "?" when neither source knows the file', function() + local stateDir, repoRoot = freshDirs(); + local v = createVersion({ stateDir = stateDir, repoRoot = repoRoot }); + testlib.assertEquals(v.forFile('/programs/ghost.lua'), '?'); +end); + +testlib.test('forSelf resolves via shell.getRunningProgram', function() + local stateDir, repoRoot = freshDirs(); + writeJson(stateDir .. '/ccpm.lock.json', { + packages = { + ['trapos-test'] = { + version = '0.2.1', + files = { 'programs/runtest.lua' }, + }, + }, + }); + local fakeShell = { getRunningProgram = function() return 'programs/runtest.lua'; end }; + local v = createVersion({ stateDir = stateDir, repoRoot = repoRoot, shell = fakeShell }); + testlib.assertEquals(v.forSelf(), '0.2.1'); +end); + +testlib.run(); +