1// Copyright (c) Microsoft Corporation. All rights reserved. 2// Licensed under the MIT License. See License.txt in the project root for license information. 3 4package autorest 5 6import ( 7 "bufio" 8 "encoding/json" 9 "fmt" 10 "io" 11 "log" 12 "os" 13 "path/filepath" 14 "strings" 15 "time" 16 17 "github.com/Azure/azure-sdk-for-go/tools/generator/autorest/model" 18 "github.com/Azure/azure-sdk-for-go/tools/internal/exports" 19 "github.com/Azure/azure-sdk-for-go/tools/internal/utils" 20) 21 22// GenerateContext describes the context that would be used in an autorest generation task 23type GenerateContext interface { 24 SDKRoot() string 25 SpecRoot() string 26 RepoContent() map[string]exports.Content 27} 28 29// GenerateInput describes the input information for a package generation 30type GenerateInput struct { 31 // Readme is the relative path of the readme file to the root directory of azure-sdk-for-go 32 Readme string 33 // Tag is the readme tag to be generated 34 Tag string 35 // CommitHash is the head commit hash of azure-rest-api-specs 36 CommitHash string 37 // Options specifies the options that this generation task will be using 38 Options model.Options 39} 40 41// GenerateOptions describes the options for a package generation 42type GenerateOptions struct { 43 // MetadataOutputRoot specifies the root directory of all the metadata goes. 44 // Metadata will be generated to a temp directory if not specified. 45 // The metadataOutput directory will not be removed after the generation succeeded 46 MetadataOutputRoot string 47 // Stderr ... 48 Stderr io.Writer 49 // Stdout ... 50 Stdout io.Writer 51 // AutoRestLogPrefix ... 52 AutoRestLogPrefix string 53 // ChangelogTitle 54 ChangelogTitle string 55 // Validators ... 56 Validators []MetadataValidateFunc 57} 58 59// GenerateResult describes the result of a generation task 60type GenerateResult struct { 61 // MetadataOutputRoot stores the metadata output root which is the same as in options, or randomly generated if not specified in options 62 MetadataOutputRoot string 63 // Metadata is the GenerationMetadata of the generated package 64 Metadata GenerationMetadata 65 // Package is the changelog information of the generated package 66 Package ChangelogResult 67} 68 69// GeneratePackage is a wrapper function of the autorest execution task 70func GeneratePackage(ctx GenerateContext, input GenerateInput, options GenerateOptions) (*GenerateResult, error) { 71 if err := input.validate(); err != nil { 72 return nil, err 73 } 74 if err := options.validate(); err != nil { 75 return nil, err 76 } 77 78 absReadme := filepath.Join(ctx.SpecRoot(), input.Readme) 79 metadataOutput := filepath.Join(options.MetadataOutputRoot, input.Tag) 80 g := NewGeneratorFromOptions(input.Options).WithTag(input.Tag).WithMetadataOutput(metadataOutput).WithReadme(absReadme) 81 82 // generate 83 if err := generate(g, options.Stdout, options.Stderr, options.AutoRestLogPrefix); err != nil { 84 return nil, fmt.Errorf("failed to execute autorest: %+v", err) 85 } 86 87 // parse the metadata from autorest 88 metadataMap, err := NewMetadataProcessorFromLocation(metadataOutput).Process() 89 if err != nil { 90 return nil, fmt.Errorf("failed to parse metadata in '%s': %+v", metadataOutput, err) 91 } 92 93 // validate 94 if err := validate(input.Readme, metadataMap, options.Validators); err != nil { 95 return nil, fmt.Errorf("failed in validation: %+v", err) 96 } 97 98 // write the changelog and metadata file 99 result, metadata, err := changelogAndMetadata(ctx, input, metadataMap, options.ChangelogTitle, g.Arguments()) 100 if err != nil { 101 return nil, err 102 } 103 104 return &GenerateResult{ 105 MetadataOutputRoot: options.MetadataOutputRoot, 106 Metadata: *metadata, 107 Package: *result, 108 }, nil 109} 110 111func generate(generator *Generator, stdout, stderr io.Writer, prefix string) error { 112 stdoutPipe, _ := generator.StdoutPipe() 113 stderrPipe, _ := generator.StderrPipe() 114 defer stdoutPipe.Close() 115 defer stderrPipe.Close() 116 var arguments []string 117 for _, o := range generator.Arguments() { 118 arguments = append(arguments, o.Format()) 119 } 120 log.Printf("Generation parameters: %s", strings.Join(arguments, ", ")) 121 _ = generator.Start() 122 // we put all the output from autorest to stderr since those are logs in order not to interrupt the proper output of the release command 123 go scannerPrint(bufio.NewScanner(stdoutPipe), stdout, prefix) 124 go scannerPrint(bufio.NewScanner(stderrPipe), stderr, prefix) 125 return generator.Wait() 126} 127 128func validate(readme string, metadataMap map[string]model.Metadata, validators []MetadataValidateFunc) error { 129 builder := validationErrorBuilder{ 130 readme: readme, 131 } 132 133 for tag, metadata := range metadataMap { 134 errors := ValidateMetadata(validators, tag, metadata) 135 if len(errors) != 0 { 136 builder.add(errors...) 137 } 138 } 139 140 return builder.build() 141} 142 143type validationErrorBuilder struct { 144 readme string 145 errors []error 146} 147 148func (b *validationErrorBuilder) add(errors ...error) { 149 b.errors = append(b.errors, errors...) 150} 151 152func (b *validationErrorBuilder) build() error { 153 if len(b.errors) == 0 { 154 return nil 155 } 156 var messages []string 157 for _, e := range b.errors { 158 messages = append(messages, e.Error()) 159 } 160 return fmt.Errorf("validation failed in readme '%s' with %d error(s): \n%s", b.readme, len(b.errors), strings.Join(messages, "\n")) 161} 162 163func changelogAndMetadata(ctx GenerateContext, input GenerateInput, metadataMap map[string]model.Metadata, changelogTitle string, argument []model.Option) (*ChangelogResult, *GenerationMetadata, error) { 164 result, err := changelog(ctx, metadataMap, changelogTitle) 165 if err != nil { 166 return nil, nil, fmt.Errorf("failed to write changelog file: %+v", err) 167 } 168 169 // write the metadata file 170 metadata, err := metadata(input, *result, argument) 171 if err != nil { 172 return nil, nil, fmt.Errorf("failed to write metadata file: %+v", err) 173 } 174 175 return result, metadata, nil 176} 177 178func changelog(ctx GenerateContext, metadataMap map[string]model.Metadata, changelogTitle string) (*ChangelogResult, error) { 179 // process the changelog 180 changelogResults, err := NewChangelogProcessorFromContext(ctx).Process(metadataMap) 181 if err != nil { 182 return nil, fmt.Errorf("failed to process the changelog: %+v", err) 183 } 184 // we should only have one changelog 185 if len(changelogResults) != 1 { 186 return nil, fmt.Errorf("expecting 1 changelog result, but got %d", len(changelogResults)) 187 } 188 189 changelogPath, err := WriteChangelogFile(changelogTitle, changelogResults[0]) 190 if err != nil { 191 return nil, fmt.Errorf("failed to write changelog file: %+v", err) 192 } 193 log.Printf("changelog file writes to '%s'", changelogPath) 194 return &changelogResults[0], nil 195} 196 197func metadata(input GenerateInput, result ChangelogResult, arguments []model.Option) (*GenerationMetadata, error) { 198 metadata := getMetadata(input, result, arguments) 199 metadataPath, err := WriteMetadataFile(result.PackageFullPath, metadata) 200 if err != nil { 201 return nil, err 202 } 203 log.Printf("metadata file writes to '%s'", metadataPath) 204 return &metadata, nil 205} 206 207func getMetadata(input GenerateInput, result ChangelogResult, arguments []model.Option) GenerationMetadata { 208 options := AdditionalOptionsToString(arguments) 209 codeGenVersion := input.Options.CodeGeneratorVersion() 210 return GenerationMetadata{ 211 CommitHash: input.CommitHash, 212 Readme: NormalizedSpecRoot + utils.NormalizePath(input.Readme), 213 Tag: input.Tag, 214 CodeGenVersion: codeGenVersion, 215 RepositoryURL: "https://github.com/Azure/azure-rest-api-specs.git", 216 AutorestCommand: fmt.Sprintf("autorest --use=%s --tag=%s --go-sdk-folder=/_/azure-sdk-for-go %s /_/azure-rest-api-specs/%s", 217 codeGenVersion, result.Tag, strings.Join(options, " "), utils.NormalizePath(input.Readme)), 218 AdditionalProperties: GenerationMetadataAdditionalProperties{ 219 AdditionalOptions: strings.Join(options, " "), 220 }, 221 } 222} 223 224func (input GenerateInput) validate() error { 225 if input.Readme == "" { 226 return fmt.Errorf("`Readme` cannot be empty in input") 227 } 228 if filepath.IsAbs(input.Readme) { 229 return fmt.Errorf("`Readme` must be a relative path") 230 } 231 if input.Tag == "" { 232 return fmt.Errorf("`Tag` cannot be empty in input") 233 } 234 if input.Options == nil { 235 return fmt.Errorf("`Options` cannot be nil") 236 } 237 return nil 238} 239 240func (options *GenerateOptions) validate() error { 241 if options.MetadataOutputRoot == "" { 242 options.MetadataOutputRoot = filepath.Join(os.TempDir(), fmt.Sprintf("generation-metadata-%v", time.Now().Unix())) 243 } 244 if options.ChangelogTitle == "" { 245 options.ChangelogTitle = "Change History" 246 } 247 return nil 248} 249 250// WriteChangelogFile writes the changelog to the disk 251func WriteChangelogFile(title string, result ChangelogResult) (string, error) { 252 fileContent := fmt.Sprintf(`# %s 253 254%s`, title, result.Changelog.ToMarkdown()) 255 path := filepath.Join(result.PackageFullPath, ChangelogFilename) 256 changelogFile, err := os.Create(path) 257 if err != nil { 258 return "", err 259 } 260 defer changelogFile.Close() 261 if _, err := changelogFile.WriteString(fileContent); err != nil { 262 return "", err 263 } 264 return path, nil 265} 266 267// WriteMetadataFile writes the metadata to the disk 268func WriteMetadataFile(packagePath string, metadata GenerationMetadata) (string, error) { 269 metadataFilepath := filepath.Join(packagePath, MetadataFilename) 270 metadataFile, err := os.Create(metadataFilepath) 271 if err != nil { 272 return "", err 273 } 274 defer metadataFile.Close() 275 276 // marshal metadata 277 b, err := json.MarshalIndent(metadata, "", " ") 278 if err != nil { 279 return "", fmt.Errorf("cannot marshal metadata: %+v", err) 280 } 281 282 if _, err := metadataFile.Write(b); err != nil { 283 return "", err 284 } 285 return metadataFilepath, nil 286} 287 288// scannerPrint prints the scanner to writer with a specified prefix 289func scannerPrint(scanner *bufio.Scanner, writer io.Writer, prefix string) error { 290 if writer == nil { 291 return nil 292 } 293 for scanner.Scan() { 294 line := scanner.Text() 295 if _, err := fmt.Fprintln(writer, fmt.Sprintf("%s%s", prefix, line)); err != nil { 296 return err 297 } 298 } 299 return nil 300} 301