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`.
- 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.
- `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`.
- `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.
- 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-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), `/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.
## Conventions

View File

@ -4,26 +4,29 @@ A small in-game operating system for ComputerCraft / CC:Tweaked, built around a
## 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-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).
@ -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-net`: routed modem networking (`net`, `router`, `ping`, `ping-server`).
- `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
bootstrap resolves their descriptors, downloads the files, and writes the aggregated
`/trapos/manifest.json` (version, branch, files, autostart) that drives `upgrade`,
`startup/motd.lua`, and `startup/servers.lua`.
The `trapos` meta-package is the user-facing full install. Package descriptors list
files and autostart servers; installed state is tracked under `/trapos`.
## ccpm
`ccpm` is the TrapOS package manager (shipped in `tos-core`). It is configured at
install with a default registry (`guillaumearm/cc-libs`).
`ccpm` is the TrapOS package manager. `install-ccpm.lua` installs it by installing
the required `tos-core` package and configures the default registry
(`guillaumearm/cc-libs`).
```
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 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
supported. State lives in `/trapos/ccpm.json` (registries) and `/trapos/ccpm.lock.json`
(installed packages). See [ADR-0010](docs/adrs/adr-0010-ccpm-package-manager.md).
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).
## APIs
- `/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.
- `ping`: pings machines using `apis/net`.
- `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
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).
--
@ -8,6 +8,7 @@ local _VERSION = '0.1.0';
-- - ccpm.json -> { registries = { { name, type, branch }, ... } }
-- - ccpm.lock.json -> { packages = { <name> = { version, registry, files,
-- dependencies, autostart } } }
-- - ccpm.cache.json -> { packages = { <name> = { version, registry } } }
--
-- Files are written under `opts.installRoot` (default '' -> filesystem root), so
-- 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 lockPath = stateDir .. '/ccpm.lock.json';
local cachePath = stateDir .. '/ccpm.cache.json';
local manifestPath = stateDir .. '/manifest.json';
local api = {};
@ -126,6 +129,49 @@ local function createCcpm(opts)
return api.readLock().packages or {};
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 ----------
function api.registryBaseUrl(registry)
@ -182,18 +228,83 @@ local function createCcpm(opts)
end
function api.search(term)
local cfg = api.readConfig();
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
local index = api.fetchJson(api.indexUrl(registry));
if index and index.packages then
for name, version in pairs(index.packages) do
if not term or term == '' or string.find(name, term, 1, true) then
results[#results + 1] = { name = name, version = version, registry = registry.name };
if not cache.packages[name] then
cache.packages[name] = { version = version, registry = registry.name };
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);
return results;
end
@ -296,9 +407,36 @@ local function createCcpm(opts)
end
api.writeLock(lock);
writeOsState(lock);
return true, lock.packages[pkg];
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) }
function api.uninstall(pkg, uninstallOpts)
uninstallOpts = uninstallOpts or {};
@ -330,6 +468,7 @@ local function createCcpm(opts)
end
lock.packages[pkg] = nil;
api.writeLock(lock);
writeOsState(lock);
return true;
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
the OS after the initial install.
We want a package manager, `ccpm` ("ComputerCraft Package Manager"), shipped as part
of the base OS, so a machine can `ccpm install tos-net`, `ccpm uninstall tos-ui`, and
manage where packages come from. TrapOS itself is **not** becoming a package for now;
the `wget run .../install.lua` bootstrap stays the recommended entry point.
We want a package manager, `ccpm` ("ComputerCraft Package Manager"), installed first
as a standalone user-facing step. After that, a machine can `ccpm update`,
`ccpm install trapos`, `ccpm install tos-net`, `ccpm uninstall tos-ui`, and manage
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
@ -40,6 +41,7 @@ The split is finer-grained than the install examples imply:
| tos-boot | motd, servers (startup) | tos-core |
| tos-net | net, router, ping, ping-server | 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
@ -51,32 +53,37 @@ Local state lives under `/trapos`:
`http`/`https` (the `name` is a base URL).
- `ccpm.lock.json` — installed packages `{ packages = { <name> = { version, registry,
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`
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
package descriptor (pulling dependencies), downloads the union of their files, and
writes:
`install-ccpm.lua` resolves only the `tos-core` package descriptor (pulling any future
dependencies), downloads its files, and writes:
- `/trapos/manifest.json` — the aggregated `{ name, version, branch, files, autostart }`
still consumed verbatim by `startup/motd.lua`, `startup/servers.lua`, and
`programs/upgrade.lua` (those three are unchanged);
still consumed by `startup/motd.lua` and `startup/servers.lua` after boot packages
are installed;
- `/trapos/ccpm.lock.json` — so right after a fresh install `ccpm install tos-core`
correctly reports "already installed";
- `/trapos/ccpm.json` — seeding/refreshing the default `guillaumearm/cc-libs` registry
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.lua --core` — only `tos-core` (i.e. just `ccpm`); the user then
cherry-picks with `ccpm install ...`.
- `wget run .../install-ccpm.lua` — install `ccpm` (`tos-core`) and seed the default
registry.
- `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
over `manifest.packages`, so a cherry-picked machine upgrades only what it actually has.
On a subsequent `upgrade`, `programs/upgrade.lua` delegates to `ccpm upgrade`, which
upgrades installed packages using `/trapos/ccpm.cache.json`. Users run `ccpm update`
first to refresh available versions.
## 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
already on fixed mounts and is unaffected.
- ccpm logic is covered by `tests/ccpm.lua` (URL resolution, dependency ordering,
cycle/missing detection, already-installed, registry CRUD, uninstall dependency
guard) with an injected `http` stub — no network in tests.
cycle/missing detection, already-installed, registry CRUD, cache update, available
status, upgrade, uninstall dependency guard) with an injected `http` stub — no
network in tests.
## Future Work
- Version ranges / `ccpm update` (today a single pinned version per package).
- Making TrapOS self-update through ccpm rather than the `wget run` bootstrap.
- Version ranges (today a single pinned version per package).
- 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 LOCAL_STATE_DIR = '/trapos';
@ -8,28 +8,27 @@ local LOCAL_LOCK_PATH = '/trapos/ccpm.lock.json';
local DEFAULT_REGISTRY_NAME = 'guillaumearm/cc-libs';
local function printUsage()
print('install usage:');
print('install-ccpm usage:');
print();
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> --stable');
end
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');
if not f then return nil end
if not f then return nil; end
local data = f.readAll();
f.close();
if not data or data == '' then return nil end
if not data or data == '' then return nil; end
return textutils.unserializeJSON(data);
end
local function writeJsonFile(path, value)
fs.makeDir(LOCAL_STATE_DIR);
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.close();
return true;
@ -41,7 +40,7 @@ local function confirmBeta()
print('Beta builds may be unstable. Continue? (y/N)');
write('> ');
local answer = read();
if not answer then return false end
if not answer then return false; end
answer = answer:lower();
return answer == 'y' or answer == 'yes';
end
@ -114,12 +113,12 @@ local function seedCcpmConfig(branch)
end
local rawArgs = table.pack(...);
local forceBeta, forceStable, forceCore = false, false, false;
local forceBeta, forceStable = false, false;
for i = 1, rawArgs.n do
local a = rawArgs[i];
if a == 'version' or a == '-version' or a == '--version' then
print('install v' .. _VERSION);
print('install-ccpm v' .. _VERSION);
return;
elseif a == 'help' or a == '-help' or a == '--help' then
printUsage();
@ -128,8 +127,6 @@ for i = 1, rawArgs.n do
forceBeta = true;
elseif a == '--stable' or a == '-stable' then
forceStable = true;
elseif a == '--core' or a == '-core' then
forceCore = true;
elseif a ~= nil and a ~= '' then
printUsage();
return;
@ -161,28 +158,7 @@ if not manifest then
return;
end
-- Decide which packages to install:
-- --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 requested = { 'tos-core' };
local resolved, resolveErr = resolvePackages(branch, requested);
if not resolved then
@ -255,9 +231,10 @@ writeJsonFile(LOCAL_LOCK_PATH, { packages = lockPackages });
seedCcpmConfig(branch);
print();
print('=> TrapOS v' .. (manifest.version or '?') .. ' installed (branch: ' .. branch .. ')');
print('=> Packages: ' .. table.concat(requested, ', '));
print('=> Execute startup/servers.lua');
print('=> ccpm installed (branch: ' .. branch .. ')');
print('=> Default registry: ' .. DEFAULT_REGISTRY_NAME);
print('=> Run: ccpm update');
print('=> Run: ccpm install trapos');
shell.execute('/startup/servers.lua');
shell.setDir(previousDir);

