From 21ea7fe8978c531cc4942454234befe170156929 Mon Sep 17 00:00:00 2001 From: Keming Date: Sat, 8 Nov 2025 23:42:59 +0800 Subject: [PATCH 1/2] feat: support install.nodejs() Signed-off-by: Keming --- .../{other_test.go => others_test.go} | 27 +++++------ e2e/language/testdata/golang/build.envd | 12 ----- e2e/language/testdata/others/build.envd | 16 +++++++ e2e/language/testdata/rust/build.envd | 12 ----- .../frontend/starlark/v1/install/const.go | 1 + .../frontend/starlark/v1/install/install.go | 14 ++++++ pkg/lang/ir/v1/interface.go | 10 ++++ pkg/lang/ir/v1/nodejs.go | 47 +++++++++++++++++++ pkg/lang/ir/v1/system.go | 4 ++ pkg/types/envd.go | 3 +- 10 files changed, 106 insertions(+), 40 deletions(-) rename e2e/language/{other_test.go => others_test.go} (70%) delete mode 100644 e2e/language/testdata/golang/build.envd create mode 100644 e2e/language/testdata/others/build.envd delete mode 100644 e2e/language/testdata/rust/build.envd create mode 100644 pkg/lang/ir/v1/nodejs.go diff --git a/e2e/language/other_test.go b/e2e/language/others_test.go similarity index 70% rename from e2e/language/other_test.go rename to e2e/language/others_test.go index d1f5903a2..0cd5ecc9b 100644 --- a/e2e/language/other_test.go +++ b/e2e/language/others_test.go @@ -24,28 +24,25 @@ import ( var _ = Describe("rust", Ordered, func() { testcase := "e2e" - Describe("Should install rust lang successfully", func() { - exampleName := "rust" + Describe("Should install rust/golang/nodejs successfully", func() { + exampleName := "others" e := e2e.NewExample(e2e.BuildContextDirWithName(exampleName), testcase) BeforeAll(e.BuildImage(true)) BeforeEach(e.RunContainer()) - It("Should have cargo installed", func() { - res, err := e.ExecRuntimeCommand("show") + It("Should have go installed", func() { + res, err := e.ExecRuntimeCommand("go version") + Expect(err).To(BeNil()) + Expect(res).To(ContainSubstring("go version")) + }) + It("Should have rust installed", func() { + res, err := e.ExecRuntimeCommand("rust version") Expect(err).To(BeNil()) Expect(res).To(ContainSubstring("toolchain")) }) - AfterEach(e.DestroyContainer()) - }) - - Describe("Should install golang successfully", func() { - exampleName := "golang" - e := e2e.NewExample(e2e.BuildContextDirWithName(exampleName), testcase) - BeforeAll(e.BuildImage(true)) - BeforeEach(e.RunContainer()) - It("Should have go installed", func() { - res, err := e.ExecRuntimeCommand("version") + It("Should have nodejs installed", func() { + res, err := e.ExecRuntimeCommand("nodejs version") Expect(err).To(BeNil()) - Expect(res).To(ContainSubstring("go version")) + Expect(res).To(ContainSubstring("v")) }) AfterEach(e.DestroyContainer()) }) diff --git a/e2e/language/testdata/golang/build.envd b/e2e/language/testdata/golang/build.envd deleted file mode 100644 index 2e32a7928..000000000 --- a/e2e/language/testdata/golang/build.envd +++ /dev/null @@ -1,12 +0,0 @@ -# syntax=v1 - - -def build(): - base(dev=True) - install.go() - shell("fish") - runtime.command( - commands={ - "version": "go version", - } - ) diff --git a/e2e/language/testdata/others/build.envd b/e2e/language/testdata/others/build.envd new file mode 100644 index 000000000..831cea4af --- /dev/null +++ b/e2e/language/testdata/others/build.envd @@ -0,0 +1,16 @@ +# syntax=v1 + + +def build(): + base(dev=True) + install.go() + install.rust() + install.nodejs() + shell("fish") + runtime.command( + commands={ + "go version": "go version", + "rust version": "rustup show", + "nodejs version": "node --version", + } + ) diff --git a/e2e/language/testdata/rust/build.envd b/e2e/language/testdata/rust/build.envd deleted file mode 100644 index a3fe3484f..000000000 --- a/e2e/language/testdata/rust/build.envd +++ /dev/null @@ -1,12 +0,0 @@ -# syntax=v1 - - -def build(): - base(dev=True) - install.rust() - shell("fish") - runtime.command( - commands={ - "show": "rustup show", - } - ) diff --git a/pkg/lang/frontend/starlark/v1/install/const.go b/pkg/lang/frontend/starlark/v1/install/const.go index 4ac147ea4..041d77824 100644 --- a/pkg/lang/frontend/starlark/v1/install/const.go +++ b/pkg/lang/frontend/starlark/v1/install/const.go @@ -24,6 +24,7 @@ const ( ruleJulia = "install.julia" ruleRust = "install.rust" ruleGo = "install.go" + ruleNodeJS = "install.nodejs" // packages ruleSystemPackage = "install.apt_packages" diff --git a/pkg/lang/frontend/starlark/v1/install/install.go b/pkg/lang/frontend/starlark/v1/install/install.go index fb362870b..fb5c05636 100644 --- a/pkg/lang/frontend/starlark/v1/install/install.go +++ b/pkg/lang/frontend/starlark/v1/install/install.go @@ -40,6 +40,7 @@ var Module = &starlarkstruct.Module{ "julia": starlark.NewBuiltin(ruleJulia, ruleFuncJulia), "rust": starlark.NewBuiltin(ruleRust, ruleFuncRust), "go": starlark.NewBuiltin(ruleGo, ruleFuncGo), + "nodejs": starlark.NewBuiltin(ruleNodeJS, ruleFuncNodeJS), // packages "apt_packages": starlark.NewBuiltin(ruleSystemPackage, ruleFuncSystemPackage), "python_packages": starlark.NewBuiltin(rulePyPIPackage, ruleFuncPyPIPackage), @@ -152,6 +153,19 @@ func ruleFuncGo(thread *starlark.Thread, _ *starlark.Builtin, return starlark.None, nil } +func ruleFuncNodeJS(thread *starlark.Thread, _ *starlark.Builtin, + args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { + logger.Debugf("rule `%s` is invoked", ruleNodeJS) + + var version string + if err := starlark.UnpackArgs(ruleNodeJS, args, kwargs, "version?", &version); err != nil { + return nil, err + } + + ir.NodeJS(version) + return starlark.None, nil +} + func ruleFuncPyPIPackage(thread *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { var name *starlark.List diff --git a/pkg/lang/ir/v1/interface.go b/pkg/lang/ir/v1/interface.go index 9a85832d2..4eb67290c 100644 --- a/pkg/lang/ir/v1/interface.go +++ b/pkg/lang/ir/v1/interface.go @@ -114,6 +114,16 @@ func Golang(version string) { g.Languages = append(g.Languages, golang) } +func NodeJS(version string) { + g := DefaultGraph.(*generalGraph) + + nodejs := ir.Language{Name: "nodejs"} + if len(version) > 0 { + nodejs.Version = &version + } + g.Languages = append(g.Languages, nodejs) +} + func PyPIPackage(deps []string, requirementsFile string, wheels []string) error { g := DefaultGraph.(*generalGraph) diff --git a/pkg/lang/ir/v1/nodejs.go b/pkg/lang/ir/v1/nodejs.go new file mode 100644 index 000000000..97117cfa9 --- /dev/null +++ b/pkg/lang/ir/v1/nodejs.go @@ -0,0 +1,47 @@ +// Copyright 2025 The envd Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +import ( + "github.com/moby/buildkit/client/llb" +) + +// from https://nodejs.org/download/release/ +const ( + nodejsDefaultVersion = "25.1.0" + nodejsTempDir = "/tmp/nodejs" + nodejsHomeDir = "/opt/nodejs" + nodejsHomeBin = "/opt/nodejs/bin" +) + +func (g *generalGraph) installNodeJS(root llb.State, version *string) llb.State { + nodejsVersion := nodejsDefaultVersion + if version != nil { + nodejsVersion = *version + } + + base := llb.Image(builderImage) + builder := base.Run( + llb.Shlexf(`sh -c "mkdir %[1]s && wget -qO- https://nodejs.org/download/release/v%[2]s/node-v%[2]s-linux-$(uname -m | sed -e 's/x86_64/x64/').tar.xz | tar -xJ --strip-components=1 -C %[1]s || exit 1"`, nodejsTempDir, nodejsVersion), + llb.WithCustomNamef("[internal] download nodejs %s", nodejsVersion), + ).Root() + + root = root.File( + llb.Copy(builder, nodejsTempDir, nodejsHomeDir), + llb.WithCustomNamef("[internal] prepare nodejs %s", nodejsVersion), + ) + g.RuntimeEnvPaths = append(g.RuntimeEnvPaths, nodejsHomeBin) + return root +} diff --git a/pkg/lang/ir/v1/system.go b/pkg/lang/ir/v1/system.go index 5a5ca7f93..0764b1c4f 100644 --- a/pkg/lang/ir/v1/system.go +++ b/pkg/lang/ir/v1/system.go @@ -172,6 +172,8 @@ func (g *generalGraph) compileLanguage(root llb.State) (llb.State, error) { root = g.installRust(root, language.Version) case "go": root = g.installGolang(root, language.Version) + case "nodejs": + root = g.installNodeJS(root, language.Version) } } return root, err @@ -189,6 +191,8 @@ func (g *generalGraph) compileLanguage(root llb.State) (llb.State, error) { lang = g.installRust(root, language.Version) case "go": lang = g.installGolang(root, language.Version) + case "nodejs": + lang = g.installNodeJS(root, language.Version) } langs = append(langs, lang) } diff --git a/pkg/types/envd.go b/pkg/types/envd.go index 0b4db9d1c..8098bb743 100644 --- a/pkg/types/envd.go +++ b/pkg/types/envd.go @@ -83,7 +83,8 @@ var BaseAptPackage = []string{ "make", "zsh", "locales", - "gpg", // used by r-lang + "gpg", // used by r-lang + "libatomic1", // used by nodejs } type EnvdImage struct { From 371e6e239d6f1b91fe8f7799f88c9bc35de880ef Mon Sep 17 00:00:00 2001 From: Keming Date: Sat, 8 Nov 2025 23:44:24 +0800 Subject: [PATCH 2/2] add to docs Signed-off-by: Keming --- envd/api/v1/install.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/envd/api/v1/install.py b/envd/api/v1/install.py index 02ad22538..c78e2de9e 100644 --- a/envd/api/v1/install.py +++ b/envd/api/v1/install.py @@ -99,6 +99,14 @@ def go(version: Optional[str] = "1.25.3"): """ +def nodejs(version: Optional[str] = "25.1.0"): + """Install NodeJS programming language. + + Args: + version (Optional[str]): NodeJS version, such as '25.1.0'. + """ + + def codex(version: Optional[str] = "0.55.0"): """Install Codex agent.