1// +build go1.9
2
3// Copyright (c) Microsoft Corporation. All rights reserved.
4// Licensed under the MIT License. See License.txt in the project root for license information.
5
6// Package model holds the business logic for the operations made available by
7// profileBuilder.
8//
9// This package is not governed by the SemVer associated with the rest of the
10// Azure-SDK-for-Go.
11package model
12
13import (
14	"bytes"
15	"fmt"
16	"go/ast"
17	"go/parser"
18	"go/printer"
19	"go/token"
20	"log"
21	"os"
22	"os/exec"
23	"path"
24	"path/filepath"
25	"strings"
26	"sync"
27
28	"github.com/Azure/azure-sdk-for-go/tools/internal/modinfo"
29
30	"golang.org/x/tools/imports"
31)
32
33// ListDefinition represents a JSON file that contains a list of packages to include
34type ListDefinition struct {
35	Include      []string          `json:"include"`
36	PathOverride map[string]string `json:"pathOverride"`
37	IgnoredPaths []string          `json:"ignoredPaths"`
38}
39
40const (
41	armPathModifier = "mgmt"
42	aliasFileName   = "models.go"
43)
44
45// BuildProfile takes a list of packages and creates a profile
46func BuildProfile(packageList ListDefinition, name, outputLocation string, outputLog, errLog *log.Logger, recursive, modules bool, semLimit int) {
47	sem := make(chan struct{}, semLimit)
48	wg := &sync.WaitGroup{}
49	wg.Add(len(packageList.Include))
50	for _, pkgDir := range packageList.Include {
51		if !filepath.IsAbs(pkgDir) {
52			abs, err := filepath.Abs(pkgDir)
53			if err != nil {
54				errLog.Fatalf("failed to convert to absolute path: %v", err)
55			}
56			pkgDir = abs
57		}
58		go func(pd string) {
59			filepath.Walk(pd, func(path string, info os.FileInfo, err error) error {
60				if !info.IsDir() {
61					return nil
62				}
63				fs := token.NewFileSet()
64				sem <- struct{}{}
65				packages, err := parser.ParseDir(fs, path, func(f os.FileInfo) bool {
66					// exclude test files
67					return !strings.HasSuffix(f.Name(), "_test.go")
68				}, 0)
69				<-sem
70				if err != nil {
71					errLog.Fatalf("failed to parse '%s': %v", path, err)
72				}
73				if len(packages) < 1 {
74					errLog.Fatalf("didn't find any packages in '%s'", path)
75				}
76				if len(packages) > 1 {
77					errLog.Fatalf("found more than one package in '%s'", path)
78				}
79				for pn := range packages {
80					p := packages[pn]
81					// trim any non-exported nodes
82					if exp := ast.PackageExports(p); !exp {
83						errLog.Fatalf("package '%s' doesn't contain any exports", pn)
84					}
85					// construct the import path from the outputLocation
86					// e.g. D:\work\src\github.com\Azure\azure-sdk-for-go\profiles\2017-03-09\compute\mgmt\compute
87					// becomes github.com/Azure/azure-sdk-for-go/profiles/2017-03-09/compute/mgmt/compute
88					i := strings.Index(path, "github.com")
89					if i == -1 {
90						errLog.Fatalf("didn't find 'github.com' in '%s'", path)
91					}
92					importPath := strings.Replace(path[i:], "\\", "/", -1)
93					ap, err := NewAliasPackage(p, importPath)
94					if err != nil {
95						errLog.Fatalf("failed to create alias package: %v", err)
96					}
97					updateAliasPackageUserAgent(ap, name)
98					// build the profile output directory, if there's an override path use that
99					var aliasPath string
100					var ok bool
101					if aliasPath, ok = packageList.PathOverride[importPath]; !ok {
102						var err error
103						if modules && modinfo.HasVersionSuffix(path) {
104							// strip off the major version dir so it's not included in the alias path
105							path = filepath.Dir(path)
106						}
107						aliasPath, err = getAliasPath(path)
108						if err != nil {
109							errLog.Fatalf("failed to calculate alias directory: %v", err)
110						}
111					}
112					aliasPath = filepath.Join(outputLocation, aliasPath)
113					if _, err := os.Stat(aliasPath); os.IsNotExist(err) {
114						err = os.MkdirAll(aliasPath, os.ModeDir|0755)
115						if err != nil {
116							errLog.Fatalf("failed to create alias directory: %v", err)
117						}
118					}
119					writeAliasPackage(ap, aliasPath, outputLog, errLog)
120				}
121				if !recursive {
122					return filepath.SkipDir
123				}
124				return nil
125			})
126			wg.Done()
127		}(pkgDir)
128	}
129	wg.Wait()
130	close(sem)
131	outputLog.Print(len(packageList.Include), " packages generated.")
132}
133
134// getAliasPath takes an existing API Version path and converts the path to a path which uses the new profile layout.
135func getAliasPath(packageDir string) (string, error) {
136	// we want to transform this:
137	//  .../services/compute/mgmt/2016-03-30/compute
138	// into this:
139	//  compute/mgmt/compute
140	// i.e. remove everything to the left of /services along with the API version
141	pi, err := DeconstructPath(packageDir)
142	if err != nil {
143		return "", err
144	}
145
146	output := []string{
147		pi.Provider,
148	}
149
150	if pi.IsArm {
151		output = append(output, armPathModifier)
152	}
153	output = append(output, pi.Group)
154	if pi.APIPkg != "" {
155		output = append(output, pi.APIPkg)
156	}
157
158	return filepath.Join(output...), nil
159}
160
161// updateAliasPackageUserAgent updates the "UserAgent" function in the generated profile, if it is present.
162func updateAliasPackageUserAgent(ap *AliasPackage, profileName string) {
163	var userAgent *ast.FuncDecl
164	for _, decl := range ap.Files[aliasFileName].Decls {
165		if fd, ok := decl.(*ast.FuncDecl); ok && fd.Name.Name == "UserAgent" {
166			userAgent = fd
167			break
168		}
169	}
170	if userAgent == nil {
171		return
172	}
173
174	// Grab the expression being returned.
175	retResults := &userAgent.Body.List[0].(*ast.ReturnStmt).Results[0]
176
177	// Append a string literal to the result
178	updated := &ast.BinaryExpr{
179		Op: token.ADD,
180		X:  *retResults,
181		Y: &ast.BasicLit{
182			Value: fmt.Sprintf(`" profiles/%s"`, profileName),
183		},
184	}
185	*retResults = updated
186}
187
188// writeAliasPackage adds the MSFT Copyright Header, then writes the alias package to disk.
189func writeAliasPackage(ap *AliasPackage, outputPath string, outputLog, errLog *log.Logger) {
190	files := token.NewFileSet()
191
192	err := os.MkdirAll(path.Dir(outputPath), 0755|os.ModeDir)
193	if err != nil {
194		errLog.Fatalf("error creating directory: %v", err)
195	}
196
197	aliasFile := filepath.Join(outputPath, aliasFileName)
198	outputFile, err := os.Create(aliasFile)
199	if err != nil {
200		errLog.Fatalf("error creating file: %v", err)
201	}
202
203	// TODO: This should really be added by the `goalias` package itself. Doing it here is a work around
204	fmt.Fprintln(outputFile, "// +build go1.9")
205	fmt.Fprintln(outputFile)
206
207	generatorStampBuilder := new(bytes.Buffer)
208
209	fmt.Fprintln(generatorStampBuilder, `// Copyright (c) Microsoft Corporation. All rights reserved.
210// Licensed under the MIT License. See License.txt in the project root for license information.`)
211
212	fmt.Fprintln(outputFile, generatorStampBuilder.String())
213
214	generatorStampBuilder.Reset()
215
216	fmt.Fprintln(generatorStampBuilder, "// This code was auto-generated by:")
217	fmt.Fprintln(generatorStampBuilder, "// github.com/Azure/azure-sdk-for-go/tools/profileBuilder")
218
219	fmt.Fprintln(generatorStampBuilder)
220	fmt.Fprint(outputFile, generatorStampBuilder.String())
221
222	outputLog.Printf("Writing File: %s", aliasFile)
223
224	file := ap.ModelFile()
225
226	var b bytes.Buffer
227	printer.Fprint(&b, files, file)
228	res, err := imports.Process(aliasFile, b.Bytes(), nil)
229	if err != nil {
230		errLog.Fatalf("failed to process imports: %v", err)
231	}
232	fmt.Fprintf(outputFile, "%s", res)
233	outputFile.Close()
234
235	// be sure to specify the file for formatting not the directory; this is to
236	// avoid race conditions when formatting parent/child directories (foo and foo/fooapi)
237	if err := exec.Command("gofmt", "-w", aliasFile).Run(); err != nil {
238		errLog.Fatalf("error formatting profile '%s': %v", aliasFile, err)
239	}
240}
241