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
57 changes: 57 additions & 0 deletions private/buf/bufgen/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ import (
"errors"
"fmt"
"log/slog"
"os/exec"
"path/filepath"
"sort"
"strings"

"buf.build/go/app"
"buf.build/go/standard/xslices"
Expand Down Expand Up @@ -195,9 +197,64 @@ func (g *generator) generateCode(
if err := responseWriter.Close(); err != nil {
return err
}
if err := g.runPostCommands(ctx, baseOutDir, pluginConfigs); err != nil {
return err
}
return nil
}

func (g *generator) runPostCommands(
ctx context.Context,
baseOutDir string,
pluginConfigs []bufconfig.GeneratePluginConfig,
) error {
for _, pluginConfig := range pluginConfigs {
postCommands := pluginConfig.PostCommands()
if len(postCommands) == 0 {
continue
}
out := pluginConfig.Out()
if baseOutDir != "" && baseOutDir != "." {
out = filepath.Join(baseOutDir, out)
}
for _, command := range postCommands {
substitutedCommand := substitutePostCommandVariables(command, pluginConfig, out)
if err := g.executePostCommand(ctx, substitutedCommand); err != nil {
return fmt.Errorf("plugin %s post-processing command %q failed: %w", pluginConfig.Name(), command, err)
}
}
}
return nil
}

func substitutePostCommandVariables(command string, pluginConfig bufconfig.GeneratePluginConfig, resolvedOut string) string {
result := command
result = strings.ReplaceAll(result, "$out", resolvedOut)
result = strings.ReplaceAll(result, "$name", pluginConfig.Name())
result = strings.ReplaceAll(result, "$opt", pluginConfig.Opt())
result = strings.ReplaceAll(result, "$path", strings.Join(pluginConfig.Path(), " "))
var strategyStr string
switch pluginConfig.Strategy() {
case bufconfig.GenerateStrategyDirectory:
strategyStr = "directory"
case bufconfig.GenerateStrategyAll:
strategyStr = "all"
}
result = strings.ReplaceAll(result, "$strategy", strategyStr)
return result
}

func (g *generator) executePostCommand(ctx context.Context, command string) error {
parts := strings.Fields(command)
if len(parts) == 0 {
return nil
}
cmd := exec.CommandContext(ctx, parts[0], parts[1:]...)
cmd.Stdout = nil
cmd.Stderr = nil
return cmd.Run()
}

func (g *generator) execPlugins(
ctx context.Context,
container app.EnvStdioContainer,
Expand Down
126 changes: 126 additions & 0 deletions private/buf/bufgen/postprocess_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// Copyright 2020-2025 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.

package bufgen

import (
"testing"

"github.com/bufbuild/buf/private/bufpkg/bufconfig"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestSubstitutePostCommandVariables(t *testing.T) {
t.Parallel()

strategy := bufconfig.GenerateStrategyDirectory
pluginConfig, err := bufconfig.NewLocalGeneratePluginConfig(
"protoc-gen-python",
"gen/python",
[]string{"paths=source_relative", "module=test"},
false,
false,
nil,
nil,
&strategy,
[]string{"/usr/bin/protoc-gen-python"},
)
require.NoError(t, err)

testCases := []struct {
name string
command string
out string
expected string
}{
{
name: "substitute $out",
command: "ruff --fix $out",
out: "gen/python",
expected: "ruff --fix gen/python",
},
{
name: "substitute $out with base dir",
command: "black $out",
out: "output/gen/python",
expected: "black output/gen/python",
},
{
name: "substitute $name",
command: "echo $name",
out: "gen/python",
expected: "echo protoc-gen-python",
},
{
name: "substitute $opt",
command: "echo $opt",
out: "gen/python",
expected: "echo paths=source_relative,module=test",
},
{
name: "substitute $path",
command: "echo $path",
out: "gen/python",
expected: "echo /usr/bin/protoc-gen-python",
},
{
name: "substitute $strategy",
command: "echo $strategy",
out: "gen/python",
expected: "echo directory",
},
{
name: "substitute multiple variables",
command: "process --dir $out --plugin $name --strategy $strategy",
out: "gen/python",
expected: "process --dir gen/python --plugin protoc-gen-python --strategy directory",
},
{
name: "no substitution needed",
command: "gofmt -w .",
out: "gen/python",
expected: "gofmt -w .",
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
result := substitutePostCommandVariables(tc.command, pluginConfig, tc.out)
assert.Equal(t, tc.expected, result)
})
}
}

func TestSubstitutePostCommandVariablesStrategyAll(t *testing.T) {
t.Parallel()

strategy := bufconfig.GenerateStrategyAll
pluginConfig, err := bufconfig.NewLocalGeneratePluginConfig(
"protoc-gen-go",
"gen/go",
nil,
false,
false,
nil,
nil,
&strategy,
[]string{"protoc-gen-go"},
)
require.NoError(t, err)

result := substitutePostCommandVariables("echo $strategy", pluginConfig, "gen/go")
assert.Equal(t, "echo all", result)
}
21 changes: 13 additions & 8 deletions private/bufpkg/bufconfig/buf_gen_yaml_file.go
Original file line number Diff line number Diff line change
Expand Up @@ -281,11 +281,12 @@ type externalBufGenYAMLFileV1Beta1 struct {

// externalGeneratePluginConfigV1Beta1 represents a single plugin config in a v1beta1 buf.gen.yaml file.
type externalGeneratePluginConfigV1Beta1 struct {
Name string `json:"name,omitempty" yaml:"name,omitempty"`
Out string `json:"out,omitempty" yaml:"out,omitempty"`
Opt any `json:"opt,omitempty" yaml:"opt,omitempty"`
Path string `json:"path,omitempty" yaml:"path,omitempty"`
Strategy string `json:"strategy,omitempty" yaml:"strategy,omitempty"`
Name string `json:"name,omitempty" yaml:"name,omitempty"`
Out string `json:"out,omitempty" yaml:"out,omitempty"`
Opt any `json:"opt,omitempty" yaml:"opt,omitempty"`
Path string `json:"path,omitempty" yaml:"path,omitempty"`
Strategy string `json:"strategy,omitempty" yaml:"strategy,omitempty"`
PostprocessCmd []string `json:"postprocess_cmd,omitempty" yaml:"postprocess_cmd,omitempty"`
}

// externalGenerateManagedConfigV1Beta1 represents the options (for managed mode) config in a v1beta1 buf.gen.yaml file.
Expand Down Expand Up @@ -316,9 +317,10 @@ type externalGeneratePluginConfigV1 struct {
// Opt can be one string or multiple strings.
Opt any `json:"opt,omitempty" yaml:"opt,omitempty"`
// Path can be one string or multiple strings.
Path any `json:"path,omitempty" yaml:"path,omitempty"`
ProtocPath any `json:"protoc_path,omitempty" yaml:"protoc_path,omitempty"`
Strategy string `json:"strategy,omitempty" yaml:"strategy,omitempty"`
Path any `json:"path,omitempty" yaml:"path,omitempty"`
ProtocPath any `json:"protoc_path,omitempty" yaml:"protoc_path,omitempty"`
Strategy string `json:"strategy,omitempty" yaml:"strategy,omitempty"`
PostprocessCmd []string `json:"postprocess_cmd,omitempty" yaml:"postprocess_cmd,omitempty"`
}

// externalGenerateManagedConfigV1 represents the managed mode config in a v1 buf.gen.yaml file.
Expand Down Expand Up @@ -533,6 +535,9 @@ type externalGeneratePluginConfigV2 struct {
Types []string `json:"types,omitempty" yaml:"types,omitempty"`
// ExcludeTypes removes types from the image.
ExcludeTypes []string `json:"exclude_types,omitempty" yaml:"exclude_types,omitempty"`
// PostprocessCmd is a list of commands to run after code generation for this plugin.
// Variables like $out, $name, $opt, $path, $strategy can be used and will be substituted.
PostprocessCmd []string `json:"postprocess_cmd,omitempty" yaml:"postprocess_cmd,omitempty"`
}

// externalGenerateManagedConfigV2 represents the managed mode config in a v2 buf.gen.yaml file.
Expand Down
59 changes: 59 additions & 0 deletions private/bufpkg/bufconfig/buf_gen_yaml_file_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,65 @@ inputs:
)
}

func TestBufGenYAMLFilePostprocessCmd(t *testing.T) {
t.Parallel()

testReadWriteBufGenYAMLFileRoundTrip(
t,
// input v2 with postprocess_cmd
`version: v2
plugins:
- local: protoc-gen-go
out: gen/go
postprocess_cmd:
- "gofmt -w $out"
- "goimports -w $out"
`,
// expected output
`version: v2
plugins:
- local: protoc-gen-go
out: gen/go
postprocess_cmd:
- gofmt -w $out
- goimports -w $out
`,
)

testReadWriteBufGenYAMLFileRoundTrip(
t,
// input v1 with postprocess_cmd
`version: v1
plugins:
- plugin: go
out: gen/go
postprocess_cmd:
- "ruff --fix $out"
`,
// expected output
`version: v2
plugins:
- local: protoc-gen-go
out: gen/go
postprocess_cmd:
- ruff --fix $out
`,
)

bufGenYAMLFile := testReadBufGenYAMLFile(t, `version: v2
plugins:
- local: protoc-gen-python
out: gen/python
opt: paths=source_relative
postprocess_cmd:
- "ruff --fix $out"
- "black $out"
`)
pluginConfigs := bufGenYAMLFile.GenerateConfig().GeneratePluginConfigs()
require.Len(t, pluginConfigs, 1)
require.Equal(t, []string{"ruff --fix $out", "black $out"}, pluginConfigs[0].PostCommands())
}

func TestBufGenYAMLFileManagedErrors(t *testing.T) {
t.Parallel()

Expand Down
Loading