Bass

Bass is a scripting language for running commands and caching the shit out of them.

Bass's goal is to make shipping software predictable, repeatable, and fun. The plan is to support sophisticated CI/CD flows while sticking to familiar ideas. CI/CD boils down to running commands. Bass leverages that instead of trying to replace it.

If you'd like to try it out, grab the latest release and skim the guide!

demo thunks & thunk paths
𝄢

Commands are represented as a data value called a thunk. Thunks are rendered as space invaders.

(from (linux/alpine)
  ($ echo "Hello, world!"))
.echo
{
:image
{
:repository "alpine"
:platform
{
:os "linux"
}
:tag "latest"
:digest "sha256:8914eb54f968791faf6a8638949e480fef81e697984fba772b3976835194c6d4"
}
:cmd .echo
:args
(
  1. "Hello, world!"
)
}
𝄢

You can run a thunk, read its output, or check if it succeeds?.

(def thunk
  (from (linux/alpine)
    ($ echo "Hello, world!")))

[(run thunk) (next (read thunk :raw)) (succeeds? thunk)]
stderr: 12 lines
=> echo "Hello, world!" [0.24s]
=> exporting to client directory [0.00s]
-> copying files 40B [0.00s]
=> resolve image config for docker.io/library/alpine@sha256:8914eb54f968791faf6a8638949e48
0fef81e697984fba772b3976835194c6d4 [0.01s]
=> docker-image://docker.io/library/alpine@sha256:8914eb54f968791faf6a8638949e480fef81e697
984fba772b3976835194c6d4 CACHED [0.00s]
-> resolve docker.io/library/alpine@sha256:8914eb54f968791faf6a8638949e480fef81e697984fba7
72b3976835194c6d4 [0.00s]
=> echo "Hello, world!" [0.24s]
Hello, world!
Hello, world!
(
  1. null
  2. "Hello, world!\n"
  3. true
)
𝄢

Files created by a thunk can be referenced as thunk paths.

(def create-file
  (from (linux/alpine)
    ($ sh -c "echo hello >> file")))

create-file/file
.sh
{
:image
{
:repository "alpine"
:platform
{
:os "linux"
}
:tag "latest"
:digest "sha256:8914eb54f968791faf6a8638949e480fef81e697984fba772b3976835194c6d4"
}
:cmd .sh
:args
(
  1. "-c"
  2. "echo hello >> file"
)
}
./file
𝄢

Thunk paths can be passed to other thunks.

(from (linux/alpine)
  ($ cat create-file/file))
.cat
{
:image
{
:repository "alpine"
:platform
{
:os "linux"
}
:tag "latest"
:digest "sha256:8914eb54f968791faf6a8638949e480fef81e697984fba772b3976835194c6d4"
}
:cmd .cat
:args
(
  1. .sh
    {
    :image
    {
    :repository "alpine"
    :platform
    {
    :os "linux"
    }
    :tag "latest"
    :digest "sha256:8914eb54f968791faf6a8638949e480fef81e697984fba772b3976835194c6d4"
    }
    :cmd .sh
    :args
    (
    1. "-c"
    2. "echo hello >> file"
    )
    }
    ./file
)
}
𝄢

Like thunks, thunk paths are just data values. The underlying thunk only runs when another thunk that needs it runs, or when you read the path itself.

(-> (from (linux/alpine)
      ($ cat create-file/file))
    (read :raw)
    next)
stderr: 10 lines
=> resolve image config for docker.io/library/alpine@sha256:8914eb54f968791faf6a8638949e48
0fef81e697984fba772b3976835194c6d4 [0.01s]
=> docker-image://docker.io/library/alpine@sha256:8914eb54f968791faf6a8638949e480fef81e697
984fba772b3976835194c6d4 CACHED [0.00s]
-> resolve docker.io/library/alpine@sha256:8914eb54f968791faf6a8638949e480fef81e697984fba7
72b3976835194c6d4 [0.01s]
=> sh -c "echo hello >> file" [0.20s]
=> cat <thunk QHHFPJ4CJQJF0: (.sh)>/file [0.22s]
=> exporting to client directory [0.00s]
-> copying files 32B [0.00s]
"hello\n"
demo fetching git repos & other inputs
𝄢

To fetch source code from a git repo you should probably use the .git module.

(use (.git (linux/alpine/git)))

(let [url "https://github.com/vito/bass"
      ref "main"]
  (git:checkout url (git:ls-remote url ref)))
stderr: 9 lines
=> resolve image config for docker.io/alpine/git@sha256:66b210a97bc07bfd4019826bcd13a488b3
71a6cbe2630a4b37d23275658bd3f2 [0.01s]
=> docker-image://docker.io/alpine/git@sha256:66b210a97bc07bfd4019826bcd13a488b371a6cbe263
0a4b37d23275658bd3f2 CACHED [0.00s]
-> resolve docker.io/alpine/git@sha256:66b210a97bc07bfd4019826bcd13a488b371a6cbe2630a4b37d
23275658bd3f2 [0.00s]
=> git ls-remote https://github.com/vito/bass main [0.48s]
=> exporting to client directory [0.00s]
-> copying files 83B [0.00s]
.git
{
:image
.git
{
:image
.git
{
:image
.git
{
:image
{
:repository "alpine/git"
:platform
{
:os "linux"
}
:tag "latest"
:digest "sha256:66b210a97bc07bfd4019826bcd13a488b371a6cbe2630a4b37d23275658bd3f2"
}
:cmd .git
:args
(
  1. "clone"
  2. "https://github.com/vito/bass"
  3. ./
)
}
:cmd .git
:args
(
  1. "fetch"
  2. "origin"
  3. "836efe1a4b59be0c148ae4cbc25f7eff581d9dde"
)
}
:cmd .git
:args
(
  1. "checkout"
  2. "836efe1a4b59be0c148ae4cbc25f7eff581d9dde"
)
}
:cmd .git
:args
(
  1. "submodule"
  2. "update"
  3. "--init"
  4. "--recursive"
)
}
./
𝄢

Using ls-remote to resolve main to a commit ensures the checkout call is hermetic.

