Skip to content
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 .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ cmd/buf/buf
cmd/buf/internal/command/alpha/protoc/internal/protoc-gen-insertion-point-receiver/protoc-gen-insertion-point-receiver
cmd/buf/internal/command/alpha/protoc/internal/protoc-gen-insertion-point-writer/protoc-gen-insertion-point-writer
cmd/buf/internal/command/alpha/protoc/test.txt
cmd/buf/internal/command/generate/internal/protoc-gen-files-to-generate-yaml/protoc-gen-files-to-generate-yaml
cmd/buf/internal/command/generate/internal/protoc-gen-top-level-type-names-yaml/protoc-gen-top-level-type-names-yaml
cmd/buf/testdata/imports/cache/v3/modulelocks/
cmd/buf/testdata/imports/corrupted_cache_dep/v3/modulelocks/
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
/cmd/buf/internal/command/alpha/protoc/internal/protoc-gen-insertion-point-receiver/protoc-gen-insertion-point-receiver
/cmd/buf/internal/command/alpha/protoc/internal/protoc-gen-insertion-point-writer/protoc-gen-insertion-point-writer
/cmd/buf/internal/command/alpha/protoc/test.txt
/cmd/buf/internal/command/generate/internal/protoc-gen-files-to-generate-yaml/protoc-gen-files-to-generate-yaml
/cmd/buf/internal/command/generate/internal/protoc-gen-top-level-type-names-yaml/protoc-gen-top-level-type-names-yaml
/cmd/buf/testdata/imports/cache/v3/modulelocks/
/cmd/buf/testdata/imports/corrupted_cache_dep/v3/modulelocks/
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
- Add `--stdin-filepath` flag to `buf format`, which reads a single `.proto` file from
stdin and writes the formatted result to stdout. The path is not read from disk, and is
only used to report parse errors and diffs.
- Fix `buf generate` with `strategy: directory` and `include_imports: true` adding imports
to the `CodeGeneratorRequest` of the directory that imports them. Imports are now split
by directory into their own requests, the same as non-imports.

## [v1.73.0] - 2026-09-11

Expand Down
97 changes: 97 additions & 0 deletions cmd/buf/internal/command/generate/generate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,103 @@ inputs:
)
}

func TestGenerateV2LocalPluginStrategyDirectoryIncludeImports(t *testing.T) {
t.Parallel()
testRunTemplate := func(t *testing.T, expect map[string][]byte, template string, extraArgs ...string) {
t.Helper()
tempDirPath := t.TempDir()
testRunSuccess(
t,
append(
[]string{
"--output",
tempDirPath,
"--template",
template,
// Only target keyvalue, so that common is an import.
"--path",
filepath.Join("testdata", "v2", "include_imports", "keyvalue"),
filepath.Join("testdata", "v2", "include_imports"),
},
extraArgs...,
)...,
)
expected, err := storagemem.NewReadBucket(expect)
require.NoError(t, err)
actual, err := storageos.NewProvider().NewReadWriteBucket(tempDirPath)
require.NoError(t, err)
diff, err := storage.DiffBytes(t.Context(), expected, actual)
require.NoError(t, err)
require.Empty(t, string(diff))
}
commonFilesToGenerate := []byte(`files:
- common/v1/value.proto
`)
keyValueFilesToGenerate := []byte(`files:
- keyvalue/v1/service.proto
`)
timestampFilesToGenerate := []byte(`files:
- google/protobuf/timestamp.proto
`)
// Without include_imports, only the directory with non-imports is generated.
testRunTemplate(
t,
map[string][]byte{
filepath.Join("gen", "keyvalue", "v1", "files-to-generate.yaml"): keyValueFilesToGenerate,
},
`version: v2
plugins:
- local: protoc-gen-files-to-generate-yaml
out: gen
strategy: directory`,
)
// With include_imports, imports are generated in a separate request for their directory.
// The plugin fails if a request has files to generate from more than one directory.
testRunTemplate(
t,
map[string][]byte{
filepath.Join("gen", "common", "v1", "files-to-generate.yaml"): commonFilesToGenerate,
filepath.Join("gen", "keyvalue", "v1", "files-to-generate.yaml"): keyValueFilesToGenerate,
},
`version: v2
plugins:
- local: protoc-gen-files-to-generate-yaml
out: gen
strategy: directory
include_imports: true`,
)
// With include_wkt as well, well-known types are generated in their own request.
testRunTemplate(
t,
map[string][]byte{
filepath.Join("gen", "common", "v1", "files-to-generate.yaml"): commonFilesToGenerate,
filepath.Join("gen", "google", "protobuf", "files-to-generate.yaml"): timestampFilesToGenerate,
filepath.Join("gen", "keyvalue", "v1", "files-to-generate.yaml"): keyValueFilesToGenerate,
},
`version: v2
plugins:
- local: protoc-gen-files-to-generate-yaml
out: gen
strategy: directory
include_imports: true
include_wkt: true`,
)
// --include-imports on the command line overrides the template.
testRunTemplate(
t,
map[string][]byte{
filepath.Join("gen", "common", "v1", "files-to-generate.yaml"): commonFilesToGenerate,
filepath.Join("gen", "keyvalue", "v1", "files-to-generate.yaml"): keyValueFilesToGenerate,
},
`version: v2
plugins:
- local: protoc-gen-files-to-generate-yaml
out: gen
strategy: directory`,
"--include-imports",
)
}

