Bass

guide

This guide glosses over language semantics in favor of being a quick reference for common tasks. If you'd like to learn the language, see bassics.

getting started

Bass is shipped as a single bass binary which needs to be installed somewhere in your $PATH.

To run Bass you'll need either Docker Engine (Linux), Docker Desktop (OS X, Windows), or Buildkit running.

With everything installed, try one of the demos:

bass demos/git-lib.bass

If you see bass: command not found, it's not in your $PATH.

If you see some other kind of error you're welcome to ask for help in GitHub or Discord.

running thunks

𝄢

Bass is built around thunks. Thunks are cacheable commands that produce files and/or a stream of values.

(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!"
)
}
𝄢

Throughout this documentation, thunks will be rendered as space invaders to make them easier to identify.

𝄢

To run a thunk's command and raise an error if it fails, call run:

(run (from (linux/alpine)
       ($ echo "Hello, world!")))
stderr: 7 lines
=> resolve image config for docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a
01e5a861a624ee31d03372cc1d138a3126 [0.02s]
=> docker-image://docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a6
24ee31d03372cc1d138a3126 [0.01s]
-> resolve docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d
03372cc1d138a3126 [0.00s]
=> echo "Hello, world!" CACHED [0.00s]
null
𝄢

To run a thunk and get true or false instead of erroring, call succeeds?:

(succeeds? (from (linux/alpine)
             ($ sh -c "exit 1")))
stderr: 8 lines
=> resolve image config for docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a
01e5a861a624ee31d03372cc1d138a3126 [0.02s]
=> docker-image://docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a6
24ee31d03372cc1d138a3126 CACHED [0.00s]
-> resolve docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d
03372cc1d138a3126 [0.00s]
=> sh -c "exit 1" ERROR [0.19s]
!!! sh -c "exit 1"
false
𝄢

Thunks are cached forever. They can be cleared with bass --prune, but this should only be necessary for regaining disk space.

𝄢

If you want to run a thunk multiple times, just set a different value as an environment variable. Tip: use now to control cache granularity.

(run (with-env
       (from (linux/alpine)
         ($ echo "Hi again!"))
       {:MINUTE (now 60)}))
stderr: 8 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]
=> echo "Hi again!" [0.18s]
Hi again!
null

reading output

𝄢

To parse a stream of JSON values from a thunk's stdout, call read with the :json protocol:

(def cat-thunk
  (from (linux/alpine)
   ; note: stdin is also JSON
    (.cat "hello" "goodbye")))

(let [stream (read cat-thunk :json)]
  [(next stream :end)
   (next stream :end)
   (next stream :end)])
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.00s]
=> .cat [0.19s]
"hello"
"goodbye"
(
  1. "hello"
  2. "goodbye"
  3. end
)
𝄢

To read output line-by-line, set the protocol to :lines:

(-> ($ ls -r /usr/bin)
    (with-image (linux/alpine))
    (read :lines)
    next)
