feat(01-03): add make:plugin and plugin:add scaffolding
- Scaffold a compiling vendor.plugin module with go.mod, plugin.go and config/ - Register the module once in summer.yaml, go.work and the app go.mod - Keep summer build on the same generate-and-compile path and print elapsed time
This commit is contained in:
@@ -13,80 +13,8 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"github.com/knadh/koanf/parsers/yaml"
|
||||
)
|
||||
|
||||
const manifestName = "summer.yaml"
|
||||
|
||||
// Manifest is the ordered app plugin list used to generate imports.
|
||||
type Manifest struct {
|
||||
Module string
|
||||
Binary string
|
||||
Plugins []Plugin
|
||||
}
|
||||
|
||||
// Plugin is one compiled plugin module in manifest order.
|
||||
type Plugin struct {
|
||||
ID string
|
||||
Module string
|
||||
}
|
||||
|
||||
// LoadManifest reads and validates summer.yaml at path.
|
||||
func LoadManifest(path string) (Manifest, error) {
|
||||
raw, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return Manifest{}, fmt.Errorf("build: read %s: %w", path, err)
|
||||
}
|
||||
tree, err := yaml.Parser().Unmarshal(raw)
|
||||
if err != nil {
|
||||
return Manifest{}, fmt.Errorf("build: parse %s: %w", path, err)
|
||||
}
|
||||
m := Manifest{
|
||||
Module: strings.TrimSpace(asString(tree["module"])),
|
||||
Binary: strings.TrimSpace(asString(tree["binary"])),
|
||||
}
|
||||
if err := validateModulePath(m.Module); err != nil {
|
||||
return Manifest{}, fmt.Errorf("build: module: %w", err)
|
||||
}
|
||||
if err := validateBinary(m.Binary); err != nil {
|
||||
return Manifest{}, fmt.Errorf("build: binary: %w", err)
|
||||
}
|
||||
|
||||
seenID := make(map[string]struct{})
|
||||
seenMod := make(map[string]struct{})
|
||||
for i, item := range asSlice(tree["plugins"]) {
|
||||
entry := asMap(item)
|
||||
if entry == nil {
|
||||
return Manifest{}, fmt.Errorf("build: plugins[%d] must be a mapping with id and module", i)
|
||||
}
|
||||
p := Plugin{
|
||||
ID: strings.TrimSpace(asString(entry["id"])),
|
||||
Module: strings.TrimSpace(asString(entry["module"])),
|
||||
}
|
||||
if p.ID == "" {
|
||||
return Manifest{}, fmt.Errorf("build: plugins[%d] is missing id", i)
|
||||
}
|
||||
if strings.ContainsAny(p.ID, " \t\r\n\"'`") {
|
||||
return Manifest{}, fmt.Errorf("build: plugins[%d] has invalid id %q", i, p.ID)
|
||||
}
|
||||
if _, dup := seenID[p.ID]; dup {
|
||||
return Manifest{}, fmt.Errorf("build: duplicate plugin id %q", p.ID)
|
||||
}
|
||||
if err := validateModulePath(p.Module); err != nil {
|
||||
return Manifest{}, fmt.Errorf("build: plugins[%d] module: %w", i, err)
|
||||
}
|
||||
if _, dup := seenMod[p.Module]; dup {
|
||||
return Manifest{}, fmt.Errorf("build: duplicate plugin module %q", p.Module)
|
||||
}
|
||||
seenID[p.ID] = struct{}{}
|
||||
seenMod[p.Module] = struct{}{}
|
||||
m.Plugins = append(m.Plugins, p)
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// App generates app sources from summer.yaml and runs go build in appDir.
|
||||
func App(ctx context.Context, appDir string, out io.Writer) error {
|
||||
if appDir == "" {
|
||||
@@ -119,6 +47,7 @@ func App(ctx context.Context, appDir string, out io.Writer) error {
|
||||
cmd.Dir = appDir
|
||||
cmd.Stdout = out
|
||||
cmd.Stderr = out
|
||||
applyGoWork(cmd, appDir)
|
||||
start := time.Now()
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("build: go build: %w", err)
|
||||
@@ -229,80 +158,3 @@ func writeIfChanged(path string, content []byte) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateBinary(name string) error {
|
||||
if name == "" {
|
||||
return fmt.Errorf("is empty")
|
||||
}
|
||||
if name != filepath.Base(name) || strings.ContainsRune(name, os.PathSeparator) {
|
||||
return fmt.Errorf("%q must be a bare file name", name)
|
||||
}
|
||||
if name == "." || name == ".." {
|
||||
return fmt.Errorf("%q is not a valid binary name", name)
|
||||
}
|
||||
for _, r := range name {
|
||||
if r > unicode.MaxASCII || !(unicode.IsLetter(r) || unicode.IsDigit(r) || r == '.' || r == '_' || r == '-') {
|
||||
return fmt.Errorf("%q contains invalid characters", name)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateModulePath(path string) error {
|
||||
if path == "" {
|
||||
return fmt.Errorf("is empty")
|
||||
}
|
||||
if strings.HasPrefix(path, "-") {
|
||||
return fmt.Errorf("%q must not start with -", path)
|
||||
}
|
||||
if strings.ContainsAny(path, " \t\r\n\\\"'`$;&|*?<>()[]{}") {
|
||||
return fmt.Errorf("%q contains invalid characters", path)
|
||||
}
|
||||
if filepath.IsAbs(path) || strings.Contains(path, `\`) {
|
||||
return fmt.Errorf("%q is not a valid module path", path)
|
||||
}
|
||||
for _, seg := range strings.Split(path, "/") {
|
||||
if seg == "" || seg == "." || seg == ".." {
|
||||
return fmt.Errorf("%q has an invalid path segment", path)
|
||||
}
|
||||
if strings.Contains(seg, "@") {
|
||||
return fmt.Errorf("%q must not contain a version suffix", path)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func asString(v any) string {
|
||||
s, _ := v.(string)
|
||||
return s
|
||||
}
|
||||
|
||||
func asSlice(v any) []any {
|
||||
switch s := v.(type) {
|
||||
case []any:
|
||||
return s
|
||||
case nil:
|
||||
return nil
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func asMap(v any) map[string]any {
|
||||
switch m := v.(type) {
|
||||
case map[string]any:
|
||||
return m
|
||||
case map[any]any:
|
||||
out := make(map[string]any, len(m))
|
||||
for k, val := range m {
|
||||
ks, ok := k.(string)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
out[ks] = val
|
||||
}
|
||||
return out
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package build
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -121,6 +122,296 @@ func TestWriteIfChangedSkipsIdenticalBytes(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestMakePluginRejectsInvalidID(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
write(t, filepath.Join(dir, "summer.yaml"), "module: example.com/app\nbinary: app\nplugins:\n")
|
||||
write(t, filepath.Join(dir, "go.mod"), "module example.com/app\n\ngo 1.27.0\n")
|
||||
|
||||
for _, id := range []string{"Golem15.Demo", "golem15", "golem15.demo/../tmp", "../evil.plugin"} {
|
||||
if _, err := MakePlugin(t.Context(), dir, id); err == nil {
|
||||
t.Fatalf("id %q: want error", id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMakeAndAddPluginOnHelloCopy(t *testing.T) {
|
||||
dir := copyHelloApp(t)
|
||||
ctx := t.Context()
|
||||
|
||||
pluginDir, err := MakePlugin(ctx, dir, "golem15.demo")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
wantDir := filepath.Join(dir, "plugins", "demo")
|
||||
if pluginDir != wantDir {
|
||||
t.Fatalf("plugin dir = %s, want %s", pluginDir, wantDir)
|
||||
}
|
||||
assertScaffoldFiles(t, pluginDir)
|
||||
|
||||
if err := AddPlugin(ctx, dir, filepath.Join("plugins", "demo")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := AddPlugin(ctx, dir, filepath.Join("plugins", "demo")); err != nil {
|
||||
t.Fatalf("repeat plugin:add: %v", err)
|
||||
}
|
||||
|
||||
m, err := LoadManifest(filepath.Join(dir, "summer.yaml"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(m.Plugins) != 4 {
|
||||
t.Fatalf("plugins = %+v", m.Plugins)
|
||||
}
|
||||
last := m.Plugins[len(m.Plugins)-1]
|
||||
if last.ID != "golem15.demo" {
|
||||
t.Fatalf("last plugin id = %q", last.ID)
|
||||
}
|
||||
if last.Module != m.Module+"/plugins/demo" {
|
||||
t.Fatalf("last plugin module = %q", last.Module)
|
||||
}
|
||||
demoCount := 0
|
||||
for _, p := range m.Plugins {
|
||||
if p.ID == "golem15.demo" {
|
||||
demoCount++
|
||||
}
|
||||
}
|
||||
if demoCount != 1 {
|
||||
t.Fatalf("golem15.demo count = %d", demoCount)
|
||||
}
|
||||
|
||||
work, err := os.ReadFile(filepath.Join(dir, "go.work"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Contains(work, []byte("plugins/demo")) {
|
||||
t.Fatalf("go.work missing plugins/demo:\n%s", work)
|
||||
}
|
||||
appMod, err := os.ReadFile(filepath.Join(dir, "go.mod"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Contains(appMod, []byte(last.Module)) {
|
||||
t.Fatalf("app go.mod missing %s:\n%s", last.Module, appMod)
|
||||
}
|
||||
|
||||
var first, second bytes.Buffer
|
||||
if err := App(ctx, dir, &first); err != nil {
|
||||
t.Fatalf("first build: %v\n%s", err, first.String())
|
||||
}
|
||||
if !strings.Contains(first.String(), "built hello in ") {
|
||||
t.Fatalf("missing elapsed build time:\n%s", first.String())
|
||||
}
|
||||
main1, err := os.ReadFile(filepath.Join(dir, "main.go"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
gen1, err := os.ReadFile(filepath.Join(dir, "plugins.gen.go"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Contains(gen1, []byte(last.Module)) {
|
||||
t.Fatalf("plugins.gen.go missing %s:\n%s", last.Module, gen1)
|
||||
}
|
||||
if !bytes.Contains(gen1, []byte(`"golem15.demo"`)) {
|
||||
t.Fatalf("plugins.gen.go missing id:\n%s", gen1)
|
||||
}
|
||||
|
||||
if err := App(ctx, dir, &second); err != nil {
|
||||
t.Fatalf("second build: %v\n%s", err, second.String())
|
||||
}
|
||||
main2, err := os.ReadFile(filepath.Join(dir, "main.go"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
gen2, err := os.ReadFile(filepath.Join(dir, "plugins.gen.go"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(main1, main2) {
|
||||
t.Fatal("main.go is not byte-stable")
|
||||
}
|
||||
if !bytes.Equal(gen1, gen2) {
|
||||
t.Fatal("plugins.gen.go is not byte-stable")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddPluginRejectsConflictingID(t *testing.T) {
|
||||
dir := copyHelloApp(t)
|
||||
ctx := t.Context()
|
||||
if _, err := MakePlugin(ctx, dir, "golem15.demo"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
src, err := os.ReadFile(filepath.Join(dir, "plugins", "demo", "plugin.go"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
patched := bytes.ReplaceAll(src, []byte(`return "golem15.demo"`), []byte(`return "golem15.hello"`))
|
||||
if err := os.WriteFile(filepath.Join(dir, "plugins", "demo", "plugin.go"), patched, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = AddPlugin(ctx, dir, filepath.Join(dir, "plugins", "demo"))
|
||||
if err == nil || !strings.Contains(err.Error(), "golem15.hello") {
|
||||
t.Fatalf("want conflicting id error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddPluginRejectsPathTraversal(t *testing.T) {
|
||||
dir := copyHelloApp(t)
|
||||
outside := t.TempDir()
|
||||
if err := AddPlugin(t.Context(), dir, outside); err == nil {
|
||||
t.Fatal("want outside-root error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseManifestRejectsUppercaseID(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "bad.yaml")
|
||||
write(t, path, "module: example.com/app\nbinary: app\nplugins:\n - id: Golem15.Demo\n module: example.com/one\n")
|
||||
if _, err := LoadManifest(path); err == nil || !strings.Contains(err.Error(), "vendor.plugin") {
|
||||
t.Fatalf("want id format error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func assertScaffoldFiles(t *testing.T, pluginDir string) {
|
||||
t.Helper()
|
||||
if _, err := os.Stat(filepath.Join(pluginDir, "go.mod")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(pluginDir, "plugin.go")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(pluginDir, "config")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
body, err := os.ReadFile(filepath.Join(pluginDir, "plugin.go"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, want := range []string{
|
||||
"func (p *Plugin) ID()",
|
||||
"func (p *Plugin) Requires()",
|
||||
"func (p *Plugin) Register",
|
||||
"func (p *Plugin) Boot",
|
||||
"party.Register(&Plugin{})",
|
||||
`return "golem15.demo"`,
|
||||
} {
|
||||
if !bytes.Contains(body, []byte(want)) {
|
||||
t.Fatalf("plugin.go missing %s:\n%s", want, body)
|
||||
}
|
||||
}
|
||||
mod, err := os.ReadFile(filepath.Join(pluginDir, "go.mod"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Contains(mod, []byte("toolchain go1.27.0")) {
|
||||
t.Fatalf("plugin go.mod missing toolchain:\n%s", mod)
|
||||
}
|
||||
if !bytes.Contains(mod, []byte("/plugins/demo")) {
|
||||
t.Fatalf("plugin go.mod missing module path:\n%s", mod)
|
||||
}
|
||||
|
||||
var extra []string
|
||||
if err := filepath.WalkDir(pluginDir, func(path string, d os.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
rel, err := filepath.Rel(pluginDir, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch rel {
|
||||
case "go.mod", "go.sum", "plugin.go", filepath.Join("config", ".gitkeep"):
|
||||
return nil
|
||||
default:
|
||||
extra = append(extra, rel)
|
||||
return nil
|
||||
}
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(extra) > 0 {
|
||||
t.Fatalf("unexpected scaffold files: %v", extra)
|
||||
}
|
||||
|
||||
cmd := execGo(t, pluginDir, "build", "-o", filepath.Join(pluginDir, "demo.bin"), ".")
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
t.Fatalf("plugin does not compile: %v\n%s", err, out)
|
||||
}
|
||||
}
|
||||
|
||||
func copyHelloApp(t *testing.T) string {
|
||||
t.Helper()
|
||||
src, err := filepath.Abs(filepath.Join("..", "..", "examples", "hello"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
framework, err := filepath.Abs(filepath.Join("..", ".."))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
dst := t.TempDir()
|
||||
if err := filepath.WalkDir(src, func(path string, d os.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rel, err := filepath.Rel(src, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rel == "bin" || strings.HasPrefix(rel, "bin"+string(os.PathSeparator)) {
|
||||
if d.IsDir() {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
target := filepath.Join(dst, rel)
|
||||
if d.IsDir() {
|
||||
return os.MkdirAll(target, 0o755)
|
||||
}
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if filepath.Base(path) == "go.mod" {
|
||||
data = rewriteFrameworkReplace(data, framework)
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(target, data, 0o644)
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
func rewriteFrameworkReplace(data []byte, framework string) []byte {
|
||||
lines := strings.Split(string(data), "\n")
|
||||
for i, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
switch {
|
||||
case strings.HasPrefix(trimmed, "replace "+frameworkModule+" =>"):
|
||||
indent := line[:len(line)-len(strings.TrimLeft(line, " \t"))]
|
||||
lines[i] = indent + "replace " + frameworkModule + " => " + framework
|
||||
case strings.HasPrefix(trimmed, frameworkModule+" =>"):
|
||||
indent := line[:len(line)-len(strings.TrimLeft(line, " \t"))]
|
||||
lines[i] = indent + frameworkModule + " => " + framework
|
||||
}
|
||||
}
|
||||
return []byte(strings.Join(lines, "\n"))
|
||||
}
|
||||
|
||||
func execGo(t *testing.T, dir string, args ...string) *exec.Cmd {
|
||||
t.Helper()
|
||||
cmd := exec.CommandContext(t.Context(), "go", args...)
|
||||
cmd.Dir = dir
|
||||
applyGoWork(cmd, dir)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func write(t *testing.T, path, body string) {
|
||||
t.Helper()
|
||||
if err := os.WriteFile(path, []byte(body), 0o644); err != nil {
|
||||
|
||||
219
internal/build/manifest.go
Normal file
219
internal/build/manifest.go
Normal file
@@ -0,0 +1,219 @@
|
||||
package build
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/knadh/koanf/parsers/yaml"
|
||||
)
|
||||
|
||||
const manifestName = "summer.yaml"
|
||||
|
||||
var pluginIDRe = regexp.MustCompile(`^[a-z][a-z0-9]*\.[a-z][a-z0-9]*$`)
|
||||
|
||||
// Manifest is the ordered app plugin list used to generate imports.
|
||||
type Manifest struct {
|
||||
Module string
|
||||
Binary string
|
||||
Plugins []Plugin
|
||||
}
|
||||
|
||||
// Plugin is one compiled plugin module in manifest order.
|
||||
type Plugin struct {
|
||||
ID string
|
||||
Module string
|
||||
}
|
||||
|
||||
// LoadManifest reads and validates summer.yaml at path.
|
||||
func LoadManifest(path string) (Manifest, error) {
|
||||
raw, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return Manifest{}, fmt.Errorf("build: read %s: %w", path, err)
|
||||
}
|
||||
tree, err := yaml.Parser().Unmarshal(raw)
|
||||
if err != nil {
|
||||
return Manifest{}, fmt.Errorf("build: parse %s: %w", path, err)
|
||||
}
|
||||
m := Manifest{
|
||||
Module: strings.TrimSpace(asString(tree["module"])),
|
||||
Binary: strings.TrimSpace(asString(tree["binary"])),
|
||||
}
|
||||
if err := validateModulePath(m.Module); err != nil {
|
||||
return Manifest{}, fmt.Errorf("build: module: %w", err)
|
||||
}
|
||||
if err := validateBinary(m.Binary); err != nil {
|
||||
return Manifest{}, fmt.Errorf("build: binary: %w", err)
|
||||
}
|
||||
|
||||
seenID := make(map[string]struct{})
|
||||
seenMod := make(map[string]struct{})
|
||||
for i, item := range asSlice(tree["plugins"]) {
|
||||
entry := asMap(item)
|
||||
if entry == nil {
|
||||
return Manifest{}, fmt.Errorf("build: plugins[%d] must be a mapping with id and module", i)
|
||||
}
|
||||
p := Plugin{
|
||||
ID: strings.TrimSpace(asString(entry["id"])),
|
||||
Module: strings.TrimSpace(asString(entry["module"])),
|
||||
}
|
||||
if err := ValidatePluginID(p.ID); err != nil {
|
||||
return Manifest{}, fmt.Errorf("build: plugins[%d]: %w", i, err)
|
||||
}
|
||||
if _, dup := seenID[p.ID]; dup {
|
||||
return Manifest{}, fmt.Errorf("build: duplicate plugin id %q", p.ID)
|
||||
}
|
||||
if err := validateModulePath(p.Module); err != nil {
|
||||
return Manifest{}, fmt.Errorf("build: plugins[%d] module: %w", i, err)
|
||||
}
|
||||
if _, dup := seenMod[p.Module]; dup {
|
||||
return Manifest{}, fmt.Errorf("build: duplicate plugin module %q", p.Module)
|
||||
}
|
||||
seenID[p.ID] = struct{}{}
|
||||
seenMod[p.Module] = struct{}{}
|
||||
m.Plugins = append(m.Plugins, p)
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// SaveManifest writes summer.yaml with plugins in slice order.
|
||||
func SaveManifest(path string, m Manifest) error {
|
||||
if err := validateModulePath(m.Module); err != nil {
|
||||
return fmt.Errorf("build: module: %w", err)
|
||||
}
|
||||
if err := validateBinary(m.Binary); err != nil {
|
||||
return fmt.Errorf("build: binary: %w", err)
|
||||
}
|
||||
var b strings.Builder
|
||||
fmt.Fprintf(&b, "module: %s\n", m.Module)
|
||||
fmt.Fprintf(&b, "binary: %s\n", m.Binary)
|
||||
b.WriteString("plugins:\n")
|
||||
for i, p := range m.Plugins {
|
||||
if err := ValidatePluginID(p.ID); err != nil {
|
||||
return fmt.Errorf("build: plugins[%d]: %w", i, err)
|
||||
}
|
||||
if err := validateModulePath(p.Module); err != nil {
|
||||
return fmt.Errorf("build: plugins[%d] module: %w", i, err)
|
||||
}
|
||||
fmt.Fprintf(&b, " - id: %s\n", p.ID)
|
||||
fmt.Fprintf(&b, " module: %s\n", p.Module)
|
||||
}
|
||||
if err := os.WriteFile(path, []byte(b.String()), 0o644); err != nil {
|
||||
return fmt.Errorf("build: write %s: %w", path, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidatePluginID requires a lowercase vendor.plugin identifier.
|
||||
func ValidatePluginID(id string) error {
|
||||
if id == "" {
|
||||
return fmt.Errorf("plugin id is empty")
|
||||
}
|
||||
if strings.ContainsAny(id, " \t\r\n\"'`/\\") {
|
||||
return fmt.Errorf("plugin id %q contains invalid characters", id)
|
||||
}
|
||||
if !pluginIDRe.MatchString(id) {
|
||||
return fmt.Errorf("plugin id %q must be lowercase vendor.plugin", id)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func findAppDir(start string) (string, error) {
|
||||
if start == "" {
|
||||
return "", fmt.Errorf("build: app directory is empty")
|
||||
}
|
||||
dir, err := filepath.Abs(start)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("build: resolve app directory: %w", err)
|
||||
}
|
||||
for {
|
||||
if _, err := os.Stat(filepath.Join(dir, manifestName)); err == nil {
|
||||
return dir, nil
|
||||
}
|
||||
parent := filepath.Dir(dir)
|
||||
if parent == dir {
|
||||
return "", fmt.Errorf("build: no %s found from %s", manifestName, start)
|
||||
}
|
||||
dir = parent
|
||||
}
|
||||
}
|
||||
|
||||
func validateBinary(name string) error {
|
||||
if name == "" {
|
||||
return fmt.Errorf("is empty")
|
||||
}
|
||||
if name != filepath.Base(name) || strings.ContainsRune(name, os.PathSeparator) {
|
||||
return fmt.Errorf("%q must be a bare file name", name)
|
||||
}
|
||||
if name == "." || name == ".." {
|
||||
return fmt.Errorf("%q is not a valid binary name", name)
|
||||
}
|
||||
for _, r := range name {
|
||||
if r > unicode.MaxASCII || !(unicode.IsLetter(r) || unicode.IsDigit(r) || r == '.' || r == '_' || r == '-') {
|
||||
return fmt.Errorf("%q contains invalid characters", name)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateModulePath(path string) error {
|
||||
if path == "" {
|
||||
return fmt.Errorf("is empty")
|
||||
}
|
||||
if strings.HasPrefix(path, "-") {
|
||||
return fmt.Errorf("%q must not start with -", path)
|
||||
}
|
||||
if strings.ContainsAny(path, " \t\r\n\\\"'`$;&|*?<>()[]{}") {
|
||||
return fmt.Errorf("%q contains invalid characters", path)
|
||||
}
|
||||
if filepath.IsAbs(path) || strings.Contains(path, `\`) {
|
||||
return fmt.Errorf("%q is not a valid module path", path)
|
||||
}
|
||||
for _, seg := range strings.Split(path, "/") {
|
||||
if seg == "" || seg == "." || seg == ".." {
|
||||
return fmt.Errorf("%q has an invalid path segment", path)
|
||||
}
|
||||
if strings.Contains(seg, "@") {
|
||||
return fmt.Errorf("%q must not contain a version suffix", path)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func asString(v any) string {
|
||||
s, _ := v.(string)
|
||||
return s
|
||||
}
|
||||
|
||||
func asSlice(v any) []any {
|
||||
switch s := v.(type) {
|
||||
case []any:
|
||||
return s
|
||||
case nil:
|
||||
return nil
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func asMap(v any) map[string]any {
|
||||
switch m := v.(type) {
|
||||
case map[string]any:
|
||||
return m
|
||||
case map[any]any:
|
||||
out := make(map[string]any, len(m))
|
||||
for k, val := range m {
|
||||
ks, ok := k.(string)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
out[ks] = val
|
||||
}
|
||||
return out
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
476
internal/build/scaffold.go
Normal file
476
internal/build/scaffold.go
Normal file
@@ -0,0 +1,476 @@
|
||||
package build
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/format"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
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/<name>.
|
||||
func MakePlugin(ctx context.Context, startDir, id string) (string, error) {
|
||||
if err := ValidatePluginID(id); err != nil {
|
||||
return "", fmt.Errorf("build: %w", err)
|
||||
}
|
||||
appDir, err := findAppDir(startDir)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
name := pluginName(id)
|
||||
pluginDir := filepath.Join(appDir, "plugins", name)
|
||||
if !underRoot(appDir, pluginDir) {
|
||||
return "", fmt.Errorf("build: plugin path %q escapes the app root", pluginDir)
|
||||
}
|
||||
if _, err := os.Stat(pluginDir); err == nil {
|
||||
return "", fmt.Errorf("build: plugin directory %s already exists", pluginDir)
|
||||
} else if !os.IsNotExist(err) {
|
||||
return "", fmt.Errorf("build: stat %s: %w", pluginDir, err)
|
||||
}
|
||||
|
||||
m, err := LoadManifest(filepath.Join(appDir, manifestName))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
modPath := m.Module + "/plugins/" + name
|
||||
if err := validateModulePath(modPath); err != nil {
|
||||
return "", fmt.Errorf("build: plugin module: %w", err)
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Join(pluginDir, "config"), 0o755); err != nil {
|
||||
return "", fmt.Errorf("build: create plugin dir: %w", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(pluginDir, "config", ".gitkeep"), []byte{}, 0o644); err != nil {
|
||||
return "", fmt.Errorf("build: write config placeholder: %w", err)
|
||||
}
|
||||
|
||||
src, err := pluginGoSource(name, id)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(pluginDir, "plugin.go"), src, 0o644); err != nil {
|
||||
return "", fmt.Errorf("build: write plugin.go: %w", err)
|
||||
}
|
||||
|
||||
replace, err := frameworkReplaceFor(pluginDir, appDir)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(pluginDir, "go.mod"), pluginGoMod(modPath, replace), 0o644); err != nil {
|
||||
return "", fmt.Errorf("build: write go.mod: %w", 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
|
||||
}
|
||||
|
||||
func pluginGoSource(pkg, id string) ([]byte, error) {
|
||||
var b strings.Builder
|
||||
b.WriteString("package " + pkg + "\n\n")
|
||||
b.WriteString("import (\n")
|
||||
b.WriteString("\t\"git.golem15.com/golem15/summercms/backpack\"\n")
|
||||
b.WriteString("\t\"git.golem15.com/golem15/summercms/party\"\n")
|
||||
b.WriteString(")\n\n")
|
||||
fmt.Fprintf(&b, "// Plugin is the %s plugin.\n", id)
|
||||
b.WriteString("type Plugin struct{}\n\n")
|
||||
fmt.Fprintf(&b, "func (p *Plugin) ID() string { return %s }\n", strconv.Quote(id))
|
||||
b.WriteString("func (p *Plugin) Requires() []string { return nil }\n\n")
|
||||
b.WriteString("func (p *Plugin) Register(*backpack.App) error { return nil }\n")
|
||||
b.WriteString("func (p *Plugin) Boot(*backpack.App) error { return nil }\n\n")
|
||||
b.WriteString("func init() {\n")
|
||||
b.WriteString("\tparty.Register(&Plugin{})\n")
|
||||
b.WriteString("}\n")
|
||||
formatted, err := format.Source([]byte(b.String()))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("build: format plugin.go: %w", err)
|
||||
}
|
||||
return formatted, nil
|
||||
}
|
||||
|
||||
func pluginGoMod(modPath, replace string) []byte {
|
||||
var b strings.Builder
|
||||
fmt.Fprintf(&b, "module %s\n\n", modPath)
|
||||
b.WriteString(goVersionLine + "\n\n")
|
||||
b.WriteString(toolchainLine + "\n\n")
|
||||
fmt.Fprintf(&b, "require %s v0.0.0\n", frameworkModule)
|
||||
if replace != "" {
|
||||
fmt.Fprintf(&b, "\nreplace %s => %s\n", frameworkModule, replace)
|
||||
}
|
||||
return []byte(b.String())
|
||||
}
|
||||
|
||||
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 := filepath.Rel(appDir, pluginDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("build: plugin replace: %w", err)
|
||||
}
|
||||
rel = filepath.ToSlash(rel)
|
||||
if rel != ".." && !strings.HasPrefix(rel, "../") && !filepath.IsAbs(rel) && !strings.HasPrefix(rel, "./") {
|
||||
rel = "./" + rel
|
||||
}
|
||||
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, appDir, gw, dir); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return runGoWorkUse(ctx, appDir, 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
|
||||
}
|
||||
|
||||
func runGoWorkUse(ctx context.Context, appDir, goWork, dir string) error {
|
||||
cmd := exec.CommandContext(ctx, "go", "work", "use", dir)
|
||||
cmd.Dir = appDir
|
||||
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")
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user