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