stderr: 149 lines
=> resolve image config for docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a
01e5a861a624ee31d03372cc1d138a3126 [0.02s]
=> docker-image://docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a6
24ee31d03372cc1d138a3126 CACHED [0.00s]
-> resolve docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d
03372cc1d138a3126 [0.01s]
=> ls -r /usr/bin [0.18s]
yes
xzcat
xxd
xargs
whois
whoami
who
which
wget
wc
volname
vlock
vi
uuencode
uudecode
uptime
unzip
unxz
unshare
unlzop
unlzma
unlink
unix2dos
uniq
unexpand
udhcpc6
ttysize
tty
truncate
traceroute6
traceroute
tr
top
timeout
time
test
tee
tail
tac
sum
strings
ssl_client
split
sort
shuf
shred
showkey
sha512sum
sha3sum
sha256sum
sha1sum
setsid
setkeycodes
seq
scanelf
resize
reset
renice
realpath
readlink
pwdx
pstree
pscan
printf
pmap
pkill
pgrep
paste
passwd
openvt
od
nslookup
nsenter
nproc
nohup
nmeter
nl
nc
mkpasswd
mkfifo
microcom
mesg
md5sum
lzopcat
lzma
lzcat
lsusb
lsof
logger
less
ldd
last
killall
ipcs
ipcrm
install
id
iconv
hostid
hexdump
head
hd
groups
getent
getconf
fuser
free
fold
flock
find
fallocate
factor
expr
expand
env
eject
du
dos2unix
dirname
diff
deallocvt
dc
cut
cryptpw
crontab
cpio
comm
cmp
clear
cksum
chvt
cal
bzip2
bzcat
bunzip2
blkdiscard
beep
bc
basename
awk
[[
[
"yes"
𝄢

To parse UNIX style tabular output, set the protocol to :unix-table:

(-> ($ ls -r /usr/bin)
    (with-image (linux/alpine))
    (read :unix-table)
    next)
stderr: 7 lines
=> resolve image config for docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a
01e5a861a624ee31d03372cc1d138a3126 [0.02s]
=> docker-image://docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a6
24ee31d03372cc1d138a3126 [0.01s]
-> resolve docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d
03372cc1d138a3126 [0.01s]
=> ls -r /usr/bin CACHED [0.00s]
(
  1. "yes"
)
𝄢

To collect all output into one big string, set the protocol to :raw:

(-> ($ echo "Hello, world!")
    (with-image (linux/alpine))
    (read :raw)
    next)
stderr: 7 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!\n"

providing secrets

𝄢

To shroud a string in secrecy, pass it to mask and give it a name.

(mask "hunter2" :nickserv)
<secret: nickserv (7 bytes)>
𝄢

Secrets can be passed to thunks as regular strings. When serialized, a secret's value is omitted.

($ echo (mask "secret" :password))
"echo"
{
:args
(
  1. "echo"
  2. <secret: password (6 bytes)>
)
}
𝄢

Bass does not mask the secret from the command's output. This may come in the future.

𝄢

Sensitive values can end up in all sorts of sneaky places. Bass does its best to prevent that from happening.

  • A thunk's command runs in an isolated environment, so an evil thunk can't* spy on your secrets.

  • A thunk's command (i.e. stdin, env, argv) isn't captured into image layer metadata, so exporting a thunk as an OCI image will not leak secrets passed to it.

  • Secret values are never serialized, so publishing a thunk path will not leak any secrets used to build it.

  • All env vars passed to bass are only provided to the entrypoint script (as *env*). They are also removed from the bass process so that they can't be sneakily accessed at runtime.

With the above precautions, passing secrets to thunks as env vars may often be most ergonomic approach. If you have more ideas, please suggest them!

𝄢

To pass a secret to a command using a secret mount, use with-mount:

(-> ($ cat /secret)
    (with-mount (mask "hello" :shh) /secret)
    (with-image (linux/alpine))
    run)
stderr: 8 lines
=> resolve image config for docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a
01e5a861a624ee31d03372cc1d138a3126 [0.02s]
=> docker-image://docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a6
24ee31d03372cc1d138a3126 CACHED [0.00s]
-> resolve docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d
03372cc1d138a3126 [0.01s]
=> cat /secret [0.21s]
hello
null

* This is all obviously to the best of my ability - I can't promise it's perfect. If you find other ways to make Bass safer, please share them!

caching directories

𝄢

Cache paths may be created using cache-dir and passed to thunks like any other path. Any data written to a cache path persists until cleared by bass --prune.

(def my-cache (cache-dir "my cache"))

(defn counter [tag]
  (from (linux/alpine)
    (-> ($ sh -c "echo $0 >> /var/cache/file; cat /var/cache/file | wc -l"
           $tag)
        (with-mount my-cache /var/cache/))))

(defn count [tag]
  (next (read (counter tag) :json)))

[(count "once")
 (count "twice")
 (count "thrice")]
stderr: 12 lines
=> sh -c "echo $0 >> /var/cache/file; cat /var/cache/file | wc -l" once [0.19s]
7
=> sh -c "echo $0 >> /var/cache/file; cat /var/cache/file | wc -l" twice [0.21s]
8
=> 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 $0 >> /var/cache/file; cat /var/cache/file | wc -l" thrice [0.18s]
9
(
  1. 7
  2. 8
  3. 9
)
𝄢

Currently only one thunk can access a cache path at a time. This may become configurable in the future.

building stuff

passing bits around

𝄢

Thunks run in an initial working directory controlled by Bass. Files created within this directory can be passed to other thunks by using thunk paths.

𝄢

Thunk paths are created by using a thunk with path notation:

(def meowed
  (from (linux/alpine)
    (-> ($ sh -c "cat > ./file")
        (with-stdin ["hello" "goodbye"]))))

meowed/file
"sh"
{
:image
<no command>
{
:image
{
:repository "alpine"
:platform
{
:architecture "amd64"
:os "linux"
}
:tag "latest"
:digest "sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126"
}
}
:args
(
  1. "sh"
  2. "-c"
  3. "cat > ./file"
)
:stdin
(
  1. "hello"
  2. "goodbye"
)
}
./file
𝄢

If the thunk isn't bound to a symbol first, you can use subpath:

(-> ($ sh -c "cat > ./file")
    (with-image (linux/alpine))
    (with-stdin ["hello" "goodbye"])
    (subpath ./file))
"sh"
{
:image
<no command>
{
:image
{
:repository "alpine"
:platform
{
:architecture "amd64"
:os "linux"
}
:tag "latest"
:digest "sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126"
}
}
:args
(
  1. "sh"
  2. "-c"
  3. "cat > ./file"
)
:stdin
(
  1. "hello"
  2. "goodbye"
)
}
./file
𝄢

Just like thunks, a thunk path is just an object. Its underlying thunk won't run until the path is needed by something.

𝄢

When you read a thunk path, Bass runs its thunk and reads the content of the path using the same protocols for reading output:

(next (read meowed/file :json))
stderr: 7 lines
=> resolve image config for docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a
01e5a861a624ee31d03372cc1d138a3126 [0.02s]
=> docker-image://docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a6
24ee31d03372cc1d138a3126 CACHED [0.00s]
-> resolve docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d
03372cc1d138a3126 [0.01s]
=> sh -c "cat > ./file" [0.22s]
"hello"
𝄢

When you pass a thunk path to an outer thunk, Bass runs the path's thunk and mounts the path into the outer thunk's working directory under a hashed directory name:

(-> ($ ls -al meowed/file)
    (with-image (linux/alpine))
    run)
stderr: 9 lines
=> docker-image://docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a6
24ee31d03372cc1d138a3126 CACHED [0.00s]
-> resolve docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d
03372cc1d138a3126 [0.01s]
=> sh -c "cat > ./file" [0.22s]
=> resolve image config for docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a
01e5a861a624ee31d03372cc1d138a3126 [0.01s]
=> ls -al {{thunk ITFTJGPTH9SDQ: sh -c "cat > ./file"}}/file [0.23s]
-rw-r--r-- 1 root root 18 Oct 26 1985 ./ITFTJGPTH9SDQ/file
null
𝄢

If the outer thunk sets a thunk path as its working directory (viw cd or with-dir), you can use ../ to refer back to the original working directory.

(defn go-build [src pkg]
  (-> (from (linux/golang)
        (cd src
          ($ go build -o ./out/ $pkg)))
      (subpath ./out/)))

(def cloned
  (from (linux/alpine/git)
    ($ git clone "https://github.com/vito/bass" ./repo/)))

(go-build cloned/repo/ "./cmd/...")
"go"
{
:image
<no command>
{
:image
{
:repository "golang"
:platform
{
:architecture "amd64"
:os "linux"
}
:tag "latest"
:digest "sha256:403f48633fb5ebd49f9a2b6ad6719f912df23dae44974a0c9445be331e72ff5e"
}
}
:args
(
  1. "go"
  2. "build"
  3. "-o"
  4. ./out/
  5. "./cmd/..."
)
:mounts
(
  1. {
    :source
    "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. ./repo/
    )
    }
    ./repo/
    :target ./
    }
)
}
./out/
𝄢

Note that any modifications made to an input thunk path will not propagate to subsequent thunks.

𝄢

Astute observers will note that cloned above is not a hermetic, because it doesn't specify a version.

𝄢

The .git module provides basic tools for cloning Git repositories in a hermetic manner.

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

(let [uri "https://github.com/vito/bass"]
  (git:checkout uri (git:ls-remote uri "HEAD")))
stderr: 8 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/bass HEAD [0.72s]
0537fa0323f1b12a43f0ab7dc90d2d00894d9c63 HEAD
"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"
)
}
./
𝄢

