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