Skip to content

Commit 41d73a7

Browse files
authored
[templates] Add ability to start project from a template (#1016)
## Summary Adds ability to initialize a project from a template. Templates are a subset of examples defined in templates.go Can specify a directory and will be created if needed. Cannot contain anything in it. ## How was it tested? ```bash devbox create foo --template php ``` To list available templates: ``` devbox create Usage: devbox create [dir] --template <template> Available templates: * csharp examples/development/csharp/hello-world * fsharp examples/development/fsharp/hello-world * go examples/development/go/hello-world * haskell examples/development/haskell * python examples/development/python * ruby examples/development/ruby * rust examples/development/rust * elixir examples/development/elixir * java examples/development/java * nim examples/development/nim/spinnytest * nodejs examples/development/nodejs ```
1 parent 37d7f53 commit 41d73a7

File tree

7 files changed

+265
-10
lines changed

7 files changed

+265
-10
lines changed

internal/boxcli/create.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
// Copyright 2023 Jetpack Technologies Inc and contributors. All rights reserved.
2+
// Use of this source code is governed by the license in the LICENSE file.
3+
4+
package boxcli
5+
6+
import (
7+
"fmt"
8+
"os"
9+
"path/filepath"
10+
11+
"github.com/spf13/cobra"
12+
"go.jetpack.io/devbox/internal/templates"
13+
"go.jetpack.io/devbox/internal/ux"
14+
)
15+
16+
type createCmdFlags struct {
17+
showAll bool
18+
template string
19+
}
20+
21+
func createCmd() *cobra.Command {
22+
flags := &createCmdFlags{}
23+
command := &cobra.Command{
24+
Use: "create [dir] --template <template>",
25+
Short: "Initialize a directory as a devbox project using a template",
26+
Args: cobra.MaximumNArgs(1),
27+
RunE: func(cmd *cobra.Command, args []string) error {
28+
if flags.template == "" {
29+
fmt.Fprintf(
30+
cmd.ErrOrStderr(),
31+
"Usage: devbox create [dir] --template <template>\n\n",
32+
)
33+
templates.List(cmd.ErrOrStderr(), flags.showAll)
34+
if !flags.showAll {
35+
fmt.Fprintf(
36+
cmd.ErrOrStderr(),
37+
"\nTo see all available templates, run `devbox create --show-all`\n",
38+
)
39+
}
40+
return nil
41+
}
42+
return runCreateCmd(cmd, args, flags)
43+
},
44+
}
45+
46+
command.Flags().StringVarP(
47+
&flags.template, "template", "t", "",
48+
"template to initialize the project with",
49+
)
50+
command.Flags().BoolVar(
51+
&flags.showAll, "show-all", false,
52+
"show all available templates",
53+
)
54+
55+
return command
56+
}
57+
58+
func runCreateCmd(
59+
cmd *cobra.Command,
60+
args []string,
61+
flags *createCmdFlags,
62+
) error {
63+
path := pathArg(args)
64+
if path == "" {
65+
wd, _ := os.Getwd()
66+
path = filepath.Join(wd, flags.template)
67+
}
68+
69+
err := templates.Init(cmd.ErrOrStderr(), flags.template, path)
70+
if err != nil {
71+
return err
72+
}
73+
74+
ux.Fsuccess(
75+
cmd.ErrOrStderr(),
76+
"Initialized devbox project using template %s\n",
77+
flags.template,
78+
)
79+
80+
return nil
81+
}

internal/boxcli/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ func RootCmd() *cobra.Command {
4646
}
4747
// Stable commands
4848
command.AddCommand(addCmd())
49+
command.AddCommand(createCmd())
4950
command.AddCommand(generateCmd())
5051
command.AddCommand(globalCmd())
5152
command.AddCommand(infoCmd())

internal/goutil/goutil.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Copyright 2023 Jetpack Technologies Inc and contributors. All rights reserved.
2+
// Use of this source code is governed by the license in the LICENSE file.
3+
4+
package goutil
5+
6+
func PickByKeysSorted[K comparable, V any](in map[K]V, keys []K) []V {
7+
out := make([]V, len(keys))
8+
for i, key := range keys {
9+
out[i] = in[key]
10+
}
11+
return out
12+
}

internal/impl/flakes.go

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package impl
66
import (
77
"github.com/samber/lo"
88

9+
"go.jetpack.io/devbox/internal/goutil"
910
"go.jetpack.io/devbox/internal/planner/plansdk"
1011
)
1112

@@ -47,14 +48,5 @@ func (d *Devbox) flakeInputs() ([]*plansdk.FlakeInput, error) {
4748
}
4849
}
4950