A non-hermetic thunk looks like this:

; BAD
(from (linux/alpine/git)
  ($ git clone "https://github.com/vito/bass" ./))
.git
{
:image
{
:repository "alpine/git"
:platform
{
:os "linux"
}
:tag "latest"
:digest "sha256:66b210a97bc07bfd4019826bcd13a488b371a6cbe2630a4b37d23275658bd3f2"
}
:cmd .git
:args
(
  1. "clone"
  2. "https://github.com/vito/bass"
  3. ./
)
}
𝄢

If you run this thunk somewhere else it might return something different. It'll also be cached forever, so you'll never get new commits.

Each input should specify an exact version to fetch. If you don't know it yet you can run another thunk to figure it out. You can keep that thunk from being cached forever by labeling it with the current time. That's how ls-remote works under the hood.

(defn ls-remote [repo ref & timestamp]
  (-> ($ git ls-remote $repo $ref)
      (with-image *git-image*)
      (with-label :at (now 60)) ; rerun every minute
      (read :unix-table) ; line and space separated table output
      next    ; first row   : <ref> <sha>
      first)) ; first column: <ref>
ls-remote
demo running tests
𝄢

To run tests, just run whatever command you would usually use to run tests.

(use (.git (linux/alpine/git)))

(defn go-test [src & args]
  (from (linux/golang)
    (cd src
      ($ go test & $args))))

(let [src git:github/vito/booklit/ref/master/]
  (succeeds? (go-test src ./tests/)))
stderr: 49 lines
=> resolve image config for docker.io/library/golang@sha256:dc76ef03e54c34a00dcdca81e55c24
2d24b34d231637776c4bb5c1a8e8514253 [0.02s]
=> resolve image config for docker.io/alpine/git@sha256:66b210a97bc07bfd4019826bcd13a488b3
71a6cbe2630a4b37d23275658bd3f2 [0.01s]
=> docker-image://docker.io/alpine/git@sha256:66b210a97bc07bfd4019826bcd13a488b371a6cbe263
0a4b37d23275658bd3f2 CACHED [0.00s]
-> resolve docker.io/alpine/git@sha256:66b210a97bc07bfd4019826bcd13a488b371a6cbe2630a4b37d
23275658bd3f2 [0.01s]
=> docker-image://docker.io/library/golang@sha256:dc76ef03e54c34a00dcdca81e55c242d24b34d23
1637776c4bb5c1a8e8514253 CACHED [0.00s]
-> resolve docker.io/library/golang@sha256:dc76ef03e54c34a00dcdca81e55c242d24b34d231637776
c4bb5c1a8e8514253 [0.01s]
=> git clone https://github.com/vito/booklit ./ [1.24s]
Cloning into '.'...
=> git fetch origin d62e0d27a9668a8cf3850e405c6b19cd213f545a [0.54s]
From https://github.com/vito/booklit
* branch d62e0d27a9668a8cf3850e405c6b19cd213f545a -> FETCH_HEAD
=> git checkout d62e0d27a9668a8cf3850e405c6b19cd213f545a [0.29s]
Note: switching to 'd62e0d27a9668a8cf3850e405c6b19cd213f545a'.
You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by switching back to a branch.
If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -c with the switch command. Example:
git switch -c <new-branch-name>
Or undo this operation with:
git switch -
Turn off this advice by setting config variable advice.detachedHead to false
HEAD is now at d62e0d2 remove stale badge, add description
=> git submodule update --init --recursive [0.26s]
=> go test ./tests/ [4.32s]
go: downloading github.com/onsi/gomega v1.19.0
go: downloading github.com/onsi/ginkgo/v2 v2.1.4
go: downloading github.com/sirupsen/logrus v1.8.1
go: downloading github.com/agext/levenshtein v1.2.3
go: downloading github.com/segmentio/textio v1.2.0
go: downloading golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a
go: downloading golang.org/x/net v0.0.0-20220225172249-27dd8689420f
go: downloading gopkg.in/yaml.v2 v2.4.0
go: downloading github.com/onsi/ginkgo v1.16.4
go: downloading golang.org/x/text v0.3.7
ok github.com/vito/booklit/tests 0.172s
true
𝄢

Don't use Go? Use a different image and run a different command:

(defn cargo-test [src & args]
  (from (linux/rust)
    (cd src
      ($ cargo test & $args))))

(let [src git:github/alacritty/alacritty/ref/master/]
  (succeeds? (cargo-test src ./alacritty_terminal/)))
stderr: 365 lines
=> resolve image config for docker.io/library/rust@sha256:6d44ed87fe759752c89d1f68596f84a2
3493d3d3395ed843d3a1c104866e5d9e [0.02s]
=> resolve image config for docker.io/alpine/git@sha256:66b210a97bc07bfd4019826bcd13a488b3
71a6cbe2630a4b37d23275658bd3f2 [0.01s]
=> docker-image://docker.io/library/rust@sha256:6d44ed87fe759752c89d1f68596f84a23493d3d339
5ed843d3a1c104866e5d9e CACHED [0.00s]
-> resolve docker.io/library/rust@sha256:6d44ed87fe759752c89d1f68596f84a23493d3d3395ed843d
3a1c104866e5d9e [0.01s]
=> docker-image://docker.io/alpine/git@sha256:66b210a97bc07bfd4019826bcd13a488b371a6cbe263
0a4b37d23275658bd3f2 CACHED [0.00s]
-> resolve docker.io/alpine/git@sha256:66b210a97bc07bfd4019826bcd13a488b371a6cbe2630a4b37d
23275658bd3f2 [0.01s]
=> git clone https://github.com/alacritty/alacritty ./ [2.11s]
Cloning into '.'...
=> git fetch origin d92a8a0e16eda3df7566e5c995aa0d258b6b76f7 [0.59s]
From https://github.com/alacritty/alacritty
* branch d92a8a0e16eda3df7566e5c995aa0d258b6b76f7 -> FETCH_HEAD
=> git checkout d92a8a0e16eda3df7566e5c995aa0d258b6b76f7 [0.33s]
Note: switching to 'd92a8a0e16eda3df7566e5c995aa0d258b6b76f7'.
You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by switching back to a branch.
If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -c with the switch command. Example:
git switch -c <new-branch-name>
Or undo this operation with:
git switch -
Turn off this advice by setting config variable advice.detachedHead to false
HEAD is now at d92a8a0e Update to notify v5 via notify_debouncer_mini
=> git submodule update --init --recursive [0.26s]
=> cargo test ./alacritty_terminal/ [118.6s]
Updating crates.io index
Downloading crates ...
Downloaded libloading v0.7.3
Downloaded log v0.4.17
Downloaded memmap2 v0.5.7
Downloaded itoa v1.0.3
Downloaded instant v0.1.12
Downloaded gl_generator v0.14.0
Downloaded lazycell v1.3.0
Downloaded memchr v2.5.0
Downloaded notify-debouncer-mini v0.2.1
Downloaded notify v5.0.0
Downloaded raw-window-handle v0.4.3
Downloaded serde_yaml v0.8.26
Downloaded signal-hook v0.3.14
Downloaded wayland-sys v0.30.0-beta.12
Downloaded minimal-lexical v0.2.1
Downloaded pkg-config v0.3.25
Downloaded proc-macro2 v1.0.43
Downloaded quick-xml v0.22.0
Downloaded proc-macro-error-attr v1.0.4
Downloaded os_str_bytes v6.3.0
Downloaded mio-extras v2.0.6
Downloaded mio v0.6.23
Downloaded regex-syntax v0.6.27
Downloaded safe_arch v0.5.2
Downloaded version_check v0.9.4
Downloaded xml-rs v0.8.4
Downloaded unicode-width v0.1.10
Downloaded dlib v0.5.0
Downloaded miniz_oxide v0.5.4
Downloaded glutin v0.30.0
Downloaded inotify-sys v0.1.5
Downloaded scopeguard v1.1.0
Downloaded signal-hook-registry v1.4.0
Downloaded png v0.17.6
Downloaded percent-encoding v2.2.0
Downloaded vec_map v0.8.2
Downloaded serde_derive v1.0.144
Downloaded termcolor v1.1.3
Downloaded thiserror v1.0.35
Downloaded utf8parse v0.2.0
Downloaded unicode-ident v1.0.4
Downloaded wayland-scanner v0.29.5
Downloaded x11-clipboard v0.6.1
Downloaded wayland-sys v0.29.5
Downloaded xcursor v0.3.4
Downloaded wayland-commons v0.29.5
Downloaded xdg v2.4.1
Downloaded x11-dl v2.20.0
Downloaded parking_lot_core v0.9.3
Downloaded copypasta v0.8.1
Downloaded clap_complete v3.2.5
Downloaded thiserror-impl v1.0.35
Downloaded dirs v4.0.0
Downloaded foreign-types-shared v0.3.1
Downloaded flate2 v1.0.24
Downloaded clap v3.2.21
Downloaded once_cell v1.14.0
Downloaded winit v0.27.5
Downloaded nom v7.1.1
Downloaded signal-hook-mio v0.2.3
Downloaded tiny-skia v0.7.0
Downloaded vte_generate_state_changes v0.1.1
Downloaded yaml-rust v0.4.5
Downloaded foreign-types-macros v0.2.2
Downloaded slotmap v1.0.6
Downloaded wayland-client v0.29.5
Downloaded xcb v1.1.1
Downloaded glutin_egl_sys v0.3.0
Downloaded cfg-if v1.0.0
Downloaded freetype-rs v0.26.0
Downloaded clap_derive v3.2.18
Downloaded linked-hash-map v0.5.6
Downloaded downcast-rs v1.2.0
Downloaded filetime v0.2.17
Downloaded servo-fontconfig-sys v5.1.0
Downloaded freetype-sys v0.13.1
Downloaded calloop v0.10.1
Downloaded adler v1.0.2
Downloaded expat-sys v2.1.6
Downloaded syn v1.0.99
Downloaded nix v0.24.2
Downloaded slab v0.4.7
Downloaded fnv v1.0.7
Downloaded hashbrown v0.12.3
Downloaded base64 v0.13.0
Downloaded foreign-types v0.5.0
Downloaded crc32fast v1.3.2
Downloaded cmake v0.1.48
Downloaded cc v1.0.73
Downloaded clap_lex v0.2.4
Downloaded bytemuck v1.12.1
Downloaded arrayvec v0.5.2
Downloaded arrayref v0.3.6
Downloaded serde v1.0.144
Downloaded smithay-client-toolkit v0.16.0
Downloaded tiny-skia-path v0.7.0
Downloaded vte v0.10.1
Downloaded serde_json v1.0.85
Downloaded textwrap v0.15.1
Downloaded regex-automata v0.1.10
Downloaded strsim v0.10.0
Downloaded smithay-clipboard v0.6.6
Downloaded servo-fontconfig v0.5.1
Downloaded scoped-tls v1.0.0
Downloaded raw-window-handle v0.5.0
Downloaded proc-macro-error v1.0.4
Downloaded atty v0.2.14
Downloaded net2 v0.2.37
Downloaded libc v0.2.132
Downloaded parking_lot v0.12.1
Downloaded autocfg v1.1.0
Downloaded khronos_api v3.1.0
Downloaded glutin_glx_sys v0.3.0
Downloaded cfg-if v0.1.10
Downloaded heck v0.4.0
Downloaded memoffset v0.6.5
Downloaded lazy_static v1.4.0
Downloaded cty v0.2.2
Downloaded bitflags v1.3.2
Downloaded wayland-cursor v0.29.5
Downloaded walkdir v2.3.2
Downloaded sctk-adwaita v0.4.3
Downloaded mio v0.8.4
Downloaded smallvec v1.9.0
Downloaded same-file v1.0.6
Downloaded ryu v1.0.11
Downloaded iovec v0.1.4
Downloaded inotify v0.9.6
Downloaded crossbeam-utils v0.8.12
Downloaded quote v1.0.21
Downloaded crossfont v0.5.1
Downloaded dirs-sys v0.3.7
Downloaded lock_api v0.4.8
Downloaded crossbeam-channel v0.5.6
Downloaded cfg_aliases v0.1.1
Downloaded wayland-protocols v0.29.5
Downloaded mio-uds v0.6.8
Downloaded indexmap v1.9.1
Compiling proc-macro2 v1.0.43
Compiling unicode-ident v1.0.4
Compiling quote v1.0.21
Compiling cfg-if v1.0.0
Compiling libc v0.2.132
Compiling syn v1.0.99
Compiling autocfg v1.1.0
Compiling serde_derive v1.0.144
Compiling serde v1.0.144
Compiling pkg-config v0.3.25
Compiling log v0.4.17
Compiling bitflags v1.3.2
Compiling xml-rs v0.8.4
Compiling version_check v0.9.4
Compiling memchr v2.5.0
Compiling lazy_static v1.4.0
Compiling cc v1.0.73
Compiling smallvec v1.9.0
Compiling libloading v0.7.3
Compiling khronos_api v3.1.0
Compiling dlib v0.5.0
Compiling once_cell v1.14.0
Compiling hashbrown v0.12.3
Compiling ryu v1.0.11
Compiling linked-hash-map v0.5.6
Compiling thiserror v1.0.35
Compiling cfg-if v0.1.10
Compiling scoped-tls v1.0.0
Compiling slotmap v1.0.6
Compiling minimal-lexical v0.2.1
Compiling cmake v0.1.48
Compiling downcast-rs v1.2.0
Compiling memoffset v0.6.5
Compiling indexmap v1.9.1
Compiling slab v0.4.7
Compiling yaml-rust v0.4.5
Compiling lock_api v0.4.8
Compiling wayland-sys v0.29.5
Compiling nom v7.1.1
Compiling parking_lot_core v0.9.3
Compiling crc32fast v1.3.2
Compiling wayland-scanner v0.29.5
Compiling servo-fontconfig-sys v5.1.0
Compiling smithay-client-toolkit v0.16.0
Compiling proc-macro-error-attr v1.0.4
Compiling scopeguard v1.1.0
Compiling adler v1.0.2
Compiling signal-hook v0.3.14
Compiling vec_map v0.8.2
Compiling miniz_oxide v0.5.4
Compiling x11-dl v2.20.0
Compiling quick-xml v0.22.0
Compiling proc-macro-error v1.0.4
Compiling bytemuck v1.12.1
Compiling crossbeam-utils v0.8.12
Compiling nix v0.24.2
Compiling net2 v0.2.37
Compiling iovec v0.1.4
Compiling memmap2 v0.5.7
Compiling dirs-sys v0.3.7
Compiling freetype-sys v0.13.1
Compiling expat-sys v2.1.6
Compiling signal-hook-registry v1.4.0
Compiling parking_lot v0.12.1
Compiling flate2 v1.0.24
Compiling dirs v4.0.0
Compiling vte_generate_state_changes v0.1.1
Compiling xcb v1.1.1
Compiling crossfont v0.5.1
Compiling arrayref v0.3.6
Compiling foreign-types-shared v0.3.1
Compiling lazycell v1.3.0
Compiling cty v0.2.2
Compiling regex-syntax v0.6.27
Compiling utf8parse v0.2.0
Compiling serde_json v1.0.85
Compiling tiny-skia-path v0.7.0
Compiling png v0.17.6
Compiling freetype-rs v0.26.0
Compiling servo-fontconfig v0.5.1
Compiling safe_arch v0.5.2
Compiling xcursor v0.3.4
Compiling vte v0.10.1
Compiling inotify-sys v0.1.5
Compiling wayland-client v0.29.5
Compiling wayland-protocols v0.29.5
Compiling wayland-sys v0.30.0-beta.12
Compiling same-file v1.0.6
Compiling base64 v0.13.0
Compiling arrayvec v0.5.2
Compiling itoa v1.0.3
Compiling os_str_bytes v6.3.0
Compiling cfg_aliases v0.1.1
Compiling unicode-width v0.1.10
Compiling heck v0.4.0
Compiling tiny-skia v0.7.0
Compiling glutin v0.30.0
Compiling walkdir v2.3.2
Compiling clap_lex v0.2.4
Compiling crossbeam-channel v0.5.6
Compiling inotify v0.9.6
Compiling raw-window-handle v0.5.0
Compiling filetime v0.2.17
Compiling atty v0.2.14
Compiling termcolor v1.1.3
Compiling textwrap v0.15.1
Compiling strsim v0.10.0
Compiling raw-window-handle v0.4.3
Compiling instant v0.1.12
Compiling percent-encoding v2.2.0
Compiling xdg v2.4.1
Compiling fnv v1.0.7
Compiling regex-automata v0.1.10
Compiling wayland-commons v0.29.5
Compiling thiserror-impl v1.0.35
Compiling foreign-types-macros v0.2.2
Compiling alacritty_config_derive v0.2.1-dev (/bass/work/alacritty_config_derive)
Compiling clap_derive v3.2.18
Compiling wayland-cursor v0.29.5
Compiling foreign-types v0.5.0
Compiling clap v3.2.21
Compiling x11-clipboard v0.6.1
Compiling clap_complete v3.2.5
Compiling serde_yaml v0.8.26
Compiling gl_generator v0.14.0
Compiling mio v0.6.23
Compiling calloop v0.10.1
Compiling mio v0.8.4
Compiling notify v5.0.0
Compiling alacritty_config v0.1.1-dev (/bass/work/alacritty_config)
Compiling notify-debouncer-mini v0.2.1
Compiling mio-uds v0.6.8
Compiling mio-extras v2.0.6
Compiling glutin_glx_sys v0.3.0
Compiling glutin_egl_sys v0.3.0
Compiling signal-hook-mio v0.2.3
Compiling alacritty v0.12.0-dev (/bass/work/alacritty)
Compiling alacritty_terminal v0.17.1-dev (/bass/work/alacritty_terminal)
Compiling smithay-clipboard v0.6.6
Compiling sctk-adwaita v0.4.3
Compiling copypasta v0.8.1
Compiling winit v0.27.5
Finished test [unoptimized + debuginfo] target(s) in 1m 57s
Running unittests src/main.rs (target/debug/deps/alacritty-de76679b5a4c517e)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 69 filtered out; finished
in 0.00s
Running unittests src/lib.rs (target/debug/deps/alacritty_config-3231bc15e45012b4
)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished i
n 0.00s
Running unittests src/lib.rs (target/debug/deps/alacritty_config_derive-1def58b48
b8fdd9d)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished i
n 0.00s
Running tests/config.rs (target/debug/deps/config-5f0e3df0a5109231)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 4 filtered out; finished i
n 0.00s
Running unittests src/lib.rs (target/debug/deps/alacritty_terminal-0a589f79f26a06
92)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 134 filtered out; finished
in 0.00s
Running tests/ref.rs (target/debug/deps/ref-654f8ed9d7831122)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 42 filtered out; finished
in 0.00s
true
demo running services
𝄢

