From ac1a7385e9c0f9f9d31987c3c5f8cba3a4173fc9 Mon Sep 17 00:00:00 2001 From: Guillaume ARM Date: Mon, 15 Jun 2026 01:12:53 +0200 Subject: [PATCH] fix(ccpm): drop github registry support --- AGENTS.md | 2 +- README.md | 5 +- apis/libccpm.lua | 5 +- docs/adrs/adr-0010-ccpm-package-manager.md | 4 +- packages/index.json | 2 +- packages/trapos-core/ccpm.json | 2 +- programs/ccpm.lua | 6 +- tests/ccpm.lua | 657 +++++++++++---------- 8 files changed, 370 insertions(+), 313 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 8c6d9a9..d9241b0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -37,7 +37,7 @@ Use [`docs/README.md`](docs/README.md) as the entrypoint for CC:Tweaked, CraftOS - Preserve `periphemu` guards used for CraftOS-PC emulation; see [`docs/craftos_pc_glossary.md`](docs/craftos_pc_glossary.md) for upstream emulator references. - Global CraftOS-PC save data lives in `~/Library/Application Support/CraftOS-PC` on macOS and `~/.local/share/craftos-pc` on Linux. Repo launchers use local state instead: `.craftos/` for TrapOS and `.craftos-vanilla/` for vanilla CraftOS-PC. See [`docs/install-craftos-pc.md`](docs/install-craftos-pc.md). - TrapOS ships as packages, each described by `packages//ccpm.json` (`name`, `version`, `dependencies`, `files`, `autostart`); `packages/index.json` lists them. Source files stay in place — descriptors only reference them. To ship a new file, add it to the right package's `files` (and `autostart` if it is a server). `packages/trapos/ccpm.json` is the full OS meta-package. See [ADR-0010](docs/adrs/adr-0010-ccpm-package-manager.md). -- `install-trapos.lua` is the one-time wget bootstrap (public entry point `https://trapos.trapcloud.fr/install`). It installs `trapos-core`/`ccpm`, seeds the default `guillaumearm/cc-libs` registry as a `gitea` registry on `git.trapcloud.fr` tracking `master` (the only channel), then runs `ccpm update` + `ccpm install trapos` and `trapos-postinstall`. Pass `--cpm-only` to stop after the ccpm bootstrap. The legacy `github` registry type still resolves but is deprecated. +- `install-trapos.lua` is the one-time wget bootstrap (public entry point `https://trapos.trapcloud.fr/install`). It installs `trapos-core`/`ccpm`, seeds the default `guillaumearm/cc-libs` registry as a `gitea` registry on `git.trapcloud.fr` tracking `master` (the only channel), then runs `ccpm update` + `ccpm install trapos` and `trapos-postinstall`. Pass `--cpm-only` to stop after the ccpm bootstrap. - `ccpm` (in `trapos-core`) is the package manager: `apis/libccpm.lua` is the testable core (factory with injectable `http`/`stateDir`/`installRoot`), `programs/ccpm.lua` the CLI. State: `/trapos/ccpm.json` (registries), `/trapos/ccpm.lock.json` (installed packages), and `/trapos/ccpm.cache.json` (available packages from `ccpm update`). Never use the word "manifest" in ccpm — it is reserved for the OS manifest. - Add new servers to `startup/servers.lua` as needed. diff --git a/README.md b/README.md index 5232a0f..4f34539 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ ccpm install ccpm reinstall ccpm uninstall ccpm update ccpm upgrade ccpm ls ccpm available [term] ccpm search [term] ccpm info -ccpm registry ls ccpm registry add [--branch ] [--type gitea|github|http] +ccpm registry ls ccpm registry add [--branch ] [--type gitea|http] ccpm registry rm ``` @@ -59,7 +59,8 @@ ccpm registry rm `ccpm available` lists cached packages and marks installed packages as up-to-date or updatable. `ccpm upgrade` upgrades installed packages based on that cache. -Registries default to GitHub (`owner/repo`); `http`/`https` base URLs are also +Registries default to gitea (`owner/repo` resolved against +`git.trapcloud.fr//`); `http`/`https` base URLs are also supported. State lives in `/trapos/ccpm.json` (registries), `/trapos/ccpm.lock.json` (installed packages), and `/trapos/ccpm.cache.json` (available packages). See [ADR-0010](docs/adrs/adr-0010-ccpm-package-manager.md). diff --git a/apis/libccpm.lua b/apis/libccpm.lua index 06fc05e..c1f5850 100644 --- a/apis/libccpm.lua +++ b/apis/libccpm.lua @@ -173,10 +173,7 @@ local function createCcpm(opts) -- ---------- URL resolution ---------- function api.registryBaseUrl(registry) - if registry.type == 'github' then - local branch = registry.branch or 'master'; - return 'https://raw.githubusercontent.com/' .. registry.name .. '/' .. branch .. '/'; - elseif registry.type == 'gitea' then + if registry.type == 'gitea' then local branch = registry.branch or 'master'; return 'https://git.trapcloud.fr/' .. registry.name .. '/raw/branch/' .. branch .. '/'; end diff --git a/docs/adrs/adr-0010-ccpm-package-manager.md b/docs/adrs/adr-0010-ccpm-package-manager.md index 801ec3c..1822cf4 100644 --- a/docs/adrs/adr-0010-ccpm-package-manager.md +++ b/docs/adrs/adr-0010-ccpm-package-manager.md @@ -67,9 +67,7 @@ Local state lives under `/trapos`: - `ccpm.json` — ordered registry list `{ registries = { { name, type, branch } } }`. `type` is `gitea` (resolves to `git.trapcloud.fr//raw/branch//`, the - default seeded by the bootstrap), `github` (resolves to - `raw.githubusercontent.com///`, deprecated but still supported), or - `http`/`https` (the `name` is a base URL). + default seeded by the bootstrap) or `http`/`https` (the `name` is a base URL). - `ccpm.lock.json` — installed packages `{ packages = { = { version, registry, files, dependencies, autostart } } }`, used by `ls`, `uninstall`, and `reinstall`. - `ccpm.cache.json` — packages advertised by configured registries, written by diff --git a/packages/index.json b/packages/index.json index a52b036..a05b04d 100644 --- a/packages/index.json +++ b/packages/index.json @@ -1,6 +1,6 @@ { "packages": { - "trapos-core": "0.6.0", + "trapos-core": "0.6.1", "trapos-test": "0.2.1", "trapos-boot": "0.3.3", "trapos-net": "0.3.0", diff --git a/packages/trapos-core/ccpm.json b/packages/trapos-core/ccpm.json index 6e5be4c..325bfae 100644 --- a/packages/trapos-core/ccpm.json +++ b/packages/trapos-core/ccpm.json @@ -1,6 +1,6 @@ { "name": "trapos-core", - "version": "0.6.0", + "version": "0.6.1", "description": "TrapOS base: package manager, event loop, upgrade and event tools", "dependencies": [], "files": [ diff --git a/programs/ccpm.lua b/programs/ccpm.lua index 70086f7..318ac0f 100644 --- a/programs/ccpm.lua +++ b/programs/ccpm.lua @@ -17,7 +17,7 @@ local function printUsage() print('\t\tccpm search [term]'); print('\t\tccpm info '); print('\t\tccpm registry ls'); - print('\t\tccpm registry add [--branch ] [--type github|gitea|http]'); + print('\t\tccpm registry add [--branch ] [--type gitea|http]'); print('\t\tccpm registry rm '); print('\t\tccpm version'); print('\t\tccpm help'); @@ -178,9 +178,7 @@ if command == 'registry' then return; end for _, r in ipairs(registries) do - if r.type == 'github' then - print(r.name .. ' (github:' .. tostring(r.branch or 'master') .. ')'); - elseif r.type == 'gitea' then + if r.type == 'gitea' then print(r.name .. ' (gitea:' .. tostring(r.branch or 'master') .. ')'); else print(r.name .. ' (' .. tostring(r.type or 'http') .. ')'); diff --git a/tests/ccpm.lua b/tests/ccpm.lua index 9eca6b9..af912ee 100644 --- a/tests/ccpm.lua +++ b/tests/ccpm.lua @@ -1,352 +1,415 @@ -local createLibTest = require('/apis/libtest'); -local createCcpm = require('/apis/libccpm'); +local createLibTest = require("/apis/libtest") +local createCcpm = require("/apis/libccpm") -local testlib = createLibTest({ ... }); +local testlib = createLibTest({ ... }) -local counter = 0; +local counter = 0 -- Fresh, isolated state + install sandbox per case (never touches real /trapos). local function freshDirs() - counter = counter + 1; - local stateDir = '/ccpm-test/state-' .. counter; - local installRoot = '/ccpm-test/root-' .. counter; - fs.delete(stateDir); - fs.delete(installRoot); - return stateDir, installRoot; + counter = counter + 1 + local stateDir = "/ccpm-test/state-" .. counter + local installRoot = "/ccpm-test/root-" .. counter + fs.delete(stateDir) + fs.delete(installRoot) + return stateDir, installRoot end -- Minimal stub of the CC `http` API backed by a url -> body map. local function fakeHttp(routes) - return { - get = function(url) - local body = routes[url]; - if not body then return nil; end - return { - readAll = function() return body; end, - close = function() end, - }; - end, - }; -end - -local function ghBase(name, branch) - return 'https://raw.githubusercontent.com/' .. name .. '/' .. branch .. '/'; + return { + get = function(url) + local body = routes[url] + if not body then + return nil + end + return { + readAll = function() + return body + end, + close = function() end, + } + end, + } end local function giteaBase(name, branch) - return 'https://git.trapcloud.fr/' .. name .. '/raw/branch/' .. branch .. '/'; + return "https://git.trapcloud.fr/" .. name .. "/raw/branch/" .. branch .. "/" end -testlib.test('registryBaseUrl resolves a github branch', function() - local ccpm = createCcpm({ stateDir = freshDirs() }); - testlib.assertEquals( - ccpm.registryBaseUrl({ name = 'guillaumearm/cc-libs', type = 'github', branch = 'next' }), - 'https://raw.githubusercontent.com/guillaumearm/cc-libs/next/' - ); -end); +testlib.test("registryBaseUrl resolves a gitea branch", function() + local ccpm = createCcpm({ stateDir = freshDirs() }) + testlib.assertEquals( + ccpm.registryBaseUrl({ name = "guillaumearm/cc-libs", type = "gitea", branch = "next" }), + "https://git.trapcloud.fr/guillaumearm/cc-libs/raw/branch/next/" + ) + testlib.assertEquals( + ccpm.descriptorUrl({ name = "guillaumearm/cc-libs", type = "gitea", branch = "next" }, "trapos-net"), + "https://git.trapcloud.fr/guillaumearm/cc-libs/raw/branch/next/packages/trapos-net/ccpm.json" + ) +end) -testlib.test('registryBaseUrl resolves a gitea branch', function() - local ccpm = createCcpm({ stateDir = freshDirs() }); - testlib.assertEquals( - ccpm.registryBaseUrl({ name = 'guillaumearm/cc-libs', type = 'gitea', branch = 'next' }), - 'https://git.trapcloud.fr/guillaumearm/cc-libs/raw/branch/next/' - ); - testlib.assertEquals( - ccpm.descriptorUrl({ name = 'guillaumearm/cc-libs', type = 'gitea', branch = 'next' }, 'trapos-net'), - 'https://git.trapcloud.fr/guillaumearm/cc-libs/raw/branch/next/packages/trapos-net/ccpm.json' - ); -end); +testlib.test("registry urls resolve an http base", function() + local ccpm = createCcpm({ stateDir = freshDirs() }) + testlib.assertEquals( + ccpm.registryBaseUrl({ name = "http://example.com/repo", type = "http" }), + "http://example.com/repo/" + ) + testlib.assertEquals( + ccpm.descriptorUrl({ name = "http://example.com/repo/", type = "http" }, "trapos-net"), + "http://example.com/repo/packages/trapos-net/ccpm.json" + ) +end) -testlib.test('registry urls resolve an http base', function() - local ccpm = createCcpm({ stateDir = freshDirs() }); - testlib.assertEquals( - ccpm.registryBaseUrl({ name = 'http://example.com/repo', type = 'http' }), - 'http://example.com/repo/' - ); - testlib.assertEquals( - ccpm.descriptorUrl({ name = 'http://example.com/repo/', type = 'http' }, 'trapos-net'), - 'http://example.com/repo/packages/trapos-net/ccpm.json' - ); -end); +testlib.test("resolve orders dependencies before dependents", function() + local base = giteaBase("me/repo", "master") + local routes = { + [base .. "packages/a/ccpm.json"] = textutils.serializeJSON({ + name = "a", + version = "1", + dependencies = { "b", "c" }, + files = {}, + }), + [base .. "packages/b/ccpm.json"] = textutils.serializeJSON({ + name = "b", + version = "1", + dependencies = { "c" }, + files = {}, + }), + [base .. "packages/c/ccpm.json"] = textutils.serializeJSON({ + name = "c", + version = "1", + dependencies = {}, + files = {}, + }), + } + local ccpm = createCcpm({ stateDir = freshDirs(), http = fakeHttp(routes) }) + ccpm.writeConfig({ registries = { { name = "me/repo", type = "gitea", branch = "master" } } }) + local ordered, err = ccpm.resolve("a") + testlib.assertTrue(ordered, tostring(err)) + testlib.assertEquals(#ordered, 3) + testlib.assertEquals(ordered[1].name, "c") + testlib.assertEquals(ordered[2].name, "b") + testlib.assertEquals(ordered[3].name, "a") +end) -testlib.test('resolve orders dependencies before dependents', function() - local base = ghBase('me/repo', 'master'); - local routes = { - [base .. 'packages/a/ccpm.json'] = textutils.serializeJSON({ name = 'a', version = '1', dependencies = { 'b', 'c' }, files = {} }), - [base .. 'packages/b/ccpm.json'] = textutils.serializeJSON({ name = 'b', version = '1', dependencies = { 'c' }, files = {} }), - [base .. 'packages/c/ccpm.json'] = textutils.serializeJSON({ name = 'c', version = '1', dependencies = {}, files = {} }), - }; - local ccpm = createCcpm({ stateDir = freshDirs(), http = fakeHttp(routes) }); - ccpm.writeConfig({ registries = { { name = 'me/repo', type = 'github', branch = 'master' } } }); - local ordered, err = ccpm.resolve('a'); - testlib.assertTrue(ordered, tostring(err)); - testlib.assertEquals(#ordered, 3); - testlib.assertEquals(ordered[1].name, 'c'); - testlib.assertEquals(ordered[2].name, 'b'); - testlib.assertEquals(ordered[3].name, 'a'); -end); +testlib.test("resolve reports a missing package", function() + local ccpm = createCcpm({ stateDir = freshDirs(), http = fakeHttp({}) }) + ccpm.writeConfig({ registries = { { name = "me/repo", type = "gitea", branch = "master" } } }) + local ordered, err = ccpm.resolve("ghost") + testlib.assertTrue(not ordered) + testlib.assertTrue(string.find(err, "not found", 1, true)) +end) -testlib.test('resolve reports a missing package', function() - local ccpm = createCcpm({ stateDir = freshDirs(), http = fakeHttp({}) }); - ccpm.writeConfig({ registries = { { name = 'me/repo', type = 'github', branch = 'master' } } }); - local ordered, err = ccpm.resolve('ghost'); - testlib.assertTrue(not ordered); - testlib.assertTrue(string.find(err, 'not found', 1, true)); -end); +testlib.test("resolve detects a dependency cycle", function() + local base = giteaBase("me/repo", "master") + local routes = { + [base .. "packages/a/ccpm.json"] = textutils.serializeJSON({ + name = "a", + version = "1", + dependencies = { "b" }, + }), + [base .. "packages/b/ccpm.json"] = textutils.serializeJSON({ + name = "b", + version = "1", + dependencies = { "a" }, + }), + } + local ccpm = createCcpm({ stateDir = freshDirs(), http = fakeHttp(routes) }) + ccpm.writeConfig({ registries = { { name = "me/repo", type = "gitea", branch = "master" } } }) + local ordered, err = ccpm.resolve("a") + testlib.assertTrue(not ordered) + testlib.assertTrue(string.find(err, "cycle", 1, true)) +end) -testlib.test('resolve detects a dependency cycle', function() - local base = ghBase('me/repo', 'master'); - local routes = { - [base .. 'packages/a/ccpm.json'] = textutils.serializeJSON({ name = 'a', version = '1', dependencies = { 'b' } }), - [base .. 'packages/b/ccpm.json'] = textutils.serializeJSON({ name = 'b', version = '1', dependencies = { 'a' } }), - }; - local ccpm = createCcpm({ stateDir = freshDirs(), http = fakeHttp(routes) }); - ccpm.writeConfig({ registries = { { name = 'me/repo', type = 'github', branch = 'master' } } }); - local ordered, err = ccpm.resolve('a'); - testlib.assertTrue(not ordered); - testlib.assertTrue(string.find(err, 'cycle', 1, true)); -end); +testlib.test("install rejects an already-installed package", function() + local ccpm = createCcpm({ stateDir = freshDirs() }) + ccpm.writeLock({ packages = { foo = { version = "1", files = {} } } }) + local ok, msg = ccpm.install("foo", {}) + testlib.assertTrue(not ok) + testlib.assertTrue(string.find(msg, "already installed", 1, true)) + testlib.assertTrue(string.find(msg, "reinstall foo", 1, true)) +end) -testlib.test('install rejects an already-installed package', function() - local ccpm = createCcpm({ stateDir = freshDirs() }); - ccpm.writeLock({ packages = { foo = { version = '1', files = {} } } }); - local ok, msg = ccpm.install('foo', {}); - testlib.assertTrue(not ok); - testlib.assertTrue(string.find(msg, 'already installed', 1, true)); - testlib.assertTrue(string.find(msg, 'reinstall foo', 1, true)); -end); +testlib.test("install downloads files and records the lock", function() + local base = giteaBase("me/repo", "master") + local routes = { + [base .. "packages/trapos-core/ccpm.json"] = textutils.serializeJSON({ + name = "trapos-core", + version = "1", + dependencies = {}, + files = { "apis/eventloop.lua" }, + }), + [base .. "packages/trapos-net/ccpm.json"] = textutils.serializeJSON({ + name = "trapos-net", + version = "1", + dependencies = { "trapos-core" }, + files = { "apis/net.lua" }, + }), + [base .. "apis/eventloop.lua"] = "eventloop-body", + [base .. "apis/net.lua"] = "net-body", + } + local sd, root = freshDirs() + local ccpm = createCcpm({ stateDir = sd, installRoot = root, http = fakeHttp(routes) }) + ccpm.writeConfig({ registries = { { name = "me/repo", type = "gitea", branch = "master" } } }) -testlib.test('install downloads files and records the lock', function() - local base = ghBase('me/repo', 'master'); - local routes = { - [base .. 'packages/trapos-core/ccpm.json'] = textutils.serializeJSON({ name = 'trapos-core', version = '1', dependencies = {}, files = { 'apis/eventloop.lua' } }), - [base .. 'packages/trapos-net/ccpm.json'] = textutils.serializeJSON({ name = 'trapos-net', version = '1', dependencies = { 'trapos-core' }, files = { 'apis/net.lua' } }), - [base .. 'apis/eventloop.lua'] = 'eventloop-body', - [base .. 'apis/net.lua'] = 'net-body', - }; - local sd, root = freshDirs(); - local ccpm = createCcpm({ stateDir = sd, installRoot = root, http = fakeHttp(routes) }); - ccpm.writeConfig({ registries = { { name = 'me/repo', type = 'github', branch = 'master' } } }); + local ok = ccpm.install("trapos-net", {}) + testlib.assertTrue(ok) + testlib.assertTrue(fs.exists(root .. "/apis/net.lua")) + testlib.assertTrue(fs.exists(root .. "/apis/eventloop.lua")) - local ok = ccpm.install('trapos-net', {}); - testlib.assertTrue(ok); - testlib.assertTrue(fs.exists(root .. '/apis/net.lua')); - testlib.assertTrue(fs.exists(root .. '/apis/eventloop.lua')); + local f = fs.open(root .. "/apis/net.lua", "r") + local body = f.readAll() + f.close() + testlib.assertEquals(body, "net-body") - local f = fs.open(root .. '/apis/net.lua', 'r'); - local body = f.readAll(); - f.close(); - testlib.assertEquals(body, 'net-body'); + local lock = ccpm.readLock() + testlib.assertTrue(lock.packages["trapos-net"]) + testlib.assertTrue(lock.packages["trapos-core"]) + testlib.assertEquals(lock.packages["trapos-net"].registry, "me/repo") +end) - local lock = ccpm.readLock(); - testlib.assertTrue(lock.packages['trapos-net']); - testlib.assertTrue(lock.packages['trapos-core']); - testlib.assertEquals(lock.packages['trapos-net'].registry, 'me/repo'); -end); +testlib.test("install downloads files from a gitea registry", function() + local base = giteaBase("guillaumearm/cc-libs", "next") + local routes = { + [base .. "packages/trapos-core/ccpm.json"] = textutils.serializeJSON({ + name = "trapos-core", + version = "1", + dependencies = {}, + files = { "apis/eventloop.lua" }, + }), + [base .. "packages/trapos-net/ccpm.json"] = textutils.serializeJSON({ + name = "trapos-net", + version = "1", + dependencies = { "trapos-core" }, + files = { "apis/net.lua" }, + }), + [base .. "apis/eventloop.lua"] = "eventloop-body", + [base .. "apis/net.lua"] = "net-body", + } + local sd, root = freshDirs() + local ccpm = createCcpm({ stateDir = sd, installRoot = root, http = fakeHttp(routes) }) + ccpm.writeConfig({ registries = { { name = "guillaumearm/cc-libs", type = "gitea", branch = "next" } } }) -testlib.test('install downloads files from a gitea registry', function() - local base = giteaBase('guillaumearm/cc-libs', 'next'); - local routes = { - [base .. 'packages/trapos-core/ccpm.json'] = textutils.serializeJSON({ name = 'trapos-core', version = '1', dependencies = {}, files = { 'apis/eventloop.lua' } }), - [base .. 'packages/trapos-net/ccpm.json'] = textutils.serializeJSON({ name = 'trapos-net', version = '1', dependencies = { 'trapos-core' }, files = { 'apis/net.lua' } }), - [base .. 'apis/eventloop.lua'] = 'eventloop-body', - [base .. 'apis/net.lua'] = 'net-body', - }; - local sd, root = freshDirs(); - local ccpm = createCcpm({ stateDir = sd, installRoot = root, http = fakeHttp(routes) }); - ccpm.writeConfig({ registries = { { name = 'guillaumearm/cc-libs', type = 'gitea', branch = 'next' } } }); + local ok = ccpm.install("trapos-net", {}) + testlib.assertTrue(ok) + testlib.assertTrue(fs.exists(root .. "/apis/net.lua")) + testlib.assertTrue(fs.exists(root .. "/apis/eventloop.lua")) - local ok = ccpm.install('trapos-net', {}); - testlib.assertTrue(ok); - testlib.assertTrue(fs.exists(root .. '/apis/net.lua')); - testlib.assertTrue(fs.exists(root .. '/apis/eventloop.lua')); + local f = fs.open(root .. "/apis/net.lua", "r") + local body = f.readAll() + f.close() + testlib.assertEquals(body, "net-body") - local f = fs.open(root .. '/apis/net.lua', 'r'); - local body = f.readAll(); - f.close(); - testlib.assertEquals(body, 'net-body'); + local lock = ccpm.readLock() + testlib.assertEquals(lock.packages["trapos-net"].registry, "guillaumearm/cc-libs") +end) - local lock = ccpm.readLock(); - testlib.assertEquals(lock.packages['trapos-net'].registry, 'guillaumearm/cc-libs'); -end); +testlib.test("installing trapos writes aggregated os state", function() + local base = giteaBase("me/repo", "master") + local routes = { + [base .. "packages/trapos/ccpm.json"] = textutils.serializeJSON({ + name = "trapos", + version = "1", + dependencies = { "trapos-net" }, + files = {}, + }), + [base .. "packages/trapos-core/ccpm.json"] = textutils.serializeJSON({ + name = "trapos-core", + version = "1", + dependencies = {}, + files = { "programs/ccpm.lua" }, + }), + [base .. "packages/trapos-net/ccpm.json"] = textutils.serializeJSON({ + name = "trapos-net", + version = "1", + dependencies = { "trapos-core" }, + files = { "apis/net.lua" }, + autostart = { "servers/ping-server" }, + }), + [base .. "programs/ccpm.lua"] = "ccpm-body", + [base .. "apis/net.lua"] = "net-body", + } + local sd, root = freshDirs() + local ccpm = createCcpm({ stateDir = sd, installRoot = root, http = fakeHttp(routes) }) + ccpm.writeConfig({ registries = { { name = "me/repo", type = "gitea", branch = "master" } } }) -testlib.test('installing trapos writes aggregated os state', function() - local base = ghBase('me/repo', 'master'); - local routes = { - [base .. 'packages/trapos/ccpm.json'] = textutils.serializeJSON({ name = 'trapos', version = '1', dependencies = { 'trapos-net' }, files = {} }), - [base .. 'packages/trapos-core/ccpm.json'] = textutils.serializeJSON({ name = 'trapos-core', version = '1', dependencies = {}, files = { 'programs/ccpm.lua' } }), - [base .. 'packages/trapos-net/ccpm.json'] = textutils.serializeJSON({ name = 'trapos-net', version = '1', dependencies = { 'trapos-core' }, files = { 'apis/net.lua' }, autostart = { 'servers/ping-server' } }), - [base .. 'programs/ccpm.lua'] = 'ccpm-body', - [base .. 'apis/net.lua'] = 'net-body', - }; - local sd, root = freshDirs(); - local ccpm = createCcpm({ stateDir = sd, installRoot = root, http = fakeHttp(routes) }); - ccpm.writeConfig({ registries = { { name = 'me/repo', type = 'github', branch = 'master' } } }); + testlib.assertTrue(ccpm.install("trapos", {})) - testlib.assertTrue(ccpm.install('trapos', {})); + local f = fs.open(sd .. "/manifest.json", "r") + local manifest = textutils.unserializeJSON(f.readAll()) + f.close() + testlib.assertEquals(manifest.version, "1") + testlib.assertEquals(#manifest.files, 2) + testlib.assertEquals(manifest.autostart[1], "servers/ping-server") +end) - local f = fs.open(sd .. '/manifest.json', 'r'); - local manifest = textutils.unserializeJSON(f.readAll()); - f.close(); - testlib.assertEquals(manifest.version, '1'); - testlib.assertEquals(#manifest.files, 2); - testlib.assertEquals(manifest.autostart[1], 'servers/ping-server'); -end); +testlib.test("registry add and remove round-trip", function() + local ccpm = createCcpm({ stateDir = freshDirs() }) + ccpm.writeConfig({ registries = {} }) + testlib.assertTrue(ccpm.addRegistry("foo/bar", { branch = "next" })) -testlib.test('registry add and remove round-trip', function() - local ccpm = createCcpm({ stateDir = freshDirs() }); - ccpm.writeConfig({ registries = {} }); - testlib.assertTrue(ccpm.addRegistry('foo/bar', { branch = 'next' })); + local regs = ccpm.listRegistries() + testlib.assertEquals(#regs, 1) + testlib.assertEquals(regs[1].name, "foo/bar") + testlib.assertEquals(regs[1].branch, "next") - local regs = ccpm.listRegistries(); - testlib.assertEquals(#regs, 1); - testlib.assertEquals(regs[1].name, 'foo/bar'); - testlib.assertEquals(regs[1].branch, 'next'); + local dupOk = ccpm.addRegistry("foo/bar", {}) + testlib.assertTrue(not dupOk) - local dupOk = ccpm.addRegistry('foo/bar', {}); - testlib.assertTrue(not dupOk); + testlib.assertTrue(ccpm.removeRegistry("foo/bar")) + testlib.assertEquals(#ccpm.listRegistries(), 0) - testlib.assertTrue(ccpm.removeRegistry('foo/bar')); - testlib.assertEquals(#ccpm.listRegistries(), 0); + local rmOk = ccpm.removeRegistry("nope") + testlib.assertTrue(not rmOk) +end) - local rmOk = ccpm.removeRegistry('nope'); - testlib.assertTrue(not rmOk); -end); +testlib.test("addRegistry defaults to a gitea registry", function() + local ccpm = createCcpm({ stateDir = freshDirs() }) + ccpm.writeConfig({ registries = {} }) -testlib.test('addRegistry defaults to a gitea registry', function() - local ccpm = createCcpm({ stateDir = freshDirs() }); - ccpm.writeConfig({ registries = {} }); + local ok, registry = ccpm.addRegistry("foo/bar", {}) + testlib.assertTrue(ok) + testlib.assertEquals(registry.type, "gitea") + testlib.assertEquals(registry.branch, "master") - local ok, registry = ccpm.addRegistry('foo/bar', {}); - testlib.assertTrue(ok); - testlib.assertEquals(registry.type, 'gitea'); - testlib.assertEquals(registry.branch, 'master'); + local regs = ccpm.listRegistries() + testlib.assertEquals(regs[1].type, "gitea") + testlib.assertEquals(ccpm.registryBaseUrl(regs[1]), "https://git.trapcloud.fr/foo/bar/raw/branch/master/") - local regs = ccpm.listRegistries(); - testlib.assertEquals(regs[1].type, 'gitea'); - -- The default registry must not silently fall back to github. - testlib.assertTrue(regs[1].type ~= 'github'); - testlib.assertEquals( - ccpm.registryBaseUrl(regs[1]), - 'https://git.trapcloud.fr/foo/bar/raw/branch/master/' - ); + -- An explicit type is still honored. + testlib.assertTrue(ccpm.addRegistry("http://example.com/repo", { type = "http" })) + local explicit = ccpm.listRegistries()[2] + testlib.assertEquals(explicit.type, "http") +end) - -- An explicit type is still honored (github stays supported but opt-in). - testlib.assertTrue(ccpm.addRegistry('legacy/repo', { type = 'github' })); - local legacy = ccpm.listRegistries()[2]; - testlib.assertEquals(legacy.type, 'github'); -end); +testlib.test("compareVersions treats padded zeros as equal", function() + -- compareVersions is internal; probe via available() status + local ccpm = createCcpm({ stateDir = freshDirs() }) + ccpm.writeCache({ packages = { pkg = { version = "1.0.0", registry = "r" } } }) + ccpm.writeLock({ packages = { pkg = { version = "1.0", registry = "r", files = {}, dependencies = {} } } }) + local avail = ccpm.available() + testlib.assertEquals(avail[1].status, "up-to-date") +end) -testlib.test('compareVersions treats padded zeros as equal', function() - -- compareVersions is internal; probe via available() status - local ccpm = createCcpm({ stateDir = freshDirs() }); - ccpm.writeCache({ packages = { pkg = { version = '1.0.0', registry = 'r' } } }); - ccpm.writeLock({ packages = { pkg = { version = '1.0', registry = 'r', files = {}, dependencies = {} } } }); - local avail = ccpm.available(); - testlib.assertEquals(avail[1].status, 'up-to-date'); -end); +testlib.test("update writes a package cache from registries", function() + local base = giteaBase("me/repo", "master") + local routes = { + [base .. "packages/index.json"] = textutils.serializeJSON({ packages = { foo = "1.0.0", bar = "2.0.0" } }), + } + local ccpm = createCcpm({ stateDir = freshDirs(), http = fakeHttp(routes) }) + ccpm.writeConfig({ registries = { { name = "me/repo", type = "gitea", branch = "master" } } }) -testlib.test('update writes a package cache from registries', function() - local base = ghBase('me/repo', 'master'); - local routes = { - [base .. 'packages/index.json'] = textutils.serializeJSON({ packages = { foo = '1.0.0', bar = '2.0.0' } }), - }; - local ccpm = createCcpm({ stateDir = freshDirs(), http = fakeHttp(routes) }); - ccpm.writeConfig({ registries = { { name = 'me/repo', type = 'github', branch = 'master' } } }); + local cache = ccpm.update() + testlib.assertEquals(cache.packages.foo.version, "1.0.0") + testlib.assertEquals(cache.packages.foo.registry, "me/repo") - local cache = ccpm.update(); - testlib.assertEquals(cache.packages.foo.version, '1.0.0'); - testlib.assertEquals(cache.packages.foo.registry, 'me/repo'); + local search = ccpm.search("ba") + testlib.assertEquals(#search, 1) + testlib.assertEquals(search[1].name, "bar") +end) - local search = ccpm.search('ba'); - testlib.assertEquals(#search, 1); - testlib.assertEquals(search[1].name, 'bar'); -end); +testlib.test("available marks cached packages by install status", function() + local ccpm = createCcpm({ stateDir = freshDirs() }) + ccpm.writeCache({ + packages = { + alpha = { version = "1.0.0", registry = "me/repo" }, + beta = { version = "2.0.0", registry = "me/repo" }, + gamma = { version = "1.0.0", registry = "me/repo" }, + }, + }) + ccpm.writeLock({ + packages = { + alpha = { version = "1.0.0", files = {}, dependencies = {} }, + beta = { version = "1.0.0", files = {}, dependencies = {} }, + }, + }) -testlib.test('available marks cached packages by install status', function() - local ccpm = createCcpm({ stateDir = freshDirs() }); - ccpm.writeCache({ packages = { - alpha = { version = '1.0.0', registry = 'me/repo' }, - beta = { version = '2.0.0', registry = 'me/repo' }, - gamma = { version = '1.0.0', registry = 'me/repo' }, - } }); - ccpm.writeLock({ packages = { - alpha = { version = '1.0.0', files = {}, dependencies = {} }, - beta = { version = '1.0.0', files = {}, dependencies = {} }, - } }); + local available = ccpm.available() + testlib.assertEquals(#available, 3) + testlib.assertEquals(available[1].name, "alpha") + testlib.assertEquals(available[1].status, "up-to-date") + testlib.assertEquals(available[2].name, "beta") + testlib.assertEquals(available[2].status, "updatable") + testlib.assertEquals(available[3].name, "gamma") + testlib.assertEquals(available[3].status, "available") +end) - local available = ccpm.available(); - testlib.assertEquals(#available, 3); - testlib.assertEquals(available[1].name, 'alpha'); - testlib.assertEquals(available[1].status, 'up-to-date'); - testlib.assertEquals(available[2].name, 'beta'); - testlib.assertEquals(available[2].status, 'updatable'); - testlib.assertEquals(available[3].name, 'gamma'); - testlib.assertEquals(available[3].status, 'available'); -end); +testlib.test("upgrade reinstalls outdated packages from cache", function() + local base = giteaBase("me/repo", "master") + local routes = { + [base .. "packages/foo/ccpm.json"] = textutils.serializeJSON({ + name = "foo", + version = "2.0.0", + dependencies = {}, + files = { "programs/foo.lua" }, + }), + [base .. "programs/foo.lua"] = "foo-v2", + } + local sd, root = freshDirs() + local ccpm = createCcpm({ stateDir = sd, installRoot = root, http = fakeHttp(routes) }) + ccpm.writeConfig({ registries = { { name = "me/repo", type = "gitea", branch = "master" } } }) + ccpm.writeCache({ packages = { foo = { version = "2.0.0", registry = "me/repo" } } }) + ccpm.writeLock({ + packages = { + foo = { version = "1.0.0", registry = "me/repo", files = { "programs/foo.lua" }, dependencies = {} }, + }, + }) -testlib.test('upgrade reinstalls outdated packages from cache', function() - local base = ghBase('me/repo', 'master'); - local routes = { - [base .. 'packages/foo/ccpm.json'] = textutils.serializeJSON({ name = 'foo', version = '2.0.0', dependencies = {}, files = { 'programs/foo.lua' } }), - [base .. 'programs/foo.lua'] = 'foo-v2', - }; - local sd, root = freshDirs(); - local ccpm = createCcpm({ stateDir = sd, installRoot = root, http = fakeHttp(routes) }); - ccpm.writeConfig({ registries = { { name = 'me/repo', type = 'github', branch = 'master' } } }); - ccpm.writeCache({ packages = { foo = { version = '2.0.0', registry = 'me/repo' } } }); - ccpm.writeLock({ packages = { foo = { version = '1.0.0', registry = 'me/repo', files = { 'programs/foo.lua' }, dependencies = {} } } }); + local ok, upgraded = ccpm.upgrade({}) + testlib.assertTrue(ok) + testlib.assertEquals(#upgraded, 1) + testlib.assertEquals(upgraded[1], "foo") + testlib.assertEquals(ccpm.readLock().packages.foo.version, "2.0.0") - local ok, upgraded = ccpm.upgrade({}); - testlib.assertTrue(ok); - testlib.assertEquals(#upgraded, 1); - testlib.assertEquals(upgraded[1], 'foo'); - testlib.assertEquals(ccpm.readLock().packages.foo.version, '2.0.0'); + local f = fs.open(root .. "/programs/foo.lua", "r") + local body = f.readAll() + f.close() + testlib.assertEquals(body, "foo-v2") +end) - local f = fs.open(root .. '/programs/foo.lua', 'r'); - local body = f.readAll(); - f.close(); - testlib.assertEquals(body, 'foo-v2'); -end); +testlib.test("upgrade requires a package cache", function() + local ccpm = createCcpm({ stateDir = freshDirs() }) + ccpm.writeLock({ packages = { foo = { version = "1.0.0", files = {}, dependencies = {} } } }) -testlib.test('upgrade requires a package cache', function() - local ccpm = createCcpm({ stateDir = freshDirs() }); - ccpm.writeLock({ packages = { foo = { version = '1.0.0', files = {}, dependencies = {} } } }); + local ok, err = ccpm.upgrade({}) + testlib.assertTrue(not ok) + testlib.assertTrue(string.find(err, "ccpm update", 1, true)) +end) - local ok, err = ccpm.upgrade({}); - testlib.assertTrue(not ok); - testlib.assertTrue(string.find(err, 'ccpm update', 1, true)); -end); +testlib.test("uninstall refuses a package with dependents", function() + local ccpm = createCcpm({ stateDir = freshDirs() }) + ccpm.writeLock({ + packages = { + ["trapos-core"] = { version = "1", files = {}, dependencies = {} }, + ["trapos-net"] = { version = "1", files = {}, dependencies = { "trapos-core" } }, + }, + }) + local ok, err = ccpm.uninstall("trapos-core", {}) + testlib.assertTrue(not ok) + testlib.assertTrue(string.find(err, "required by", 1, true)) + testlib.assertTrue(string.find(err, "trapos-net", 1, true)) +end) -testlib.test('uninstall refuses a package with dependents', function() - local ccpm = createCcpm({ stateDir = freshDirs() }); - ccpm.writeLock({ packages = { - ['trapos-core'] = { version = '1', files = {}, dependencies = {} }, - ['trapos-net'] = { version = '1', files = {}, dependencies = { 'trapos-core' } }, - } }); - local ok, err = ccpm.uninstall('trapos-core', {}); - testlib.assertTrue(not ok); - testlib.assertTrue(string.find(err, 'required by', 1, true)); - testlib.assertTrue(string.find(err, 'trapos-net', 1, true)); -end); +testlib.test("uninstall removes a leaf package and its files", function() + local sd, root = freshDirs() + fs.makeDir(root .. "/apis") + local f = fs.open(root .. "/apis/net.lua", "w") + f.write("x") + f.close() -testlib.test('uninstall removes a leaf package and its files', function() - local sd, root = freshDirs(); - fs.makeDir(root .. '/apis'); - local f = fs.open(root .. '/apis/net.lua', 'w'); - f.write('x'); - f.close(); + local ccpm = createCcpm({ stateDir = sd, installRoot = root }) + ccpm.writeLock({ + packages = { + ["trapos-core"] = { version = "1", files = {}, dependencies = {} }, + ["trapos-net"] = { version = "1", files = { "apis/net.lua" }, dependencies = { "trapos-core" } }, + }, + }) - local ccpm = createCcpm({ stateDir = sd, installRoot = root }); - ccpm.writeLock({ packages = { - ['trapos-core'] = { version = '1', files = {}, dependencies = {} }, - ['trapos-net'] = { version = '1', files = { 'apis/net.lua' }, dependencies = { 'trapos-core' } }, - } }); + testlib.assertTrue(ccpm.uninstall("trapos-net", {})) + testlib.assertTrue(not fs.exists(root .. "/apis/net.lua")) + testlib.assertTrue(ccpm.readLock().packages["trapos-net"] == nil) + testlib.assertTrue(ccpm.readLock().packages["trapos-core"] ~= nil) +end) - testlib.assertTrue(ccpm.uninstall('trapos-net', {})); - testlib.assertTrue(not fs.exists(root .. '/apis/net.lua')); - testlib.assertTrue(ccpm.readLock().packages['trapos-net'] == nil); - testlib.assertTrue(ccpm.readLock().packages['trapos-core'] ~= nil); -end); - -testlib.run(); +testlib.run()