// RAINBOND, Application Management Platform // Copyright (C) 2014-2017 Goodrain Co., Ltd. // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. For any non-GPL usage of Rainbond, // one or multiple Commercial Licenses authorized by Goodrain Co., Ltd. // must be obtained first. // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // You should have received a copy of the GNU General Public License // along with this program. If not, see . package build import ( "context" "encoding/json" "fmt" "io" "io/ioutil" "os" "os/exec" "path" "strings" "time" "github.com/goodrain/rainbond/db" "github.com/eapache/channels" "github.com/goodrain/rainbond/builder" jobc "github.com/goodrain/rainbond/builder/job" "github.com/goodrain/rainbond/builder/parser/code" "github.com/goodrain/rainbond/builder/sources" "github.com/goodrain/rainbond/util" "github.com/sirupsen/logrus" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) func slugBuilder() (Build, error) { return &slugBuild{}, nil } type slugBuild struct { tgzDir string buildCacheDir string re *Request } func (s *slugBuild) Build(re *Request) (*Response, error) { re.Logger.Info(util.Translation("Start compiling the source code"), map[string]string{"step": "build-exector"}) s.tgzDir = re.TGZDir normalizedReq := *re normalizedReq.BuildEnvs = expandBuildEnvsForSlugBuild(re.BuildEnvs) re = &normalizedReq s.re = re s.buildCacheDir = re.CacheDir packageName := fmt.Sprintf("%s/%s.tgz", s.tgzDir, re.DeployVersion) //Stops previous build tasks for the same component //If an error occurs, it does not affect the current build task if err := s.stopPreBuildJob(re); err != nil { logrus.Errorf("stop pre build job for service %s failure %s", re.ServiceID, err.Error()) } if err := s.runBuildJob(re); err != nil { re.Logger.Error(util.Translation("Compiling the source code failure"), map[string]string{"step": "build-code", "status": "failure"}) logrus.Error("build slug in container error,", err.Error()) return nil, err } re.Logger.Info("code build success", map[string]string{"step": "build-exector"}) defer func() { if err := os.Remove(packageName); err != nil && !strings.Contains(err.Error(), "no such file or directory") { logrus.Warningf("pkg name: %s; remove slug pkg: %v", packageName, err) } }() fileInfo, err := os.Stat(packageName) if err != nil { re.Logger.Error(util.Translation("Check that the build result failure"), map[string]string{"step": "build-code", "status": "failure"}) logrus.Error("build package check error", err.Error()) return nil, fmt.Errorf("build package failure") } if fileInfo.Size() == 0 { re.Logger.Error(util.Translation("Source build failure and result slug size is 0"), map[string]string{"step": "build-code", "status": "failure"}) return nil, fmt.Errorf("build package failure") } //After 5.1.5 version, wrap slug pacage in the runner image imageName, err := s.buildRunnerImage(packageName) if err != nil { re.Logger.Error(util.Translation("Build runner image failure"), map[string]string{"step": "build-code", "status": "failure"}) return nil, fmt.Errorf("build runner image failure") } re.Logger.Info(util.Translation("build runtime image success"), map[string]string{"step": "build-code", "status": "success"}) res := &Response{ MediumType: ImageMediumType, MediumPath: imageName, } return res, nil } func expandBuildEnvsForSlugBuild(envs map[string]string) map[string]string { if envs == nil { return nil } expanded := make(map[string]string, len(envs)+8) for key, value := range envs { expanded[key] = value } legacyAliases := map[string]string{ "BUILD_NO_CACHE": "NO_CACHE", "BUILD_ENABLE_ORACLEJDK": "ENABLE_ORACLEJDK", "BUILD_ORACLEJDK_URL": "ORACLEJDK_URL", "BUILD_MAVEN_SETTING_NAME": "MAVEN_SETTING_NAME", "BUILD_MAVEN_CUSTOM_OPTS": "MAVEN_CUSTOM_OPTS", "BUILD_MAVEN_CUSTOM_GOALS": "MAVEN_CUSTOM_GOALS", "BUILD_MAVEN_JAVA_OPTS": "MAVEN_JAVA_OPTS", "BUILD_MAVEN_MIRROR_DISABLE": "MAVEN_MIRROR_DISABLE", "BUILD_MAVEN_MIRROR_OF": "MAVEN_MIRROR_OF", "BUILD_MAVEN_MIRROR_URL": "MAVEN_MIRROR_URL", "BUILD_GOPROXY": "GOPROXY", "BUILD_GOPRIVATE": "GOPRIVATE", "BUILD_GO_INSTALL_PACKAGE_SPEC": "GO_INSTALL_PACKAGE_SPEC", "BUILD_PIP_INDEX_URL": "PIP_INDEX_URL", "BUILD_PACKAGE_TOOL": "PACKAGE_TOOL", "BUILD_NODE_ENV": "NODE_ENV", "BUILD_NODE_MODULES_CACHE": "NODE_MODULES_CACHE", "BUILD_NODE_BUILD_CMD": "NODE_BUILD_CMD", "BUILD_NPMRC_CONTENT": "NPMRC_CONTENT", "BUILD_NPM_REGISTRY": "NPM_REGISTRY", "BUILD_YARNRC_CONTENT": "YARNRC_CONTENT", "BUILD_YARN_REGISTRY": "YARN_REGISTRY", "BUILD_PROCFILE": "PROCFILE", "BUILD_RUNTIMES": "RUNTIMES", "BUILD_RUNTIMES_MAVEN": "RUNTIMES_MAVEN", "BUILD_RUNTIMES_SERVER": "RUNTIMES_SERVER", "BUILD_GOVERSION": "GOVERSION", } for buildKey, legacyKey := range legacyAliases { if value, exists := expanded[legacyKey]; exists && strings.TrimSpace(value) != "" { continue } if value, ok := expanded[buildKey]; ok && strings.TrimSpace(value) != "" { expanded[legacyKey] = value } } backfillLegacyAlias(expanded, "DIST_DIR", "BUILD_DIST_DIR", "BUILD_OUTPUT_DIR") backfillLegacyAlias(expanded, "NODE_BUILD_CMD", "BUILD_NODE_BUILD_CMD", "BUILD_BUILD_CMD") return expanded } func backfillLegacyAlias(envs map[string]string, legacyKey string, buildKeys ...string) { if envs == nil { return } if value, exists := envs[legacyKey]; exists && strings.TrimSpace(value) != "" { return } for _, buildKey := range buildKeys { if value, ok := envs[buildKey]; ok && strings.TrimSpace(value) != "" { envs[legacyKey] = value return } } } func (s *slugBuild) writeRunDockerfile(sourceDir, packageName string, envs map[string]string) error { runDockerfile := ` FROM %s ARG CODE_COMMIT_HASH ARG CODE_COMMIT_USER ARG CODE_COMMIT_MESSAGE COPY %s /tmp/slug/slug.tgz RUN chown rain:rain /tmp/slug/slug.tgz ENV CODE_COMMIT_HASH=${CODE_COMMIT_HASH} ENV CODE_COMMIT_USER=${CODE_COMMIT_USER} ENV CODE_COMMIT_MESSAGE=${CODE_COMMIT_MESSAGE} ENV VERSION=%s ` logrus.Debugf("cacheDir:%v, from:%v, packageName:%v, Dir(slugPackage):%v", sourceDir, builder.RUNNERIMAGENAME, packageName, path.Dir(packageName)) result := util.ParseVariable(fmt.Sprintf(runDockerfile, builder.RUNNERIMAGENAME, packageName, s.re.DeployVersion), envs) return ioutil.WriteFile(path.Join(sourceDir, "Dockerfile"), []byte(result), 0755) } // buildRunnerImage Wrap slug in the runner image func (s *slugBuild) buildRunnerImage(slugPackage string) (string, error) { imageName := CreateImageName(s.re.ServiceID, s.re.DeployVersion) cacheDir := path.Join(path.Dir(slugPackage), "."+s.re.DeployVersion) if err := util.CheckAndCreateDir(cacheDir); err != nil { return "", fmt.Errorf("create cache package dir failure %s", err.Error()) } defer func() { if err := os.RemoveAll(cacheDir); err != nil { logrus.Errorf("remove cache dir %s failure %s", cacheDir, err.Error()) } }() packageName := path.Base(slugPackage) if err := util.Rename(slugPackage, path.Join(cacheDir, packageName)); err != nil { return "", fmt.Errorf("move code package failure %s", err.Error()) } //write default runtime dockerfile if err := s.writeRunDockerfile(cacheDir, packageName, s.re.BuildEnvs); err != nil { return "", fmt.Errorf("write default runtime dockerfile error:%s", err.Error()) } //build runtime image if err := s.re.ImageClient.ImagesPullAndPush(builder.RUNNERIMAGENAME, builder.GetRunnerImage(s.re.BRVersion), "", "", s.re.Logger); err != nil { failMsg := fmt.Sprintf("%s: %s", util.Translation("Pull runner image failed, please check if the image is accessible"), builder.RUNNERIMAGENAME) s.re.Logger.Error(failMsg, map[string]string{"step": "builder-exector", "status": "failure"}) return "", fmt.Errorf("pull image %s: %v", builder.RUNNERIMAGENAME, err) } logrus.Infof("pull image %s successfully.", builder.RUNNERIMAGENAME) err := sources.ImageBuild(s.re.Arch, cacheDir, s.re.RbdNamespace, s.re.ServiceID, s.re.DeployVersion, s.re.Logger, "run-build", "", s.re.BuildKitImage, s.re.BuildKitArgs, s.re.BuildKitCache, s.re.KubeClient) if err != nil { failMsg := util.Translation("Build image failed, please check build logs") s.re.Logger.Error(failMsg, map[string]string{"step": "builder-exector", "status": "failure"}) logrus.Errorf("build image error: %s", err.Error()) return "", err } return imageName, nil } func (s *slugBuild) getSourceCodeTarFile(re *Request) (string, error) { var cmd []string sourceTarFile := fmt.Sprintf("%s/%s-%s.tar", util.GetParentDirectory(re.SourceDir), re.ServiceID, re.DeployVersion) // 验证并处理支持的ServerType switch re.ServerType { case "svn": cmd = append(cmd, "tar", "-cf", sourceTarFile, "./") case "git": cmd = append(cmd, "tar", "-cf", sourceTarFile, "./") default: return "", fmt.Errorf("unsupported server type '%s' for source code tar operation, only 'git' and 'svn' are supported", re.ServerType) } // 防御性检查:确保cmd不为空 if len(cmd) == 0 { return "", fmt.Errorf("no tar command generated for server type '%s'", re.ServerType) } source := exec.Command(cmd[0], cmd[1:]...) source.Dir = re.SourceDir logrus.Debugf("tar source code to file %s", sourceTarFile) if err := source.Run(); err != nil && err.Error() != "exit status 1" { return "", fmt.Errorf("command %s: %v", source.String(), err) } return sourceTarFile, nil } // stopPreBuildJob Stops previous build tasks for the same component // The same component retains only one build task to perform func (s *slugBuild) stopPreBuildJob(re *Request) error { jobList, err := jobc.GetJobController().GetServiceJobs(re.ServiceID) if err != nil { logrus.Errorf("get pre build job for service %s failure ,%s", re.ServiceID, err.Error()) } if len(jobList) > 0 { for _, job := range jobList { jobc.GetJobController().DeleteJob(job.Name) } } return nil } func (s *slugBuild) createVolumeAndMount(re *Request, sourceTarFileName string, buildNoCache bool) (volumes []corev1.Volume, volumeMounts []corev1.VolumeMount) { hostPathType := corev1.HostPathDirectoryOrCreate volumeMounts = []corev1.VolumeMount{ { Name: "slug", MountPath: "/tmp/slug", }, { Name: "app", MountPath: "/tmp/app", }, } volumes = []corev1.Volume{ { Name: "app", VolumeSource: corev1.VolumeSource{ HostPath: &corev1.HostPathVolumeSource{ Path: path.Join("/opt/rainbond/", re.SourceDir), Type: &hostPathType, }, }, }, { Name: "slug", VolumeSource: corev1.VolumeSource{ HostPath: &corev1.HostPathVolumeSource{ Path: path.Join("/opt/rainbond/", re.TGZDir), Type: &hostPathType, }, }, }, } if !buildNoCache { volumes = append(volumes, corev1.Volume{ Name: "cache", VolumeSource: corev1.VolumeSource{ HostPath: &corev1.HostPathVolumeSource{ Path: path.Join("/opt/rainbond/", re.CacheDir), Type: &hostPathType, }, }, }) volumeMounts = append(volumeMounts, corev1.VolumeMount{ Name: "cache", MountPath: "/tmp/cache", }) } return volumes, volumeMounts } func newSlugBuildPodSpec(arch, hostIP, cacheMode string) corev1.PodSpec { podSpec := corev1.PodSpec{ RestartPolicy: corev1.RestartPolicyOnFailure, Affinity: &corev1.Affinity{ NodeAffinity: &corev1.NodeAffinity{ RequiredDuringSchedulingIgnoredDuringExecution: &corev1.NodeSelector{ NodeSelectorTerms: []corev1.NodeSelectorTerm{{ MatchExpressions: []corev1.NodeSelectorRequirement{ { Key: "kubernetes.io/arch", Operator: corev1.NodeSelectorOpIn, Values: []string{arch}, }, { Key: "kubernetes.io/hostname", Operator: corev1.NodeSelectorOpIn, Values: []string{hostIP}, }, }, }, }, }, }, }, } if hostIP != "" { // All cache modes use the current chaos node, including dedicated or cordoned nodes. podSpec.Tolerations = []corev1.Toleration{{Operator: corev1.TolerationOpExists}} if cacheMode == "hostpath" { logrus.Debugf("builder cache mode using hostpath, schedule job into current node") podSpec.NodeSelector = map[string]string{ "kubernetes.io/hostname": hostIP, } } } return podSpec } func (s *slugBuild) runBuildJob(re *Request) error { //prepare build code dir re.Logger.Info(util.Translation("Start make code package"), map[string]string{"step": "build-exector"}) start := time.Now() var sourceTarFileName string // 只有 git 和 svn 类型需要创建源码 tar 文件 if re.ServerType == "git" || re.ServerType == "svn" { var err error // handle nodejs or static dir if err := s.HandleNodeJsDir(re); err != nil { logrus.Error("handle nodejs code error:", err) re.Logger.Error(util.Translation("Handle nodejs code error"), map[string]string{"step": "builder-exector", "status": "failure"}) return err } sourceTarFileName, err = s.getSourceCodeTarFile(re) if err != nil { return fmt.Errorf("create source code tar file error:%s", err.Error()) } // remove source cache tar file defer func() { os.Remove(sourceTarFileName) }() } re.Logger.Info(util.Translation("make code package success"), map[string]string{"step": "build-exector"}) logrus.Infof("package code for building service %s version %s successful, take time %s", re.ServiceID, re.DeployVersion, time.Now().Sub(start)) name := fmt.Sprintf("%s-%s", re.ServiceID, re.DeployVersion) namespace := re.RbdNamespace job := corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: namespace, Labels: map[string]string{ "service": re.ServiceID, "job": "codebuild", }, }, } envs := []corev1.EnvVar{ {Name: "SLUG_VERSION", Value: re.DeployVersion}, {Name: "SERVICE_ID", Value: re.ServiceID}, {Name: "TENANT_ID", Value: re.TenantID}, {Name: "CODE_COMMIT_HASH", Value: re.Commit.Hash}, {Name: "CODE_COMMIT_USER", Value: re.Commit.User}, {Name: "CODE_COMMIT_MESSAGE", Value: re.Commit.Message}, {Name: "LANGUAGE", Value: re.Lang.String()}, } // Add LANG_GOODRAIN_ME from environment variable if set if langGoodRainMe := os.Getenv("LANG_GOODRAIN_ME"); langGoodRainMe != "" { envs = append(envs, corev1.EnvVar{Name: "LANG_GOODRAIN_ME", Value: langGoodRainMe}) } if re.ServerType == "oss" { envs = append(envs, corev1.EnvVar{Name: "PACKAGE_DOWNLOAD_URL", Value: re.RepositoryURL}) envs = append(envs, corev1.EnvVar{Name: "PACKAGE_DOWNLOAD_USER", Value: re.CodeSouceInfo.User}) envs = append(envs, corev1.EnvVar{Name: "PACKAGE_DOWNLOAD_PASS", Value: re.CodeSouceInfo.Password}) } var mavenSettingName string var buildNoCache bool for k, v := range re.BuildEnvs { if k == "MAVEN_SETTING_NAME" { mavenSettingName = v continue } if k == "PROCFILE" { if !strings.HasPrefix(v, "web:") { v = "web: " + v } else if v[4] != ' ' { v = "web: " + v[4:] } } if k == "RUNTIMES" || k == "GOVERSION" { var lang string switch re.Lang { case code.Python: lang = "python" case code.Golang: lang = "golang" case code.JavaJar, code.JavaMaven, code.JaveWar, code.Gradle: lang = "openJDK" case code.PHP: lang = "php" case code.Nodejs, code.NodeJSStatic: lang = "node" default: // Handle combined language types (e.g., "Node.js,static") if strings.Contains(string(re.Lang), string(code.Nodejs)) { lang = "node" } } version, err := db.GetManager().LongVersionDao().GetVersionByLanguageAndVersion(lang, v) if err != nil { return err } if !version.System { envs = append(envs, corev1.EnvVar{Name: "CUSTOMIZE_RUNTIMES", Value: "true"}) envs = append(envs, corev1.EnvVar{Name: "CUSTOMIZE_RUNTIMES_URL", Value: fmt.Sprintf("rbd-api-websocket:6060/lg_pack_operate/download/%v/%v", lang, v)}) } } if k == "RUNTIMES_MAVEN" { version, err := db.GetManager().LongVersionDao().GetVersionByLanguageAndVersion("maven", v) if err != nil { return err } if !version.System { envs = append(envs, corev1.EnvVar{Name: "CUSTOMIZE_RUNTIMES_MAVEN", Value: "true"}) envs = append(envs, corev1.EnvVar{Name: "CUSTOMIZE_RUNTIMES_MAVEN_URL", Value: fmt.Sprintf("rbd-api-websocket:6060/lg_pack_operate/download/maven/%v", v)}) } } if k == "RUNTIMES_SERVER" { language := "java_server" if re.Lang == code.Static || re.Lang == code.PHP { language = "web_runtime" } version, err := db.GetManager().LongVersionDao().GetVersionByLanguageAndVersion(language, v) if err != nil { return err } if !version.System { envs = append(envs, corev1.EnvVar{Name: "CUSTOMIZE_RUNTIMES_SERVER", Value: "true"}) envs = append(envs, corev1.EnvVar{Name: "CUSTOMIZE_RUNTIMES_SERVER_URL", Value: fmt.Sprintf("rbd-api-websocket:6060/lg_pack_operate/download/%v/%v", language, v)}) } } envs = append(envs, corev1.EnvVar{Name: k, Value: v}) if k == "PROC_ENV" { var mapdata = make(map[string]interface{}) if err := json.Unmarshal([]byte(v), &mapdata); err == nil { if runtime, ok := mapdata["runtimes"]; ok { envs = append(envs, corev1.EnvVar{Name: "RUNTIME", Value: runtime.(string)}) } } } if k == "NO_CACHE" && strings.EqualFold(strings.TrimSpace(v), "true") { buildNoCache = true } } podSpec := newSlugBuildPodSpec(re.Arch, os.Getenv("HOST_IP"), re.CacheMode) logrus.Debugf("request is: %+v", re) volumes, mounts := s.createVolumeAndMount(re, sourceTarFileName, buildNoCache) podSpec.Volumes = volumes container := corev1.Container{ Name: name, Image: builder.BUILDERIMAGENAME, Stdin: true, StdinOnce: true, Env: envs, Args: []string{"local"}, } container.VolumeMounts = mounts //set maven setting var mavenSettingConfigName string if mavenSettingName != "" && re.Lang.String() == code.JavaMaven.String() { if setting := jobc.GetJobController().GetLanguageBuildSetting(re.Ctx, code.JavaMaven, mavenSettingName); setting != "" { mavenSettingConfigName = setting } } if mavenSettingConfigName == "" { if settingName := jobc.GetJobController().GetDefaultLanguageBuildSetting(re.Ctx, code.JavaMaven); settingName != "" { mavenSettingConfigName = settingName } else { logrus.Warnf("maven setting config %s not found", mavenSettingName) } } if mavenSettingConfigName != "" { podSpec.Volumes = append(podSpec.Volumes, corev1.Volume{ Name: "mavensetting", VolumeSource: corev1.VolumeSource{ ConfigMap: &corev1.ConfigMapVolumeSource{ LocalObjectReference: corev1.LocalObjectReference{ Name: mavenSettingConfigName, }, }, }, }) mountPath := "/etc/maven/setting.xml" container.VolumeMounts = append(container.VolumeMounts, corev1.VolumeMount{ MountPath: mountPath, SubPath: "mavensetting", Name: "mavensetting", }) container.Env = append(container.Env, corev1.EnvVar{ Name: "MAVEN_SETTINGS_PATH", Value: mountPath, }) container.Env = append(container.Env, corev1.EnvVar{ Name: "MAVEN_MIRROR_DISABLE", Value: "true", }) logrus.Infof("set maven setting config %s success", mavenSettingName) } podSpec.Containers = append(podSpec.Containers, container) podSpec.HostAliases = append(podSpec.HostAliases, NormalizeHostAliases(re.HostAlias)...) job.Spec = podSpec s.setImagePullSecretsForPod(&job) writer := re.Logger.GetWriter("builder", "info") reChan := channels.NewRingChannel(10) ctx, cancel := context.WithCancel(context.Background()) defer cancel() // Get builder image at build time if err := s.re.ImageClient.ImagesPullAndPush(builder.BUILDERIMAGENAME, builder.GetBuilderImage(s.re.BRVersion), "", "", re.Logger); err != nil { failMsg := fmt.Sprintf("%s: %s", util.Translation("Pull builder image failed"), builder.BUILDERIMAGENAME) re.Logger.Error(failMsg, map[string]string{"step": "builder-exector", "status": "failure"}) return err } logrus.Debugf("create job[name: %s; namespace: %s]", job.Name, job.Namespace) err := jobc.GetJobController().ExecJob(ctx, &job, writer, reChan) if err != nil { logrus.Errorf("create new job:%s failed: %s", name, err.Error()) re.Logger.Error(util.Translation("Create build job failed"), map[string]string{"step": "builder-exector", "status": "failure"}) return err } re.Logger.Info(util.Translation("create build code job success"), map[string]string{"step": "build-exector"}) logrus.Infof("create build job %s for service %s build version %s", job.Name, re.ServiceID, re.DeployVersion) // delete job after complete defer jobc.GetJobController().DeleteJob(job.Name) return s.waitingComplete(re, reChan) } func (s *slugBuild) waitingComplete(re *Request, reChan *channels.RingChannel) (err error) { var logComplete = false var jobComplete = false timeout := time.NewTimer(time.Minute * 60) for { select { case <-timeout.C: return fmt.Errorf("build time out (more than 60 minute)") case jobStatus := <-reChan.Out(): status := jobStatus.(string) switch status { case "complete": jobComplete = true if logComplete { return nil } re.Logger.Info(util.Translation("build code job exec completed"), map[string]string{"step": "build-exector"}) case "failed": jobComplete = true err = fmt.Errorf("build code job exec failure") if logComplete { return err } re.Logger.Info(util.Translation("build code job exec failed"), map[string]string{"step": "build-exector"}) case "cancel": jobComplete = true err = fmt.Errorf("build code job is canceled") if logComplete { return err } case "logcomplete": logComplete = true if jobComplete { return err } } } } } func (s *slugBuild) setImagePullSecretsForPod(pod *corev1.Pod) { imagePullSecretName := os.Getenv("IMAGE_PULL_SECRET") if imagePullSecretName == "" { return } pod.Spec.ImagePullSecrets = []corev1.LocalObjectReference{ {Name: imagePullSecretName}, } } // ErrorBuild build error type ErrorBuild struct { Code int } func (e *ErrorBuild) Error() string { return fmt.Sprintf("Run build return %d", e.Code) } func (s *slugBuild) HandleNodeJsDir(re *Request) error { // For slug builds, use language type to determine if static file handling is needed. // RUNTIME_TYPE is set by framework detection for CNB builds and should not be used here. if re.Lang == code.NodeJSStatic { if ok, _ := util.FileExists(path.Join(re.SourceDir, "nodestatic.json")); ok { if err := os.RemoveAll(path.Join(re.SourceDir, "nodestatic.json")); err != nil { logrus.Error("remove nodestatic json error:", err) return err } } filePtr, err := os.Create(path.Join(re.SourceDir, "nodestatic.json")) if err != nil { logrus.Error("create nodestatic json error:", err) return err } defer filePtr.Close() if dir, ok := re.BuildEnvs["DIST_DIR"]; !ok || dir == "" { re.BuildEnvs["DIST_DIR"] = "dist" } _, err = io.WriteString(filePtr, fmt.Sprintf("{\"path\":\"%v\"}", re.BuildEnvs["DIST_DIR"])) if err != nil { logrus.Error("write nodestatic json error:", err) return err } } // Handle pnpm package manager if re.BuildEnvs["PACKAGE_TOOL"] == "pnpm" { // Ensure pnpm-lock.yaml exists if ok, _ := util.FileExists(path.Join(re.SourceDir, "pnpm-lock.yaml")); !ok { filePtr, err := os.Create(path.Join(re.SourceDir, "pnpm-lock.yaml")) if err != nil { logrus.Error("create pnpm-lock.yaml error:", err) return err } defer filePtr.Close() } // Remove conflicting lock files if ok, _ := util.FileExists(path.Join(re.SourceDir, "package-lock.json")); ok { if err := os.RemoveAll(path.Join(re.SourceDir, "package-lock.json")); err != nil { logrus.Error("remove package-lock.json error:", err) return err } } if ok, _ := util.FileExists(path.Join(re.SourceDir, "yarn.lock")); ok { if err := os.RemoveAll(path.Join(re.SourceDir, "yarn.lock")); err != nil { logrus.Error("remove yarn.lock error:", err) return err } } } if re.BuildEnvs["PACKAGE_TOOL"] == "yarn" { if ok, _ := util.FileExists(path.Join(re.SourceDir, "yarn.lock")); !ok { filePtr, err := os.Create(path.Join(re.SourceDir, "yarn.lock")) if err != nil { logrus.Error("create nodestatic json error:", err) return err } defer filePtr.Close() } if ok, _ := util.FileExists(path.Join(re.SourceDir, "package-lock.json")); ok { if err := os.RemoveAll(path.Join(re.SourceDir, "package-lock.json")); err != nil { logrus.Error("remove package-lock json error:", err) return err } } } if re.BuildEnvs["PACKAGE_TOOL"] == "npm" { if ok, _ := util.FileExists(path.Join(re.SourceDir, "package-lock.json")); !ok { filePtr, err := os.Create(path.Join(re.SourceDir, "package-lock.json")) if err != nil { logrus.Error("create package-lock json error:", err) return err } defer filePtr.Close() } if ok, _ := util.FileExists(path.Join(re.SourceDir, "yarn.lock")); ok { if err := os.RemoveAll(path.Join(re.SourceDir, "yarn.lock")); err != nil { logrus.Error("remove yarn.lock error:", err) return err } } } // Inject .npmrc from UI configuration if npmrcContent := re.BuildEnvs["NPMRC_CONTENT"]; npmrcContent != "" { npmrcPath := path.Join(re.SourceDir, ".npmrc") // Backup existing file if ok, _ := util.FileExists(npmrcPath); ok { if err := os.Rename(npmrcPath, npmrcPath+".bak"); err != nil { logrus.Warn("backup .npmrc error:", err) } } if err := os.WriteFile(npmrcPath, []byte(npmrcContent), 0644); err != nil { logrus.Error("write .npmrc error:", err) return err } } // Inject .yarnrc.yml from UI configuration if yarnrcContent := re.BuildEnvs["YARNRC_CONTENT"]; yarnrcContent != "" { yarnrcPath := path.Join(re.SourceDir, ".yarnrc.yml") if err := os.WriteFile(yarnrcPath, []byte(yarnrcContent), 0644); err != nil { logrus.Error("write .yarnrc.yml error:", err) return err } } return nil }