// +build go1.9 // Copyright 2017 Microsoft Corporation and contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // profileBuilder creates a series of packages filled entirely with alias types // and functions supporting those alias types by directing traffic to the // functions supporting the original types. This is useful associating a series // of packages in separate API Versions for easier/safer use. // // The Azure-SDK-for-Go teams intends to use this tool to generated profiles // that we will publish in this repository for general use. However, this tool // in the case that one has their own list of Services at given API Versions, // this may prove to be a useful tool for you. package main import ( "bytes" "errors" "flag" "fmt" "go/ast" "go/parser" "go/printer" "go/token" "io" "io/ioutil" "log" "os" "os/exec" "path" "path/filepath" "strings" "time" "github.com/marstr/collection" goalias "github.com/marstr/goalias/model" "github.com/marstr/randname" ) var ( profileName string outputLocation string inputRoot string inputList io.Reader packageStrategy collection.Enumerable outputLog *log.Logger errLog *log.Logger ) // WellKnownStrategy is an Enumerable which lists all known strategies for choosing packages for a profile. type WellKnownStrategy string // This block declares the definitive list of WellKnownStrategies const ( WellKnownStrategyList WellKnownStrategy = "list" WellKnownStrategyLatest WellKnownStrategy = "latest" WellKnownStrategyPreview WellKnownStrategy = "preview" ) const armPathModifier = "mgmt" // If not the empty string, this string should be stamped into files generated by the profileBuilder. // Note: This variable should be set by passing the argument "-X main.version=`{your value}`" to the Go linker. example: `go build -ldflags "-X main.version=f43d726b6e3f1e3eb7cbdba3982f0253000d5dc5"` var version string func main() { var packages collection.Enumerator type alias struct { *goalias.AliasPackage TargetPath string } // Find the names of all of the packages for inclusion in this profile. packages = packageStrategy.Enumerate(nil).Select(func(x interface{}) interface{} { if cast, ok := x.(string); ok { return cast } return nil }) // Parse the packages that were selected for inclusion in this profile. packages = packages.SelectMany(func(x interface{}) collection.Enumerator { results := make(chan interface{}) go func() { defer close(results) cast, ok := x.(string) if !ok { return } files := token.NewFileSet() parsed, err := parser.ParseDir(files, cast, nil, 0) if err != nil { errLog.Printf("Couldn't open %q because: %v", cast, err) return } for _, entry := range parsed { results <- entry } }() return results }) // Generate the alias package from the originally parsed one. packages = packages.ParallelSelect(func(x interface{}) interface{} { var err error var subject *goalias.AliasPackage cast, ok := x.(*ast.Package) if !ok { return nil } var bundle alias for filename := range cast.Files { bundle.TargetPath = filepath.Dir(filename) bundle.TargetPath = trimGoPath(bundle.TargetPath) subject, err = goalias.NewAliasPackage(cast, bundle.TargetPath) if err != nil { errLog.Print(err) return nil } bundle.TargetPath, err = getAliasPath(bundle.TargetPath, profileName) if err != nil { errLog.Print(err) return nil } break } bundle.AliasPackage = subject return &bundle }) packages = packages.Where(func(x interface{}) bool { return x != nil }) // Update the "UserAgent" function in the generated profile, if it is present. packages = packages.Select(func(x interface{}) interface{} { cast := x.(*alias) var userAgent *ast.FuncDecl // Grab all functions in the alias package named "UserAgent" userAgentCandidates := collection.Where(collection.AsEnumerable(cast.Files["models.go"].Decls), func(x interface{}) bool { cast, ok := x.(*ast.FuncDecl) return ok && cast.Name.Name == "UserAgent" }) // There should really only be one of them, otherwise bailout because we don't understand the world anymore. candidate, err := collection.Single(userAgentCandidates) if err != nil { return x } userAgent, ok := candidate.(*ast.FuncDecl) if !ok { return x } // Grab the expression being returned. retResults := &userAgent.Body.List[0].(*ast.ReturnStmt).Results[0] // Append a string literal to the result updated := &ast.BinaryExpr{ Op: token.ADD, X: *retResults, Y: &ast.BasicLit{ Value: fmt.Sprintf("\" profiles/%s\"", profileName), }, } *retResults = updated return x }) // Add the MSFT Copyright Header, then write the alias package to disk. products := packages.ParallelSelect(func(x interface{}) interface{} { cast, ok := x.(*alias) if !ok { return false } files := token.NewFileSet() outputPath := filepath.Join(outputLocation, cast.TargetPath, "models.go") outputPath = strings.Replace(outputPath, `\`, `/`, -1) err := os.MkdirAll(path.Dir(outputPath), os.ModePerm|os.ModeDir) if err != nil { errLog.Print("error creating directory:", err) return false } outputFile, err := os.Create(outputPath) if err != nil { errLog.Print("error creating file: ", err) return false } // TODO: This should really be added by the `goalias` package itself. Doing it here is a work around fmt.Fprintln(outputFile, "// +build go1.9") fmt.Fprintln(outputFile) generatorStampBuilder := new(bytes.Buffer) fmt.Fprintf(generatorStampBuilder, "// Copyright %4d Microsoft Corporation\n", time.Now().Year()) fmt.Fprintln(generatorStampBuilder, `// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License.`) fmt.Fprintln(outputFile, generatorStampBuilder.String()) generatorStampBuilder.Reset() fmt.Fprintln(generatorStampBuilder, "// This code was auto-generated by:") fmt.Fprintln(generatorStampBuilder, "// github.com/Azure/azure-sdk-for-go/tools/profileBuilder") if version != "" { fmt.Fprintln(generatorStampBuilder, "// commit ID:", version) } fmt.Fprintln(generatorStampBuilder) fmt.Fprint(outputFile, generatorStampBuilder.String()) outputLog.Printf("Writing File: %s", outputPath) printer.Fprint(outputFile, files, cast.ModelFile()) return true }) generated := 0 // Write each aliased package that was found for entry := range products { if entry.(bool) { generated++ } } outputLog.Print(generated, " packages generated.") if err := exec.Command("gofmt", "-w", outputLocation).Run(); err == nil { outputLog.Print("Success formatting profile.") } else { errLog.Print("Trouble formatting profile: ", err) } } func init() { const defaultName = "{randomly generated}" var selectedStrategy string var inputListLocation string var useVerbose bool flag.StringVar(&profileName, "name", defaultName, "The name that should be given to the generated profile.") flag.StringVar(&outputLocation, "o", defaultOutputLocation(), "The output location for the package generated as a profile.") flag.StringVar(&inputRoot, "root", defaultInputRoot(), "The location of the Azure SDK for Go's service packages.") flag.StringVar(&inputListLocation, "l", "", "If the `list` strategy is chosen, -l is the location of the file to read for said list. If not present, stdin is used.") flag.StringVar(&selectedStrategy, "s", string(WellKnownStrategyLatest), "The strategy to employ for finding packages to put in a profile.") flag.BoolVar(&useVerbose, "v", false, "Write status to stderr as the program progresses") flag.Parse() // Setup Verbose Status Log and Error Log var logWriter io.Writer if useVerbose { logWriter = os.Stderr } else { logWriter = ioutil.Discard } outputLog = log.New(logWriter, "[STATUS] ", 0) outputLog.Print("Status Logging Enabled") errLog = log.New(logWriter, "[ERROR] ", 0) if version != "" { outputLog.Print("profileBuilder Version: ", version) } // Sort out the Profile Name to be used. if profileName == defaultName { profileName = randname.AdjNoun{}.Generate() outputLog.Print("Profile Name Set to: ", profileName) } inputList = os.Stdin if inputListLocation == "" { outputLog.Print("Reading input from standard input") } else { var err error outputLog.Print("Reading input from: ", inputListLocation) inputList, err = os.Open(inputListLocation) if err != nil { errLog.Print(err) os.Exit(1) } } wellKnownStrategies := map[WellKnownStrategy]collection.Enumerable{ WellKnownStrategyList: ListStrategy{Reader: inputList}, WellKnownStrategyLatest: LatestStrategy{Root: inputRoot, Predicate: IgnorePreview, VerboseOutput: outputLog}, WellKnownStrategyPreview: LatestStrategy{Root: inputRoot, Predicate: AcceptAll}, } if s, ok := wellKnownStrategies[WellKnownStrategy(selectedStrategy)]; ok { packageStrategy = s outputLog.Printf("Using Well Known Strategy: %s", selectedStrategy) } else { errLog.Printf("Unknown strategy for identifying packages: %s\n", selectedStrategy) os.Exit(1) } } // AzureSDKforGoLocation returns the default location for the Azure-SDK-for-Go to reside. func AzureSDKforGoLocation() string { return path.Join( os.Getenv("GOPATH"), "src", "github.com", "Azure", "azure-sdk-for-go", ) } func defaultOutputLocation() string { return path.Join(AzureSDKforGoLocation(), "profiles") } func defaultInputRoot() string { return path.Join(AzureSDKforGoLocation(), "services") } // getAliasPath takes an existing API Version path and a package name, and converts the path // to a path which uses the new profile layout. func getAliasPath(subject, profile string) (transformed string, err error) { subject = strings.TrimSuffix(subject, "/") subject = trimGoPath(subject) matches := packageName.FindAllStringSubmatch(subject, -1) if matches == nil { err = errors.New("path does not resemble a known package path") return } output := []string{ profile, matches[0][1], } if matches[0][2] == armPathModifier { output = append(output, armPathModifier) } output = append(output, matches[0][4]) transformed = strings.Join(output, "/") return } // trimGoPath removes the prefix defined in the environment variabe GOPATH if it is present in the string provided. var trimGoPath = func() func(string) string { splitGo := strings.Split(os.Getenv("GOPATH"), string(os.PathSeparator)) splitGo = append(splitGo, "src") return func(subject string) string { splitPath := strings.Split(subject, string(os.PathSeparator)) for i, dir := range splitGo { if splitPath[i] != dir { return subject } } packageIdentifier := splitPath[len(splitGo):] return path.Join(packageIdentifier...) } }()