The .git module also provides github, a path root for repositories hosted at GitHub.

git:github/vito/bass/ref/HEAD/
"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"
)
}
./

troubleshooting

When something goes wrong, Bass tries to provide an ergonomic error message. Backtraces show annotated source code complete with syntax highlighting. When a thunk fails its output is included in the error message at the bottom of the screen so you don't have to skim the whole output.

(defn echo-sleep-exit [msg seconds exit-code]
  (subpath
    (from (linux/alpine)
      (with-env
        ($ sleep (str seconds))
        {:AT (now 0)})
      ($ sh -c (str "echo \"$0\"; exit " exit-code) $msg))
    ./))

(defn ls paths
  (run (from (linux/alpine)
         ($ ls & $paths))))

(defn main []
  (ls (echo-sleep-exit "hello\nanother line" 1 1)
      (echo-sleep-exit "oh no" 3 42)))
stderr: 45 lines
=> docker-image://docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a6
24ee31d03372cc1d138a3126 CACHED [0.00s]
-> resolve docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d
03372cc1d138a3126 [0.01s]
=> resolve image config for docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a
01e5a861a624ee31d03372cc1d138a3126 [0.01s]
=> sleep 1 [1.25s]
=> sh -c "echo \"$0\"; exit 1" "hello\nanother line" ERROR [0.23s]
hello
another line
=> sleep 3 CANCELED [1.56s]
!!! sh -c "echo \"$0\"; exit 1" "hello\nanother line"
4: [0.16s] hello
4: [0.16s] another line
error! call trace (oldest first):
┆ <fs>/multi-fail.bass:15:2..16:37
14 │ (defn main []
15 │ (ls (echo-sleep-exit "hello\nanother line" 1 1)
16 │ (echo-sleep-exit "oh no" 3 42)))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
┆ <fs>/multi-fail.bass:11:2..12:26
10 │ (defn ls paths
11 │ (run (from (linux/alpine)
12 │ ($ ls & $paths))))
^^^^^^^^^^^^^^^^^^^^^^^^^
resolve failed: exit code: 1
run summary:
=> docker-image://docker.io/library/alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861
a624ee31d03372cc1d138a3126
=> sleep 1 [1.25s]
=> sleep 3 [canceled] [1.56s]
=> sh -c "echo \"$0\"; exit 1" "hello\nanother line" [0.23s]
hello
another line
ERROR: exit code: 1
=> sh -c "echo \"$0\"; exit 42" "oh no"
=> ls {{thunk 4G7OBEL2GH0SI: sh -c "echo \"$0\"; exit 1" "hello\nanother line"}}/ {{thun
k 585RSM82KR2VE: sh -c "echo \"$0\"; exit 42" "oh no"}}/
for more information, refer to the full output above

