Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add --watch flag for generate command to regenerate files on changes #3208

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## [Unreleased]

- Add new flag `buf generate --watch`. It tracks changes in proto files and automatically runs `buf generate`.
- Add `clean` as a top-level option in `buf.gen.yaml`, matching the `buf generate --clean` flag. If
set to true, this will delete the directories, jar files, or zip files set to `out` for each
plugin.
Expand Down
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ require (
github.com/bufbuild/protoplugin v0.0.0-20240323223605-e2735f6c31ee
github.com/bufbuild/protovalidate-go v0.6.2
github.com/bufbuild/protoyaml-go v0.1.9
github.com/docker/docker v27.0.0+incompatible
github.com/docker/docker v27.0.1+incompatible
github.com/fsnotify/fsnotify v1.7.0
github.com/go-chi/chi/v5 v5.0.14
github.com/gofrs/flock v0.8.1
github.com/gofrs/uuid/v5 v5.2.0
Expand Down
6 changes: 4 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ github.com/docker/cli v26.1.4+incompatible h1:I8PHdc0MtxEADqYJZvhBrW9bo8gawKwwen
github.com/docker/cli v26.1.4+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v27.0.0+incompatible h1:JRugTYuelmWlW0M3jakcIadDx2HUoUO6+Tf2C5jVfwA=
github.com/docker/docker v27.0.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v27.0.1+incompatible h1:AbszR+lCnR3f297p/g0arbQoyhAkImxQOR/XO9YZeIg=
github.com/docker/docker v27.0.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo=
github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
Expand All @@ -64,6 +64,8 @@ github.com/felixge/fgprof v0.9.4 h1:ocDNwMFlnA0NU0zSB3I52xkO4sFXk80VK9lXjLClu88=
github.com/felixge/fgprof v0.9.4/go.mod h1:yKl+ERSa++RYOs32d8K6WEXCB4uXdLls4ZaZPpayhMM=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/go-chi/chi/v5 v5.0.14 h1:PyEwo2Vudraa0x/Wl6eDRRW2NXBvekgfxyydcM0WGE0=
github.com/go-chi/chi/v5 v5.0.14/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
Expand Down
7 changes: 7 additions & 0 deletions private/buf/bufgen/bufgen.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,3 +153,10 @@ func GenerateWithIncludeWellKnownTypesOverride(includeWellKnownTypes bool) Gener
generateOptions.includeWellKnownTypesOverride = &includeWellKnownTypes
}
}

// GenerateWithWatch results in the option for regenerating code on changes.
func GenerateWithWatch(watch bool) GenerateOption {
return func(generateOptions *generateOptions) {
generateOptions.watch = watch
}
}
80 changes: 80 additions & 0 deletions private/buf/bufgen/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"errors"
"fmt"
"path/filepath"
"strings"

