github.com/jonsyu1/godel@v0.0.0-20171017211503-64567a0cf169/apps/distgo/cmd/build/build.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 "os/exec" 22 "path" 23 "reflect" 24 "regexp" 25 "runtime" 26 "strings" 27 "sync" 28 "time" 29 30 "github.com/pkg/errors" 31 32 "github.com/palantir/godel/apps/distgo/cmd" 33 "github.com/palantir/godel/apps/distgo/params" 34 "github.com/palantir/godel/apps/distgo/pkg/osarch" 35 "github.com/palantir/godel/apps/distgo/pkg/script" 36 ) 37 38 type buildUnit struct { 39 buildSpec params.ProductBuildSpec 40 osArch osarch.OSArch 41 } 42 43 type Context struct { 44 Parallel bool 45 Install bool 46 Pkgdir bool 47 } 48 49 func Products(products []string, osArchs cmd.OSArchFilter, buildCtx Context, cfg params.Project, wd string, stdout io.Writer) error { 50 return RunBuildFunc(func(buildSpec []params.ProductBuildSpecWithDeps, stdout io.Writer) error { 51 specs := make([]params.ProductBuildSpec, len(buildSpec)) 52 for i, curr := range buildSpec { 53 specs[i] = curr.Spec 54 } 55 return Run(specs, osArchs, buildCtx, stdout) 56 }, cfg, products, wd, stdout) 57 } 58 59 // Run builds all of the executables specified by buildSpecs using the mode specified in ctx. If ctx.Parallel is true, 60 // then the products will be built in parallel with N workers, where N is the number of logical processors reported by 61 // Go. When builds occur in parallel, each (Product, OSArch) pair is treated as an individual unit of work. Thus, it is 62 // possible that different products may be built in parallel. If any build process returns an error, the first error 63 // returned is propagated back (and any builds that have not started will not be started). If ctx.PkgDir is true, a 64 // custom per-OS/Arch "pkg" directory is used and the "install" command is run before build for each unit, which can 65 // speed up compilations on repeated runs by writing compiled packages to disk for reuse. 66 func Run(buildSpecs []params.ProductBuildSpec, osArchs cmd.OSArchFilter, ctx Context, stdout io.Writer) error { 67 var units []buildUnit 68 for _, currSpec := range distinct(buildSpecs) { 69 // execute pre-build script 70 distEnvVars := cmd.ScriptEnvVariables(currSpec, "") 71 if err := script.WriteAndExecute(currSpec, currSpec.Build.Script, stdout, os.Stderr, distEnvVars); err != nil { 72 return errors.Wrapf(err, "failed to execute build script for %v", currSpec.ProductName) 73 } 74 75 for _, currOSArch := range currSpec.Build.OSArchs { 76 if osArchs.Matches(currOSArch) { 77 units = append(units, buildUnit{ 78 buildSpec: currSpec, 79 osArch: currOSArch, 80 }) 81 } 82 } 83 } 84 85 if len(units) == 1 || !ctx.Parallel { 86 // process serially 87 for _, currUnit := range units { 88 if err := executeBuild(stdout, currUnit.buildSpec, ctx, currUnit.osArch); err != nil { 89 return err 90 } 91 } 92 } else { 93 done := make(chan struct{}) 94 defer close(done) 95 96 // send all jobs 97 nUnits := len(units) 98 buildUnitsJobs := make(chan buildUnit, nUnits) 99 for _, currUnit := range units { 100 buildUnitsJobs <- currUnit 101 } 102 close(buildUnitsJobs) 103 104 // create workers 105 nWorkers := runtime.NumCPU() 106 if nUnits < nWorkers { 107 nWorkers = nUnits 108 } 109 var cs []<-chan error 110 for i := 0; i < nWorkers; i++ { 111 cs = append(cs, worker(stdout, buildUnitsJobs, ctx)) 112 } 113 114 for err := range merge(done, cs...) { 115 if err != nil { 116 return err 117 } 118 } 119 } 120 121 return nil 122 } 123 124 // ArtifactPaths returns a map that contains the paths to the executables created by the provided spec. The keys in the 125 // map are the OS/architecture of the executable, and the value is the output path for the executable for that 126 // OS/architecture. 127 func ArtifactPaths(buildSpec params.ProductBuildSpec) map[osarch.OSArch]string { 128 paths := make(map[osarch.OSArch]string) 129 for _, osArch := range buildSpec.Build.OSArchs { 130 paths[osArch] = path.Join(buildSpec.ProjectDir, buildSpec.Build.OutputDir, buildSpec.VersionInfo.Version, osArch.String(), ExecutableName(buildSpec.ProductName, osArch.OS)) 131 } 132 return paths 133 } 134 135 // merge handles "fanning in" the result of multiple output channels into a single output channel. If a signal is 136 // received on the "done" channel, output processing will stop. 137 func merge(done <-chan struct{}, cs ...<-chan error) <-chan error { 138 var wg sync.WaitGroup 139 out := make(chan error) 140 141 output := func(c <-chan error) { 142 defer wg.Done() 143 for err := range c { 144 select { 145 case out <- err: 146 case <-done: 147 return 148 } 149 } 150 } 151 152 wg.Add(len(cs)) 153 for _, c := range cs { 154 go output(c) 155 } 156 157 go func() { 158 wg.Wait() 159 close(out) 160 }() 161 return out 162 } 163 164 func worker(stdout io.Writer, in <-chan buildUnit, ctx Context) <-chan error { 165 out := make(chan error) 166 go func() { 167 for unit := range in { 168 out <- executeBuild(stdout, unit.buildSpec, ctx, unit.osArch) 169 } 170 close(out) 171 }() 172 return out 173 } 174 175 func executeBuild(stdout io.Writer, buildSpec params.ProductBuildSpec, ctx Context, osArch osarch.OSArch) error { 176 name := buildSpec.ProductName 177 178 if buildSpec.Build.Skip { 179 fmt.Fprintf(stdout, "Skipping build for %s because skip configuration for product is true\n", name) 180 return nil 181 } 182 183 start := time.Now() 184 outputArtifactPath, ok := ArtifactPaths(buildSpec)[osArch] 185 if !ok { 186 return fmt.Errorf("failed to determine artifact path for %s for %s", name, osArch.String()) 187 } 188 currOutputDir := path.Dir(outputArtifactPath) 189 fmt.Fprintf(stdout, "Building %s for %s at %s\n", name, osArch.String(), path.Join(currOutputDir, name)) 190 191 if err := os.MkdirAll(currOutputDir, 0755); err != nil { 192 return errors.Wrapf(err, "failed to create directories for %s", currOutputDir) 193 } 194 if ctx.Install { 195 if err := doBuildAction(doInstall, buildSpec, "", osArch, ctx.Pkgdir); err != nil { 196 return fmt.Errorf("go install failed: %v", err) 197 } 198 } 199 if err := doBuildAction(doBuild, buildSpec, currOutputDir, osArch, ctx.Pkgdir); err != nil { 200 return errors.Wrapf(err, "go build failed") 201 } 202 203 elapsed := time.Since(start) 204 fmt.Fprintf(stdout, "Finished building %s for %s (%.3fs)\n", name, osArch.String(), elapsed.Seconds()) 205 206 return nil 207 } 208 209 type buildAction int 210 211 const ( 212 doBuild buildAction = iota 213 doInstall 214 ) 215 216 func doBuildAction(action buildAction, buildSpec params.ProductBuildSpec, outputDir string, osArch osarch.OSArch, pkgdir bool) error { 217 cmd := exec.Command("go") 218 cmd.Dir = buildSpec.ProjectDir 219 220 var env []string 221 goos := runtime.GOOS 222 if osArch.OS != "" { 223 env = append(env, "GOOS="+osArch.OS) 224 goos = osArch.OS 225 } 226 goarch := runtime.GOARCH 227 if osArch.Arch != "" { 228 env = append(env, "GOARCH="+osArch.Arch) 229 goarch = osArch.Arch 230 } 231 for k, v := range buildSpec.Build.Environment { 232 env = append(env, fmt.Sprintf("%v=%v", k, v)) 233 } 234 cmd.Env = append(os.Environ(), env...) 235 236 args := []string{cmd.Path} 237 switch action { 238 case doBuild: 239 args = append(args, "build") 240 args = append(args, "-o", path.Join(outputDir, ExecutableName(buildSpec.ProductName, goos))) 241 case doInstall: 242 args = append(args, "install") 243 default: 244 return errors.Errorf("unrecognized action: %v", action) 245 } 246 247 // get build args 248 buildArgs, err := script.GetBuildArgs(buildSpec, buildSpec.Build.BuildArgsScript) 249 if err != nil { 250 return err 251 } 252 args = append(args, buildArgs...) 253 254 if buildSpec.Build.VersionVar != "" { 255 args = append(args, "-ldflags", fmt.Sprintf("-X %v=%v", buildSpec.Build.VersionVar, buildSpec.ProductVersion)) 256 } 257 258 if pkgdir { 259 // specify custom pkgdir if isolation of packages is desired 260 args = append(args, "-pkgdir", fmt.Sprintf("%v/pkg/_%v_%v", os.Getenv("GOPATH"), goos, goarch)) 261 } 262 args = append(args, buildSpec.Build.MainPkg) 263 cmd.Args = args 264 265 if output, err := cmd.CombinedOutput(); err != nil { 266 errOutput := strings.TrimSpace(string(output)) 267 err = fmt.Errorf("build command %v run with additional environment variables %v failed with output:\n%s", cmd.Args, env, errOutput) 268 269 if action == doInstall && regexp.MustCompile(installPermissionDenied).MatchString(errOutput) { 270 // if "install" command failed due to lack of permissions, return error that contains explanation 271 return fmt.Errorf(goInstallErrorMsg(osArch, err)) 272 } 273 return err 274 } 275 return nil 276 } 277 278 const installPermissionDenied = `^go install [a-zA-Z0-9_/]+: mkdir .+: permission denied$` 279 280 func goInstallErrorMsg(osArch osarch.OSArch, err error) string { 281 goBinary := "go" 282 if output, err := exec.Command("command", "-v", "go").CombinedOutput(); err == nil { 283 goBinary = strings.TrimSpace(string(output)) 284 } 285 return strings.Join([]string{ 286 `failed to install a Go standard library package due to insufficient permissions to create directory.`, 287 `This typically means that the standard library for the OS/architecture combination have not been installed locally and the current user does not have write permissions to GOROOT/pkg.`, 288 fmt.Sprintf(`Run "sudo env GOOS=%s GOARCH=%s %s install std" to install the standard packages for this combination as root and then try again.`, osArch.OS, osArch.Arch, goBinary), 289 fmt.Sprintf(`Full error: %s`, err.Error()), 290 }, "\n") 291 } 292 293 func distinct(buildSpecs []params.ProductBuildSpec) []params.ProductBuildSpec { 294 distinctSpecs := make([]params.ProductBuildSpec, 0, len(buildSpecs)) 295 for _, spec := range buildSpecs { 296 if contains(distinctSpecs, spec) { 297 continue 298 } 299 distinctSpecs = append(distinctSpecs, spec) 300 } 301 return distinctSpecs 302 } 303 304 func contains(specs []params.ProductBuildSpec, spec params.ProductBuildSpec) bool { 305 for _, currSpec := range specs { 306 if reflect.DeepEqual(currSpec, spec) { 307 return true 308 } 309 } 310 return false 311 }