- kiosk.ReadIdentity: ID()/Requires() as literals or constants across packages and modules, read with go/parser, never run - plugin minimums from go.mod requires; v0.0.0 is an error, a pseudo-version warns - kiosk.Validate, kiosk.CheckT (no testing import) - plugin:add reads IDs through kiosk.ReadIdentity, so const-ID plugins register
113 lines
3.2 KiB
Go
113 lines
3.2 KiB
Go
package kiosk
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
)
|
|
|
|
// Generate derives the manifest of the plugin in dir and returns the
|
|
// plugin.yaml bytes and any warnings, without writing anything.
|
|
func Generate(ctx context.Context, dir string, opts DeriveOptions) ([]byte, []string, error) {
|
|
m, warnings, err := Derive(ctx, dir, opts)
|
|
if err != nil {
|
|
return nil, warnings, err
|
|
}
|
|
if err := Validate(m); err != nil {
|
|
return nil, warnings, err
|
|
}
|
|
out, err := Marshal(m)
|
|
if err != nil {
|
|
return nil, warnings, err
|
|
}
|
|
return out, warnings, nil
|
|
}
|
|
|
|
// Write generates dir's plugin.yaml and writes it, returning its path.
|
|
func Write(ctx context.Context, dir string, opts DeriveOptions) (path string, warnings []string, err error) {
|
|
out, warnings, err := Generate(ctx, dir, opts)
|
|
if err != nil {
|
|
return "", warnings, err
|
|
}
|
|
path = filepath.Join(dir, FileName)
|
|
if err := os.WriteFile(path, out, 0o644); err != nil {
|
|
return "", warnings, fmt.Errorf("kiosk: write %s: %w", path, err)
|
|
}
|
|
return path, warnings, nil
|
|
}
|
|
|
|
// StaleError reports a plugin.yaml that is missing or differs from what
|
|
// summer plugin:manifest would write now.
|
|
type StaleError struct {
|
|
// Path is the plugin.yaml path.
|
|
Path string
|
|
// Missing is true when the file does not exist.
|
|
Missing bool
|
|
// Want is the freshly generated content; Got is the committed one.
|
|
Want, Got []byte
|
|
}
|
|
|
|
// Error names the path and, for a stale file, the first differing line.
|
|
func (e *StaleError) Error() string {
|
|
if e.Missing {
|
|
return fmt.Sprintf("kiosk: %s is missing; generate it with summer plugin:manifest", e.Path)
|
|
}
|
|
want := bytes.Split(e.Want, []byte("\n"))
|
|
got := bytes.Split(e.Got, []byte("\n"))
|
|
for i := 0; i < len(want) || i < len(got); i++ {
|
|
var w, g []byte
|
|
if i < len(want) {
|
|
w = want[i]
|
|
}
|
|
if i < len(got) {
|
|
g = got[i]
|
|
}
|
|
if !bytes.Equal(w, g) {
|
|
return fmt.Sprintf("kiosk: %s is stale: line %d is %q, want %q", e.Path, i+1, g, w)
|
|
}
|
|
}
|
|
return fmt.Sprintf("kiosk: %s is stale", e.Path)
|
|
}
|
|
|
|
// Check regenerates dir's plugin.yaml in memory, keeping its overrides,
|
|
// and returns a *StaleError when the file on disk is missing or differs.
|
|
func Check(ctx context.Context, dir string) error {
|
|
want, _, err := Generate(ctx, dir, DeriveOptions{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
path := filepath.Join(dir, FileName)
|
|
got, err := os.ReadFile(path)
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
return &StaleError{Path: path, Missing: true, Want: want}
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("kiosk: read %s: %w", path, err)
|
|
}
|
|
if !bytes.Equal(want, got) {
|
|
return &StaleError{Path: path, Want: want, Got: got}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// TB is the part of testing.TB that CheckT uses; *testing.T satisfies it.
|
|
type TB interface {
|
|
Helper()
|
|
Fatalf(format string, args ...any)
|
|
}
|
|
|
|
// CheckT fails the test when dir's plugin.yaml is missing or stale. A
|
|
// plugin repository calls it from one test, so go test catches a manifest
|
|
// that no longer matches the code:
|
|
//
|
|
// func TestPluginManifest(t *testing.T) { kiosk.CheckT(t, ".") }
|
|
func CheckT(t TB, dir string) {
|
|
t.Helper()
|
|
if err := Check(context.Background(), dir); err != nil {
|
|
t.Fatalf("%v\nregenerate it with: summer plugin:manifest %s", err, dir)
|
|
}
|
|
}
|