plugins: a dependency can declare the architecture its package needs
Minesweeper could not be installed by the plugin flow at all. Its game is an i386 PE, so it needs wine32:i386 — and that package does not exist on a 64-bit host until dpkg has been told the architecture does. No package name can say that, so the step fell outside the manifest entirely and had to be run by hand, which meant an install that reported success and left a game dying with `Bad EXE format` several steps from anything mentioning architectures. Two fields, both declarative: `multiarch: 'i386'` on an osDependency. The platform runs `dpkg --add-architecture` and `apt-get update` before installing, apt hosts only, skipped when already enabled. It is a closed enum rather than a command because it runs as root: a manifest that could supply the string could run anything as root on every machine that installs the plugin, which is a much larger grant than "may install wine". Same principle as a plugin websocket naming a sidecar rather than a port. `packages.apt` may now be a LIST. One binary really can need more than one package — `wine` provides the launcher on PATH and `wine32:i386` the 32-bit loader, and either alone gives a wine that cannot run what the plugin came for. Every element goes through the same regex and reaches apt as separate argv, so a list is no more powerful than the names in it. The regex also now accepts an architecture suffix from the same closed set. Enabling an architecture is system-wide and permanent — nothing here ever disables one, including plugins:delete, because by then something else may depend on it. So it is reported before the owner consents rather than mentioned in a log afterwards, and only for dependencies actually being installed: a binary already present had its architecture enabled at some point in the past. multiarch.test.ts pins the enum against shell-shaped input, the suffix against an unknown arch, and the report against re-announcing a change that is not happening. 1409 pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
This commit is contained in:
@@ -105,7 +105,7 @@ export type OsDependency = {
|
||||
* which for a binary that is present anyway is correct and silent, and otherwise becomes an instruction
|
||||
* to the owner rather than a guess.
|
||||
*/
|
||||
packages: Partial<Record<PackageManager, string>>;
|
||||
packages: Partial<Record<PackageManager, string | string[]>>;
|
||||
/**
|
||||
* WHO has to be able to run this.
|
||||
*
|
||||
@@ -125,6 +125,33 @@ export type OsDependency = {
|
||||
* See `docs/plugin-dependency-scope.md`.
|
||||
*/
|
||||
scope?: 'owner' | 'shared';
|
||||
/**
|
||||
* An architecture this dependency needs enabled before its package can be installed.
|
||||
*
|
||||
* Debian and Ubuntu only, and it means exactly one thing: run `dpkg --add-architecture <arch>` followed
|
||||
* by `apt-get update` before installing, if that architecture is not already enabled.
|
||||
*
|
||||
* ── Why this is a field and not a package name ──
|
||||
*
|
||||
* Because it cannot be one. A 32-bit package on a 64-bit host is `wine32:i386`, and installing it fails
|
||||
* outright until the host has been told i386 exists — which is a change to the machine's dpkg
|
||||
* configuration, not a package to fetch. Minesweeper found this the direct way: `wine` alone installs
|
||||
* 64-bit-only, the plugin reports a clean install, and the game dies with `Bad EXE format`.
|
||||
*
|
||||
* ── Why it is an enum and not a command ──
|
||||
*
|
||||
* This runs as root. A manifest that could supply the command would be a manifest that can run anything
|
||||
* as root on every machine that installs it, which is a far larger grant than "this plugin may install
|
||||
* wine". The manifest picks a value from `DPKG_ARCHITECTURES`; the platform owns the sentence it turns
|
||||
* into. Same principle as the plugin websocket naming a sidecar rather than a port.
|
||||
*
|
||||
* It is also SYSTEM-WIDE and does not go away when the plugin does. `plugins:delete` will not disable an
|
||||
* architecture something else may now depend on, so this is shown to the owner before they consent
|
||||
* rather than mentioned in a log afterwards.
|
||||
*
|
||||
* Ignored where it means nothing — a pacman or brew host is already multi-arch or has no such concept.
|
||||
*/
|
||||
multiarch?: DpkgArchitecture;
|
||||
/**
|
||||
* A release binary, for a dependency no package manager carries.
|
||||
*
|
||||
@@ -509,7 +536,26 @@ export const PACKAGE_MANAGERS: PackageManager[] = ['apt', 'pacman', 'dnf', 'brew
|
||||
* not a regex that already allows it.
|
||||
*/
|
||||
const BINARY_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/;
|
||||
const PACKAGE_RE = /^[a-z0-9][a-z0-9.+-]{0,63}$/;
|
||||
/**
|
||||
* A package name, optionally qualified by an architecture.
|
||||
*
|
||||
* `wine32:i386` is the motivating case and it is not an edge case: on Debian and Ubuntu a 32-bit package
|
||||
* on a 64-bit host can only be named that way. The suffix is drawn from `DPKG_ARCHITECTURES` below rather
|
||||
* than being any word, so this stays a closed vocabulary — the point of a narrow regex here is that a
|
||||
* manifest cannot get a metacharacter onto an `apt-get` command line, and widening it to `[a-z0-9]+`
|
||||
* after the colon would still be safe but would stop being a vocabulary.
|
||||
*/
|
||||
const PACKAGE_RE = /^[a-z0-9][a-z0-9.+-]{0,63}(:(i386|amd64|arm64|armhf|ppc64el|s390x))?$/;
|
||||
|
||||
/**
|
||||
* Architectures `dpkg --add-architecture` accepts, and the only values a manifest may ask to enable.
|
||||
*
|
||||
* Closed on purpose. `multiarch` turns into a command run as root, so the manifest gets to pick from a
|
||||
* list rather than to supply a string — the same reasoning as the package regex one line up, applied to
|
||||
* the one field that would otherwise be a way to say "run this as root" out loud.
|
||||
*/
|
||||
export const DPKG_ARCHITECTURES = ['i386', 'amd64', 'arm64', 'armhf', 'ppc64el', 's390x'] as const;
|
||||
export type DpkgArchitecture = (typeof DPKG_ARCHITECTURES)[number];
|
||||
|
||||
/** Reasons an `osDependencies` list is unusable. Empty when it is fine. */
|
||||
function osDependencyProblems(deps: unknown): string[] {
|
||||
@@ -540,6 +586,13 @@ function osDependencyProblems(deps: unknown): string[] {
|
||||
problems.push(`${at}.packages must be an object keyed by package manager`);
|
||||
continue;
|
||||
}
|
||||
const multiarch = (dep as { multiarch?: unknown }).multiarch;
|
||||
if (multiarch !== undefined && !DPKG_ARCHITECTURES.includes(multiarch as never)) {
|
||||
// Refused rather than ignored: a typo here silently drops the one step that makes the package
|
||||
// installable at all, and the failure surfaces much later as the plugin not working.
|
||||
problems.push(`${at}.multiarch must be one of ${DPKG_ARCHITECTURES.join(', ')}`);
|
||||
}
|
||||
|
||||
for (const [manager, name] of Object.entries(packages)) {
|
||||
if (!PACKAGE_MANAGERS.includes(manager as PackageManager)) {
|
||||
problems.push(
|
||||
@@ -547,8 +600,19 @@ function osDependencyProblems(deps: unknown): string[] {
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (typeof name !== 'string' || !PACKAGE_RE.test(name)) {
|
||||
problems.push(`${at}.packages.${manager} must be a plain package name — lowercase, digits, dot, plus, dash`);
|
||||
// A LIST is allowed, because one binary genuinely can need more than one package. wine is the case:
|
||||
// `wine` provides the launcher on PATH and `wine32:i386` provides the 32-bit loader it needs to run
|
||||
// an i386 executable, and installing either alone produces a wine that cannot run the thing the
|
||||
// plugin came for. Every element is validated the same way and they reach apt as separate argv
|
||||
// entries, so a list is no more powerful than the names in it.
|
||||
const names = Array.isArray(name) ? name : [name];
|
||||
const bad = names.length === 0 || names.some((n) => typeof n !== 'string' || !PACKAGE_RE.test(n));
|
||||
if (bad) {
|
||||
problems.push(
|
||||
`${at}.packages.${manager} must be a plain package name (or a non-empty list of them) — ` +
|
||||
`lowercase, digits, dot, plus, dash, optionally with an architecture suffix ` +
|
||||
`(${DPKG_ARCHITECTURES.join(', ')})`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user