- Global options -v/-vv/-vvv, -q, --ansi/--no-ansi, -n and --env on every command; PeekGlobals reads them before config loads, OnGlobals hooks re-apply them after parsing (diary.ApplyGlobals in the generated main) - Bare binary, --help and list print the Artisan list with the version header from BuildVersion and namespace blocks - Execute runs the root through fang (no version, man page or completions) and returns 0, 1 or 2; Fail renders errors before the root exists - NewRoot rejects duplicate command names (ErrDuplicateCommand) and flags that reuse a global option (ErrReservedFlag) - Generated main and summer use PeekGlobals, compass.Open with --env, diary.FromGlobals and bonfire.Execute; docs checker knows the globals
186 lines
6.4 KiB
Go
186 lines
6.4 KiB
Go
package build
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"go/format"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// 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 == "" {
|
|
return fmt.Errorf("build: app directory is empty")
|
|
}
|
|
if !filepath.IsAbs(appDir) {
|
|
abs, err := filepath.Abs(appDir)
|
|
if err != nil {
|
|
return fmt.Errorf("build: resolve app directory: %w", err)
|
|
}
|
|
appDir = abs
|
|
}
|
|
if out == nil {
|
|
out = os.Stdout
|
|
}
|
|
m, err := LoadManifest(filepath.Join(appDir, manifestName))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := generate(appDir, m); err != nil {
|
|
return err
|
|
}
|
|
if err := checkModelsLeaf(appDir, m); err != nil {
|
|
return err
|
|
}
|
|
|
|
binRel := filepath.Join("bin", m.Binary)
|
|
if err := os.MkdirAll(filepath.Join(appDir, "bin"), 0o755); err != nil {
|
|
return fmt.Errorf("build: create bin dir: %w", err)
|
|
}
|
|
|
|
if len(m.Tags) > 0 {
|
|
fmt.Fprintf(out, "build tags: %s\n", strings.Join(m.Tags, ","))
|
|
}
|
|
cmd := exec.CommandContext(ctx, "go", goBuildArgs(binRel, m.Tags)...)
|
|
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)
|
|
}
|
|
fmt.Fprintf(out, "built %s in %s\n", m.Binary, time.Since(start).Round(time.Millisecond))
|
|
return nil
|
|
}
|
|
|
|
// goBuildArgs returns the go command arguments for summer build: build, the
|
|
// -tags flag when the manifest lists build tags, then -o binRel and the
|
|
// application package. The tags travel as one exec argument, never a shell.
|
|
func goBuildArgs(binRel string, tags []string) []string {
|
|
args := []string{"build"}
|
|
if len(tags) > 0 {
|
|
args = append(args, "-tags="+strings.Join(tags, ","))
|
|
}
|
|
return append(args, "-o", binRel, ".")
|
|
}
|
|
|
|
func generate(appDir string, m Manifest) error {
|
|
mainSrc, err := generateMain(m)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
pluginsSrc, err := generatePluginsGen(m)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := writeIfChanged(filepath.Join(appDir, "main.go"), mainSrc); err != nil {
|
|
return err
|
|
}
|
|
if err := writeIfChanged(filepath.Join(appDir, "plugins.gen.go"), pluginsSrc); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func generateMain(m Manifest) ([]byte, error) {
|
|
var b strings.Builder
|
|
b.WriteString("// Code generated by summer build. DO NOT EDIT.\n\n")
|
|
b.WriteString("package main\n\n")
|
|
b.WriteString("import (\n")
|
|
b.WriteString("\t\"context\"\n")
|
|
b.WriteString("\t\"io\"\n")
|
|
b.WriteString("\t\"os\"\n\n")
|
|
b.WriteString("\t\"git.golem15.com/golem15/summercms/modules/backpack\"\n")
|
|
b.WriteString("\t\"git.golem15.com/golem15/summercms/modules/bonfire\"\n")
|
|
b.WriteString("\t\"git.golem15.com/golem15/summercms/modules/cabana\"\n")
|
|
b.WriteString("\t\"git.golem15.com/golem15/summercms/modules/compass\"\n")
|
|
b.WriteString("\t\"git.golem15.com/golem15/summercms/modules/conga\"\n")
|
|
b.WriteString("\t\"git.golem15.com/golem15/summercms/modules/diary\"\n")
|
|
b.WriteString("\t\"git.golem15.com/golem15/summercms/modules/lagoon\"\n")
|
|
b.WriteString("\t\"git.golem15.com/golem15/summercms/modules/pact\"\n")
|
|
b.WriteString("\t\"git.golem15.com/golem15/summercms/modules/party\"\n")
|
|
b.WriteString("\t\"git.golem15.com/golem15/summercms/modules/surf\"\n")
|
|
b.WriteString(")\n\n")
|
|
b.WriteString("func main() {\n")
|
|
b.WriteString("\tos.Exit(run(os.Args[1:], os.Stdout))\n")
|
|
b.WriteString("}\n\n")
|
|
binary := strconv.Quote(m.Binary)
|
|
fail := "\t\treturn bonfire.Fail(os.Stderr, " + binary + ", err, g)\n"
|
|
b.WriteString("func run(args []string, out io.Writer) int {\n")
|
|
b.WriteString("\tg := bonfire.PeekGlobals(args)\n")
|
|
b.WriteString("\tdiary.Bootstrap(os.Stderr, diary.FromGlobals(g))\n")
|
|
b.WriteString("\tcfg, err := compass.Open(compass.Options{Dir: \"config\", Env: g.Env})\n")
|
|
b.WriteString("\tif err != nil {\n" + fail + "\t}\n")
|
|
b.WriteString("\tif err := diary.Setup(cfg, diary.FromGlobals(g)); err != nil {\n" + fail + "\t}\n")
|
|
b.WriteString("\tapp := backpack.New(cfg)\n")
|
|
b.WriteString("\tplugins, err := party.Activate(app, PluginIDs)\n")
|
|
b.WriteString("\tif err != nil {\n" + fail + "\t}\n")
|
|
b.WriteString("\tcommands := lagoon.RuntimeCommands(app, plugins)\n")
|
|
b.WriteString("\tcommands = append(commands, conga.RuntimeCommands(app, plugins)...)\n")
|
|
b.WriteString("\tcommands = append(commands, surf.ServeCommand(app, plugins))\n")
|
|
b.WriteString("\tcommands = append(commands, surf.RouteListCommand(app, plugins))\n")
|
|
b.WriteString("\tcommands = append(commands, cabana.RuntimeCommands(app)...)\n")
|
|
b.WriteString("\tfor _, plugin := range plugins {\n")
|
|
b.WriteString("\t\tif hasCommands, ok := plugin.(pact.HasCommands); ok {\n")
|
|
b.WriteString("\t\t\tcommands = append(commands, hasCommands.Commands()...)\n")
|
|
b.WriteString("\t\t}\n")
|
|
b.WriteString("\t}\n")
|
|
b.WriteString("\tif err := app.Publish(bonfire.NewCatalog(commands)); err != nil {\n" + fail + "\t}\n")
|
|
fmt.Fprintf(&b, "\troot, err := bonfire.NewRoot(%s, commands, out, bonfire.WithVersion(bonfire.BuildVersion()), bonfire.OnGlobals(diary.ApplyGlobals))\n", binary)
|
|
b.WriteString("\tif err != nil {\n" + fail + "\t}\n")
|
|
b.WriteString("\treturn bonfire.Execute(context.Background(), root, args)\n")
|
|
b.WriteString("}\n")
|
|
return formatSource([]byte(b.String()))
|
|
}
|
|
|
|
func generatePluginsGen(m Manifest) ([]byte, error) {
|
|
var b strings.Builder
|
|
b.WriteString("// Code generated by summer build. DO NOT EDIT.\n\n")
|
|
b.WriteString("package main\n\n")
|
|
if len(m.Plugins) > 0 {
|
|
b.WriteString("import (\n")
|
|
for _, p := range m.Plugins {
|
|
fmt.Fprintf(&b, "\t_ %s\n", strconv.Quote(p.Module))
|
|
}
|
|
b.WriteString(")\n\n")
|
|
}
|
|
b.WriteString("// PluginIDs is the ordered plugin list from summer.yaml.\n")
|
|
b.WriteString("var PluginIDs = []string{\n")
|
|
for _, p := range m.Plugins {
|
|
fmt.Fprintf(&b, "\t%s,\n", strconv.Quote(p.ID))
|
|
}
|
|
b.WriteString("}\n")
|
|
return formatSource([]byte(b.String()))
|
|
}
|
|
|
|
func formatSource(src []byte) ([]byte, error) {
|
|
formatted, err := format.Source(src)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("build: format generated go: %w", err)
|
|
}
|
|
return formatted, nil
|
|
}
|
|
|
|
func writeIfChanged(path string, content []byte) error {
|
|
existing, err := os.ReadFile(path)
|
|
if err == nil && bytes.Equal(existing, content) {
|
|
return nil
|
|
}
|
|
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
|
return fmt.Errorf("build: read %s: %w", path, err)
|
|
}
|
|
if err := os.WriteFile(path, content, 0o644); err != nil {
|
|
return fmt.Errorf("build: write %s: %w", path, err)
|
|
}
|
|
return nil
|
|
}
|