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
{
:platform
{
:os "linux"
}
:repository "alpine"
:tag "latest"
:digest "sha256:686d8c9dfa6f3ccfc8230bc3178d23f84eeaf7e457f36f271ab1acc53015037c"
}
:cmd .echo
:args
(
  1. "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: 8 lines
=> resolve image config for docker.io/library/alpine@sha256:686d8c9dfa6f3ccfc8230bc3178d23
f84eeaf7e457f36f271ab1acc53015037c [0.01s]
=> docker-image://docker.io/library/alpine@sha256:686d8c9dfa6f3ccfc8230bc3178d23f84eeaf7e4
57f36f271ab1acc53015037c CACHED [0.00s]
-> resolve docker.io/library/alpine@sha256:686d8c9dfa6f3ccfc8230bc3178d23f84eeaf7e457f36f2
71ab1acc53015037c [0.01s]
=> echo "Hello, world!" [0.27s]
Hello, world!
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:686d8c9dfa6f3ccfc8230bc3178d23
f84eeaf7e457f36f271ab1acc53015037c [0.02s]
=> docker-image://docker.io/library/alpine@sha256:686d8c9dfa6f3ccfc8230bc3178d23f84eeaf7e4
57f36f271ab1acc53015037c CACHED [0.00s]
-> resolve docker.io/library/alpine@sha256:686d8c9dfa6f3ccfc8230bc3178d23f84eeaf7e457f36f2
71ab1acc53015037c [0.01s]
=> sh -c "exit 1" ERROR [0.25s]
!!! 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.

𝄢

To influence caching, use with-label to stamp thunks with arbitrary data. Two thunks that differ only in labels will be cached independently.

(run (with-label
       (from (linux/alpine)
         ($ echo "Hello, world!"))
       :foo "bar"))
stderr: 8 lines
=> resolve image config for docker.io/library/alpine@sha256:686d8c9dfa6f3ccfc8230bc3178d23
f84eeaf7e457f36f271ab1acc53015037c [0.22s]
=> docker-image://docker.io/library/alpine@sha256:686d8c9dfa6f3ccfc8230bc3178d23f84eeaf7e4
57f36f271ab1acc53015037c CACHED [0.00s]
-> resolve docker.io/library/alpine@sha256:686d8c9dfa6f3ccfc8230bc3178d23f84eeaf7e457f36f2
71ab1acc53015037c [0.01s]
=> echo "Hello, world!" [0.62s]
Hello, world!
null
𝄢

Tip: to avoid deep nesting like above, consider the alternative -> form.

(-> ($ echo "Hello, world!")
    (with-image (linux/alpine))
    (with-label :foo "bar")
    run)
stderr: 8 lines
=> resolve image config for docker.io/library/alpine@sha256:686d8c9dfa6f3ccfc8230bc3178d23
f84eeaf7e457f36f271ab1acc53015037c [0.17s]
=> docker-image://docker.io/library/alpine@sha256:686d8c9dfa6f3ccfc8230bc3178d23f84eeaf7e4
57f36f271ab1acc53015037c CACHED [0.00s]
-> resolve docker.io/library/alpine@sha256:686d8c9dfa6f3ccfc8230bc3178d23f84eeaf7e457f36f2
71ab1acc53015037c [0.32s]
=> echo "Hello, world!" [0.80s]
Hello, world!
null

reading output

𝄢

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

(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:686d8c9dfa6f3ccfc8230bc3178d23
f84eeaf7e457f36f271ab1acc53015037c [0.08s]
=> docker-image://docker.io/library/alpine@sha256:686d8c9dfa6f3ccfc8230bc3178d23f84eeaf7e4
57f36f271ab1acc53015037c CACHED [0.00s]
-> resolve docker.io/library/alpine@sha256:686d8c9dfa6f3ccfc8230bc3178d23f84eeaf7e457f36f2
71ab1acc53015037c [0.01s]
=> cat [0.46s]
=> exporting to client [0.00s]
-> copying files 44B [0.00s]
(
  1. "hello"
  2. "goodbye"
  3. end
)
𝄢

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: 9 lines
=> resolve image config for docker.io/library/alpine@sha256:686d8c9dfa6f3ccfc8230bc3178d23
f84eeaf7e457f36f271ab1acc53015037c [0.09s]
=> docker-image://docker.io/library/alpine@sha256:686d8c9dfa6f3ccfc8230bc3178d23f84eeaf7e4
57f36f271ab1acc53015037c CACHED [0.00s]
-> resolve docker.io/library/alpine@sha256:686d8c9dfa6f3ccfc8230bc3178d23f84eeaf7e457f36f2
71ab1acc53015037c [0.43s]
=> ls -r /usr/bin [1.22s]
=> exporting to client [0.00s]
-> copying files 924B [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: 9 lines
=> resolve image config for docker.io/library/alpine@sha256:686d8c9dfa6f3ccfc8230bc3178d23
f84eeaf7e457f36f271ab1acc53015037c [0.01s]
=> docker-image://docker.io/library/alpine@sha256:686d8c9dfa6f3ccfc8230bc3178d23f84eeaf7e4
57f36f271ab1acc53015037c CACHED [0.00s]
-> resolve docker.io/library/alpine@sha256:686d8c9dfa6f3ccfc8230bc3178d23f84eeaf7e457f36f2
71ab1acc53015037c [0.01s]
=> echo "Hello, world!" [0.20s]
=> exporting to client [0.00s]
-> copying files 40B [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
{
:cmd .echo
:args
(
  1. <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:686d8c9dfa6f3ccfc8230bc3178d23
f84eeaf7e457f36f271ab1acc53015037c [0.01s]
=> docker-image://docker.io/library/alpine@sha256:686d8c9dfa6f3ccfc8230bc3178d23f84eeaf7e4
57f36f271ab1acc53015037c CACHED [0.00s]
-> resolve docker.io/library/alpine@sha256:686d8c9dfa6f3ccfc8230bc3178d23f84eeaf7e457f36f2
71ab1acc53015037c [0.01s]
=> cat /secret [0.15s]
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 x >> /var/cache/file; cat /var/cache/file | wc -l")
        (with-label :tag tag)
        (with-mount my-cache /var/cache/))))

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

[(count "once")
 (count "twice")
 (count "thrice")]
stderr: 11 lines
=> sh -c "echo x >> /var/cache/file; cat /var/cache/file | wc -l" [0.12s]
=> sh -c "echo x >> /var/cache/file; cat /var/cache/file | wc -l" [0.12s]
=> resolve image config for docker.io/library/alpine@sha256:686d8c9dfa6f3ccfc8230bc3178d23
f84eeaf7e457f36f271ab1acc53015037c [0.01s]
=> docker-image://docker.io/library/alpine@sha256:686d8c9dfa6f3ccfc8230bc3178d23f84eeaf7e4
57f36f271ab1acc53015037c CACHED [0.00s]
-> resolve docker.io/library/alpine@sha256:686d8c9dfa6f3ccfc8230bc3178d23f84eeaf7e457f36f2
71ab1acc53015037c [0.01s]
=> sh -c "echo x >> /var/cache/file; cat /var/cache/file | wc -l" [0.12s]
=> exporting to client [0.00s]
-> copying files 28B [0.00s]
(
  1. 1
  2. 1
  3. 1
)
𝄢

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
{
:platform
{
:os "linux"
}
:repository "alpine"
:tag "latest"
:digest "sha256:686d8c9dfa6f3ccfc8230bc3178d23f84eeaf7e457f36f271ab1acc53015037c"
}
:cmd .sh
:args
(
  1. "-c"
  2. "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
{
:platform
{
:os "linux"
}
:repository "alpine"
:tag "latest"
:digest "sha256:686d8c9dfa6f3ccfc8230bc3178d23f84eeaf7e457f36f271ab1acc53015037c"
}
:cmd .sh
:args
(
  1. "-c"
  2. "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: 9 lines
=> resolve image config for docker.io/library/alpine@sha256:686d8c9dfa6f3ccfc8230bc3178d23
f84eeaf7e457f36f271ab1acc53015037c [0.01s]
=> docker-image://docker.io/library/alpine@sha256:686d8c9dfa6f3ccfc8230bc3178d23f84eeaf7e4
57f36f271ab1acc53015037c CACHED [0.00s]
-> resolve docker.io/library/alpine@sha256:686d8c9dfa6f3ccfc8230bc3178d23f84eeaf7e457f36f2
71ab1acc53015037c [0.01s]
=> sh -c "cat > ./file" [0.10s]
=> exporting to client [0.00s]
-> sending tarball [0.00s]
"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
=> resolve image config for docker.io/library/alpine@sha256:686d8c9dfa6f3ccfc8230bc3178d23
f84eeaf7e457f36f271ab1acc53015037c [0.01s]
=> docker-image://docker.io/library/alpine@sha256:686d8c9dfa6f3ccfc8230bc3178d23f84eeaf7e4
57f36f271ab1acc53015037c CACHED [0.00s]
-> resolve docker.io/library/alpine@sha256:686d8c9dfa6f3ccfc8230bc3178d23f84eeaf7e457f36f2
71ab1acc53015037c [0.01s]
=> sh -c "cat > ./file" [0.14s]
=> ls -al <thunk x_vZPyWs4P0=: (.sh)>/file [0.12s]
-rw-r--r-- 1 root root 18 Oct 26 1985 ./x_vZPyWs4P0=/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
{
:platform
{
:os "linux"
}
:repository "golang"
:tag "latest"
:digest "sha256:a452d6273ad03a47c2f29b898d6bb57630e77baf839651ef77d03e4e049c5bf3"
}
:cmd .go
:args
(
  1. "build"
  2. "-o"
  3. ./out/
  4. "./cmd/..."
)
:mounts
(
  1. {
    :source
    .git
    {
    :image
    {
    :platform
    {
    :os "linux"
    }
    :repository "alpine/git"
    :tag "latest"
    :digest "sha256:23dcd3edfd1d9c7cbb14f7823d07a4934716cfa4d4dbc402d37ee011c440a685"
    }
    :cmd .git
    :args
    (
    1. "clone"
    2. "https://github.com/vito/bass"
    3. ./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: 9 lines
=> resolve image config for docker.io/alpine/git@sha256:23dcd3edfd1d9c7cbb14f7823d07a49347
16cfa4d4dbc402d37ee011c440a685 [0.01s]
=> docker-image://docker.io/alpine/git@sha256:23dcd3edfd1d9c7cbb14f7823d07a4934716cfa4d4db
c402d37ee011c440a685 CACHED [0.00s]
-> resolve docker.io/alpine/git@sha256:23dcd3edfd1d9c7cbb14f7823d07a4934716cfa4d4dbc402d37
ee011c440a685 [0.01s]
=> git ls-remote https://github.com/vito/bass HEAD [0.33s]
=> exporting to client [0.00s]
-> copying files 72B [0.00s]
.git
{
:image
.git
{
:image
.git
{
:image
.git
{
:image
{
:platform
{
:os "linux"
}
:repository "alpine/git"
:tag "latest"
:digest "sha256:23dcd3edfd1d9c7cbb14f7823d07a4934716cfa4d4dbc402d37ee011c440a685"
}
:cmd .git
:args
(
  1. "clone"
  2. "https://github.com/vito/bass"
  3. ./
)
}
:cmd .git
:args
(
  1. "fetch"
  2. "origin"
  3. "6c037a9d134c8b60d61fcd0023494358b94e265f"
)
}
:cmd .git
:args
(
  1. "checkout"
  2. "6c037a9d134c8b60d61fcd0023494358b94e265f"
)
}
:cmd .git
:args
(
  1. "submodule"
  2. "update"
  3. "--init"
  4. "--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
{
:platform
{
:os "linux"
}
:repository "alpine/git"
:tag "latest"
:digest "sha256:23dcd3edfd1d9c7cbb14f7823d07a4934716cfa4d4dbc402d37ee011c440a685"
}
:cmd .git
:args
(
  1. "clone"
  2. "https://github.com/vito/bass"
  3. ./
)
}
:cmd .git
:args
(
  1. "fetch"
  2. "origin"
  3. "8bc8572e8bdb8856451ca8ea16f735b4a0aeb047"
)
}
:cmd .git
:args
(
  1. "checkout"
  2. "8bc8572e8bdb8856451ca8ea16f735b4a0aeb047"
)
}
:cmd .git
:args
(
  1. "submodule"
  2. "update"
  3. "--init"
  4. "--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-label ($ 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: 46 lines
=> resolve image config for docker.io/library/alpine@sha256:686d8c9dfa6f3ccfc8230bc3178d23
f84eeaf7e457f36f271ab1acc53015037c [0.01s]
=> docker-image://docker.io/library/alpine@sha256:686d8c9dfa6f3ccfc8230bc3178d23f84eeaf7e4
57f36f271ab1acc53015037c CACHED [0.00s]
-> resolve docker.io/library/alpine@sha256:686d8c9dfa6f3ccfc8230bc3178d23f84eeaf7e457f36f2
71ab1acc53015037c [0.02s]
=> sleep 1 [1.16s]
=> sh -c "echo \"$0\"; exit 1" "hello\nanother line" ERROR [0.17s]
hello
another line
=> sleep 3 CANCELED [1.38s]
!!! sh -c "echo \"$0\"; exit 1" "hello\nanother line"
8: [0.15s] hello
8: [0.15s] another line
error! call trace (oldest first):
┆ <fs>/multi-fail.bass:13:2..14:37
12 │ (defn main []
13 │ (ls (echo-sleep-exit "hello\nanother line" 1 1)
14 │ (echo-sleep-exit "oh no" 3 42)))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
┆ <fs>/multi-fail.bass:9:2..10:26
8 │ (defn ls paths
9 │ (run (from (linux/alpine)
10 │ ($ ls & $paths))))
^^^^^^^^^^^^^^^^^^^^^^^^^
build failed: exit code: 1
run summary:
=> resolve image config for docker.io/library/alpine@sha256:686d8c9dfa6f3ccfc8230bc3178d
23f84eeaf7e457f36f271ab1acc53015037c [0.01s]
=> docker-image://docker.io/library/alpine@sha256:686d8c9dfa6f3ccfc8230bc3178d23f84eeaf7
e457f36f271ab1acc53015037c
=> sleep 3 [canceled] [1.38s]
=> sleep 1 [1.16s]
=> sh -c "echo \"$0\"; exit 1" "hello\nanother line" [0.17s]
hello
another line
ERROR: exit code: 1
=> sh -c "echo \"$0\"; exit 42" "oh no"
=> ls <thunk 4BFQMgw3PwM=: (.sh)>/ <thunk Rl-wshJv2Nk=: (.sh)>/
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": {
      "ref": {
        "platform": {
          "os": "linux"
        },
        "repository": "golang",
        "tag": "latest",
        "digest": "sha256:a452d6273ad03a47c2f29b898d6bb57630e77baf839651ef77d03e4e049c5bf3"
      }
    },
    "cmd": {
      "command": {
        "name": "go"
      }
    },
    "args": [
      {
        "string": {
          "value": "build"
        }
      },
      {
        "string": {
          "value": "-o"
        }
      },
      {
        "dirPath": {
          "path": "../out"
        }
      },
      {
        "string": {
          "value": "./cmd/..."
        }
      }
    ],
    "dir": {
      "thunk": {
        "thunk": {
          "image": {
            "thunk": {
              "image": {
                "thunk": {
                  "image": {
                    "thunk": {
                      "image": {
                        "ref": {
                          "platform": {
                            "os": "linux"
                          },
                          "repository": "alpine/git",
                          "tag": "latest",
                          "digest": "sha256:23dcd3edfd1d9c7cbb14f7823d07a4934716cfa4d4dbc402d37ee011c440a685"
                        }
                      },
                      "cmd": {
                        "command": {
                          "name": "git"
                        }
                      },
                      "args": [
                        {
                          "string": {
                            "value": "clone"
                          }
                        },
                        {
                          "string": {
                            "value": "https://github.com/vito/bass"
                          }
                        },
                        {
                          "dirPath": {
                            "path": "."
                          }
                        }
                      ]
                    }
                  },
                  "cmd": {
                    "command": {
                      "name": "git"
                    }
                  },
                  "args": [
                    {
                      "string": {
                        "value": "fetch"
                      }
                    },
                    {
                      "string": {
                        "value": "origin"
                      }
                    },
                    {
                      "string": {
                        "value": "8bc8572e8bdb8856451ca8ea16f735b4a0aeb047"
                      }
                    }
                  ]
                }
              },
              "cmd": {
                "command": {
                  "name": "git"
                }
              },
              "args": [
                {
                  "string": {
                    "value": "checkout"
                  }
                },
                {
                  "string": {
                    "value": "8bc8572e8bdb8856451ca8ea16f735b4a0aeb047"
                  }
                }
              ]
            }
          },
          "cmd": {
            "command": {
              "name": "git"
            }
          },
          "args": [
            {
              "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": {
        "ref": {
          "platform": {
            "os": "linux"
          },
          "repository": "ubuntu",
          "tag": "latest",
          "digest": "sha256:b6b83d3c331794420340093eb706a6f152d9c1fa51b262d9bf34594887c2c7ac"
        }
      },
      "cmd": {
        "command": {
          "name": "apt-get"
        }
      },
      "args": [
        {
          "string": {
            "value": "update"
          }
        }
      ]
    }
  },
  "cmd": {
    "command": {
      "name": "apt-get"
    }
  },
  "args": [
    {
      "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: 9 lines
=> resolve image config for docker.io/alpine/git@sha256:23dcd3edfd1d9c7cbb14f7823d07a49347
16cfa4d4dbc402d37ee011c440a685 [0.01s]
=> docker-image://docker.io/alpine/git@sha256:23dcd3edfd1d9c7cbb14f7823d07a4934716cfa4d4db
c402d37ee011c440a685 CACHED [0.00s]
-> resolve docker.io/alpine/git@sha256:23dcd3edfd1d9c7cbb14f7823d07a4934716cfa4d4dbc402d37
ee011c440a685 [0.01s]
=> git ls-remote https://github.com/moby/buildkit HEAD [0.46s]
=> exporting to client [0.00s]
-> copying files 72B [0.00s]
"7b8733f64707a7248c5fff48a7ea08514c3ef02e"
𝄢

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")
"7b8733f64707a7248c5fff48a7ea08514c3ef02e"
𝄢

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: 10 lines
=> resolve image config for docker.io/alpine/git:latest [0.14s]
=> resolve image config for docker.io/alpine/git@sha256:760aaf0d59c93f87572ec40dee1efd10a7
ea13a78dff1f59a904e908449329ae [0.01s]
=> docker-image://docker.io/alpine/git@sha256:760aaf0d59c93f87572ec40dee1efd10a7ea13a78dff
1f59a904e908449329ae CACHED [0.00s]
-> resolve docker.io/alpine/git@sha256:760aaf0d59c93f87572ec40dee1efd10a7ea13a78dff1f59a90
4e908449329ae [0.01s]
=> git ls-remote https://github.com/vito/bass main [0.39s]
=> exporting to client [0.00s]
-> copying files 83B [0.00s]
.git
{
:image
.git
{
:image
.git
{
:image
.git
{
:image
{
:platform
{
:os "linux"
}
:repository "alpine/git"
:tag "latest"
:digest "sha256:760aaf0d59c93f87572ec40dee1efd10a7ea13a78dff1f59a904e908449329ae"
}
:cmd .git
:args
(
  1. "clone"
  2. "https://github.com/vito/bass"
  3. ./
)
}
:cmd .git
:args
(
  1. "fetch"
  2. "origin"
  3. "6c037a9d134c8b60d61fcd0023494358b94e265f"
)
}
:cmd .git
:args
(
  1. "checkout"
  2. "6c037a9d134c8b60d61fcd0023494358b94e265f"
)
}
:cmd .git
:args
(
  1. "submodule"
  2. "update"
  3. "--init"
  4. "--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*/bass.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: 9 lines
=> resolve image config for docker.io/alpine/git@sha256:760aaf0d59c93f87572ec40dee1efd10a7
ea13a78dff1f59a904e908449329ae [0.01s]
=> docker-image://docker.io/alpine/git@sha256:760aaf0d59c93f87572ec40dee1efd10a7ea13a78dff
1f59a904e908449329ae CACHED [0.00s]
-> resolve docker.io/alpine/git@sha256:760aaf0d59c93f87572ec40dee1efd10a7ea13a78dff1f59a90
4e908449329ae [0.01s]
=> git ls-remote https://github.com/vito/tabs main [0.37s]
=> exporting to client [0.00s]
-> copying files 83B [0.00s]
{
  "thunk": {
    "image": {
      "ref": {
        "platform": {
          "os": "linux"
        },
        "file": {
          "thunk": {
            "image": {
              "thunk": {
                "image": {
                  "thunk": {
                    "image": {
                      "thunk": {
                        "image": {
                          "thunk": {
                            "image": {
                              "ref": {
                                "platform": {
                                  "os": "linux"
                                },
                                "repository": "nixos/nix",
                                "tag": "latest",
                                "digest": "sha256:800bd47a0587a69351155195fb343a8cbf8bda3b08822324419b95ca940aced6"
                              }
                            },
                            "cmd": {
                              "command": {
                                "name": "cp"
                              }
                            },
                            "args": [
                              {
                                "string": {
                                  "value": "-anT"
                                }
                              },
                              {
                                "dirPath": {
                                  "path": "/nix"
                                }
                              },
                              {
                                "dirPath": {
                                  "path": "/cache"
                                }
                              }
                            ],
                            "mounts": [
                              {
                                "source": {
                                  "cache": {
                                    "id": "nix-cache:nixos/nix:latest@sha256:800bd47a0587a69351155195fb343a8cbf8bda3b08822324419b95ca940aced6",
                                    "path": {
                                      "dir": {
                                        "path": "."
                                      }
                                    }
                                  }
                                },
                                "target": {
                                  "dir": {
                                    "path": "/cache"
                                  }
                                }
                              }
                            ]
                          }
                        },
                        "cmd": {
                          "command": {
                            "name": "sh"
                          }
                        },
                        "args": [
                          {
                            "string": {
                              "value": "-c"
                            }
                          },
                          {
                            "string": {
                              "value": "echo accept-flake-config = true >> /etc/nix/nix.conf"
                            }
                          }
                        ],
                        "mounts": [
                          {
                            "source": {
                              "cache": {
                                "id": "nix-cache:nixos/nix:latest@sha256:800bd47a0587a69351155195fb343a8cbf8bda3b08822324419b95ca940aced6",
                                "path": {
                                  "dir": {
                                    "path": "."
                                  }
                                }
                              }
                            },
                            "target": {
                              "dir": {
                                "path": "/nix"
                              }
                            }
                          }
                        ]
                      }
                    },
                    "cmd": {
                      "command": {
                        "name": "sh"
                      }
                    },
                    "args": [
                      {
                        "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:800bd47a0587a69351155195fb343a8cbf8bda3b08822324419b95ca940aced6",
                            "path": {
                              "dir": {
                                "path": "."
                              }
                            }
                          }
                        },
                        "target": {
                          "dir": {
                            "path": "/nix"
                          }
                        }
                      }
                    ]
                  }
                },
                "cmd": {
                  "command": {
                    "name": "nix"
                  }
                },
                "args": [
                  {
                    "string": {
                      "value": "build"
                    }
                  },
                  {
                    "string": {
                      "value": ".#wget"
                    }
                  }
                ],
                "mounts": [
                  {
                    "source": {
                      "thunk": {
                        "thunk": {
                          "image": {
                            "thunk": {
                              "image": {
                                "thunk": {
                                  "image": {
                                    "thunk": {
                                      "image": {
                                        "ref": {
                                          "platform": {
                                            "os": "linux"
                                          },
                                          "repository": "alpine/git",
                                          "tag": "latest",
                                          "digest": "sha256:760aaf0d59c93f87572ec40dee1efd10a7ea13a78dff1f59a904e908449329ae"
                                        }
                                      },
                                      "cmd": {
                                        "command": {
                                          "name": "git"
                                        }
                                      },
                                      "args": [
                                        {
                                          "string": {
                                            "value": "clone"
                                          }
                                        },
                                        {
                                          "string": {
                                            "value": "https://github.com/vito/tabs"
                                          }
                                        },
                                        {
                                          "dirPath": {
                                            "path": "."
                                          }
                                        }
                                      ]
                                    }
                                  },
                                  "cmd": {
                                    "command": {
                                      "name": "git"
                                    }
                                  },
                                  "args": [
                                    {
                                      "string": {
                                        "value": "fetch"
                                      }
                                    },
                                    {
                                      "string": {
                                        "value": "origin"
                                      }
                                    },
                                    {
                                      "string": {
                                        "value": "b3109bfbd35fd036bb53872e5d96efbe4e70d63c"
                                      }
                                    }
                                  ]
                                }
                              },
                              "cmd": {
                                "command": {
                                  "name": "git"
                                }
                              },
                              "args": [
                                {
                                  "string": {
                                    "value": "checkout"
                                  }
                                },
                                {
                                  "string": {
                                    "value": "b3109bfbd35fd036bb53872e5d96efbe4e70d63c"
                                  }
                                }
                              ]
                            }
                          },
                          "cmd": {
                            "command": {
                              "name": "git"
                            }
                          },
                          "args": [
                            {
                              "string": {
                                "value": "submodule"
                              }
                            },
                            {
                              "string": {
                                "value": "update"
                              }
                            },
                            {
                              "string": {
                                "value": "--init"
                              }
                            },
                            {
                              "string": {
                                "value": "--recursive"
                              }
                            }
                          ]
                        },
                        "path": {
                          "dir": {
                            "path": "."
                          }
                        }
                      }
                    },
                    "target": {
                      "dir": {
                        "path": "."
                      }
                    }
                  },
                  {
                    "source": {
                      "cache": {
                        "id": "nix-cache:nixos/nix:latest@sha256:800bd47a0587a69351155195fb343a8cbf8bda3b08822324419b95ca940aced6",
                        "path": {
                          "dir": {
                            "path": "."
                          }
                        }
                      }
                    },
                    "target": {
                      "dir": {
                        "path": "/nix"
                      }
                    }
                  }
                ]
              }
            },
            "cmd": {
              "command": {
                "name": "cp"
              }
            },
            "args": [
              {
                "string": {
                  "value": "-aL"
                }
              },
              {
                "filePath": {
                  "path": "result"
                }
              },
              {
                "filePath": {
                  "path": "image.tar"
                }
              }
            ],
            "mounts": [
              {
                "source": {
                  "cache": {
                    "id": "nix-cache:nixos/nix:latest@sha256:800bd47a0587a69351155195fb343a8cbf8bda3b08822324419b95ca940aced6",
                    "path": {
                      "dir": {
                        "path": "."
                      }
                    }
                  }
                },
                "target": {
                  "dir": {
                    "path": "/nix"
                  }
                }
              }
            ]
          },
          "path": {
            "file": {
              "path": "image.tar"
            }
          }
        },
        "tag": "latest"
      }
    },
    "cmd": {
      "command": {
        "name": "wget"
      }
    },
    "args": [
      {
        "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: