Nov 2, 2023Julian K. Arni, Alex David, and Sönke Hahn

Announcing Garn

Announcing garn, a build tool and environment manager based on Nix, Deno and Typescript

tl;drshare

Today we are announcing garn, an open source (Apache-licensed) command-line tool for declaratively configuring a project's builds and environments. It is much like, and based on, Nix. But it uses Typescript instead of the Nix language, and with a new CLI experience designed from the start to be intuitive. We think these changes make Garn much more accessible and easy to get started with than Nix, without sacrificing Nix's power. You can read more about the history and context of Garn below, or head to garn.io to try it out. Currently go, npm, and haskell projects are supported.

Nixshare

It is the best of tools; it is the worst of tools. It is indispensable; it is unusable. It makes the impossible possible; it makes easy things hard. Nothing ever breaks; nothing ever works.

"It" is Nix, and opinion of Nix is, in short, all over the place. Rightly so, too: it does provide both amazing power and great frustration.

But does it need to be this way? Is Nix fundamentally a trade-off?

We don't think so. The advantages of Nix — reproducibility, isolated dev environments, composability, abstraction, caching — only require one thing: that you be a little bit more explicit about how to build software than implicitly depending on your environment. Everything else — an unintuitive CLI, an unfamiliar language, hard-to-find-documentation (which is moreover often outdated), poor error messages — is incidental complexity. We developed a new tool that does away with most of that incidental complexity; it is called Garn, and we are releasing it today. It comes from observing the complaints people have of Nix, and trying hard to find workable solutions for them.

Enter Garnshare

To use Garn, you write a garn.ts file (or have Garn itself do it), that looks like this:

import * as garn from "https://garn.io/ts/v0.0.13/mod.ts";
import * as pkgs from "https://garn.io/ts/v0.0.13/nixpkgs.ts";

export const frontend = garn.javascript.mkNpmProject({
  description: "My npm app",
  src: "frontend",
  nodeVersion: "18",
})
  .withDevTools([pkgs.cowsay])
  .addCheck("run-tests")`npm run test -- --watchAll=false`

export const backend = garn.go.mkGoProject({
  description: "A go server",
  src: "backend",
  goVersion: "1.20",
})
  .addExecutable("start")`go run main.go`;

And then get to:

  • garn run backend.start to run the Go project executable.
  • garn enter [frontend|backend] to enter an environment with all the packages needed to build or develop your frontend/backend project. That includes the right version of the Go compiler, or of NodeJS. (This isn't a container, so tools such as your text editor, will continue working normally in this environment!)
  • garn build backend to build your Go executable
  • garn check frontend to test your frontend. This is reproducible, so — modulo some exceptions like different architectures — it fails or succeeds for everyone. And it can be run as is on CI (such as, cough cough, Garnix, where CI runs without any further configuration).

Typescriptshare

One of the core ideas is allowing Typescript instead of Nix as the language of configuration. This is likely the thing that most sets Garn apart from not only Nix, but also tools like devshell, devbox, devenv and flox.

With Typescript, you get the full power of programming — the ability to easily factor out variables, to connect and abstract functionality, to reuse code, to write tests. But you don't need to learn a new language.? You don't need to find documentation (it's right there in your editor, if you have LSP set up). You don't need to try to make sense of a page of Nix traceback (type errors will show you where you went wrong much earlier and more precisely).

How does Garn work, that it allows you to write configuration in Typescript? The garn.ts file is expected to export one of a certain number of types — specifically, Projects, Environments, Executables, Checks, or Packages. And each one of those can be converted into the string representation of a Nix expression. And underneath, we have a little domain-specific language in Typescript for Nix-expressions-in-Typescript. It's a little like Pulumi, but generating Nix.

From these exports, Garn generates a Nix flakes file, and uses Nix to run the corresponding command. You generally commit this flake file. This means everything that works with Nix works with Garn.


We also generate Typescript from Nix, and ship it as a library. This is used to generate the nixpkgs.ts file imported in the code snippet above. That in turn means that (if you set up LSP) you get completion on packages, as well as a short description of them as you autocomplete. This alone makes a huge difference! ?

Garn more specifically uses Deno for running your garn.ts files (hence the imports from URLs). But you don't need to install Deno yourself — Garn handles that for you.

CLIshare

Nix has the burden of a long and complex history. This shows up in APIs that aren't always how one would do things from scratch and with the benefit of hindsight. An example is the somewhat cumbersome and inconsistent CLI interface.

The Garn CLI is a fresh start. Garn has a contextual help text and shell completion. If you don't have a garn.ts file yet, the only command it presents you with is init:

garn - the project manager

Usage: garn COMMAND

  Develop, build, and test your projects reliably and easily

Available options:
  -h,--help                Show this help text

Available commands:
  init                     Infer a garn.ts file from the project layout

Unavailable commands:
  build
  run
  enter
  generate
  check

If on the other hand a garn.ts file is present, then all other options become available, and it is init that is hidden (you can't initialize an already initialized project!).

Moreover, the CLI incorporates facts about your specific project, such as what it is you can build, and what the description of it is:

$ garn build --help
Usage: garn build COMMAND

  Build the default executable of a project

Available options:
  -h,--help                Show this help text

Available commands:
  backend                  A go server
  frontend                 My npm app

garn initshare

We mentioned the garn init command earlier. What that does is look at the current directory, and try to generate the correct, or approximately correct, garn.ts file for it. It's a little like Heroku's Buildpack.

If it can't figure out your project structure (e.g., your directory is empty), it generates a default garn.ts that shows the most important features of garn.ts files. This gives you a way to get started; autocompletion and type errors can guide you the rest of the way. (The hardest part of writing a garn.ts file from scratch is actually the import statement!)?

Roadmapshare

Garn is still beta-quality software; there's a lot of work ahead of us.

We intend to soon support more languages. The priority with which we add them is up for discussion, so if you have a request, make noise in our issue tracker.

We also hope to work on simplifying the logs and error messages you see. Right now all Nix logs make their way to your terminal intact; but there's a lot of noise in them, and our user tests indicate that that leads people to ignore messages that might actually be informative.

If you have feedback, feature requests or general thoughts, please reach out on our Discord channel or get in touch on the garn project on GitHub.

Continue Reading

Nov 16, 2024Julian K. Arni

We've added incremental compilation to garnix. In this blog, we discuss prior art on incremental compilation in Nix, and describe our own design.

Nov 11, 2024Julian K. Arni

How we designed our private caches.

Aug 27, 2024Julian K. Arni

A short note about custom typing for functions in Nix

View Archive
black globe

Say hi, ask questions, give feedback