To run a service thunk, assign names to its ports using with-port. The provided ports will be healthchecked whenever the service runs.

(defn http-server [index]
  (from (linux/python)
    (-> ($ python -m http.server)
        (with-mount (mkfile ./index.html index) ./index.html)
        (with-port :http 8000))))

(http-server "Hello, world!")
.python
{
:image
{
:repository "python"
:platform
{
:os "linux"
}
:tag "latest"
:digest "sha256:10fc14aa6ae69f69e4c953cffd9b0964843d8c163950491d2138af891377bc1d"
}
:cmd .python
:args
(
  1. "-m"
  2. "http.server"
)
:mounts
(
  1. {
    :source <fs>/index.html
    :target ./index.html
    }
)
}
𝄢

You can use addr to construct a thunk addr. A thunk addr is like a thunk path except it references a named port provided by the thunk rather than a file created by it.

(defn echo [msg]
  (let [server (http-server msg)]
    (from (linux/alpine)
      ($ wget -O- (addr server :http "http://$host:$port")))))

(echo "Hello, world!")
.wget
{
:image
{
:repository "alpine"
:platform
{
:os "linux"
}
:tag "latest"
:digest "sha256:8914eb54f968791faf6a8638949e480fef81e697984fba772b3976835194c6d4"
}
:cmd .wget
:args
(
  1. "-O-"
  2. .python
    {
    :image
    {
    :repository "python"
    :platform
    {
    :os "linux"
    }
    :tag "latest"
    :digest "sha256:10fc14aa6ae69f69e4c953cffd9b0964843d8c163950491d2138af891377bc1d"
    }
    :cmd .python
    :args
    (
    1. "-m"
    2. "http.server"
    )
    :mounts
    (
    1. {
      :source <fs>/index.html
      :target ./index.html
      }
    )
    }
    http
)
}
𝄢

