Skip to content
Merged
Show file tree
Hide file tree
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
25 changes: 17 additions & 8 deletions mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,25 @@ import Path from "./src/utils/Path.ts"
export * as types from "./src/types.ts"
import * as pkg from "./src/utils/pkg.ts"

import TeaError, { panic } from "./src/utils/error.ts"
import { panic, TeaError } from "./src/utils/error.ts"
import useConfig from "./src/hooks/useConfig.ts"
import useOffLicense from "./src/hooks/useOffLicense.ts"
import useCache from "./src/hooks/useCache.ts"
import useCellar from "./src/hooks/useCellar.ts"
import useCellar, { InstallationNotFoundError} from "./src/hooks/useCellar.ts"
import useMoustaches from "./src/hooks/useMoustaches.ts"
import usePantry from "./src/hooks/usePantry.ts"
import usePantry, { PantryError, PantryParseError, PantryNotFoundError, PackageNotFoundError } from "./src/hooks/usePantry.ts"
import useFetch from "./src/hooks/useFetch.ts"
import useDownload from "./src/hooks/useDownload.ts"
import useDownload, { DownloadError } from "./src/hooks/useDownload.ts"
import useShellEnv from "./src/hooks/useShellEnv.ts"
import useInventory from "./src/hooks/useInventory.ts"
import hydrate from "./src/plumbing/hydrate.ts"
import which from "./src/plumbing/which.ts"
import link from "./src/plumbing/link.ts"
import install, { ConsoleLogger } from "./src/plumbing/install.ts"
import resolve from "./src/plumbing/resolve.ts"
import resolve, { ResolveError } from "./src/plumbing/resolve.ts"
import { validatePackageRequirement } from "./src/utils/hacks.ts"
import useSync from "./src/hooks/useSync.ts"
import run from "./src/porcelain/run.ts"
import run, { RunError } from "./src/porcelain/run.ts"
import porcelain_install from "./src/porcelain/install.ts"

