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