feat(version): resolve versions from packages

This commit is contained in:
Guillaume ARM 2026-06-09 06:25:32 +02:00
parent 54ffe82786
commit 58af683ca8
29 changed files with 238 additions and 67 deletions

View File

@ -41,7 +41,7 @@ Use [`docs/README.md`](docs/README.md) as the entrypoint for CC:Tweaked, CraftOS
## Conventions ## Conventions
- Bump `local _VERSION = '...'` when changing module behavior. - Bump the owning `packages/<name>/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`. - Programs support `-version`/`--version` and `-help`/`--help`; router also supports `-silent`/`--silent`.
- French or English comments are fine; match surrounding code. - French or English comments are fine; match surrounding code.
- Commit messages use lightweight conventional style: `topic(scope): description` or `topic: description`. - Commit messages use lightweight conventional style: `topic(scope): description` or `topic: description`.

View File

@ -1,5 +1,3 @@
local _VERSION = '2.0.0'
-- Basic event loop library for computer craft -- Basic event loop library for computer craft
-- --
-- Example usage: -- Example usage:

View File

@ -1,5 +1,3 @@
local _VERSION = '0.4.1';
local PING_PROMPT = 'reply with exactly: pong'; local PING_PROMPT = 'reply with exactly: pong';
local DEFAULT_TIMEOUT_SECONDS = 1200; local DEFAULT_TIMEOUT_SECONDS = 1200;
@ -71,10 +69,6 @@ local function createAi(opts)
local api = {}; local api = {};
function api.version()
return _VERSION;
end
local function resolveTimeout(options) local function resolveTimeout(options)
local raw = options.timeoutSeconds; local raw = options.timeoutSeconds;
if raw == nil then raw = settingsLib.get('opencc.timeout_seconds'); end if raw == nil then raw = settingsLib.get('opencc.timeout_seconds'); end

View File

@ -1,5 +1,3 @@
local _VERSION = '0.2.0';
-- libccpm: the testable core of the TrapOS package manager (ccpm). -- libccpm: the testable core of the TrapOS package manager (ccpm).
-- --
-- A factory: `local createCcpm = require('/apis/libccpm'); local ccpm = createCcpm();` -- A factory: `local createCcpm = require('/apis/libccpm'); local ccpm = createCcpm();`
@ -472,10 +470,6 @@ local function createCcpm(opts)
return true; return true;
end end
function api.version()
return _VERSION;
end
return api; return api;
end end

View File

@ -1,5 +1,3 @@
local _VERSION = "1.5.0"
local DEFAULT_TIMEOUT_SECONDS = 3 local DEFAULT_TIMEOUT_SECONDS = 3
local function createLibTest(args) local function createLibTest(args)
@ -137,10 +135,6 @@ local function createLibTest(args)
return true return true
end end
function api.version()
return _VERSION
end
return api return api
end end

View File

@ -1,4 +1,3 @@
local _VERSION = '0.1.2';
local utf8 = rawget(_G, 'utf8'); local utf8 = rawget(_G, 'utf8');
local NODE_TEXT = 'text'; local NODE_TEXT = 'text';
@ -711,7 +710,6 @@ local function createTui(eventloop)
api.List = makeList; api.List = makeList;
api.list = makeList; api.list = makeList;
api.Fragment = makeFragment; api.Fragment = makeFragment;
api.version = _VERSION;
api.eventloop = eventloop; api.eventloop = eventloop;
return api; return api;

102
apis/libversion.lua Normal file
View File

