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:
@@ -85,6 +85,10 @@ files: every build rewrites them.`,
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
if rest, ok := strings.CutPrefix(line, "warning: "); ok {
|
||||
out.Warning(strings.TrimSuffix(rest, "\n"))
|
||||
continue
|
||||
}
|
||||
if rest, ok := strings.CutPrefix(line, "built "); ok && err == nil {
|
||||
out.Success("Built bin/" + strings.TrimSuffix(rest, "\n"))
|
||||
continue
|
||||
|
||||
@@ -86,6 +86,13 @@ build:
|
||||
|
||||
`summer build` turns this list into `plugins.gen.go`. The order is the manifest order, adjusted so that each plugin comes after the plugins it requires. Plugins with no dependency between them keep the order you wrote.
|
||||
|
||||
`plugins.gen.go` also carries what the build knew about each plugin and the framework:
|
||||
|
||||
- `PluginInfo`, one `party.Info` per plugin in manifest order: the ID, the Go module and package, the name and description from the plugin's `plugin.yaml` (see [Publishing plugins](../plugins/marketplace.md)), the version and the source. A plugin from the module cache (`go get`) reports its module version and the source `module`. A plugin directory that is its own git repository, such as a submodule, reports `git describe --tags` output like `v0.3.1-4-gabc1234` and the source `submodule`; an uncommitted change does not alter it. A plugin inside the application's own repository has no version and the source `local`. A plugin without `plugin.yaml` has no name or description.
|
||||
- `FrameworkVersion`, the framework version the application was built against. When `go.mod` or `go.work` points the framework at a local checkout, it is the newest tag reachable in that checkout; otherwise it is the version `go.mod` requires.
|
||||
|
||||
The generated `main` publishes both with `party.Describe` before activating the plugins, so the admin can show the installed plugins with `app.Lookup[party.Installed]()`. When the metadata cannot be resolved, `summer build` prints a warning and bakes the IDs only; it never fails a build. A submodule bump or a new framework tag therefore changes `plugins.gen.go`: commit the regenerated file with the bump.
|
||||
|
||||
`build.tags` lists Go build tags that `summer build` (and `summer dev`) passes to `go build` as `-tags=<tag>,<tag>`, printing them as `build tags: …`. Each tag must consist of lower-case letters, digits and underscores; a duplicate is ignored. Framework features that pull in large dependencies sit behind a tag, such as `summercms_s3` for S3 uploads buckets (see [Storage](../services/storage.md#s3-and-s3-compatible-buckets)). `summer plugin:add` keeps the list when it rewrites the manifest. A script that runs its own `go build` must pass the same tags.
|
||||
|
||||
## Local plugins in a workspace
|
||||
|
||||
@@ -88,6 +88,10 @@ func TestPluginManifest(t *testing.T) {
|
||||
|
||||
`kiosk.CheckT` runs the same check as `summer plugin:manifest --check` and names the command to rerun. A nested plugin gets its own test in its own directory.
|
||||
|
||||
## Installed plugin metadata
|
||||
|
||||
`summer build` reads the `plugin.yaml` of every plugin the application lists and bakes the name and description into the generated `plugins.gen.go` as `PluginInfo`, next to each plugin's version and the framework version (`FrameworkVersion`). At start-up the application publishes them as `party.Installed`, which the admin's Plugins screen reads. [Go modules and workspaces](../architecture/go-modules-and-workspaces.md#the-summeryaml-manifest) explains where the versions come from.
|
||||
|
||||
## Registry API v1
|
||||
|
||||
summercms.io publishes the plugins it lists through a read-only JSON API. The marketplace pages, `summer` and the admin's Plugins screen read it. Its base path is `/api/registry/v1` (`kiosk.RegistryAPIPath`), and the `kiosk` module holds its wire types, so every client and the server share one definition of the shape.
|
||||
|
||||
@@ -34,6 +34,9 @@ func run(args []string, out io.Writer) int {
|
||||
return bonfire.Fail(os.Stderr, "hello", err, g)
|
||||
}
|
||||
app := backpack.New(cfg)
|
||||
if err := party.Describe(app, PluginInfo, FrameworkVersion); err != nil {
|
||||
return bonfire.Fail(os.Stderr, "hello", err, g)
|
||||
}
|
||||
plugins, err := party.Activate(app, PluginIDs)
|
||||
if err != nil {
|
||||
return bonfire.Fail(os.Stderr, "hello", err, g)
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
_ "git.golem15.com/golem15/summercms/examples/hello/plugins/base"
|
||||
_ "git.golem15.com/golem15/summercms/examples/hello/plugins/greeter"
|
||||
_ "git.golem15.com/golem15/summercms/examples/hello/plugins/optional"
|
||||
|
||||
"git.golem15.com/golem15/summercms/modules/party"
|
||||
)
|
||||
|
||||
// PluginIDs is the ordered plugin list from summer.yaml.
|
||||
@@ -14,3 +16,14 @@ var PluginIDs = []string{
|
||||
"golem15.greeter",
|
||||
"golem15.optional",
|
||||
}
|
||||
|
||||
// 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.hello", Module: "git.golem15.com/golem15/summercms/examples/hello/plugins/base", Package: "git.golem15.com/golem15/summercms/examples/hello/plugins/base", Source: party.SourceLocal},
|
||||
{ID: "golem15.greeter", Module: "git.golem15.com/golem15/summercms/examples/hello/plugins/greeter", Package: "git.golem15.com/golem15/summercms/examples/hello/plugins/greeter", Source: party.SourceLocal},
|
||||
{ID: "golem15.optional", Module: "git.golem15.com/golem15/summercms/examples/hello/plugins/optional", Package: "git.golem15.com/golem15/summercms/examples/hello/plugins/optional", Source: party.SourceLocal},
|
||||
}
|
||||
|
||||
// FrameworkVersion is the framework version the app was built against.
|
||||
const FrameworkVersion = "v0.5.3"
|
||||
|
||||
@@ -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 = ""
|
||||
|
||||
@@ -26,6 +26,7 @@ Use cases:
|
||||
- A process-wide, concurrency-safe registry filled by `party.Register` (nil plugins are ignored).
|
||||
- `party.Activate` selects plugins by manifest ID and fails on an empty ID, a duplicate ID, an unregistered plugin, a missing requirement or a dependency cycle.
|
||||
- Stable topological ordering: plugins without a dependency relation keep their manifest order.
|
||||
- Build-time metadata: `party.Describe` publishes `party.Installed`, the plugin metadata (`party.Info`: ID, module, package, name, description, version and source) and the framework version that `summer build` baked into the application's `plugins.gen.go`. The generated `main` calls it before `party.Activate`.
|
||||
- Activation sequence: records the ordered IDs on the `backpack.App`, publishes `party.Activated` (the ordered plugin list for commands that need plugin scopes at Run time), merges each `pact.HasConfig` tree into the app config under the plugin ID, runs every Register, publishes the translator ([phrasebook](../phrasebook/README.md)), the mailer ([postcard](../postcard/README.md)) and the menu item type registry ([signpost](../signpost/README.md), collected from every `pact.HasMenuItemTypes` plugin; a duplicate type key fails the activation), then registers each plugin's mail templates and runs its Boot.
|
||||
|
||||
## Usage
|
||||
@@ -52,7 +53,7 @@ func init() {
|
||||
}
|
||||
```
|
||||
|
||||
The application activates the plugins it lists, in dependency order:
|
||||
The application publishes its build-time metadata and activates the plugins it lists, in dependency order (`summer build` generates both lists):
|
||||
|
||||
```go
|
||||
cfg, err := compass.Load("config")
|
||||
@@ -60,6 +61,13 @@ if err != nil {
|
||||
return err
|
||||
}
|
||||
app := backpack.New(cfg)
|
||||
info := []party.Info{
|
||||
{ID: "acme.user", Source: party.SourceLocal},
|
||||
{ID: "acme.blog", Version: "v0.2.5", Source: party.SourceModule},
|
||||
}
|
||||
if err := party.Describe(app, info, "v0.6.0"); err != nil {
|
||||
return err
|
||||
}
|
||||
plugins, err := party.Activate(app, []string{"acme.user", "acme.blog"})
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -73,6 +81,12 @@ if err != nil {
|
||||
| `party.Plugin` | Interface every compiled plugin implements: ID, required plugin IDs, Register and Boot. |
|
||||
| `party.Register` | Adds a plugin to the process-wide registry; called from the plugin's `init`. |
|
||||
| `party.Activate` | Selects registered plugins by ID, orders them by `party.Plugin.Requires` and runs the config, Register, translation, mail, menu item type and Boot steps; returns the ordered plugins. |
|
||||
| `party.Info` | One plugin's build-time metadata: `ID`, `Module`, `Package`, `Name`, `Description` (from its `plugin.yaml`), `Version` and `Source`. |
|
||||
| `party.SourceModule` | Source of a plugin built from the Go module cache; its version is the module version. |
|
||||
| `party.SourceSubmodule` | Source of a plugin directory that is its own git repository; its version is `git describe --tags` output. |
|
||||
| `party.SourceLocal` | Source of a plugin inside the application repository; it has no version. |
|
||||
| `party.Installed` | What the application was built with: `Framework` and `Plugins` in manifest order; look it up with `app.Lookup[party.Installed]()`. |
|
||||
| `party.Describe` | Publishes `party.Installed` on the app; a nil app is an error. |
|
||||
| `party.Activated` | The ordered `[]party.Plugin` published by `party.Activate`; commands look it up at Run time with `app.Lookup[party.Activated]()`. |
|
||||
|
||||
## Dependencies
|
||||
|
||||
56
modules/party/info.go
Normal file
56
modules/party/info.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package party
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.golem15.com/golem15/summercms/modules/backpack"
|
||||
)
|
||||
|
||||
// Where an installed plugin's source came from at build time.
|
||||
const (
|
||||
// SourceModule is a plugin built from the Go module cache (go get).
|
||||
SourceModule = "module"
|
||||
// SourceSubmodule is a plugin directory that is its own git repository,
|
||||
// such as a git submodule; its version is git describe output.
|
||||
SourceSubmodule = "submodule"
|
||||
// SourceLocal is a plugin directory inside the application repository;
|
||||
// it has no version of its own.
|
||||
SourceLocal = "local"
|
||||
)
|
||||
|
||||
// Info is the metadata summer build bakes into plugins.gen.go for one
|
||||
// plugin: its ID, Go module and package, the name and description from its
|
||||
// plugin.yaml, its version and where it came from (SourceModule,
|
||||
// SourceSubmodule or SourceLocal). A plugin without plugin.yaml has no
|
||||
// name or description.
|
||||
type Info struct {
|
||||
ID string
|
||||
Module string
|
||||
Package string
|
||||
Name string
|
||||
Description string
|
||||
Version string
|
||||
Source string
|
||||
}
|
||||
|
||||
// Installed is what the application was built with: the effective
|
||||
// framework version and the plugin metadata in summer.yaml order. Describe
|
||||
// publishes it; look it up with app.Lookup[party.Installed]().
|
||||
type Installed struct {
|
||||
Framework string
|
||||
Plugins []Info
|
||||
}
|
||||
|
||||
// Describe publishes the build-time plugin metadata and framework version
|
||||
// as Installed. The generated main calls it before Activate.
|
||||
func Describe(app *backpack.App, plugins []Info, framework string) error {
|
||||
if app == nil {
|
||||
return fmt.Errorf("party: app is nil")
|
||||
}
|
||||
list := make([]Info, len(plugins))
|
||||
copy(list, plugins)
|
||||
if err := app.Publish(Installed{Framework: framework, Plugins: list}); err != nil {
|
||||
return fmt.Errorf("party: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user