View File

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

View File

@ -1,9 +1,10 @@
{
"packages": {
"tos-core": "0.1.0",
"tos-core": "0.2.0",
"tos-test": "0.1.0",
"tos-boot": "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",
"version": "0.1.0",
"version": "0.2.0",
"description": "TrapOS base: package manager, event loop, upgrade and event tools",
"dependencies": [],
"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');
@ -11,7 +11,10 @@ local function printUsage()
print('\t\tccpm install <package>');
print('\t\tccpm reinstall <package>');
print('\t\tccpm uninstall <package>');
print('\t\tccpm update');
print('\t\tccpm upgrade');
print('\t\tccpm ls');
print('\t\tccpm available [term]');
print('\t\tccpm search [term]');
print('\t\tccpm info <package>');
print('\t\tccpm registry ls');
@ -37,6 +40,32 @@ local function logLine(msg)
print(msg);
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
local pkg = args[2];
if not pkg then printUsage(); return; end
@ -49,6 +78,28 @@ if command == 'install' or command == 'reinstall' then
return;
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
local pkg = args[2];
if not pkg then printUsage(); return; end
@ -88,6 +139,18 @@ if command == 'search' then
return;
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
local pkg = args[2];
if not pkg then printUsage(); return; end

View File

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

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');
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()
local ccpm = createCcpm({ stateDir = freshDirs() });
ccpm.writeConfig({ registries = {} });
@ -148,6 +171,87 @@ testlib.test('registry add and remove round-trip', function()
testlib.assertTrue(not rmOk);
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()
local ccpm = createCcpm({ stateDir = freshDirs() });
ccpm.writeLock({ packages = {