@ -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. `<stateDir>/ccpm.lock.json` — production lookup against installed packages.
-- 2. `<repoRoot>/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;

View File

@ -1,5 +1,3 @@
local _VERSION = '2.1.2';
local createEventLoop = require('/apis/eventloop'); local createEventLoop = require('/apis/eventloop');
local DEFAULT_TIMEOUT_WAIT_MESSAGE = 0.5; -- in seconds local DEFAULT_TIMEOUT_WAIT_MESSAGE = 0.5; -- in seconds

View File

@ -1,6 +1,6 @@
{ {
"name": "TrapOS", "name": "TrapOS",
"version": "0.5.5", "version": "0.6.0",
"branch": "next", "branch": "next",
"packages": [ "packages": [
"trapos" "trapos"

View File

@ -1,11 +1,11 @@
{ {
"packages": { "packages": {
"trapos-core": "0.3.0", "trapos-core": "0.4.0",
"trapos-test": "0.2.0", "trapos-test": "0.2.1",
"trapos-boot": "0.2.1", "trapos-boot": "0.2.2",
"trapos-net": "0.2.0", "trapos-net": "0.2.1",
"trapos-ui": "0.2.1", "trapos-ui": "0.2.2",
"trapos-ai": "0.4.1", "trapos-ai": "0.4.2",
"trapos": "0.5.5" "trapos": "0.6.0"
} }
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "trapos-ai", "name": "trapos-ai",
"version": "0.4.1", "version": "0.4.2",
"description": "TrapOS AI client for opencode serve", "description": "TrapOS AI client for opencode serve",
"dependencies": ["trapos-core"], "dependencies": ["trapos-core"],
"files": [ "files": [

View File

@ -1,6 +1,6 @@
{ {
"name": "trapos-boot", "name": "trapos-boot",
"version": "0.2.1", "version": "0.2.2",
"description": "TrapOS boot: startup MOTD and autostart server launcher", "description": "TrapOS boot: startup MOTD and autostart server launcher",
"dependencies": ["trapos-core"], "dependencies": ["trapos-core"],
"files": [ "files": [

View File

@ -1,11 +1,12 @@
{ {
"name": "trapos-core", "name": "trapos-core",
"version": "0.3.0", "version": "0.4.0",
"description": "TrapOS base: package manager, event loop, upgrade and event tools", "description": "TrapOS base: package manager, event loop, upgrade and event tools",
"dependencies": [], "dependencies": [],
"files": [ "files": [
"apis/eventloop.lua", "apis/eventloop.lua",
"apis/libccpm.lua", "apis/libccpm.lua",
"apis/libversion.lua",
"programs/ccpm.lua", "programs/ccpm.lua",
"programs/upgrade.lua", "programs/upgrade.lua",
"programs/events.lua" "programs/events.lua"

View File

@ -1,6 +1,6 @@
{ {
"name": "trapos-net", "name": "trapos-net",
"version": "0.2.0", "version": "0.2.1",
"description": "TrapOS networking: routed modem messaging, router, ping", "description": "TrapOS networking: routed modem messaging, router, ping",
"dependencies": ["trapos-core"], "dependencies": ["trapos-core"],
"files": [ "files": [

View File

@ -1,6 +1,6 @@
{ {
"name": "trapos-test", "name": "trapos-test",
"version": "0.2.0", "version": "0.2.1",
"description": "TrapOS test framework and CraftOS-PC suite runner", "description": "TrapOS test framework and CraftOS-PC suite runner",
"dependencies": ["trapos-core"], "dependencies": ["trapos-core"],
"files": [ "files": [

View File

@ -1,6 +1,6 @@
{ {
"name": "trapos-ui", "name": "trapos-ui",
"version": "0.2.1", "version": "0.2.2",
"description": "TrapOS terminal UI toolkit and demo", "description": "TrapOS terminal UI toolkit and demo",
"dependencies": ["trapos-core"], "dependencies": ["trapos-core"],
"files": [ "files": [

View File

@ -1,6 +1,6 @@
{ {
"name": "trapos", "name": "trapos",
"version": "0.5.5", "version": "0.6.0",
"description": "TrapOS full install meta-package", "description": "TrapOS full install meta-package",
"dependencies": ["trapos-boot", "trapos-net", "trapos-ui", "trapos-test", "trapos-ai"], "dependencies": ["trapos-boot", "trapos-net", "trapos-ui", "trapos-test", "trapos-ai"],
"files": [], "files": [],

View File

@ -1,6 +1,5 @@
local _VERSION = '0.4.1';
local createAi = require('/apis/libai'); local createAi = require('/apis/libai');
local createVersion = require('/apis/libversion');
local args = table.pack(...); local args = table.pack(...);
@ -60,7 +59,7 @@ end
local command = args[1]; local command = args[1];
if command == '--version' or command == '-version' or command == 'version' then if command == '--version' or command == '-version' or command == 'version' then
print('ai v' .. _VERSION); print('v' .. createVersion().forSelf());
return; return;
end end

View File

@ -1,6 +1,5 @@
local _VERSION = '0.2.0';
local createCcpm = require('/apis/libccpm'); local createCcpm = require('/apis/libccpm');
local createVersion = require('/apis/libversion');
local args = table.pack(...); local args = table.pack(...);
local command = args[1]; local command = args[1];
@ -25,7 +24,7 @@ local function printUsage()
end end
if command == 'version' or command == '-version' or command == '--version' then if command == 'version' or command == '-version' or command == '--version' then
print('ccpm v' .. _VERSION); print('v' .. createVersion().forSelf());
return; return;
end end

View File

@ -1,4 +1,4 @@
local _VERSION = '1.0.2'; local createVersion = require('/apis/libversion');
local command = ...; local command = ...;
@ -51,7 +51,7 @@ local function valueToString(value)
end end
if command == 'version' or command == '-version' or command == '--version' then if command == 'version' or command == '-version' or command == '--version' then
print('events v' .. _VERSION); print('v' .. createVersion().forSelf());
return; return;
end end

View File

@ -1,8 +1,6 @@
local _VERSION = '2.0.2';
local firstArg = ...; local firstArg = ...;
if firstArg == '-version' or firstArg == '--version' then if firstArg == '-version' or firstArg == '--version' then
print('v' .. _VERSION); print('v' .. require('/apis/libversion')().forSelf());
return; return;
end end

View File

@ -1,9 +1,7 @@
local _VERSION = '1.3.1';
local firstArg = ...; local firstArg = ...;
if firstArg == '-version' or firstArg == '--version' then if firstArg == '-version' or firstArg == '--version' then
print('v' .. _VERSION); print('v' .. require('/apis/libversion')().forSelf());
return; return;
end end

View File

@ -1,4 +1,4 @@
local _VERSION = "1.1.0" local createVersion = require("/apis/libversion")
local SUCCESS_MARKER = "__TRAPOS_TEST_OK__" local SUCCESS_MARKER = "__TRAPOS_TEST_OK__"
local DEFAULT_REPORT_PATH = "/trapos-test-report" local DEFAULT_REPORT_PATH = "/trapos-test-report"
@ -26,7 +26,7 @@ local function parseArgs(args)
while i <= #args do while i <= #args do
local arg = args[i] local arg = args[i]
if arg == "--version" or arg == "-version" then if arg == "--version" or arg == "-version" then
print("runtest v" .. _VERSION) print("v" .. createVersion().forSelf())
return nil return nil
elseif arg == "--help" or arg == "-help" then elseif arg == "--help" or arg == "-help" then
printUsage() printUsage()

View File

@ -1,4 +1,4 @@
local _VERSION = '0.1.0'; local createVersion = require('/apis/libversion');
local command = ...; local command = ...;
@ -11,7 +11,7 @@ local function printUsage()
end end
if command == 'version' or command == '-version' or command == '--version' then if command == 'version' or command == '-version' or command == '--version' then
print('tuidemo v' .. _VERSION); print('v' .. createVersion().forSelf());
return; return;
end end

View File

@ -1,4 +1,4 @@
local _VERSION = '2.0.0'; local createVersion = require('/apis/libversion');
local function printUsage() local function printUsage()
print('upgrade usage:'); print('upgrade usage:');
@ -11,7 +11,7 @@ end
local command = ...; local command = ...;
if command == 'version' or command == '-version' or command == '--version' then if command == 'version' or command == '-version' or command == '--version' then
print('upgrade v' .. _VERSION); print('v' .. createVersion().forSelf());
return; return;
end end

View File

@ -1,10 +1,9 @@
local _VERSION = "2.0.0"
-- -- Example: implementation simple de ping-server -- -- Example: implementation simple de ping-server
local PING_CHANNEL = 9; local PING_CHANNEL = 9;
local MODEM_DETECTION_TIME = 3; -- in seconds local MODEM_DETECTION_TIME = 3; -- in seconds
local createNet = require('/apis/net'); local createNet = require('/apis/net');
local createVersion = require('/apis/libversion');
local modem = peripheral.find('modem'); local modem = peripheral.find('modem');
@ -27,6 +26,6 @@ net.listenRequest(PING_CHANNEL, 'ping', function(message, reply)
end end
end) end)
print('ping-server v' .. _VERSION .. ' started.') print('ping-server v' .. createVersion().forSelf() .. ' started.')
net.start(); net.start();

View File

@ -1,5 +1,3 @@
local _VERSION = '1.0.0';
local LOCAL_MANIFEST_PATH = '/trapos/manifest.json'; local LOCAL_MANIFEST_PATH = '/trapos/manifest.json';
local function readLocalManifest() local function readLocalManifest()

View File

@ -1,5 +1,3 @@
local _VERSION = '1.3.3'
local LOCAL_MANIFEST_PATH = '/trapos/manifest.json'; local LOCAL_MANIFEST_PATH = '/trapos/manifest.json';
local function readLocalManifest() local function readLocalManifest()

103
tests/libversion.lua Normal file
View File

@ -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/<name>/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();