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