Skip to content

Commit 3917b38

Browse files
authored
add which so run("node^16") works (pkgxdev#16)
1 parent c431f8c commit 3917b38

File tree

6 files changed

+131
-36
lines changed

6 files changed

+131
-36
lines changed

README.md

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -74,11 +74,7 @@ Need a specific version of something? [tea][tea/cli] can install any version
7474
of any package:
7575

7676
```ts
77-
const { install, run } = porcelain;
78-
79-
const node16 = await install("nodejs.org^16.18"); // ※ https://devhints.io/semver
80-
81-
await run(['node', '-e', 'console.log(process.version)']);
77+
await run("node^16 -e 'console.log(process.version)'");
8278
// => v16.18.1
8379
```
8480

mod.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import useDownload from "./src/hooks/useDownload.ts"
1919
import useShellEnv from "./src/hooks/useShellEnv.ts"
2020
import useInventory from "./src/hooks/useInventory.ts"
2121
import hydrate from "./src/plumbing/hydrate.ts"
22+
import which from "./src/plumbing/which.ts"
2223
import link from "./src/plumbing/link.ts"
2324
import install, { ConsoleLogger } from "./src/plumbing/install.ts"
2425
import resolve from "./src/plumbing/resolve.ts"
@@ -49,7 +50,8 @@ const plumbing = {
4950
hydrate,
5051
link,
5152
install,
52-
resolve
53+
resolve,
54+
which
5355
}
5456

5557
const porcelain = {

src/plumbing/which.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { PackageRequirement } from "../types.ts"
2+
import usePantry from "../hooks/usePantry.ts"
3+
import * as semver from "../utils/semver.ts"
4+
5+
export type WhichResult = PackageRequirement & {
6+
shebang: string[]
7+
}
8+
9+
export default async function(arg0: string, opts = { providers: true }): Promise<WhichResult | undefined> {
10+
arg0 = arg0.trim()
11+
/// sanitize and reject anything with path components
12+
if (!arg0 || arg0.includes("/")) return
13+
14+
const pantry = usePantry()
15+
let found: { project: string, constraint: semver.Range, shebang: string[] } | undefined
16+
const promises: Promise<void>[] = []
17+
18+
for await (const entry of pantry.ls()) {
19+
if (found) break
20+
const p = pantry.project(entry).provides().then(providers => {
21+
for (const provider of providers) {
22+
if (found) {
23+
return
24+
} else if (provider == arg0) {
25+
const constraint = new semver.Range("*")
26+
found = {...entry, constraint, shebang: [provider] }
27+
} else if (arg0.startsWith(provider)) {
28+
// eg. `node^16` symlink
29+
try {
30+
const constraint = new semver.Range(arg0.substring(provider.length))
31+
found = {...entry, constraint, shebang: [provider] }
32+
} catch {
33+
// not a valid semver range; fallthrough
34+
}
35+
} else {
36+
//TODO more efficient to check the prefix fits arg0 first
37+
// eg. if python3 then check if the provides starts with python before
38+
// doing all the regex shit. Matters because there's a *lot* of YAMLs
39+
40+
let rx = /({{\s*version\.(marketing|major)\s*}})/
41+
let match = provider.match(rx)
42+
if (!match?.index) continue
43+
const regx = match[2] == 'major' ? '\\d+' : '\\d+\\.\\d+'
44+
const foo = subst(match.index, match.index + match[1].length, provider, `(${regx})`)
45+
rx = new RegExp(`^${foo}$`)
46+
match = arg0.match(rx)
47+
if (match) {
48+
const constraint = new semver.Range(`~${match[1]}`)
49+
found = {...entry, constraint, shebang: [arg0] }
50+
}
51+
}
52+
}
53+
}).swallow(/^parser: pantry: package.yml/)
54+
promises.push(p)
55+
56+
if (opts.providers) {
57+
const pp = pantry.project(entry).provider().then(f => {
58+
if (!f) return
59+
const rv = f(arg0)
60+
if (rv) found = {
61+
...entry,
62+
constraint: new semver.Range('*'),
63+
shebang: [...rv, arg0]
64+
}
65+
})
66+
promises.push(pp)
67+
}
68+
}
69+
70+
if (!found) {
71+
// if we didn’t find anything yet then we have to wait on the promises
72+
// otherwise we can ignore them
73+
await Promise.all(promises)
74+
}
75+
76+
if (found) {
77+
return found
78+
}
79+
}
80+
81+
const subst = function(start: number, end: number, input: string, what: string) {
82+
return input.substring(0, start) + what + input.substring(end)
83+
}

src/porcelain/install.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
import { Installation, PackageSpecification } from "../types.ts"
21
import install, { Logger as InstallLogger } from "../plumbing/install.ts"
2+
import { Installation, PackageSpecification } from "../types.ts"
3+
import resolve, { Resolution } from "../plumbing/resolve.ts"
34
import usePantry from "../hooks/usePantry.ts"
45
import hydrate from "../plumbing/hydrate.ts"
5-
import resolve, { Resolution } from "../plumbing/resolve.ts"
6-
import { isArray, isString } from "is-what"
6+
import useSync from "../hooks/useSync.ts"
77
import { parse } from "../utils/pkg.ts"
88
import link from "../plumbing/link.ts"
9-
import useSync from "../hooks/useSync.ts";
9+
import { isString } from "is-what"
1010

1111
export interface Logger extends InstallLogger {
1212
resolved?(resolution: Resolution): void
@@ -15,7 +15,7 @@ export interface Logger extends InstallLogger {
1515
/// eg. install("python.org~3.10")
1616
export default async function(pkgs: PackageSpecification[] | string[] | string, logger?: Logger): Promise<Installation[]> {
1717

18-
if (!isArray(pkgs)) pkgs = [pkgs]
18+
if (isString(pkgs)) pkgs = pkgs.split(/\s+/)
1919
pkgs = pkgs.map(pkg => isString(pkg) ? parse(pkg) : pkg)
2020

2121
const pantry = usePantry()

src/porcelain/run.test.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { assertEquals, assertRejects } from "deno/testing/asserts.ts"
1+
import { assertEquals, assertMatch, assertRejects } from "deno/testing/asserts.ts"
22
import { useTestConfig } from "../hooks/useTestConfig.ts"
33
import run from "./run.ts"
44

@@ -11,6 +11,19 @@ Deno.test("porcelain.run", async runner => {
1111
assertEquals(status, 0)
1212
})
1313

14+
await runner.step("node^16", async runner => {
15+
useTestConfig()
16+
await runner.step("string", async () => {
17+
const { stdout } = await run('node^16 --version', { stdout: true })
18+
assertMatch(stdout, /^v16\./)
19+
})
20+
21+
await runner.step("array", async () => {
22+
const { stdout } = await run(['node^16', '--version'], { stdout: true })
23+
assertMatch(stdout, /^v16\./)
24+
})
25+
})
26+
1427
await runner.step("env", async () => {
1528
useTestConfig()
1629
await run(['node', '-e', 'if (process.env.FOO !== "FOO") throw new Error()'], { env: { FOO: "FOO" }})

src/porcelain/run.ts

Lines changed: 25 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import install, { Logger } from "../plumbing/install.ts"
22
import useShellEnv from '../hooks/useShellEnv.ts'
33
import usePantry from '../hooks/usePantry.ts'
4-
import * as semver from "../utils/semver.ts"
54
import hydrate from "../plumbing/hydrate.ts"
65
import resolve from "../plumbing/resolve.ts"
76
import { spawn } from "node:child_process"
87
import useSync from "../hooks/useSync.ts"
8+
import which from "../plumbing/which.ts"
99
import link from "../plumbing/link.ts"
1010
import Path from "../utils/Path.ts"
1111
import { isArray } from "is-what"
@@ -34,24 +34,33 @@ export default async function run(cmd: Cmd, opts: {stdout: true, status: true} &
3434
export default async function run(cmd: Cmd, opts: {stderr: true, status: true} & OptsEx): Promise<{ stderr: string, status: number }>;
3535
export default async function run(cmd: Cmd, opts: {stdout: true, stderr: true, status: true } & OptsEx): Promise<{ stdout: string, stderr: string, status: number }>;
3636
export default async function run(cmd: Cmd, opts?: Options): Promise<void|{ stdout?: string|undefined; stderr?: string|undefined; status?: number|undefined; }> {
37-
const [arg0, [spawn0, args]] = (() => {
38-
if (isArray(cmd)) {
39-
if (cmd.length == 0) {
40-
throw new RunError('EUSAGE', `\`cmd\` evaluated empty: ${cmd}`)
41-
}
42-
const arg0 = cmd.shift()!.toString()
43-
return [arg0, [arg0, cmd.map(x => x.toString())]]
44-
} else {
37+
38+
const { usesh, arg0: whom } = (() => {
39+
if (!isArray(cmd)) {
4540
const s = cmd.trim()
4641
const i = s.indexOf(' ')
47-
return [s.slice(0, i), ['/bin/sh', ['-c', cmd]]]
42+
const usesh = i >= 0
43+
const arg0 = s.slice(0, i)
44+
cmd = s.slice(i + 1)
45+
return { usesh, arg0 }
46+
} else if (cmd.length == 0) {
47+
throw new RunError('EUSAGE', `\`cmd\` evaluated empty: ${cmd}`)
48+
} else {
49+
return {
50+
usesh: false,
51+
arg0: cmd.shift()!.toString().trim()
52+
}
4853
}
4954
})()
5055

51-
const env = await setup(arg0, opts?.env ?? Deno.env.toObject(), opts?.logger)
56+
const { env, shebang } = await setup(whom, opts?.env ?? Deno.env.toObject(), opts?.logger)
57+
const arg0 = usesh ? '/bin/sh' : shebang.shift()!
58+
const args = usesh
59+
? ['-c', `${shebang.join(' ')} ${cmd}`]
60+
: [...shebang, ...(cmd as (string | Path)[]).map(x => x.toString())]
5261

5362
return new Promise((resolve, reject) => {
54-
const proc = spawn(spawn0, args, {
63+
const proc = spawn(arg0, args, {
5564
env,
5665
stdio: [
5766
"pipe",
@@ -84,18 +93,10 @@ async function setup(cmd: string, env: Record<string, string | undefined>, logge
8493
await useSync()
8594
}
8695

87-
const project = await (async () => {
88-
for await (const { project } of pantry.ls()) {
89-
const provides = await pantry.project(project).provides()
90-
//TODO handle eg. node^16 here
91-
if (provides.includes(cmd)) {
92-
return project
93-
}
94-
}
95-
throw new RunError('ENOENT', `No project in pantry provides ${cmd}`)
96-
})()
96+
const wut = await which(cmd)
97+
if (!wut) throw new RunError('ENOENT', `No project in pantry provides ${cmd}`)
9798

98-
const { pkgs } = await hydrate({ project, constraint: new semver.Range('*') })
99+
const { pkgs } = await hydrate(wut)
99100
const { pending, installed } = await resolve(pkgs)
100101
for (const pkg of pending) {
101102
const installation = await install(pkg, logger)
@@ -115,7 +116,7 @@ async function setup(cmd: string, env: Record<string, string | undefined>, logge
115116
}
116117
}
117118

118-
return sh.flatten(pkgenv)
119+
return { env: sh.flatten(pkgenv), shebang: wut.shebang }
119120
}
120121

121122

0 commit comments

Comments
 (0)