forked from Azure/azure-sdk-for-go
-
Notifications
You must be signed in to change notification settings - Fork 1
/
root.go
261 lines (233 loc) · 7.48 KB
/
root.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
package cmd
import (
"fmt"
"log"
"os"
"path/filepath"
"sort"
"strings"
"time"
"github.com/Azure/azure-sdk-for-go/tools/generator/autorest/model"
"github.com/Azure/azure-sdk-for-go/tools/generator/pipeline"
"github.com/Azure/azure-sdk-for-go/tools/generator/utils"
"github.com/Azure/azure-sdk-for-go/tools/internal/ioext"
"github.com/spf13/cobra"
)
const (
defaultOptionPath = "generate_options.json"
)
// Command returns the command for the generator. Note that this command is designed to run in the root directory of
// azure-sdk-for-go. It does not work if you are running this tool in somewhere else
func Command() *cobra.Command {
rootCmd := &cobra.Command{
Use: "generator <generate input filepath> <generate output filepath>",
Args: cobra.ExactArgs(2),
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
log.SetFlags(0) // remove the time stamp prefix
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
optionPath, err := cmd.Flags().GetString("options")
if err != nil {
return err
}
return execute(args[0], args[1], Flags{
OptionPath: optionPath,
})
},
SilenceUsage: true, // this command is used for a pipeline, the usage should never show
}
flags := rootCmd.Flags()
flags.String("options", defaultOptionPath, "Specify a file with the autorest options")
return rootCmd
}
// Flags ...
type Flags struct {
OptionPath string
}
func execute(inputPath, outputPath string, flags Flags) error {
log.Printf("Reading generate input file from '%s'...", inputPath)
input, err := pipeline.ReadInput(inputPath)
if err != nil {
return fmt.Errorf("cannot read generate input: %+v", err)
}
log.Printf("Generating using the following GenerateInput...\n%s", input.String())
cwd, err := os.Getwd()
if err != nil {
return err
}
log.Printf("Backuping azure-sdk-for-go to temp directory...")
backupRoot, err := backupSDKRepository(cwd)
if err != nil {
return err
}
defer eraseBackup(backupRoot)
log.Printf("Finished backuping to '%s'", backupRoot)
ctx := generateContext{
sdkRoot: utils.NormalizePath(cwd),
clnRoot: backupRoot,
specRoot: input.SpecFolder,
commitHash: input.HeadSha,
optionPath: flags.OptionPath,
}
output, err := ctx.generate(input)
if err != nil {
return err
}
log.Printf("Output generated: \n%s", output.String())
log.Printf("Writing output to file '%s'...", outputPath)
if err := pipeline.WriteOutput(outputPath, output); err != nil {
return fmt.Errorf("cannot write generate output: %+v", err)
}
return nil
}
func backupSDKRepository(sdk string) (string, error) {
tempRepoDir := filepath.Join(tempDir(), fmt.Sprintf("generator-%v", time.Now().Unix()))
if err := ioext.CopyDir(sdk, tempRepoDir); err != nil {
return "", fmt.Errorf("failed to backup azure-sdk-for-go to '%s': %+v", tempRepoDir, err)
}
return tempRepoDir, nil
}
func eraseBackup(tempDir string) error {
return os.RemoveAll(tempDir)
}
func tempDir() string {
if dir := os.Getenv("TMP_DIR"); dir != "" {
return dir
}
return os.TempDir()
}
type generateContext struct {
sdkRoot string
clnRoot string
specRoot string
commitHash string
optionPath string
}
// TODO -- support dry run
func (ctx generateContext) generate(input *pipeline.GenerateInput) (*pipeline.GenerateOutput, error) {
if input.DryRun {
return nil, fmt.Errorf("dry run not supported yet")
}
log.Printf("Reading options from file '%s'...", ctx.optionPath)
// now we summary all the metadata in sdk
log.Printf("Cleaning up all the packages related with the following readme files: [%s]", strings.Join(input.RelatedReadmeMdFiles, ", "))
cleanUpCtx := cleanUpContext{
root: filepath.Join(ctx.sdkRoot, "services"),
readmeFiles: input.RelatedReadmeMdFiles,
}
removedPackages, err := cleanUpCtx.clean()
if err != nil {
return nil, err
}
var removedPackagePaths []string
for _, p := range removedPackages.packages() {
removedPackagePaths = append(removedPackagePaths, p.outputFolder)
}
log.Printf("The following %d package(s) have been cleaned up: [%s]", len(removedPackagePaths), strings.Join(removedPackagePaths, ", "))
optionFile, err := os.Open(ctx.optionPath)
if err != nil {
return nil, err
}
options, err := model.NewOptionsFrom(optionFile)
if err != nil {
return nil, err
}
log.Printf("Autorest options: \n%+v", options)
// iterate over all the readme
results := make([]pipeline.PackageResult, 0)
errorBuilder := generateErrorBuilder{}
for _, readme := range input.RelatedReadmeMdFiles {
log.Printf("Processing readme '%s'...", readme)
absReadme := filepath.Join(input.SpecFolder, readme)
// generate code
g := autorestContext{
absReadme: absReadme,
metadataOutput: filepath.Dir(absReadme),
options: options,
}
if err := g.generate(); err != nil {
errorBuilder.add(fmt.Errorf("cannot generate readme '%s': %+v", readme, err))
continue
}
m := changelogContext{
sdkRoot: ctx.sdkRoot,
clnRoot: ctx.clnRoot,
specRoot: ctx.specRoot,
commitHash: ctx.commitHash,
codeGenVer: options.CodeGeneratorVersion(),
readme: readme,
removedPackages: removedPackages[readme],
}
log.Printf("Processing metadata generated in readme '%s'...", readme)
packages, err := m.process(g.metadataOutput)
if err != nil {
errorBuilder.add(fmt.Errorf("cannot process metadata for readme '%s': %+v", readme, err))
continue
}
// iterate over the changed packages
set := packageResultSet{}
for _, p := range packages {
log.Printf("Getting package result for package '%s'", p.PackageName)
content := p.Changelog.ToCompactMarkdown()
breaking := p.Changelog.HasBreakingChanges()
set.add(pipeline.PackageResult{
PackageName: getPackageIdentifier(p.PackageName),
Path: []string{p.PackageName},
ReadmeMd: []string{readme},
Changelog: &pipeline.Changelog{
Content: &content,
HasBreakingChange: &breaking,
},
})
}
results = append(results, set.toSlice()...)
}
return &pipeline.GenerateOutput{
Packages: results,
}, errorBuilder.build()
}
type generateErrorBuilder struct {
errors []error
}
func (b *generateErrorBuilder) add(err error) {
b.errors = append(b.errors, err)
}
func (b *generateErrorBuilder) build() error {
if len(b.errors) == 0 {
return nil
}
var messages []string
for _, err := range b.errors {
messages = append(messages, err.Error())
}
return fmt.Errorf("total %d error(s): \n%s", len(b.errors), strings.Join(messages, "\n"))
}
type packageResultSet map[string]pipeline.PackageResult
func (s *packageResultSet) contains(r pipeline.PackageResult) bool {
_, ok := (*s)[r.PackageName]
return ok
}
func (s *packageResultSet) add(r pipeline.PackageResult) {
if s.contains(r) {
log.Printf("[WARNING] The result set already contains key %s with value %+v, but we are still trying to insert a new value %+v on the same key", r.PackageName, (*s)[r.PackageName], r)
}
(*s)[r.PackageName] = r
}
func (s *packageResultSet) toSlice() []pipeline.PackageResult {
results := make([]pipeline.PackageResult, 0)
for _, r := range *s {
results = append(results, r)
}
// sort the results
sort.SliceStable(results, func(i, j int) bool {
// we first clip the preview segment and then sort by string literal
pI := strings.Replace(results[i].PackageName, "preview/", "/", 1)
pJ := strings.Replace(results[j].PackageName, "preview/", "/", 1)
return pI > pJ
})
return results
}
func getPackageIdentifier(pkg string) string {
return strings.TrimPrefix(utils.NormalizePath(pkg), "services/")
}