That being said, there's a good chance you'll run into a cryptic error message now and then while I work towards making them friendly. If you find one, please open an issue.

exporting files

𝄢

Thunk paths can be saved in JSON format for archival, auditing, efficient distribution, or just for funsies.

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

(-> ($ go build -o ../out/ "./cmd/...")
    (with-dir git:github/vito/bass/ref/HEAD/)
    (with-image (linux/golang))
    (subpath ./out/)
    (emit *stdout*))
{
  "thunk": {
    "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": "-o"
        }
      },
      {
        "dirPath": {
          "path": "../out"
        }
      },
      {
        "string": {
          "value": "./cmd/..."
        }
      }
    ],
    "dir": {
      "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/bass"
                          }
                        },
                        {
                          "dirPath": {
                            "path": "."
                          }
                        }
                      ]
                    }
                  },
                  "args": [
                    {
                      "string": {
                        "value": "git"
                      }
                    },
                    {
                      "string": {
                        "value": "fetch"
                      }
                    },
                    {
                      "string": {
                        "value": "origin"
                      }
                    },
                    {
                      "string": {
                        "value": "0537fa0323f1b12a43f0ab7dc90d2d00894d9c63"
                      }
                    }
                  ]
                }
              },
              "args": [
                {
                  "string": {
                    "value": "git"
                  }
                },
                {
                  "string": {
                    "value": "checkout"
                  }
                },
                {
                  "string": {
                    "value": "0537fa0323f1b12a43f0ab7dc90d2d00894d9c63"
                  }
                }
              ]
            }
          },
          "args": [
            {
              "string": {
                "value": "git"
              }
            },
            {
              "string": {
                "value": "submodule"
              }
            },
            {
              "string": {
                "value": "update"
              }
            },
            {
              "string": {
                "value": "--init"
              }
            },
            {
              "string": {
                "value": "--recursive"
              }
            }
          ]
        },
        "path": {
          "dir": {
            "path": "."
          }
        }
      }
    }
  },
  "path": {
    "dir": {
      "path": "out"
    }
  }
}
null
𝄢

