make:*, plugin:add and new inserted toolchain go1.27.0 into go.mod and go.work whenever that exact line was missing. A file that already named another toolchain (go mod tidy and go work use write the running one) ended up with two toolchain lines, and Go refused to load it with "repeated toolchain statement". ensureToolchain now keeps an existing line, replaces it only when it names an older toolchain, and inserts one only when there is none.
655 lines
18 KiB
Go
655 lines
18 KiB
Go
package build
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"embed"
|
|
"fmt"
|
|
"go/ast"
|
|
"go/format"
|
|
"go/parser"
|
|
"go/token"
|
|
"go/version"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"text/template"
|
|
|
|
"git.golem15.com/golem15/summercms/modules/bonfire"
|
|
)
|
|
|
|
//go:embed stubs/*.tmpl
|
|
var stubFS embed.FS
|
|
|
|
var stubTmpl = template.Must(template.ParseFS(stubFS, "stubs/*.tmpl"))
|
|
|
|
var pluginLeaves = []string{
|
|
"models",
|
|
"classes",
|
|
"controllers",
|
|
"console",
|
|
"jobs",
|
|
"middleware",
|
|
"updates",
|
|
}
|
|
|
|
type stubData struct {
|
|
Package string
|
|
ID string
|
|
Module string
|
|
Framework string
|
|
GoVersion string
|
|
Toolchain string
|
|
Replace string
|
|
// StarterRoute renders routes.go with a GET / route (summer new).
|
|
StarterRoute bool
|
|
}
|
|
|
|
// pluginOptions tunes the scaffold makePlugin writes. The zero value is the
|
|
// plain make:plugin output.
|
|
type pluginOptions struct {
|
|
StarterRoute bool
|
|
}
|
|
|
|
const (
|
|
frameworkModule = "git.golem15.com/golem15/summercms"
|
|
goVersionLine = "go 1.27.0"
|
|
toolchainLine = "toolchain go1.27.0"
|
|
)
|
|
|
|
// MakePlugin scaffolds a compiling plugin module under plugins/<vendor>/<name>
|
|
// for the ID <vendor>.<name>; its module path is the application module
|
|
// followed by /plugins/<vendor>/<name>, and its Go package is <name>.
|
|
func MakePlugin(ctx context.Context, startDir, id string) (string, error) {
|
|
return makePlugin(ctx, startDir, id, pluginOptions{})
|
|
}
|
|
|
|
func makePlugin(ctx context.Context, startDir, id string, opts pluginOptions) (string, error) {
|
|
if err := ValidatePluginID(id); err != nil {
|
|
return "", bonfire.WithHint(fmt.Errorf("build: %w", err),
|
|
"A plugin ID is the vendor and the plugin name in lower case letters and digits, joined by a dot.",
|
|
"{bin} make:plugin acme.blog")
|
|
}
|
|
appDir, err := findAppDir(startDir)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
vendor, name := pluginVendor(id), pluginName(id)
|
|
pluginDir := filepath.Join(appDir, "plugins", vendor, name)
|
|
if !underRoot(appDir, pluginDir) {
|
|
return "", fmt.Errorf("build: plugin path %q escapes the app root", pluginDir)
|
|
}
|
|
addArg := "plugins/" + vendor + "/" + name
|
|
if _, err := os.Stat(pluginDir); err == nil {
|
|
return "", bonfire.WithHint(fmt.Errorf("build: plugin directory %s already exists", pluginDir),
|
|
"summer make:plugin creates a new directory under plugins/ and never overwrites one; register an existing plugin with plugin:add.",
|
|
"{bin} plugin:add "+addArg)
|
|
} else if !os.IsNotExist(err) {
|
|
return "", fmt.Errorf("build: stat %s: %w", pluginDir, err)
|
|
}
|
|
// A legacy plugins/<vendor> module would swallow the new directory, and a
|
|
// legacy plugins/<name> with this ID is the same plugin.
|
|
vendorDir := filepath.Join(appDir, "plugins", vendor)
|
|
if _, err := os.Stat(filepath.Join(vendorDir, "go.mod")); err == nil {
|
|
return "", bonfire.WithHint(fmt.Errorf("build: %s is a plugin module, not a vendor directory", vendorDir),
|
|
"summer make:plugin writes plugins/<vendor>/<name>; plugins/"+vendor+" already holds a plugin in the older plugins/<name> layout. Move that plugin first, or choose another vendor.",
|
|
"{bin} make:plugin <vendor>."+name)
|
|
}
|
|
legacyDir := filepath.Join(appDir, "plugins", name)
|
|
if got, err := pluginIDFromGo(filepath.Join(legacyDir, "plugin.go")); err == nil && got == id {
|
|
return "", bonfire.WithHint(fmt.Errorf("build: plugin %s already exists in %s", id, legacyDir),
|
|
"summer make:plugin never creates a second copy of a plugin; register the existing one with plugin:add.",
|
|
"{bin} plugin:add plugins/"+name)
|
|
}
|
|
|
|
m, err := LoadManifest(filepath.Join(appDir, manifestName))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
modPath := m.Module + "/" + addArg
|
|
if err := validateModulePath(modPath); err != nil {
|
|
return "", fmt.Errorf("build: plugin module: %w", err)
|
|
}
|
|
|
|
replace, err := frameworkReplaceFor(pluginDir, appDir)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
data := stubData{
|
|
Package: name,
|
|
ID: id,
|
|
Module: modPath,
|
|
Framework: frameworkModule,
|
|
GoVersion: goVersionLine,
|
|
Toolchain: toolchainLine,
|
|
Replace: replace,
|
|
|
|
StarterRoute: opts.StarterRoute,
|
|
}
|
|
if err := writePluginScaffold(pluginDir, data); err != nil {
|
|
return "", err
|
|
}
|
|
if err := runGo(ctx, pluginDir, "mod", "tidy"); err != nil {
|
|
return "", err
|
|
}
|
|
if err := ensureToolchain(filepath.Join(pluginDir, "go.mod")); err != nil {
|
|
return "", err
|
|
}
|
|
return pluginDir, nil
|
|
}
|
|
|
|
// AddPlugin registers a local plugin module in summer.yaml, go.work and the app go.mod.
|
|
func AddPlugin(ctx context.Context, startDir, pluginDir string) error {
|
|
appDir, err := findAppDir(startDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if pluginDir == "" {
|
|
return fmt.Errorf("build: plugin directory is empty")
|
|
}
|
|
if !filepath.IsAbs(pluginDir) {
|
|
pluginDir = filepath.Join(startDir, pluginDir)
|
|
}
|
|
pluginDir, err = filepath.Abs(pluginDir)
|
|
if err != nil {
|
|
return fmt.Errorf("build: resolve plugin directory: %w", err)
|
|
}
|
|
if !underRoot(appDir, pluginDir) {
|
|
return fmt.Errorf("build: plugin directory %q is outside the app root", pluginDir)
|
|
}
|
|
|
|
modPath, err := readModulePath(filepath.Join(pluginDir, "go.mod"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := validateModulePath(modPath); err != nil {
|
|
return fmt.Errorf("build: plugin module: %w", err)
|
|
}
|
|
id, err := pluginIDFromGo(filepath.Join(pluginDir, "plugin.go"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := ValidatePluginID(id); err != nil {
|
|
return fmt.Errorf("build: %w", err)
|
|
}
|
|
|
|
manifestPath := filepath.Join(appDir, manifestName)
|
|
m, err := LoadManifest(manifestPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
already := false
|
|
for _, p := range m.Plugins {
|
|
if p.ID == id && p.Module == modPath {
|
|
already = true
|
|
continue
|
|
}
|
|
if p.ID == id {
|
|
return fmt.Errorf("build: plugin id %q is already registered as %s", id, p.Module)
|
|
}
|
|
if p.Module == modPath {
|
|
return fmt.Errorf("build: plugin module %q is already registered as %s", modPath, p.ID)
|
|
}
|
|
}
|
|
if !already {
|
|
m.Plugins = append(m.Plugins, Plugin{ID: id, Module: modPath})
|
|
}
|
|
|
|
if err := editAppModule(ctx, appDir, pluginDir, modPath); err != nil {
|
|
return err
|
|
}
|
|
if err := ensureGoWork(ctx, appDir, m, pluginDir); err != nil {
|
|
return err
|
|
}
|
|
if already {
|
|
return nil
|
|
}
|
|
return SaveManifest(manifestPath, m)
|
|
}
|
|
|
|
func pluginName(id string) string {
|
|
_, name, _ := strings.Cut(id, ".")
|
|
return name
|
|
}
|
|
|
|
// pluginVendor returns the vendor part of a plugin ID, before the dot.
|
|
func pluginVendor(id string) string {
|
|
vendor, _, _ := strings.Cut(id, ".")
|
|
return vendor
|
|
}
|
|
|
|
// pluginDirCandidates lists the default directories of a plugin that
|
|
// summer.yaml does not place, in lookup order: plugins/<vendor>/<name>, the
|
|
// layout make:plugin writes, then the older plugins/<name>.
|
|
func pluginDirCandidates(appDir, id string) []string {
|
|
return []string{
|
|
filepath.Join(appDir, "plugins", pluginVendor(id), pluginName(id)),
|
|
filepath.Join(appDir, "plugins", pluginName(id)),
|
|
}
|
|
}
|
|
|
|
// defaultPluginDir returns the first of pluginDirCandidates that is an
|
|
// existing directory, or "" when none is. A candidate outside the app root is
|
|
// an error.
|
|
func defaultPluginDir(appDir, id string) (string, error) {
|
|
for _, dir := range pluginDirCandidates(appDir, id) {
|
|
if !underRoot(appDir, dir) {
|
|
return "", fmt.Errorf("build: plugin path %q escapes the app root", dir)
|
|
}
|
|
st, err := os.Stat(dir)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
continue
|
|
}
|
|
return "", fmt.Errorf("build: stat %s: %w", dir, err)
|
|
}
|
|
if st.IsDir() {
|
|
return dir, nil
|
|
}
|
|
}
|
|
return "", nil
|
|
}
|
|
|
|
func writePluginScaffold(pluginDir string, data stubData) error {
|
|
if err := os.MkdirAll(pluginDir, 0o755); err != nil {
|
|
return fmt.Errorf("build: create plugin dir: %w", err)
|
|
}
|
|
pluginSrc, err := renderStub("plugin.go", data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := writeFile(filepath.Join(pluginDir, "plugin.go"), pluginSrc); err != nil {
|
|
return err
|
|
}
|
|
routesSrc, err := renderStub("routes.go", data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := writeFile(filepath.Join(pluginDir, "routes.go"), routesSrc); err != nil {
|
|
return err
|
|
}
|
|
for _, leaf := range pluginLeaves {
|
|
src, err := renderStub(leaf+"/doc.go", data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := writeFile(filepath.Join(pluginDir, leaf, "doc.go"), src); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for _, asset := range pluginAssets {
|
|
if err := writeFile(filepath.Join(pluginDir, filepath.FromSlash(asset.rel)), []byte(asset.body)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if err := writeRegistry(pluginDir, emptyRegistry(data.Package, data.Module)); err != nil {
|
|
return err
|
|
}
|
|
modSrc, err := renderStub("go.mod", data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return writeFile(filepath.Join(pluginDir, "go.mod"), modSrc)
|
|
}
|
|
|
|
type pluginAsset struct {
|
|
rel string
|
|
body string
|
|
}
|
|
|
|
var pluginAssets = []pluginAsset{
|
|
{rel: "config/config.yaml", body: "{}\n"},
|
|
{rel: "lang/en/lang.yaml", body: "{}\n"},
|
|
{rel: "views/mail/welcome.htm", body: "subject = \"Welcome\"\ndescription = \"Placeholder mail template\"\n==\nHello.\n"},
|
|
}
|
|
|
|
func renderStub(name string, data any) ([]byte, error) {
|
|
var buf bytes.Buffer
|
|
if err := stubTmpl.ExecuteTemplate(&buf, name, data); err != nil {
|
|
return nil, fmt.Errorf("build: render stub %s: %w", name, err)
|
|
}
|
|
raw := buf.Bytes()
|
|
if !strings.HasSuffix(name, ".go") {
|
|
return raw, nil
|
|
}
|
|
formatted, err := format.Source(raw)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("build: format stub %s: %w\n%s", name, err, raw)
|
|
}
|
|
return formatted, nil
|
|
}
|
|
|
|
func writeFile(path string, data []byte) error {
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
return fmt.Errorf("build: mkdir %s: %w", filepath.Dir(path), err)
|
|
}
|
|
if err := os.WriteFile(path, data, 0o644); err != nil {
|
|
return fmt.Errorf("build: write %s: %w", path, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func frameworkReplaceFor(pluginDir, appDir string) (string, error) {
|
|
_, replaces, err := parseGoMod(filepath.Join(appDir, "go.mod"))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
appReplace, ok := replaces[frameworkModule]
|
|
if !ok || appReplace == "" {
|
|
return "", nil
|
|
}
|
|
target := appReplace
|
|
if filepath.IsAbs(appReplace) {
|
|
return filepath.ToSlash(appReplace), nil
|
|
}
|
|
target = filepath.Join(appDir, target)
|
|
target, err = filepath.Abs(target)
|
|
if err != nil {
|
|
return "", fmt.Errorf("build: plugin replace: %w", err)
|
|
}
|
|
rel, err := filepath.Rel(pluginDir, target)
|
|
if err != nil {
|
|
return filepath.ToSlash(target), nil
|
|
}
|
|
return filepath.ToSlash(rel), nil
|
|
}
|
|
|
|
func pluginIDFromGo(path string) (string, error) {
|
|
fset := token.NewFileSet()
|
|
file, err := parser.ParseFile(fset, path, nil, 0)
|
|
if err != nil {
|
|
return "", fmt.Errorf("build: parse %s: %w", path, err)
|
|
}
|
|
var id string
|
|
ast.Inspect(file, func(n ast.Node) bool {
|
|
fn, ok := n.(*ast.FuncDecl)
|
|
if !ok || fn.Name == nil || fn.Name.Name != "ID" || fn.Recv == nil || fn.Body == nil {
|
|
return true
|
|
}
|
|
for _, stmt := range fn.Body.List {
|
|
ret, ok := stmt.(*ast.ReturnStmt)
|
|
if !ok || len(ret.Results) != 1 {
|
|
continue
|
|
}
|
|
lit, ok := ret.Results[0].(*ast.BasicLit)
|
|
if !ok || lit.Kind != token.STRING {
|
|
continue
|
|
}
|
|
s, err := strconv.Unquote(lit.Value)
|
|
if err == nil {
|
|
id = s
|
|
}
|
|
}
|
|
return true
|
|
})
|
|
if id == "" {
|
|
return "", fmt.Errorf("build: %s does not return a plugin id", path)
|
|
}
|
|
return id, nil
|
|
}
|
|
|
|
func editAppModule(ctx context.Context, appDir, pluginDir, modPath string) error {
|
|
if err := runGo(ctx, appDir, "mod", "edit", "-require="+modPath+"@v0.0.0"); err != nil {
|
|
return err
|
|
}
|
|
rel, err := relModulePath(appDir, pluginDir)
|
|
if err != nil {
|
|
return fmt.Errorf("build: plugin replace: %w", err)
|
|
}
|
|
if err := runGo(ctx, appDir, "mod", "edit", "-replace="+modPath+"="+rel); err != nil {
|
|
return err
|
|
}
|
|
return ensureToolchain(filepath.Join(appDir, "go.mod"))
|
|
}
|
|
|
|
func ensureGoWork(ctx context.Context, appDir string, m Manifest, pluginDir string) error {
|
|
gw := findGoWork(appDir)
|
|
if gw == "" {
|
|
if err := writeGoWork(filepath.Join(appDir, "go.work")); err != nil {
|
|
return err
|
|
}
|
|
gw = filepath.Join(appDir, "go.work")
|
|
for _, p := range m.Plugins {
|
|
if dir := localModuleDir(appDir, p.Module); dir != "" && dir != pluginDir {
|
|
if err := runGoWorkUse(ctx, gw, dir); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return runGoWorkUse(ctx, gw, pluginDir)
|
|
}
|
|
|
|
func writeGoWork(path string) error {
|
|
body := goVersionLine + "\n\n" + toolchainLine + "\n\nuse (\n\t.\n)\n"
|
|
if err := os.WriteFile(path, []byte(body), 0o644); err != nil {
|
|
return fmt.Errorf("build: write go.work: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// relModulePath returns target relative to base as a slash-separated local
|
|
// module path: "./x", "../x", "." or "..". Go reads a replace or use path
|
|
// without a ./ or ../ prefix as a module path, so the prefix is required.
|
|
func relModulePath(base, target string) (string, error) {
|
|
rel, err := filepath.Rel(base, target)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
rel = filepath.ToSlash(rel)
|
|
if rel != "." && rel != ".." && !strings.HasPrefix(rel, "../") && !filepath.IsAbs(rel) && !strings.HasPrefix(rel, "./") {
|
|
rel = "./" + rel
|
|
}
|
|
return rel, nil
|
|
}
|
|
|
|
// runGoWorkUse adds dir to goWork with a path relative to the go.work
|
|
// directory, so a committed go.work never carries a machine-specific
|
|
// absolute path.
|
|
func runGoWorkUse(ctx context.Context, goWork, dir string) error {
|
|
workDir := filepath.Dir(goWork)
|
|
arg := dir
|
|
if rel, err := relModulePath(workDir, dir); err == nil {
|
|
arg = rel
|
|
}
|
|
cmd := exec.CommandContext(ctx, "go", "work", "use", arg)
|
|
cmd.Dir = workDir
|
|
applyGoWorkFile(cmd, goWork)
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return fmt.Errorf("build: go work use: %w\n%s", err, out)
|
|
}
|
|
return ensureToolchain(goWork)
|
|
}
|
|
|
|
func localModuleDir(appDir, module string) string {
|
|
_, replaces, err := parseGoMod(filepath.Join(appDir, "go.mod"))
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
r, ok := replaces[module]
|
|
if !ok || r == "" {
|
|
return ""
|
|
}
|
|
if filepath.IsAbs(r) {
|
|
return r
|
|
}
|
|
return filepath.Clean(filepath.Join(appDir, r))
|
|
}
|
|
|
|
func readModulePath(path string) (string, error) {
|
|
module, _, err := parseGoMod(path)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if module == "" {
|
|
return "", fmt.Errorf("build: %s has no module path", path)
|
|
}
|
|
return module, nil
|
|
}
|
|
|
|
func parseGoMod(path string) (string, map[string]string, error) {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return "", nil, fmt.Errorf("build: read %s: %w", path, err)
|
|
}
|
|
module := ""
|
|
replaces := make(map[string]string)
|
|
inReplace := false
|
|
for _, raw := range strings.Split(string(data), "\n") {
|
|
line := strings.TrimSpace(raw)
|
|
if i := strings.Index(line, "//"); i >= 0 {
|
|
line = strings.TrimSpace(line[:i])
|
|
}
|
|
if line == "" {
|
|
continue
|
|
}
|
|
if module == "" && strings.HasPrefix(line, "module ") {
|
|
module = strings.TrimSpace(strings.TrimPrefix(line, "module "))
|
|
continue
|
|
}
|
|
if line == "replace (" {
|
|
inReplace = true
|
|
continue
|
|
}
|
|
if inReplace {
|
|
if line == ")" {
|
|
inReplace = false
|
|
continue
|
|
}
|
|
if old, new, ok := splitReplace(line); ok {
|
|
replaces[old] = new
|
|
}
|
|
continue
|
|
}
|
|
if strings.HasPrefix(line, "replace ") {
|
|
if old, new, ok := splitReplace(strings.TrimPrefix(line, "replace ")); ok {
|
|
replaces[old] = new
|
|
}
|
|
}
|
|
}
|
|
return module, replaces, nil
|
|
}
|
|
|
|
func splitReplace(s string) (old, new string, ok bool) {
|
|
parts := strings.SplitN(s, "=>", 2)
|
|
if len(parts) != 2 {
|
|
return "", "", false
|
|
}
|
|
oldFields := strings.Fields(strings.TrimSpace(parts[0]))
|
|
newFields := strings.Fields(strings.TrimSpace(parts[1]))
|
|
if len(oldFields) == 0 || len(newFields) == 0 {
|
|
return "", "", false
|
|
}
|
|
return oldFields[0], newFields[0], true
|
|
}
|
|
|
|
func ensureToolchain(path string) error {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return fmt.Errorf("build: read %s: %w", path, err)
|
|
}
|
|
if bytes.Contains(data, []byte(toolchainLine)) {
|
|
return nil
|
|
}
|
|
lines := strings.Split(string(data), "\n")
|
|
// A file may carry its own toolchain line (go mod tidy and go work use
|
|
// write the running toolchain). Go refuses a second one, so keep an
|
|
// existing line unless it names an older toolchain than ours.
|
|
want := strings.TrimSpace(strings.TrimPrefix(toolchainLine, "toolchain"))
|
|
for i, line := range lines {
|
|
fields := strings.Fields(line)
|
|
if len(fields) != 2 || fields[0] != "toolchain" {
|
|
continue
|
|
}
|
|
if version.Compare(fields[1], want) >= 0 {
|
|
return nil
|
|
}
|
|
lines[i] = toolchainLine
|
|
if err := os.WriteFile(path, []byte(strings.Join(lines, "\n")), 0o644); err != nil {
|
|
return fmt.Errorf("build: write %s: %w", path, err)
|
|
}
|
|
return nil
|
|
}
|
|
out := make([]string, 0, len(lines)+2)
|
|
inserted := false
|
|
for _, line := range lines {
|
|
out = append(out, line)
|
|
if !inserted && strings.HasPrefix(strings.TrimSpace(line), "go ") {
|
|
out = append(out, "", toolchainLine)
|
|
inserted = true
|
|
}
|
|
}
|
|
if !inserted {
|
|
out = append([]string{toolchainLine, ""}, out...)
|
|
}
|
|
if err := os.WriteFile(path, []byte(strings.Join(out, "\n")), 0o644); err != nil {
|
|
return fmt.Errorf("build: write %s: %w", path, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func underRoot(root, path string) bool {
|
|
rel, err := filepath.Rel(root, path)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
if rel == ".." || strings.HasPrefix(rel, ".."+string(os.PathSeparator)) {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func findGoWork(start string) string {
|
|
dir := start
|
|
for {
|
|
candidate := filepath.Join(dir, "go.work")
|
|
if st, err := os.Stat(candidate); err == nil && !st.IsDir() {
|
|
return candidate
|
|
}
|
|
parent := filepath.Dir(dir)
|
|
if parent == dir {
|
|
return ""
|
|
}
|
|
dir = parent
|
|
}
|
|
}
|
|
|
|
func runGo(ctx context.Context, dir string, args ...string) error {
|
|
cmd := exec.CommandContext(ctx, "go", args...)
|
|
cmd.Dir = dir
|
|
applyGoWork(cmd, dir)
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return fmt.Errorf("build: go %s: %w\n%s", strings.Join(args, " "), err, out)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func applyGoWork(cmd *exec.Cmd, dir string) {
|
|
if gw := findGoWork(dir); gw != "" {
|
|
applyGoWorkFile(cmd, gw)
|
|
return
|
|
}
|
|
applyGoWorkFile(cmd, "off")
|
|
}
|
|
|
|
func applyGoWorkFile(cmd *exec.Cmd, goWork string) {
|
|
env := os.Environ()
|
|
filtered := make([]string, 0, len(env)+2)
|
|
for _, e := range env {
|
|
if strings.HasPrefix(e, "GOWORK=") || strings.HasPrefix(e, "GOFLAGS=") {
|
|
continue
|
|
}
|
|
filtered = append(filtered, e)
|
|
}
|
|
filtered = append(filtered, "GOWORK="+goWork)
|
|
if goWork == "off" {
|
|
filtered = append(filtered, "GOFLAGS=-mod=mod")
|
|
}
|
|
cmd.Env = filtered
|
|
}
|