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
2 changes: 2 additions & 0 deletions examples/nix/hello/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
flake.nix
flake.lock
70 changes: 70 additions & 0 deletions examples/nix/hello/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Custom Nix Package Example

This example shows how to include a locally-authored Nix expression in your
devbox shell. The `hello-pkg/default.nix` file defines a trivial shell script
using `pkgs.writeShellScriptBin`, and devbox consumes it via its existing
local-flake pipeline.

## One-time scaffolding

devbox's package syntax expects a flake, so the first time you set this up
(or whenever you change the pinned nixpkgs), generate a thin wrapper flake
next to the `default.nix`:

```sh
devbox generate flake-wrapper ./hello-pkg
```

This writes `hello-pkg/flake.nix` (see below). The wrapper is intentionally
not committed to this example repo so you can see the scaffolding step.

## devbox.json

```json
{
"packages": {
"./hello-pkg": ""
}
}
```

The `./hello-pkg` entry is a standard local-flake reference: devbox passes it
to Nix as `path:./hello-pkg` and adds `packages.${system}.default` from that
flake to the shell's `buildInputs`.

## Running the example

```sh
devbox generate flake-wrapper ./hello-pkg
devbox shell -- hello
# Hello from a custom Nix package!
```

## What the generated wrapper looks like

`devbox generate flake-wrapper ./hello-pkg` produces something like:

```nix
{
description = "devbox wrapper flake for hello-pkg";

inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";

outputs = { self, nixpkgs }: let
forAllSystems = f: nixpkgs.lib.genAttrs
nixpkgs.lib.systems.flakeExposed
(system: f nixpkgs.legacyPackages.${system});
in {
packages = forAllSystems (pkgs: {
default = pkgs.callPackage ./default.nix {};
});
};
}
```

`pkgs.callPackage ./default.nix {}` auto-injects the usual nixpkgs arguments
(`stdenv`, `lib`, `bash`, ...) into the expression. You can hand-edit the
wrapper to pass overrides (e.g. `pkgs.callPackage ./default.nix { withSsl = true; }`),
expose additional attributes, or pin a different nixpkgs — and re-run
`devbox generate flake-wrapper --force ./hello-pkg` whenever you want to
regenerate it from scratch.
5 changes: 5 additions & 0 deletions examples/nix/hello/devbox.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"packages": {
"./hello-pkg": ""
}
}
9 changes: 9 additions & 0 deletions examples/nix/hello/devbox.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"lockfile_version": "1",
"packages": {
"github:NixOS/nixpkgs/nixpkgs-unstable": {
"last_modified": "2026-04-05T15:42:39Z",
"resolved": "github:NixOS/nixpkgs/5e11f7acce6c3469bef9df154d78534fa7ae8b6c?lastModified=1775403759&narHash=sha256-cGyKiTspHEUx3QwAnV3RfyT%2BVOXhHLs%2BNEr17HU34Wo%3D"
}
}
}
5 changes: 5 additions & 0 deletions examples/nix/hello/hello-pkg/default.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{ stdenv, lib, bash, writeShellScriptBin }:

