feat(build): bake plugin metadata and the framework version into plugins.gen.go
- party.Info, party.Installed, party.Describe (additive; Plugin and Activate unchanged) - summer build resolves plugin dirs with one go list, reads plugin.yaml, versions per D-07 (module version, git describe without --dirty, none for in-app plugins) - build.EffectiveFramework: newest tag of a locally replaced or used framework checkout, else the go.mod require - generated main calls party.Describe(app, PluginInfo, FrameworkVersion) before Activate; metadata failures warn and fall back to IDs - summer new goldens and examples/hello regenerated
This commit is contained in:
@@ -13,6 +13,8 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.golem15.com/golem15/summercms/modules/party"
|
||||
)
|
||||
|
||||
// App generates app sources from summer.yaml and runs go build in appDir.
|
||||
@@ -34,7 +36,11 @@ func App(ctx context.Context, appDir string, out io.Writer) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := generate(appDir, m); err != nil {
|
||||
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 {
|
||||
@@ -73,12 +79,12 @@ func goBuildArgs(binRel string, tags []string) []string {
|
||||
return append(args, "-o", binRel, ".")
|
||||
}
|
||||
|
||||
func generate(appDir string, m Manifest) error {
|
||||
func generate(appDir string, m Manifest, meta buildMeta) error {
|
||||
mainSrc, err := generateMain(m)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pluginsSrc, err := generatePluginsGen(m)
|
||||
pluginsSrc, err := generatePluginsGen(m, meta)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -122,6 +128,7 @@ func generateMain(m Manifest) ([]byte, error) {
|
||||
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("\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")
|
||||
@@ -142,26 +149,72 @@ func generateMain(m Manifest) ([]byte, error) {
|
||||
return formatSource([]byte(b.String()))
|
||||
}
|
||||
|
||||
func generatePluginsGen(m Manifest) ([]byte, error) {
|
||||
// generatePluginsGen writes plugins.gen.go: the blank plugin imports, the
|
||||
// ordered PluginIDs, the PluginInfo metadata and FrameworkVersion. Every
|
||||
// baked string goes through strconv.Quote.
|
||||
func generatePluginsGen(m Manifest, meta buildMeta) ([]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("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")
|
||||
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()))
|
||||
}
|
||||
|
||||
// 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 {
|
||||
|
||||
@@ -66,11 +66,11 @@ func TestGenerateStableQuotedImportsInManifestOrder(t *testing.T) {
|
||||
{ID: "golem15.greeter", Module: "example.com/app/plugins/greeter"},
|
||||
},
|
||||
}
|
||||
first, err := generatePluginsGen(m)
|
||||
first, err := generatePluginsGen(m, buildMeta{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
second, err := generatePluginsGen(m)
|
||||
second, err := generatePluginsGen(m, buildMeta{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -450,7 +450,9 @@ func populateApp(ctx context.Context, opts NewOptions, target string, files []ge
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return generate(target, m)
|
||||
// summer new bakes IDs only and an empty FrameworkVersion; the first
|
||||
// summer build fills both in.
|
||||
return generate(target, m, idOnlyMeta(m))
|
||||
}
|
||||
|
||||
// addStarterPlugin creates golem15.<name> through the make:plugin and
|
||||
|
||||
332
internal/build/plugindirs.go
Normal file
332
internal/build/plugindirs.go
Normal file
@@ -0,0 +1,332 @@
|
||||
package build
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"git.golem15.com/golem15/summercms/modules/kiosk"
|
||||
"git.golem15.com/golem15/summercms/modules/party"
|
||||
)
|
||||
|
||||
// buildMeta is the plugin metadata and framework version summer build
|
||||
// bakes into plugins.gen.go.
|
||||
type buildMeta struct {
|
||||
Plugins []party.Info
|
||||
Framework string
|
||||
}
|
||||
|
||||
// listedPackage is the part of `go list -json` output the metadata step
|
||||
// reads for one plugin package.
|
||||
type listedPackage struct {
|
||||
ImportPath string
|
||||
Dir string
|
||||
Module *listedPackageModule
|
||||
Error *struct{ Err string }
|
||||
}
|
||||
|
||||
type listedPackageModule struct {
|
||||
Path string
|
||||
Version string
|
||||
Dir string
|
||||
Main bool
|
||||
Replace *struct {
|
||||
Path string
|
||||
Version string
|
||||
Dir string
|
||||
}
|
||||
}
|
||||
|
||||
// listPackages runs `go list` for the plugin packages; tests replace it.
|
||||
var listPackages = goListPackages
|
||||
|
||||
// goListPackages resolves import paths to directories and modules with one
|
||||
// `go list -e -json` in appDir, honouring the app's go.work.
|
||||
func goListPackages(ctx context.Context, appDir string, paths []string) ([]listedPackage, error) {
|
||||
args := append([]string{"list", "-e", "-json=ImportPath,Dir,Module,Error", "--"}, paths...)
|
||||
cmd := exec.CommandContext(ctx, "go", args...)
|
||||
cmd.Dir = appDir
|
||||
applyGoWork(cmd, appDir)
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stderr = &stderr
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("go list: %w: %s", err, bytes.TrimSpace(stderr.Bytes()))
|
||||
}
|
||||
var pkgs []listedPackage
|
||||
dec := json.NewDecoder(bytes.NewReader(out))
|
||||
for dec.More() {
|
||||
var p listedPackage
|
||||
if err := dec.Decode(&p); err != nil {
|
||||
return nil, fmt.Errorf("go list: %w", err)
|
||||
}
|
||||
pkgs = append(pkgs, p)
|
||||
}
|
||||
return pkgs, nil
|
||||
}
|
||||
|
||||
// idOnlyMeta is the metadata of a build that knows nothing but the IDs.
|
||||
func idOnlyMeta(m Manifest) buildMeta {
|
||||
infos := make([]party.Info, len(m.Plugins))
|
||||
for i, p := range m.Plugins {
|
||||
infos[i] = party.Info{ID: p.ID}
|
||||
}
|
||||
return buildMeta{Plugins: infos}
|
||||
}
|
||||
|
||||
// resolveBuildMeta resolves the plugin metadata and the effective
|
||||
// framework version. It never fails: what it cannot resolve stays empty
|
||||
// and comes back as a warning, so metadata never breaks a build.
|
||||
func resolveBuildMeta(ctx context.Context, appDir string, m Manifest) (buildMeta, []string) {
|
||||
infos, warnings := resolvePluginInfo(ctx, appDir, m)
|
||||
fw, err := EffectiveFramework(ctx, appDir)
|
||||
if err != nil {
|
||||
warnings = append(warnings, err.Error())
|
||||
}
|
||||
return buildMeta{Plugins: infos, Framework: fw}, warnings
|
||||
}
|
||||
|
||||
// resolvePluginInfo returns one party.Info per summer.yaml plugin, in
|
||||
// manifest order: module and package from go list, name and description
|
||||
// from the plugin's plugin.yaml, and the version per D-07. No filesystem
|
||||
// path is ever stored.
|
||||
func resolvePluginInfo(ctx context.Context, appDir string, m Manifest) ([]party.Info, []string) {
|
||||
infos := idOnlyMeta(m).Plugins
|
||||
if len(m.Plugins) == 0 {
|
||||
return infos, nil
|
||||
}
|
||||
paths := make([]string, len(m.Plugins))
|
||||
for i, p := range m.Plugins {
|
||||
paths[i] = p.Module
|
||||
}
|
||||
pkgs, err := listPackages(ctx, appDir, paths)
|
||||
if err != nil {
|
||||
return infos, []string{fmt.Sprintf("plugin metadata: %v; PluginInfo carries plugin IDs only", err)}
|
||||
}
|
||||
byPath := make(map[string]listedPackage, len(pkgs))
|
||||
for _, p := range pkgs {
|
||||
byPath[p.ImportPath] = p
|
||||
}
|
||||
var warnings []string
|
||||
for i, p := range m.Plugins {
|
||||
pkg, ok := byPath[p.Module]
|
||||
if !ok || pkg.Dir == "" {
|
||||
reason := "package not found"
|
||||
if ok && pkg.Error != nil {
|
||||
reason = pkg.Error.Err
|
||||
}
|
||||
warnings = append(warnings, fmt.Sprintf("plugin metadata: %s: %s", p.ID, reason))
|
||||
continue
|
||||
}
|
||||
info := &infos[i]
|
||||
info.Package = pkg.ImportPath
|
||||
if pkg.Module != nil {
|
||||
info.Module = pkg.Module.Path
|
||||
}
|
||||
if w := readPluginYAML(pkg.Dir, info); w != "" {
|
||||
warnings = append(warnings, fmt.Sprintf("plugin metadata: %s: %s", p.ID, w))
|
||||
}
|
||||
pluginVersion(ctx, pkg, info)
|
||||
}
|
||||
return infos, warnings
|
||||
}
|
||||
|
||||
// readPluginYAML copies name and description from dir/plugin.yaml into
|
||||
// info. It returns a warning when the file exists but cannot be used.
|
||||
func readPluginYAML(dir string, info *party.Info) string {
|
||||
raw, err := os.ReadFile(filepath.Join(dir, kiosk.FileName))
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return ""
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Sprintf("%s: %v; ignored", kiosk.FileName, err)
|
||||
}
|
||||
pm, err := kiosk.Parse(raw)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("%s: %v; ignored", kiosk.FileName, err)
|
||||
}
|
||||
if pm.ID != info.ID {
|
||||
return fmt.Sprintf("%s describes %s, not %s; ignored", kiosk.FileName, pm.ID, info.ID)
|
||||
}
|
||||
info.Name, info.Description = pm.Name, pm.Description
|
||||
if info.Module == "" {
|
||||
info.Module = pm.Module
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// pluginVersion sets info.Source and info.Version (D-07): the module
|
||||
// version for a plugin from the module cache, git describe for a plugin
|
||||
// directory that is its own repository, nothing for an in-app plugin.
|
||||
func pluginVersion(ctx context.Context, pkg listedPackage, info *party.Info) {
|
||||
mod := pkg.Module
|
||||
switch {
|
||||
case mod == nil:
|
||||
info.Source = party.SourceLocal
|
||||
case mod.Replace != nil && mod.Replace.Version != "":
|
||||
info.Source, info.Version = party.SourceModule, mod.Replace.Version
|
||||
case !mod.Main && mod.Replace == nil:
|
||||
info.Source, info.Version = party.SourceModule, mod.Version
|
||||
default:
|
||||
root := mod.Dir
|
||||
if root == "" {
|
||||
root = pkg.Dir
|
||||
}
|
||||
if isOwnRepository(ctx, root) {
|
||||
info.Source = party.SourceSubmodule
|
||||
info.Version = gitDescribe(ctx, root)
|
||||
} else {
|
||||
info.Source = party.SourceLocal
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// isOwnRepository reports whether dir is the top level of a git work tree
|
||||
// (its own repository or a submodule), not a directory inside the app's.
|
||||
func isOwnRepository(ctx context.Context, dir string) bool {
|
||||
out, err := gitOutput(ctx, dir, "rev-parse", "--show-toplevel")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return samePath(strings.TrimSpace(string(out)), dir)
|
||||
}
|
||||
|
||||
// gitDescribe is `git describe --tags --match v[0-9]* --always` in dir,
|
||||
// without --dirty: an uncommitted change in a plugin must not rewrite the
|
||||
// app's tracked plugins.gen.go (research Pitfall 6).
|
||||
func gitDescribe(ctx context.Context, dir string) string {
|
||||
out, err := gitOutput(ctx, dir, "describe", "--tags", "--match", "v[0-9]*", "--always")
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(string(out))
|
||||
}
|
||||
|
||||
func gitOutput(ctx context.Context, dir string, args ...string) ([]byte, error) {
|
||||
cmd := exec.CommandContext(ctx, "git", append([]string{"-C", dir}, args...)...)
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stderr = &stderr
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
msg := strings.TrimSpace(stderr.String())
|
||||
if msg == "" {
|
||||
msg = err.Error()
|
||||
}
|
||||
return nil, errors.New(msg)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func samePath(a, b string) bool {
|
||||
ra, err := filepath.EvalSymlinks(a)
|
||||
if err != nil {
|
||||
ra = a
|
||||
}
|
||||
rb, err := filepath.EvalSymlinks(b)
|
||||
if err != nil {
|
||||
rb = b
|
||||
}
|
||||
return filepath.Clean(ra) == filepath.Clean(rb)
|
||||
}
|
||||
|
||||
// goModJSON is the part of `go mod edit -json` and `go work edit -json`
|
||||
// output EffectiveFramework reads.
|
||||
type goModJSON struct {
|
||||
Require []struct {
|
||||
Path string
|
||||
Version string
|
||||
}
|
||||
Replace []struct {
|
||||
Old struct{ Path string }
|
||||
New struct{ Path, Version string }
|
||||
}
|
||||
Use []struct {
|
||||
DiskPath string
|
||||
}
|
||||
}
|
||||
|
||||
func editJSON(ctx context.Context, dir string, args ...string) (goModJSON, error) {
|
||||
cmd := exec.CommandContext(ctx, "go", args...)
|
||||
cmd.Dir = dir
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stderr = &stderr
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return goModJSON{}, fmt.Errorf("go %s: %w: %s", strings.Join(args, " "), err, bytes.TrimSpace(stderr.Bytes()))
|
||||
}
|
||||
var j goModJSON
|
||||
if err := json.Unmarshal(out, &j); err != nil {
|
||||
return goModJSON{}, fmt.Errorf("go %s: %w", strings.Join(args, " "), err)
|
||||
}
|
||||
return j, nil
|
||||
}
|
||||
|
||||
// EffectiveFramework returns the framework version an app in appDir builds
|
||||
// against (research F3). When go.work or go.mod replaces the framework with
|
||||
// a local directory, or go.work uses one, it is the newest tag reachable
|
||||
// from that checkout (`git describe --tags --abbrev=0`); a replace with a
|
||||
// module version gives that version; otherwise it is the go.mod require.
|
||||
// When git fails it returns the require version together with a non-nil
|
||||
// error the caller prints as a warning.
|
||||
func EffectiveFramework(ctx context.Context, appDir string) (string, error) {
|
||||
gomod, err := editJSON(ctx, appDir, "mod", "edit", "-json", filepath.Join(appDir, "go.mod"))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("framework version: %w", err)
|
||||
}
|
||||
required := ""
|
||||
for _, r := range gomod.Require {
|
||||
if r.Path == frameworkModule {
|
||||
required = r.Version
|
||||
}
|
||||
}
|
||||
localDir, version := "", ""
|
||||
for _, r := range gomod.Replace {
|
||||
if r.Old.Path == frameworkModule {
|
||||
localDir, version = r.New.Path, r.New.Version
|
||||
if version == "" && !filepath.IsAbs(localDir) {
|
||||
localDir = filepath.Join(appDir, localDir)
|
||||
}
|
||||
}
|
||||
}
|
||||
if gw := findGoWork(appDir); gw != "" {
|
||||
work, err := editJSON(ctx, appDir, "work", "edit", "-json", gw)
|
||||
if err != nil {
|
||||
return required, fmt.Errorf("framework version: %w; using the go.mod require %s", err, required)
|
||||
}
|
||||
base := filepath.Dir(gw)
|
||||
for _, u := range work.Use {
|
||||
dir := u.DiskPath
|
||||
if !filepath.IsAbs(dir) {
|
||||
dir = filepath.Join(base, dir)
|
||||
}
|
||||
if mod, err := readModulePath(filepath.Join(dir, "go.mod")); err == nil && mod == frameworkModule {
|
||||
localDir, version = dir, ""
|
||||
}
|
||||
}
|
||||
for _, r := range work.Replace {
|
||||
if r.Old.Path == frameworkModule {
|
||||
localDir, version = r.New.Path, r.New.Version
|
||||
if version == "" && !filepath.IsAbs(localDir) {
|
||||
localDir = filepath.Join(base, localDir)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
switch {
|
||||
case version != "":
|
||||
return version, nil
|
||||
case localDir == "":
|
||||
return required, nil
|
||||
}
|
||||
out, err := gitOutput(ctx, localDir, "describe", "--tags", "--abbrev=0", "--match", "v[0-9]*")
|
||||
if err != nil {
|
||||
return required, fmt.Errorf("framework version: git describe in %s: %v; using the go.mod require %s", localDir, err, required)
|
||||
}
|
||||
return strings.TrimSpace(string(out)), nil
|
||||
}
|
||||
242
internal/build/plugininfo_smoke_test.go
Normal file
242
internal/build/plugininfo_smoke_test.go
Normal file
@@ -0,0 +1,242 @@
|
||||
package build
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.golem15.com/golem15/summercms/modules/backpack"
|
||||
"git.golem15.com/golem15/summercms/modules/compass"
|
||||
"git.golem15.com/golem15/summercms/modules/party"
|
||||
)
|
||||
|
||||
// TestPluginInfoSmoke covers the build-time plugin metadata (D-06, D-07):
|
||||
// generation, plugin directory resolution, versions, the effective
|
||||
// framework version, the fallback and the runtime publication.
|
||||
func TestPluginInfoSmoke(t *testing.T) {
|
||||
m := Manifest{
|
||||
Module: "example.com/app",
|
||||
Binary: "app",
|
||||
Plugins: []Plugin{
|
||||
{ID: "acme.blog", Module: "example.com/sm-blog-plugin"},
|
||||
{ID: "acme.local", Module: "example.com/app/plugins/acme/local"},
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("generate", func(t *testing.T) {
|
||||
meta := buildMeta{
|
||||
Framework: "v0.6.0",
|
||||
Plugins: []party.Info{
|
||||
{ID: "acme.blog", Module: "example.com/sm-blog-plugin", Package: "example.com/sm-blog-plugin",
|
||||
Name: "Blog", Description: "Posts with \"quotes\",\nnewlines and `ticks`.", Version: "v0.2.5", Source: party.SourceModule},
|
||||
{ID: "acme.local", Package: "example.com/app/plugins/acme/local", Source: party.SourceLocal},
|
||||
},
|
||||
}
|
||||
first, err := generatePluginsGen(m, meta)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
second, err := generatePluginsGen(m, meta)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(first, second) {
|
||||
t.Fatal("plugins.gen.go is not byte-stable")
|
||||
}
|
||||
src := string(first)
|
||||
for _, want := range []string{
|
||||
`"git.golem15.com/golem15/summercms/modules/party"`,
|
||||
"var PluginInfo = []party.Info{",
|
||||
`{ID: "acme.blog", Module: "example.com/sm-blog-plugin", Package: "example.com/sm-blog-plugin", Name: "Blog", Description: "Posts with \"quotes\",\nnewlines and ` + "`ticks`" + `.", Version: "v0.2.5", Source: party.SourceModule},`,
|
||||
`{ID: "acme.local", Package: "example.com/app/plugins/acme/local", Source: party.SourceLocal},`,
|
||||
`const FrameworkVersion = "v0.6.0"`,
|
||||
} {
|
||||
if !strings.Contains(src, want) {
|
||||
t.Errorf("plugins.gen.go lacks %s\n%s", want, src)
|
||||
}
|
||||
}
|
||||
if strings.Index(src, `ID: "acme.blog"`) > strings.Index(src, `ID: "acme.local"`) {
|
||||
t.Error("PluginInfo is not in summer.yaml order")
|
||||
}
|
||||
if strings.Index(src, "var PluginIDs") > strings.Index(src, "var PluginInfo") {
|
||||
t.Error("PluginInfo comes before PluginIDs")
|
||||
}
|
||||
|
||||
// Missing metadata degrades to ID-only entries.
|
||||
idOnly, err := generatePluginsGen(m, buildMeta{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(string(idOnly), `{ID: "acme.blog"},`) || !strings.Contains(string(idOnly), `const FrameworkVersion = ""`) {
|
||||
t.Errorf("ID-only plugins.gen.go:\n%s", idOnly)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("main", func(t *testing.T) {
|
||||
src, err := generateMain(m)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
describe := strings.Index(string(src), "if err := party.Describe(app, PluginInfo, FrameworkVersion); err != nil {")
|
||||
activate := strings.Index(string(src), "party.Activate(app, PluginIDs)")
|
||||
if describe < 0 || activate < 0 || describe > activate {
|
||||
t.Fatalf("Describe must come before Activate:\n%s", src)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("dirs", func(t *testing.T) {
|
||||
requireGit(t)
|
||||
app := t.TempDir()
|
||||
write(t, filepath.Join(app, "go.mod"), "module example.test/app\n\ngo 1.27.0\n\n"+
|
||||
"require (\n\texample.test/acme/inapp v0.0.0\n\texample.test/acme/sub v0.0.0\n)\n\n"+
|
||||
"replace example.test/acme/inapp => ./plugins/acme/inapp\n\nreplace example.test/acme/sub => ./plugins/acme/sub\n")
|
||||
write(t, filepath.Join(app, "main.go"), "package main\n\nimport (\n\t_ \"example.test/acme/inapp\"\n\t_ \"example.test/acme/sub\"\n)\n\nfunc main() {}\n")
|
||||
for _, name := range []string{"inapp", "sub"} {
|
||||
dir := filepath.Join(app, "plugins", "acme", name)
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
write(t, filepath.Join(dir, "go.mod"), "module example.test/acme/"+name+"\n\ngo 1.27.0\n")
|
||||
write(t, filepath.Join(dir, "plugin.go"), "package "+name+"\n")
|
||||
}
|
||||
inapp := filepath.Join(app, "plugins", "acme", "inapp")
|
||||
sub := filepath.Join(app, "plugins", "acme", "sub")
|
||||
write(t, filepath.Join(inapp, "plugin.yaml"), "schema: [not, a, manifest\n")
|
||||
write(t, filepath.Join(sub, "plugin.yaml"), "# Generated by summer plugin:manifest. Do not edit; rerun the command.\n"+
|
||||
"schema: 1\nid: acme.sub\nmodule: example.test/acme/sub\npackage: example.test/acme/sub\n"+
|
||||
"name: Sub\ndescription: A plugin in its own repository.\nframework: v0.6.0\n")
|
||||
runTestGit(t, app, "init", "-q")
|
||||
runTestGit(t, sub, "init", "-q")
|
||||
runTestGit(t, sub, "add", ".")
|
||||
runTestGit(t, sub, "commit", "-q", "-m", "first")
|
||||
runTestGit(t, sub, "tag", "v0.1.0")
|
||||
write(t, filepath.Join(sub, "extra.go"), "package sub\n")
|
||||
runTestGit(t, sub, "add", ".")
|
||||
runTestGit(t, sub, "commit", "-q", "-m", "second")
|
||||
// Uncommitted change: the version must not carry -dirty.
|
||||
write(t, filepath.Join(sub, "plugin.go"), "package sub\n\n// changed\n")
|
||||
|
||||
manifest := Manifest{Module: "example.test/app", Binary: "app", Plugins: []Plugin{
|
||||
{ID: "acme.inapp", Module: "example.test/acme/inapp"},
|
||||
{ID: "acme.sub", Module: "example.test/acme/sub"},
|
||||
}}
|
||||
infos, warnings := resolvePluginInfo(t.Context(), app, manifest)
|
||||
if len(infos) != 2 {
|
||||
t.Fatalf("infos = %+v", infos)
|
||||
}
|
||||
in, sb := infos[0], infos[1]
|
||||
if in.ID != "acme.inapp" || in.Source != party.SourceLocal || in.Version != "" || in.Name != "" ||
|
||||
in.Module != "example.test/acme/inapp" || in.Package != "example.test/acme/inapp" {
|
||||
t.Errorf("in-app plugin = %+v", in)
|
||||
}
|
||||
if sb.ID != "acme.sub" || sb.Source != party.SourceSubmodule || !strings.HasPrefix(sb.Version, "v0.1.0-1-g") ||
|
||||
strings.Contains(sb.Version, "dirty") || sb.Name != "Sub" || sb.Description != "A plugin in its own repository." {
|
||||
t.Errorf("own-repository plugin = %+v", sb)
|
||||
}
|
||||
if len(warnings) != 1 || !strings.Contains(warnings[0], "plugin.yaml") {
|
||||
t.Errorf("warnings = %q, want one about the malformed plugin.yaml", warnings)
|
||||
}
|
||||
for _, info := range infos {
|
||||
for _, s := range []string{info.Module, info.Package, info.Version, info.Name, info.Description} {
|
||||
if strings.Contains(s, app) {
|
||||
t.Errorf("a filesystem path leaked into %+v", info)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("framework", func(t *testing.T) {
|
||||
requireGit(t)
|
||||
ctx := t.Context()
|
||||
|
||||
fw := t.TempDir()
|
||||
write(t, filepath.Join(fw, "go.mod"), "module "+frameworkModule+"\n\ngo 1.27.0\n")
|
||||
runTestGit(t, fw, "init", "-q")
|
||||
runTestGit(t, fw, "add", ".")
|
||||
runTestGit(t, fw, "commit", "-q", "-m", "one")
|
||||
runTestGit(t, fw, "tag", "v0.5.3")
|
||||
write(t, filepath.Join(fw, "a.go"), "package summercms\n")
|
||||
runTestGit(t, fw, "add", ".")
|
||||
runTestGit(t, fw, "commit", "-q", "-m", "two")
|
||||
runTestGit(t, fw, "tag", "v0.6.0")
|
||||
write(t, filepath.Join(fw, "b.go"), "package summercms\n")
|
||||
runTestGit(t, fw, "add", ".")
|
||||
runTestGit(t, fw, "commit", "-q", "-m", "three")
|
||||
|
||||
replaced := t.TempDir()
|
||||
write(t, filepath.Join(replaced, "go.mod"), "module example.test/app\n\ngo 1.27.0\n\nrequire "+frameworkModule+" v0.0.0\n\nreplace "+frameworkModule+" => "+fw+"\n")
|
||||
if v, err := EffectiveFramework(ctx, replaced); err != nil || v != "v0.6.0" {
|
||||
t.Errorf("replaced framework = %q, %v; want v0.6.0", v, err)
|
||||
}
|
||||
|
||||
required := t.TempDir()
|
||||
write(t, filepath.Join(required, "go.mod"), "module example.test/app\n\ngo 1.27.0\n\nrequire "+frameworkModule+" v0.5.3\n")
|
||||
if v, err := EffectiveFramework(ctx, required); err != nil || v != "v0.5.3" {
|
||||
t.Errorf("required framework = %q, %v; want v0.5.3", v, err)
|
||||
}
|
||||
|
||||
notGit := t.TempDir()
|
||||
noRepo := t.TempDir()
|
||||
write(t, filepath.Join(noRepo, "go.mod"), "module "+frameworkModule+"\n\ngo 1.27.0\n")
|
||||
write(t, filepath.Join(notGit, "go.mod"), "module example.test/app\n\ngo 1.27.0\n\nrequire "+frameworkModule+" v0.5.1\n\nreplace "+frameworkModule+" => "+noRepo+"\n")
|
||||
if v, err := EffectiveFramework(ctx, notGit); err == nil || v != "v0.5.1" {
|
||||
t.Errorf("framework without git = %q, %v; want v0.5.1 and a warning", v, err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("fallback", func(t *testing.T) {
|
||||
dir := copyHelloApp(t)
|
||||
orig := listPackages
|
||||
listPackages = func(context.Context, string, []string) ([]listedPackage, error) {
|
||||
return nil, errors.New("go list failed")
|
||||
}
|
||||
t.Cleanup(func() { listPackages = orig })
|
||||
var out bytes.Buffer
|
||||
if err := App(t.Context(), dir, &out); err != nil {
|
||||
t.Fatalf("build with failing metadata: %v\n%s", err, out.String())
|
||||
}
|
||||
if n := strings.Count(out.String(), "warning:"); n != 1 {
|
||||
t.Errorf("warnings = %d, want 1:\n%s", n, out.String())
|
||||
}
|
||||
gen, err := os.ReadFile(filepath.Join(dir, "plugins.gen.go"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(string(gen), `{ID: "golem15.hello"},`) || !strings.Contains(string(gen), "const FrameworkVersion = ") {
|
||||
t.Errorf("plugins.gen.go after a metadata failure:\n%s", gen)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("describe", func(t *testing.T) {
|
||||
app := backpack.New(&compass.Config{})
|
||||
plugins := []party.Info{{ID: "acme.blog", Name: "Blog", Version: "v0.2.5", Source: party.SourceModule}}
|
||||
if err := party.Describe(app, plugins, "v0.6.0"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, ok := app.Lookup[party.Installed]()
|
||||
if !ok || got.Framework != "v0.6.0" || len(got.Plugins) != 1 || got.Plugins[0] != plugins[0] {
|
||||
t.Fatalf("Installed = %+v, %v", got, ok)
|
||||
}
|
||||
if err := party.Describe(nil, plugins, "v0.6.0"); err == nil {
|
||||
t.Fatal("Describe(nil app) succeeded")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// git runs git in dir with a test-local identity and no signing.
|
||||
func runTestGit(t *testing.T, dir string, args ...string) {
|
||||
t.Helper()
|
||||
full := append([]string{"-c", "user.name=Test", "-c", "user.email=test@example.test",
|
||||
"-c", "commit.gpgsign=false", "-c", "tag.gpgsign=false", "-c", "init.defaultBranch=master"}, args...)
|
||||
cmd := exec.CommandContext(t.Context(), "git", full...)
|
||||
cmd.Dir = dir
|
||||
cmd.Env = append(os.Environ(), "GIT_CONFIG_NOSYSTEM=1", "GIT_CONFIG_GLOBAL=/dev/null")
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
t.Fatalf("git %s: %v\n%s", strings.Join(args, " "), err, out)
|
||||
}
|
||||
}
|
||||
@@ -2,5 +2,16 @@
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"git.golem15.com/golem15/summercms/modules/party"
|
||||
)
|
||||
|
||||
// PluginIDs is the ordered plugin list from summer.yaml.
|
||||
var PluginIDs = []string{}
|
||||
|
||||
// PluginInfo is each plugin's metadata in summer.yaml order: name and
|
||||
// description from its plugin.yaml, version and source from summer build.
|
||||
var PluginInfo = []party.Info{}
|
||||
|
||||
// FrameworkVersion is the framework version the app was built against.
|
||||
const FrameworkVersion = ""
|
||||
|
||||
3
internal/build/testdata/new/main.go.golden
vendored
3
internal/build/testdata/new/main.go.golden
vendored
@@ -34,6 +34,9 @@ func run(args []string, out io.Writer) int {
|
||||
return bonfire.Fail(os.Stderr, "demo", err, g)
|
||||
}
|
||||
app := backpack.New(cfg)
|
||||
if err := party.Describe(app, PluginInfo, FrameworkVersion); err != nil {
|
||||
return bonfire.Fail(os.Stderr, "demo", err, g)
|
||||
}
|
||||
plugins, err := party.Activate(app, PluginIDs)
|
||||
if err != nil {
|
||||
return bonfire.Fail(os.Stderr, "demo", err, g)
|
||||
|
||||
@@ -4,9 +4,20 @@ package main
|
||||
|
||||
import (
|
||||
_ "git.golem15.com/golem15/sm-demo-app/plugins/golem15/demo"
|
||||
|
||||
"git.golem15.com/golem15/summercms/modules/party"
|
||||
)
|
||||
|
||||
// PluginIDs is the ordered plugin list from summer.yaml.
|
||||
var PluginIDs = []string{
|
||||
"golem15.demo",
|
||||
}
|
||||
|
||||
// PluginInfo is each plugin's metadata in summer.yaml order: name and
|
||||
// description from its plugin.yaml, version and source from summer build.
|
||||
var PluginInfo = []party.Info{
|
||||
{ID: "golem15.demo"},
|
||||
}
|
||||
|
||||
// FrameworkVersion is the framework version the app was built against.
|
||||
const FrameworkVersion = ""
|
||||
|
||||
Reference in New Issue
Block a user