| // Copyright 2019 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| // Package builder implement local build process. |
| package builder |
| |
| import ( |
| "context" |
| "fmt" |
| "io/ioutil" |
| "os" |
| "path/filepath" |
| "strings" |
| |
| "go.chromium.org/luci/common/data/stringset" |
| "go.chromium.org/luci/common/errors" |
| "go.chromium.org/luci/common/logging" |
| |
| "infra/cmd/cloudbuildhelper/fileset" |
| "infra/cmd/cloudbuildhelper/gitignore" |
| "infra/cmd/cloudbuildhelper/manifest" |
| ) |
| |
| // Builder executes build steps specified via "build" field in the manifest. |
| type Builder struct { |
| tmpDir string |
| } |
| |
| // New initializes a builder, allocating a temp directory for it. |
| func New() (*Builder, error) { |
| tmpDir, err := ioutil.TempDir("", "cloudbuildhelper") |
| if err != nil { |
| return nil, errors.Annotate(err, "failed to allocate a temporary directory").Err() |
| } |
| return &Builder{tmpDir: tmpDir}, nil |
| } |
| |
| // Close removes all temporary files held by this Builder. |
| // |
| // Closing the builder invalidates all outputs it ever produced, thus this |
| // should be done only after outputs are processed. |
| // |
| // Idempotent. |
| func (b *Builder) Close() error { |
| if b.tmpDir != "" { |
| if err := os.RemoveAll(b.tmpDir); err != nil { |
| return errors.Annotate(err, "failed to remove builder temp dir").Err() |
| } |
| b.tmpDir = "" |
| } |
| return nil |
| } |
| |
| // Build executes all local builds steps specified in the manifest. |
| // |
| // The result of this process is a fileset.Set with m.ContextDir and outputs of |
| // all build steps. Note that Builder is oblivious of Dockerfile or any other |
| // docker specifics. It just executes local steps specified via "build" field in |
| // the manifest. |
| // |
| // The returned fileset should not outlive Builder, since it may reference |
| // temporary files owned by Builder. |
| func (b *Builder) Build(ctx context.Context, m *manifest.Manifest) (*fileset.Set, error) { |
| logging.Debugf(ctx, "Starting the local build using temp dir %q", b.tmpDir) |
| |
| out := &fileset.Set{} |
| |
| if m.ContextDir != "" { |
| logging.Debugf(ctx, "Adding %q to the output set...", m.ContextDir) |
| excluder, err := gitignore.NewExcluder(m.ContextDir) |
| if err != nil { |
| return nil, errors.Annotate(err, "when loading .gitignore files").Err() |
| } |
| if err := out.AddFromDisk(m.ContextDir, ".", excluder); err != nil { |
| return nil, errors.Annotate(err, "failed to add contextdir %q to output set", m.ContextDir).Err() |
| } |
| } |
| |
| state := builderState{} |
| |
| for idx, bs := range m.Build { |
| concrete := bs.Concrete() |
| logging.Debugf(ctx, "Executing local build step #%d - %s", idx+1, concrete) |
| |
| var runner stepRunner |
| switch concrete.(type) { |
| case *manifest.CopyBuildStep: |
| runner = runCopyBuildStep |
| case *manifest.GoBuildStep: |
| runner = runGoBuildStep |
| case *manifest.RunBuildStep: |
| runner = runRunBuildStep |
| case *manifest.GoGAEBundleBuildStep: |
| runner = runGoGAEBundleBuildStep |
| default: |
| panic("impossible, did you forget to implement a step?") |
| } |
| |
| err := runner(ctx, &stepRunnerInv{ |
| State: &state, |
| Manifest: m, |
| BuildStep: bs, |
| Output: out, |
| TempDir: b.tmpDir, |
| TempSuffix: fmt.Sprintf("_%d", idx), |
| }) |
| if err != nil { |
| return nil, errors.Annotate(err, "local build step #%d (%s) failed", idx+1, concrete).Err() |
| } |
| } |
| |
| return out, nil |
| } |
| |
| // stepRunner executes on local build step 'bs', writing result to 'out'. |
| type stepRunner func(ctx context.Context, inv *stepRunnerInv) error |
| |
| // stepRunnerInv is a bundle of parameters for a stepRunner invocation. |
| type stepRunnerInv struct { |
| State *builderState // a state is carried over between steps |
| Manifest *manifest.Manifest // fully populated target manifest |
| BuildStep *manifest.BuildStep // build step we are executing |
| Output *fileset.Set // where to put output files |
| TempDir string // can be used to drop arbitrary files into |
| TempSuffix string // unique per-step suffix, to name temp files |
| } |
| |
| // builderState is carried over between steps. |
| // |
| // They can mutate it if they want. |
| type builderState struct { |
| goDepsPerVersion map[string]stringset.Set // import paths of packages copied to the _gopath already |
| } |
| |
| // goDeps is a set of imported packages copied to the _gopath already. |
| func (s *builderState) goDeps(goVer string) stringset.Set { |
| if s.goDepsPerVersion == nil { |
| s.goDepsPerVersion = make(map[string]stringset.Set, 1) |
| } |
| ss := s.goDepsPerVersion[goVer] |
| if ss == nil { |
| ss = stringset.New(0) |
| s.goDepsPerVersion[goVer] = ss |
| } |
| return ss |
| } |
| |
| // addFilesToOutput adds `src` (which is an existing file or directory on disk) |
| // to the output set as filepath.Rel(contextDir, dst), failing if the result is |
| // outside of the context dir. |
| func (inv *stepRunnerInv) addFilesToOutput(ctx context.Context, src, dst string, exclude fileset.Excluder) error { |
| rel, err := filepath.Rel(inv.Manifest.ContextDir, dst) |
| if err != nil { |
| return err |
| } |
| logging.Infof(ctx, "Copying %s => ${contextdir}/%s", filepath.Base(src), rel) |
| if strings.HasPrefix(rel, ".."+string(filepath.Separator)) { |
| return errors.Reason("the destination should be under the context directory, got %q", rel).Err() |
| } |
| return inv.Output.AddFromDisk(src, rel, exclude) |
| } |
| |
| // addBlobToOutput adds a non-executable regular file to the output set as |
| // filepath.Rel(contextDir, dst), failing if the result is outside of the |
| // context dir. |
| func (inv *stepRunnerInv) addBlobToOutput(ctx context.Context, dst string, blob []byte) error { |
| rel, err := filepath.Rel(inv.Manifest.ContextDir, dst) |
| if err != nil { |
| return err |
| } |
| logging.Infof(ctx, "Writing ${contextdir}/%s", rel) |
| if strings.HasPrefix(rel, ".."+string(filepath.Separator)) { |
| return errors.Reason("the destination should be under the context directory, got %q", rel).Err() |
| } |
| return inv.Output.AddFromMemory(rel, blob, nil) |
| } |