feat(ccpm): install trapos from package cache
This commit is contained in:
parent
51194b9866
commit
8ae947ac3d
@ -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
|
||||
|
||||
44
README.md
44
README.md
@ -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.
|
||||
|
||||
147
apis/libccpm.lua
147
apis/libccpm.lua
@ -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
|
||||
|
||||
|
||||
@ -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).
|
||||
|
||||
@ -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);
|
||||
@ -3,10 +3,6 @@
|
||||
"version": "0.4.0",
|
||||
"branch": "next",
|
||||
"packages": [
|
||||
"tos-core",
|
||||
"tos-test",
|
||||
"tos-boot",
|
||||
"tos-net",
|
||||
"tos-ui"
|
||||
"trapos"
|
||||
]
|
||||
}
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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": [
|
||||
|
||||
8
packages/trapos/ccpm.json
Normal file
8
packages/trapos/ccpm.json
Normal 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": []
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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');
|
||||
|
||||
104
tests/ccpm.lua
104
tests/ccpm.lua
@ -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 = {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user