Feeding thunk path JSON to bass --export will print a tar stream containing the file tree.

exporting images

𝄢

Feeding thunk JSON to bass --export will print an OCI image tar stream, which can be piped to docker load for troubleshooting with docker run. This will be made easier in the future.

(emit
  (from (linux/ubuntu)
    ($ apt-get update)
    ($ apt-get -y install git))
  *stdout*)
{
  "image": {
    "thunk": {
      "image": {
        "thunk": {
          "image": {
            "ref": {
              "platform": {
                "os": "linux",
                "arch": "amd64"
              },
              "repository": "ubuntu",
              "tag": "latest",
              "digest": "sha256:67211c14fa74f070d27cc59d69a7fa9aeff8e28ea118ef3babc295a0428a6d21"
            }
          }
        }
      },
      "args": [
        {
          "string": {
            "value": "apt-get"
          }
        },
        {
          "string": {
            "value": "update"
          }
        }
      ]
    }
  },
  "args": [
    {
      "string": {
        "value": "apt-get"
      }
    },
    {
      "string": {
        "value": "-y"
      }
    },
    {
      "string": {
        "value": "install"
      }
    },
    {
      "string": {
        "value": "git"
      }
    }
  ]
}
null

special tactics

pinning in bass.lock

𝄢

Bass comes with baby's first dependency pinning solution: memo. It works by storing results of functions loaded from Bass modules into a file typically called bass.lock and committed to your repository.

𝄢

memo takes a bass.lock path, a module thunk, and a symbol, and returns a memoized function.

(def memo-ls-remote
  (memo *dir*/bass.lock (.git (linux/alpine/git)) :ls-remote))
memo-ls-remote
𝄢

Calling the function passes through to the specified function from the loaded module.

(memo-ls-remote "https://github.com/moby/buildkit" "HEAD")
stderr: 8 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/moby/buildkit HEAD [0.58s]
f1f27537acc758d10765876fca9e5270c674de55 HEAD
"f1f27537acc758d10765876fca9e5270c674de55"
𝄢

When the function is called again with the same arguments, the cached response value is returned instead of making the call again:

(memo-ls-remote "https://github.com/moby/buildkit" "HEAD")
"f1f27537acc758d10765876fca9e5270c674de55"
𝄢

Use bass --bump to refresh every dependency in a bass.lock file:

bass --bump bass.lock

The bass --bump command re-loads all embedded module thunks and calls each function with each of its its associated arguments, updating the file in-place.

𝄢

Memoization is mostly leveraged for caching dependency version resolution. For this, your module must define the bass.lock path as a special binding: *memos*.

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

The linux and github path roots use this binding to automatically discover the memos location.

(use (.git (linux/alpine/git)))
git:github/vito/bass/ref/main/
stderr: 9 lines
=> resolve image config for docker.io/alpine/git:latest [0.15s]
=> 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 main [0.52s]
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"
)
}
./
𝄢

Third-party modules may respect this binding too. Here's how linux is defined, for reference:

(defop linux args scope
  (let [path-root (path {:os "linux"} (:*memos* scope null))]
    (eval [path-root & args] scope)))
linux

sharing bass code

Using bass.lock files lets you share and reuse Bass code in git repos:

