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
<no command>
{
:image
{
:repository "alpine"
:platform
{
:architecture "amd64"
:os "linux"
}
:tag "latest"
:digest "sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126"
}
}
:args
(
  1. "echo"
  2. "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: 9 lines
=> resolve image config for docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a
01e5a861a624ee31d03372cc1d138a3126 [0.01s]
=> docker-image://docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a6
24ee31d03372cc1d138a3126 [0.00s]
-> resolve docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d
03372cc1d138a3126 [0.00s]
=> echo "Hello, world!" CACHED [0.00s]
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
<no command>
{
:image
{
:repository "alpine"
:platform
{
:architecture "amd64"
:os "linux"
}
:tag "latest"
:digest "sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126"
}
}
:args
(
  1. "sh"
  2. "-c"
  3. "echo hello >> file"
)
}
./file
𝄢

Thunk paths can be passed to other thunks.

(from (linux/alpine)
  ($ cat create-file/file))
"cat"
{
:image
<no command>
{
:image
{
:repository "alpine"
:platform
{
:architecture "amd64"
:os "linux"
}
:tag "latest"
:digest "sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126"
}
}
:args
(
  1. "cat"
  2. "sh"
    {
    :image
    <no command>
    {
    :image
    {
    :repository "alpine"
    :platform
    {
    :architecture "amd64"
    :os "linux"
    }
    :tag "latest"
    :digest "sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126"
    }
    }
    :args
    (
    1. "sh"
    2. "-c"
    3. "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: 9 lines
=> resolve image config for docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a
01e5a861a624ee31d03372cc1d138a3126 [0.01s]
=> docker-image://docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a6
24ee31d03372cc1d138a3126 CACHED [0.00s]
-> resolve docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d
03372cc1d138a3126 [0.01s]
=> sh -c "echo hello >> file" [0.18s]
=> cat {{thunk QJ49D61EQ6076: sh -c "echo hello >> file"}}/file [0.20s]
hello
"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: 8 lines
=> resolve image config for docker.io/alpine/git@sha256:66b210a97bc07bfd4019826bcd13a488b3
71a6cbe2630a4b37d23275658bd3f2 [0.13s]
=> 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 main [0.53s]
0537fa0323f1b12a43f0ab7dc90d2d00894d9c63 refs/heads/main
"git"
{
:image
"git"
{
:image
"git"
{
:image
"git"
{
:image
<no command>
{
:image
{
:repository "alpine/git"
:platform
{
:architecture "amd64"
:os "linux"
}
:tag "latest"
:digest "sha256:66b210a97bc07bfd4019826bcd13a488b371a6cbe2630a4b37d23275658bd3f2"
}
}
:args
(
  1. "git"
  2. "clone"
  3. "https://github.com/vito/bass"
  4. ./
)
}
:args
(
  1. "git"
  2. "fetch"
  3. "origin"
  4. "0537fa0323f1b12a43f0ab7dc90d2d00894d9c63"
)
}
:args
(
  1. "git"
  2. "checkout"
  3. "0537fa0323f1b12a43f0ab7dc90d2d00894d9c63"
)
}
:args
(
  1. "git"
  2. "submodule"
  3. "update"
  4. "--init"
  5. "--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
<no command>
{
:image
{
:repository "alpine/git"
:platform
{
:architecture "amd64"
:os "linux"
}
:tag "latest"
:digest "sha256:66b210a97bc07bfd4019826bcd13a488b371a6cbe2630a4b37d23275658bd3f2"
}
}
:args
(
  1. "git"
  2. "clone"
  3. "https://github.com/vito/bass"
  4. ./
)
}
𝄢

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-env {:MINUTE (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: 50 lines
=> resolve image config for docker.io/library/golang@sha256:403f48633fb5ebd49f9a2b6ad6719f
912df23dae44974a0c9445be331e72ff5e [0.27s]
=> resolve image config for docker.io/alpine/git@sha256:66b210a97bc07bfd4019826bcd13a488b3
71a6cbe2630a4b37d23275658bd3f2 [0.01s]
=> docker-image://docker.io/library/golang@sha256:403f48633fb5ebd49f9a2b6ad6719f912df23dae
44974a0c9445be331e72ff5e CACHED [0.00s]
-> resolve docker.io/library/golang@sha256:403f48633fb5ebd49f9a2b6ad6719f912df23dae44974a0
c9445be331e72ff5e [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.21s]
Cloning into '.'...
=> git fetch origin 07747d66601bc5433b37a64943ba4ef44b67a958 [0.54s]
From https://github.com/vito/booklit
* branch 07747d66601bc5433b37a64943ba4ef44b67a958 -> FETCH_HEAD
=> git checkout 07747d66601bc5433b37a64943ba4ef44b67a958 [0.25s]
Note: switching to '07747d66601bc5433b37a64943ba4ef44b67a958'.
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 07747d6 Merge pull request #53 from vito/dependabot/npm_and_yarn/qs-6.5
.3
=> git submodule update --init --recursive [0.23s]
=> go test ./tests/ [10.2s]
go: downloading github.com/onsi/ginkgo/v2 v2.1.4
go: downloading github.com/onsi/gomega v1.19.0
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.124s
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: 367 lines
=> resolve image config for docker.io/library/rust@sha256:346a6ecb2d445d27c70f4cc99c4a56ae
f75b47e62482c9734287cd6d98e061f0 [0.38s]
=> resolve image config for docker.io/alpine/git@sha256:66b210a97bc07bfd4019826bcd13a488b3
71a6cbe2630a4b37d23275658bd3f2 [0.01s]
=> docker-image://docker.io/library/rust@sha256:346a6ecb2d445d27c70f4cc99c4a56aef75b47e624
82c9734287cd6d98e061f0 CACHED [0.00s]
-> resolve docker.io/library/rust@sha256:346a6ecb2d445d27c70f4cc99c4a56aef75b47e62482c9734
287cd6d98e061f0 [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.15s]
Cloning into '.'...
=> git fetch origin ead65221ebe06ff5689e65b866d735d4365d0e9e [0.74s]
From https://github.com/alacritty/alacritty
* branch ead65221ebe06ff5689e65b866d735d4365d0e9e -> FETCH_HEAD
=> git checkout ead65221ebe06ff5689e65b866d735d4365d0e9e [0.42s]
Note: switching to 'ead65221ebe06ff5689e65b866d735d4365d0e9e'.
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 ead65221 Omit urgency hint focused window
=> git submodule update --init --recursive [0.28s]
=> cargo test ./alacritty_terminal/ [98.6s]
Updating crates.io index
Downloading crates ...
Downloaded arrayref v0.3.6
Downloaded dirs v4.0.0
Downloaded foreign-types-shared v0.3.1
Downloaded glutin_egl_sys v0.4.0
Downloaded heck v0.4.0
Downloaded glutin_glx_sys v0.4.0
Downloaded lazycell v1.3.0
Downloaded minimal-lexical v0.2.1
Downloaded parking_lot v0.12.1
Downloaded slotmap v1.0.6
Downloaded instant v0.1.12
Downloaded unicode-width v0.1.10
Downloaded vte_generate_state_changes v0.1.1
Downloaded fnv v1.0.7
Downloaded os_str_bytes v6.3.0
Downloaded servo-fontconfig v0.5.1
Downloaded itoa v1.0.3
Downloaded iovec v0.1.4
Downloaded glutin v0.30.7
Downloaded sctk-adwaita v0.5.3
Downloaded hashbrown v0.12.3
Downloaded strict-num v0.1.0
Downloaded lock_api v0.4.8
Downloaded tiny-skia v0.8.3
Downloaded net2 v0.2.37
Downloaded xdg v2.4.1
Downloaded freetype-rs v0.26.0
Downloaded foreign-types v0.5.0
Downloaded nix v0.24.2
Downloaded scoped-tls v1.0.0
Downloaded scopeguard v1.1.0
Downloaded signal-hook-mio v0.2.3
Downloaded proc-macro-error-attr v1.0.4
Downloaded serde_yaml v0.8.26
Downloaded same-file v1.0.6
Downloaded regex-automata v0.1.10
Downloaded walkdir v2.3.2
Downloaded slab v0.4.7
Downloaded xcursor v0.3.4
Downloaded wayland-cursor v0.29.5
Downloaded yaml-rust v0.4.5
Downloaded mio v0.6.23
Downloaded arrayvec v0.7.2
Downloaded bitflags v1.3.2
Downloaded dlib v0.5.0
Downloaded gethostname v0.2.3
Downloaded crossbeam-channel v0.5.6
Downloaded regex-syntax v0.6.27
Downloaded termcolor v1.1.3
Downloaded notify v5.1.0
Downloaded textwrap v0.15.1
Downloaded serde_derive v1.0.144
Downloaded smithay-client-toolkit v0.16.0
Downloaded smallvec v1.9.0
Downloaded vte v0.10.1
Downloaded wayland-scanner v0.29.5
Downloaded wayland-protocols v0.29.5
Downloaded wayland-commons v0.29.5
Downloaded xml-rs v0.8.4
Downloaded x11rb v0.10.1
Downloaded khronos_api v3.1.0
Downloaded wayland-sys v0.29.5
Downloaded thiserror-impl v1.0.35
Downloaded bytemuck v1.12.1
Downloaded base64 v0.13.0
Downloaded flate2 v1.0.24
Downloaded mio-uds v0.6.8
Downloaded indexmap v1.9.1
Downloaded copypasta v0.8.2
Downloaded thiserror v1.0.35
Downloaded x11rb-protocol v0.10.0
Downloaded libc v0.2.132
Downloaded winit v0.28.3
Downloaded expat-sys v2.1.6
Downloaded quote v1.0.21
Downloaded once_cell v1.14.0
Downloaded servo-fontconfig-sys v5.1.0
Downloaded gl_generator v0.14.0
Downloaded freetype-sys v0.13.1
Downloaded crossbeam-utils v0.8.12
Downloaded cmake v0.1.48
Downloaded clap_complete v3.2.5
Downloaded cc v1.0.73
Downloaded wayland-client v0.29.5
Downloaded syn v1.0.99
Downloaded unicode-ident v1.0.4
Downloaded signal-hook v0.3.14
Downloaded serde_json v1.0.85
Downloaded memmap2 v0.5.10
Downloaded signal-hook-registry v1.4.0
Downloaded serde v1.0.144
Downloaded nix v0.25.1
Downloaded ryu v1.0.11
Downloaded proc-macro2 v1.0.43
Downloaded pkg-config v0.3.25
Downloaded nom v7.1.1
Downloaded mio v0.8.4
Downloaded inotify v0.9.6
Downloaded foreign-types-macros v0.2.2
Downloaded clap v3.2.21
Downloaded parking_lot_core v0.9.3
Downloaded mio-extras v2.0.6
Downloaded memchr v2.5.0
Downloaded lazy_static v1.4.0
Downloaded cfg-if v0.1.10
Downloaded libloading v0.7.3
Downloaded cfg-if v1.0.0
Downloaded downcast-rs v1.2.0
Downloaded cty v0.2.2
Downloaded autocfg v1.1.0
Downloaded filetime v0.2.17
Downloaded dirs-sys v0.3.7
Downloaded clap_derive v3.2.18
Downloaded cfg_aliases v0.1.1
Downloaded version_check v0.9.4
Downloaded utf8parse v0.2.0
Downloaded strsim v0.10.0
Downloaded raw-window-handle v0.5.0
Downloaded proc-macro-error v1.0.4
Downloaded log v0.4.17
Downloaded percent-encoding v2.2.0
Downloaded memoffset v0.6.5
Downloaded png v0.17.6
Downloaded inotify-sys v0.1.5
Downloaded crossfont v0.5.1
Downloaded calloop v0.10.4
Downloaded x11-dl v2.20.0
Downloaded x11-clipboard v0.7.1
Downloaded wayland-sys v0.30.0
Downloaded tiny-skia-path v0.8.3
Downloaded miniz_oxide v0.5.4
Downloaded smithay-clipboard v0.6.6
Downloaded linked-hash-map v0.5.6
Downloaded jobserver v0.1.25
Downloaded crc32fast v1.3.2
Downloaded clap_lex v0.2.4
Downloaded atty v0.2.14
Downloaded adler v1.0.2
Downloaded vec_map v0.8.2
Compiling proc-macro2 v1.0.43
Compiling quote v1.0.21
Compiling unicode-ident v1.0.4
Compiling libc v0.2.132
Compiling cfg-if v1.0.0
Compiling syn v1.0.99
Compiling autocfg v1.1.0
Compiling serde_derive v1.0.144
Compiling pkg-config v0.3.25
Compiling serde v1.0.144
Compiling log v0.4.17
Compiling bitflags v1.3.2
Compiling xml-rs v0.8.4
Compiling version_check v0.9.4
Compiling lazy_static v1.4.0
Compiling smallvec v1.9.0
Compiling libloading v0.7.3
Compiling khronos_api v3.1.0
Compiling once_cell v1.14.0
Compiling hashbrown v0.12.3
Compiling dlib v0.5.0
Compiling memchr v2.5.0
Compiling linked-hash-map v0.5.6
Compiling ryu v1.0.11
Compiling thiserror v1.0.35
Compiling minimal-lexical v0.2.1
Compiling scoped-tls v1.0.0
Compiling cfg-if v0.1.10
Compiling yaml-rust v0.4.5
Compiling downcast-rs v1.2.0
Compiling crc32fast v1.3.2
Compiling memoffset v0.6.5
Compiling indexmap v1.9.1
Compiling slab v0.4.7
Compiling slotmap v1.0.6
Compiling proc-macro-error-attr v1.0.4
Compiling lock_api v0.4.8
Compiling parking_lot_core v0.9.3
Compiling cfg_aliases v0.1.1
Compiling signal-hook v0.3.14
Compiling adler v1.0.2
Compiling vec_map v0.8.2
Compiling wayland-sys v0.29.5
Compiling servo-fontconfig-sys v5.1.0
Compiling smithay-client-toolkit v0.16.0
Compiling miniz_oxide v0.5.4
Compiling x11-dl v2.20.0
Compiling wayland-scanner v0.29.5
Compiling nom v7.1.1
Compiling proc-macro-error v1.0.4
Compiling scopeguard v1.1.0
Compiling crossfont v0.5.1
Compiling serde_json v1.0.85
Compiling strict-num v0.1.0
Compiling utf8parse v0.2.0
Compiling flate2 v1.0.24
Compiling regex-syntax v0.6.27
Compiling jobserver v0.1.25
Compiling nix v0.24.2
Compiling iovec v0.1.4
Compiling net2 v0.2.37
Compiling nix v0.25.1
Compiling signal-hook-registry v1.4.0
Compiling dirs-sys v0.3.7
Compiling cc v1.0.73
Compiling memmap2 v0.5.10
Compiling dirs v4.0.0
Compiling vte_generate_state_changes v0.1.1
Compiling foreign-types-shared v0.3.1
Compiling bytemuck v1.12.1
Compiling lazycell v1.3.0
Compiling cmake v0.1.48
Compiling crossbeam-utils v0.8.12
Compiling arrayref v0.3.6
Compiling parking_lot v0.12.1
Compiling tiny-skia-path v0.8.3
Compiling png v0.17.6
Compiling gethostname v0.2.3
Compiling vte v0.10.1
Compiling wayland-sys v0.30.0
Compiling xcursor v0.3.4
Compiling base64 v0.13.0
Compiling itoa v1.0.3
Compiling wayland-client v0.29.5
Compiling wayland-protocols v0.29.5
Compiling unicode-width v0.1.10
Compiling cty v0.2.2
Compiling os_str_bytes v6.3.0
Compiling heck v0.4.0
Compiling arrayvec v0.7.2
Compiling regex-automata v0.1.10
Compiling tiny-skia v0.8.3
Compiling clap_lex v0.2.4
Compiling raw-window-handle v0.5.0
Compiling winit v0.28.3
Compiling atty v0.2.14
Compiling freetype-sys v0.13.1
Compiling expat-sys v2.1.6
Compiling inotify-sys v0.1.5
Compiling glutin v0.30.7
Compiling termcolor v1.1.3
Compiling textwrap v0.15.1
Compiling strsim v0.10.0
Compiling same-file v1.0.6
Compiling freetype-rs v0.26.0
Compiling servo-fontconfig v0.5.1
Compiling walkdir v2.3.2
Compiling crossbeam-channel v0.5.6
Compiling inotify v0.9.6
Compiling filetime v0.2.17
Compiling wayland-commons v0.29.5
Compiling x11rb-protocol v0.10.0
Compiling instant v0.1.12
Compiling percent-encoding v2.2.0
Compiling xdg v2.4.1
Compiling fnv v1.0.7
Compiling thiserror-impl v1.0.35
Compiling alacritty_config_derive v0.2.2-dev (/bass/work/alacritty_config_derive)
Compiling foreign-types-macros v0.2.2
Compiling clap_derive v3.2.18
Compiling wayland-cursor v0.29.5
Compiling foreign-types v0.5.0
Compiling clap v3.2.21
Compiling clap_complete v3.2.5
Compiling x11rb v0.10.1
Compiling serde_yaml v0.8.26
Compiling gl_generator v0.14.0
Compiling mio v0.6.23
Compiling calloop v0.10.4
Compiling mio v0.8.4
Compiling notify v5.1.0
Compiling alacritty_config v0.1.2-dev (/bass/work/alacritty_config)
Compiling mio-uds v0.6.8
Compiling signal-hook-mio v0.2.3
Compiling mio-extras v2.0.6
Compiling x11-clipboard v0.7.1
Compiling glutin_egl_sys v0.4.0
Compiling glutin_glx_sys v0.4.0
Compiling alacritty_terminal v0.18.1-dev (/bass/work/alacritty_terminal)
Compiling alacritty v0.13.0-dev (/bass/work/alacritty)
Compiling smithay-clipboard v0.6.6
Compiling sctk-adwaita v0.5.3
Compiling copypasta v0.8.2
Finished test [unoptimized + debuginfo] target(s) in 1m 37s
Running unittests src/main.rs (target/debug/deps/alacritty-f805289f0ace5c81)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 70 filtered out; finished
in 0.00s
Running unittests src/lib.rs (target/debug/deps/alacritty_config-d8b06d7c12b52e13
)
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-2f1b7e9a0
f024a16)
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-bd1bb342a160056f)
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-c71bd28054e412
30)
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-eb0236d173b5ecae)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 43 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
<no command>
{
:image
{
:repository "python"
:platform
{
:architecture "amd64"
:os "linux"
}
:tag "latest"
:digest "sha256:f7382f4f9dbc51183c72d621b9c196c1565f713a1fe40c119d215c961fa22815"
}
}
:args
(
  1. "python"
  2. "-m"
  3. "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
<no command>
{
:image
{
:repository "alpine"
:platform
{
:architecture "amd64"
:os "linux"
}
:tag "latest"
:digest "sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126"
}
}
:args
(
  1. "wget"
  2. "-O-"
  3. "python"
    {
    :image
    <no command>
    {
    :image
    {
    :repository "python"
    :platform
    {
    :architecture "amd64"
    :os "linux"
    }
    :tag "latest"
    :digest "sha256:f7382f4f9dbc51183c72d621b9c196c1565f713a1fe40c119d215c961fa22815"
    }
    }
    :args
    (
    1. "python"
    2. "-m"
    3. "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: 20 lines
=> resolve image config for docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a
01e5a861a624ee31d03372cc1d138a3126 [0.01s]
=> resolve image config for docker.io/library/python@sha256:f7382f4f9dbc51183c72d621b9c196
c1565f713a1fe40c119d215c961fa22815 [0.41s]
=> mkfile /index.html CACHED [0.00s]
=> docker-image://docker.io/library/python@sha256:f7382f4f9dbc51183c72d621b9c196c1565f713a
1fe40c119d215c961fa22815 CACHED [0.00s]
-> resolve docker.io/library/python@sha256:f7382f4f9dbc51183c72d621b9c196c1565f713a1fe40c1
19d215c961fa22815 [0.01s]
=> python -m http.server CANCELED [1.07s]
=> docker-image://docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a6
24ee31d03372cc1d138a3126 CACHED [0.00s]
-> resolve docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d
03372cc1d138a3126 [0.00s]
=> wget -O- {{thunk 29S6L2EK18L8E: python -m http.server}}:http [0.18s]
Connecting to 29S6L2EK18L8E:8000 (10.73.0.18:8000)
writing to stdout
- 100% |********************************| 13 0:00:00 ETA
written to stdout
Hello, world!
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: 31 lines
=> resolve image config for docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a
01e5a861a624ee31d03372cc1d138a3126 [0.01s]
=> resolve image config for docker.io/library/golang@sha256:403f48633fb5ebd49f9a2b6ad6719f
912df23dae44974a0c9445be331e72ff5e [0.01s]
=> resolve image config for docker.io/alpine/git@sha256:66b210a97bc07bfd4019826bcd13a488b3
71a6cbe2630a4b37d23275658bd3f2 [0.00s]
=> docker-image://docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a6
24ee31d03372cc1d138a3126 CACHED [0.00s]
-> resolve docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d
03372cc1d138a3126 [0.01s]
=> docker-image://docker.io/library/golang@sha256:403f48633fb5ebd49f9a2b6ad6719f912df23dae
44974a0c9445be331e72ff5e CACHED [0.00s]
-> resolve docker.io/library/golang@sha256:403f48633fb5ebd49f9a2b6ad6719f912df23dae44974a0
c9445be331e72ff5e [0.01s]
=> docker-image://docker.io/alpine/git@sha256:66b210a97bc07bfd4019826bcd13a488b371a6cbe263
0a4b37d23275658bd3f2 [0.01s]
-> resolve docker.io/alpine/git@sha256:66b210a97bc07bfd4019826bcd13a488b371a6cbe2630a4b37d
23275658bd3f2 [0.01s]
=> git fetch origin 07747d66601bc5433b37a64943ba4ef44b67a958 CACHED [0.00s]
=> git checkout 07747d66601bc5433b37a64943ba4ef44b67a958 CACHED [0.00s]
=> git clone https://github.com/vito/booklit ./ CACHED [0.00s]
=> git submodule update --init --recursive CACHED [0.00s]
=> go build ./cmd/booklit [8.26s]
go: downloading github.com/jessevdk/go-flags v1.4.0
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
=> {{thunk N4TBK599C35KG: go build ./cmd/booklit}}/booklit --version [0.24s]
time="2023-04-18T04:59:06Z" level=info msg="plugin registered" plugin=baselit
0.0.0-dev
"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": {
    "thunk": {
      "image": {
        "ref": {
          "platform": {
            "os": "linux",
            "arch": "amd64"
          },
          "repository": "golang",
          "tag": "latest",
          "digest": "sha256:403f48633fb5ebd49f9a2b6ad6719f912df23dae44974a0c9445be331e72ff5e"
        }
      }
    }
  },
  "args": [
    {
      "string": {
        "value": "go"
      }
    },
    {
      "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": {
                          "thunk": {
                            "image": {
                              "ref": {
                                "platform": {
                                  "os": "linux",
                                  "arch": "amd64"
                                },
                                "repository": "alpine/git",
                                "tag": "latest",
                                "digest": "sha256:66b210a97bc07bfd4019826bcd13a488b371a6cbe2630a4b37d23275658bd3f2"
                              }
                            }
                          }
                        },
                        "args": [
                          {
                            "string": {
                              "value": "git"
                            }
                          },
                          {
                            "string": {
                              "value": "clone"
                            }
                          },
                          {
                            "string": {
                              "value": "https://github.com/vito/booklit"
                            }
                          },
                          {
                            "dirPath": {
                              "path": "."
                            }
                          }
                        ]
                      }
                    },
                    "args": [
                      {
                        "string": {
                          "value": "git"
                        }
                      },
                      {
                        "string": {
                          "value": "fetch"
                        }
                      },
                      {
                        "string": {
                          "value": "origin"
                        }
                      },
                      {
                        "string": {
                          "value": "07747d66601bc5433b37a64943ba4ef44b67a958"
                        }
                      }
                    ]
                  }
                },
                "args": [
                  {
                    "string": {
                      "value": "git"
                    }
                  },
                  {
                    "string": {
                      "value": "checkout"
                    }
                  },
                  {
                    "string": {
                      "value": "07747d66601bc5433b37a64943ba4ef44b67a958"
                    }
                  }
                ]
              }
            },
            "args": [
              {
                "string": {
                  "value": "git"
                }
              },
              {
                "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
<no command>
{
:image
{
:repository "nixery.dev/gh"
:platform
{
:architecture "amd64"
:os "linux"
}
:tag "latest"
:digest "sha256:c2f477df8a042f74be03653149ac918d2071e5783c3625030c475953c741b929"
}
}
:args
(
  1. "gh"
  2. "release"
  3. "create"
  4. "v0.0.1"
  5. <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.

(run (from (linux/alpine) ; resolves linux/alpine and writes to *memos*
       ($ echo hi)))

(run (from (linux/alpine) ; uses the digest from *memos*
       ($ cat $*memos*))) ; reveals the wizard behind the curtain
stderr: 82 lines
=> resolve image config for docker.io/library/alpine:latest [0.21s]
=> echo hi [0.17s]
hi
=> resolve image config for docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a
01e5a861a624ee31d03372cc1d138a3126 [0.01s]
=> upload <host: /tmp/bass-scope3540661251>/bass.lock [0.01s]
-> transferring /tmp/bass-scope3540661251: 1.50kB [0.00s]
=> docker-image://docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a6
24ee31d03372cc1d138a3126 CACHED [0.00s]
-> resolve docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d
03372cc1d138a3126 [0.01s]
=> copy /bass.lock /bass.lock CACHED [0.00s]
=> cat <host: /tmp/bass-scope3540661251>/bass.lock [0.19s]
memos: {
module: {
args: {
command_path: {
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"
}
}
}
bindings: {
symbol: "tag"
value: {
string: {
value: "latest"
}
}
}
}
}
}
}
output: {
thunk: {
image: {
ref: {
platform: {
os: "linux"
arch: "amd64"
}
repository: "alpine"
tag: "latest"
digest: "sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d
138a3126"
}
}
}
}
}
}
}
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: 7 lines
error! call trace (oldest first):
┆ <fs>/literate-9:1:0..1:35
1 │ git:github/vito/booklit/ref/master/
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
unbound symbol: git
𝄢

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: 15 lines
error! call trace (oldest first):
┆ <fs>/literate-9:1:0..4:47
1 │ (let [src git:github/vito/booklit/ref/master/]
2 │ (use (src/bass/booklit.bass))
3 │ (when (succeeds? (booklit:tests src))
4 │ (booklit:build src "dev" "linux" "amd64")))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
┆ (10 internal calls elided)
┆ <fs>/literate-9:1:10..1:45
1 │ (let [src git:github/vito/booklit/ref/master/]
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
unbound symbol: git
𝄢

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: 34 lines
=> resolve image config for docker.io/alpine/git:latest [0.10s]
=> git ls-remote https://github.com/vito/bass-loop main [0.52s]
f38035a7a676ffed5e10b0eb3a0185d2f504d07d refs/heads/main
=> 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 clone https://github.com/vito/bass-loop ./ [1.54s]
Cloning into '.'...
=> git fetch origin f38035a7a676ffed5e10b0eb3a0185d2f504d07d [0.57s]
From https://github.com/vito/bass-loop
* branch f38035a7a676ffed5e10b0eb3a0185d2f504d07d -> FETCH_HEAD
=> git checkout f38035a7a676ffed5e10b0eb3a0185d2f504d07d [0.30s]
Note: switching to 'f38035a7a676ffed5e10b0eb3a0185d2f504d07d'.
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 f38035a bump bass
=> git submodule update --init --recursive [0.27s]
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.