Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: envs module #46

Merged
merged 22 commits into from
Jun 21, 2024
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
389 changes: 253 additions & 136 deletions .ghjk/deno.lock

Large diffs are not rendered by default.

1,047 changes: 508 additions & 539 deletions .ghjk/lock.json

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions .github/workflows/nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ on:
workflow_dispatch:

env:
DENO_VERSION: "1.42.1"
DENO_VERSION: "1.44.2"
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GHJK_LOG_PANIC_LEVEL: error
DENO_DIR: .deno-dir
Expand Down Expand Up @@ -55,7 +55,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: metatypedev/setup-ghjk@2e8bbf084060a18828338a7cdd43fde6feb2a3cc
- uses: metatypedev/setup-ghjk@318209a9d215f70716a4ac89dbeb9653a2deb8bc
with:
installer-url: ./install.ts
env:
Expand Down
21 changes: 15 additions & 6 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ on:
- ready_for_review

env:
DENO_VERSION: "1.42.1"
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
DENO_VERSION: "1.44.2"
GHJK_LOG: debug
GHJK_LOG_PANIC_LEVEL: error
DENO_DIR: .deno-dir
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# removing the images after every test is unncessary
DOCKER_NO_RMI: 1

jobs:
changes:
Expand All @@ -30,7 +32,14 @@ jobs:
- uses: denoland/setup-deno@v1
with:
deno-version: ${{ env.DENO_VERSION }}
# run ghjk once to avoid trigger file changes when
# pre commit runs ghjk. We'll always see changes
# to lock.json since GITHUB_TOKEN is different
# in the CI
- run: deno run --unstable -A main.ts print config
- uses: pre-commit/[email protected]
env:
SKIP: ghjk-resolve

test-e2e:
runs-on: "${{ matrix.os }}"
Expand All @@ -40,7 +49,7 @@ jobs:
- os: ubuntu-latest
platform: linux/x86_64
e2eType: "docker"
- os: custom-macos
- os: custom-arm
platform: linux/aarch64
e2eType: "docker"
- os: macos-latest
Expand Down Expand Up @@ -77,12 +86,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: metatypedev/setup-ghjk@2e8bbf084060a18828338a7cdd43fde6feb2a3cc
- uses: metatypedev/setup-ghjk@318209a9d215f70716a4ac89dbeb9653a2deb8bc
with:
installer-url: ./install.ts
env:
GHJKFILE: ./examples/protoc/ghjk.ts
- run: |
cd examples/protoc
cd examples/tasks
. $(ghjk print share-dir-path)/env.sh
protoc --version
ghjk x hey
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
.DS_Store
play.*
examples/**/.ghjk
10 changes: 10 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,16 @@ repos:
- commit-msg
- repo: local
hooks:
- id: lock-sed
name: Sed lock
language: system
entry: bash -c 'deno run --unstable -A main.ts x lock-sed'
pass_filenames: false
- id: ghjk-resolve
name: Ghjk resolve
language: system
entry: bash -c 'deno run --unstable -A main.ts p resolve'
pass_filenames: false
- id: deno-fmt
name: Deno format
language: system
Expand Down
210 changes: 162 additions & 48 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,73 +11,187 @@ ghjk /jk/ is a programmable runtime manager.

## Features

- install and manage tools (e.g. rustup, deno, node, etc.)
- [ ] fuzzy match the version
- support dependencies between tools
- [ ] setup runtime helpers (e.g. pre-commit, linting, ignore, etc.)
- [ ] provide a general regex based lockfile
- enforce custom rules
- [ ] create aliases and shortcuts
- `meta` -> `cargo run -p meta`
- `x meta` -> `cargo run -p meta` (avoid conflicts and provide autocompletion)
- [ ] load environment variables and prompt for missing ones
- [ ] define build tasks with dependencies
- `task("build", {depends_on: [rust], if: Deno.build.os === "Macos" })`
- `task.bash("ls")`
- [x] compatible with continuous integration (e.g. github actions, gitlab)
- Soft-reproducable developer environments.
- Install posix programs from different backend like npm, pypi, crates.io.
- Tasks written in typescript.
- Run tasks when entering/exiting envs.

## Getting started

```bash
# stable
curl -fsSL https://raw.githubusercontent.com/metatypedev/ghjk/main/install.sh | bash
curl -fsSL https://raw.githubusercontent.com/metatypedev/ghjk/0.2.0/install.sh | bash
# latest (main)
curl -fsSL https://raw.githubusercontent.com/metatypedev/ghjk/main/install.sh | GHJK_VERSION=main bash
curl -fsSL https://raw.githubusercontent.com/metatypedev/ghjk/0.2.0/install.sh | GHJK_VERSION=main bash/fish/zsh
```

In your project, create a configuration file `ghjk.ts`:
In your project, create a configuration file called `ghjk.ts` that look something like:

```ts
export { ghjk } from "https://raw.githubusercontent.com/metatypedev/ghjk/main/mod.ts";
import * as ghjk from "https://raw.githubusercontent.com/metatypedev/ghjk/main/mod.ts";
import node from "https://raw.githubusercontent.com/metatypedev/ghjk/main/ports/node.ts";
// NOTE: All the calls in your `ghjk.ts` file are ultimately modifying the 'sophon' proxy
// object exported here.
// WARN: always import `hack.ts` file first
export { sophon } from "https://raw.githubusercontent.com/metatypedev/ghjk/0.2.0/hack.ts";
import {
install, task,
} from "https://raw.githubusercontent.com/metatypedev/ghjk/0.2.0/hack.ts";
import node from "https://raw.githubusercontent.com/metatypedev/ghjk/0.2.0/ports/node.ts";

// install programs (ports) into your env
install(
node({ version: "14.17.0" }),
);

// write simple scripts and execute them using
// `$ ghjk x greet`
task("greet", async ($, { argv: [name] }) => {
await $`echo Hello ${name}!`;
});
```

Use the following command to then access your environment:

```bash
ghjk sync
```

### Environments

Ghjk is primarily configured through constructs called "environments" or "envs" for short.
They serve as recipes for making (mostly) reproducable posix shells.

```ts
export { sophon } from "https://raw.githubusercontent.com/metatypedev/ghjk/0.2.0/hack.ts";
import * as ghjk from "https://raw.githubusercontent.com/metatypedev/ghjk/0.2.0/hack.ts";
import * as ports from "https://raw.githubusercontent.com/metatypedev/ghjk/0.2.0/ports/mod.ts";

// top level `install`s go to the `main` env
ghjk.install(ports.protoc());
ghjk.install(ports.rust());

// the previous block is equivalent to
ghjk.env("main", {
installs: [
ports.protoc(),
ports.rust(),
],
});

ghjk.env("dev", {
// by default, all envs are additively based on `main`
// pass false here to make env independent.
// or pass name(s) of another env to base on top of
inherit: false,
// envs can specify posix env vars
vars: { CARGO_TARGET_DIR: "my_target" },
installs: [
ports.cargobi({ crateName: "cargo-insta" }),
ports.act(),
],
})
// use env hooks to run code on activation/deactivation
.onEnter(ghjk.task(($) => $`echo dev activated`))
.onExit(ghjk.task(($) => $`echo dev de-activated`));

ghjk.env({
name: "docker",
desc: "for Dockerfile usage",
// NOTE: env references are order-independent
inherit: "ci",
installs: [
ports.cargobi({ crateName: "cargo-chef" }),
ports.zstd(),
],
});

// builder syntax is also availaible
ghjk.env("ci")
.var("CI", "1")
.install(
ports.opentofu_ghrel(),
);

// each task describes it's own env as well
ghjk.task({
name: "run",
inherit: "dev",
fn: () => console.log("online"),
});
```

Once you've configured your environments:

- `$ ghjk envs cook $name` to reify and install an environment.
- `$ ghjk envs activate $name` to switch to an environment.
- And **most** usefully, `$ ghjk sync $name` to cook and _then_ activate an
environment.
- If shell is already in the specified env, it only does cooking.
- Make sure to `sync` or `cook` your envs after changes.
- If no `$name` is provided, most of these commands will operate on the default
or currently active environment.

### Ports

TBD: this feature is in development.
Look in the [kitchen sink](./examples/kitchen/ghjk.ts) for what's currently implemented.

ghjk.install(node({ version: "14.17.0" }));
### Tasks

TBD: this feature is still in development.
Look in the [tasks example](./examples/tasks/ghjk.ts) for what's currently implemented.

#### Anonymous tasks

Tasks that aren't give names cannot be invoked from the CLI.
They can be useful for tasks that are meant to be common dependencies of other tasks.

### `hack.ts`

The imports from the `hack.ts` module, while nice and striaght forward to use, hold and modify global state.
Any malicious third-party module your ghjkfile imports will thus be able to access them as well, provided they import the same version of the module.

```ts
// evil.ts
import { env, task } from "https://.../ghjk/hack.ts";

env("main")
// lol
.onEnter(task($ => $`rm -rf --no-preserve-root`);
```

## How it works

The only required dependency is `deno`. Everything else is managed automatically
and looks as follows (abstracting away some implementation details):

- the installer sets up a directory hook in your shell profile
- `.bashrc`
- `.zshrc`
- `.config/fish/config.fish`
- for every visited directory, the hook looks for `$PWD/ghjk.ts` in the
directory or its parents, and
- adds the `$HOME/.local/share/ghjk/envs/$PWD/shims/{bin,lib,include}` to your
paths
- sources environment variables in
`$HOME/.local/share/ghjk/envs/$PWD/loader.{sh,fish}` and clear previously
loaded ones (if any)
- you can then
- sync your runtime with `ghjk ports sync` which
- installs the missing tools at `$HOME/.local/share/ghjk/ports/installs`
- regenerates the shims with symlinks and environment variables
- detects any violation of the enforced rules
- [ ] `ghjk ports list`: list installed tools and versions
- [ ] `ghjk ports outdated`: list outdated tools
- [ ] `ghjk ports cleanup`: remove unused tools and versions

## Extending `ghjk`
To prevent this scenario, the exports from `hack.ts` inspect the call stack and panic if they detect more than one module using them.
This means if you want to spread your ghjkfile across multiple modules, you'll need to use functions described below.

> [!CAUTION]
> The panic protections of `hack.ts` described above only work if the module is the first import in your ghjkfile.
> If a malicious script gets imported first, it might be able to modify global primordials and get around them.
> We have more ideas to explore on hardening Ghjk security.
> This _hack_ is only a temporary compromise while Ghjk is in alpha state.

The `hack.ts` file is only optional though and a more verbose but safe way exists through...

```ts
import { file } from "https://.../ghjk/mod.ts";

const ghjk = file({
// items from `config()` are availaible here
defaultEnv: "dev",

// can even directly add installs, tasks and envs here
installs: [],
});

// we still need this export for this file to be a valid ghjkfile
export const sophon = ghjk.sophon;

// the builder functions are also accessible here
const { install, env, task, config } = ghjk;
```

If you intend on using un-trusted third-party scripts in your ghjk, it's recommended you avoid `hack.ts`.

## Development

```bash
cat install.sh | GHJK_INSTALLER_URL=$(pwd)/install.ts bash
$ cat install.sh | GHJK_INSTALLER_URL=$(pwd)/install.ts bash/fish/zsh
```
4 changes: 3 additions & 1 deletion check.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
#!/bin/env -S ghjk deno run --allow-env --allow-run --allow-read --allow-write=.
// # FIXME: find a way to resolve !DENO_EXEC_PATH in shebangs

import "./setup_logger.ts";
import { $ } from "./utils/mod.ts";

const files = (await Array.fromAsync(
$.path(import.meta.url).parentOrThrow().expandGlob("**/*.ts", {
exclude: [
".git",
"play.ts",
".ghjk/**",
".deno-dir/**",
"vendor/**",
".git/**", // was throwing an error without this
],
}),
)).map((ref) => ref.path.toString());
Expand Down
20 changes: 17 additions & 3 deletions deno.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,36 @@
"tasks": {
"test": "deno test --parallel --unstable-worker-options --unstable-kv -A tests/*",
"cache": "deno cache deps/*",
"check": "deno run -A check.ts"
"check": "deno run -A ./check.ts"
},
"fmt": {
"exclude": [
"*.md",
"**/*.md",
".ghjk/**",
".deno-dir/**"
".deno-dir/**",
"vendor/**"
]
},
"lint": {
"exclude": [
".deno-dir/**",
"ghjk.ts",
"play.ts"
"play.ts",
"vendor/**"
],
"rules": {
"include": [
"no-console",
"no-sync-fn-in-async-fn",
"no-external-import",
"no-inferrable-types",
"no-self-compare",
"no-throw-literal"
// "verbatim-module-syntax"
// "no-await-in-loop"
// "ban-untagged-todo"
],
"exclude": [
"no-explicit-any"
]
Expand Down
Loading
Loading