Skip to content

Commit

Permalink
Merge pull request pkgxdev#251 from teaxyz/allow-versioned-provides
Browse files Browse the repository at this point in the history
Allow versioned provides
  • Loading branch information
mxcl authored Dec 10, 2022
2 parents 8ad5375 + 5642436 commit a14735b
Show file tree
Hide file tree
Showing 8 changed files with 117 additions and 55 deletions.
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
.DS_Store
tea
/tea

# exlcluding this is temporary pre stability (ie. v1)
/deno.lock
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ All you need is `tea`.
 


# tea/cli 0.16.1
# tea/cli 0.16.2

Open source is a treasure trove—yet those chests are sealed with gnarly locks.
tea is the key:
Expand Down Expand Up @@ -681,6 +681,18 @@ be a little something extra for those who helped build tea. 😶‍🌫️
# FAQ
## How do I update packages
```sh
$ tea --sync
# ^^ updates the pantries, and any packages in the virtual-environment
$ tea --sync +deno.land
# ^^ updates specific packages
```
Of course this is limited and more is required here. We’re working on it.
## Where’s `tea install`?
tea works differently. It’s not “I want to install Freetype” it’s
Expand Down
19 changes: 4 additions & 15 deletions src/app.X.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,20 @@
import { Args } from "hooks/useFlags.ts"
import { useCellar, usePantry } from "hooks"
import * as semver from "semver"
import { handler, prepare_exec_cmd } from "./app.exec.ts"
import { useCellar } from "hooks"
import { handler, prepare_exec_cmd, which } from "./app.exec.ts"
import { panic, run, TeaError, UsageError } from "utils"