50-
return PickByKeysSorted(inputs, order), nil
51-
}
52-
53-
// TODO: move this to a util package
54-
func PickByKeysSorted[K comparable, V any](in map[K]V, keys []K) []V {
55-
out := make([]V, len(keys))
56-
for i, key := range keys {
57-
out[i] = in[key]
58-
}
59-
return out
51+
return goutil.PickByKeysSorted(inputs, order), nil
6052
}

internal/templates/template.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
// Copyright 2023 Jetpack Technologies Inc and contributors. All rights reserved.
2+
// Use of this source code is governed by the license in the LICENSE file.
3+
4+
package templates
5+
6+
import (
7+
"fmt"
8+
"io"
9+
"os"
10+
"os/exec"
11+
"path/filepath"
12+
13+
"github.com/pkg/errors"
14+
"github.com/samber/lo"
15+
"go.jetpack.io/devbox/internal/boxcli/usererr"
16+
"golang.org/x/exp/slices"
17+
)
18+
19+
func Init(w io.Writer, template, dir string) error {
20+
if err := createDirAndEnsureEmpty(dir); err != nil {
21+
return err
22+
}
23+
24+
templatePath, ok := templates[template]
25+
if !ok {
26+
return usererr.New("unknown template %q", template)
27+
}
28+
29+
tmp, err := os.MkdirTemp("", "devbox-template")
30+
if err != nil {
31+
return errors.WithStack(err)
32+
}
33+
cmd := exec.Command(
34+
"git", "clone", "[email protected]:jetpack-io/devbox.git", tmp,
35+
)
36+
fmt.Fprintf(w, "%s\n", cmd)
37+
cmd.Stderr = os.Stderr
38+
cmd.Stdout = os.Stdout
39+
if err = cmd.Run(); err != nil {
40+
return errors.WithStack(err)
41+
}
42+
43+
cmd = exec.Command(
44+
"sh", "-c",
45+
fmt.Sprintf("cp -r %s %s", filepath.Join(tmp, templatePath, "*"), dir),
46+
)
47+
fmt.Fprintf(w, "%s\n", cmd)
48+
cmd.Stderr = os.Stderr
49+
cmd.Stdout = os.Stdout
50+
return errors.WithStack(cmd.Run())
51+
}
52+
53+
func List(w io.Writer, showAll bool) {
54+
fmt.Fprintf(w, "Templates:\n\n")
55+
keysToShow := popularTemplates
56+
if showAll {
57+
keysToShow = lo.Keys(templates)
58+
}
59+
60+
slices.Sort(keysToShow)
61+
for _, key := range keysToShow {
62+
fmt.Fprintf(w, "* %-15s %s\n", key, templates[key])
63+
}
64+
}
65+
66+
func createDirAndEnsureEmpty(dir string) error {
67+
entries, err := os.ReadDir(dir)
68+
if errors.Is(err, os.ErrNotExist) {
69+
if err = os.MkdirAll(dir, 0755); err != nil {
70+
return errors.WithStack(err)
71+
}
72+
} else if err != nil {
73+
return errors.WithStack(err)
74+
}
75+
76+
if len(entries) > 0 {
77+
return usererr.New("directory %q is not empty", dir)
78+
}
79+
80+
return nil
81+
}

