summer build always emitted party.Describe and []party.Info, so an application whose framework requirement predates v0.6.0 stopped compiling once the CLI was updated. The build now checks the party package go list resolves in the application (a static look for Describe and Info, not a version compare, since a local checkout reports the previous tag until it is tagged) and, when they are missing, writes main.go and plugins.gen.go exactly as before, with a warning to move the framework to v0.6.0. An unresolvable party package keeps the current sources.
275 lines
9.7 KiB
Go
275 lines
9.7 KiB
Go
package build
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"go/format"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.golem15.com/golem15/summercms/modules/party"
|
|
)
|
|
|
|
// 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
|
|
}
|
|
meta, warnings := resolveBuildMeta(ctx, appDir, m)
|
|
for _, w := range warnings {
|
|
fmt.Fprintf(out, "warning: %s\n", w)
|
|
}
|
|
if err := generate(appDir, m, meta); 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, meta buildMeta) error {
|
|
mainSrc, err := generateMainFor(m, !meta.Legacy)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
pluginsSrc, err := generatePluginsGen(m, meta)
|
|
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
|
|
}
|
|
|
|
// generateMain writes main.go for a framework that has party.Describe.
|
|
func generateMain(m Manifest) ([]byte, error) {
|
|
return generateMainFor(m, true)
|
|
}
|
|
|
|
// generateMainFor writes main.go; describe adds the party.Describe call,
|
|
// which a framework before v0.6.0 does not have (WR-01).
|
|
func generateMainFor(m Manifest, describe bool) ([]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")
|
|
if describe {
|
|
b.WriteString("\tif err := party.Describe(app, PluginInfo, FrameworkVersion); err != nil {\n" + fail + "\t}\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()))
|
|
}
|
|
|
|
// generatePluginsGen writes plugins.gen.go: the blank plugin imports, the
|
|
// ordered PluginIDs, the PluginInfo metadata and FrameworkVersion. Every
|
|
// baked string goes through strconv.Quote. For a framework without
|
|
// party.Info (meta.Legacy) it writes the imports and PluginIDs only, as
|
|
// summer build did before v0.6.0.
|
|
func generatePluginsGen(m Manifest, meta buildMeta) ([]byte, error) {
|
|
if meta.Legacy {
|
|
return generateLegacyPluginsGen(m)
|
|
}
|
|
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")
|
|
for _, p := range m.Plugins {
|
|
fmt.Fprintf(&b, "\t_ %s\n", strconv.Quote(p.Module))
|
|
}
|
|
if len(m.Plugins) > 0 {
|
|
b.WriteString("\n")
|
|
}
|
|
b.WriteString("\t\"git.golem15.com/golem15/summercms/modules/party\"\n")
|
|
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\n")
|
|
b.WriteString("// PluginInfo is each plugin's metadata in summer.yaml order: name and\n")
|
|
b.WriteString("// description from its plugin.yaml, version and source from summer build.\n")
|
|
b.WriteString("var PluginInfo = []party.Info{\n")
|
|
for i, p := range m.Plugins {
|
|
info := party.Info{ID: p.ID}
|
|
if i < len(meta.Plugins) && meta.Plugins[i].ID == p.ID {
|
|
info = meta.Plugins[i]
|
|
}
|
|
fmt.Fprintf(&b, "\t{%s},\n", infoLiteral(info))
|
|
}
|
|
b.WriteString("}\n\n")
|
|
b.WriteString("// FrameworkVersion is the framework version the app was built against.\n")
|
|
fmt.Fprintf(&b, "const FrameworkVersion = %s\n", strconv.Quote(meta.Framework))
|
|
return formatSource([]byte(b.String()))
|
|
}
|
|
|
|
// generateLegacyPluginsGen is plugins.gen.go for a framework before
|
|
// party.Info: the blank plugin imports and PluginIDs.
|
|
func generateLegacyPluginsGen(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()))
|
|
}
|
|
|
|
// infoLiteral is the field list of a party.Info composite literal with the
|
|
// empty fields left out.
|
|
func infoLiteral(info party.Info) string {
|
|
fields := []string{"ID: " + strconv.Quote(info.ID)}
|
|
add := func(name, v string) {
|
|
if v != "" {
|
|
fields = append(fields, name+": "+strconv.Quote(v))
|
|
}
|
|
}
|
|
add("Module", info.Module)
|
|
add("Package", info.Package)
|
|
add("Name", info.Name)
|
|
add("Description", info.Description)
|
|
add("Version", info.Version)
|
|
switch info.Source {
|
|
case "":
|
|
case party.SourceModule:
|
|
fields = append(fields, "Source: party.SourceModule")
|
|
case party.SourceSubmodule:
|
|
fields = append(fields, "Source: party.SourceSubmodule")
|
|
case party.SourceLocal:
|
|
fields = append(fields, "Source: party.SourceLocal")
|
|
default:
|
|
fields = append(fields, "Source: "+strconv.Quote(info.Source))
|
|
}
|
|
return strings.Join(fields, ", ")
|
|
}
|
|
|
|
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
|
|
}
|