writeShellScriptBin "hello" ''
echo "Hello from a custom Nix package!"
''
109 changes: 109 additions & 0 deletions internal/boxcli/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package boxcli
import (
"cmp"
"fmt"
"path/filepath"
"regexp"

"github.com/pkg/errors"
Expand All @@ -16,6 +17,7 @@ import (
"go.jetify.com/devbox/internal/devbox"
"go.jetify.com/devbox/internal/devbox/devopt"
"go.jetify.com/devbox/internal/devbox/docgen"
"go.jetify.com/devbox/internal/devbox/flakegen"
)

type generateCmdFlags struct {
Expand Down Expand Up @@ -44,6 +46,14 @@ type GenerateAliasCmdFlags struct {
noPrefix bool
}

type genFlakeWrapperCmdFlags struct {
config configFlags
force bool
nixpkgs string
attr string
print bool
}

func generateCmd() *cobra.Command {
flags := &generateCmdFlags{}

Expand All @@ -59,6 +69,7 @@ func generateCmd() *cobra.Command {
command.AddCommand(dockerfileCmd())
command.AddCommand(debugCmd())
command.AddCommand(direnvCmd())
command.AddCommand(genFlakeWrapperCmd())
command.AddCommand(genReadmeCmd())
flags.config.register(command)

Expand Down Expand Up @@ -255,6 +266,46 @@ func genAliasCmd() *cobra.Command {
return command
}

func genFlakeWrapperCmd() *cobra.Command {
flags := &genFlakeWrapperCmdFlags{}
command := &cobra.Command{
Use: "flake-wrapper [path]",
Short: "Generate a flake.nix wrapping an existing .nix expression",
Long: "Generate a flake.nix next to an existing .nix expression so " +
"the directory can be consumed as a local flake in devbox.json " +
"(e.g. \"packages\": { \"./my-pkg\": \"\" }). The path may be a " +
"directory containing a default.nix, or a specific .nix file. " +
"The generated flake imports the sibling .nix file via " +
"pkgs.callPackage.",
Args: cobra.MaximumNArgs(1),
// This command is pure text templating and does not need Nix.
// Override the parent generate command's ensureNixInstalled check.
PersistentPreRunE: func(*cobra.Command, []string) error { return nil },
RunE: func(cmd *cobra.Command, args []string) error {
target := "."
if len(args) == 1 {
target = args[0]
}
return runGenFlakeWrapperCmd(cmd, target, flags)
},
}
flags.config.register(command)
command.Flags().BoolVarP(
&flags.force, "force", "f", false,
"overwrite flake.nix if it already exists")
command.Flags().StringVar(
&flags.nixpkgs, "nixpkgs", "",
"nixpkgs input URL to pin (defaults to the project's stdenv if run "+
"inside a devbox project, else "+flakegen.DefaultNixpkgsURL+")")
command.Flags().StringVar(
&flags.attr, "attr", "default",
"attribute name to expose under packages.${system}")
command.Flags().BoolVar(
&flags.print, "print", false,
"print the generated flake.nix to stdout instead of writing it")
return command
}

func runGenerateCmd(cmd *cobra.Command, flags *generateCmdFlags) error {
// Check the directory exists.
box, err := devbox.Open(&devopt.Opts{
Expand Down Expand Up @@ -310,3 +361,61 @@ func runGenerateDirenvCmd(cmd *cobra.Command, flags *generateCmdFlags) error {

return box.GenerateEnvrcFile(cmd.Context(), generateEnvrcOpts)
}

func runGenFlakeWrapperCmd(
cmd *cobra.Command,
target string,
flags *genFlakeWrapperCmdFlags,
) error {
nixPath, err := flakegen.ResolveNixFile(target)
if err != nil {
return err
}
flakePath, err := flakegen.Generate(flakegen.Opts{
NixFile: nixPath,
NixpkgsURL: resolveFlakeWrapperNixpkgs(cmd, flags),
Attr: flags.attr,
Force: flags.force,
Print: flags.print,
Out: cmd.OutOrStdout(),
})
if err != nil {
return err
}
if flags.print {
return nil
}
out := cmd.OutOrStdout()
fmt.Fprintf(out, "Wrote %s.\n", flakePath)
fmt.Fprintln(out, "Add it to devbox.json:")
fmt.Fprintln(out)
fmt.Fprintln(out, " \"packages\": {")
fmt.Fprintf(out, " \"./%s\": \"\"\n", filepath.Base(filepath.Dir(nixPath)))
fmt.Fprintln(out, " }")
return nil
}

// resolveFlakeWrapperNixpkgs determines which nixpkgs URL to pin in the
// generated flake. An explicit --nixpkgs flag wins; otherwise, if the command
// is run inside a devbox project we use that project's stdenv so the wrapper
// matches it; otherwise fall back to flakegen.DefaultNixpkgsURL.
func resolveFlakeWrapperNixpkgs(
cmd *cobra.Command,
flags *genFlakeWrapperCmdFlags,
) string {
if flags.nixpkgs != "" {
return flags.nixpkgs
}
box, err := devbox.Open(&devopt.Opts{
Dir: flags.config.path,
Stderr: cmd.ErrOrStderr(),
})
if err != nil {
return flakegen.DefaultNixpkgsURL
}
stdenv := box.Stdenv().String()
if stdenv == "" {
return flakegen.DefaultNixpkgsURL
}
return stdenv
}
Loading