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 }