Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 18 additions & 4 deletions internal/devpkg/narinfo_cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,29 @@ import (
"github.com/pkg/errors"
"go.jetify.com/devbox/internal/debug"
"go.jetify.com/devbox/internal/devbox/providers/nixcache"
"go.jetify.com/devbox/internal/envir"
"go.jetify.com/devbox/internal/goutil"
"go.jetify.com/devbox/internal/lock"
"go.jetify.com/devbox/internal/nix"
"golang.org/x/sync/errgroup"
)

// binaryCache is the store from which to fetch this package's binaries.
// It is used as FromStore in builtins.fetchClosure.
const binaryCache = "https://cache.nixos.org"
// defaultBinaryCache is the public Nix binary cache that Devbox queries by
// default for prebuilt package outputs.
const defaultBinaryCache = "https://cache.nixos.org"

// BinaryCache is the store from which to fetch a package's binaries. It is
// used as the FromStore in builtins.fetchClosure and as the first cache
// queried when checking whether a package's outputs are already built.
//
// It defaults to the public cache (https://cache.nixos.org) but can be
// overridden with the DEVBOX_NIX_BINARY_CACHE environment variable. This is
// useful in network-restricted environments where the public cache is
// unreachable and an internal mirror/proxy serving the same store paths must
// be used instead.
func BinaryCache() string {
return envir.GetValueOrDefault(envir.DevboxNixBinaryCache, defaultBinaryCache)
}