connect "connectrpc.com/connect"
"github.com/bufbuild/buf/private/buf/bufprotopluginexec"
Expand All @@ -34,10 +35,13 @@ import (
"github.com/bufbuild/buf/private/pkg/app"
"github.com/bufbuild/buf/private/pkg/command"
"github.com/bufbuild/buf/private/pkg/connectclient"
"github.com/bufbuild/buf/private/pkg/osext"
"github.com/bufbuild/buf/private/pkg/slicesext"
"github.com/bufbuild/buf/private/pkg/storage/storageos"
"github.com/bufbuild/buf/private/pkg/thread"
"github.com/bufbuild/buf/private/pkg/tracing"
"github.com/bufbuild/buf/private/pkg/watcher"
"github.com/fsnotify/fsnotify"
"go.uber.org/multierr"
"go.uber.org/zap"
"google.golang.org/protobuf/types/pluginpb"
Expand Down Expand Up @@ -84,6 +88,9 @@ func newGenerator(
//
// This behavior is equivalent to protoc, which only writes out the content
// for each of the plugins if all of the plugins are successful.
//
// If watch option is true, the function will block and regenerate code on
// filesystem changes. It will return when ctx is done or a signal (SIGNIT or SIGTERM) is received.
func (g *generator) Generate(
ctx context.Context,
container app.EnvStdioContainer,
Expand All @@ -105,6 +112,36 @@ func (g *generator) Generate(
return err
}
}

if generateOptions.watch {
return g.watch(ctx, func() error {
return g.generate(
ctx,
container,
config,
images,
generateOptions,
)
})
}

return g.generate(
ctx,
container,
config,
images,
generateOptions,
)
}

func (g *generator) generate(
ctx context.Context,
container app.EnvStdioContainer,
config bufconfig.GenerateConfig,
images []bufimage.Image,
generateOptions *generateOptions,
) error {
// Clean the output directories if necessary.
shouldDeleteOuts := config.CleanPluginOuts()
if generateOptions.deleteOuts != nil {
shouldDeleteOuts = *generateOptions.deleteOuts
Expand All @@ -118,6 +155,8 @@ func (g *generator) Generate(
return err
}
}

// Generate the code for each image.
for _, image := range images {
if err := g.generateCode(
ctx,
Expand All @@ -131,9 +170,49 @@ func (g *generator) Generate(
return err
}
}

return nil
}

// watch is a blocking function that watches the filesystem for changes and
// regenerates code when a change is detected.
//
// This function will block until ctx is done, a signal is received or an error occurs.
func (g *generator) watch(ctx context.Context, callback func() error) error {
cwd, err := osext.Getwd()
if err != nil {
return fmt.Errorf("getting current directory: %w", err)
}

watch, err := watcher.New(g.logger.Named("watcher"))
if err != nil {
return fmt.Errorf("initializing filewatcher: %w", err)
}

err = watch.AddRecursive(cwd)
if err != nil {
return fmt.Errorf("adding watch path: %w", err)
}

g.logger.Sugar().Infof("Watching filesystem changes at %s...", cwd)

return watch.Watch(ctx, func(ctx context.Context, name string, _ fsnotify.Op) error {
// ignore all except for .proto files.
if !strings.HasSuffix(name, ".proto") {
return nil
}

changedFile := strings.TrimPrefix(name, cwd)
g.logger.Sugar().Infof("Change detected at %s. Regenerating...", changedFile)

if err := callback(); err != nil {
return fmt.Errorf("generating code: %w", err)
}

return nil
})
}

func (g *generator) deleteOuts(
ctx context.Context,
baseOutDir string,
Expand Down Expand Up @@ -491,6 +570,7 @@ type generateOptions struct {
deleteOuts *bool
includeImportsOverride *bool
includeWellKnownTypesOverride *bool
watch bool
}

func newGenerateOptions() *generateOptions {
Expand Down
9 changes: 9 additions & 0 deletions private/buf/cmd/buf/command/generate/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ const (
disableSymlinksFlagName = "disable-symlinks"
typeFlagName = "type"
typeDeprecatedFlagName = "include-types"
watchFlagName = "watch"
)

// NewCommand returns a new Command.
Expand Down Expand Up @@ -382,6 +383,7 @@ type flags struct {
IncludeWKTOverride *bool
ExcludePaths []string
DisableSymlinks bool
Watch bool
// We may be able to bind two flags to one string slice but I don't
// want to find out what will break if we do.
Types []string
Expand Down Expand Up @@ -460,6 +462,12 @@ func (f *flags) Bind(flagSet *pflag.FlagSet) {
nil,
"The types (package, message, enum, extension, service, method) that should be included in this image. When specified, the resulting image will only include descriptors to describe the requested types. Flag usage overrides buf.gen.yaml",
)
flagSet.BoolVar(
&f.Watch,
watchFlagName,
false,
`Watch changes and regenrate code.`,
)
_ = flagSet.MarkDeprecated(typeDeprecatedFlagName, fmt.Sprintf("use --%s instead", typeFlagName))
_ = flagSet.MarkHidden(typeDeprecatedFlagName)
}
Expand Down Expand Up @@ -521,6 +529,7 @@ func run(
}
generateOptions := []bufgen.GenerateOption{
bufgen.GenerateWithBaseOutDirPath(flags.BaseOutDirPath),
bufgen.GenerateWithWatch(flags.Watch),
}
if flags.DeleteOuts != nil {
generateOptions = append(
Expand Down
Loading