github.com/joshdk/godel@v0.0.0-20170529232908-862138a45aee/apps/distgo/cmd/build/specbuilder.go (about)

     1  // Copyright 2016 Palantir Technologies, Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package build
    16  
    17  import (
    18  	"fmt"
    19  	"io"
    20  	"os"
    21  	"path"
    22  	"sort"
    23  
    24  	"github.com/palantir/pkg/matcher"
    25  	"github.com/palantir/pkg/pkgpath"
    26  	"github.com/pkg/errors"
    27  
    28  	"github.com/palantir/godel/apps/distgo/cmd"
    29  	"github.com/palantir/godel/apps/distgo/params"
    30  	"github.com/palantir/godel/apps/distgo/pkg/git"
    31  	"github.com/palantir/godel/apps/distgo/pkg/imports"
    32  	"github.com/palantir/godel/apps/distgo/pkg/osarch"
    33  )
    34  
    35  // RequiresBuild returns a slice that contains the ProductBuildSpecs that have not been built for the provided
    36  // ProductBuildSpecWithDeps matching the provided osArchs filter. A product is considered to require building if its
    37  // output executable does not exist or if the output executable's modification date is older than any of the Go files
    38  // required to build the product.
    39  func RequiresBuild(specWithDeps params.ProductBuildSpecWithDeps, osArchs cmd.OSArchFilter) RequiresBuildInfo {
    40  	info := newRequiresBuildInfo(specWithDeps, osArchs)
    41  	for _, currSpec := range specWithDeps.AllSpecs() {
    42  		if currSpec.Build.Skip {
    43  			continue
    44  		}
    45  		paths := ArtifactPaths(currSpec)
    46  		for _, currOSArch := range currSpec.Build.OSArchs {
    47  			if osArchs.Matches(currOSArch) {
    48  				if fi, err := os.Stat(paths[currOSArch]); err == nil {
    49  					if goFiles, err := imports.AllFiles(path.Join(currSpec.ProjectDir, currSpec.Build.MainPkg)); err == nil {
    50  						if newerThan, err := goFiles.NewerThan(fi); err == nil && !newerThan {
    51  							// if the build artifact for the product already exists and none of the source files for the
    52  							// product are newer than the build artifact, consider spec up-to-date
    53  							continue
    54  						}
    55  					}
    56  				}
    57  				// spec/osArch combination requires build
    58  				info.addInfo(currSpec, currOSArch)
    59  			}
    60  		}
    61  	}
    62  	return info
    63  }
    64  
    65  type RequiresBuildInfo interface {
    66  	Specs() []params.ProductBuildSpec
    67  	RequiresBuild(product string, osArch osarch.OSArch) bool
    68  	addInfo(spec params.ProductBuildSpec, osArch osarch.OSArch)
    69  }
    70  
    71  type requiresBuildInfo struct {
    72  	// ordered slice of product names
    73  	orderedProducts []string
    74  	// map from product name to build spec for the product
    75  	products map[string]params.ProductBuildSpec
    76  	// map from product name to OS/Archs for which product requires build
    77  	productsRequiresBuildOSArch map[string][]osarch.OSArch
    78  	// the products that were examined in creating this requiresBuildInfo
    79  	examinedProducts map[string]struct{}
    80  	// the OSArchFilter used when creating this requiresBuildInfo
    81  	examinedOSArchs cmd.OSArchFilter
    82  }
    83  
    84  func newRequiresBuildInfo(specWithDeps params.ProductBuildSpecWithDeps, osArchs cmd.OSArchFilter) RequiresBuildInfo {
    85  	examinedProducts := make(map[string]struct{})
    86  	for _, spec := range specWithDeps.AllSpecs() {
    87  		examinedProducts[spec.ProductName] = struct{}{}
    88  	}
    89  
    90  	return &requiresBuildInfo{
    91  		products:                    make(map[string]params.ProductBuildSpec),
    92  		productsRequiresBuildOSArch: make(map[string][]osarch.OSArch),
    93  		examinedProducts:            examinedProducts,
    94  		examinedOSArchs:             osArchs,
    95  	}
    96  }
    97  
    98  func (b *requiresBuildInfo) addInfo(spec params.ProductBuildSpec, osArch osarch.OSArch) {
    99  	k := spec.ProductName
   100  	_, productSeen := b.products[k]
   101  	if !productSeen {
   102  		b.orderedProducts = append(b.orderedProducts, k)
   103  	}
   104  	b.products[k] = spec
   105  	b.productsRequiresBuildOSArch[k] = append(b.productsRequiresBuildOSArch[k], osArch)
   106  }
   107  
   108  func (b *requiresBuildInfo) RequiresBuild(product string, osArch osarch.OSArch) bool {
   109  	// if required product/OSArch was not considered, return true (assume it needs to be built)
   110  	if _, ok := b.examinedProducts[product]; !ok || !b.examinedOSArchs.Matches(osArch) {
   111  		return true
   112  	}
   113  	for _, v := range b.productsRequiresBuildOSArch[product] {
   114  		if v == osArch {
   115  			return true
   116  		}
   117  	}
   118  	return false
   119  }
   120  
   121  func (b *requiresBuildInfo) Specs() []params.ProductBuildSpec {
   122  	specs := make([]params.ProductBuildSpec, len(b.orderedProducts))
   123  	for i, k := range b.orderedProducts {
   124  		specs[i] = b.products[k]
   125  	}
   126  	return specs
   127  }
   128  
   129  func RunBuildFunc(buildActionFunc cmd.BuildFunc, cfg params.Project, products []string, wd string, stdout io.Writer) error {
   130  	buildSpecsWithDeps, err := SpecsWithDepsForArgs(cfg, products, wd)
   131  	if err != nil {
   132  		return err
   133  	}
   134  	if err := buildActionFunc(buildSpecsWithDeps, stdout); err != nil {
   135  		return err
   136  	}
   137  	return nil
   138  }
   139  
   140  func SpecsWithDepsForArgs(cfg params.Project, products []string, wd string) ([]params.ProductBuildSpecWithDeps, error) {
   141  	// if configuration is empty, default to all main pkgs
   142  	if len(cfg.Products) == 0 {
   143  		cfg.Products = make(map[string]params.Product)
   144  		if err := addMainPkgsToConfig(cfg, wd); err != nil {
   145  			return nil, errors.Wrapf(err, "failed to get main packages from %v", wd)
   146  		}
   147  	}
   148  
   149  	// determine version for git directory
   150  	productInfo, err := git.NewProjectInfo(wd)
   151  	if err != nil {
   152  		// if version could not be determined, use "unspecified"
   153  		productInfo.Version = "unspecified"
   154  	}
   155  
   156  	// create BuildSpec for all products
   157  	allBuildSpecs := make(map[string]params.ProductBuildSpec)
   158  	for currProduct, currProductCfg := range cfg.Products {
   159  		allBuildSpecs[currProduct] = params.NewProductBuildSpec(wd, currProduct, productInfo, currProductCfg, cfg)
   160  	}
   161  
   162  	// get products that aren't excluded by configuration
   163  	filteredProducts := cfg.FilteredProducts()
   164  
   165  	if len(filteredProducts) == 0 {
   166  		return nil, fmt.Errorf("No products found.")
   167  	}
   168  
   169  	// if arguments are provided, filter to only build products named in arguments
   170  	if len(products) != 0 {
   171  		var unknownProducts []string
   172  		// create map of provided products
   173  		argProducts := make(map[string]bool, len(products))
   174  		for _, currArg := range products {
   175  			argProducts[currArg] = true
   176  			if _, ok := filteredProducts[currArg]; !ok {
   177  				unknownProducts = append(unknownProducts, currArg)
   178  			}
   179  		}
   180  
   181  		// throw error if any of the specified products were unknown
   182  		if len(unknownProducts) > 0 {
   183  			sort.Strings(unknownProducts)
   184  			sortedKnownProducts := make([]string, 0, len(filteredProducts))
   185  			for currProduct := range filteredProducts {
   186  				sortedKnownProducts = append(sortedKnownProducts, currProduct)
   187  			}
   188  			sort.Strings(sortedKnownProducts)
   189  			return nil, fmt.Errorf("Invalid products: %v. Valid products are %v.", unknownProducts, sortedKnownProducts)
   190  		}
   191  
   192  		// iterate over filteredProducts map and remove any keys not present in provided arguments
   193  		for k := range filteredProducts {
   194  			if _, ok := argProducts[k]; !ok {
   195  				delete(filteredProducts, k)
   196  			}
   197  		}
   198  	}
   199  
   200  	sortedFilteredProducts := make([]string, 0, len(filteredProducts))
   201  	for currProduct := range filteredProducts {
   202  		sortedFilteredProducts = append(sortedFilteredProducts, currProduct)
   203  	}
   204  	sort.Strings(sortedFilteredProducts)
   205  
   206  	var buildSpecsWithDeps []params.ProductBuildSpecWithDeps
   207  	for _, currProduct := range sortedFilteredProducts {
   208  		currSpec, err := params.NewProductBuildSpecWithDeps(allBuildSpecs[currProduct], allBuildSpecs)
   209  		if err != nil {
   210  			return nil, errors.Wrapf(err, "failed to create build spec for %v", currProduct)
   211  		}
   212  
   213  		buildSpecsWithDeps = append(buildSpecsWithDeps, currSpec)
   214  	}
   215  	return buildSpecsWithDeps, nil
   216  }
   217  
   218  func addMainPkgsToConfig(cfg params.Project, projectDir string) error {
   219  	mainPkgPaths, err := mainPkgPaths(projectDir)
   220  	if err != nil {
   221  		return errors.Wrapf(err, "failed to determine paths to main packages in %v", projectDir)
   222  	}
   223  
   224  	for _, currMainPkgPath := range mainPkgPaths {
   225  		currMainPkgAbsPath := path.Join(projectDir, currMainPkgPath)
   226  		productName := path.Base(currMainPkgAbsPath)
   227  		cfg.Products[productName] = params.Product{
   228  			Build: params.Build{
   229  				MainPkg: currMainPkgPath,
   230  			},
   231  		}
   232  	}
   233  	return nil
   234  }
   235  
   236  func mainPkgPaths(projectDir string) ([]string, error) {
   237  	// TODO: this should use Exclude specified in config to determine directories to examine
   238  	pkgs, err := pkgpath.PackagesInDir(projectDir, matcher.Name("vendor"))
   239  	if err != nil {
   240  		return nil, errors.Wrapf(err, "failed to list packages in project %v", projectDir)
   241  	}
   242  
   243  	pkgsMap, err := pkgs.Packages(pkgpath.Relative)
   244  	if err != nil {
   245  		return nil, errors.Wrapf(err, "failed to get paths for packges")
   246  	}
   247  
   248  	var mainPkgPaths []string
   249  	for currPath, currPkg := range pkgsMap {
   250  		if currPkg == "main" {
   251  			mainPkgPaths = append(mainPkgPaths, currPath)
   252  		}
   253  	}
   254  	return mainPkgPaths, nil
   255  }