Skip to content

Commit 3d8739b

Browse files
Copilotddstreet
authored andcommitted
feat(upstreamcommit): add generated upstream-commit store
Add a store for the generated component configuration that pins resolved upstream commits in lock-file-free mode. Each component gets a normal component TOML holding only 'spec.upstream-commit', written with a header marking it as generated, so the project's ordinary include and merge rules supply the pin to every other command. The package is self-contained and unused until the component commands adopt it; nothing changes in azldev's default mode. Refs: microsoft#323
1 parent f41c79b commit 3d8739b

2 files changed

Lines changed: 419 additions & 0 deletions

File tree

internal/upstreamcommit/store.go

Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
// Package upstreamcommit manages generated component configuration files that
5+
// pin resolved upstream commits.
6+
package upstreamcommit
7+
8+
import (
9+
"errors"
10+
"fmt"
11+
"log/slog"
12+
"path/filepath"
13+
"sort"
14+
"strings"
15+
16+
"github.com/microsoft/azure-linux-dev-tools/internal/global/opctx"
17+
"github.com/microsoft/azure-linux-dev-tools/internal/projectconfig"
18+
"github.com/microsoft/azure-linux-dev-tools/internal/utils/fileperms"
19+
"github.com/microsoft/azure-linux-dev-tools/internal/utils/fileutils"
20+
toml "github.com/pelletier/go-toml/v2"
21+
)
22+
23+
const fileExtension = ".toml"
24+
25+
const generatedFileHeader = `# This file was generated by 'azldev component refresh-upstream-commit'
26+
# Do not edit this file, changes will be lost
27+
# For more details see 'azldev component refresh-upstream-commit --help'
28+
`
29+
30+
type generatedConfig struct {
31+
Components map[string]generatedComponent `toml:"components"`
32+
}
33+
34+
type generatedComponent struct {
35+
Spec generatedSpec `toml:"spec"`
36+
}
37+
38+
type generatedSpec struct {
39+
UpstreamCommit string `toml:"upstream-commit"`
40+
}
41+
42+
// Store reads and writes generated upstream-commit component configuration.
43+
type Store struct {
44+
fs opctx.FS
45+
dir string
46+
}
47+
48+
// NewStore creates a store rooted at dir.
49+
func NewStore(fs opctx.FS, dir string) *Store {
50+
return &Store{fs: fs, dir: dir}
51+
}
52+
53+
// Dir returns the generated configuration directory.
54+
func (s *Store) Dir() string {
55+
return s.dir
56+
}
57+
58+
// Path returns the generated TOML path for componentName.
59+
func (s *Store) Path(componentName string) (string, error) {
60+
if err := fileutils.ValidateFilename(componentName); err != nil {
61+
return "", fmt.Errorf("validating component name %#q for upstream commit TOML path:\n%w",
62+
componentName, err)
63+
}
64+
65+
return filepath.Join(s.dir, componentName+fileExtension), nil
66+
}
67+
68+
// Get returns the upstream commit in a component's generated TOML file.
69+
func (s *Store) Get(componentName string) (commit string, exists bool, err error) {
70+
path, err := s.Path(componentName)
71+
if err != nil {
72+
return "", false, err
73+
}
74+
75+
exists, err = fileutils.Exists(s.fs, path)
76+
if err != nil {
77+
return "", false, fmt.Errorf("checking upstream commit TOML %#q:\n%w", path, err)
78+
}
79+
80+
if !exists {
81+
return "", false, nil
82+
}
83+
84+
data, err := fileutils.ReadFile(s.fs, path)
85+
if err != nil {
86+
return "", true, fmt.Errorf("reading upstream commit TOML %#q:\n%w", path, err)
87+
}
88+
89+
var config projectconfig.ConfigFile
90+
if err := toml.Unmarshal(data, &config); err != nil {
91+
return "", true, fmt.Errorf("parsing upstream commit TOML %#q:\n%w", path, err)
92+
}
93+
94+
component, ok := config.Components[componentName]
95+
if !ok {
96+
return "", true, fmt.Errorf(
97+
"upstream commit TOML %#q does not define component %#q", path, componentName)
98+
}
99+
100+
return component.Spec.UpstreamCommit, true, nil
101+
}
102+
103+
// Exists reports whether a generated TOML exists for componentName.
104+
func (s *Store) Exists(componentName string) (bool, error) {
105+
path, err := s.Path(componentName)
106+
if err != nil {
107+
return false, err
108+
}
109+
110+
exists, err := fileutils.Exists(s.fs, path)
111+
if err != nil {
112+
return false, fmt.Errorf("checking upstream commit TOML %#q:\n%w", path, err)
113+
}
114+
115+
return exists, nil
116+
}
117+
118+
// Remove deletes the generated TOML for componentName if it exists.
119+
func (s *Store) Remove(componentName string) (bool, error) {
120+
path, err := s.Path(componentName)
121+
if err != nil {
122+
return false, err
123+
}
124+
125+
exists, err := s.Exists(componentName)
126+
if err != nil {
127+
return false, err
128+
}
129+
130+
if !exists {
131+
return false, nil
132+
}
133+
134+
if err := s.fs.Remove(path); err != nil {
135+
return false, fmt.Errorf("removing upstream commit TOML %#q:\n%w", path, err)
136+
}
137+
138+
return true, nil
139+
}
140+
141+
// Save writes a generated component TOML override containing only upstreamCommit.
142+
func (s *Store) Save(componentName, upstreamCommit string) error {
143+
path, err := s.Path(componentName)
144+
if err != nil {
145+
return err
146+
}
147+
148+
config := generatedConfig{
149+
Components: map[string]generatedComponent{
150+
componentName: {
151+
Spec: generatedSpec{UpstreamCommit: upstreamCommit},
152+
},
153+
},
154+
}
155+
156+
data, err := toml.Marshal(config)
157+
if err != nil {
158+
return fmt.Errorf("serializing upstream commit TOML %#q:\n%w", path, err)
159+
}
160+
161+
data = append([]byte(generatedFileHeader), data...)
162+
163+
if err := fileutils.MkdirAll(s.fs, s.dir); err != nil {
164+
return fmt.Errorf("creating upstream commit TOML directory %#q:\n%w", s.dir, err)
165+
}
166+
167+
if err := fileutils.WriteFile(s.fs, path, data, fileperms.PublicFile); err != nil {
168+
return fmt.Errorf("writing upstream commit TOML %#q:\n%w", path, err)
169+
}
170+
171+
return nil
172+
}
173+
174+
// FindOrphans returns generated component TOMLs that do not correspond to an
175+
// upstream component in components.
176+
func (s *Store) FindOrphans(
177+
components map[string]projectconfig.ComponentConfig,
178+
) ([]string, error) {
179+
entries, err := fileutils.ReadDir(s.fs, s.dir)
180+
if err != nil {
181+
exists, existsErr := fileutils.DirExists(s.fs, s.dir)
182+
if existsErr != nil {
183+
return nil, fmt.Errorf("checking upstream commit TOML directory %#q:\n%w", s.dir, existsErr)
184+
}
185+
186+
if !exists {
187+
return nil, nil
188+
}
189+
190+
return nil, fmt.Errorf("reading upstream commit TOML directory %#q:\n%w", s.dir, err)
191+
}
192+
193+
var orphans []string
194+
195+
for _, entry := range entries {
196+
if entry.IsDir() || strings.HasPrefix(entry.Name(), ".") ||
197+
!strings.HasSuffix(entry.Name(), fileExtension) {
198+
continue
199+
}
200+
201+
name := strings.TrimSuffix(entry.Name(), fileExtension)
202+
203+
component, ok := components[name]
204+
if !ok || component.Spec.SourceType != projectconfig.SpecSourceTypeUpstream {
205+
orphans = append(orphans, name)
206+
}
207+
}
208+
209+
sort.Strings(orphans)
210+
211+
return orphans, nil
212+
}
213+
214+
// PruneOrphans removes generated TOMLs that are no longer needed.
215+
func (s *Store) PruneOrphans(components map[string]projectconfig.ComponentConfig) (int, error) {
216+
orphans, err := s.FindOrphans(components)
217+
if err != nil {
218+
return 0, err
219+
}
220+
221+
var errs []error
222+
223+
pruned := 0
224+
225+
for _, name := range orphans {
226+
path, pathErr := s.Path(name)
227+
if pathErr != nil {
228+
errs = append(errs, pathErr)
229+
230+
continue
231+
}
232+
233+
slog.Info("Removing orphan upstream commit TOML", "component", name)
234+
235+
if removeErr := s.fs.Remove(path); removeErr != nil {
236+
errs = append(errs, fmt.Errorf("removing upstream commit TOML for %#q:\n%w", name, removeErr))
237+
238+
continue
239+
}
240+
241+
pruned++
242+
}
243+
244+
if len(errs) > 0 {
245+
return pruned, errors.Join(errs...)
246+
}
247+
248+
return pruned, nil
249+
}

0 commit comments

Comments
 (0)