a plugin can depend on a release binary, and prove it is the right one
osDependencies could only say "this package, from this manager". Gitea's `tea`
is not in any distribution — it is a Go binary from dl.gitea.com — and Debian
has an UNRELATED package of that name: a graphical text editor. So the one
dependency gitea actually wants could not be expressed, and its marketplace
page showed no dependencies at all while music's showed two.
`release: { url, identify }` covers it. {os} and {arch} are filled from the
host in the spelling Go releases use, and the binary is fetched to
~/.local/bin — no root, and nothing a plugin downloaded from the internet
sitting beside what the distribution installed. Written to a temporary name and
moved into place, so a half-downloaded file is never on PATH under the real
one.
`identify` is the part that matters. Bun.which('tea') cannot tell Gitea's CLI
from the text editor, so presence was never the same question as identity —
and reporting the wrong program as satisfied is worse than reporting it
missing, because the install succeeds and every call afterwards fails
strangely. A binary that fails to identify is now `present: false, imposter:
true`.
Found while testing: Bun.which() with no options reads the PATH the PROCESS
started with, which under PM2 frequently excludes ~/.local/bin entirely. A
release binary fetched during an install would have been reported missing
seconds later by stillMissing, failing an install that had just succeeded. The
search now passes PATH explicitly, with ~/.local/bin first.
Verified against the real binaries on this machine: the genuine tea identifies,
a fake that prints text-editor output is caught as an imposter, and both
dl.gitea.com URLs (linux-amd64, darwin-arm64) answer 200.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -20,7 +20,7 @@ import { refreshPluginMounts, snapshotPlugins } from './mount';
|
||||
import { generatePluginSchemas, pushPluginSchema } from './schema';
|
||||
import { declaredTables, dropTables, existingTables } from './tables';
|
||||
import type { OnLog } from './run-logged';
|
||||
import { installDependencies, manualInstallHint, reportDependencies, stillMissing } from './os-deps';
|
||||
import { installDependencies, installRelease, manualInstallHint, reportDependencies, stillMissing } from './os-deps';
|
||||
import { mountPrefix, type DiscoveredPlugin } from './manifest';
|
||||
|
||||
// The install runner: the four verbs, each as a short ordered list of effects.
|
||||
@@ -219,8 +219,24 @@ export async function installPlugin(
|
||||
};
|
||||
}
|
||||
|
||||
await step(steps, onStep, `dependencies: installing ${deps.packagesToInstall.join(', ')} with ${deps.manager}`);
|
||||
const installed = await installDependencies(deps.manager!, deps.packagesToInstall);
|
||||
// Release binaries first, and separately: they need no package manager, which is the reason they
|
||||
// exist. `tea` is the motivating case — Gitea publishes it from dl.gitea.com, and the Debian
|
||||
// package of that name is an unrelated text editor.
|
||||
for (const dep of deps.missing.filter((d) => d.releaseUrl)) {
|
||||
await step(steps, onStep, `dependencies: fetching ${dep.binary} from ${dep.releaseUrl}`);
|
||||
const got = await installRelease(dep);
|
||||
if (!got.ok) {
|
||||
return { ok: false, appName, steps, error: `could not fetch ${dep.binary}: ${got.error}` };
|
||||
}
|
||||
await step(steps, onStep, `dependencies: ${dep.binary} installed to ${got.path}`);
|
||||
}
|
||||
|
||||
if (deps.packagesToInstall.length) {
|
||||
await step(steps, onStep, `dependencies: installing ${deps.packagesToInstall.join(', ')} with ${deps.manager}`);
|
||||
}
|
||||
const installed = deps.packagesToInstall.length
|
||||
? await installDependencies(deps.manager!, deps.packagesToInstall)
|
||||
: { ok: true as const, installed: [], output: '' };
|
||||
if (!installed.ok) {
|
||||
return {
|
||||
ok: false,
|
||||
|
||||
Reference in New Issue
Block a user