; image and git path resolution will be cached here.
;
; images store digests, git paths store shas.
(def *memos* *dir*/git-lib.lock)

(use (.git (linux/alpine/git))
     (git:github/vito/tabs/ref/main/wget.bass))

(defn main []
  (emit (wget:wget "https://example.com" ./index.html) *stdout*))
stderr: 31 lines
=> resolve image config for docker.io/alpine/git@sha256:66b210a97bc07bfd4019826bcd13a488b3
71a6cbe2630a4b37d23275658bd3f2 [0.00s]
=> docker-image://docker.io/alpine/git@sha256:66b210a97bc07bfd4019826bcd13a488b371a6cbe263
0a4b37d23275658bd3f2 [0.00s]
-> resolve docker.io/alpine/git@sha256:66b210a97bc07bfd4019826bcd13a488b371a6cbe2630a4b37d
23275658bd3f2 [0.00s]
=> git fetch origin 6ca3f15d70b739a7929886e20b61dbc4cbdc4e22 CACHED [0.00s]
From https://github.com/vito/tabs
* branch 6ca3f15d70b739a7929886e20b61dbc4cbdc4e22 -> FETCH_HEAD
=> git checkout 6ca3f15d70b739a7929886e20b61dbc4cbdc4e22 CACHED [0.00s]
Note: switching to '6ca3f15d70b739a7929886e20b61dbc4cbdc4e22'.
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 6ca3f15 nix/linux: build FHS layout
=> git clone https://github.com/vito/tabs ./ CACHED [0.00s]
Cloning into '.'...
=> git submodule update --init --recursive CACHED [0.00s]
{
  "thunk": {
    "image": {
      "thunk": {
        "image": {
          "archive": {
            "platform": {
              "os": "linux",
              "arch": "amd64"
            },
            "file": {
              "thunk": {
                "thunk": {
                  "image": {
                    "thunk": {
                      "image": {
                        "thunk": {
                          "image": {
                            "thunk": {
                              "image": {
                                "thunk": {
                                  "image": {
                                    "ref": {
                                      "platform": {
                                        "os": "linux",
                                        "arch": "amd64"
                                      },
                                      "repository": "nixos/nix",
                                      "tag": "latest",
                                      "digest": "sha256:1f8fa57de6f2f9ea5ea8d115b339fa68d2f98f20b59438bdb9d3a082ad64d4bf"
                                    }
                                  },
                                  "args": [
                                    {
                                      "string": {
                                        "value": "cp"
                                      }
                                    },
                                    {
                                      "string": {
                                        "value": "-anT"
                                      }
                                    },
                                    {
                                      "dirPath": {
                                        "path": "/nix"
                                      }
                                    },
                                    {
                                      "dirPath": {
                                        "path": "/cache"
                                      }
                                    }
                                  ],
                                  "mounts": [
                                    {
                                      "source": {
                                        "cache": {
                                          "id": "nix-cache:nixos/nix:latest@sha256:1f8fa57de6f2f9ea5ea8d115b339fa68d2f98f20b59438bdb9d3a082ad64d4bf",
                                          "path": {
                                            "dir": {
                                              "path": "."
                                            }
                                          }
                                        }
                                      },
                                      "target": {
                                        "dir": {
                                          "path": "/cache"
                                        }
                                      }
                                    }
                                  ]
                                }
                              },
                              "args": [
                                {
                                  "string": {
                                    "value": "sh"
                                  }
                                },
                                {
                                  "string": {
                                    "value": "-c"
                                  }
                                },
                                {
                                  "string": {
                                    "value": "echo accept-flake-config = true >> /etc/nix/nix.conf"
                                  }
                                }
                              ],
                              "mounts": [
                                {
                                  "source": {
                                    "cache": {
                                      "id": "nix-cache:nixos/nix:latest@sha256:1f8fa57de6f2f9ea5ea8d115b339fa68d2f98f20b59438bdb9d3a082ad64d4bf",
                                      "path": {
                                        "dir": {
                                          "path": "."
                                        }
                                      }
                                    }
                                  },
                                  "target": {
                                    "dir": {
                                      "path": "/nix"
                                    }
                                  }
                                }
                              ]
                            }
                          },
                          "args": [
                            {
                              "string": {
                                "value": "sh"
                              }
                            },
                            {
                              "string": {
                                "value": "-c"
                              }
                            },
                            {
                              "string": {
                                "value": "echo experimental-features = nix-command flakes >> /etc/nix/nix.conf"
                              }
                            }
                          ],
                          "mounts": [
                            {
                              "source": {
                                "cache": {
                                  "id": "nix-cache:nixos/nix:latest@sha256:1f8fa57de6f2f9ea5ea8d115b339fa68d2f98f20b59438bdb9d3a082ad64d4bf",
                                  "path": {
                                    "dir": {
                                      "path": "."
                                    }
                                  }
                                }
                              },
                              "target": {
                                "dir": {
                                  "path": "/nix"
                                }
                              }
                            }
                          ]
                        }
                      },
                      "args": [
                        {
                          "string": {
                            "value": "nix"
                          }
                        },
                        {
                          "string": {
                            "value": "build"
                          }
                        },
                        {
                          "string": {
                            "value": "-f"
                          }
                        },
                        {
                          "filePath": {
                            "path": "image.nix"
                          }
                        }
                      ],
                      "mounts": [
                        {
                          "source": {
                            "logical": {
                              "file": {
                                "name": "image.nix",
                                "content": "bGV0CiAgZmxha2UgPSBidWlsdGlucy5nZXRGbGFrZSAiL2ZsYWtlIjsKICBpbnB1dHMgPSBmbGFrZS5pbnB1dHM7CiAgcGtncyA9IGltcG9ydCBpbnB1dHMubml4cGtncyB7fTsKICBmaHMgPSAocGtncy5jYWxsUGFja2FnZSA8bml4cGtncy9wa2dzL2J1aWxkLXN1cHBvcnQvYnVpbGQtZmhzLXVzZXJlbnYvZW52Lm5peD4ge30pIHsKICAgIG5hbWUgPSAiY29udGFpbmVyLWZocyI7CiAgICB0YXJnZXRQa2dzID0gcGtnczogd2l0aCBwa2dzOyBbd2dldF07CiAgICBtdWx0aVBrZ3MgPSBudWxsOyAgIyBEb24ndCBpbmNsdWRlIGdsaWJjJ3MgbXVsdGlsaWIKICB9OwogIHN0cmVhbSA9IHBrZ3MuZG9ja2VyVG9vbHMuc3RyZWFtTGF5ZXJlZEltYWdlIHsKICAgIG5hbWUgPSAibml4cGtncy93Z2V0IjsKICAgIGNvbnRlbnRzID0gcGtncy5zeW1saW5rSm9pbiB7CiAgICAgIG5hbWUgPSAiY29udGVudHMiOwogICAgICBwYXRocyA9IFsgZmhzIF07CiAgICB9OwogICAgY29uZmlnID0gewogICAgICBFbnYgPSBbCiAgICAgICAgIlBBVEg9L2JpbiIKICAgICAgICAiU1NMX0NFUlRfRklMRT0ke3BrZ3MuY2FjZXJ0fS9ldGMvc3NsL2NlcnRzL2NhLWJ1bmRsZS5jcnQiCiAgICAgIF07CiAgICB9OwogIH07CmluCnBrZ3MucnVuQ29tbWFuZCAid3JpdGUtZG9ja2VyLXRhciIge30gJycKICAke3N0cmVhbX0gPiAkb3V0Cicn"
                              }
                            }
                          },
                          "target": {
                            "file": {
                              "path": "image.nix"
                            }
                          }
                        },
                        {
                          "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/tabs"
                                                }
                                              },
                                              {
                                                "dirPath": {
                                                  "path": "."
                                                }
                                              }
                                            ]
                                          }
                                        },
                                        "args": [
                                          {
                                            "string": {
                                              "value": "git"
                                            }
                                          },
                                          {
                                            "string": {
                                              "value": "fetch"
                                            }
                                          },
                                          {
                                            "string": {
                                              "value": "origin"
                                            }
                                          },
                                          {
                                            "string": {
                                              "value": "6ca3f15d70b739a7929886e20b61dbc4cbdc4e22"
                                            }
                                          }
                                        ]
                                      }
                                    },
                                    "args": [
                                      {
                                        "string": {
                                          "value": "git"
                                        }
                                      },
                                      {
                                        "string": {
                                          "value": "checkout"
                                        }
                                      },
                                      {
                                        "string": {
                                          "value": "6ca3f15d70b739a7929886e20b61dbc4cbdc4e22"
                                        }
                                      }
                                    ]
                                  }
                                },
                                "args": [
                                  {
                                    "string": {
                                      "value": "git"
                                    }
                                  },
                                  {
                                    "string": {
                                      "value": "submodule"
                                    }
                                  },
                                  {
                                    "string": {
                                      "value": "update"
                                    }
                                  },
                                  {
                                    "string": {
                                      "value": "--init"
                                    }
                                  },
                                  {
                                    "string": {
                                      "value": "--recursive"
                                    }
                                  }
                                ]
                              },
                              "path": {
                                "dir": {
                                  "path": "."
                                }
                              }
                            }
                          },
                          "target": {
                            "dir": {
                              "path": "/flake"
                            }
                          }
                        },
                        {
                          "source": {
                            "cache": {
                              "id": "nix-cache:nixos/nix:latest@sha256:1f8fa57de6f2f9ea5ea8d115b339fa68d2f98f20b59438bdb9d3a082ad64d4bf",
                              "path": {
                                "dir": {
                                  "path": "."
                                }
                              }
                            }
                          },
                          "target": {
                            "dir": {
                              "path": "/nix"
                            }
                          }
                        }
                      ]
                    }
                  },
                  "args": [
                    {
                      "string": {
                        "value": "cp"
                      }
                    },
                    {
                      "string": {
                        "value": "-aL"
                      }
                    },
                    {
                      "filePath": {
                        "path": "result"
                      }
                    },
                    {
                      "filePath": {
                        "path": "image.tar"
                      }
                    }
                  ],
                  "mounts": [
                    {
                      "source": {
                        "cache": {
                          "id": "nix-cache:nixos/nix:latest@sha256:1f8fa57de6f2f9ea5ea8d115b339fa68d2f98f20b59438bdb9d3a082ad64d4bf",
                          "path": {
                            "dir": {
                              "path": "."
                            }
                          }
                        }
                      },
                      "target": {
                        "dir": {
                          "path": "/nix"
                        }
                      }
                    }
                  ]
                },
                "path": {
                  "file": {
                    "path": "image.tar"
                  }
                }
              }
            }
          }
        }
      }
    },
    "args": [
      {
        "string": {
          "value": "wget"
        }
      },
      {
        "string": {
          "value": "https://example.com"
        }
      },
      {
        "string": {
          "value": "-O"
        }
      },
      {
        "filePath": {
          "path": "index.html"
        }
      }
    ]
  },
  "path": {
    "file": {
      "path": "index.html"
    }
  }
}