const utils = {
Expand Down Expand Up @@ -64,10 +64,19 @@ const hacks = {
validatePackageRequirement
}

export { utils, hooks, plumbing, porcelain, hacks, semver }
export {
utils, hooks, plumbing, porcelain, hacks,
semver,
TeaError,
RunError,
ResolveError,
PantryError, PantryParseError, PantryNotFoundError, PackageNotFoundError,
InstallationNotFoundError,
DownloadError
}

/// export types
// we cannot add these to the above objects or they cannot be used as types
export { TeaError, Path, SemVer }
export { Path, SemVer }
export * from "./src/types.ts"
export type { SupportedArchitecture, SupportedPlatform }
15 changes: 14 additions & 1 deletion src/hooks/useCellar.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { assertEquals } from "deno/testing/asserts.ts"
import SemVer, * as semver from "../utils/semver.ts"
import { useTestConfig } from "./useTestConfig.ts"
import install from "../plumbing/install.ts"
import useCellar from "./useCellar.ts"

Deno.test("resolve()", async () => {
Deno.test("useCellar.resolve()", async () => {
useTestConfig()

const pkgrq = { project: "python.org", version: new SemVer("3.11.3")}
Expand All @@ -14,3 +15,15 @@ Deno.test("resolve()", async () => {
await useCellar().resolve({ project: "python.org", constraint: new semver.Range("^3") })
await useCellar().resolve(installation.path)
})

Deno.test("useCellar.has()", async () => {
useTestConfig()

const rq = { project: "beyondgrep.com", version: new SemVer("3.6.0") }

assertEquals(await useCellar().has(rq), undefined)

const installation = await install(rq)

assertEquals(await useCellar().has(rq), installation)
})
17 changes: 14 additions & 3 deletions src/hooks/useCellar.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
import { Package, PackageRequirement, Installation } from "../types.ts"
import { TeaError } from "../utils/error.ts"
import * as pkgutils from "../utils/pkg.ts"
import SemVer from "../utils/semver.ts"
import useConfig from "./useConfig.ts"
import Path from "../utils/Path.ts"

export class InstallationNotFoundError extends TeaError {
pkg: Package | PackageRequirement

constructor(pkg: Package | PackageRequirement) {
super(`not found: ${pkgutils.str(pkg)}`)
this.pkg = pkg
}
}

export default function useCellar() {
const config = useConfig()

Expand All @@ -14,7 +24,7 @@ export default function useCellar() {
const keg = (pkg: Package) => shelf(pkg.project).join(`v${pkg.version}`)

/// returns the `Installation` if the pkg is installed
const has = (pkg: Package | PackageRequirement | Path) => resolve(pkg).swallow(/^not-found:/)
const has = (pkg: Package | PackageRequirement | Path) => resolve(pkg).swallow(InstallationNotFoundError)

return {
has,
Expand Down Expand Up @@ -70,12 +80,13 @@ export default function useCellar() {
if (version) {
const path = installations.find(({pkg: {version: v}}) => v.eq(version))!.path
return { path, pkg: { project: pkg.project, version } }
} else {
throw new InstallationNotFoundError(pkg)
}
}
throw new Error(`not-found:${pkgutils.str(pkg)}`)
})()
if (await vacant(installation.path)) {
throw new Error(`not-found: ${pkgutils.str(installation.pkg)}`)
throw new InstallationNotFoundError(installation.pkg)
}
return installation
}
Expand Down
72 changes: 41 additions & 31 deletions src/hooks/useDownload.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { deno } from "../deps.ts"
const { crypto: crypto_, streams: { writeAll } } = deno
const { toHashString, crypto } = crypto_
import TeaError, { panic } from "../utils/error.ts"
import { TeaError, panic } from "../utils/error.ts"
import useConfig from "./useConfig.ts"
import useFetch from "./useFetch.ts"
import Path from "../utils/Path.ts"
Expand All @@ -15,42 +15,52 @@ interface DownloadOptions {
logger?: (info: {src: URL, dst: Path, rcvd?: number, total?: number }) => void
}

export class DownloadError extends TeaError {
status: number
src: URL
headers?: Record<string, string>

constructor(status: number, opts: { src: URL, headers?: Record<string, string>}) {
super(`http: ${status}: ${opts.src}`)
this.name = 'DownloadError'
this.status = status
this.src = opts.src
this.headers = opts.headers
}
}

const tmpname = (dst: Path) => dst.parent().join(dst.basename() + ".incomplete")

async function download(opts: DownloadOptions, chunk?: (blob: Uint8Array) => Promise<void>): Promise<Path> {
try {
const [dst, stream] = await the_meat(opts)

if (stream || chunk) {
const reader = stream ?? fs.createReadStream(dst.string)

const writer = await (() => {
if (stream) {
dst.parent().mkdir('p')
return Deno.open(tmpname(dst).string, {write: true, create: true, truncate: true})
}
})()

for await (const blob of reader) {
const pp: Promise<void>[] = []
if (writer) pp.push(writeAll(writer, blob))
if (chunk) pp.push(chunk(blob))
await Promise.all(pp)
}
const [dst, stream] = await the_meat(opts)

if (reader instanceof fs.ReadStream) {
reader.close()
}
if (writer) {
writer.close()
tmpname(dst).mv({ to: dst, force: true })
if (stream || chunk) {
const reader = stream ?? fs.createReadStream(dst.string)

const writer = await (() => {
if (stream) {
dst.parent().mkdir('p')
return Deno.open(tmpname(dst).string, {write: true, create: true, truncate: true})
}
})()

for await (const blob of reader) {
const pp: Promise<void>[] = []
if (writer) pp.push(writeAll(writer, blob))
if (chunk) pp.push(chunk(blob))
await Promise.all(pp)
}

return dst
} catch (cause) {
throw new TeaError('http', {cause, ...opts})
if (reader instanceof fs.ReadStream) {
reader.close()
}
if (writer) {
writer.close()
tmpname(dst).mv({ to: dst, force: true })
}
}

return dst
}

function cache({ for: url }: {for: URL}): Path {
Expand Down Expand Up @@ -79,7 +89,7 @@ export default function useDownload() {

/// internal

async function the_meat<T>({ src, headers, logger, dst }: DownloadOptions): Promise<[Path, ReadableStream<Uint8Array> | undefined, number | undefined]>
async function the_meat<T>({ src, logger, headers, dst }: DownloadOptions): Promise<[Path, ReadableStream<Uint8Array> | undefined, number | undefined]>
{
const hash = cache({ for: src })
const mtime_entry = hash.join("mtime")
Expand Down Expand Up @@ -136,6 +146,6 @@ async function the_meat<T>({ src, headers, logger, dst }: DownloadOptions): Prom
return [dst, undefined, sz]
}
default:
throw new Error(`${rsp.status}: ${src}`)
throw new DownloadError(rsp.status, { src, headers })
}
}
10 changes: 3 additions & 7 deletions src/hooks/useInventory.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Package, PackageRequirement } from "../types.ts"
import TeaError, * as error from "../utils/error.ts"
import { DownloadError } from "./useDownload.ts"
import SemVer from "../utils/semver.ts"
import useFetch from "./useFetch.ts"
import host from "../utils/host.ts"
Expand Down Expand Up @@ -33,8 +33,7 @@ const get = async (rq: PackageRequirement | Package) => {
const rsp = await useFetch(url)

if (!rsp.ok) {
const cause = new Error(`${rsp.status}: ${url}`)
throw new TeaError('http', {cause})
throw new DownloadError(rsp.status, {src: url})
}

const releases = await rsp.text()
Expand All @@ -52,10 +51,7 @@ const get = async (rq: PackageRequirement | Package) => {
}

export default function useInventory() {
return {
select: error.wrap(select, 'http'),
get
}
return { select, get }
}

export const _internals = { get }
52 changes: 42 additions & 10 deletions src/hooks/usePantry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ const { isNumber, isPlainObject, isString, isArray, isPrimitive, isBoolean } = i
import { validatePackageRequirement } from "../utils/hacks.ts"
import { Package, Installation } from "../types.ts"
import useMoustaches from "./useMoustaches.ts"
import { TeaError } from "../utils/error.ts"
import { validate } from "../utils/misc.ts"
import TeaError from "../utils/error.ts"
import SemVer from "../utils/semver.ts"
import useConfig from "./useConfig.ts"
import host from "../utils/host.ts"
Expand All @@ -15,6 +15,35 @@ export interface Interpreter {
args: string[]
}

export class PantryError extends TeaError
{}

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 config = useConfig()
const prefix = config.prefix.join('tea.xyz/var/pantry/projects')
Expand All @@ -35,7 +64,7 @@ export default function usePantry() {

const yaml = (() => {
for (const prefix of pantry_paths()) {
if (!prefix.exists()) throw new TeaError('not-found: pantry', { path: prefix.parent() })
if (!prefix.exists()) throw new PantryNotFoundError(prefix.parent())
const dir = prefix.join(project)
const filename = dir.join("package.yml")
if (!filename.exists()) continue
Expand All @@ -44,9 +73,9 @@ export default function usePantry() {

return () => memo ?? (memo = filename.readYAML()
.then(validate.obj)
.catch(cause => { throw new TeaError('parser: pantry: package.yml', {cause, project, filename}) }))
.catch(cause => { throw new PantryParseError(project, filename, cause) }))
}
throw new TeaError('not-found: pantry: package.yml', {project}, )
throw new PackageNotFoundError(project)
})()

const companions = async () => parse_pkgs_node((await yaml())["companions"])
Expand All @@ -61,7 +90,7 @@ export default function usePantry() {
let { platforms } = await yaml()
if (!platforms) return true
if (isString(platforms)) platforms = [platforms]
if (!isArray(platforms)) throw new Error("bad-yaml")
if (!isArray(platforms)) throw new PantryParseError(project)
return platforms.includes(host().platform) ||platforms.includes(`${host().platform}/${host().arch}`)
}

Expand All @@ -73,7 +102,7 @@ export default function usePantry() {
if (isPlainObject(node)) {
node = node[host().platform]
}
if (!isArray(node)) throw new Error("bad-yaml")
if (!isArray(node)) throw new PantryParseError(project)

return node.compact(x => {
if (isPlainObject(x)) {
Expand Down Expand Up @@ -180,7 +209,7 @@ export default function usePantry() {
}

if (rv.length == 0) {
throw new TeaError("not-found: pantry", {path: prefix})
throw new PantryNotFoundError(prefix)
}

return rv
Expand Down Expand Up @@ -253,7 +282,7 @@ export function expand_env_obj(env_: PlainObject, pkg: Package, deps: Installati

// deno-lint-ignore no-explicit-any
function transform(value: any): string {
if (!isPrimitive(value)) throw new Error(`invalid-env-value: ${JSON.stringify(value)}`)
if (!isPrimitive(value)) throw new PantryParseError(pkg.project, undefined, JSON.stringify(value))

if (isBoolean(value)) {
return value ? "1" : "0"
Expand All @@ -271,7 +300,10 @@ export function expand_env_obj(env_: PlainObject, pkg: Package, deps: Installati
} else if (isNumber(value)) {
return value.toString()
}
throw new Error("unexpected-error")

const e = new Error("unexpected error")
e.cause = value
throw e
}
}

Expand All @@ -281,7 +313,7 @@ interface LsEntry {
}

async function* _ls_pantry(dir: Path): AsyncGenerator<Path> {
if (!dir.isDirectory()) throw new TeaError('not-found: pantry', { path: dir })
if (!dir.isDirectory()) throw new PantryNotFoundError(dir)

for await (const [path, { name, isDirectory }] of dir.ls()) {
if (isDirectory) {
Expand Down
Loading