diff --git a/.ghjk/lock.json b/.ghjk/lock.json index 88c73206..42f2d6d5 100644 --- a/.ghjk/lock.json +++ b/.ghjk/lock.json @@ -17,11 +17,29 @@ "portRef": "act_ghrel@0.1.0", "specifiedVersion": false }, + "bciqj4p5hoqweghbuvz52rupja7sqze34z63dd62nz632c5zxikv6ezy": { + "version": "1.34", + "buildDepConfigs": {}, + "portRef": "tar_aa@0.1.0", + "specifiedVersion": false + }, + "bciqe6fwheayositrdk7rkr2ngdr4wizldakex23tgivss7w6z7g3q3y": { + "version": "v1.4.8,", + "buildDepConfigs": {}, + "portRef": "zstd_aa@0.1.0", + "specifiedVersion": false + }, + "bciqfvlwwndlfuqibybkgee3fgt7cst5ltpztmm3by6hib5veial5spy": { + "version": "v1.44.2", + "buildDepConfigs": {}, + "portRef": "deno_ghrel@0.1.0", + "specifiedVersion": true + }, "bciqao2s3r3r33ruox4qknfrxqrmemuccxn64dze2ylojrzp2bwvt4ji": { - "version": "3.7.1", + "version": "3.8.0", "buildDepConfigs": { "cpy_bs_ghrel": { - "version": "3.12.4", + "version": "3.12.5", "buildDepConfigs": { "tar_aa": { "version": "1.34", @@ -45,7 +63,7 @@ "specifiedVersion": false }, "bciqij3g6mmbjn4a6ps4eipcy2fmw2zumgv5a3gbxycthroffihwquoi": { - "version": "3.12.4", + "version": "3.12.5", "buildDepConfigs": { "tar_aa": { "version": "1.34", @@ -62,24 +80,6 @@ }, "portRef": "cpy_bs_ghrel@0.1.0", "specifiedVersion": false - }, - "bciqj4p5hoqweghbuvz52rupja7sqze34z63dd62nz632c5zxikv6ezy": { - "version": "1.34", - "buildDepConfigs": {}, - "portRef": "tar_aa@0.1.0", - "specifiedVersion": false - }, - "bciqe6fwheayositrdk7rkr2ngdr4wizldakex23tgivss7w6z7g3q3y": { - "version": "v1.4.8,", - "buildDepConfigs": {}, - "portRef": "zstd_aa@0.1.0", - "specifiedVersion": false - }, - "bciqfvlwwndlfuqibybkgee3fgt7cst5ltpztmm3by6hib5veial5spy": { - "version": "v1.44.2", - "buildDepConfigs": {}, - "portRef": "deno_ghrel@0.1.0", - "specifiedVersion": true } } }, diff --git a/examples/kitchen/.gitignore b/examples/kitchen/.gitignore new file mode 100644 index 00000000..21d0b898 --- /dev/null +++ b/examples/kitchen/.gitignore @@ -0,0 +1 @@ +.venv/ diff --git a/examples/kitchen/ghjk.ts b/examples/kitchen/ghjk.ts index a88897e4..138e12fc 100644 --- a/examples/kitchen/ghjk.ts +++ b/examples/kitchen/ghjk.ts @@ -1,6 +1,7 @@ import { stdDeps } from "../../files/mod.ts"; import { file } from "../../mod.ts"; import * as ports from "../../ports/mod.ts"; +import { pyEnv } from "../../std/py.ts"; const ghjk = file({ // configre an empty env so that no ports are avail by default in our workdir @@ -106,7 +107,8 @@ env("python") ports.cpy_bs({ version: "3.8.18", releaseTag: "20240224" }), ports.tar(), ports.zstd(), - ); + ) + .mixin(pyEnv()); env("dev") // we can inherit from many envs @@ -120,3 +122,6 @@ env("dev") workingDir: "..", fn: ($) => $`ls`, })); + +env("venv") + .inherit(["python"]); diff --git a/files/cookbook.ts b/files/cookbook.ts new file mode 100644 index 00000000..0b6c07a0 --- /dev/null +++ b/files/cookbook.ts @@ -0,0 +1,179 @@ +import { + EnvFinalizer, + FinalizedEnv, + InlineTaskHookProvision, + objectHashSafe, + TaskDefTyped, +} from "./mod.ts"; +import { + EnvRecipe, + EnvsModuleConfig, + WellKnownProvision, +} from "../modules/envs/types.ts"; +import { InstallSetRefProvision, unwrapZodRes } from "../port.ts"; +import { InstallSet, MergedEnvs } from "./merged_envs.ts"; +import envsValidators from "../modules/envs/types.ts"; +import getLogger from "../utils/logger.ts"; + +export type Final = ReturnType & { + envBaseResolved: null | string[]; +}; + +const logger = getLogger(import.meta); + +interface MergedEntries { + vars: Record; + dynVars: Record; +} + +export class Cookbook { + #moduleConfig: EnvsModuleConfig; + + constructor( + public installSets: Map, + public finalizedEnvs: Record, + public tasks: Map, + defaultEnv: string, + ) { + this.#moduleConfig = { + envs: {}, + defaultEnv, + envsNamed: {}, + }; + } + + public registerEnv(final: Final, merged: MergedEnvs) { + const recipe = new RecipeBuilder(this, merged).build(); + + const installSetId = this.#getInstallSetId(final, merged.installSet); + if (installSetId) { + const prov: InstallSetRefProvision = { + ty: "ghjk.ports.InstallSetRef", + setId: installSetId, + }; + recipe.provides.push(prov); + } + + const hash = objectHashSafe(recipe); + this.finalizedEnvs[final.key] = { + installSetId, + finalized: final, + merged, + envHash: hash, + }; + + logger.debug("registering env", { key: final.key, name: final.name, hash }); + this.#moduleConfig.envs[hash] = recipe; + if (final.name) { + this.#moduleConfig.envsNamed[final.name] = hash; + } + } + + get moduleConfig() { + return this.#moduleConfig; + } + + #getInstallSetId(final: Final, baseSet: InstallSet): string | undefined { + const installSet = this.installSets.get(final.installSetId); + if (installSet) { + installSet.installs = installSet.installs.union( + baseSet.installs, + ); + for ( + const [key, val] of Object.entries( + baseSet.allowedBuildDeps, + ) + ) { + // prefer the port dep config of the child over any + // similar deps in the base + if (!installSet.allowedBuildDeps[key]) { + installSet.allowedBuildDeps[key] = val; + } + } + return final.installSetId; + } // if there's no install set found under the id + else { + // implies that the env has not ports explicitly configured + if (final.envBaseResolved) { + // has a singluar parent + if (final.envBaseResolved.length == 1) { + return this.finalizedEnvs[final.envBaseResolved[0]] + .installSetId; + } else { + this.installSets.set( + final.installSetId, + baseSet, + ); + return final.installSetId; + } + } + } + } +} + +class RecipeBuilder { + constructor( + private book: Cookbook, + private compactEnv: MergedEnvs, + ) {} + + build(): EnvRecipe { + return { + desc: this.compactEnv.desc, + provides: [ + ...Object.entries(this.compactEnv.vars).map(([key, val]) => { + const prov: WellKnownProvision = { ty: "posix.envVar", key, val }; + return prov; + }), + ...Object.entries(this.compactEnv.dynVars).map(([key, val]) => { + const prov = { ty: "posix.envVarDyn", key, taskKey: val }; + return unwrapZodRes( + envsValidators.envVarDynProvision.safeParse(prov), + prov, + ); + }), + ...this.compactEnv.posixDirs, + ...this.compactEnv.dynamicPosixDirs, + // env hooks + ...this.#getHooks(), + ], + }; + } + + #getHooks(): InlineTaskHookProvision[] { + return [ + ...this.compactEnv.onEnterHookTasks.map( + (key) => [key, "hook.onEnter.ghjkTask"] as const, + ), + ...this.compactEnv.onExitHookTasks.map( + (key) => [key, "hook.onExit.ghjkTask"] as const, + ), + ].map(([taskKey, ty]) => { + const task = this.book.tasks.get(taskKey); + if (!task) { + throw new Error("unable to find task for onEnterHook", { + cause: { + env: this.compactEnv.name, + taskKey, + }, + }); + } + if (task.ty == "denoFile@v1") { + const prov: InlineTaskHookProvision = { + ty, + taskKey, + }; + return prov; + } + throw new Error( + `unsupported task type "${task.ty}" used for environment hook`, + { + cause: { + taskKey, + task, + }, + }, + ); + }); + } +} diff --git a/files/merged_envs.ts b/files/merged_envs.ts new file mode 100644 index 00000000..d43df7cb --- /dev/null +++ b/files/merged_envs.ts @@ -0,0 +1,166 @@ +import { deep_eql } from "../deps/common.ts"; +import { + DynamicPosixDirProvision, + PosixDirProvision, +} from "../modules/envs/types.ts"; +import getLogger from "../utils/logger.ts"; +import { Final } from "./cookbook.ts"; + +const logger = getLogger(import.meta); + +type Var = + | { kind: "static"; value: string; parentName: string } + | { kind: "dynamic"; taskId: string; parentName: string }; + +export interface InstallSet { + installs: Set; + allowedBuildDeps: Record; +} + +export interface MergedEnvs { + desc: string | undefined; + name: string | undefined; + installSet: InstallSet; + onEnterHookTasks: string[]; + onExitHookTasks: string[]; + vars: Record; + dynVars: Record; + posixDirs: PosixDirProvision[]; + dynamicPosixDirs: DynamicPosixDirProvision[]; +} + +export class ParentEnvs { + #childName: string; + #vars: Map = new Map(); + #posixDirs: Array = []; + #dynamicPosixDirs: Array = []; + #installs: Set = new Set(); + #onEnterHooks: string[] = []; + #onExitHooks: string[] = []; + #allowedBuildDeps: Map = new Map(); + + constructor(childName: string) { + this.#childName = childName; + } + + addHooks(onEnterHooks: string[], onExitHooks: string[]) { + this.#onEnterHooks.push(...onEnterHooks); + this.#onExitHooks.push(...onExitHooks); + } + + mergeVars(parentName: string, vars: Record) { + for (const [key, value] of Object.entries(vars)) { + const conflict = this.#vars.get(key); + + if ( + conflict && + !(conflict.kind === "static" && conflict.value === value) + ) { + logger.warn( + "environment variable conflict on multiple env inheritance, parent 2 was chosen", + { + child: this.#childName, + parent1: conflict.parentName, + parent2: parentName, + variable: key, + }, + ); + } + + this.#vars.set(key, { kind: "static", value, parentName }); + } + } + + mergeDynVars(parentName: string, dynVars: Record) { + for (const [key, taskId] of Object.entries(dynVars)) { + const conflict = this.#vars.get(key); + + if ( + conflict && + !(conflict.kind === "dynamic" && conflict.taskId === taskId) + ) { + logger.warn( + "dynamic environment variable conflict on multiple env inheritance, parent 2 was chosen", + { + child: this.#childName, + parent1: conflict.parentName, + parent2: parentName, + variable: key, + }, + ); + } + + this.#vars.set(key, { kind: "dynamic", taskId, parentName }); + } + } + + mergePosixDirs( + posixDirs: Array, + dynamicPosixDirs: Array, + ) { + this.#posixDirs.push(...posixDirs); + this.#dynamicPosixDirs.push(...dynamicPosixDirs); + } + + mergeInstalls( + parentName: string, + installs: Set, + allowedBuildDeps: Record, + ) { + this.#installs = this.#installs.union(installs); + + for (const [key, val] of Object.entries(allowedBuildDeps)) { + const conflict = this.#allowedBuildDeps.get(key); + if (conflict && !deep_eql(val, conflict[0])) { + logger.warn( + "allowedBuildDeps conflict on multiple env inheritance, parent 2 was chosen", + { + child: this.#childName, + parent1: conflict[1], + parent2: parentName, + variable: key, + }, + ); + } + + this.#allowedBuildDeps.set(key, [val, parentName]); + } + } + + withChild(child: Final): MergedEnvs { + const vars: Record = {}; + const dynVars: Record = {}; + + for (const [key, value] of this.#vars) { + if (value.kind === "static") { + vars[key] = value.value; + } else { + dynVars[key] = value.taskId; + } + } + + return { + desc: child.desc, + name: child.name, + // installSets are not merged here... + installSet: { + installs: this.#installs, + allowedBuildDeps: Object.fromEntries( + [...this.#allowedBuildDeps.entries()].map(([key, [val]]) => [ + key, + val, + ]), + ), + }, + onEnterHookTasks: [...this.#onEnterHooks, ...child.onEnterHookTasks], + onExitHookTasks: [...this.#onExitHooks, ...child.onExitHookTasks], + vars: { ...vars, ...child.vars }, + dynVars: { ...dynVars, ...child.dynVars }, + posixDirs: [...child.posixDirs, ...this.#posixDirs], + dynamicPosixDirs: [ + ...child.dynamicPosixDirs, + ...this.#dynamicPosixDirs, + ], + }; + } +} diff --git a/files/mod.ts b/files/mod.ts index 44c03228..46bf7097 100644 --- a/files/mod.ts +++ b/files/mod.ts @@ -4,14 +4,13 @@ // here to make the resulting config reasonably stable // across repeated serializaitons. No random identifiers. -import { deep_eql, multibase32, multibase64, zod } from "../deps/common.ts"; +import { multibase32, multibase64, zod } from "../deps/common.ts"; // ports specific imports import portsValidators from "../modules/ports/types.ts"; import type { AllowedPortDep, InstallConfigFat, - InstallSetRefProvision, PortsModuleConfigHashed, } from "../modules/ports/types.ts"; import getLogger from "../utils/logger.ts"; @@ -37,13 +36,17 @@ import type { ExecTaskArgs } from "../modules/tasks/deno.ts"; import { TaskDefHashed, TasksModuleConfig } from "../modules/tasks/types.ts"; // envs import { - type EnvRecipe, + DynamicPosixDirProvision, type EnvsModuleConfig, + PosixDirProvision, + PosixDirProvisionType, type Provision, type WellKnownProvision, } from "../modules/envs/types.ts"; import envsValidators from "../modules/envs/types.ts"; import modulesValidators from "../modules/types.ts"; +import { InstallSet, MergedEnvs, ParentEnvs } from "./merged_envs.ts"; +import { Cookbook, Final } from "./cookbook.ts"; const validators = { envVars: zod.record( @@ -71,6 +74,9 @@ export type EnvDefArgs = { inherit?: EnvParent; desc?: string; vars?: Record; + execDirs?: string[]; + sharedLibDirs?: string[]; + headerDirs?: string[]; /** * Task to execute when environment is activated. */ @@ -120,7 +126,7 @@ export type DenoTaskDefArgs = TaskDefArgs & { * execution of a specific task, we identify each using a hash. * The {@field fn} is `toString`ed in the hash input. * If a ghjkfile is produing identical anonymous tasks for - * instance, it can provide a none to disambiguate beteween each + * instance, it can provide a nonce to disambiguate between each * through hash differences. * * NOTE: the nonce must be stable across serialization. @@ -131,27 +137,23 @@ export type DenoTaskDefArgs = TaskDefArgs & { nonce?: string; }; -type TaskDefTyped = DenoTaskDefArgs & { ty: "denoFile@v1" }; +export type TaskDefTyped = DenoTaskDefArgs & { ty: "denoFile@v1" }; + +export type FinalizedEnv = { + finalized: ReturnType; + installSetId?: string; + merged: MergedEnvs; + envHash: string; +}; export class Ghjkfile { - #installSets = new Map< - string, - { installs: Set; allowedBuildDeps: Record } - >(); + #installSets = new Map(); #seenInstallConfs = new Map(); #seenAllowedDepPorts = new Map(); #tasks = new Map(); #bb = new Map(); #seenEnvs: Record = {}; - #finalizedEnvs: Record< - string, - { - finalized: ReturnType; - installSetId?: string; - vars: Record; - envHash: string; - } - > = {}; + #finalizedEnvs: Record = {}; /* dump() { return { @@ -196,9 +198,7 @@ export class Ghjkfile { ) { const set = this.#getSet(setId); set.allowedBuildDeps = Object.fromEntries( - reduceAllowedDeps(deps).map(( - dep, - ) => { + reduceAllowedDeps(deps).map((dep) => { const hash = objectHashSafe(dep); this.#seenAllowedDepPorts.set(hash, dep); return [dep.manifest.name, hash]; @@ -234,9 +234,7 @@ export class Ghjkfile { } : {}), }); - key = multibase64.base64urlpad.encode( - multibase32.base32.decode(key), - ); + key = multibase64.base64urlpad.encode(multibase32.base32.decode(key)); break; } default: @@ -280,6 +278,21 @@ export class Ghjkfile { if (args.vars) { env.vars(args.vars); } + if (args.execDirs) { + for (const dir of args.execDirs) { + env.execDir(dir); + } + } + if (args.sharedLibDirs) { + for (const dir of args.sharedLibDirs) { + env.sharedLibDir(dir); + } + } + if (args.headerDirs) { + for (const dir of args.headerDirs) { + env.headerDir(dir); + } + } if (args.onEnter) { env.onEnter(...args.onEnter); } @@ -289,9 +302,7 @@ export class Ghjkfile { return env; } - async execTask( - { key, workingDir, envVars, argv }: ExecTaskArgs, - ) { + async execTask({ key, workingDir, envVars, argv }: ExecTaskArgs) { const task = this.#tasks.get(key); if (!task) { throw new Error(`no task defined under "${key}"`); @@ -315,13 +326,14 @@ export class Ghjkfile { } } - toConfig( - { defaultEnv, defaultBaseEnv }: { - defaultEnv: string; - defaultBaseEnv: string; - ghjkfileUrl: string; - }, - ) { + toConfig({ + defaultEnv, + defaultBaseEnv, + }: { + defaultEnv: string; + defaultBaseEnv: string; + ghjkfileUrl: string; + }) { // make sure referenced envs exist this.addEnv(defaultEnv, { name: defaultEnv }); this.addEnv(defaultBaseEnv, { name: defaultBaseEnv }); @@ -329,8 +341,10 @@ export class Ghjkfile { // crearte the envs used by the tasks const taskToEnvMap = {} as Record; for ( - const [key, { inherit, vars, installs, allowedBuildDeps }] of this.#tasks - .entries() + const [ + key, + { inherit, vars, installs, allowedBuildDeps }, + ] of this.#tasks.entries() ) { const envKey = `____task_env_${key}`; this.addEnv(envKey, { @@ -348,27 +362,29 @@ export class Ghjkfile { defaultBaseEnv, taskToEnvMap, ); - const tasksConfig = this.#processTasks( - envsConfig, - taskToEnvMap, - ); + const tasksConfig = this.#processTasks(envsConfig, taskToEnvMap); const portsConfig = this.#processInstalls(); const config: SerializedConfig = { blackboard: Object.fromEntries(this.#bb.entries()), - modules: [{ - id: std_modules.ports, - config: portsConfig, - }, { - id: std_modules.tasks, - config: tasksConfig, - }, { - id: std_modules.envs, - config: envsConfig, - }], + modules: [ + { + id: std_modules.ports, + config: portsConfig, + }, + { + id: std_modules.tasks, + config: tasksConfig, + }, + { + id: std_modules.envs, + config: envsConfig, + }, + ], }; return config; } catch (cause) { + logger.error(`error constructing config for serialization`, { cause }); throw new Error(`error constructing config for serialization`, { cause }); } } @@ -392,76 +408,35 @@ export class Ghjkfile { return hash; } - #mergeEnvs(keys: string[], childName: string) { - const mergedVars = {} as Record; - let mergedInstalls = new Set(); - const mergedOnEnterHooks = []; - const mergedOnExitHooks = []; - const mergedAllowedBuildDeps = {} as Record< - string, - [string, string] | undefined - >; - for (const parentName of keys) { - const { vars, installSetId, finalized } = this.#finalizedEnvs[parentName]; - mergedOnEnterHooks.push(...finalized.onEnterHookTasks); - mergedOnExitHooks.push(...finalized.onExitHookTasks); - for (const [key, val] of Object.entries(vars)) { - const conflict = mergedVars[key]; - // if parents share a parent themselves, they will have - // the same item so it's not exactly a conflict - if (conflict && val !== conflict[0]) { - logger.warn( - "environment variable conflict on multiple env inheritance, parent2 was chosen", - { - child: childName, - parent1: conflict[1], - parent2: parentName, - variable: key, - }, - ); - } - mergedVars[key] = [val, parentName]; - } - if (!installSetId) { - continue; - } - const set = this.#installSets.get(installSetId)!; - mergedInstalls = mergedInstalls.union(set.installs); - for ( - const [key, val] of Object.entries(set.allowedBuildDeps) - ) { - const conflict = mergedAllowedBuildDeps[key]; - if (conflict && !deep_eql(val, conflict[0])) { - logger.warn( - "allowedBuildDeps conflict on multiple env inheritance, parent2 was chosen", - { - child: childName, - parent1: conflict[1], - parent2: parentName, - depPort: key, - }, - ); - } - mergedAllowedBuildDeps[key] = [val, parentName]; + #mergeEnvs(final: Final) { + const parents = final.envBaseResolved ?? []; + logger.debug("merging envs", { base: parents, child: final.key }); + const childName = final.key; + const parentEnvs = new ParentEnvs(childName); + for (const parentName of parents) { + const { installSetId, merged: base } = this.#finalizedEnvs[parentName]; + // FIXME unique?? + parentEnvs.addHooks( + base.onEnterHookTasks, + base.onExitHookTasks, + ); + parentEnvs.mergeVars(parentName, base.vars); + parentEnvs.mergeDynVars(parentName, base.dynVars); + parentEnvs.mergePosixDirs( + base.posixDirs, + base.dynamicPosixDirs, + ); + if (installSetId) { + const set = this.#installSets.get(installSetId)!; + parentEnvs.mergeInstalls( + parentName, + set.installs, + set.allowedBuildDeps, + ); } } - const outInstallSet = { - installs: mergedInstalls, - allowedBuildDeps: Object.fromEntries( - Object.entries(mergedAllowedBuildDeps).map(( - [key, val], - ) => [key, val![0]]), - ), - }; - const outVars = Object.fromEntries( - Object.entries(mergedVars).map(([key, val]) => [key, val![0]]), - ); - return { - installSet: outInstallSet, - onEnterHookTasks: mergedOnEnterHooks, - onExitHookTasks: mergedOnExitHooks, - vars: outVars, - }; + + return parentEnvs.withChild(final); } #resolveEnvBases( @@ -524,7 +499,9 @@ export class Ghjkfile { const deps = new Map(); const revDeps = new Map(); for ( - const [_key, [_builder, finalizer]] of Object.entries(this.#seenEnvs) + const [_key, [_builder, finalizer]] of Object.entries( + this.#seenEnvs, + ) ) { const final = finalizer(); @@ -550,12 +527,13 @@ export class Ghjkfile { } } - const moduleConfig: EnvsModuleConfig = { - envs: {}, - defaultEnv, - envsNamed: {}, - }; const workingSet = indie; + const cookbook = new Cookbook( + this.#installSets, + this.#finalizedEnvs, + this.#tasks, + defaultEnv, + ); // console.log({ // indie, // deps, @@ -564,132 +542,9 @@ export class Ghjkfile { const item = workingSet.pop()!; const final = all[item]; - const base = this.#mergeEnvs(final.envBaseResolved ?? [], final.key); - // console.log({ parents: final.envBaseResolved, child: final.key, base }); + const mergedEnvs = this.#mergeEnvs(final); - const finalVars = { - ...base.vars, - ...final.vars, - }; - - let finalInstallSetId: string | undefined; - { - const installSet = this.#installSets.get(final.installSetId); - if (installSet) { - installSet.installs = installSet.installs - .union(base.installSet.installs); - for ( - const [key, val] of Object.entries(base.installSet.allowedBuildDeps) - ) { - // prefer the port dep config of the child over any - // similar deps in the base - if (!installSet.allowedBuildDeps[key]) { - installSet.allowedBuildDeps[key] = val; - } - } - finalInstallSetId = final.installSetId; - } // if there's no install set found under the id - else { - // implies that the env has not ports explicitly configured - if (final.envBaseResolved) { - // has a singluar parent - if (final.envBaseResolved.length == 1) { - finalInstallSetId = - this.#finalizedEnvs[final.envBaseResolved[0]].installSetId; - } else { - this.#installSets.set(final.installSetId, base.installSet); - finalInstallSetId = final.installSetId; - } - } - } - } - const hooks = [ - ...base.onEnterHookTasks.map( - (key) => [key, "hook.onEnter.ghjkTask"] as const, - ), - ...final.onEnterHookTasks.map( - (key) => [key, "hook.onEnter.ghjkTask"] as const, - ), - ...base.onExitHookTasks.map( - (key) => [key, "hook.onExit.ghjkTask"] as const, - ), - ...final.onExitHookTasks.map( - (key) => [key, "hook.onExit.ghjkTask"] as const, - ), - ].map(([taskKey, ty]) => { - const task = this.#tasks.get(taskKey); - if (!task) { - throw new Error("unable to find task for onEnterHook", { - cause: { - env: final.name, - taskKey, - }, - }); - } - if (task.ty == "denoFile@v1") { - const prov: InlineTaskHookProvision = { - ty, - taskKey, - }; - return prov; - } - throw new Error( - `unsupported task type "${task.ty}" used for environment hook`, - { - cause: { - taskKey, - task, - }, - }, - ); - }); - - // the actual final final recipe - const recipe: EnvRecipe = { - desc: final.desc, - provides: [ - ...Object.entries(finalVars).map(( - [key, val], - ) => { - const prov: WellKnownProvision = { ty: "posix.envVar", key, val }; - return prov; - }), - ...Object.entries(final.dynVars).map(( - [key, val], - ) => { - const prov = { ty: "posix.envVarDyn", key, taskKey: val }; - return unwrapZodRes( - envsValidators.envVarDynProvision.safeParse(prov), - prov, - ); - }), - // env hooks - ...hooks, - ], - }; - - if (finalInstallSetId) { - const prov: InstallSetRefProvision = { - ty: "ghjk.ports.InstallSetRef", - setId: finalInstallSetId, - }; - recipe.provides.push(prov); - } - - // hashing takes care of deduplication - const envHash = objectHashSafe(recipe); - this.#finalizedEnvs[final.key] = { - installSetId: finalInstallSetId, - vars: finalVars, - finalized: final, - envHash, - }; - // hashing takes care of deduplication - moduleConfig.envs[envHash] = recipe; - - if (final.name) { - moduleConfig.envsNamed[final.name] = envHash; - } + cookbook.registerEnv(final, mergedEnvs); for (const revDepKey of revDeps.get(final.key) ?? []) { const revDepDeps = deps.get(revDepKey)!; @@ -716,7 +571,8 @@ export class Ghjkfile { }, }); } - return moduleConfig; + + return cookbook.moduleConfig; } #processTasks( @@ -733,9 +589,9 @@ export class Ghjkfile { ); for (const [key, args] of this.#tasks) { if (args.dependsOn && args.dependsOn.length > 0) { - const depKeys = - (Array.isArray(args.dependsOn) ? args.dependsOn : [args.dependsOn]) - .map((nameOrKey) => nameToKey[nameOrKey] ?? nameOrKey); + const depKeys = ( + Array.isArray(args.dependsOn) ? args.dependsOn : [args.dependsOn] + ).map((nameOrKey) => nameToKey[nameOrKey] ?? nameOrKey); deps.set(key, depKeys); for (const depKey of depKeys) { const depRevDeps = revDeps.get(depKey); @@ -770,14 +626,15 @@ export class Ghjkfile { ? workingDir.toString() : workingDir, desc, - ...dependsOn + ...(dependsOn ? { dependsOn: (Array.isArray(dependsOn) ? dependsOn : [dependsOn]) - ?.map((keyOrHash) => - localToFinalKey[nameToKey[keyOrHash] ?? keyOrHash] + ?.map( + (keyOrHash) => + localToFinalKey[nameToKey[keyOrHash] ?? keyOrHash], ), } - : {}, + : {}), envKey: envHash, }; const taskHash = objectHash(def); @@ -837,26 +694,24 @@ export class Ghjkfile { // reduce task based env hooks for (const [_name, env] of Object.entries(envsConfig.envs)) { - env.provides = env.provides.map( - (prov) => { - if ( - prov.ty == "hook.onEnter.ghjkTask" || - prov.ty == "hook.onExit.ghjkTask" - ) { - const inlineProv = prov as InlineTaskHookProvision; - const taskKey = localToFinalKey[inlineProv.taskKey]; - const out: WellKnownProvision = { - ty: /onEnter/.test(prov.ty) - ? "hook.onEnter.posixExec" - : "hook.onExit.posixExec", - program: "ghjk", - arguments: ["x", taskKey], - }; - return out; - } - return prov; - }, - ); + env.provides = env.provides.map((prov) => { + if ( + prov.ty == "hook.onEnter.ghjkTask" || + prov.ty == "hook.onExit.ghjkTask" + ) { + const inlineProv = prov as InlineTaskHookProvision; + const taskKey = localToFinalKey[inlineProv.taskKey]; + const out: WellKnownProvision = { + ty: /onEnter/.test(prov.ty) + ? "hook.onEnter.posixExec" + : "hook.onExit.posixExec", + program: "ghjk", + arguments: ["x", taskKey], + }; + return out; + } + return prov; + }); } return moduleConfig; @@ -866,50 +721,48 @@ export class Ghjkfile { const out: PortsModuleConfigHashed = { sets: {}, }; - for ( - const [setId, set] of this.#installSets.entries() - ) { + for (const [setId, set] of this.#installSets.entries()) { out.sets[setId] = { - installs: [...set.installs.values()] - .map((instHash) => - this.#addToBlackboard(this.#seenInstallConfs.get(instHash)) - ), - allowedBuildDeps: this.#addToBlackboard(Object.fromEntries( - Object.entries(set.allowedBuildDeps).map( - ( - [key, depHash], - ) => [ + installs: [...set.installs.values()].map((instHash) => + this.#addToBlackboard(this.#seenInstallConfs.get(instHash)) + ), + allowedBuildDeps: this.#addToBlackboard( + Object.fromEntries( + Object.entries(set.allowedBuildDeps).map(([key, depHash]) => [ key, this.#addToBlackboard(this.#seenAllowedDepPorts.get(depHash)), - ], + ]), ), - )), + ), }; } return out; } } -type EnvFinalizer = () => { +export type EnvFinalizer = () => { key: string; name?: string; installSetId: string; inherit: string | string[] | boolean; vars: Record; dynVars: Record; + posixDirs: Array; + dynamicPosixDirs: Array; desc?: string; onEnterHookTasks: string[]; onExitHookTasks: string[]; }; -export type EnvDefArgsPartial = - & { name?: string } - & Omit; +export type EnvDefArgsPartial = { name?: string } & Omit; export type DynEnvValue = - | (() => string | number) - | (($_: typeof $) => string | number) - | (($_: typeof $) => Promise); + | ((...params: Parameters) => string | number) + | ((...params: Parameters) => Promise); + +export type DynamicPathVarFn = + | ((...params: Parameters) => string) + | ((...params: Parameters) => Promise); // // /** @@ -956,6 +809,8 @@ export class EnvBuilder { #inherit: string | string[] | boolean = true; #vars: Record = {}; #dynVars: Record = {}; + #posixDirs: Array = []; + #dynamicPosixDirs: Array = []; #desc?: string; #onEnterHookTasks: string[] = []; #onExitHookTasks: string[] = []; @@ -977,6 +832,8 @@ export class EnvBuilder { Object.entries(this.#vars).map(([key, val]) => [key, val.toString()]), ), dynVars: this.#dynVars, + posixDirs: this.#posixDirs, + dynamicPosixDirs: this.#dynamicPosixDirs, desc: this.#desc, onExitHookTasks: this.#onExitHookTasks, onEnterHookTasks: this.#onEnterHookTasks, @@ -1019,7 +876,8 @@ export class EnvBuilder { * Add multiple environment variable. */ vars(envVars: Record) { - const vars = {}, dynVars = {}; + const vars = {}, + dynVars = {}; for (const [k, v] of Object.entries(envVars)) { switch (typeof v) { case "string": @@ -1046,13 +904,56 @@ export class EnvBuilder { this.#vars, unwrapZodRes(validators.envVars.safeParse(vars), { envVars: vars }), ); - Object.assign( - this.#dynVars, - dynVars, - ); + Object.assign(this.#dynVars, dynVars); + return this; + } + + /** + * Add a directory to the path environment variable + * $PATH, $LD_LIBRARY_PATH, $INCLUDE_PATH, etc. + */ + posixDir(type: PosixDirProvisionType, val: string | DynamicPathVarFn) { + switch (typeof val) { + case "string": { + const prov = { ty: type, path: val }; + this.#posixDirs.push( + unwrapZodRes(envsValidators.posixDirProvision.safeParse(prov), prov), + ); + break; + } + + case "function": { + const taskKey = this.#file.addTask({ + ty: "denoFile@v1", + fn: val, + }); + const prov = { ty: type + ".dynamic", taskKey }; + this.#dynamicPosixDirs.push( + unwrapZodRes( + envsValidators.dynamicPosixDirProvision.safeParse(prov), + prov, + ), + ); + break; + } + + default: + throw new Error(`type "${typeof val}" is not supported for path`); + } + return this; } + execDir(val: string | DynamicPathVarFn) { + return this.posixDir("posix.execDir", val); + } + sharedLibDir(val: string | DynamicPathVarFn) { + return this.posixDir("posix.sharedLibDir", val); + } + headerDir(val: string | DynamicPathVarFn) { + return this.posixDir("posix.headerDir", val); + } + /** * Description of the environment. */ @@ -1076,19 +977,26 @@ export class EnvBuilder { this.#onExitHookTasks.push(...taskKey); return this; } + + mixin( + setup: ( + builder: EnvBuilder, + ghjk: { task(args: DenoTaskDefArgs): void }, + ) => void, + ) { + setup(this, { + task: (args) => { + return this.#file.addTask({ ...args, ty: "denoFile@v1" }); + }, + }); + return this; + } } export function stdDeps(args = { enableRuntimes: false }) { - const out: AllowedPortDep[] = [ - ...Object.values(std_ports.map), - ]; + const out: AllowedPortDep[] = [...Object.values(std_ports.map)]; if (args.enableRuntimes) { - out.push( - ...reduceAllowedDeps([ - node.default(), - cpy.default(), - ]), - ); + out.push(...reduceAllowedDeps([node.default(), cpy.default()])); } return out; } @@ -1115,7 +1023,7 @@ function task$( return custom$; } -type InlineTaskHookProvision = Provision & { +export type InlineTaskHookProvision = Provision & { ty: "hook.onExit.ghjkTask" | "hook.onEnter.ghjkTask"; taskKey: string; }; @@ -1123,26 +1031,24 @@ type InlineTaskHookProvision = Provision & { export function reduceAllowedDeps( deps: (AllowedPortDep | InstallConfigFat)[], ): AllowedPortDep[] { - return deps.map( - (dep: any) => { - { - const res = portsValidators.allowedPortDep.safeParse(dep); - if (res.success) return res.data; - } - const inst = unwrapZodRes( - portsValidators.installConfigFat.safeParse(dep), - dep, - "invalid allowed dep object, provide either InstallConfigFat or AllowedPortDep objects", - ); - const out: AllowedPortDep = { - manifest: inst.port, - defaultInst: thinInstallConfig(inst), - }; - return portsValidators.allowedPortDep.parse(out); - }, - ); + return deps.map((dep: any) => { + { + const res = portsValidators.allowedPortDep.safeParse(dep); + if (res.success) return res.data; + } + const inst = unwrapZodRes( + portsValidators.installConfigFat.safeParse(dep), + dep, + "invalid allowed dep object, provide either InstallConfigFat or AllowedPortDep objects", + ); + const out: AllowedPortDep = { + manifest: inst.port, + defaultInst: thinInstallConfig(inst), + }; + return portsValidators.allowedPortDep.parse(out); + }); } -function objectHashSafe(obj: unknown) { +export function objectHashSafe(obj: unknown) { return objectHash(JSON.parse(JSON.stringify(obj))); } diff --git a/modules/envs/mod.ts b/modules/envs/mod.ts index 6258f72b..37785401 100644 --- a/modules/envs/mod.ts +++ b/modules/envs/mod.ts @@ -394,7 +394,7 @@ async function activateEnv(envKey: string) { const shell = await detectShellPath(); if (!shell) { throw new Error( - "unable to detct shell in use. Use `--shell` flag to explicitly pass shell program.", + "unable to detect shell in use. Use `--shell` flag to explicitly pass shell program.", ); } // FIXME: the ghjk process will be around and consumer resources diff --git a/modules/envs/posix.ts b/modules/envs/posix.ts index 5e775f50..e490864b 100644 --- a/modules/envs/posix.ts +++ b/modules/envs/posix.ts @@ -12,16 +12,21 @@ import getLogger from "../../utils/logger.ts"; const logger = getLogger(import.meta); -export async function cookPosixEnv( - { gcx, recipe, envKey, envDir, createShellLoaders = false }: { - gcx: GhjkCtx; - recipe: EnvRecipeX; - envKey: string; - envDir: string; - createShellLoaders?: boolean; - }, -) { +export async function cookPosixEnv({ + gcx, + recipe, + envKey, + envDir, + createShellLoaders = false, +}: { + gcx: GhjkCtx; + recipe: EnvRecipeX; + envKey: string; + envDir: string; + createShellLoaders?: boolean; +}) { logger.debug("cooking env", envKey, { envDir }); + // logger.debug("recipe", recipe); const reducedRecipe = await reduceStrangeProvisions(gcx, recipe); await $.removeIfExists(envDir); // create the shims for the user's environment @@ -38,6 +43,9 @@ export async function cookPosixEnv( const binPaths = [] as string[]; const libPaths = [] as string[]; const includePaths = [] as string[]; + const execDirs = [] as string[]; + const sharedLibDirs = [] as string[]; + const headerDirs = [] as string[]; const vars = { GHJK_ENV: envKey, } as Record; @@ -46,67 +54,69 @@ export async function cookPosixEnv( // FIXME: detect shim conflicts // FIXME: better support for multi installs - await Promise.all(reducedRecipe.provides.map((item) => { - if (!wellKnownProvisionTypes.includes(item.ty)) { - return Promise.resolve(); - } + await Promise.all( + reducedRecipe.provides.map((item) => { + if (!wellKnownProvisionTypes.includes(item.ty)) { + return Promise.resolve(); + } - const wellKnownProv = item as WellKnownProvision; - switch (wellKnownProv.ty) { - case "posix.exec": - binPaths.push(wellKnownProv.absolutePath); - break; - case "posix.sharedLib": - libPaths.push(wellKnownProv.absolutePath); - break; - case "posix.headerFile": - includePaths.push(wellKnownProv.absolutePath); - break; - // case "posix.envVarDyn": - case "posix.envVar": - if (vars[wellKnownProv.key]) { - throw new Error( - `env var conflict cooking unix env: key "${wellKnownProv.key}" has entries "${ - vars[wellKnownProv.key] - }" and "${wellKnownProv.val}"`, + const wellKnownProv = item as WellKnownProvision; + switch (wellKnownProv.ty) { + case "posix.exec": + binPaths.push(wellKnownProv.absolutePath); + break; + case "posix.sharedLib": + libPaths.push(wellKnownProv.absolutePath); + break; + case "posix.headerFile": + includePaths.push(wellKnownProv.absolutePath); + break; + // case "posix.envVarDyn": + case "posix.envVar": + if (vars[wellKnownProv.key]) { + throw new Error( + `env var conflict cooking unix env: key "${wellKnownProv.key}" has entries "${ + vars[wellKnownProv.key] + }" and "${wellKnownProv.val}"`, + ); + } + vars[wellKnownProv.key] = wellKnownProv.val; + // installSetIds.push(wellKnownProv.installSetIdProvision!.id); + break; + case "posix.execDir": + execDirs.push(wellKnownProv.path); + break; + case "posix.sharedLibDir": + sharedLibDirs.push(wellKnownProv.path); + break; + case "posix.headerDir": + headerDirs.push(wellKnownProv.path); + break; + case "hook.onEnter.posixExec": + onEnterHooks.push([wellKnownProv.program, wellKnownProv.arguments]); + break; + case "hook.onExit.posixExec": + onExitHooks.push([wellKnownProv.program, wellKnownProv.arguments]); + break; + case "ghjk.ports.Install": + // do nothing + break; + default: + throw Error( + `unsupported provision type: ${(wellKnownProv as any).ty}`, ); - } - vars[wellKnownProv.key] = wellKnownProv.val; - // installSetIds.push(wellKnownProv.installSetIdProvision!.id); - break; - case "hook.onEnter.posixExec": - onEnterHooks.push([wellKnownProv.program, wellKnownProv.arguments]); - break; - case "hook.onExit.posixExec": - onExitHooks.push([wellKnownProv.program, wellKnownProv.arguments]); - break; - case "ghjk.ports.Install": - // do nothing - break; - default: - throw Error( - `unsupported provision type: ${(wellKnownProv as any).ty}`, - ); - } - })); - void await Promise.all([ + } + }), + ); + void (await Promise.all([ // bin shims - await shimLinkPaths( - binPaths, - binShimDir, - ), + await shimLinkPaths(binPaths, binShimDir), // lib shims - await shimLinkPaths( - libPaths, - libShimDir, - ), + await shimLinkPaths(libPaths, libShimDir), // include shims - await shimLinkPaths( - includePaths, - includeShimDir, - ), + await shimLinkPaths(includePaths, includeShimDir), $.path(envDir).join("recipe.json").writeJsonPretty(reducedRecipe), - ]); + ])); // FIXME: prevent malicious env manipulations let LD_LIBRARY_ENV: string; switch (Deno.build.os) { @@ -119,12 +129,15 @@ export async function cookPosixEnv( default: throw new Error(`unsupported os ${Deno.build.os}`); } + execDirs.push(`${envDir}/shims/bin`); + sharedLibDirs.push(`${envDir}/shims/lib`); + headerDirs.push(`${envDir}/shims/include`); const pathVars = { - PATH: `${envDir}/shims/bin`, - LIBRARY_PATH: `${envDir}/shims/lib`, - [LD_LIBRARY_ENV]: `${envDir}/shims/lib`, - C_INCLUDE_PATH: `${envDir}/shims/include`, - CPLUS_INCLUDE_PATH: `${envDir}/shims/include`, + PATH: execDirs.join(":"), + LIBRARY_PATH: sharedLibDirs.join(":"), + [LD_LIBRARY_ENV]: sharedLibDirs.join(":"), + C_INCLUDE_PATH: headerDirs.join(":"), + CPLUS_INCLUDE_PATH: headerDirs.join(":"), }; if (createShellLoaders) { // write loader for the env vars mandated by the installs @@ -146,10 +159,7 @@ export async function cookPosixEnv( } /// This expands globs found in the targetPaths -async function shimLinkPaths( - targetPaths: string[], - shimDir: Path, -) { +async function shimLinkPaths(targetPaths: string[], shimDir: Path) { // map of filename to shimPath const shims: Record = {}; // a work sack to append to incase there are globs expanded @@ -158,8 +168,9 @@ async function shimLinkPaths( const file = foundTargetPaths.pop()!; if (std_path.isGlob(file)) { foundTargetPaths.push( - ...(await Array.fromAsync(std_fs.expandGlob(file))) - .map((entry) => entry.path), + ...(await Array.fromAsync(std_fs.expandGlob(file))).map( + (entry) => entry.path, + ), ); continue; } @@ -205,9 +216,7 @@ async function writeActivators( const shareDirVar = "_ghjk_share_dir"; pathVars = { ...Object.fromEntries( - Object.entries(pathVars).map(( - [key, val], - ) => [ + Object.entries(pathVars).map(([key, val]) => [ key, val .replace(gcx.ghjkDir.toString(), "$" + ghjkDirVar) @@ -224,7 +233,8 @@ async function writeActivators( ); const onExitHooksEscaped = onExitHooks.map(([cmd, args]) => [cmd == "ghjk" ? ghjkShimName : cmd, ...args] - .join(" ").replaceAll("'", "'\\''") + .join(" ") + .replaceAll("'", "'\\''") ); // ghjk.sh sets the DENO_DIR so we can usually @@ -255,7 +265,9 @@ async function writeActivators( // be differeint than `key` // TODO: avoid invalid key values elsewhere const safeComparisionKey = `$\{${key}:-_${ - val.replace(/['"]/g, "").slice(0, 2) + val + .replace(/['"]/g, "") + .slice(0, 2) }}`; return [ // we only restore the old $KEY value at cleanup if value of $KEY @@ -368,8 +380,8 @@ async function writeActivators( , ``, ` # on exit hooks`, - ...onExitHooksEscaped.map((cmd) => - ` set --global --append GHJK_CLEANUP_FISH '${cmd};';` + ...onExitHooksEscaped.map( + (cmd) => ` set --global --append GHJK_CLEANUP_FISH '${cmd};';`, ), `end`, ], diff --git a/modules/envs/reducer.ts b/modules/envs/reducer.ts index 64cfd708..04ff5f12 100644 --- a/modules/envs/reducer.ts +++ b/modules/envs/reducer.ts @@ -4,12 +4,17 @@ import { getTasksCtx } from "../tasks/inter.ts"; import type { GhjkCtx } from "../types.ts"; import type { EnvRecipeX, + PosixDirProvisionType, Provision, ProvisionReducer, WellKnownEnvRecipeX, WellKnownProvision, } from "./types.ts"; -import { envVarDynTy, wellKnownProvisionTypes } from "./types.ts"; +import { + envVarDynTy, + posixDirProvisionTypes, + wellKnownProvisionTypes, +} from "./types.ts"; import validators from "./types.ts"; export type ProvisionReducerStore = Map< @@ -22,13 +27,9 @@ export type ProvisionReducerStore = Map< * environment provisions, {@link ProvisionReducer}s can be registered * here. */ -export function getProvisionReducerStore( - gcx: GhjkCtx, -) { +export function getProvisionReducerStore(gcx: GhjkCtx) { const id = "provisionReducerStore"; - let store = gcx.blackboard.get(id) as - | ProvisionReducerStore - | undefined; + let store = gcx.blackboard.get(id) as ProvisionReducerStore | undefined; if (!store) { store = new Map(); gcx.blackboard.set(id, store); @@ -37,6 +38,15 @@ export function getProvisionReducerStore( envVarDynTy, installDynEnvReducer(gcx) as ProvisionReducer, ); + for (const ty of posixDirProvisionTypes) { + store?.set( + ty + ".dynamic", + installDynamicPathVarReducer(gcx, ty) as ProvisionReducer< + Provision, + Provision + >, + ); + } return store; } @@ -45,10 +55,7 @@ export function getProvisionReducerStore( * {@link WellKnownProvision}, looks for reducers in * {@link ProvisionReducer} to convert it to one. */ -export async function reduceStrangeProvisions( - gcx: GhjkCtx, - env: EnvRecipeX, -) { +export async function reduceStrangeProvisions(gcx: GhjkCtx, env: EnvRecipeX) { const reducerStore = getProvisionReducerStore(gcx); // Replace by `Object.groupBy` once the types for it are fixed const bins = {} as Record; @@ -112,7 +119,7 @@ export function installDynEnvReducer(gcx: GhjkCtx) { if (targetKey) { // console.log("key", key, " maps to target ", targetKey); const results = await execTask(gcx, taskConf, taskGraph, targetKey, []); - output.push({ ...provision, ty, val: results[key] as any ?? "" }); + output.push({ ...provision, ty, val: (results[key] as any) ?? "" }); } else { badProvisions.push(provision); } @@ -126,3 +133,40 @@ export function installDynEnvReducer(gcx: GhjkCtx) { return output; }; } + +export function installDynamicPathVarReducer( + gcx: GhjkCtx, + ty: PosixDirProvisionType, +) { + return async (provisions: Provision[]) => { + const output = []; + const badProvisions = []; + const taskCtx = getTasksCtx(gcx); + + for (const provision of provisions) { + const key = provision.taskKey as string; + + const taskGraph = taskCtx.taskGraph; + const taskConf = taskCtx.config; + + const targetKey = Object.entries(taskConf.tasks).find( + ([_, task]) => task.key === key, + )?.[0]; + + if (targetKey) { + const results = await execTask(gcx, taskConf, taskGraph, targetKey, []); + output.push({ ...provision, ty, path: results[key] as string }); + } else { + badProvisions.push(provision); + } + + if (badProvisions.length >= 1) { + throw new Error("cannot deduce task from keys", { + cause: { badProvisions }, + }); + } + + return output; + } + }; +} diff --git a/modules/envs/types.ts b/modules/envs/types.ts index 1a0372cd..22cfdbd7 100644 --- a/modules/envs/types.ts +++ b/modules/envs/types.ts @@ -12,6 +12,27 @@ const posixFileProvisionTypes = [ "posix.headerFile", ] as const; +export const posixDirProvisionTypes = [ + "posix.execDir", + "posix.sharedLibDir", + "posix.headerDir", +] as const; + +export type PosixDirProvisionType = typeof posixDirProvisionTypes[number]; + +const posixDirProvision = zod.object({ + ty: zod.enum(posixDirProvisionTypes), + path: absolutePath, +}); + +export type PosixDirProvision = zod.infer; + +const dynamicPosixDirProvisionTypes = [ + "posix.execDir.dynamic", + "posix.sharedLibDir.dynamic", + "posix.headerDir.dynamic", +] as const; + export const hookProvisionTypes = [ "hook.onEnter.posixExec", "hook.onExit.posixExec", @@ -27,6 +48,7 @@ export const envVarDynTy = "posix.envVarDyn"; // array in the interest of type inference export const wellKnownProvisionTypes = [ "posix.envVar", + ...posixDirProvisionTypes, ...posixFileProvisionTypes, ...hookProvisionTypes, ...installProvisionTypes, @@ -40,6 +62,12 @@ const wellKnownProvision = zod.discriminatedUnion( key: moduleValidators.envVarName, val: zod.string(), }), + ...posixDirProvisionTypes.map((ty) => + zod.object({ + ty: zod.literal(ty), + path: absolutePath, + }) + ), ...hookProvisionTypes.map((ty) => zod.object({ ty: zod.literal(ty), @@ -86,10 +114,20 @@ const envVarDynProvision = zod.object({ taskKey: zod.string(), }); +const dynamicPosixDirProvision = zod.object({ + ty: zod.enum(dynamicPosixDirProvisionTypes), + taskKey: zod.string(), +}); +export type DynamicPosixDirProvision = zod.infer< + typeof dynamicPosixDirProvision +>; + const validators = { provision, wellKnownProvision, envVarDynProvision, + posixDirProvision, + dynamicPosixDirProvision, envRecipe, envsModuleConfig, wellKnownEnvRecipe, diff --git a/std/py.ts b/std/py.ts new file mode 100644 index 00000000..529c7d8b --- /dev/null +++ b/std/py.ts @@ -0,0 +1,49 @@ +import { EnvBuilder } from "../files/mod.ts"; +import * as ports from "../ports/mod.ts"; + +interface PyEnvConfig { + install?: { + /** Python version */ + version: string; + releaseTag: string; + }; + /** venv dir, relative to Ghjk dir; default: ".venv" */ + dir?: string; + /** create the venv if missing; default: true */ + create?: boolean; +} + +export function pyEnv( + { install, dir = ".venv", create = true }: PyEnvConfig = {}, +) { + return (builder: EnvBuilder, ghjk) => { + if (install) { + const { version, releaseTag } = install; + builder.install( + ports.cpy_bs({ version, releaseTag }), + ); + } + if (create) { + builder.onEnter(ghjk.task({ + name: "create-py-venv", + fn: async ($, { workingDir }) => { + const venvDir = $.path(workingDir).join(dir); + if (!(await venvDir.exists())) { + await $`echo "Creating python venv at ${dir}"`; + await $`python3 -m venv ${dir}`; + } + }, + })); + } + + builder.var("VIRTUAL_ENV", ($, { workingDir }) => { + const venvDir = $.path(workingDir).join(dir); + return venvDir.toString(); + }); + + builder.execDir(($, { workingDir }) => { + const path = $.path(workingDir).join(dir).join("bin"); + return path.toString(); + }); + }; +}