feat(ccpm): install trapos from package cache

This commit is contained in:
Guillaume ARM 2026-06-09 00:09:58 +02:00
parent 51194b9866
commit 8ae947ac3d
12 changed files with 398 additions and 126 deletions

View File

@ -34,9 +34,9 @@ Use `docs/README.md` as the entrypoint for CC:Tweaked, CraftOS-PC, Advanced Peri
- `startup/servers.lua` starts `/programs`, the shell, and configured servers via `parallel.waitForAll`. - `startup/servers.lua` starts `/programs`, the shell, and configured servers via `parallel.waitForAll`.
- Preserve `periphemu` guards used for CraftOS-PC emulation; see `docs/craftos_pc_glossary.md` for upstream emulator references. - Preserve `periphemu` guards used for CraftOS-PC emulation; see `docs/craftos_pc_glossary.md` for upstream emulator references.
- TrapOS ships as packages, each described by `packages/<name>/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). See ADR-0010. - TrapOS ships as packages, each described by `packages/<name>/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.
- `install.lua` reads `manifest.json` (`packages` list) from `master` by default, or `next` with `--beta`; it resolves descriptors, downloads files, and writes `/trapos/manifest.json` (aggregated OS state for motd/servers/upgrade) plus the ccpm state files. `--core` installs only `tos-core`. - `install-ccpm.lua` is the one-time wget bootstrap. It installs only `tos-core`/`ccpm`, seeds the default `guillaumearm/cc-libs` registry on `master` (or `next` with `--beta`), and tells users to run `ccpm update` then `ccpm install trapos`.
- `ccpm` (in `tos-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) and `/trapos/ccpm.lock.json` (installed packages). Never use the word "manifest" in ccpm — it is reserved for the OS manifest. - `ccpm` (in `tos-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. - Add new servers to `startup/servers.lua` as needed.
## Conventions ## Conventions

View File

@ -4,26 +4,29 @@ A small in-game operating system for ComputerCraft / CC:Tweaked, built around a
## Installation ## Installation
Full install (all packages): Install `ccpm` first. This is the only step that needs `wget` with a URL:
``` ```
wget run https://raw.githubusercontent.com/guillaumearm/cc-libs/master/install.lua wget run https://raw.githubusercontent.com/guillaumearm/cc-libs/master/install-ccpm.lua
``` ```
Minimal install (only `tos-core`, i.e. just the package manager) so you can cherry-pick the rest yourself: Then sync the default registry (`guillaumearm/cc-libs`) and install TrapOS:
``` ```
wget run https://raw.githubusercontent.com/guillaumearm/cc-libs/master/install.lua --core ccpm update
ccpm install trapos
``` ```
Install individual packages instead if you want to cherry-pick:
``` ```
> ccpm install tos-net > ccpm install tos-net
> ccpm install tos-ui > ccpm install tos-ui
``` ```
Install the beta branch (one-time opt-in, asks for confirmation): Install `ccpm` from the beta branch (one-time opt-in, asks for confirmation):
``` ```
wget run https://raw.githubusercontent.com/guillaumearm/cc-libs/next/install.lua --beta wget run https://raw.githubusercontent.com/guillaumearm/cc-libs/next/install-ccpm.lua --beta
``` ```
Once a machine is on beta, `upgrade` keeps it on beta — `--beta` is not needed again. Use `upgrade --stable` to go back to the stable branch. Once `ccpm` is installed from beta, the default registry tracks `next`; `ccpm update` and `ccpm upgrade` keep using that branch.
After install, every boot shows a colored MOTD with the installed version and branch (lime for stable, orange + `[BETA]` for beta). After install, every boot shows a colored MOTD with the installed version and branch (lime for stable, orange + `[BETA]` for beta).
@ -36,27 +39,34 @@ TrapOS is split into packages, each described by a `packages/<name>/ccpm.json`:
- `tos-boot`: the startup MOTD and autostart server launcher. - `tos-boot`: the startup MOTD and autostart server launcher.
- `tos-net`: routed modem networking (`net`, `router`, `ping`, `ping-server`). - `tos-net`: routed modem networking (`net`, `router`, `ping`, `ping-server`).
- `tos-ui`: the terminal UI toolkit (`libtui`, `tuidemo`). - `tos-ui`: the terminal UI toolkit (`libtui`, `tuidemo`).
- `trapos`: full TrapOS meta-package (`tos-boot`, `tos-net`, `tos-ui`, and `tos-test` during beta).
`manifest.json` at the repo root lists which packages a full install ships. The The `trapos` meta-package is the user-facing full install. Package descriptors list
bootstrap resolves their descriptors, downloads the files, and writes the aggregated files and autostart servers; installed state is tracked under `/trapos`.
`/trapos/manifest.json` (version, branch, files, autostart) that drives `upgrade`,
`startup/motd.lua`, and `startup/servers.lua`.
## ccpm ## ccpm
`ccpm` is the TrapOS package manager (shipped in `tos-core`). It is configured at `ccpm` is the TrapOS package manager. `install-ccpm.lua` installs it by installing
install with a default registry (`guillaumearm/cc-libs`). the required `tos-core` package and configures the default registry
(`guillaumearm/cc-libs`).
``` ```
ccpm install <package> ccpm reinstall <package> ccpm uninstall <package> ccpm install <package> ccpm reinstall <package> ccpm uninstall <package>
ccpm ls ccpm search [term] ccpm info <package> ccpm update ccpm upgrade
ccpm ls ccpm available [term] ccpm search [term]
ccpm info <package>
ccpm registry ls ccpm registry add <name> [--branch <b>] [--type github|http] ccpm registry ls ccpm registry add <name> [--branch <b>] [--type github|http]
ccpm registry rm <name> ccpm registry rm <name>
``` ```
`ccpm update` fetches registry package indexes into `/trapos/ccpm.cache.json`.
`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 GitHub (`owner/repo`); `http`/`https` base URLs are also
supported. State lives in `/trapos/ccpm.json` (registries) and `/trapos/ccpm.lock.json` supported. State lives in `/trapos/ccpm.json` (registries),
(installed packages). See [ADR-0010](docs/adrs/adr-0010-ccpm-package-manager.md). `/trapos/ccpm.lock.json` (installed packages), and `/trapos/ccpm.cache.json`
(available packages). See [ADR-0010](docs/adrs/adr-0010-ccpm-package-manager.md).
## APIs ## APIs
- `/apis/eventloop`: a simple event loop API. - `/apis/eventloop`: a simple event loop API.
@ -71,7 +81,7 @@ Servers listed in `manifest.autostart` are launched at boot by `startup/servers.
- `router`: routes messages. You need to set up a router to use all `apis/net`-based programs and libraries. - `router`: routes messages. You need to set up a router to use all `apis/net`-based programs and libraries.
- `ping`: pings machines using `apis/net`. - `ping`: pings machines using `apis/net`.
- `events`: emits and logs computer events. - `events`: emits and logs computer events.
- `upgrade`: upgrades the machine. Reads `/trapos/manifest.json` to stay on the current branch; use `--beta` to opt in or `--stable` to opt out. - `upgrade`: alias for `ccpm upgrade`.
## Development ## Development
See [DEVELOPMENT.md](./DEVELOPMENT.md) for development setup and workflow. See [DEVELOPMENT.md](./DEVELOPMENT.md) for development setup and workflow.

View File

@ -1,4 +1,4 @@
local _VERSION = '0.1.0'; 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).
-- --
@ -8,6 +8,7 @@ local _VERSION = '0.1.0';
-- - ccpm.json -> { registries = { { name, type, branch }, ... } } -- - ccpm.json -> { registries = { { name, type, branch }, ... } }
-- - ccpm.lock.json -> { packages = { <name> = { version, registry, files, -- - ccpm.lock.json -> { packages = { <name> = { version, registry, files,
-- dependencies, autostart } } } -- dependencies, autostart } } }
-- - ccpm.cache.json -> { packages = { <name> = { version, registry } } }
-- --
-- Files are written under `opts.installRoot` (default '' -> filesystem root), so -- Files are written under `opts.installRoot` (default '' -> filesystem root), so
-- tests can sandbox downloads. `opts.http` overrides the global `http` for tests. -- tests can sandbox downloads. `opts.http` overrides the global `http` for tests.
@ -34,6 +35,8 @@ local function createCcpm(opts)
local configPath = stateDir .. '/ccpm.json'; local configPath = stateDir .. '/ccpm.json';
local lockPath = stateDir .. '/ccpm.lock.json'; local lockPath = stateDir .. '/ccpm.lock.json';
local cachePath = stateDir .. '/ccpm.cache.json';
local manifestPath = stateDir .. '/manifest.json';
local api = {}; local api = {};
@ -126,6 +129,49 @@ local function createCcpm(opts)
return api.readLock().packages or {}; return api.readLock().packages or {};
end end
local function writeOsState(lock)
local files = {};
local seenFile = {};
local autostart = {};
local seenAuto = {};
for _, entry in pairs(lock.packages or {}) do
for _, filePath in ipairs(entry.files or {}) do
if not seenFile[filePath] then
seenFile[filePath] = true;
files[#files + 1] = filePath;
end
end
for _, server in ipairs(entry.autostart or {}) do
if not seenAuto[server] then
seenAuto[server] = true;
autostart[#autostart + 1] = server;
end
end
end
table.sort(files);
table.sort(autostart);
local cfg = api.readConfig();
local registry = cfg.registries and cfg.registries[1] or {};
local trapos = lock.packages and lock.packages.trapos;
return writeJsonFile(manifestPath, {
name = 'TrapOS',
version = trapos and trapos.version or '?',
branch = registry.branch or 'master',
files = files,
autostart = autostart,
});
end
-- ---------- cache (available packages) ----------
function api.readCache()
return readJsonFile(cachePath) or { packages = {} };
end
function api.writeCache(cache)
return writeJsonFile(cachePath, cache);
end
-- ---------- URL resolution ---------- -- ---------- URL resolution ----------
function api.registryBaseUrl(registry) function api.registryBaseUrl(registry)
@ -182,18 +228,83 @@ local function createCcpm(opts)
end end
function api.search(term) function api.search(term)
local cfg = api.readConfig();
local results = {}; local results = {};
local cache = api.readCache();
for name, entry in pairs(cache.packages or {}) do
if not term or term == '' or string.find(name, term, 1, true) then
results[#results + 1] = { name = name, version = entry.version, registry = entry.registry };
end
end
table.sort(results, function(a, b) return a.name < b.name; end);
return results;
end
function api.update()
local cfg = api.readConfig();
local cache = { packages = {} };
for _, registry in ipairs(cfg.registries or {}) do for _, registry in ipairs(cfg.registries or {}) do
local index = api.fetchJson(api.indexUrl(registry)); local index = api.fetchJson(api.indexUrl(registry));
if index and index.packages then if index and index.packages then
for name, version in pairs(index.packages) do for name, version in pairs(index.packages) do
if not term or term == '' or string.find(name, term, 1, true) then if not cache.packages[name] then
results[#results + 1] = { name = name, version = version, registry = registry.name }; cache.packages[name] = { version = version, registry = registry.name };
end end
end end
end end
end end
api.writeCache(cache);
return cache;
end
local function compareVersions(a, b)
a = tostring(a or '');
b = tostring(b or '');
if a == b then return 0; end
local aparts = {};
local bparts = {};
for part in string.gmatch(a, '[^%.]+') do aparts[#aparts + 1] = part; end
for part in string.gmatch(b, '[^%.]+') do bparts[#bparts + 1] = part; end
local max = math.max(#aparts, #bparts);
local hitBreak = false;
for i = 1, max do
local apart = aparts[i] or '0';
local bpart = bparts[i] or '0';
local anum = tonumber(apart);
local bnum = tonumber(bpart);
if not anum or not bnum then hitBreak = true; break; end
if anum < bnum then return -1; end
if anum > bnum then return 1; end
end
if not hitBreak then return 0; end
if a < b then return -1; end
return 1;
end
function api.available(term)
local cache = api.readCache();
local installed = api.list();
local results = {};
for name, entry in pairs(cache.packages or {}) do
if not term or term == '' or string.find(name, term, 1, true) then
local status = 'available';
local installedVersion = nil;
if installed[name] then
installedVersion = installed[name].version;
if compareVersions(installedVersion, entry.version) < 0 then
status = 'updatable';
else
status = 'up-to-date';
end
end
results[#results + 1] = {
name = name,
version = entry.version,
installedVersion = installedVersion,
registry = entry.registry,
status = status,
};
end
end
table.sort(results, function(a, b) return a.name < b.name; end); table.sort(results, function(a, b) return a.name < b.name; end);
return results; return results;
end end
@ -296,9 +407,36 @@ local function createCcpm(opts)
end end
api.writeLock(lock); api.writeLock(lock);
writeOsState(lock);
return true, lock.packages[pkg]; return true, lock.packages[pkg];
end end
function api.upgrade(upgradeOpts)
upgradeOpts = upgradeOpts or {};
local log = upgradeOpts.log or function() end;
local lock = api.readLock();
local cache = api.readCache();
if not cache.packages or not next(cache.packages) then
return false, "package cache is empty, run 'ccpm update' first.";
end
local names = {};
for name, entry in pairs(lock.packages or {}) do
local cached = cache.packages[name];
if cached and compareVersions(entry.version, cached.version) < 0 then
names[#names + 1] = name;
end
end
table.sort(names);
for _, name in ipairs(names) do
log('upgrade ' .. name);
local ok, err = api.install(name, { force = true, log = log });
if not ok then return false, err; end
end
return true, names;
end
-- uninstallOpts: { force = bool, log = function(msg) } -- uninstallOpts: { force = bool, log = function(msg) }
function api.uninstall(pkg, uninstallOpts) function api.uninstall(pkg, uninstallOpts)
uninstallOpts = uninstallOpts or {}; uninstallOpts = uninstallOpts or {};
@ -330,6 +468,7 @@ local function createCcpm(opts)
end end
lock.packages[pkg] = nil; lock.packages[pkg] = nil;
api.writeLock(lock); api.writeLock(lock);
writeOsState(lock);
return true; return true;
end end

View File

@ -15,10 +15,11 @@ file list from a branch and `wget`s every file. That is all-or-nothing. There is
way to install just networking or just the UI, and no way to add or remove pieces of way to install just networking or just the UI, and no way to add or remove pieces of
the OS after the initial install. the OS after the initial install.
We want a package manager, `ccpm` ("ComputerCraft Package Manager"), shipped as part We want a package manager, `ccpm` ("ComputerCraft Package Manager"), installed first
of the base OS, so a machine can `ccpm install tos-net`, `ccpm uninstall tos-ui`, and as a standalone user-facing step. After that, a machine can `ccpm update`,
manage where packages come from. TrapOS itself is **not** becoming a package for now; `ccpm install trapos`, `ccpm install tos-net`, `ccpm uninstall tos-ui`, and manage
the `wget run .../install.lua` bootstrap stays the recommended entry point. where packages come from. TrapOS itself is installed through a `trapos` meta-package;
the `wget run .../install-ccpm.lua` bootstrap exists only to install `ccpm`.
## Decision ## Decision
@ -40,6 +41,7 @@ The split is finer-grained than the install examples imply:
| tos-boot | motd, servers (startup) | tos-core | | tos-boot | motd, servers (startup) | tos-core |
| tos-net | net, router, ping, ping-server | tos-core | | tos-net | net, router, ping, ping-server | tos-core |
| tos-ui | libtui, tuidemo | tos-core | | tos-ui | libtui, tuidemo | tos-core |
| trapos | full TrapOS meta-package | tos-boot, tos-net, tos-ui, tos-test |
### Two files for ccpm, "manifest" reserved for the OS ### Two files for ccpm, "manifest" reserved for the OS
@ -51,32 +53,37 @@ Local state lives under `/trapos`:
`http`/`https` (the `name` is a base URL). `http`/`https` (the `name` is a base URL).
- `ccpm.lock.json` — installed packages `{ packages = { <name> = { version, registry, - `ccpm.lock.json` — installed packages `{ packages = { <name> = { version, registry,
files, dependencies, autostart } } }`, used by `ls`, `uninstall`, and `reinstall`. files, dependencies, autostart } } }`, used by `ls`, `uninstall`, and `reinstall`.
- `ccpm.cache.json` — packages advertised by configured registries, written by
`ccpm update` from each registry's `packages/index.json`, used by `ccpm search`,
`ccpm available`, and `ccpm upgrade`.
`apis/libccpm.lua` is the testable core (a factory; `http`/`stateDir`/`installRoot` `apis/libccpm.lua` is the testable core (a factory; `http`/`stateDir`/`installRoot`
are injectable for tests). `programs/ccpm.lua` is a thin CLI over it. are injectable for tests). `programs/ccpm.lua` is a thin CLI over it.
### The bootstrap is package-aware ### The bootstrap installs only ccpm
`manifest.json` now lists `packages` instead of `files`. `install.lua` resolves each `install-ccpm.lua` resolves only the `tos-core` package descriptor (pulling any future
package descriptor (pulling dependencies), downloads the union of their files, and dependencies), downloads its files, and writes:
writes:
- `/trapos/manifest.json` — the aggregated `{ name, version, branch, files, autostart }` - `/trapos/manifest.json` — the aggregated `{ name, version, branch, files, autostart }`
still consumed verbatim by `startup/motd.lua`, `startup/servers.lua`, and still consumed by `startup/motd.lua` and `startup/servers.lua` after boot packages
`programs/upgrade.lua` (those three are unchanged); are installed;
- `/trapos/ccpm.lock.json` — so right after a fresh install `ccpm install tos-core` - `/trapos/ccpm.lock.json` — so right after a fresh install `ccpm install tos-core`
correctly reports "already installed"; correctly reports "already installed";
- `/trapos/ccpm.json` — seeding/refreshing the default `guillaumearm/cc-libs` registry - `/trapos/ccpm.json` — seeding/refreshing the default `guillaumearm/cc-libs` registry
to track the install branch. to track the install branch.
Two install paths follow: The install path is:
- `wget run .../install.lua` — full OS (all packages in `manifest.packages`). - `wget run .../install-ccpm.lua` — install `ccpm` (`tos-core`) and seed the default
- `wget run .../install.lua --core` — only `tos-core` (i.e. just `ccpm`); the user then registry.
cherry-picks with `ccpm install ...`. - `ccpm update` — refresh the local package cache.
- `ccpm install trapos` — install the full OS. During beta, `trapos` includes
`tos-test` by default.
On a subsequent `upgrade`, `install.lua` prefers the existing lockfile's package set On a subsequent `upgrade`, `programs/upgrade.lua` delegates to `ccpm upgrade`, which
over `manifest.packages`, so a cherry-picked machine upgrades only what it actually has. upgrades installed packages using `/trapos/ccpm.cache.json`. Users run `ccpm update`
first to refresh available versions.
## Consequences ## Consequences
@ -85,11 +92,11 @@ over `manifest.packages`, so a cherry-picked machine upgrades only what it actua
`.packages`); it mounts a fixed list of top-level dirs instead. `just test` was `.packages`); it mounts a fixed list of top-level dirs instead. `just test` was
already on fixed mounts and is unaffected. already on fixed mounts and is unaffected.
- ccpm logic is covered by `tests/ccpm.lua` (URL resolution, dependency ordering, - ccpm logic is covered by `tests/ccpm.lua` (URL resolution, dependency ordering,
cycle/missing detection, already-installed, registry CRUD, uninstall dependency cycle/missing detection, already-installed, registry CRUD, cache update, available
guard) with an injected `http` stub — no network in tests. status, upgrade, uninstall dependency guard) with an injected `http` stub — no
network in tests.
## Future Work ## Future Work
- Version ranges / `ccpm update` (today a single pinned version per package). - Version ranges (today a single pinned version per package).
- Making TrapOS self-update through ccpm rather than the `wget run` bootstrap.
- http/https registries beyond a plain base URL (auth, caching). - http/https registries beyond a plain base URL (auth, caching).

View File

@ -1,4 +1,4 @@
local _VERSION = '4.0.0'; local _VERSION = '5.0.0';
local REPO_BASE = 'https://raw.githubusercontent.com/guillaumearm/cc-libs/'; local REPO_BASE = 'https://raw.githubusercontent.com/guillaumearm/cc-libs/';
local LOCAL_STATE_DIR = '/trapos'; local LOCAL_STATE_DIR = '/trapos';
@ -8,28 +8,27 @@ local LOCAL_LOCK_PATH = '/trapos/ccpm.lock.json';
local DEFAULT_REGISTRY_NAME = 'guillaumearm/cc-libs'; local DEFAULT_REGISTRY_NAME = 'guillaumearm/cc-libs';
local function printUsage() local function printUsage()
print('install usage:'); print('install-ccpm usage:');
print(); print();
print('\t\twget run <install-url>'); print('\t\twget run <install-url>');
print('\t\twget run <install-url> --core');
print('\t\twget run <install-url> --beta'); print('\t\twget run <install-url> --beta');
print('\t\twget run <install-url> --stable'); print('\t\twget run <install-url> --stable');
end end
local function readJsonFile(path) local function readJsonFile(path)
if not fs.exists(path) then return nil end if not fs.exists(path) then return nil; end
local f = fs.open(path, 'r'); local f = fs.open(path, 'r');
if not f then return nil end if not f then return nil; end
local data = f.readAll(); local data = f.readAll();
f.close(); f.close();
if not data or data == '' then return nil end if not data or data == '' then return nil; end
return textutils.unserializeJSON(data); return textutils.unserializeJSON(data);
end end
local function writeJsonFile(path, value) local function writeJsonFile(path, value)
fs.makeDir(LOCAL_STATE_DIR); fs.makeDir(LOCAL_STATE_DIR);
local f = fs.open(path, 'w'); local f = fs.open(path, 'w');
if not f then return false end if not f then return false; end
f.write(textutils.serializeJSON(value)); f.write(textutils.serializeJSON(value));
f.close(); f.close();
return true; return true;
@ -41,7 +40,7 @@ local function confirmBeta()
print('Beta builds may be unstable. Continue? (y/N)'); print('Beta builds may be unstable. Continue? (y/N)');
write('> '); write('> ');
local answer = read(); local answer = read();
if not answer then return false end if not answer then return false; end
answer = answer:lower(); answer = answer:lower();
return answer == 'y' or answer == 'yes'; return answer == 'y' or answer == 'yes';
end end
@ -114,12 +113,12 @@ local function seedCcpmConfig(branch)
end end
local rawArgs = table.pack(...); local rawArgs = table.pack(...);
local forceBeta, forceStable, forceCore = false, false, false; local forceBeta, forceStable = false, false;
for i = 1, rawArgs.n do for i = 1, rawArgs.n do
local a = rawArgs[i]; local a = rawArgs[i];
if a == 'version' or a == '-version' or a == '--version' then if a == 'version' or a == '-version' or a == '--version' then
print('install v' .. _VERSION); print('install-ccpm v' .. _VERSION);
return; return;
elseif a == 'help' or a == '-help' or a == '--help' then elseif a == 'help' or a == '-help' or a == '--help' then
printUsage(); printUsage();
@ -128,8 +127,6 @@ for i = 1, rawArgs.n do
forceBeta = true; forceBeta = true;
elseif a == '--stable' or a == '-stable' then elseif a == '--stable' or a == '-stable' then
forceStable = true; forceStable = true;
elseif a == '--core' or a == '-core' then
forceCore = true;
elseif a ~= nil and a ~= '' then elseif a ~= nil and a ~= '' then
printUsage(); printUsage();
return; return;
@ -161,28 +158,7 @@ if not manifest then
return; return;
end end
-- Decide which packages to install: local requested = { 'tos-core' };
-- --core -> only tos-core (the package manager bootstrap)
-- existing lockfile -> whatever is already installed (upgrade in place)
-- otherwise -> the full set listed in manifest.packages
local requested;
if forceCore then
requested = { 'tos-core' };
else
local lock = readJsonFile(LOCAL_LOCK_PATH);
if lock and type(lock.packages) == 'table' and next(lock.packages) then
requested = {};
for name in pairs(lock.packages) do requested[#requested + 1] = name end
table.sort(requested);
else
requested = manifest.packages;
end
end
if type(requested) ~= 'table' or #requested == 0 then
print('No packages to install (manifest.packages missing?)');
return;
end
local resolved, resolveErr = resolvePackages(branch, requested); local resolved, resolveErr = resolvePackages(branch, requested);
if not resolved then if not resolved then
@ -255,9 +231,10 @@ writeJsonFile(LOCAL_LOCK_PATH, { packages = lockPackages });
seedCcpmConfig(branch); seedCcpmConfig(branch);
print(); print();
print('=> TrapOS v' .. (manifest.version or '?') .. ' installed (branch: ' .. branch .. ')'); print('=> ccpm installed (branch: ' .. branch .. ')');
print('=> Packages: ' .. table.concat(requested, ', ')); print('=> Default registry: ' .. DEFAULT_REGISTRY_NAME);
print('=> Execute startup/servers.lua'); print('=> Run: ccpm update');
print('=> Run: ccpm install trapos');
shell.execute('/startup/servers.lua'); shell.execute('/startup/servers.lua');
shell.setDir(previousDir); shell.setDir(previousDir);

View File

@ -3,10 +3,6 @@
"version": "0.4.0", "version": "0.4.0",
"branch": "next", "branch": "next",
"packages": [ "packages": [
"tos-core", "trapos"
"tos-test",
"tos-boot",
"tos-net",
"tos-ui"
] ]
} }

View File

@ -1,9 +1,10 @@
{ {
"packages": { "packages": {
"tos-core": "0.1.0", "tos-core": "0.2.0",
"tos-test": "0.1.0", "tos-test": "0.1.0",
"tos-boot": "0.1.0", "tos-boot": "0.1.0",
"tos-net": "0.1.0", "tos-net": "0.1.0",
"tos-ui": "0.1.0" "tos-ui": "0.1.0",
"trapos": "0.4.0"
} }
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "tos-core", "name": "tos-core",
"version": "0.1.0", "version": "0.2.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": [

View File

@ -0,0 +1,8 @@
{
"name": "trapos",
"version": "0.4.0",
"description": "TrapOS full install meta-package",
"dependencies": ["tos-boot", "tos-net", "tos-ui", "tos-test"],
"files": [],
"autostart": []
}

View File

@ -1,4 +1,4 @@
local _VERSION = '0.1.0'; local _VERSION = '0.2.0';
local createCcpm = require('/apis/libccpm'); local createCcpm = require('/apis/libccpm');
@ -11,7 +11,10 @@ local function printUsage()
print('\t\tccpm install <package>'); print('\t\tccpm install <package>');
print('\t\tccpm reinstall <package>'); print('\t\tccpm reinstall <package>');
print('\t\tccpm uninstall <package>'); print('\t\tccpm uninstall <package>');
print('\t\tccpm update');
print('\t\tccpm upgrade');
print('\t\tccpm ls'); print('\t\tccpm ls');
print('\t\tccpm available [term]');
print('\t\tccpm search [term]'); print('\t\tccpm search [term]');
print('\t\tccpm info <package>'); print('\t\tccpm info <package>');
print('\t\tccpm registry ls'); print('\t\tccpm registry ls');
@ -37,6 +40,32 @@ local function logLine(msg)
print(msg); print(msg);
end end
local function printColored(msg, color)
if term.isColor and term.isColor() then
local previous = term.getTextColor();
term.setTextColor(color);
print(msg);
term.setTextColor(previous);
else
print(msg);
end
end
local function printAvailableRow(r)
local installed = '';
if r.installedVersion then
installed = ' installed v' .. tostring(r.installedVersion);
end
local line = r.name .. ' v' .. tostring(r.version) .. ' (' .. r.registry .. ') ' .. r.status .. installed;
if r.status == 'up-to-date' then
printColored(line, colors.lime);
elseif r.status == 'updatable' then
printColored(line, colors.orange);
else
print(line);
end
end
if command == 'install' or command == 'reinstall' then if command == 'install' or command == 'reinstall' then
local pkg = args[2]; local pkg = args[2];
if not pkg then printUsage(); return; end if not pkg then printUsage(); return; end
@ -49,6 +78,28 @@ if command == 'install' or command == 'reinstall' then
return; return;
end end
if command == 'update' then
local cache = ccpm.update();
local count = 0;
for _ in pairs(cache.packages or {}) do count = count + 1; end
print('=> package cache updated (' .. count .. ' packages).');
return;
end
if command == 'upgrade' then
local ok, result = ccpm.upgrade({ log = logLine });
if not ok then
print(result);
return;
end
if #result == 0 then
print('=> all packages up-to-date.');
else
print('=> upgraded: ' .. table.concat(result, ', '));
end
return;
end
if command == 'uninstall' or command == 'remove' or command == 'rm' then if command == 'uninstall' or command == 'remove' or command == 'rm' then
local pkg = args[2]; local pkg = args[2];
if not pkg then printUsage(); return; end if not pkg then printUsage(); return; end
@ -88,6 +139,18 @@ if command == 'search' then
return; return;
end end
if command == 'available' then
local results = ccpm.available(args[2]);
if #results == 0 then
print("No packages found. Run 'ccpm update' first if the cache is empty.");
return;
end
for _, r in ipairs(results) do
printAvailableRow(r);
end
return;
end
if command == 'info' then if command == 'info' then
local pkg = args[2]; local pkg = args[2];
if not pkg then printUsage(); return; end if not pkg then printUsage(); return; end

View File

@ -1,29 +1,13 @@
local _VERSION = '1.4.0'; local _VERSION = '2.0.0';
local REPO_BASE = 'https://raw.githubusercontent.com/guillaumearm/cc-libs/';
local LOCAL_MANIFEST_PATH = '/trapos/manifest.json';
local function printUsage() local function printUsage()
print('upgrade usage:'); print('upgrade usage:');
print(); print();
print('\t\tupgrade'); print('\t\tupgrade');
print('\t\tupgrade --beta');
print('\t\tupgrade --stable');
print('\t\tupgrade version'); print('\t\tupgrade version');
print('\t\tupgrade help'); print('\t\tupgrade help');
end end
local function readLocalBranch()
if not fs.exists(LOCAL_MANIFEST_PATH) then return nil end
local f = fs.open(LOCAL_MANIFEST_PATH, 'r');
if not f then return nil end
local data = f.readAll();
f.close();
if not data or data == '' then return nil end
local manifest = textutils.unserializeJSON(data);
return manifest and manifest.branch or nil;
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
@ -36,26 +20,9 @@ if command == 'help' or command == '-help' or command == '--help' then
return; return;
end end
local branch; if command ~= nil and command ~= '' then
local extraFlag;
if command == '--beta' or command == '-beta' then
branch = 'next';
extraFlag = '--beta';
elseif command == '--stable' or command == '-stable' then
branch = 'master';
extraFlag = '--stable';
elseif command ~= nil and command ~= '' then
printUsage(); printUsage();
return; return;
else
branch = readLocalBranch() or 'master';
end end
local installUrl = REPO_BASE .. branch .. '/install.lua'; shell.execute('ccpm', 'upgrade');
if extraFlag then
shell.execute('wget', 'run', installUrl, extraFlag);
else
shell.execute('wget', 'run', installUrl);
end

View File

@ -128,6 +128,29 @@ testlib.test('install downloads files and records the lock', function()
testlib.assertEquals(lock.packages['tos-net'].registry, 'me/repo'); testlib.assertEquals(lock.packages['tos-net'].registry, 'me/repo');
end); end);
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 = { 'tos-net' }, files = {} }),
[base .. 'packages/tos-core/ccpm.json'] = textutils.serializeJSON({ name = 'tos-core', version = '1', dependencies = {}, files = { 'programs/ccpm.lua' } }),
[base .. 'packages/tos-net/ccpm.json'] = textutils.serializeJSON({ name = 'tos-net', version = '1', dependencies = { 'tos-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', {}));
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() testlib.test('registry add and remove round-trip', function()
local ccpm = createCcpm({ stateDir = freshDirs() }); local ccpm = createCcpm({ stateDir = freshDirs() });
ccpm.writeConfig({ registries = {} }); ccpm.writeConfig({ registries = {} });
@ -148,6 +171,87 @@ testlib.test('registry add and remove round-trip', function()
testlib.assertTrue(not rmOk); testlib.assertTrue(not rmOk);
end); 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 = 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 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 = {} },
} });
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 = 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 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 = {} } } });
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() testlib.test('uninstall refuses a package with dependents', function()
local ccpm = createCcpm({ stateDir = freshDirs() }); local ccpm = createCcpm({ stateDir = freshDirs() });
ccpm.writeLock({ packages = { ccpm.writeLock({ packages = {