This library implements a prototype of an alternative to nixpkgs'
buildGoModule
capable of incremental compilation. By directly calling
the Go compiler and linker (instead of the Go build system—i.e. the go
command), individual packages can be built as derivations to better leverage
Nix's robust caching system. With the addition of the experimental "floating
content-addressed derivations"
feature, builds can further be cached to the level of package export data,
matching the efficiency of
Google's Blaze.
Builds are initially slower due to the significant overhead of building and
tearing down sandboxes while realizing derivations, but subsequent builds
are substantially faster than any other Nix-based approach I'm aware of—even
without ca-derivations
enabled. The downside to this approach is that Nix
must be aware of the full package build graph at evaluation time (at least until
dynamic derivations matures). For
now, I have resorted to manually specifying dependencies, but this could be
automated with a code generator similar to Bazel's
Gazelle or Meta's
Reindeer.
Warning
This is not production software. Much of this library was spun-off from an unrelated personal project and is being shared because I was told others would find the idea useful. If you are interested in building a more serious tool based on this, please reach out!
The easiest way to include gopkg2nix-incremental in your project is as a nixpkgs
overlay. The overlay can be imported traditionally from overlay.nix or with
flakes as gopkg2nix-incremental.overlays.default
. If not using the overlay,
gopkg2nix-incremental can be instantiated by calling it as a function with
an attribute set containing these attributes: system
, the system string;
lib
, an instance of nixpkgs/lib; and go
, a derivation for the Go compiler
toolchain.
Example: Importing gopkg2nix-incremental in a flake
{
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
gopkg2nix-incremental.url = "github:jbellerb/gopkg2nix-incremental";
};
outputs =
{ self, nixpkgs, ... }@inputs:
let
systems = [
"x86_64-linux"
"aarch64-darwin"
"x86_64-darwin"
];
in
builtins.foldl' nixpkgs.lib.recursiveUpdate { } (
builtins.map (
system:
let
pkgs = import nixpkgs {
inherit system;
overlays = [ inputs.gopkg2nix-incremental.overlays.default ];
# optionally enable `ca-derivations` for better caching
config.contentAddressedGoPackages = true;
};
# or if not using an overlay
goLib = import inputs.gopkg2nix-incremental {
inherit system;
inherit (pkgs) lib go;
useCaDerivations = true;
};
in
{
...
}
) systems
);
}
Once imported, usage is largely similar to Bazel's rules_go
. Building any
project is done with a combination of two functions: buildGoLibrary
for
compiling library packages that can be used as dependencies, and buildGoBinary
for compiling and linking executable packages.
Both functions take a srcs
argument which refers to a list of source files
(either Go, Assembly, or Headers. go:embed
is not directly supported). These
source files can refer to packages specified by the inputs
argument, a list of
dependencies. The Go standard library is automatically included as a dependency
unless noStd = true
. All dependencies in this list must be the result of
a call to buildGoLibrary
. Finally, buildGoLibrary
takes the argument
packagePath
for the path used in Go when importing that package. Equivalently,
buildGoLibrary
takes the argument name
for the name of the derivation and
the output binary.
Example: Building an executable with an external dependency
In this example we will build a simple command line utility that converts
between currencies using the foreign exchange data published by the
European Central Bank.
Create a flake.nix
for compiling the binary:
{
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
gopkg2nix-incremental.url = "github:jbellerb/gopkg2nix-incremental";
};
outputs =
{ self, nixpkgs, ... }@inputs:
let
systems = [
"x86_64-linux"
"aarch64-darwin"
"x86_64-darwin"
];
in
builtins.foldl' nixpkgs.lib.recursiveUpdate { } (
builtins.map (
system:
let
pkgs = import nixpkgs {
inherit system;
overlays = [ inputs.gopkg2nix-incremental.overlays.default ];
# optionally enable `ca-derivations` for better caching
config.contentAddressedGoPackages = true;
};
in
{
packages."${system}".default = pkgs.buildGoBinary {
name = "currency";
srcs = [ ./currency.go ];
};
}
) systems
);
}
And now the source of the program, currency.go
:
package main
import (
"encoding/xml"
"flag"
"fmt"
"log"
"net/http"
)
const source = "https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml"
type ReferenceRates struct {
Date string `xml:"time,attr"`
Currencies []struct {
Name string `xml:"currency,attr"`
Rate float64 `xml:"rate,attr"`
} `xml:"Cube"`
}
var rates = map[string]float64{"EUR": 1.0}
func currencyRate(name string) float64 {
value, ok := rates[name]
if !ok {
log.Fatalf("unsupported currency: %s", name)
}
return value
}
func main() {
log.SetFlags(0)
from := flag.String("f", "USD", "currency to convert from")
to := flag.String("t", "EUR", "currency to convert to")
n := flag.Float64("n", 1.0, "quantity of currency to convert")
flag.Parse()
res, err := http.Get(source)
if err != nil {
log.Fatal(err)
}
defer res.Body.Close()
decoder := xml.NewDecoder(res.Body)
var envelope struct {
Data ReferenceRates `xml:"Cube>Cube"`
}
if err := decoder.Decode(&envelope); err != nil {
log.Fatalf("failed to parse currency data: %v", err)
}
for _, currency := range envelope.Data.Currencies {
rates[currency.Name] = currency.Rate
}
fmt.Printf(
"%s %.2f = %s %.2f (on %s)\n",
*from, *n, *to, *n*currencyRate(*to)/currencyRate(*from), envelope.Data.Date,
)
}
Try it out! If this is your first time compiling, it may take a while to
build the standard library. Depending on how frequently you run garbage
collection, it may be helpful to create garbage collector roots for
pkgs.goPackages.std.export
and pkgs.goPackages.std.lib
.
$ nix run . -- -f INR -t ISK -n 150
INR 150.00 = ISK 223.98 (on 2025-05-26)
It would be nice to have the currencies formatted in their local formatting.
This can be done with the golang.org/x/text/currency
package. While that
package doesn't have any external dependencies, it does use quite a few internal
modules which all have to be manually specified so Nix knows the correct order
to build them in. These lists get quite long so I prefer to keep them in a
separate file. Create a new file called third-party.nix
:
Show contents of third-party.nix
{ buildGoLibrary, fetchgit }:
let
src = src: prefix: builtins.map (path: "${srcs."${src}"}/${prefix}/${path}");
srcs = {
"golang.org/x/text" = fetchgit {
url = "https://go.googlesource.com/text";
rev = "700cc20645cf719b928f5fce7e07528c4f7fa601";
hash = "sha256-jibU2+7j+NLUORjR5uGMjn88BAbCPZWQX05fQKsYJeU=";
};
};
goPackages = {
"golang.org/x/text/currency" = buildGoLibrary {
packagePath = "golang.org/x/text/currency";
srcs = src "golang.org/x/text" "currency" [
"common.go"
"currency.go"
"format.go"
"query.go"
"tables.go"
];
imports = [
goPackages."golang.org/x/text/internal/format"
goPackages."golang.org/x/text/internal/language/compact"
goPackages."golang.org/x/text/internal/number"
goPackages."golang.org/x/text/internal/tag"
goPackages."golang.org/x/text/language"
];
};
"golang.org/x/text/internal/format" = buildGoLibrary {
packagePath = "golang.org/x/text/internal/format";
srcs = src "golang.org/x/text" "internal/format" [
"format.go"
"parser.go"
];
imports = [ goPackages."golang.org/x/text/language" ];
};
"golang.org/x/text/internal/language" = buildGoLibrary {
packagePath = "golang.org/x/text/internal/language";
srcs = src "golang.org/x/text" "internal/language" [
"common.go"
"compact.go"
"compose.go"
"coverage.go"
"language.go"
"lookup.go"
"match.go"
"parse.go"
"tables.go"
"tags.go"
];
imports = [ goPackages."golang.org/x/text/internal/tag" ];
};
"golang.org/x/text/internal/language/compact" = buildGoLibrary {
packagePath = "golang.org/x/text/internal/language/compact";
srcs = src "golang.org/x/text" "internal/language/compact" [
"compact.go"
"language.go"
"parents.go"
"tables.go"
"tags.go"
];
imports = [ goPackages."golang.org/x/text/internal/language" ];
};
"golang.org/x/text/internal/number" = buildGoLibrary {
packagePath = "golang.org/x/text/internal/number";
srcs = src "golang.org/x/text" "internal/number" [
"common.go"
"decimal.go"
"format.go"
"number.go"
"pattern.go"
"roundingmode_string.go"
"tables.go"
];
imports = [
goPackages."golang.org/x/text/internal/language/compact"
goPackages."golang.org/x/text/internal/stringset"
goPackages."golang.org/x/text/language"
];
};
"golang.org/x/text/internal/stringset" = buildGoLibrary {
packagePath = "golang.org/x/text/internal/stringset";
srcs = src "golang.org/x/text" "internal/stringset" [ "set.go" ];
};
"golang.org/x/text/internal/tag" = buildGoLibrary {
packagePath = "golang.org/x/text/internal/tag";
srcs = src "golang.org/x/text" "internal/tag" [ "tag.go" ];
};
"golang.org/x/text/language" = buildGoLibrary {
packagePath = "golang.org/x/text/language";
srcs = src "golang.org/x/text" "language" [
"coverage.go"
"doc.go"
"language.go"
"match.go"
"parse.go"
"tables.go"
"tags.go"
];
imports = [
goPackages."golang.org/x/text/internal/language"
goPackages."golang.org/x/text/internal/language/compact"
];
};
};
in
goPackages
Now with our dependencies set up, we can import this into our flake and register the currency package as an import of our app.
pkgs = import nixpkgs {
inherit system;
overlays = [ inputs.gopkg2nix-incremental.overlays.default ];
};
+
+ thirdParty = pkgs.callPackage ./third-party.nix {};
in
{
packages."${system}" = {
default = pkgs.buildGoBinary {
name = "currency";
srcs = [ ./currency.go ];
+ imports = [ thirdParty."golang.org/x/text/currency" ];
};
};
}
Finally, the package can be used in our program's code.
"fmt"
"log"
"net/http"
+
+ "golang.org/x/text/currency"
)
const source = "https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml"
...
rates[currency.Name] = currency.Rate
}
+ fromRate, fromUnit := currencyRate(*from), currency.MustParseISO(*from)
+ toRate, toUnit := currencyRate(*to), currency.MustParseISO(*to)
+ fromAmount, toAmount := fromUnit.Amount(*n), toUnit.Amount(*n*toRate/fromRate)
fmt.Printf(
- "%s %.2f = %s %.2f (on %s)\n",
- *from, *n, *to, *n*currencyRate(*to)/currencyRate(*from), envelope.Data.Date,
+ "%v = %v (on %s)\n",
+ currency.Symbol(fromAmount), currency.Symbol(toAmount), envelope.Data.Date,
)
}
And our app now returns values with nice formatting!
$ nix run . -- -f INR -t ISK -n 150
₹ 150.00 = ISK 224 (on 2025-05-26)
Copyright (C) jae beller, 2025.
Released under the GNU Lesser General Public License, Version 3.0 or later. See LICENSE for more information.