func TestOutputFlag(t *testing.T) {
t.Parallel()
for _, paths := range []struct {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Copyright 2020-2026 Buf Technologies, Inc.
//
// 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.

// protoc-gen-files-to-generate-yaml writes one file per CodeGeneratorRequest
// listing the files to generate in that request.
//
// All files to generate in a request must be in the same directory, and the
// output is written to that directory. This is used to test that
// strategy: directory splits requests by directory, including when
// include_imports is set.
package main

import (
"context"
"errors"
"fmt"
"path"
"slices"
"strings"

"github.com/bufbuild/protoplugin"
"gopkg.in/yaml.v3"
)

const fileName = "files-to-generate.yaml"

func main() {
protoplugin.Main(protoplugin.HandlerFunc(handle))
}

func handle(
_ context.Context,
_ protoplugin.PluginEnv,
responseWriter protoplugin.ResponseWriter,
request protoplugin.Request,
) error {
filesToGenerate := request.CodeGeneratorRequest().GetFileToGenerate()
if len(filesToGenerate) == 0 {
return errors.New("no files to generate")
}
var dirs []string
for _, fileToGenerate := range filesToGenerate {
dir := path.Dir(fileToGenerate)
if !slices.Contains(dirs, dir) {
dirs = append(dirs, dir)
}
}
if len(dirs) > 1 {
return fmt.Errorf("files to generate span multiple directories: %s", strings.Join(dirs, ", "))
}
data, err := yaml.Marshal(&externalFile{Files: filesToGenerate})
if err != nil {
return err
}
responseWriter.AddFile(path.Join(dirs[0], fileName), string(data))
return nil
}

type externalFile struct {
Files []string `json:"files,omitempty" yaml:"files,omitempty"`
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
version: v2
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
syntax = "proto3";

package common.v1;

message Value {
string value = 1;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
syntax = "proto3";

package keyvalue.v1;

import "common/v1/value.proto";
import "google/protobuf/timestamp.proto";

message Entry {
common.v1.Value value = 1;
google.protobuf.Timestamp updated_at = 2;
}
1 change: 1 addition & 0 deletions etc/windows/test.bash
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ go install connectrpc.com/connect/cmd/protoc-gen-connect-go@${CONNECT_VERSION}
go install ./cmd/buf \
./cmd/buf/internal/command/alpha/protoc/internal/protoc-gen-insertion-point-writer \
./cmd/buf/internal/command/alpha/protoc/internal/protoc-gen-insertion-point-receiver \
./cmd/buf/internal/command/generate/internal/protoc-gen-files-to-generate-yaml \
./cmd/buf/internal/command/generate/internal/protoc-gen-top-level-type-names-yaml \
./private/bufpkg/bufcheck/internal/cmd/buf-plugin-panic \
./private/bufpkg/bufcheck/internal/cmd/buf-plugin-suffix \
Expand Down
1 change: 1 addition & 0 deletions make/buf/all.mk
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ GO_BINS := $(GO_BINS) \
GO_TEST_BINS := $(GO_TEST_BINS) \
cmd/buf/internal/command/alpha/protoc/internal/protoc-gen-insertion-point-receiver \
cmd/buf/internal/command/alpha/protoc/internal/protoc-gen-insertion-point-writer \
cmd/buf/internal/command/generate/internal/protoc-gen-files-to-generate-yaml \
cmd/buf/internal/command/generate/internal/protoc-gen-top-level-type-names-yaml \
private/bufpkg/bufcheck/internal/cmd/buf-plugin-panic \
private/bufpkg/bufcheck/internal/cmd/buf-plugin-suffix \
Expand Down
47 changes: 34 additions & 13 deletions private/buf/bufgen/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -232,19 +232,6 @@ func (g *generator) execPlugins(
}

// Local plugins.
var images []bufimage.Image
switch Strategy(pluginConfigForKey.Strategy()) {
case StrategyAll:
images = []bufimage.Image{image}
case StrategyDirectory:
var err error
images, err = bufimage.ImageByDir(image)
if err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("unknown strategy: %v", pluginConfigForKey.Strategy())
}
for _, indexedPluginConfig := range indexedPluginConfigs {
jobs = append(jobs, func(ctx context.Context) error {
includeImports := indexedPluginConfig.Value.IncludeImports()
Expand All @@ -255,6 +242,15 @@ func (g *generator) execPlugins(
if includeWellKnownTypesOverride != nil {
includeWellKnownTypes = *includeWellKnownTypesOverride
}
images, err := imagesForStrategy(
image,
Strategy(indexedPluginConfig.Value.Strategy()),
includeImports,
includeWellKnownTypes,
)
if err != nil {
return err
}
response, err := g.execLocalPlugin(
ctx,
container,
Expand Down Expand Up @@ -331,6 +327,31 @@ func (g *generator) execLocalPlugin(
return response, nil
}

// imagesForStrategy returns the Images to use for a local plugin with the given Strategy.
func imagesForStrategy(
image bufimage.Image,
strategy Strategy,
includeImports bool,
includeWellKnownTypes bool,
) ([]bufimage.Image, error) {
switch strategy {
case StrategyAll:
return []bufimage.Image{image}, nil
// Split imports by directory similar to non-imports.
case StrategyDirectory:
var imageByDirOptions []bufimage.ImageByDirOption
if includeImports {
imageByDirOptions = append(imageByDirOptions, bufimage.ImageByDirWithIncludeImports())
if includeWellKnownTypes {
imageByDirOptions = append(imageByDirOptions, bufimage.ImageByDirWithIncludeWellKnownTypes())
}
}
return bufimage.ImageByDir(image, imageByDirOptions...)
default:
return nil, fmt.Errorf("unknown strategy: %v", strategy)
}
}

func (g *generator) execRemotePluginsV2(
ctx context.Context,
container app.EnvStdioContainer,
Expand Down
57 changes: 53 additions & 4 deletions private/bufpkg/bufimage/bufimage.go
Original file line number Diff line number Diff line change
Expand Up @@ -517,15 +517,32 @@ func ImageWithOnlyPathsAllowNotExist(
// by directory.
//
// That is, each Image will only contain a single directory's files
// as it's non-imports, along with all required imports for the
// as its non-imports, along with all required imports for the
// files in that directory.
func ImageByDir(image Image) ([]Image, error) {
//
// If ImageByDirWithIncludeImports is set, imports are split by directory as well.
// Each non-well-known-type import is a non-import in the Image for its directory,
// and remains an import in every other Image that requires it. If
// ImageByDirWithIncludeWellKnownTypes is also set, well-known-type imports are split
// by directory too. ImageByDirWithIncludeWellKnownTypes has no effect if
// ImageByDirWithIncludeImports is not set.
func ImageByDir(image Image, options ...ImageByDirOption) ([]Image, error) {
imageByDirOptions := newImageByDirOptions()
for _, option := range options {
option(imageByDirOptions)
}
imageFiles := image.Files()
paths := make([]string, 0, len(imageFiles))
for _, imageFile := range imageFiles {
if !imageFile.IsImport() {
paths = append(paths, imageFile.Path())
if imageFile.IsImport() {
if !imageByDirOptions.includeImports {
continue
}
if !imageByDirOptions.includeWellKnownTypes && datawkt.Exists(imageFile.Path()) {
continue
}
}
paths = append(paths, imageFile.Path())
}
dirToPaths := normalpath.ByDir(paths...)
// we need this to produce a deterministic order of the returned Images
Expand All @@ -541,6 +558,8 @@ func ImageByDir(image Image) ([]Image, error) {
// this should never happen
return nil, fmt.Errorf("no dir for %q in dirToPaths", dir)
}
// When includeImports is set, `paths` includes imports, and this call effectively
// promotes them to non-imports in their own image for generation.
newImage, err := ImageWithOnlyPaths(image, paths, nil)
if err != nil {
return nil, err
Expand All @@ -550,6 +569,27 @@ func ImageByDir(image Image) ([]Image, error) {
return newImages, nil
}

// ImageByDirOption is an option for ImageByDir.
type ImageByDirOption func(*imageByDirOptions)

// ImageByDirWithIncludeImports returns a new ImageByDirOption that splits
// non-well-known-type imports by directory alongside non-imports.
func ImageByDirWithIncludeImports() ImageByDirOption {
return func(imageByDirOptions *imageByDirOptions) {
imageByDirOptions.includeImports = true
}
}

// ImageByDirWithIncludeWellKnownTypes returns a new ImageByDirOption that also
// splits well-known-type imports by directory.
//
// This has no effect if ImageByDirWithIncludeImports is not set.
func ImageByDirWithIncludeWellKnownTypes() ImageByDirOption {
return func(imageByDirOptions *imageByDirOptions) {
imageByDirOptions.includeWellKnownTypes = true
}
}

// ImageToProtoImage returns a new ProtoImage for the Image.
func ImageToProtoImage(image Image) (*imagev1.Image, error) {
imageFiles := image.Files()
Expand Down Expand Up @@ -669,6 +709,15 @@ type newImageForProtoOptions struct {
computeUnusedImports bool
}

type imageByDirOptions struct {
includeImports bool
includeWellKnownTypes bool
}

func newImageByDirOptions() *imageByDirOptions {
return &imageByDirOptions{}
}

func reparseImageProto(protoImage *imagev1.Image, resolver protoencoding.Resolver, computeUnusedImports bool) error {
if err := protoencoding.ReparseExtensions(resolver, protoImage.ProtoReflect()); err != nil {
return fmt.Errorf("could not reparse image: %v", err)
Expand Down
Loading
Loading