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