Like thunks and thunk paths, thunk addrs are just data values. The underlying service thunk only runs when another thunk that needs it runs.

(run (echo "Hello, world!"))
stderr: 21 lines
=> resolve image config for docker.io/library/python@sha256:10fc14aa6ae69f69e4c953cffd9b09
64843d8c163950491d2138af891377bc1d [0.02s]
=> mkfile /index.html CACHED [0.00s]
=> docker-image://docker.io/library/python@sha256:10fc14aa6ae69f69e4c953cffd9b0964843d8c16
3950491d2138af891377bc1d CACHED [0.00s]
-> resolve docker.io/library/python@sha256:10fc14aa6ae69f69e4c953cffd9b0964843d8c163950491
d2138af891377bc1d [0.01s]
=> resolve image config for docker.io/library/alpine@sha256:8914eb54f968791faf6a8638949e48
0fef81e697984fba772b3976835194c6d4 [0.01s]
=> docker-image://docker.io/library/alpine@sha256:8914eb54f968791faf6a8638949e480fef81e697
984fba772b3976835194c6d4 CACHED [0.00s]
-> resolve docker.io/library/alpine@sha256:8914eb54f968791faf6a8638949e480fef81e697984fba7
72b3976835194c6d4 [0.00s]
=> wget -O- <thunk SBACV3TQV5B52: (.python)>:http [0.21s]
Connecting to SBACV3TQV5B52:8000 (10.64.3.223:8000)
writing to stdout
- 100% |********************************| 13 0:00:00 ETA
written to stdout
Hello, world!
=> python -m http.server CANCELED [1.36s]
10.64.3.224 - - [28/Nov/2022 01:27:20] "GET / HTTP/1.1" 200 -
null
demo building & publishing artifacts
𝄢

To build from source just run whatever build command you already use.

(use (.git (linux/alpine/git)))

(defn go-build [src & args]
  (from (linux/golang)
    (cd src
      (-> ($ go build & $args)
          (with-env {:CGO_ENABLED "0"})))))

(let [src git:github/vito/booklit/ref/master/
      built (go-build src "./cmd/booklit")]
  (-> (from (linux/alpine)
        ($ built/booklit --version))
      (read :raw)
      next))
stderr: 53 lines
=> resolve image config for docker.io/library/alpine@sha256:8914eb54f968791faf6a8638949e48
0fef81e697984fba772b3976835194c6d4 [0.01s]
=> resolve image config for docker.io/alpine/git@sha256:66b210a97bc07bfd4019826bcd13a488b3
71a6cbe2630a4b37d23275658bd3f2 [0.00s]
=> resolve image config for docker.io/library/golang@sha256:dc76ef03e54c34a00dcdca81e55c24
2d24b34d231637776c4bb5c1a8e8514253 [0.01s]
=> docker-image://docker.io/alpine/git@sha256:66b210a97bc07bfd4019826bcd13a488b371a6cbe263
0a4b37d23275658bd3f2 CACHED [0.00s]
-> resolve docker.io/alpine/git@sha256:66b210a97bc07bfd4019826bcd13a488b371a6cbe2630a4b37d
23275658bd3f2 [0.01s]
=> docker-image://docker.io/library/golang@sha256:dc76ef03e54c34a00dcdca81e55c242d24b34d23
1637776c4bb5c1a8e8514253 CACHED [0.00s]
-> resolve docker.io/library/golang@sha256:dc76ef03e54c34a00dcdca81e55c242d24b34d231637776
c4bb5c1a8e8514253 [0.01s]
=> docker-image://docker.io/library/alpine@sha256:8914eb54f968791faf6a8638949e480fef81e697
984fba772b3976835194c6d4 CACHED [0.00s]
-> resolve docker.io/library/alpine@sha256:8914eb54f968791faf6a8638949e480fef81e697984fba7
72b3976835194c6d4 [0.01s]
=> git clone https://github.com/vito/booklit ./ [1.33s]
Cloning into '.'...
=> git fetch origin d62e0d27a9668a8cf3850e405c6b19cd213f545a [0.53s]
From https://github.com/vito/booklit
* branch d62e0d27a9668a8cf3850e405c6b19cd213f545a -> FETCH_HEAD
=> git checkout d62e0d27a9668a8cf3850e405c6b19cd213f545a [0.29s]
Note: switching to 'd62e0d27a9668a8cf3850e405c6b19cd213f545a'.
You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by switching back to a branch.
If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -c with the switch command. Example:
git switch -c <new-branch-name>
Or undo this operation with:
git switch -
Turn off this advice by setting config variable advice.detachedHead to false
HEAD is now at d62e0d2 remove stale badge, add description
=> git submodule update --init --recursive [0.26s]
=> go build ./cmd/booklit [3.87s]
go: downloading github.com/sirupsen/logrus v1.8.1
go: downloading github.com/jessevdk/go-flags v1.4.0
go: downloading github.com/agext/levenshtein v1.2.3
go: downloading github.com/segmentio/textio v1.2.0
go: downloading golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a
=> <thunk 9UL23OIV07G5S: (.go)>/booklit --version [0.25s]
time="2022-11-28T01:27:27Z" level=info msg="plugin registered" plugin=baselit
=> exporting to client directory [0.00s]
-> copying files 36B [0.00s]
"0.0.0-dev\n"
𝄢

Thunk paths can be serialized to JSON. If all thunks involved in its creation are hermetic the JSON structure represents a repeatable artifact.

(def built
  (go-build git:github/vito/booklit/ref/master/ "./cmd/booklit"))

(emit built *stdout*)
{
  "image": {
    "ref": {
      "repository": "golang",
      "platform": {
        "os": "linux"
      },
      "tag": "latest",
      "digest": "sha256:dc76ef03e54c34a00dcdca81e55c242d24b34d231637776c4bb5c1a8e8514253"
    }
  },
  "cmd": {
    "command": {
      "name": "go"
    }
  },
  "args": [
    {
      "string": {
        "value": "build"
      }
    },
    {
      "string": {
        "value": "./cmd/booklit"
      }
    }
  ],
  "env": [
    {
      "symbol": "CGO_ENABLED",
      "value": {
        "string": {
          "value": "0"
        }
      }
    }
  ],
  "mounts": [
    {
      "source": {
        "thunk": {
          "thunk": {
            "image": {
              "thunk": {
                "image": {
                  "thunk": {
                    "image": {
                      "thunk": {
                        "image": {
                          "ref": {
                            "repository": "alpine/git",
                            "platform": {
                              "os": "linux"
                            },
                            "tag": "latest",
                            "digest": "sha256:66b210a97bc07bfd4019826bcd13a488b371a6cbe2630a4b37d23275658bd3f2"
                          }
                        },
                        "cmd": {
                          "command": {
                            "name": "git"
                          }
                        },
                        "args": [
                          {
                            "string": {
                              "value": "clone"
                            }
                          },
                          {
                            "string": {
                              "value": "https://github.com/vito/booklit"
                            }
                          },
                          {
                            "dirPath": {
                              "path": "."
                            }
                          }
                        ]
                      }
                    },
                    "cmd": {
                      "command": {
                        "name": "git"
                      }
                    },
                    "args": [
                      {
                        "string": {
                          "value": "fetch"
                        }
                      },
                      {
                        "string": {
                          "value": "origin"
                        }
                      },
                      {
                        "string": {
                          "value": "d62e0d27a9668a8cf3850e405c6b19cd213f545a"
                        }
                      }
                    ]
                  }
                },
                "cmd": {
                  "command": {
                    "name": "git"
                  }
                },
                "args": [
                  {
                    "string": {
                      "value": "checkout"
                    }
                  },
                  {
                    "string": {
                      "value": "d62e0d27a9668a8cf3850e405c6b19cd213f545a"
                    }
                  }
                ]
              }
            },
            "cmd": {
              "command": {
                "name": "git"
              }
            },
            "args": [
              {
                "string": {
                  "value": "submodule"
                }
              },
              {
                "string": {
                  "value": "update"
                }
              },
              {
                "string": {
                  "value": "--init"
                }
              },
              {
                "string": {
                  "value": "--recursive"
                }
              }
            ]
          },
          "path": {
            "dir": {
              "path": "."
            }
          }
        }
      },
      "target": {
        "dir": {
          "path": "."
        }
      }
    }
  ]
}
null
𝄢

The exact format is not finalized and probably needs versioning and deduping.

A thunk path's JSON form can be piped to bass --export to build the artifact and emit a tar stream.

cat thunk-path.json | bass --export | tar -xf -

You can publish thunk path JSON as part of your release as a form of provenance:

(let [repro (mkfile ./file.json (json built))]
  (from (linux/nixery.dev/gh)
    ($ gh release create v0.0.1 $repro)))
.gh
{
:image
{
:repository "nixery.dev/gh"
:platform
{
:os "linux"
}
:tag "latest"
:digest "sha256:89475f369233a3c9d17c384df58f4143c5b732ed16e2870b6af88cf84500db4c"
}
:cmd .gh
:args
(
  1. "release"
  2. "create"
  3. "v0.0.1"
  4. <fs>/file.json
)
}
demo pinning dependencies
𝄢

To pin dependencies, configure a path to a bass.lock file as the magic *memos* binding.

(def *memos* *dir*/bass.lock)
*memos*
𝄢

The linux path root resolves an image reference to a digest and memoizes its result into *memos* if defined.

(use (.git (linux/alpine/git))) ; saves digest into *memos*

(run (from (linux/alpine/git) ; uses the digest from *memos*
        ($ cat $*memos*)))    ; reveal the wizard behind the curtain
stderr: 108 lines
=> resolve image config for docker.io/alpine/git:latest [0.16s]
=> resolve image config for docker.io/alpine/git@sha256:66b210a97bc07bfd4019826bcd13a488b3
71a6cbe2630a4b37d23275658bd3f2 [0.02s]
=> local:///tmp/nix-shell.fUJHAA/bass-scope3883237306 [0.01s]
-> transferring /tmp/nix-shell.fUJHAA/bass-scope3883237306: 2.11kB [0.00s]
=> docker-image://docker.io/alpine/git@sha256:66b210a97bc07bfd4019826bcd13a488b371a6cbe263
0a4b37d23275658bd3f2 CACHED [0.00s]
-> resolve docker.io/alpine/git@sha256:66b210a97bc07bfd4019826bcd13a488b371a6cbe2630a4b37d
23275658bd3f2 [0.01s]
=> copy /bass.lock /bass.lock CACHED [0.00s]
=> cat <host: /tmp/nix-shell.fUJHAA/bass-scope3883237306/bass.lock> [0.25s]
memos: {
module: {
cmd: {
command: {
name: "run"
}
}
}
calls: {
binding: "resolve"
results: {
input: {
array: {
values: {
object: {
bindings: {
symbol: "platform"
value: {
object: {
bindings: {
symbol: "os"
value: {
string: {
value: "linux"
}
}
}
}
}
}
bindings: {
symbol: "repository"
value: {
string: {
value: "alpine/git"
}
}
}
bindings: {
symbol: "tag"
value: {
string: {
value: "latest"
}
}
}
}
}
}
}
output: {
object: {
bindings: {
symbol: "repository"
value: {
string: {
value: "alpine/git"
}
}
}
bindings: {
symbol: "platform"
value: {
object: {
bindings: {
symbol: "os"
value: {
string: {
value: "linux"
}
}
}
}
}
}
bindings: {
symbol: "tag"
value: {
string: {
value: "latest"
}
}
}
bindings: {
symbol: "digest"
value: {
string: {
value: "sha256:66b210a97bc07bfd4019826bcd13a488b371a6cbe2630a4b37d2327
5658bd3f2"
}
}
}
}
}
}
}
}
null
𝄢

The github path root resolves a branch or tag reference to a commit and returns its checkout, memoizing the commit in *memos* if defined.

git:github/vito/booklit/ref/master/
stderr: 9 lines
=> resolve image config for docker.io/alpine/git@sha256:66b210a97bc07bfd4019826bcd13a488b3
71a6cbe2630a4b37d23275658bd3f2 [0.02s]
=> docker-image://docker.io/alpine/git@sha256:66b210a97bc07bfd4019826bcd13a488b371a6cbe263
0a4b37d23275658bd3f2 CACHED [0.00s]
-> resolve docker.io/alpine/git@sha256:66b210a97bc07bfd4019826bcd13a488b371a6cbe2630a4b37d
23275658bd3f2 [0.01s]
=> git ls-remote https://github.com/vito/booklit master [0.50s]
=> exporting to client directory [0.00s]
-> copying files 85B [0.00s]
.git
{
:image
.git
{
:image
.git
{
:image
.git
{
:image
{
:repository "alpine/git"
:platform
{
:os "linux"
}
:tag "latest"
:digest "sha256:66b210a97bc07bfd4019826bcd13a488b371a6cbe2630a4b37d23275658bd3f2"
}
:cmd .git
:args
(
  1. "clone"
  2. "https://github.com/vito/booklit"
  3. ./
)
}
:cmd .git
:args
(
  1. "fetch"
  2. "origin"
  3. "d62e0d27a9668a8cf3850e405c6b19cd213f545a"
)
}
:cmd .git
:args
(
  1. "checkout"
  2. "d62e0d27a9668a8cf3850e405c6b19cd213f545a"
)
}
:cmd .git
:args
(
  1. "submodule"
  2. "update"
  3. "--init"
  4. "--recursive"
)
}
./
𝄢

Paths like above are often used with use to load Bass modules from thunk paths. Bass doesn't have its own package system; it uses thunks for that too.

(let [src git:github/vito/booklit/ref/master/]
  (use (src/bass/booklit.bass))
  (when (succeeds? (booklit:tests src))
    (booklit:build src "dev" "linux" "amd64")))
