Skip to content

Commit 285e1b0

Browse files
authored
Update fetcher to bump package versions in nuget config (#2361)
1 parent 3806978 commit 285e1b0

3 files changed

Lines changed: 243 additions & 0 deletions

File tree

internal/cmd/fetcher/main.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"github.com/bufbuild/plugins/internal/fetchclient"
2727
"github.com/bufbuild/plugins/internal/git"
2828
"github.com/bufbuild/plugins/internal/maven"
29+
"github.com/bufbuild/plugins/internal/nuget"
2930
"github.com/bufbuild/plugins/internal/plugin"
3031
"github.com/bufbuild/plugins/internal/source"
3132
)
@@ -144,6 +145,9 @@ func postProcessCreatedPlugins(ctx context.Context, logger *slog.Logger, plugins
144145
if err := regenerateMavenDeps(plugin); err != nil {
145146
return fmt.Errorf("failed to regenerate maven deps for %s: %w", newPluginRef, err)
146147
}
148+
if err := regenerateNugetDeps(plugin); err != nil {
149+
return fmt.Errorf("failed to regenerate nuget deps for %s: %w", newPluginRef, err)
150+
}
147151
if err := runGoModTidy(ctx, logger, plugin); err != nil {
148152
return fmt.Errorf("failed to run go mod tidy for %s: %w", newPluginRef, err)
149153
}
@@ -293,6 +297,13 @@ func regenerateMavenDeps(plugin createdPlugin) error {
293297
return maven.RegenerateMavenDeps(versionDir, pluginsDir)
294298
}
295299

300+
// regenerateNugetDeps regenerates the build.csproj from the plugin's buf.plugin.yaml.
301+
func regenerateNugetDeps(plugin createdPlugin) error {
302+
versionDir := filepath.Join(plugin.pluginDir, plugin.newVersion)
303+
pluginsDir := filepath.Dir(filepath.Dir(plugin.pluginDir))
304+
return nuget.RegenerateNugetDeps(versionDir, pluginsDir)
305+
}
306+
296307
// runPluginTests runs 'make test PLUGINS="org/name:v<new>"' in order to generate plugin.sum files.
297308
func runPluginTests(ctx context.Context, logger *slog.Logger, plugins []createdPlugin) error {
298309
pluginsEnv := make([]string, 0, len(plugins))

internal/nuget/regenerate.go

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
package nuget
2+
3+
import (
4+
"encoding/xml"
5+
"fmt"
6+
"os"
7+
"path/filepath"
8+
"regexp"
9+
"sort"
10+
"strings"
11+
12+
"github.com/bufbuild/buf/private/bufpkg/bufremoteplugin/bufremotepluginconfig"
13+
)
14+
15+
var selfClosingTagPattern = regexp.MustCompile(`(<\w+[^>]*?)></\w+>`)
16+
17+
// RegenerateNugetDeps processes a NuGet plugin version directory by
18+
// collecting all transitive NuGet dependencies from the plugin's
19+
// buf.plugin.yaml and regenerating the build.csproj file.
20+
func RegenerateNugetDeps(pluginVersionDir, pluginsDir string) error {
21+
yamlPath := filepath.Join(pluginVersionDir, "buf.plugin.yaml")
22+
pluginConfig, err := bufremotepluginconfig.ParseConfig(yamlPath)
23+
if err != nil {
24+
return err
25+
}
26+
if pluginConfig.Registry == nil || pluginConfig.Registry.Nuget == nil {
27+
return nil
28+
}
29+
dependencies, err := collectAllNugetDeps(pluginConfig, pluginsDir)
30+
if err != nil {
31+
return fmt.Errorf("collecting nuget deps: %w", err)
32+
}
33+
csproj, err := renderCsproj(pluginConfig.Registry.Nuget.TargetFrameworks, dependencies)
34+
if err != nil {
35+
return fmt.Errorf("rendering csproj: %w", err)
36+
}
37+
csprojPath := filepath.Join(pluginVersionDir, "build.csproj")
38+
if err := os.WriteFile(csprojPath, []byte(csproj), 0644); err != nil { //nolint:gosec // file permissions are intentional
39+
return fmt.Errorf("writing build.csproj: %w", err)
40+
}
41+
return nil
42+
}
43+
44+
// nugetDep represents a single NuGet package dependency.
45+
type nugetDep struct {
46+
name string
47+
version string
48+
}
49+
50+
// collectAllNugetDeps walks the plugin's dependency tree and collects
51+
// all NuGet dependencies, including transitive ones from plugin deps.
52+
// Dependencies from deeper in the tree are collected first, matching
53+
// the order used by the test's populateNugetDeps function.
54+
func collectAllNugetDeps(
55+
pluginConfig *bufremotepluginconfig.Config,
56+
pluginsDir string,
57+
) ([]nugetDep, error) {
58+
dependencies := make(map[string]string)
59+
visited := make(map[string]bool)
60+
if err := collectNugetDepsRecursive(pluginConfig, pluginsDir, visited, dependencies); err != nil {
61+
return nil, err
62+
}
63+
result := make([]nugetDep, 0, len(dependencies))
64+
for name, version := range dependencies {
65+
result = append(result, nugetDep{name: name, version: version})
66+
}
67+
sort.Slice(result, func(i, j int) bool {
68+
return result[i].name < result[j].name
69+
})
70+
return result, nil
71+
}
72+
73+
func collectNugetDepsRecursive(
74+
pluginConfig *bufremotepluginconfig.Config,
75+
pluginsDir string,
76+
visited map[string]bool,
77+
dependencies map[string]string,
78+
) error {
79+
// First recurse into plugin dependencies.
80+
for _, dep := range pluginConfig.Dependencies {
81+
depKey := dep.IdentityString() + ":" + dep.Version()
82+
if visited[depKey] {
83+
continue
84+
}
85+
visited[depKey] = true
86+
depPath := filepath.Join(
87+
pluginsDir, dep.Owner(), dep.Plugin(),
88+
dep.Version(), "buf.plugin.yaml",
89+
)
90+
depConfig, err := bufremotepluginconfig.ParseConfig(depPath)
91+
if err != nil {
92+
return fmt.Errorf("loading dep config %s from %s: %w", depKey, depPath, err)
93+
}
94+
if err := collectNugetDepsRecursive(depConfig, pluginsDir, visited, dependencies); err != nil {
95+
return err
96+
}
97+
}
98+
// Then collect this plugin's own NuGet deps.
99+
if pluginConfig.Registry != nil && pluginConfig.Registry.Nuget != nil {
100+
for _, dep := range pluginConfig.Registry.Nuget.Deps {
101+
dependencies[dep.Name] = dep.Version
102+
}
103+
}
104+
return nil
105+
}
106+
107+
// packageReference represents a PackageReference element in a csproj file.
108+
type packageReference struct {
109+
XMLName xml.Name `xml:"PackageReference"`
110+
Include string `xml:"Include,attr"`
111+
Version string `xml:"Version,attr"`
112+
}
113+
114+
// propertyGroup represents a PropertyGroup element in a csproj file.
115+
type propertyGroup struct {
116+
XMLName xml.Name `xml:"PropertyGroup"`
117+
TargetFramework string `xml:"TargetFramework,omitempty"`
118+
TargetFrameworks string `xml:"TargetFrameworks,omitempty"`
119+
}
120+
121+
// itemGroup represents an ItemGroup element in a csproj file.
122+
type itemGroup struct {
123+
XMLName xml.Name `xml:"ItemGroup"`
124+
PackageReferences []packageReference `xml:"PackageReference"`
125+
}
126+
127+
// csharpProject represents a .csproj XML file.
128+
type csharpProject struct {
129+
XMLName xml.Name `xml:"Project"`
130+
SDK string `xml:"Sdk,attr"`
131+
PropertyGroup propertyGroup `xml:"PropertyGroup"`
132+
ItemGroup itemGroup `xml:"ItemGroup"`
133+
}
134+
135+
// renderCsproj generates a build.csproj file from target frameworks and dependencies.
136+
func renderCsproj(targetFrameworks []string, dependencies []nugetDep) (string, error) {
137+
project := csharpProject{
138+
SDK: "Microsoft.NET.Sdk",
139+
}
140+
if len(targetFrameworks) == 1 {
141+
project.PropertyGroup.TargetFramework = targetFrameworks[0]
142+
} else {
143+
project.PropertyGroup.TargetFrameworks = strings.Join(targetFrameworks, ";")
144+
}
145+
for _, dep := range dependencies {
146+
project.ItemGroup.PackageReferences = append(project.ItemGroup.PackageReferences, packageReference{
147+
Include: dep.name,
148+
Version: dep.version,
149+
})
150+
}
151+
output, err := xml.MarshalIndent(project, "", " ")
152+
if err != nil {
153+
return "", fmt.Errorf("marshaling csproj: %w", err)
154+
}
155+
// Convert empty XML elements to self-closing tags to match .csproj conventions.
156+
result := selfClosingTagPattern.ReplaceAllString(string(output), "$1 />")
157+
return result + "\n", nil
158+
}

internal/nuget/regenerate_test.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package nuget
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
"github.com/stretchr/testify/require"
8+
)
9+
10+
func TestRenderCsproj(t *testing.T) {
11+
t.Parallel()
12+
t.Run("single_framework", func(t *testing.T) {
13+
t.Parallel()
14+
got, err := renderCsproj(
15+
[]string{"netstandard2.0"},
16+
[]nugetDep{
17+
{name: "Google.Protobuf", version: "3.34.1"},
18+
},
19+
)
20+
require.NoError(t, err)
21+
want := `<Project Sdk="Microsoft.NET.Sdk">
22+
<PropertyGroup>
23+
<TargetFramework>netstandard2.0</TargetFramework>
24+
</PropertyGroup>
25+
<ItemGroup>
26+
<PackageReference Include="Google.Protobuf" Version="3.34.1" />
27+
</ItemGroup>
28+
</Project>
29+
`
30+
assert.Equal(t, want, got)
31+
})
32+
t.Run("multiple_deps", func(t *testing.T) {
33+
t.Parallel()
34+
got, err := renderCsproj(
35+
[]string{"netstandard2.0"},
36+
[]nugetDep{
37+
{name: "Google.Protobuf", version: "3.34.1"},
38+
{name: "Grpc.Net.Common", version: "2.76.0"},
39+
},
40+
)
41+
require.NoError(t, err)
42+
want := `<Project Sdk="Microsoft.NET.Sdk">
43+
<PropertyGroup>
44+
<TargetFramework>netstandard2.0</TargetFramework>
45+
</PropertyGroup>
46+
<ItemGroup>
47+
<PackageReference Include="Google.Protobuf" Version="3.34.1" />
48+
<PackageReference Include="Grpc.Net.Common" Version="2.76.0" />
49+
</ItemGroup>
50+
</Project>
51+
`
52+
assert.Equal(t, want, got)
53+
})
54+
t.Run("multiple_frameworks", func(t *testing.T) {
55+
t.Parallel()
56+
got, err := renderCsproj(
57+
[]string{"netstandard2.0", "net6.0"},
58+
[]nugetDep{
59+
{name: "Google.Protobuf", version: "3.34.1"},
60+
},
61+
)
62+
require.NoError(t, err)
63+
want := `<Project Sdk="Microsoft.NET.Sdk">
64+
<PropertyGroup>
65+
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks>
66+
</PropertyGroup>
67+
<ItemGroup>
68+
<PackageReference Include="Google.Protobuf" Version="3.34.1" />
69+
</ItemGroup>
70+
</Project>
71+
`
72+
assert.Equal(t, want, got)
73+
})
74+
}

0 commit comments

Comments
 (0)