Skip to content

Commit 91e2863

Browse files
committed
ci fixes + #86
this should closes #86 [sic] done
1 parent 0218030 commit 91e2863

2 files changed

Lines changed: 86 additions & 9 deletions

File tree

.github/workflows/ci.yml

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,9 @@ jobs:
7575
- run: ./pkgm.ts update
7676
- run: if pkgx semverator satisfies '>=1.19' "$(hyperfine --version | cut -f 2 -d ' ')"; then false; fi
7777
# …until the pin is widened to `*`, after which `update` tracks latest.
78-
- run: sed -i'' 's/= "~1.18"/= "*"/' "${XDG_CONFIG_HOME:-$HOME/.config}/pkgm/manifest.toml"
78+
# `-i.bak` (not bare `-i`) so it's portable: GNU and BSD/macOS sed both
79+
# take the suffix here; the leftover .bak file is harmless.
80+
- run: sed -i.bak 's/= "~1.18"/= "*"/' "${XDG_CONFIG_HOME:-$HOME/.config}/pkgm/manifest.toml"
7981
- run: ./pkgm.ts outdated | grep hyperfine
8082
- run: ./pkgm.ts update
8183
- run: pkgx semverator satisfies '>=1.19' "$(hyperfine --version | cut -f 2 -d ' ')"
@@ -130,6 +132,13 @@ jobs:
130132
- name: sudo install drops privileges and overrides HOME
131133
run: |
132134
set -eux
135+
# The pkgm.ts shebang makes pkgx fetch a private deno (and unzip, to
136+
# unpack deno’s .zip) the first time it runs. Under sudo that bootstrap
137+
# is the kernel-invoked `pkgx … deno run` executing as root with
138+
# HOME=/root, so it lands in /root/.pkgx before any pkgm code runs —
139+
# pkgm can’t relocate it. Warm it before the marker so the checks below
140+
# scope only to what the install itself creates.
141+
sudo ./pkgm.ts --version
133142
# marker to scope ownership checks to files created by this install
134143
touch /tmp/pkgm-sudo-marker
135144
sudo ./pkgm.ts i hyperfine
@@ -233,7 +242,9 @@ jobs:
233242
./pkgm.ts i uv
234243
out=$(./pkgm.ts outdated 2>&1 || true)
235244
echo "$out"
236-
echo "$out" | (! grep -q "Uncaught")
245+
if echo "$out" | grep -q "Uncaught"; then
246+
echo "outdated raised an uncaught exception"; exit 1
247+
fi
237248
- name: uv then node keeps outdated/update alive
238249
run: |
239250
set -eux
@@ -242,7 +253,9 @@ jobs:
242253
./pkgm.ts i node
243254
out=$(./pkgm.ts outdated 2>&1 || true)
244255
echo "$out"
245-
echo "$out" | (! grep -q "Uncaught")
256+
if echo "$out" | grep -q "Uncaught"; then
257+
echo "outdated raised an uncaught exception"; exit 1
258+
fi
246259
247260
# #88 root cause: a later install must resolve against the whole requested
248261
# set (via the manifest), so python's strict zlib pin (pulled in by uv)

pkgm.ts

