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