// useDefaultOutputs is a special value for the outputName parameter of
// fetchNarInfoStatusOnce, which indicates that the default outputs should be
Expand Down Expand Up @@ -320,7 +334,7 @@ func fetchNarInfoStatusFromS3(
var nixCacheIsConfigured = goutil.OnceValueWithContext(nixcache.IsConfigured)

func readCaches(ctx context.Context) ([]string, error) {
cacheURIs := []string{binaryCache}
cacheURIs := []string{BinaryCache()}
if !nixCacheIsConfigured.Do(ctx) {
return cacheURIs, nil
}
Expand Down
30 changes: 30 additions & 0 deletions internal/devpkg/narinfo_cache_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright 2024 Jetify Inc. and contributors. All rights reserved.
// Use of this source code is governed by the license in the LICENSE file.

package devpkg

import (
"testing"

"go.jetify.com/devbox/internal/envir"
)

func TestBinaryCache(t *testing.T) {
t.Run("defaults to the public cache", func(t *testing.T) {
// t.Setenv guarantees the env var is restored after the test; set it
// empty to ensure we exercise the default path regardless of the
// caller's environment.
t.Setenv(envir.DevboxNixBinaryCache, "")
if got, want := BinaryCache(), defaultBinaryCache; got != want {
t.Errorf("BinaryCache() = %q, want default %q", got, want)
}
})

t.Run("is overridable via DEVBOX_NIX_BINARY_CACHE", func(t *testing.T) {
const mirror = "https://nix-cache.example.com/mirror"
t.Setenv(envir.DevboxNixBinaryCache, mirror)
if got := BinaryCache(); got != mirror {
t.Errorf("BinaryCache() = %q, want override %q", got, mirror)
}
})
}
7 changes: 7 additions & 0 deletions internal/envir/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ const (
// the flag is awkward, such as a Dockerfile.
DevboxConfig = "DEVBOX_CONFIG"
DevboxGateway = "DEVBOX_GATEWAY"
// DevboxNixBinaryCache overrides the default Nix binary cache that Devbox
// queries for prebuilt package outputs (https://cache.nixos.org). This is
// useful in network-restricted environments where the public cache is
// unreachable and an internal mirror/proxy must be used instead. The value
// should be a substituter URL serving the same store paths (e.g. an
// Artifactory/Nexus generic remote that mirrors cache.nixos.org).
DevboxNixBinaryCache = "DEVBOX_NIX_BINARY_CACHE"
// DevboxLatestVersion is the latest version available of the devbox CLI binary.
// NOTE: it should NOT start with v (like 0.4.8)
DevboxLatestVersion = "DEVBOX_LATEST_VERSION"
Expand Down
39 changes: 36 additions & 3 deletions internal/shellgen/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/pkg/errors"
"go.jetify.com/devbox/internal/cuecfg"
"go.jetify.com/devbox/internal/debug"
"go.jetify.com/devbox/internal/devpkg"
"go.jetify.com/devbox/internal/redact"
)

Expand Down Expand Up @@ -161,9 +162,41 @@ func toJSON(a any) string {
}

var templateFuncs = template.FuncMap{
"json": toJSON,
"contains": strings.Contains,
"debug": debug.IsEnabled,
"json": toJSON,
"contains": strings.Contains,
"debug": debug.IsEnabled,
"fetchClosureStore": fetchClosureStore,
"nixString": nixString,
}

// fetchClosureStore returns the store URL to use as the fromStore of a
// builtins.fetchClosure in the generated flake.
//
// fetchClosure only supports http(s) stores, so when a package's outputs were
// found in an http(s) cache we use that cache directly (it's where the path
// actually lives, which may be the public cache or a configured mirror via
// DEVBOX_NIX_BINARY_CACHE). For s3 caches fetchClosure can't fetch directly, so
// we fall back to the configured binary cache as a placeholder store — the path
// is already built locally, so fetchClosure won't actually fetch from it.
func fetchClosureStore(cacheURI string) string {
if strings.HasPrefix(cacheURI, "http://") || strings.HasPrefix(cacheURI, "https://") {
return cacheURI
}
return devpkg.BinaryCache()
}

// nixString renders s as a double-quoted Nix string literal, escaping the
// characters that are special inside one: backslash, double quote, and the
// "${" antiquotation (interpolation) start. This keeps values that originate
// from configuration (e.g. a cache URL from DEVBOX_NIX_BINARY_CACHE) from
// breaking flake evaluation or being interpreted as Nix interpolation.
func nixString(s string) string {
r := strings.NewReplacer(
`\`, `\\`,
`"`, `\"`,
`${`, `\${`,
)
return `"` + r.Replace(s) + `"`
}

func makeFlakeFile(d devboxer, plan *flakePlan) error {
Expand Down
103 changes: 103 additions & 0 deletions internal/shellgen/generate_internal_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// Copyright 2024 Jetify Inc. and contributors. All rights reserved.
// Use of this source code is governed by the license in the LICENSE file.

package shellgen

import (
"testing"

"go.jetify.com/devbox/internal/envir"
)

func TestFetchClosureStore(t *testing.T) {
const customCache = "https://nix-cache.example.com/mirror"

tests := []struct {
name string
cacheURI string
envValue string
want string
}{
{
name: "https cache uri is used directly",
cacheURI: "https://cache.nixos.org",
want: "https://cache.nixos.org",
},
{
name: "http cache uri is used directly",
cacheURI: "http://nix-cache.example.com",
want: "http://nix-cache.example.com",
},
{
name: "https mirror cache uri is used directly",
cacheURI: customCache,
want: customCache,
},
{
name: "s3 cache falls back to default binary cache",
cacheURI: "s3://my-bucket",
want: "https://cache.nixos.org",
},
{
name: "s3 cache falls back to configured binary cache",
cacheURI: "s3://my-bucket",
envValue: customCache,
want: customCache,
},
{
name: "empty cache falls back to default binary cache",
cacheURI: "",
want: "https://cache.nixos.org",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Always set the env var (empty when unset) so the test is
// isolated from the caller's environment: t.Setenv with an empty
// value still exercises the default-cache path via
// GetValueOrDefault, and restores any prior value afterward.
t.Setenv(envir.DevboxNixBinaryCache, tt.envValue)
if got := fetchClosureStore(tt.cacheURI); got != tt.want {
t.Errorf("fetchClosureStore(%q) = %q, want %q", tt.cacheURI, got, tt.want)
}
})
}
}

func TestNixString(t *testing.T) {
tests := []struct {
name string
in string
want string
}{
{
name: "plain url is just quoted",
in: "https://cache.nixos.org",
want: `"https://cache.nixos.org"`,
},
{
name: "double quote is escaped",
in: `a"b`,
want: `"a\"b"`,
},
{
name: "backslash is escaped",
in: `a\b`,
want: `"a\\b"`,
},
{
name: "antiquotation start is escaped",
in: "a${b}",
want: `"a\${b}"`,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := nixString(tt.in); got != tt.want {
t.Errorf("nixString(%q) = %q, want %q", tt.in, got, tt.want)
}
})
}
}
15 changes: 9 additions & 6 deletions internal/shellgen/tmpl/flake.nix.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,16 @@
{{ if $output.CacheURI -}}
(builtins.trace "downloading {{ $pkg.Versioned }}" (builtins.fetchClosure {
{{/*
HACK HACK HACK! fetchClosure only supports http(s) caches and not
s3 caches. Until we implement that, we put a fake store here.
Since we pre-build everything, fetchClosure will not actually
fetch anything and just use the local version. This may break
if user somehow removes the local store path.
fetchClosure only supports http(s) caches and not s3 caches.
fetchClosureStore returns the http(s) cache the output was found
in (the public cache or a configured mirror via
DEVBOX_NIX_BINARY_CACHE), falling back to the default binary
cache for s3 caches. Since we pre-build everything, fetchClosure
will not actually fetch anything for an s3 cache and just use the
local version. This may break if the user somehow removes the
local store path.
*/}}
fromStore = "https://cache.nixos.org";
fromStore = {{ fetchClosureStore $output.CacheURI | nixString }};
fromPath = "{{ $pkg.InputAddressedPathForOutput $output.Name }}";
inputAddressed = true;
}))
Expand Down