server mode

I'm not sure if this is the right design for this yet, but it seems nifty and it works. Expect this to change at any moment. Suggestions welcome!

To serve Bass scripts in ./srv/ over HTTP on port 6455 ("bass"), run:

bass --serve 6455 ./srv/

This is particularly handy for cobbling together endpoints for receiving webhooks (e.g. a GitHub App for CI/CD).

HTTP requests sent to http://localhost:6455/foo will run the ./srv/foo Bass script.

The HTTP request sent on *stdin* as a structure like the following:

{:headers {:Accept "application/json"}
 :body "{\"foo\":1}"}

Values emitted to *stdout* will be sent as the response. If the script fails a 500 status code will be returned.

The UX here is very spartan at the moment. Notably there is no way to view progress over HTTP; it's only rendered server-side in the console.

I'd like the server-side to self-update somehow, but haven't figured that out yet.

webhooks based CI/CD

The Bass project uses Bass Loop to receive GitHub webhooks and run its own builds. Docs coming soon - see the announcement for now.

pipeline based CI/CD

Trigging builds on push is just one form of CI/CD. What if you have external dependencies you'd like to trigger builds from? What if you want to write sophisticated pipelines with fan-in and fan-out semantics?

Dunno yet! I think we're a few steps away from this, but we need to figure out the best steps.

Ideas for the future: