feat(19.3-03): summer.yaml build.tags and the summercms_s3 tag that links the S3 uploads driver
- Manifest.Tags from build.tags (^[a-z0-9_]+$, duplicates dropped), round-tripped by SaveManifest - summer build passes -tags=<list> through goBuildArgs and prints the build tags line - attach: s3blob imported only by s3_on.go (//go:build summercms_s3); an untagged binary refuses an s3:// bucket_url naming the tag, build.tags and -tags - go mod tidy adds the AWS SDK v2 modules as indirect requirements - storage, modules, scaffolding, configuration and scaling docs plus the lagoon README
This commit is contained in:
@@ -46,7 +46,10 @@ func App(ctx context.Context, appDir string, out io.Writer) error {
|
||||
return fmt.Errorf("build: create bin dir: %w", err)
|
||||
}
|
||||
|
||||
cmd := exec.CommandContext(ctx, "go", "build", "-o", binRel, ".")
|
||||
if len(m.Tags) > 0 {
|
||||
fmt.Fprintf(out, "build tags: %s\n", strings.Join(m.Tags, ","))
|
||||
}
|
||||
cmd := exec.CommandContext(ctx, "go", goBuildArgs(binRel, m.Tags)...)
|
||||
cmd.Dir = appDir
|
||||
cmd.Stdout = out
|
||||
cmd.Stderr = out
|
||||
@@ -59,6 +62,17 @@ func App(ctx context.Context, appDir string, out io.Writer) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// goBuildArgs returns the go command arguments for summer build: build, the
|
||||
// -tags flag when the manifest lists build tags, then -o binRel and the
|
||||
// application package. The tags travel as one exec argument, never a shell.
|
||||
func goBuildArgs(binRel string, tags []string) []string {
|
||||
args := []string{"build"}
|
||||
if len(tags) > 0 {
|
||||
args = append(args, "-tags="+strings.Join(tags, ","))
|
||||
}
|
||||
return append(args, "-o", binRel, ".")
|
||||
}
|
||||
|
||||
func generate(appDir string, m Manifest) error {
|
||||
mainSrc, err := generateMain(m)
|
||||
if err != nil {
|
||||
|
||||
@@ -13,13 +13,18 @@ import (
|
||||
|
||||
const manifestName = "summer.yaml"
|
||||
|
||||
var pluginIDRe = regexp.MustCompile(`^[a-z][a-z0-9]*\.[a-z][a-z0-9]*$`)
|
||||
var (
|
||||
pluginIDRe = regexp.MustCompile(`^[a-z][a-z0-9]*\.[a-z][a-z0-9]*$`)
|
||||
buildTagRe = regexp.MustCompile(`^[a-z0-9_]+$`)
|
||||
)
|
||||
|
||||
// Manifest is the ordered app plugin list used to generate imports.
|
||||
// Manifest is the ordered app plugin list used to generate imports, plus
|
||||
// the Go build tags summer build passes to go build (summer.yaml build.tags).
|
||||
type Manifest struct {
|
||||
Module string
|
||||
Binary string
|
||||
Plugins []Plugin
|
||||
Tags []string
|
||||
}
|
||||
|
||||
// Plugin is one compiled plugin module in manifest order.
|
||||
@@ -76,10 +81,67 @@ func LoadManifest(path string) (Manifest, error) {
|
||||
seenMod[p.Module] = struct{}{}
|
||||
m.Plugins = append(m.Plugins, p)
|
||||
}
|
||||
|
||||
tags, err := loadBuildTags(tree["build"])
|
||||
if err != nil {
|
||||
return Manifest{}, err
|
||||
}
|
||||
m.Tags = tags
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// SaveManifest writes summer.yaml with plugins in slice order.
|
||||
// loadBuildTags reads the optional build mapping and its tags list. Entries
|
||||
// are trimmed and validated; duplicates are dropped keeping the first.
|
||||
func loadBuildTags(raw any) ([]string, error) {
|
||||
if raw == nil {
|
||||
return nil, nil
|
||||
}
|
||||
section := asMap(raw)
|
||||
if section == nil {
|
||||
return nil, fmt.Errorf("build: build must be a mapping (build.tags)")
|
||||
}
|
||||
rawTags, ok := section["tags"]
|
||||
if !ok || rawTags == nil {
|
||||
return nil, nil
|
||||
}
|
||||
list, ok := rawTags.([]any)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("build: build.tags must be a list of tags")
|
||||
}
|
||||
var tags []string
|
||||
seen := make(map[string]struct{}, len(list))
|
||||
for i, item := range list {
|
||||
s, ok := item.(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("build: build.tags[%d] must be a string", i)
|
||||
}
|
||||
tag := strings.TrimSpace(s)
|
||||
if err := ValidateBuildTag(tag); err != nil {
|
||||
return nil, fmt.Errorf("build: build.tags[%d]: %w", i, err)
|
||||
}
|
||||
if _, dup := seen[tag]; dup {
|
||||
continue
|
||||
}
|
||||
seen[tag] = struct{}{}
|
||||
tags = append(tags, tag)
|
||||
}
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
// ValidateBuildTag requires a Go build tag of lowercase letters, digits and
|
||||
// underscores, so a summer.yaml value can never smuggle a flag into go build.
|
||||
func ValidateBuildTag(tag string) error {
|
||||
if tag == "" {
|
||||
return fmt.Errorf("build tag is empty")
|
||||
}
|
||||
if !buildTagRe.MatchString(tag) {
|
||||
return fmt.Errorf("build tag %q must match ^[a-z0-9_]+$", tag)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SaveManifest writes summer.yaml with plugins in slice order and, when
|
||||
// present, the build.tags list.
|
||||
func SaveManifest(path string, m Manifest) error {
|
||||
if err := validateModulePath(m.Module); err != nil {
|
||||
return fmt.Errorf("build: module: %w", err)
|
||||
@@ -101,6 +163,16 @@ func SaveManifest(path string, m Manifest) error {
|
||||
fmt.Fprintf(&b, " - id: %s\n", p.ID)
|
||||
fmt.Fprintf(&b, " module: %s\n", p.Module)
|
||||
}
|
||||
if len(m.Tags) > 0 {
|
||||
b.WriteString("build:\n")
|
||||
b.WriteString(" tags:\n")
|
||||
for i, tag := range m.Tags {
|
||||
if err := ValidateBuildTag(tag); err != nil {
|
||||
return fmt.Errorf("build: build.tags[%d]: %w", i, err)
|
||||
}
|
||||
fmt.Fprintf(&b, " - %s\n", tag)
|
||||
}
|
||||
}
|
||||
if err := os.WriteFile(path, []byte(b.String()), 0o644); err != nil {
|
||||
return fmt.Errorf("build: write %s: %w", path, err)
|
||||
}
|
||||
|
||||
70
internal/build/manifest_tags_smoke_test.go
Normal file
70
internal/build/manifest_tags_smoke_test.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package build
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestManifestBuildTagsSmoke(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, manifestName)
|
||||
body := "module: git.golem15.com/acme/acme\n" +
|
||||
"binary: acme\n" +
|
||||
"plugins:\n" +
|
||||
" - id: acme.blog\n" +
|
||||
" module: git.golem15.com/acme/acme/plugins/blog\n" +
|
||||
"build:\n" +
|
||||
" tags:\n" +
|
||||
" - summercms_s3\n" +
|
||||
" - \" summercms_s3 \"\n"
|
||||
if err := os.WriteFile(path, []byte(body), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
m, err := LoadManifest(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !slices.Equal(m.Tags, []string{"summercms_s3"}) {
|
||||
t.Fatalf("Tags = %q, want [summercms_s3]", m.Tags)
|
||||
}
|
||||
|
||||
if err := SaveManifest(path, m); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
again, err := LoadManifest(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !slices.Equal(again.Tags, m.Tags) || len(again.Plugins) != 1 {
|
||||
t.Fatalf("round trip = %+v, want tags %q and one plugin", again, m.Tags)
|
||||
}
|
||||
|
||||
args := goBuildArgs("bin/acme", again.Tags)
|
||||
tagAt := slices.Index(args, "-tags=summercms_s3")
|
||||
outAt := slices.Index(args, "-o")
|
||||
if tagAt < 0 || outAt < 0 || tagAt > outAt {
|
||||
t.Fatalf("goBuildArgs = %q, want -tags=summercms_s3 before -o", args)
|
||||
}
|
||||
if plain := goBuildArgs("bin/acme", nil); !slices.Equal(plain, []string{"build", "-o", "bin/acme", "."}) {
|
||||
t.Fatalf("goBuildArgs without tags = %q", plain)
|
||||
}
|
||||
|
||||
for _, bad := range []string{"summercms-s3", "summercms s3"} {
|
||||
if err := ValidateBuildTag(bad); err == nil {
|
||||
t.Fatalf("ValidateBuildTag(%q) accepted", bad)
|
||||
}
|
||||
raw := "module: git.golem15.com/acme/acme\nbinary: acme\nbuild:\n tags:\n - \"" + bad + "\"\n"
|
||||
if err := os.WriteFile(path, []byte(raw), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := LoadManifest(path); err == nil || !strings.Contains(err.Error(), "build.tags") {
|
||||
t.Fatalf("LoadManifest with tag %q: err = %v, want a build.tags error", bad, err)
|
||||
}
|
||||
}
|
||||
if err := SaveManifest(path, Manifest{Module: "git.golem15.com/acme/acme", Binary: "acme", Tags: []string{"a b"}}); err == nil {
|
||||
t.Fatal("SaveManifest accepted an invalid tag")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user