Skip to content

Commit

Permalink
Add build tags support
Browse files Browse the repository at this point in the history
`go build` allows the user to specify build tags. These tags enable the
user to discard files based on various tags when they build the
application binary.

`weaver generate` which is a wrapper around `go build` doesn't allow the
user to specify build tags.

This PR adds built tags support for the `weaver generate` command.

The syntax of passing build tags to the `weaver generate` command is similar to how build tags are specified when using `go build`.

E.g.,

weaver generate -tags good,prod
weaver generate --tags=good
  • Loading branch information
rgrandl committed Sep 4, 2024
1 parent 3f55afa commit a7cd571
Show file tree
Hide file tree
Showing 5 changed files with 158 additions and 10 deletions.
16 changes: 15 additions & 1 deletion cmd/weaver/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,25 @@ func main() {
switch flag.Arg(0) {
case "generate":
generateFlags := flag.NewFlagSet("generate", flag.ExitOnError)
tags := generateFlags.String("tags", "not specified", "Optional tags for the generate command")
generateFlags.Usage = func() {
fmt.Fprintln(os.Stderr, generate.Usage)
}
generateFlags.Parse(flag.Args()[1:])
if err := generate.Generate(".", flag.Args()[1:], generate.Options{}); err != nil {
buildTags := "--tags=ignoreWeaverGen"
if *tags != "not specified" { // tags flag was specified
if *tags == "" || *tags == "." {
// User specified the tags flag but didn't provide any tags.
fmt.Fprintln(os.Stderr, "No tags provided.")
os.Exit(1)
}
// TODO(rgrandl): we assume that the user specify the tags properly. I.e.,
// a single tag, or a list of tags separated by comma. We may want to do
// extra validation at some point.
buildTags = buildTags + "," + *tags
}
idx := len(flag.Args()) - len(generateFlags.Args())
if err := generate.Generate(".", flag.Args()[idx:], generate.Options{BuildTags: buildTags}); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
Expand Down
30 changes: 22 additions & 8 deletions internal/tool/generate/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ const (
Usage = `Generate code for a Service Weaver application.
Usage:
weaver generate [packages]
weaver generate [tags] [packages]
Description:
"weaver generate" generates code for the Service Weaver applications in the
Expand All @@ -64,6 +64,9 @@ Description:
file in the package's directory. For example, "weaver generate . ./foo" will
create ./weaver_gen.go and ./foo/weaver_gen.go.
You specify build tags for "weaver generate" in the same way you specify build
tags for go build. See "go help build" for more information.
You specify packages for "weaver generate" in the same way you specify
packages for go build, go test, go vet, etc. See "go help packages" for more
information.
Expand All @@ -86,13 +89,22 @@ Examples:
weaver generate ./foo
# Generate code for all packages in all subdirectories of current directory.
weaver generate ./...`
weaver generate ./...
# Generate code for all files that have a "// +build good" line at the top of
the file.
weaver generate -tags good
# Generate code for all files that have a "// +build good,prod" line at the
top of the file.
weaver generate -tags good,prod`
)

// Options controls the operation of Generate.
type Options struct {
// If non-nil, use the specified function to report warnings.
Warn func(error)
Warn func(error)
BuildTags string
}

// Generate generates Service Weaver code for the specified packages.
Expand All @@ -104,11 +116,13 @@ func Generate(dir string, pkgs []string, opt Options) error {
}
fset := token.NewFileSet()
cfg := &packages.Config{
Mode: packages.NeedName | packages.NeedSyntax | packages.NeedImports | packages.NeedTypes | packages.NeedTypesInfo,
Dir: dir,
Fset: fset,
ParseFile: parseNonWeaverGenFile,
BuildFlags: []string{"--tags=ignoreWeaverGen"},
Mode: packages.NeedName | packages.NeedSyntax | packages.NeedImports | packages.NeedTypes | packages.NeedTypesInfo,
Dir: dir,
Fset: fset,
ParseFile: parseNonWeaverGenFile,
}
if len(opt.BuildTags) > 0 {
cfg.BuildFlags = []string{opt.BuildTags}
}
pkgList, err := packages.Load(cfg, pkgs...)
if err != nil {
Expand Down
77 changes: 76 additions & 1 deletion internal/tool/generate/generator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,8 @@ func runGenerator(t *testing.T, directory, filename, contents string, subdirs []

// Run "weaver generate".
opt := Options{
Warn: func(err error) { t.Log(err) },
Warn: func(err error) { t.Log(err) },
BuildTags: "--tags=ignoreWeaverGen",
}
if err := Generate(tmp, []string{tmp}, opt); err != nil {
return "", err
Expand Down Expand Up @@ -237,6 +238,80 @@ func TestGenerator(t *testing.T) {
}
}

// TestGeneratorBuildTags runs "weaver generate" on all the files in
// testdata/tags and checks if the command succeeds. Each file should have some build tags.
func TestGeneratorBuildTags(t *testing.T) {
const dir = "testdata/tags"
files, err := os.ReadDir(dir)
if err != nil {
t.Fatalf("cannot list files in %q", dir)
}

tmp := t.TempDir()
save := func(f, data string) {
if err := os.WriteFile(filepath.Join(tmp, f), []byte(data), 0644); err != nil {
t.Fatalf("error writing %s: %v", f, err)
}
}

// Copy the files from dir to the temp directory.
for _, file := range files {
filename := file.Name()
if !strings.HasSuffix(filename, ".go") || strings.HasSuffix(filename, generatedCodeFile) {
continue
}

// Read the test file.
bits, err := os.ReadFile(filepath.Join(dir, filename))
if err != nil {
t.Fatalf("cannot read %q: %v", filename, err)
}
contents := string(bits)
save(filename, contents)
}
save("go.mod", goModFile)

// Run "go mod tidy"
tidy := exec.Command("go", "mod", "tidy")
tidy.Dir = tmp
tidy.Stdout = os.Stdout
tidy.Stderr = os.Stderr
if err := tidy.Run(); err != nil {
t.Fatalf("go mod tidy: %v", err)
}

// Run the "weaver generate" command with no build tags. Verify that the command
// doesn't succeed because bad.go does not compile.
err = Generate(tmp, []string{tmp}, Options{Warn: func(err error) { t.Log(err) }})
if err == nil {
t.Fatal("expected generator to return an error; got nil error")
}
// Verify that no weaver_gen.go file was generated.
_, err = os.ReadFile(filepath.Join(tmp, generatedCodeFile))
if err == nil {
t.Fatal("expected no generated file")
}

// Run the "weaver generate" command with the build tag "good". Verify that the
// command succeeds because the bad.go file is ignored, and the weaver_gen.go
// contains only generated code for the good.go.
err = Generate(tmp, []string{tmp}, Options{Warn: func(err error) { t.Log(err) }, BuildTags: "--tags=good"})
if err != nil {
t.Fatalf("unexpected generator error: %v", err)
}
// Verify that the weaver_gen.go file doesn't contain generated code for the bad service.
output, err := os.ReadFile(filepath.Join(tmp, generatedCodeFile))
if err != nil {
t.Fatalf("unable to read the ")
}
if strings.Contains(string(output), "bad") {
t.Fatalf("unexpected generated code for the bad service")
}
if !strings.Contains(string(output), "good") {
t.Fatalf("expected generated code for the good service")
}
}

// TestGeneratorErrors runs "weaver generate" on all of the files in
// testdata/errors.
// Every file in testdata/errors must begin with a single line header that looks
Expand Down
22 changes: 22 additions & 0 deletions internal/tool/generate/testdata/tags/bad.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//go:build !good

package tags

import (
"context"
"fmt"

"github.com/ServiceWeaver/weaver"
)

type BadService interface {
DoSomething(context.Context) error
}

type badServiceImpl struct {
weaver.Implements[BadService]
}

func (b *badServiceImpl) DoSomething(context.Context) error {
Some code that does not compile
}
23 changes: 23 additions & 0 deletions internal/tool/generate/testdata/tags/good.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//go:build good

package tags

import (
"context"
"fmt"

"github.com/ServiceWeaver/weaver"
)

type GoodService interface {
DoSomething(context.Context) error
}

type goodServiceImpl struct {
weaver.Implements[GoodService]
}

func (g *goodServiceImpl) DoSomething(context.Context) error {
fmt.Println("Hello world!")
return nil
}

0 comments on commit a7cd571

Please sign in to comment.