internal/templates/templates.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// Copyright 2023 Jetpack Technologies Inc and contributors. All rights reserved.
2+
// Use of this source code is governed by the license in the LICENSE file.
3+
4+
package templates
5+
6+
var popularTemplates = []string{
7+
"node-npm",
8+
"node-typescript",
9+
"node-yarn",
10+
"python-pip",
11+
"python-pipenv",
12+
"python-poetry",
13+
"php",
14+
"ruby",
15+
"rust",
16+
"go",
17+
}
18+
19+
var templates = map[string]string{
20+
"apache": "examples/servers/apache/",
21+
"argo": "examples/cloud_development/argo-workflows/",
22+
"caddy": "examples/servers/caddy/",
23+
"django": "examples/stacks/django/",
24+
"dotnet": "examples/development/csharp/hello-world/",
25+
"drupal": "examples/stacks/drupal/",
26+
"elixir": "examples/development/elixir/elixir_hello/",
27+
"fsharp": "examples/development/fsharp/hello-world/",
28+
"go": "examples/development/go/hello-world/",
29+
"gradio": "examples/data_science/pytorch/gradio/",
30+
"haskell": "examples/development/haskell/",
31+
"java-gradle": "examples/development/java/gradle/hello-world/",
32+
"java-maven": "examples/development/java/maven/hello-world/",
33+
"jekyll": "examples/stacks/jekyll/",
34+
"jupyter": "examples/data_science/jupyter/",
35+
"lapp-stack": "examples/stacks/lapp-stack/",
36+
"lepp-stack": "examples/stacks/lepp-stack/",
37+
"llama": "examples/data_science/llama/",
38+
"maelstrom": "examples/cloud_development/maelstrom/",
39+
"minikube": "examples/cloud_development/minikube/",
40+
"mariadb": "examples/databases/mariadb/",
41+
"nginx": "examples/servers/nginx/",
42+
"nim": "examples/development/nim/spinnytest/",
43+
"node-npm": "examples/development/nodejs/nodejs-npm/",
44+
"node-typescript": "examples/development/nodejs/nodejs-typescript/",
45+
"node-yarn": "examples/development/nodejs/nodejs-yarn/",
46+
"php": "examples/development/php/php8.1/",
47+
"postgres": "examples/databases/postgres/",
48+
"python-pip": "examples/development/python/pip/",
49+
"python-pipenv": "examples/development/python/pipenv/",
50+
"python-poetry": "examples/development/python/poetry/poetry-demo/",
51+
"pytorch": "examples/data_science/pytorch/basic-example/",
52+
"rails": "examples/stacks/rails/",
53+
"redis": "examples/databases/redis/",
54+
"ruby": "examples/development/ruby/",
55+
"rust": "examples/development/rust/rust-stable-hello-world/",
56+
"temporal": "examples/cloud_development/temporal/",
57+
"tensorflow": "examples/data_science/tensorflow/",
58+
"tutorial": "examples/tutorial/",
59+
"zig": "examples/development/zig/zig-hello-world/",
60+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Copyright 2023 Jetpack Technologies Inc and contributors. All rights reserved.
2+
// Use of this source code is governed by the license in the LICENSE file.
3+
package templates
4+
5+
import (
6+
"os"
7+
"path/filepath"
8+
"testing"
9+
10+
"github.com/pkg/errors"
11+
)
12+
13+
func TestTemplatesExist(t *testing.T) {
14+
curDir := ""
15+
// Try to find examples dir. After 10 hops, we give up.
16+
for i := 0; i < 10; i++ {
17+
if _, err := os.Stat(curDir + "examples"); err == nil {
18+
break
19+
}
20+
curDir += "../"
21+
}
22+
for _, path := range templates {
23+
_, err := os.Stat(filepath.Join(curDir, path, "devbox.json"))
24+
if errors.Is(err, os.ErrNotExist) {
25+
t.Errorf("Directory/devbox.json for %s does not exist", path)
26+
}
27+
}
28+
}

0 commit comments

Comments
 (0)