export default async function X(opts: Args) {
const arg0 = opts.args[0]
if (!arg0) throw new UsageError()

let found: { project: string } | undefined

const pantry = usePantry()
for await (const entry of pantry.ls()) {
if (found) break
pantry.getProvides(entry).then(provides => {
if (!found && provides.includes(arg0)) {
found = entry
}
})
}
const found = await which(arg0)

if (!found) {
throw new TeaError('not-found: tea -X: arg0', {arg0})
}

opts.mode = 'exec'
opts.pkgs.push({ ...found, constraint: new semver.Range('*') })
opts.pkgs.push({ ...found })

const { env, pkgs } = await prepare_exec_cmd(opts.pkgs, {env: opts.env ?? false})
const pkg = pkgs.find(x => x.project == found!.project) ?? panic()
Expand Down
67 changes: 46 additions & 21 deletions src/app.exec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,21 @@ import * as semver from "semver"
import Path from "path"
import { Interpreter } from "hooks/usePantry.ts";

//TODO specifying explicit pkgs or versions on the command line should replace anything deeper
//TODO specifying explicit pkgs or versions on the command line should regxace anything deeper
// RATIONALE: so you can override if you are testing locally


export default async function(opts: Args) {
const { verbosity, ...flags } = useFlags()
const assessment = assess(opts.args)

if (assessment.type == 'repl') {
if (!opts.pkgs.length && flags.sync) Deno.exit(0) // `tea -S` is not an error or a repl
if (!opts.pkgs.length && verbosity > 0) Deno.exit(0) // `tea -v` is not an error or a repl
if (assessment.type == 'regx') {
if (!opts.pkgs.length && flags.sync) Deno.exit(0) // `tea -S` is not an error or a regx
if (!opts.pkgs.length && verbosity > 0) Deno.exit(0) // `tea -v` is not an error or a regx
if (!opts.pkgs.length) throw new UsageError()

const { installed, env } = await install(opts.pkgs)
await repl(installed, env)
await regx(installed, env)

} else try {
const refinement = await refine(assessment)
Expand Down Expand Up @@ -215,29 +215,54 @@ async function extract_shebang(path: Path) {
}
}

async function which(arg0: string) {
/// some special casing because we cannot represent version’d stuff in pantry yet
switch (arg0) {
case 'python2':
return { project: 'python.org', constraint: new semver.Range("2"), shebang: arg0 }
case 'python3':
return { project: 'python.org', constraint: new semver.Range("3"), shebang: arg0 }
}
const subst = function(start: number, end: number, input: string, what: string) {
return input.substring(0, start) + what + input.substring(end)
};

export async function which(arg0: string) {
const pantry = usePantry()
let found: { project: string } | undefined
let found: { project: string, constraint: semver.Range } | undefined
const promises: Promise<void>[] = []

for await (const entry of pantry.ls()) {
if (found) break
pantry.getProvides(entry).then(provides => {
if (!found && provides.includes(arg0)) {
found = entry
const p = pantry.getProvides(entry).then(providers => {
for (const provider of providers) {
if (found) {
return
} else if (provider == arg0) {
const constraint = new semver.Range("*")
found = {...entry, constraint}
} else {
//TODO more efficient to check the prefix fits arg0 first
// eg. if python3 then check if the provides starts with python before
// doing all the regex shit. Matters because there's a *lot* of YAMLs

let rx = /({{\s*version\.(marketing|major)\s*}})/
let match = provider.match(rx)
if (!match?.index) continue
const regx = match[2] == 'major' ? '\\d+' : '\\d+\\.\\d+'
const foo = subst(match.index, match.index + match[1].length, provider, `(${regx})`)
rx = new RegExp(`^${foo}$`)
match = arg0.match(rx)
if (match) {
const constraint = new semver.Range(match[1])
found = {...entry, constraint}
}
}
}
})
promises.push(p)
}

if (!found) {
// if we didn’t find anything yet then we have to wait on the promises
// otherwise we can ignore them
await Promise.all(promises)
}

if (found) {
return {...found, constraint: new semver.Range("*"), shebang: arg0}
return {...found, shebang: arg0}
}
}

Expand Down Expand Up @@ -298,7 +323,7 @@ function supp(env: Record<string, string>, blueprint?: VirtualEnv) {
import { basename } from "deno/path/mod.ts"
import { isArray, isNumber } from "is_what"

async function repl(installations: Installation[], env: Record<string, string>) {
async function regx(installations: Installation[], env: Record<string, string>) {
const pkgs_str = () => installations.map(({pkg}) => gray(pkgutils.str(pkg))).join(", ")
console.info('this is a temporary shell containing the following packages:')
console.info(pkgs_str())
Expand Down Expand Up @@ -346,7 +371,7 @@ type RV2 = RV1 |
{ type: "url", url: URL, args: string[] } |
{ type: "dir", path: Path, args: string[] }
type RV3 = RV2 |
{ type: 'repl' }
{ type: 'regx' }

function assess_file(path: Path, args: string[]): RV0 {
return isMarkdown(path)
Expand All @@ -356,7 +381,7 @@ function assess_file(path: Path, args: string[]): RV0 {

function assess([arg0, ...args]: string[]): RV3 {
if (!arg0?.trim()) {
return { type: 'repl' }
return { type: 'regx' }
}
const url = urlify(arg0)
if (url) {
Expand Down
1 change: 1 addition & 0 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ try {
}
} catch (err) {
await err_handler(err)
Deno.exit(1)
}

async function announce() {
Expand Down
5 changes: 3 additions & 2 deletions src/hooks/useLogger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,11 @@ export default function useLogger(prefix?: string) {
}

function colorIfTTY(x: string, colorMethod: (x: string)=>string) {
if(Deno.isatty(Deno.stdout.rid) && Deno.isatty(Deno.stderr.rid)) {
if (Deno.isatty(Deno.stdout.rid) && Deno.isatty(Deno.stderr.rid)) {
return colorMethod(x)
} else {
return x
}
return x
}

export const teal = (x: string) => colorIfTTY(x, (x) => colors.rgb8(x, 86))
Expand Down
25 changes: 25 additions & 0 deletions tests/integration/tea-XX.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { assertEquals } from "deno/testing/asserts.ts"
import { sandbox } from '../utils.ts'

//TODO verify that python version is actually what we request

Deno.test("tea -X python", async () => {
await sandbox(async ({ run }) => {
const out = await run({args: ["-SX", "python", "-c", "print(1)"], net: true }).stdout()
assertEquals(out, "1\n")
})
})

Deno.test("tea -SX python3", async () => {
await sandbox(async ({ run }) => {
const out = await run({args: ["-SX", "python3", "-c", "print(2)"], net: true }).stdout()
assertEquals(out, "2\n")
})
})

Deno.test("tea -SX python3.11", async () => {
await sandbox(async ({ run }) => {
const out = await run({args: ["-SX", "python3.11", "-c", "print(3)"], net: true }).stdout()
assertEquals(out, "3\n")
})
})
36 changes: 21 additions & 15 deletions tests/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ interface Tea {
tmpdir: Path
}

export async function sandbox<T>(body: (tea: Tea) => Promise<T>) {
export async function sandbox<T>(body: (tea: Tea) => Promise<T>, { throws }: { throws: boolean } = {throws: true}) {
const TEA_PREFIX = new Path(await Deno.makeTempDir({ prefix: "tea" }))

const existing_www_cache = Path.home().join(".tea/tea.xyz/var/www")
Expand All @@ -38,11 +38,11 @@ export async function sandbox<T>(body: (tea: Tea) => Promise<T>) {

const PATH = Deno.env.get("PATH")
const HOME = Deno.env.get("HOME")
const CI = Deno.env.get("HOME")
if (!env) env = {}
Object.assign(env, {
PATH,
TEA_PREFIX: TEA_PREFIX.string,
HOME
PATH, HOME, CI,
TEA_PREFIX: TEA_PREFIX.string
})

cmd.push(
Expand All @@ -54,26 +54,32 @@ export async function sandbox<T>(body: (tea: Tea) => Promise<T>) {
)

let stdout: "piped" | undefined
let proc: Deno.Process | undefined

// we delay instantiating the proc so we can set `stdout` if the user calls that function
// so the contract is the user must call `stdout` within this event loop iteration
const p = Promise.resolve().then(() => {
proc = Deno.run({ cmd, cwd: TEA_PREFIX.string, stdout, env, clearEnv: true})
return proc.status()
const p = Promise.resolve().then(async () => {
const proc = Deno.run({ cmd, cwd: TEA_PREFIX.string, stdout, env, clearEnv: true})
try {
const status = await proc.status()
if (throws && !status.success) {
throw status
}
if (stdout == 'piped') {
const out = await proc.output()
return new TextDecoder().decode(out)
} else {
return status
}
} finally {
proc.close()
}
}) as Promise<number> & Enhancements

p.stdout = () => {
stdout = "piped"
return p.then(async () => {
const out = await proc!.output()
return new TextDecoder().decode(out)
})
return p as unknown as Promise<string>
}

p.then(() => proc!.close())
p.catch(() => proc?.close())

return p
}

Expand Down

0 comments on commit a14735b

Please sign in to comment.