diff --git a/CLAUDE.md b/CLAUDE.md index e686897..73809c3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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//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//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 diff --git a/README.md b/README.md index 54fc739..a42cf32 100644 --- a/README.md +++ b/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//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 ccpm reinstall ccpm uninstall -ccpm ls ccpm search [term] ccpm info +ccpm update ccpm upgrade +ccpm ls ccpm available [term] ccpm search [term] +ccpm info ccpm registry ls ccpm registry add [--branch ] [--type github|http] ccpm registry rm ``` +`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. diff --git a/apis/libccpm.lua b/apis/libccpm.lua index 367e6d2..ac13699 100644 --- a/apis/libccpm.lua +++ b/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 = { = { version, registry, files, -- dependencies, autostart } } } +-- - ccpm.cache.json -> { packages = { = { 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 diff --git a/docs/adrs/adr-0010-ccpm-package-manager.md b/docs/adrs/adr-0010-ccpm-package-manager.md index b9a1897..3eb44e5 100644 --- a/docs/adrs/adr-0010-ccpm-package-manager.md +++ b/docs/adrs/adr-0010-ccpm-package-manager.md @@ -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 = { = { 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). diff --git a/install.lua b/install-ccpm.lua similarity index 81% rename from install.lua rename to install-ccpm.lua index dfafb11..10b364a 100644 --- a/install.lua +++ b/install-ccpm.lua @@ -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 '); - print('\t\twget run --core'); print('\t\twget run --beta'); print('\t\twget run --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); diff --git a/manifest.json b/manifest.json index 9f44a1c..662cc13 100644 --- a/manifest.json +++ b/manifest.json @@ -3,10 +3,6 @@ "version": "0.4.0", "branch": "next", "packages": [ - "tos-core", - "tos-test", - "tos-boot", - "tos-net", - "tos-ui" + "trapos" ] } diff --git a/packages/index.json b/packages/index.json index ce6151c..0c54f19 100644 --- a/packages/index.json +++ b/packages/index.json @@ -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" } } diff --git a/packages/tos-core/ccpm.json b/packages/tos-core/ccpm.json index e49ab3e..51c7f19 100644 --- a/packages/tos-core/ccpm.json +++ b/packages/tos-core/ccpm.json @@ -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": [ diff --git a/packages/trapos/ccpm.json b/packages/trapos/ccpm.json new file mode 100644 index 0000000..0ddbf5c --- /dev/null +++ b/packages/trapos/ccpm.json @@ -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": [] +} diff --git a/programs/ccpm.lua b/programs/ccpm.lua index bef9f9c..b149e2c 100644 --- a/programs/ccpm.lua +++ b/programs/ccpm.lua @@ -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 '); print('\t\tccpm reinstall '); print('\t\tccpm uninstall '); + 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 '); 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 diff --git a/programs/upgrade.lua b/programs/upgrade.lua index 6f093f1..ec07372 100644 --- a/programs/upgrade.lua +++ b/programs/upgrade.lua @@ -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'); diff --git a/tests/ccpm.lua b/tests/ccpm.lua index 8577938..b66f6e9 100644 --- a/tests/ccpm.lua +++ b/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 = {