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) } cmd := exec.CommandContext(ctx, "go", "build", "-o", binRel, ".") 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 } 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\"fmt\"\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/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("\tif err := run(os.Args[1:], os.Stdout); err != nil {\n") b.WriteString("\t\tfmt.Fprintln(os.Stderr, err)\n") b.WriteString("\t\tos.Exit(1)\n") b.WriteString("\t}\n") b.WriteString("}\n\n") b.WriteString("func run(args []string, out io.Writer) error {\n") b.WriteString("\tcfg, err := compass.Load(\"config\")\n") b.WriteString("\tif err != nil {\n") b.WriteString("\t\treturn err\n") b.WriteString("\t}\n") b.WriteString("\tapp := backpack.New(cfg)\n") b.WriteString("\tplugins, err := party.Activate(app, PluginIDs)\n") b.WriteString("\tif err != nil {\n") b.WriteString("\t\treturn err\n") b.WriteString("\t}\n") b.WriteString("\tcommands := lagoon.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") fmt.Fprintf(&b, "\troot, err := bonfire.NewRoot(%s, commands, out)\n", strconv.Quote(m.Binary)) b.WriteString("\tif err != nil {\n") b.WriteString("\t\treturn err\n") b.WriteString("\t}\n") b.WriteString("\troot.SetArgs(args)\n") b.WriteString("\treturn root.Execute()\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 }