-
Notifications
You must be signed in to change notification settings - Fork 11
Expand file tree
/
Copy pathusePantry.ts
More file actions
389 lines (336 loc) · 10.9 KB
/
usePantry.ts
File metadata and controls
389 lines (336 loc) · 10.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
import { is_what, PlainObject } from "../deps.ts"
const { isNumber, isPlainObject, isString, isArray, isPrimitive, isBoolean } = is_what
import { Package, Installation, PackageRequirement } from "../types.ts"
import SemVer, * as semver from "../utils/semver.ts"
import useMoustaches from "./useMoustaches.ts"
import { PkgxError } from "../utils/error.ts"
import { validate } from "../utils/misc.ts"
import useConfig from "./useConfig.ts"
import host from "../utils/host.ts"
import Path from "../utils/Path.ts"
export interface Interpreter {
project: string // FIXME: should probably be a stronger type
args: string[]
}
export class PantryError extends PkgxError
{}
export class PantryParseError extends PantryError {
project: string
path?: Path
constructor(project: string, path?: Path, cause?: unknown) {
super(`package.yml parse error: ${path ?? project}`)
this.project = project
this.path = path
this.cause = cause
}
}
export class PackageNotFoundError extends PantryError {
project: string
constructor(project: string) {
super(`pkg not found: ${project}`)
this.project = project
}
}
export class PantryNotFoundError extends PantryError {
constructor(path: Path) {
super(`pantry not found: ${path}`)
}
}
export default function usePantry() {
const prefix = useConfig().data.join("pantry/projects")
async function* ls(): AsyncGenerator<LsEntry> {
const seen = new Set()
for (const prefix of pantry_paths()) {
for await (const path of _ls_pantry(prefix)) {
const project = path.parent().relative({ to: prefix })
if (seen.insert(project).inserted) {
yield { project, path }
}
}
}
}
const project = (input: string | { project: string }) => {
const project = isString(input) ? input : input.project
const yaml = (() => {
for (const prefix of pantry_paths()) {
if (!prefix.exists()) throw new PantryNotFoundError(prefix.parent())
const dir = prefix.join(project)
const filename = dir.join("package.yml")
if (!filename.exists()) continue
let memo: Promise<PlainObject> | undefined
return () => memo ?? (memo = filename.readYAML()
.then(validate.obj)
.catch(cause => { throw new PantryParseError(project, filename, cause) }))
}
throw new PackageNotFoundError(project)
})()
const companions = async () => parse_pkgs_node((await yaml())["companions"])
const runtime_env = async (version: SemVer, deps: Installation[]) => {
const yml = await yaml()
const obj = validate.obj(yml["runtime"]?.["env"] ?? {})
return expand_env_obj(obj, { project, version }, deps)
}
const available = async (): Promise<boolean> => {
let { platforms } = await yaml()
if (!platforms) return true
if (isString(platforms)) platforms = [platforms]
if (!isArray(platforms)) throw new PantryParseError(project)
return platforms.includes(host().platform) ||platforms.includes(`${host().platform}/${host().arch}`)
}
const drydeps = async () => parse_pkgs_node((await yaml()).dependencies)
const provides = async () => {
let node = (await yaml())["provides"]
if (!node) return []
if (isPlainObject(node)) {
node = node[host().platform]
}
if (!isArray(node)) throw new PantryParseError(project)
return node.compact(x => {
if (isPlainObject(x)) {
x = x["executable"]
}
if (isString(x)) {
if (x.startsWith("bin/")) return x.slice(4)
if (x.startsWith("sbin/")) return x.slice(5)
}
})
}
const provider = async () => {
for (const prefix of pantry_paths()) {
if (!prefix.exists()) continue
const dir = prefix.join(project)
const filename = dir.join("provider.yml")
if (!filename.exists()) continue
const yaml = validate.obj(await filename.readYAML())
const cmds = validate.arr<string>(yaml.cmds)
return (binname: string) => {
if (!cmds.includes(binname)) return
const args = yaml['args']
if (isPlainObject(args)) {
if (args[binname]) {
return get_args(args[binname])
} else {
return get_args(args['...'])
}
} else {
return get_args(args)
}
}
}
function get_args(input: unknown) {
if (isString(input)) {
return input.split(/\s+/)
} else {
return validate.arr<string>(input)
}
}
}
return {
companions,
runtime: {
env: runtime_env,
deps: drydeps
},
available,
provides,
provider,
yaml
}
}
/// finds a project that matches the input string on either name, display-name or FQD project name
/// - Returns: Project[] since there may by multiple matches, if you want a single match you should use `project()`
async function find(name: string) {
type Foo = ReturnType<typeof project> & LsEntry
name = name.toLowerCase()
//TODO not very performant due to serial awaits
const rv: Foo[] = []
for await (const pkg of ls()) {
const proj = {...project(pkg.project), ...pkg}
if (pkg.project.toLowerCase() == name) {
rv.push(proj)
continue
}
const yaml = await proj.yaml().swallow()
if (!yaml) {
console.warn("warn: parse failure:", pkg.project)
} else if (yaml["display-name"]?.toLowerCase() == name) {
rv.push(proj)
} else if ((await proj.provides()).map(x => x.toLowerCase()).includes(name)) {
rv.push(proj)
}
}
return rv
}
async function which({ interprets: extension }: { interprets: string }): Promise<Interpreter | undefined> {
if (extension[0] == '.') extension = extension.slice(1)
if (!extension) return
for await (const pkg of ls()) {
const yml = await project(pkg).yaml()
const node = yml["interprets"]
if (!isPlainObject(node)) continue
try {
const { extensions, args } = yml["interprets"]
if ((isString(extensions) && extensions === extension) ||
(isArray(extensions) && extensions.includes(extension))) {
return { project: pkg.project, args: isArray(args) ? args : [args] }
}
} catch {
continue
}
}
return undefined
}
const missing = () => {
try {
return !pantry_paths().some(x => x.exists())
} catch (e) {
if (e instanceof PantryNotFoundError) {
return true
} else {
throw e
}
}
}
const neglected = () => {
if (!prefix.exists()) return true
const stat = Deno.statSync(prefix.string)
if (!stat.mtime) return true
return (Date.now() - stat.mtime.getTime()) > 24 * 60 * 60 * 1000
}
return {
prefix,
which,
ls,
project,
find,
parse_pkgs_node,
expand_env_obj,
missing,
neglected
}
function pantry_paths(): Path[] {
const rv: Path[] = []
if (prefix.isDirectory()) {
rv.push(prefix)
}
for (const path of useConfig().pantries.reverse()) {
rv.unshift(path.join("projects"))
}
if (rv.length == 0) {
throw new PantryNotFoundError(prefix)
}
return rv
}
}
// deno-lint-ignore no-explicit-any
export function parse_pkgs_node(node: any) {
if (!node) return []
node = validate.obj(node)
platform_reduce(node)
return Object.entries(node)
.compact(([project, constraint]) =>
validatePackageRequirement(project, constraint))
}
export function validatePackageRequirement(project: string, constraint: unknown): PackageRequirement {
if (isNumber(constraint)) {
constraint = `^${constraint}`
} else if (!isString(constraint)) {
throw new Error(`invalid constraint for ${project}: ${constraint}`)
}
constraint = semver.Range.parse(constraint as string)
if (!constraint) {
throw new PkgxError("invalid constraint for " + project + ": " + constraint)
}
return {
project,
constraint: constraint as semver.Range
}
}
/// expands platform specific keys into the object
/// expands inplace because JS is nuts and you have to suck it up
function platform_reduce(env: PlainObject) {
const sys = host()
for (const [key, value] of Object.entries(env)) {
const [os, arch] = (() => {
let match = key.match(/^(darwin|linux)\/(aarch64|x86-64)$/)
if (match) return [match[1], match[2]]
if ((match = key.match(/^(darwin|linux)$/))) return [match[1]]
if ((match = key.match(/^(aarch64|x86-64)$/))) return [,match[1]]
return []
})()
if (!os && !arch) continue
delete env[key]
if (os && os != sys.platform) continue
if (arch && arch != sys.arch) continue
const dict = validate.obj(value)
for (const [key, value] of Object.entries(dict)) {
// if user specifies an array then we assume we are supplementing
// otherwise we are replacing. If this is too magical let us know
if (isArray(value)) {
if (!env[key]) env[key] = []
else if (!isArray(env[key])) env[key] = [env[key]]
//TODO if all-platforms version comes after the specific then order accordingly
env[key].push(...value)
} else {
env[key] = value
}
}
}
}
export function expand_env_obj(env_: PlainObject, pkg: Package, deps: Installation[]): Record<string, string> {
const env = {...env_}
platform_reduce(env)
const rv: Record<string, string> = {}
for (let [key, value] of Object.entries(env)) {
if (isArray(value)) {
value = value.map(x => transform(x)).join(" ")
} else {
value = transform(value)
}
if (Deno.build.os == 'windows') {
// we standardize on UNIX directory separators
// NOTE hopefully this won’t break anything :/
value = value.replaceAll('/', '\\')
}
rv[key] = value
}
return rv
// deno-lint-ignore no-explicit-any
function transform(value: any): string {
if (!isPrimitive(value)) throw new PantryParseError(pkg.project, undefined, JSON.stringify(value))
if (isBoolean(value)) {
return value ? "1" : "0"
} else if (value === undefined || value === null) {
return "0"
} else if (isString(value)) {
const mm = useMoustaches()
const home = Path.home().string
const obj = [
{ from: 'home', to: home } // remove, stick with just ~
]
obj.push(...mm.tokenize.all(pkg, deps))
return mm.apply(value, obj)
} else if (isNumber(value)) {
return value.toString()
}
const e = new Error("unexpected error")
e.cause = value
throw e
}
}
interface LsEntry {
project: string
path: Path
}
async function* _ls_pantry(dir: Path): AsyncGenerator<Path> {
if (!dir.isDirectory()) throw new PantryNotFoundError(dir)
for await (const [path, { name, isDirectory }] of dir.ls()) {
if (isDirectory) {
for await (const x of _ls_pantry(path)) {
yield x
}
} else if (name === "package.yml" || name === "package.yaml") {
yield path
}
}
}