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  }