Lines changed: 70 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ if (parsedArgs.help || parsedArgs._[0] == "help") {
9696
case "li":
9797
if (install_prefix().string != "/usr/local") {
9898
const dst = Path.home().join(".local");
99+
assert_manifest_resolved(dst);
99100
await install(await combined_specs(dst, args), dst.string);
100101
await record_install(dst, args);
101102
} else {
@@ -329,7 +330,8 @@ async function query_pkgx(
329330
const needs_sudo_backwards = install_prefix().string == "/usr/local";
330331
let cmd = needs_sudo_backwards ? "/usr/bin/sudo" : pkgx;
331332
if (needs_sudo_backwards) {
332-
if (!Deno.env.get("SUDO_USER")) {
333+
const sudo_user = Deno.env.get("SUDO_USER");
334+
if (!sudo_user) {
333335
if (Deno.uid() == 0) {
334336
console.error(
335337
"%cwarning",
@@ -338,8 +340,18 @@ async function query_pkgx(
338340
);
339341
}
340342
cmd = pkgx;
343+
} else if (reachable_as(pkgx, sudo_user)) {
344+
// drop privileges so pkgx writes its cache as the invoking user, and point
345+
// HOME at their home so it doesn’t cache back under /root/ where they
346+
// couldn’t reach it on the next invocation.
347+
const home = user_home(sudo_user);
348+
if (home) env.HOME = home;
349+
args.unshift("-u", sudo_user, pkgx);
341350
} else {
342-
args.unshift("-u", Deno.env.get("SUDO_USER")!, pkgx);
351+
// pkgx lives somewhere the unprivileged user can’t execute it (e.g. only
352+
// under /root/.pkgx). dropping privileges would abort with “Permission
353+
// denied”, so run it as root instead (pkgxdev/pkgm#68).
354+
cmd = pkgx;
343355
}
344356
}
345357

@@ -379,6 +391,34 @@ async function query_pkgx(
379391
];
380392
}
381393

394+
// home directory of the invoking (pre-sudo) user, so pkgx caches under their
395+
// tree rather than /root/. getent is the portable lookup on Linux; on macOS it
396+
// is absent, but the /root/.pkgx scenario this guards against doesn’t arise
397+
// there in practice, so a missing home (→ no override) is fine.
398+
function user_home(user: string): string | undefined {
399+
try {
400+
const out = new Deno.Command("/usr/bin/getent", {
401+
args: ["passwd", user],
402+
}).outputSync();
403+
if (!out.success) return undefined;
404+
const fields = new TextDecoder().decode(out.stdout).trim().split(":");
405+
return fields[5] || undefined;
406+
} catch {
407+
return undefined;
408+
}
409+
}
410+
411+
// can `user` execute the binary at `p`? private home dirs are typically mode
412+
// 700, so a path under another user’s home is unreachable; system paths and the
413+
// user’s own home are assumed reachable. used to decide whether dropping
414+
// privileges to `user` would leave pkgx unrunnable (pkgxdev/pkgm#68).
415+
function reachable_as(p: string, user: string): boolean {
416+
if (p.startsWith("/root/")) return user === "root";
417+
const m = p.match(/^\/home\/([^/]+)\//);
418+
if (m) return m[1] === user;
419+
return true;
420+
}
421+
382422
async function mirror_directory(dst: string, src: string, prefix: string) {
383423
let warned_copy_fallback = false;
384424
await processEntry(join(src, prefix), join(dst, prefix));
@@ -794,6 +834,14 @@ async function update() {
794834
}
795835
}
796836

837+
// nothing satisfies a newer in-range version: everything’s current. return
838+
// before calling install(), which treats an empty arg list as a usage error
839+
// ("no packages specified") and exits non-zero.
840+
if (update_list.length == 0) {
841+
console.error("everything is up-to-date");
842+
return;
843+
}
844+
797845
for (const pkgspec of update_list) {
798846
const pkg = utils.pkg.parse(pkgspec);
799847
console.log(
@@ -858,7 +906,22 @@ function manifest_entry_kind(value: string): ManifestEntryKind {
858906
// finished. no manifest at all → legacy behavior (see hydrate_graph), so
859907
// existing installs keep working untouched.
860908
function assert_manifest_resolved(prefix = install_prefix()) {
861-
const manifest = read_manifest(prefix);
909+
let manifest;
910+
try {
911+
manifest = read_manifest(prefix);
912+
} catch (err) {
913+
// invalid TOML (e.g. a partially-finished hand-edit) must block commands
914+
// with a clear message, not crash with an uncaught parse stack trace.
915+
console.error(
916+
"%cerror",
917+
"color:red",
918+
`could not parse ${manifest_path(prefix)}: ${
919+
err instanceof Error ? err.message : err
920+
}`,
921+
);
922+
console.error("fix the TOML syntax and re-run");
923+
Deno.exit(1);
924+
}
862925
if (!manifest) return;
863926
const unresolved = Object.entries(manifest)
864927
.filter(([, value]) => manifest_entry_kind(value) == "unresolved")
@@ -1039,9 +1102,10 @@ async function combined_specs(prefix: Path, args: string[]): Promise<string[]> {
10391102
}
10401103

10411104
// map each requested arg to its canonical project and the constraint you asked
1042-
// for. a bare name (`node`) yields `*` — track latest; a versioned spec
1043-
// (`node@22`) yields its range (`^22`). pantry hiccups are non-fatal: the
1044-
// package will still be picked up as a pin by record_install’s tree walk.
1105+
// for. a bare name (`node`) yields `*` here, which record_install then
1106+
// major-locks to the installed version (`^<installed>`); a versioned spec
1107+
// (`node@22`) yields its range (`^22`) verbatim. pantry hiccups are non-fatal:
1108+
// the package will still be picked up as a pin by record_install’s tree walk.
10451109
async function requested_constraints(
10461110
args: string[],
10471111
): Promise<Record<string, string>> {

0 commit comments

Comments
 (0)