Skip to content

Commit

Permalink
[devbox] Allow for custom install/build/start commands (#43)
Browse files Browse the repository at this point in the history
## Summary
Allow for custom install/build/start commands by customizing them in `devbox.json`. This makes it possible for users to build docker images in the cases when they need to do some customization, or when we have not yet written a language planner for that language.

## How was it tested?
Built, and ran `devbox plan` and `devbox generate` against a few examples.
  • Loading branch information
loreto committed Aug 31, 2022
1 parent fa25b9d commit a4c2b40
Show file tree
Hide file tree
Showing 19 changed files with 117 additions and 67 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ jobs:
with:
distribution: goreleaser
version: latest
args: release --rm-dist --skip-publish --snapshot
args: release --rm-dist --skip-publish --skip-announce --snapshot
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TELEMETRY_KEY: ${{ secrets.TELEMETRY_KEY }}
Expand Down
7 changes: 4 additions & 3 deletions boxcli/add.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ import (

func AddCmd() *cobra.Command {
command := &cobra.Command{
Use: "add <pkg>...",
Args: cobra.MinimumNArgs(1),
RunE: runAddCmd,
Use: "add <pkg>...",
Short: "Add a new package to your devbox",
Args: cobra.MinimumNArgs(1),
RunE: runAddCmd,
}
return command
}
Expand Down
8 changes: 4 additions & 4 deletions boxcli/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ func BuildCmd() *cobra.Command {
flags := &docker.BuildFlags{}

command := &cobra.Command{
Use: "build [<dir>]",
Args: cobra.MaximumNArgs(1),
Hidden: true, // Hide until ready for release.
RunE: buildCmdFunc(flags),
Use: "build [<dir>]",
Short: "Build an OCI image that can run as a container",
Args: cobra.MaximumNArgs(1),
RunE: buildCmdFunc(flags),
}

command.Flags().BoolVar(
Expand Down
7 changes: 4 additions & 3 deletions boxcli/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ import (

func InitCmd() *cobra.Command {
command := &cobra.Command{
Use: "init [<dir>]",
Args: cobra.MaximumNArgs(1),
RunE: runInitCmd,
Use: "init [<dir>]",
Short: "Initialize a directory as a devbox project",
Args: cobra.MaximumNArgs(1),
RunE: runInitCmd,
}
return command
}
Expand Down
7 changes: 4 additions & 3 deletions boxcli/plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ import (

func PlanCmd() *cobra.Command {
command := &cobra.Command{
Use: "plan [<dir>]",
Args: cobra.MaximumNArgs(1),
RunE: runPlanCmd,
Use: "plan [<dir>]",
Short: "Preview the plan used to build your environment",
Args: cobra.MaximumNArgs(1),
RunE: runPlanCmd,
}
return command
}
Expand Down
7 changes: 4 additions & 3 deletions boxcli/rm.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ import (

func RemoveCmd() *cobra.Command {
command := &cobra.Command{
Use: "rm <pkg>...",
Args: cobra.MinimumNArgs(1),
RunE: runRemoveCmd,
Use: "rm <pkg>...",
Short: "Remove a package from your devbox",
Args: cobra.MinimumNArgs(1),
RunE: runRemoveCmd,
}
return command
}
Expand Down
3 changes: 2 additions & 1 deletion boxcli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ import (

func RootCmd() *cobra.Command {
command := &cobra.Command{
Use: "devbox",
Use: "devbox",
Short: "Instant, easy, predictable shells and containers",
PersistentPreRun: func(cmd *cobra.Command, args []string) {
// Don't display 'usage' on application errors.
cmd.SilenceUsage = true
Expand Down
7 changes: 4 additions & 3 deletions boxcli/shell.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ import (

func ShellCmd() *cobra.Command {
command := &cobra.Command{
Use: "shell [<dir>]",
Args: cobra.MaximumNArgs(1),
RunE: runShellCmd,
Use: "shell [<dir>]",
Short: "Start a new shell with access to your packages",
Args: cobra.MaximumNArgs(1),
RunE: runShellCmd,
}
return command
}
Expand Down
7 changes: 4 additions & 3 deletions boxcli/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@ import (
func VersionCmd() *cobra.Command {
flags := &versionFlags{}
command := &cobra.Command{
Use: "version",
Args: cobra.NoArgs,
RunE: versionCmdFunc(flags),
Use: "version",
Short: "Print version information",
Args: cobra.NoArgs,
RunE: versionCmdFunc(flags),
}

command.Flags().BoolVarP(&flags.verbose, "verbose", "v", false, // value
Expand Down
5 changes: 2 additions & 3 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,12 @@ package devbox
import (
"github.com/pkg/errors"
"go.jetpack.io/devbox/cuecfg"
"go.jetpack.io/devbox/planner"
)

// Config defines a devbox environment as JSON.
type Config struct {
// Packages is the slice of Nix packages that devbox makes available in
// its environment.
Packages []string `cue:"[...string]" json:"packages,omitempty"`
planner.Plan
}

// ReadConfig reads a devbox config file.
Expand Down
8 changes: 3 additions & 5 deletions devbox.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,9 @@ func (d *Devbox) Build(opts ...docker.BuildOptions) error {

// Plan creates a plan of the actions that devbox will take to generate its
// environment.
func (d *Devbox) Plan() *planner.BuildPlan {
basePlan := &planner.BuildPlan{
Packages: d.cfg.Packages,
}
return planner.MergePlans(basePlan, planner.Plan(d.srcDir))
func (d *Devbox) Plan() *planner.Plan {
basePlan := &d.cfg.Plan
return planner.MergePlans(basePlan, planner.GetPlan(d.srcDir))
}

// Generate creates the directory of Nix files and the Dockerfile that define
Expand Down
4 changes: 2 additions & 2 deletions generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import (
//go:embed tmpl/* tmpl/.*
var tmplFS embed.FS

func generate(rootPath string, plan *planner.BuildPlan) error {
func generate(rootPath string, plan *planner.Plan) error {
// TODO: we should also generate a .dockerignore file
files := []string{".gitignore", "Dockerfile", "shell.nix", "default.nix"}

Expand All @@ -34,7 +34,7 @@ func generate(rootPath string, plan *planner.BuildPlan) error {
return nil
}

func writeFromTemplate(path string, plan *planner.BuildPlan, tmplName string) error {
func writeFromTemplate(path string, plan *planner.Plan, tmplName string) error {
embeddedPath := fmt.Sprintf("tmpl/%s.tmpl", tmplName)

// Should we clear the directory so we start "fresh"?
Expand Down
4 changes: 2 additions & 2 deletions planner/empty_planner.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,6 @@ func (g *EmptyPlanner) IsRelevant(srcDir string) bool {
return false
}

func (g *EmptyPlanner) Plan(srcDir string) *BuildPlan {
return &BuildPlan{}
func (g *EmptyPlanner) GetPlan(srcDir string) *Plan {
return &Plan{}
}
16 changes: 11 additions & 5 deletions planner/go_planner.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,20 @@ func (g *GoPlanner) IsRelevant(srcDir string) bool {
return fileExists(goModPath)
}

func (g *GoPlanner) Plan(srcDir string) *BuildPlan {
return &BuildPlan{
func (g *GoPlanner) GetPlan(srcDir string) *Plan {
return &Plan{
Packages: []string{
"go",
},
InstallCommand: "go get",
BuildCommand: "CGO_ENABLED=0 go build -o out",
StartCommand: "./out", // TODO: Move gin specific stuff elsewhere.
InstallStage: &Stage{
Command: "go get",
},
BuildStage: &Stage{
Command: "CGO_ENABLED=0 go build -o app",
},
StartStage: &Stage{
Command: "./app",
},
}
}

Expand Down
36 changes: 26 additions & 10 deletions planner/plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,41 @@ import (
"github.com/imdario/mergo"
)

// TODO: decide if BuildPlan should continue to be a separate structure
// or whether it should be the same structure as devbox.Config.
type BuildPlan struct {
Packages []string `cue:"[...string]" json:"packages"`
InstallCommand string `cue:"string" json:"install_command,omitempty"`
BuildCommand string `cue:"string" json:"build_command,omitempty"`
StartCommand string `cue:"string" json:"start_command,omitempty"`
// Note: The Plan struct is exposed in `devbox.json` – be thoughful of how
// we evolve the schema, and make sure we keep backwards compatibility.

type Plan struct {
// Packages is the slice of Nix packages that devbox makes available in
// its environment.
Packages []string `cue:"[...string]" json:"packages"`
// InstallStage defines the actions that should be taken when
// installing language-specific libraries.
// Ex: pip install, yarn install, go get
InstallStage *Stage `json:"install_stage,omitempty"`
// BuildStage defines the actions that should be taken when
// compiling the application binary.
// Ex: go build -o app
BuildStage *Stage `json:"build_stage,omitempty"`
// StartStage defines the actions that should be taken when
// starting (running) the application.
// Ex: python main.py
StartStage *Stage `json:"start_stage,omitempty"`
}

type Stage struct {
Command string `cue:"string" json:"command"`
}

func (p *BuildPlan) String() string {
func (p *Plan) String() string {
b, err := json.MarshalIndent(p, "", " ")
if err != nil {
panic(err)
}
return string(b)
}

func MergePlans(plans ...*BuildPlan) *BuildPlan {
plan := &BuildPlan{
func MergePlans(plans ...*Plan) *Plan {
plan := &Plan{
Packages: []string{},
}
for _, p := range plans {
Expand Down
26 changes: 23 additions & 3 deletions planner/plan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,35 @@ import (

func TestMergePlans(t *testing.T) {
// Packages get appended
plan1 := &BuildPlan{
plan1 := &Plan{
Packages: []string{"foo", "bar"},
}
plan2 := &BuildPlan{
plan2 := &Plan{
Packages: []string{"baz"},
}
expected := &BuildPlan{
expected := &Plan{
Packages: []string{"foo", "bar", "baz"},
}
actual := MergePlans(plan1, plan2)
assert.Equal(t, expected, actual)

// Base plan (the first one) takes precedence:
plan1 = &Plan{
BuildStage: &Stage{
Command: "plan1",
},
}
plan2 = &Plan{
BuildStage: &Stage{
Command: "plan2",
},
}
expected = &Plan{
Packages: []string{},
BuildStage: &Stage{
Command: "plan1",
},
}
actual = MergePlans(plan1, plan2)
assert.Equal(t, expected, actual)
}
8 changes: 4 additions & 4 deletions planner/planner.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,21 @@ package planner
type Planner interface {
Name() string
IsRelevant(srcDir string) bool
Plan(srcDir string) *BuildPlan
GetPlan(srcDir string) *Plan
}

var PLANNERS = []Planner{
&GoPlanner{},
&PythonPlanner{},
}

func Plan(srcDir string) *BuildPlan {
result := &BuildPlan{
func GetPlan(srcDir string) *Plan {
result := &Plan{
Packages: []string{},
}
for _, planner := range PLANNERS {
if planner.IsRelevant(srcDir) {
plan := planner.Plan(srcDir)
plan := planner.GetPlan(srcDir)
result = MergePlans(result, plan)
}
}
Expand Down
4 changes: 2 additions & 2 deletions planner/python_planner.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,6 @@ func (g *PythonPlanner) IsRelevant(srcDir string) bool {
return false
}

func (g *PythonPlanner) Plan(srcDir string) *BuildPlan {
return &BuildPlan{}
func (g *PythonPlanner) GetPlan(srcDir string) *Plan {
return &Plan{}
}
18 changes: 11 additions & 7 deletions tmpl/Dockerfile.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ FROM nixos/nix:2.10.3 as base

FROM base as builder

# 1. SETUP PHASE
# 1. SETUP STAGE
# Setup the container, and install nix packages.
# -------------------------------------------------

Expand All @@ -21,19 +21,19 @@ COPY --link ./.devbox/gen/default.nix ./.devbox/gen/
RUN --mount=type=cache,target=/nix/store,from=base,source=/nix/store \
nix-env -if ./.devbox/gen/default.nix

# 2. INSTALL PHASE
# 2. INSTALL STAGE
# Install libraries needed by the source code.
# -----------------------------------------------

COPY --link . ./
RUN --mount=type=cache,target=/nix/store,from=base,source=/nix/store {{.InstallCommand}}
RUN --mount=type=cache,target=/nix/store,from=base,source=/nix/store {{.InstallStage.Command}}

# 3. BUILD PHASE
# 3. BUILD STAGE
# Compile the source code into an executable.
# ----------------------------------------------
RUN --mount=type=cache,target=/nix/store,from=base,source=/nix/store {{.BuildCommand}}
RUN --mount=type=cache,target=/nix/store,from=base,source=/nix/store {{.BuildStage.Command}}

# 4. PACKAGING PHASE
# 4. PACKAGING STAGE
# Create a minimal image that contains the executable.
# -------------------------------------------------------

Expand All @@ -46,6 +46,10 @@ SHELL [ "/busybox/sh", "-eu", "-o", "pipefail", "-c"]
COPY --link --from=builder /scratch/. /app/
WORKDIR /app

# 4. START STAGE
# Execute the application inside the final container.
# ------------------------------------------------------

# We default to ENTRYPOINT instead of CMD as we consider it best practice
# when the container is wrapping an application or service.
ENTRYPOINT {{.StartCommand}}
ENTRYPOINT {{.StartStage.Command}}

0 comments on commit a4c2b40

Please sign in to comment.