Skip to content

Commit 207c30e

Browse files
authored
feat: add pre-defined envd template (#1991)
* feat: add pre-defined envd template Signed-off-by: Keming <kemingyang@tensorchord.ai> * fix lint Signed-off-by: Keming <kemingyang@tensorchord.ai> --------- Signed-off-by: Keming <kemingyang@tensorchord.ai>
1 parent 1089f98 commit 207c30e

File tree

9 files changed

+218
-28
lines changed

9 files changed

+218
-28
lines changed

pkg/app/app.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ func New() EnvdApp {
9999
CommandVersion,
100100
CommandTop,
101101
CommandReference,
102+
CommandNew,
102103
}
103104

104105
internalApp.CustomAppHelpTemplate = ` envd - Development environment for data science and AI/ML teams

pkg/app/bootstrap.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,9 @@ func bootstrap(clicontext *cli.Context) error {
106106
}, {
107107
"buildkit",
108108
buildkit,
109+
}, {
110+
"add pre-defined templates",
111+
addTemplates,
109112
}}
110113

111114
total := len(stages)

pkg/app/new.go

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
// Copyright 2025 The envd Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package app
16+
17+
import (
18+
_ "embed"
19+
"fmt"
20+
"os"
21+
"path/filepath"
22+
"strings"
23+
24+
"github.com/cockroachdb/errors"
25+
"github.com/sirupsen/logrus"
26+
cli "github.com/urfave/cli/v2"
27+
28+
"github.com/tensorchord/envd/pkg/util/fileutil"
29+
)
30+
31+
var (
32+
//go:embed template/uv.envd
33+
templateUV string
34+
//go:embed template/conda.envd
35+
templateConda string
36+
//go:embed template/torch.envd
37+
templateTorch string
38+
39+
templates = map[string]string{
40+
"uv": templateUV,
41+
"conda": templateConda,
42+
"torch": templateTorch,
43+
}
44+
)
45+
46+
func joinKeysToString(table map[string]string) string {
47+
keys := make([]string, 0, len(table))
48+
for k := range table {
49+
keys = append(keys, k)
50+
}
51+
return strings.Join(keys, ", ")
52+
}
53+
54+
func isDefaultTemplate(name string) bool {
55+
_, ok := templates[name]
56+
return ok
57+
}
58+
59+
var CommandNew = &cli.Command{
60+
Name: "new",
61+
Category: CategoryBasic,
62+
Aliases: []string{"n"},
63+
Usage: "Create a new `build.envd` file from pre-defined templates",
64+
Description: `The template used by this command is stored in the
65+
'$HOME/.config/envd/templates' directory, we provide some pre-defined templates for you
66+
to use during 'envd bootstrap', you can also add your own templates to this directory.`,
67+
Flags: []cli.Flag{
68+
&cli.StringFlag{
69+
Name: "template",
70+
Usage: fmt.Sprintf("Template name to use (`envd bootstrap` will add [%s])", joinKeysToString(templates)),
71+
Aliases: []string{"t"},
72+
Required: true,
73+
},
74+
&cli.BoolFlag{
75+
Name: "force",
76+
Usage: "Overwrite the build.envd if existed",
77+
Aliases: []string{"f"},
78+
Required: false,
79+
},
80+
&cli.PathFlag{
81+
Name: "path",
82+
Usage: "Path to the directory of the build.envd",
83+
Aliases: []string{"p"},
84+
Value: ".",
85+
},
86+
},
87+
Action: newCommand,
88+
}
89+
90+
func newCommand(clicontext *cli.Context) error {
91+
workDir, err := filepath.Abs(clicontext.Path("path"))
92+
if err != nil {
93+
return errors.Wrap(err, "failed to get absolute path")
94+
}
95+
96+
force := clicontext.Bool("force")
97+
filePath := filepath.Join(workDir, "build.envd")
98+
exists, err := fileutil.FileExists(filePath)
99+
if err != nil {
100+
return errors.Wrap(err, "failed to check file exists")
101+
}
102+
if exists && !force {
103+
return errors.New("build.envd already exists, use `--force` to overwrite")
104+
}
105+
106+
template := clicontext.String("template")
107+
templateFile := fmt.Sprintf("%s.envd", template)
108+
templatePath, err := fileutil.TemplateFile(templateFile)
109+
if err != nil {
110+
return errors.Wrapf(err, "failed to get template file: `%s`", templateFile)
111+
}
112+
113+
content, err := os.ReadFile(templatePath)
114+
if err != nil {
115+
if os.IsNotExist(err) && isDefaultTemplate(template) {
116+
// Add default templates to the template directory if not exist
117+
err = addTemplates(clicontext)
118+
if err != nil {
119+
return err
120+
}
121+
content, err = os.ReadFile(templatePath)
122+
if err != nil {
123+
return err
124+
}
125+
} else {
126+
return errors.Wrapf(err, "failed to read the template file `%s`", templatePath)
127+
}
128+
}
129+
err = os.WriteFile(filePath, content, 0644)
130+
if err != nil {
131+
return errors.Wrapf(err, "failed to write the build.envd file")
132+
}
133+
logrus.Infof("Template `%s` is created in `%s`", template, filePath)
134+
135+
return nil
136+
}
137+
138+
func addTemplates(clicontext *cli.Context) error {
139+
for name, content := range templates {
140+
file, err := fileutil.TemplateFile(name + ".envd")
141+
if err != nil {
142+
return errors.Wrapf(err, "failed to get template file path: %s", name)
143+
}
144+
exist, err := fileutil.FileExists(file)
145+
if err != nil {
146+
return errors.Wrapf(err, "failed to check file exists: %s", file)
147+
}
148+
if exist {
149+
logrus.Debugf("Template file `%s` already exists in `%s`", name, file)
150+
continue
151+
}
152+
err = os.WriteFile(file, []byte(content), 0644)
153+
if err != nil {
154+
return errors.Wrapf(err, "failed to write template file: %s", name)
155+
}
156+
logrus.Debugf("Template file `%s` is added to `%s`", name, file)
157+
}
158+
159+
return nil
160+
}

pkg/app/template/conda.envd

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
def build():
2+
base(dev=True)
3+
install.conda()
4+
install.python()
5+
shell("fish")

pkg/app/template/julia.envd

Lines changed: 0 additions & 11 deletions
This file was deleted.

pkg/app/template/r.envd

Lines changed: 0 additions & 14 deletions
This file was deleted.

pkg/app/template/torch.envd

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
def default():
2+
shell("fish")
3+
install.conda()
4+
install.python()
5+
install.apt_packages(name=["build-essential"])
6+
7+
8+
def others():
9+
install.python_packages(
10+
name=[
11+
"transformers",
12+
]
13+
)
14+
15+
16+
def gpu():
17+
base(dev=True, image="nvidia/cuda:12.6.3-cudnn-devel-ubuntu22.04")
18+
default()
19+
install.python_packages(
20+
name=["torch --index-url https://download.pytorch.org/whl/cu126"]
21+
)
22+
others()
23+
24+
25+
def cpu():
26+
base(dev=True)
27+
default()
28+
install.python_packages(
29+
name=["torch --index-url https://download.pytorch.org/whl/cpu"]
30+
)
31+
others()
32+
33+
34+
def build():
35+
cpu()

pkg/app/template/uv.envd

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
def build():
2+
base(dev=True)
3+
install.uv()
4+
shell("fish")

pkg/util/fileutil/file.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,10 @@ import (
2727
)
2828

2929
var (
30-
DefaultConfigDir string
31-
DefaultCacheDir string
32-
DefaultEnvdLibDir string
30+
DefaultConfigDir string
31+
DefaultCacheDir string
32+
DefaultEnvdLibDir string
33+
DefaultTemplateDir string
3334
)
3435

3536
func init() {
@@ -40,6 +41,7 @@ func init() {
4041
DefaultConfigDir = filepath.Join(home, ".config", "envd")
4142
DefaultCacheDir = filepath.Join(home, ".cache", "envd")
4243
DefaultEnvdLibDir = filepath.Join(DefaultCacheDir, "envdlib")
44+
DefaultTemplateDir = filepath.Join(DefaultConfigDir, "templates")
4345
}
4446

4547
// FileExists returns true if the file exists
@@ -136,6 +138,11 @@ func CacheFile(filename string) (string, error) {
136138
return validateAndJoin(DefaultCacheDir, filename)
137139
}
138140

141+
// TemplateFile returns the location for the specified envd template file
142+
func TemplateFile(filename string) (string, error) {
143+
return validateAndJoin(DefaultTemplateDir, filename)
144+
}
145+
139146
func validateAndJoin(dir, file string) (string, error) {
140147
if strings.ContainsRune(file, os.PathSeparator) {
141148
return "", errors.Newf("filename %s should not contain any path separator", file)

0 commit comments

Comments
 (0)