Skip to content

Commit

Permalink
Implements an intermediate build cache (#305)
Browse files Browse the repository at this point in the history
* Implements an intermediate build cache

* Uses Directory.Build.props file to set BaseIntermediateOutputPath to fix strange interaction with Nuget and SDK

* copy obj directory into layer for build caching

* simplify build caching

* add test coverage for build caching

Co-authored-by: Frankie Gallina-Jones <[email protected]>
  • Loading branch information
ForestEckhardt and Frankie Gallina-Jones committed Apr 25, 2022
1 parent 8352b84 commit 2e057bb
Show file tree
Hide file tree
Showing 8 changed files with 376 additions and 149 deletions.
21 changes: 19 additions & 2 deletions build.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ type SourceRemover interface {

//go:generate faux --interface PublishProcess --output fakes/publish_process.go
type PublishProcess interface {
Execute(workingDir, rootDir, nugetCachePath, projectPath, outputPath string, flags []string) error
Execute(workingDir, rootDir, nugetCachePath, intermediateBuildCachePath, projectPath, outputPath string, flags []string) error
}

//go:generate faux --interface BindingResolver --output fakes/binding_resolver.go
Expand Down Expand Up @@ -99,8 +99,15 @@ func Build(

nugetCache.Cache = true

intermediateBuildCache, err := context.Layers.Get("intermediate-build-cache")
if err != nil {
return packit.BuildResult{}, err
}

intermediateBuildCache.Cache = true

logger.Process("Executing build process")
err = publishProcess.Execute(context.WorkingDir, os.Getenv("DOTNET_ROOT"), nugetCache.Path, projectPath, tempDir, flags)
err = publishProcess.Execute(context.WorkingDir, os.Getenv("DOTNET_ROOT"), nugetCache.Path, intermediateBuildCache.Path, projectPath, tempDir, flags)
if err != nil {
return packit.BuildResult{}, err
}
Expand Down Expand Up @@ -135,6 +142,16 @@ func Build(
return packit.BuildResult{}, err
}

exists, err = fs.Exists(intermediateBuildCache.Path)
if exists {
if !fs.IsEmptyDir(intermediateBuildCache.Path) {
layers = append(layers, intermediateBuildCache)
}
}
if err != nil {
return packit.BuildResult{}, err
}

return packit.BuildResult{
Layers: layers,
}, nil
Expand Down
50 changes: 31 additions & 19 deletions build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"os"
"path/filepath"
"testing"
"time"

dotnetpublish "github.com/paketo-buildpacks/dotnet-publish"
"github.com/paketo-buildpacks/dotnet-publish/fakes"
Expand All @@ -23,7 +22,6 @@ func testBuild(t *testing.T, context spec.G, it spec.S) {
var (
Expect = NewWithT(t).Expect

timestamp time.Time
buffer *bytes.Buffer
workingDir string
homeDir string
Expand Down Expand Up @@ -66,17 +64,15 @@ func testBuild(t *testing.T, context spec.G, it spec.S) {
Expect(os.MkdirAll(filepath.Join(layersDir, "nuget-cache"), os.ModePerm)).To(Succeed())
Expect(os.WriteFile(filepath.Join(layersDir, "nuget-cache", "some-cache"), []byte{}, 0600)).To(Succeed())

Expect(os.MkdirAll(filepath.Join(layersDir, "intermediate-build-cache"), os.ModePerm)).To(Succeed())
Expect(os.WriteFile(filepath.Join(layersDir, "intermediate-build-cache", "some-cache"), []byte{}, 0600)).To(Succeed())

os.Setenv("DOTNET_ROOT", "some-existing-root-dir")

buffer = bytes.NewBuffer(nil)
logger := scribe.NewLogger(buffer)

timestamp = time.Now()
clock := chronos.NewClock(func() time.Time {
return timestamp
})

build = dotnetpublish.Build(sourceRemover, bindingResolver, homeDir, symlinker, publishProcess, buildpackYMLParser, commandConfigParser, clock, logger)
build = dotnetpublish.Build(sourceRemover, bindingResolver, homeDir, symlinker, publishProcess, buildpackYMLParser, commandConfigParser, chronos.DefaultClock, logger)
})

it.After(func() {
Expand All @@ -100,12 +96,17 @@ func testBuild(t *testing.T, context spec.G, it spec.S) {
})
Expect(err).NotTo(HaveOccurred())

Expect(result.Layers).To(HaveLen(1))
layer := result.Layers[0]
Expect(result.Layers).To(HaveLen(2))

Expect(layer.Name).To(Equal("nuget-cache"))
Expect(layer.Path).To(Equal(filepath.Join(layersDir, "nuget-cache")))
Expect(layer.Cache).To(BeTrue())
nugetCacheLayer := result.Layers[0]
Expect(nugetCacheLayer.Name).To(Equal("nuget-cache"))
Expect(nugetCacheLayer.Path).To(Equal(filepath.Join(layersDir, "nuget-cache")))
Expect(nugetCacheLayer.Cache).To(BeTrue())

intermediateBuildCacheLayer := result.Layers[1]
Expect(intermediateBuildCacheLayer.Name).To(Equal("intermediate-build-cache"))
Expect(intermediateBuildCacheLayer.Path).To(Equal(filepath.Join(layersDir, "intermediate-build-cache")))
Expect(intermediateBuildCacheLayer.Cache).To(BeTrue())

Expect(sourceRemover.RemoveCall.Receives.WorkingDir).To(Equal(workingDir))
Expect(sourceRemover.RemoveCall.Receives.PublishOutputDir).To(MatchRegexp(`dotnet-publish-output\d+`))
Expand All @@ -119,6 +120,9 @@ func testBuild(t *testing.T, context spec.G, it spec.S) {
Expect(publishProcess.ExecuteCall.Receives.WorkingDir).To(Equal(workingDir))
Expect(publishProcess.ExecuteCall.Receives.RootDir).To(Equal("some-existing-root-dir"))
Expect(publishProcess.ExecuteCall.Receives.ProjectPath).To(Equal("some/project/path"))
Expect(publishProcess.ExecuteCall.Receives.NugetCachePath).To(Equal(nugetCacheLayer.Path))
Expect(publishProcess.ExecuteCall.Receives.IntermediateBuildCachePath).To(Equal(intermediateBuildCacheLayer.Path))
Expect(publishProcess.ExecuteCall.Receives.ProjectPath).To(Equal("some/project/path"))
Expect(publishProcess.ExecuteCall.Receives.OutputPath).To(MatchRegexp(`dotnet-publish-output\d+`))
Expect(publishProcess.ExecuteCall.Receives.Flags).To(Equal([]string{"--publishflag", "value"}))

Expand All @@ -128,9 +132,10 @@ func testBuild(t *testing.T, context spec.G, it spec.S) {
Expect(buffer.String()).To(ContainSubstring("Please specify the project path through the $BP_DOTNET_PROJECT_PATH environment variable instead. See README.md or the documentation on paketo.io for more information."))
})

context("the cache layer is empty", func() {
context("the cache layers are empty", func() {
it.Before(func() {
Expect(os.Remove(filepath.Join(layersDir, "nuget-cache", "some-cache"))).To(Succeed())
Expect(os.Remove(filepath.Join(layersDir, "intermediate-build-cache", "some-cache"))).To(Succeed())
})

it("does not return a layer", func() {
Expand Down Expand Up @@ -171,12 +176,17 @@ func testBuild(t *testing.T, context spec.G, it spec.S) {
})
Expect(err).NotTo(HaveOccurred())

Expect(result.Layers).To(HaveLen(1))
layer := result.Layers[0]
Expect(result.Layers).To(HaveLen(2))

nugetCacheLayer := result.Layers[0]
Expect(nugetCacheLayer.Name).To(Equal("nuget-cache"))
Expect(nugetCacheLayer.Path).To(Equal(filepath.Join(layersDir, "nuget-cache")))
Expect(nugetCacheLayer.Cache).To(BeTrue())

Expect(layer.Name).To(Equal("nuget-cache"))
Expect(layer.Path).To(Equal(filepath.Join(layersDir, "nuget-cache")))
Expect(layer.Cache).To(BeTrue())
intermediateBuildCacheLayer := result.Layers[1]
Expect(intermediateBuildCacheLayer.Name).To(Equal("intermediate-build-cache"))
Expect(intermediateBuildCacheLayer.Path).To(Equal(filepath.Join(layersDir, "intermediate-build-cache")))
Expect(intermediateBuildCacheLayer.Cache).To(BeTrue())

Expect(sourceRemover.RemoveCall.Receives.WorkingDir).To(Equal(workingDir))
Expect(sourceRemover.RemoveCall.Receives.PublishOutputDir).To(MatchRegexp(`dotnet-publish-output\d+`))
Expand All @@ -185,6 +195,8 @@ func testBuild(t *testing.T, context spec.G, it spec.S) {
Expect(publishProcess.ExecuteCall.Receives.WorkingDir).To(Equal(workingDir))
Expect(publishProcess.ExecuteCall.Receives.RootDir).To(Equal("some-existing-root-dir"))
Expect(publishProcess.ExecuteCall.Receives.ProjectPath).To(Equal("some/project/path"))
Expect(publishProcess.ExecuteCall.Receives.NugetCachePath).To(Equal(nugetCacheLayer.Path))
Expect(publishProcess.ExecuteCall.Receives.IntermediateBuildCachePath).To(Equal(intermediateBuildCacheLayer.Path))
Expect(publishProcess.ExecuteCall.Receives.OutputPath).To(MatchRegexp(`dotnet-publish-output\d+`))
Expect(publishProcess.ExecuteCall.Receives.Flags).To(Equal([]string{"--publishflag", "value"}))

Expand Down
62 changes: 60 additions & 2 deletions dotnet_publish_process.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"strings"

"github.com/paketo-buildpacks/packit/v2/chronos"
"github.com/paketo-buildpacks/packit/v2/fs"
"github.com/paketo-buildpacks/packit/v2/pexec"
"github.com/paketo-buildpacks/packit/v2/scribe"
)
Expand All @@ -30,9 +31,15 @@ func NewDotnetPublishProcess(executable Executable, logger scribe.Logger, clock
}
}

func (p DotnetPublishProcess) Execute(workingDir, root, nugetCachePath, projectPath, outputPath string, flags []string) error {
func (p DotnetPublishProcess) Execute(workingDir, root, nugetCachePath, intermediateBuildCachePath, projectPath, outputPath string, flags []string) error {
err := loadBuildCache(workingDir, projectPath, intermediateBuildCachePath)
if err != nil {
return fmt.Errorf("failed to load build cache: %w", err)
}

args := []string{
"publish", filepath.Join(workingDir, projectPath), // change to workingDir plus project path
"publish",
filepath.Join(workingDir, projectPath),
}

if !containsFlag(flags, "--configuration") && !containsFlag(flags, "-c") {
Expand Down Expand Up @@ -73,5 +80,56 @@ func (p DotnetPublishProcess) Execute(workingDir, root, nugetCachePath, projectP
p.logger.Action("Completed in %s", duration)
p.logger.Break()

err = recreateBuildCache(workingDir, projectPath, intermediateBuildCachePath)
if err != nil {
return err
}

return nil
}

func loadBuildCache(workingDir, projectPath, cachePath string) error {
obj, err := fs.Exists(filepath.Join(cachePath, "obj"))
if err != nil {
return err
}

if obj {
// RemoveAll to clear the contents of the directory, which fs.Copy won't do
err = os.RemoveAll(filepath.Join(workingDir, projectPath, "obj"))
if err != nil {
return err
}
err = fs.Copy(filepath.Join(cachePath, "obj"), filepath.Join(workingDir, projectPath, "obj"))
if err != nil {
return err
}
}
return nil
}

func recreateBuildCache(workingDir, projectPath, cachePath string) error {
obj, err := fs.Exists(filepath.Join(workingDir, projectPath, "obj"))
if err != nil {
return fmt.Errorf("failed to locate build cache: %w", err)
}

if obj {
// RemoveAll to clear the contents of the directory, which fs.Copy won't do
err = os.RemoveAll(filepath.Join(cachePath, "obj"))
if err != nil {
// not tested
return fmt.Errorf("failed to reset build cache: %w", err)
}
err = os.MkdirAll(filepath.Join(cachePath, "obj"), os.ModePerm)
if err != nil {
// not tested
return fmt.Errorf("failed to reset build cache: %w", err)
}
err = fs.Copy(filepath.Join(workingDir, projectPath, "obj"), filepath.Join(cachePath, "obj"))
if err != nil {
return fmt.Errorf("failed to store build cache: %w", err)
}
}
return nil
}
Loading

0 comments on commit 2e057bb

Please sign in to comment.