stderr: 81 lines
=> resolve image config for docker.io/alpine/git@sha256:66b210a97bc07bfd4019826bcd13a488b3
71a6cbe2630a4b37d23275658bd3f2 [0.02s]
=> resolve image config for docker.io/library/golang@sha256:a452d6273ad03a47c2f29b898d6bb5
7630e77baf839651ef77d03e4e049c5bf3 [0.01s]
=> docker-image://docker.io/library/golang@sha256:a452d6273ad03a47c2f29b898d6bb57630e77baf
839651ef77d03e4e049c5bf3 CACHED [0.00s]
-> resolve docker.io/library/golang@sha256:a452d6273ad03a47c2f29b898d6bb57630e77baf839651e
f77d03e4e049c5bf3 [0.01s]
=> docker-image://docker.io/alpine/git@sha256:66b210a97bc07bfd4019826bcd13a488b371a6cbe263
0a4b37d23275658bd3f2 CACHED [0.00s]
-> resolve docker.io/alpine/git@sha256:66b210a97bc07bfd4019826bcd13a488b371a6cbe2630a4b37d
23275658bd3f2 [0.01s]
=> git clone https://github.com/vito/booklit ./ [1.61s]
Cloning into '.'...
=> git fetch origin d62e0d27a9668a8cf3850e405c6b19cd213f545a [0.55s]
From https://github.com/vito/booklit
* branch d62e0d27a9668a8cf3850e405c6b19cd213f545a -> FETCH_HEAD
=> git checkout d62e0d27a9668a8cf3850e405c6b19cd213f545a [0.29s]
Note: switching to 'd62e0d27a9668a8cf3850e405c6b19cd213f545a'.
You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by switching back to a branch.
If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -c with the switch command. Example:
git switch -c <new-branch-name>
Or undo this operation with:
git switch -
Turn off this advice by setting config variable advice.detachedHead to false
HEAD is now at d62e0d2 remove stale badge, add description
=> git submodule update --init --recursive [0.25s]
=> go install -mod=mod github.com/onsi/ginkgo/v2/ginkgo [2.61s]
go: downloading github.com/onsi/ginkgo/v2 v2.1.4
go: downloading github.com/onsi/ginkgo v1.16.4
go: downloading golang.org/x/tools v0.1.10
go: downloading github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0
go: downloading github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38
=> ./scripts/test -p [4.79s]
go: downloading github.com/sirupsen/logrus v1.8.1
go: downloading github.com/alecthomas/chroma v0.9.2
go: downloading github.com/onsi/gomega v1.19.0
go: downloading github.com/jessevdk/go-flags v1.4.0
go: downloading github.com/agext/levenshtein v1.2.3
go: downloading github.com/segmentio/textio v1.2.0
go: downloading golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a
go: downloading golang.org/x/net v0.0.0-20220225172249-27dd8689420f
go: downloading gopkg.in/yaml.v2 v2.4.0
go: downloading golang.org/x/text v0.3.7
go: downloading github.com/dlclark/regexp2 v1.4.0
go: downloading github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964
warning: no packages being tested depend on matches for pattern github.com/vito/bookli
t/booklitcmd
warning: no packages being tested depend on matches for pattern github.com/vito/bookli
t/chroma
warning: no packages being tested depend on matches for pattern github.com/vito/bookli
t/chroma/plugin
warning: no packages being tested depend on matches for pattern github.com/vito/bookli
t/docs/go
Running Suite: Booklit Suite - /bass/work/tests
===============================================
Random Seed: 1669598857
Will run 62 of 62 specs
Running in parallel across 15 processes
••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••
Ran 62 of 62 Specs in 0.049 seconds
SUCCESS! -- 62 Passed | 0 Failed | 0 Pending | 0 Skipped
coverage: 62.2% of statements
composite coverage: 62.2% of statements
Ginkgo ran 1 suite in 2.949404069s
Test Suite Passed
.go
{
:image
{
:repository "golang"
:platform
{
:os "linux"
}
:tag "latest"
:digest "sha256:a452d6273ad03a47c2f29b898d6bb57630e77baf839651ef77d03e4e049c5bf3"
}
:cmd .go
:args
(
  1. "build"
  2. "-o"
  3. ../booklit_linux_amd64
  4. "--ldflags"
  5. "-X github.com/vito/booklit.Version=dev"
  6. ./cmd/booklit
)
:env
{
:GOOS "linux"
:GOARCH "amd64"
}
:dir
.git
{
:image
.git
{
:image
.git
{
:image
.git
{
:image
{
:repository "alpine/git"
:platform
{
:os "linux"
}
:tag "latest"
:digest "sha256:66b210a97bc07bfd4019826bcd13a488b371a6cbe2630a4b37d23275658bd3f2"
}
:cmd .git
:args
(
  1. "clone"
  2. "https://github.com/vito/booklit"
  3. ./
)
}
:cmd .git
:args
(
  1. "fetch"
  2. "origin"
  3. "d62e0d27a9668a8cf3850e405c6b19cd213f545a"
)
}
:cmd .git
:args
(
  1. "checkout"
  2. "d62e0d27a9668a8cf3850e405c6b19cd213f545a"
)
}
:cmd .git
:args
(
  1. "submodule"
  2. "update"
  3. "--init"
  4. "--recursive"
)
}
./
:mounts
(
  1. {
    :source <cache: booklit-go-cache>/
    :target /go/
    }
)
}
./booklit_linux_amd64
𝄢

To re-evaluate and update all memoized results, run bass --bump:

bass --bump bass.lock

This command loads each module and re-evalutes each memoized call, updating the bass.lock file in-place.

demo webhook-driven CI/CD
𝄢

Bass Loop is a public service for calling Bass code in response to webhooks.

First, install the GitHub app and put a script like this in your repo at bass/github-hook:

; file for memoized dependency resolution
(def *memos* *dir*/bass.lock)

; load dependencies
(use (.git (linux/alpine/git))
     (git:github/vito/bass-loop/ref/main/bass/github.bass))

; run Go tests
(defn go-test [src & args]
  (from (linux/golang)
    (cd src
      ($ go test & $args))))

; standard suite of validations for the repo
(defn checks [src]
  {:test (go-test src "./...")})

; called by bass-loop
(defn main []
  (for [event *stdin*]
    (github:check-hook event git:checkout checks)))
stderr: 10 lines
=> resolve image config for docker.io/alpine/git:latest [0.16s]
=> resolve image config for docker.io/alpine/git@sha256:66b210a97bc07bfd4019826bcd13a488b3
71a6cbe2630a4b37d23275658bd3f2 [0.02s]
=> docker-image://docker.io/alpine/git@sha256:66b210a97bc07bfd4019826bcd13a488b371a6cbe263
0a4b37d23275658bd3f2 CACHED [0.00s]
-> resolve docker.io/alpine/git@sha256:66b210a97bc07bfd4019826bcd13a488b371a6cbe2630a4b37d
23275658bd3f2 [0.01s]
=> git ls-remote https://github.com/vito/bass-loop main [0.48s]
=> exporting to client directory [0.00s]
-> copying files 83B [0.00s]
main
𝄢

Next start a Bass runner to let Bass Loop use your local runtimes:

bass --runner myuser@github.bass-lang.org

From here on anything that myuser does to the repo will route an event to the bass/github-hook script with myuser's runners available for running thunks.

The github:check-hook helper handles check-related events by running thunks as GitHub status checks. Other events may be interpreted however you like.