fix(34): WR-01 keep summer build working for frameworks before party.Describe
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.
This commit is contained in:
@@ -80,7 +80,7 @@ func goBuildArgs(binRel string, tags []string) []string {
|
||||
}
|
||||
|
||||
func generate(appDir string, m Manifest, meta buildMeta) error {
|
||||
mainSrc, err := generateMain(m)
|
||||
mainSrc, err := generateMainFor(m, !meta.Legacy)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -97,7 +97,14 @@ func generate(appDir string, m Manifest, meta buildMeta) error {
|
||||
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")
|
||||
@@ -128,7 +135,9 @@ 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")
|
||||
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")
|
||||
@@ -151,8 +160,13 @@ func generateMain(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.
|
||||
// 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")
|
||||
@@ -187,6 +201,28 @@ func generatePluginsGen(m Manifest, meta buildMeta) ([]byte, error) {
|
||||
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 {
|
||||
|
||||
@@ -6,6 +6,9 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
@@ -20,8 +23,16 @@ import (
|
||||
type buildMeta struct {
|
||||
Plugins []party.Info
|
||||
Framework string
|
||||
// Legacy is set when the framework the application builds against has
|
||||
// no party.Describe (before v0.6.0): the generated sources then leave
|
||||
// the metadata out, as they did before, so the application still
|
||||
// compiles (WR-01).
|
||||
Legacy bool
|
||||
}
|
||||
|
||||
// partyPackage is the framework package that holds party.Describe.
|
||||
const partyPackage = frameworkModule + "/modules/party"
|
||||
|
||||
// listedPackage is the part of `go list -json` output the metadata step
|
||||
// reads for one plugin package.
|
||||
type listedPackage struct {
|
||||
@@ -84,6 +95,12 @@ func idOnlyMeta(m Manifest) buildMeta {
|
||||
// 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) {
|
||||
if !frameworkDescribes(ctx, appDir) {
|
||||
fw, _ := EffectiveFramework(ctx, appDir)
|
||||
return buildMeta{Framework: fw, Legacy: true}, []string{fmt.Sprintf(
|
||||
"framework %s has no party.Describe (added in v0.6.0); the generated sources carry no plugin metadata, so the admin's Plugins screen lists plugin IDs only. Update the framework requirement to v0.6.0 or newer to get it.",
|
||||
displayVersion(fw))}
|
||||
}
|
||||
infos, warnings := resolvePluginInfo(ctx, appDir, m)
|
||||
fw, err := EffectiveFramework(ctx, appDir)
|
||||
if err != nil {
|
||||
@@ -92,6 +109,64 @@ func resolveBuildMeta(ctx context.Context, appDir string, m Manifest) (buildMeta
|
||||
return buildMeta{Plugins: infos, Framework: fw}, warnings
|
||||
}
|
||||
|
||||
// frameworkDescribes reports whether the framework the application builds
|
||||
// against provides party.Describe and party.Info: the party package that
|
||||
// go list resolves in appDir (go.mod, its replaces and go.work alike)
|
||||
// declares both. The check is static, not by version, because a local
|
||||
// framework checkout reports the previous tag until it is tagged. When
|
||||
// the package cannot be resolved it answers true: the sources stay the
|
||||
// current ones and go build reports the real problem.
|
||||
func frameworkDescribes(ctx context.Context, appDir string) bool {
|
||||
pkgs, err := listPackages(ctx, appDir, []string{partyPackage})
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
for _, p := range pkgs {
|
||||
if p.ImportPath != partyPackage || p.Dir == "" || p.Error != nil {
|
||||
continue
|
||||
}
|
||||
return declaresDescribe(p.Dir)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// declaresDescribe reports whether the Go package in dir declares the
|
||||
// function Describe and the type Info; a directory it cannot read
|
||||
// answers true.
|
||||
func declaresDescribe(dir string) bool {
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
describe, info := false, false
|
||||
fset := token.NewFileSet()
|
||||
for _, e := range entries {
|
||||
name := e.Name()
|
||||
if e.IsDir() || !strings.HasSuffix(name, ".go") || strings.HasSuffix(name, "_test.go") {
|
||||
continue
|
||||
}
|
||||
f, err := parser.ParseFile(fset, filepath.Join(dir, name), nil, parser.SkipObjectResolution)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, d := range f.Decls {
|
||||
switch d := d.(type) {
|
||||
case *ast.FuncDecl:
|
||||
if d.Recv == nil && d.Name.Name == "Describe" {
|
||||
describe = true
|
||||
}
|
||||
case *ast.GenDecl:
|
||||
for _, spec := range d.Specs {
|
||||
if ts, ok := spec.(*ast.TypeSpec); ok && ts.Name.Name == "Info" {
|
||||
info = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return describe && info
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
@@ -301,3 +301,68 @@ func TestEffectiveFramework(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestLegacyFramework pins WR-01: an application whose framework has no
|
||||
// party.Describe (before v0.6.0) gets the sources summer build wrote
|
||||
// before, without the plugin metadata, so it still compiles.
|
||||
func TestLegacyFramework(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
app := func(t *testing.T, partySrc string) string {
|
||||
t.Helper()
|
||||
root := t.TempDir()
|
||||
fw := filepath.Join(root, "framework")
|
||||
writeAll(t, filepath.Join(fw, "go.mod"), "module "+frameworkModule+"\n\ngo 1.27.0\n")
|
||||
writeAll(t, filepath.Join(fw, "modules", "party", "party.go"), partySrc)
|
||||
writeAll(t, filepath.Join(fw, "modules", "party", "party_test.go"), "package party\n\nfunc Describe() {}\n\ntype Info struct{}\n")
|
||||
dir := filepath.Join(root, "app")
|
||||
writeAll(t, filepath.Join(dir, "go.mod"), "module example.test/app\n\ngo 1.27.0\n\nrequire "+frameworkModule+" v0.5.3\n\nreplace "+frameworkModule+" => ../framework\n")
|
||||
return dir
|
||||
}
|
||||
m := Manifest{Module: "example.test/app", Binary: "app", Plugins: []Plugin{{ID: "acme.blog", Module: "example.test/blog"}}}
|
||||
|
||||
t.Run("before party.Describe", func(t *testing.T) {
|
||||
meta, warnings := resolveBuildMeta(ctx, app(t, "package party\n\nfunc Activate() {}\n"), Manifest{Module: "example.test/app", Binary: "app"})
|
||||
if !meta.Legacy || len(warnings) != 1 || !strings.Contains(warnings[0], "has no party.Describe") || !strings.Contains(warnings[0], "v0.6.0") {
|
||||
t.Fatalf("meta %+v, warnings %q", meta, warnings)
|
||||
}
|
||||
mainSrc, err := generateMainFor(m, !meta.Legacy)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
gen, err := generatePluginsGen(m, meta)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Contains(string(mainSrc), "party.Describe") || !strings.Contains(string(mainSrc), "party.Activate(app, PluginIDs)") {
|
||||
t.Errorf("main.go:\n%s", mainSrc)
|
||||
}
|
||||
want := "// Code generated by summer build. DO NOT EDIT.\n\npackage main\n\nimport (\n\t_ \"example.test/blog\"\n)\n\n" +
|
||||
"// PluginIDs is the ordered plugin list from summer.yaml.\nvar PluginIDs = []string{\n\t\"acme.blog\",\n}\n"
|
||||
if string(gen) != want {
|
||||
t.Errorf("plugins.gen.go:\n%s\nwant:\n%s", gen, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("with party.Describe", func(t *testing.T) {
|
||||
src := "package party\n\ntype Info struct{ ID string }\n\nfunc Describe(any, []Info, string) error { return nil }\n"
|
||||
meta, warnings := resolveBuildMeta(ctx, app(t, src), Manifest{Module: "example.test/app", Binary: "app"})
|
||||
if meta.Legacy {
|
||||
t.Fatalf("meta %+v, warnings %q", meta, warnings)
|
||||
}
|
||||
mainSrc, err := generateMainFor(m, !meta.Legacy)
|
||||
if err != nil || !strings.Contains(string(mainSrc), "party.Describe(app, PluginInfo, FrameworkVersion)") {
|
||||
t.Errorf("main.go: %v\n%s", err, mainSrc)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("unresolvable party package keeps the current sources", func(t *testing.T) {
|
||||
orig := listPackages
|
||||
listPackages = func(context.Context, string, []string) ([]listedPackage, error) {
|
||||
return nil, os.ErrPermission
|
||||
}
|
||||
t.Cleanup(func() { listPackages = orig })
|
||||
if !frameworkDescribes(ctx, t.TempDir()) {
|
||||
t.Error("a failing go list switched to the legacy sources")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user