Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 140 additions & 12 deletions pkgm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -297,25 +297,43 @@ async function query_pkgx(
set("PKGX_DIST_URL");
set("XDG_DATA_HOME");

const needs_sudo_backwards = install_prefix().string == "/usr/local";
let cmd = needs_sudo_backwards ? "/usr/bin/sudo" : pkgx;
if (needs_sudo_backwards) {
if (!Deno.env.get("SUDO_USER")) {
if (Deno.uid() == 0) {
const isRoot = Deno.uid() == 0;
const sudoUser = Deno.env.get("SUDO_USER");
const prefix = install_prefix().string;
const isSystemPrefix = prefix == "/usr/local";

let cmd = pkgx;
let cmd_args = args;

if (isSystemPrefix) {
if (isRoot && sudoUser) {
// Drop privileges so pkgx writes its cache as the invoking user, not root.
// But only if pkgx is reachable from sudoUser — otherwise the inner sudo
// aborts with "unable to execute …: Permission denied" (pkgxdev/pkgm#68).
const reachable = pkgx_reachable_as(pkgx, sudoUser);
if (reachable) {
cmd = "/usr/bin/sudo";
cmd_args = ["-u", sudoUser, "--", reachable, ...args];
// Override HOME, or pkgx will cache back under /root/ where sudoUser
// can't reach it on the next invocation.
const home = user_home(sudoUser);
if (home) env.HOME = home;
Comment on lines +313 to +320
} else if (Deno.env.get("PKGM_DEBUG")) {
console.error(
"%cwarning",
"color:yellow",
"installing as root; installing via `sudo` is preferred",
`pkgm: \`pkgx\` at ${pkgx} is not reachable as ${sudoUser}; running it as root`,
);
}
cmd = pkgx;
} else {
args.unshift("-u", Deno.env.get("SUDO_USER")!, pkgx);
} else if (isRoot) {
console.error(
"%cwarning",
"color:yellow",
"installing as root; installing via `sudo` is preferred",
);
}
}

const proc = new Deno.Command(cmd, {
args: [...args, "--json=v1"],
args: [...cmd_args, "--json=v1"],
stdout: "piped",
env,
clearEnv: true,
Expand Down Expand Up @@ -766,6 +784,116 @@ function install_prefix() {
}
}

function user_home_from_passwd(user: string): string | undefined {
try {
const passwd = Deno.readTextFileSync("/etc/passwd");
for (const line of passwd.split("\n")) {
if (!line || line.startsWith("#")) continue;
const fields = line.split(":");
if (fields[0] === user) return fields[5] || undefined;
}
} catch {
// Ignore unreadable or absent passwd database and fall back to other lookups.
}

return undefined;
}

function user_home_from_dscl(user: string): string | undefined {
if (!existsSync("/usr/bin/dscl")) return undefined;

try {
const out = new Deno.Command("/usr/bin/dscl", {
args: [".", "-read", `/Users/${user}`, "NFSHomeDirectory"],
}).outputSync();
if (!out.success) return undefined;

const line = new TextDecoder().decode(out.stdout).trim();
const prefix = "NFSHomeDirectory:";
if (!line.startsWith(prefix)) return undefined;

const home = line.slice(prefix.length).trim();
return home || undefined;
} catch {
return undefined;
}
}

function user_home(user: string): string | undefined {
// Prefer getent where available, but fall back to passwd parsing and macOS
// dscl so HOME can still be resolved when dropping privileges on systems
// without getent.
const getent = existsSync("/usr/bin/getent")
? "/usr/bin/getent"
: existsSync("/bin/getent")
? "/bin/getent"
: undefined;

if (getent) {
try {
const out = new Deno.Command(getent, {
args: ["passwd", user],
}).outputSync();
if (out.success) {
const fields = new TextDecoder().decode(out.stdout).trim().split(":");
if (fields[5]) return fields[5];
}
} catch {
// Ignore getent lookup failures and try portable fallbacks below.
}
}

return user_home_from_passwd(user) ?? user_home_from_dscl(user);
}

function pkgx_reachable_as(current: string, user: string): string | undefined {
if (reachable_as(current, user)) return current;

const home = user_home(user);
if (home) {
// Versioned pkgx.sh layout: ~/.pkgx/pkgx.sh/v<x.y.z>/bin/pkgx — pick the highest.
const root = join(home, ".pkgx/pkgx.sh");
if (existsSync(root)) {
let best: { v: SemVer; path: string } | undefined;
try {
if (Deno.statSync(root).isDirectory) {
for (const entry of Deno.readDirSync(root)) {
if (!entry.isDirectory || !entry.name.startsWith("v")) continue;
try {
const v = new SemVer(entry.name.slice(1));
const path = join(root, entry.name, "bin/pkgx");
if (!existsSync(path)) continue;
if (!best || v.gt(best.v)) best = { v, path };
} catch { /* skip malformed version dir */ }
}
}
} catch {
// Ignore unreadable/non-directory pkgx.sh roots and fall back to other locations.
}
if (best) return best.path;
Comment on lines +856 to +873
}
const local = join(home, ".local/bin/pkgx");
if (existsSync(local)) return local;
}
if (existsSync("/usr/local/bin/pkgx")) return "/usr/local/bin/pkgx";
return undefined;
Comment on lines +875 to +879
}

function reachable_as(p: string, user: string): boolean {
// Conservative heuristic: private home dirs are typically mode 700, so a
// path under another user's home is unreachable. System paths and the
// user's own home are assumed reachable.
const home = user_home(user);
if (home && (p === home || p.startsWith(`${home}/`))) return true;

if (p === "/root" || p.startsWith("/root/")) return false;
if (p === "/var/root" || p.startsWith("/var/root/")) return false;

if (p.match(/^\/(home|Users)\/([^/]+)(?:\/|$)/)) return false;

return true;
Comment on lines +882 to +894
}

function dev_stub_text(selfpath: string, bin_prefix: string, name: string) {
if (selfpath.startsWith("/usr/local") && selfpath != "/usr/local/bin/dev") {
return `
Expand Down
Loading