Skip to content

Commit

Permalink
Add build tags support (ServiceWeaver#796)
Browse files Browse the repository at this point in the history
* Add build tags support

`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

* Addressed sanjay's comments.

* Rewrote TestGeneratorWithBuildTags to use runGenerator

* Fixed comments in generator_test
  • Loading branch information
rgrandl authored Sep 10, 2024
1 parent d69b553 commit 0a1e0d5
Show file tree
Hide file tree
Showing 5 changed files with 152 additions and 15 deletions.
10 changes: 9 additions & 1 deletion cmd/weaver/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,19 @@ func main() {
switch flag.Arg(0) {
case "generate":
generateFlags := flag.NewFlagSet("generate", flag.ExitOnError)
tags := generateFlags.String("tags", "", "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 := "ignoreWeaverGen"
if *tags != "" { // tags flag was specified
// 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
}
if err := generate.Generate(".", generateFlags.Args(), generate.Options{BuildTags: buildTags}); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
Expand Down
31 changes: 22 additions & 9 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 taglist] [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,21 @@ 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 "//go:build good" line at the top of
the file.
weaver generate -tags good
# Generate code for all files that have a "//go: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) // If non-nil, use the specified function to report warnings
BuildTags string
}

// Generate generates Service Weaver code for the specified packages.
Expand All @@ -104,11 +115,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{"-tags", opt.BuildTags}
}
pkgList, err := packages.Load(cfg, pkgs...)
if err != nil {
Expand Down
53 changes: 48 additions & 5 deletions internal/tool/generate/generator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ replace github.com/ServiceWeaver/weaver => %s
//
// If "weaver generate" succeeds, the produced weaver_gen.go file is written in
// the provided directory with name ${filename}_weaver_gen.go.
func runGenerator(t *testing.T, directory, filename, contents string, subdirs []string) (string, error) {
func runGenerator(t *testing.T, directory, filename, contents string, subdirs []string,
buildTags []string) (string, error) {
// runGenerator creates a temporary directory, copies the file and all
// subdirs into it, writes a go.mod file, runs "go mod tidy", and finally
// runs "weaver generate".
Expand Down Expand Up @@ -102,7 +103,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: "ignoreWeaverGen" + "," + strings.Join(buildTags, ","),
}
if err := Generate(tmp, []string{tmp}, opt); err != nil {
return "", err
Expand Down Expand Up @@ -134,7 +136,7 @@ func runGenerator(t *testing.T, directory, filename, contents string, subdirs []
if err := tidy.Run(); err != nil {
t.Fatalf("go mod tidy: %v", err)
}
gobuild := exec.Command("go", "build")
gobuild := exec.Command("go", "build", "-tags="+opt.BuildTags)
gobuild.Dir = tmp
gobuild.Stdout = os.Stdout
gobuild.Stderr = os.Stderr
Expand Down Expand Up @@ -218,7 +220,7 @@ func TestGenerator(t *testing.T) {
}

// Run "weaver generate".
output, err := runGenerator(t, dir, filename, contents, []string{"sub1", "sub2"})
output, err := runGenerator(t, dir, filename, contents, []string{"sub1", "sub2"}, nil)
if err != nil {
t.Fatalf("error running generator: %v", err)
}
Expand All @@ -237,6 +239,47 @@ func TestGenerator(t *testing.T) {
}
}

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

for _, file := range files {
filename := file.Name()
if !strings.HasSuffix(filename, ".go") || strings.HasSuffix(filename, generatedCodeFile) {
continue
}
t.Run(filename, func(t *testing.T) {
t.Parallel()

// 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)
// Run "weaver generate".
output, err := runGenerator(t, dir, filename, contents, nil, []string{"good"})

if filename == "good.go" {
// Verify that the error is nil and the weaver_gen.go contains generated code for the good service.
if err != nil || !strings.Contains(output, "GoodService") {
t.Fatalf("expected generated code for the good service")
}
return
}
// For the bad.go verify that the error is not nil and there is no output.
if err == nil || len(output) > 0 {
t.Fatalf("expected no 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 Expand Up @@ -286,7 +329,7 @@ func TestGeneratorErrors(t *testing.T) {
}

// Run "weaver generate".
output, err := runGenerator(t, dir, filename, contents, []string{})
output, err := runGenerator(t, dir, filename, contents, nil, nil)
errfile := strings.TrimSuffix(filename, ".go") + "_error.txt"
if err == nil {
os.Remove(filepath.Join(dir, errfile))
Expand Down
36 changes: 36 additions & 0 deletions internal/tool/generate/testdata/tags/bad.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//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
}
37 changes: 37 additions & 0 deletions internal/tool/generate/testdata/tags/good.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//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 0a1e0d5

Please sign in to comment.