2017-12-03 20:01:25 +00:00
// +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"
2018-01-23 18:40:42 +00:00
"os/exec"
2017-12-03 20:01:25 +00:00
"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." )
2018-01-23 18:40:42 +00:00
if err := exec . Command ( "gofmt" , "-w" , outputLocation ) . Run ( ) ; err == nil {
outputLog . Print ( "Success formatting profile." )
} else {
errLog . Print ( "Trouble formatting profile: " , err )
}
2017-12-03 20:01:25 +00:00
}
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 ... )
}
} ( )