Skip to content

Commit

Permalink
Remove layer caching in favor of just running cargo
Browse files Browse the repository at this point in the history
- Remove buildpack's layer caching. Caching for the layer could be difficult, so removing it just leaves things up to Cargo which is the most accurate
- Creates two folders under the cargo cache layer, `target/` and `home/`. The former is where build files go while the latter is where `cargo` cached downloads, like from crates.io go.
- At the moment, a layer cached by the lifecycle does not preserve file mtimes. They are squashed in the name of reproducible builds. To get around this, the buildpack will preserve the mtimes of everything install, after cargo runs & then restore them the next time before cargo runs. This keeps consistent file mtimes, which is necessary for cargo to work properly.
- Removes unnecessary directories from cargo's home directory, based on https://doc.rust-lang.org/cargo/guide/cargo-home.html#caching-the-cargo-home-in-ci, which saves space on the cache layer.

Resolves: #24 and #25
  • Loading branch information
Daniel Mikusa authored and ForestEckhardt committed Apr 21, 2021
1 parent 89869a1 commit 31537e2
Show file tree
Hide file tree
Showing 12 changed files with 474 additions and 146 deletions.
47 changes: 14 additions & 33 deletions cargo/build.go
Original file line number Diff line number Diff line change
@@ -1,21 +1,14 @@
package cargo

import (
"path/filepath"
"github.com/dmikusa/rust-cargo-cnb/mtimes"
"time"

"github.com/paketo-buildpacks/packit"
"github.com/paketo-buildpacks/packit/chronos"
"github.com/paketo-buildpacks/packit/scribe"
)

//go:generate mockery --name Summer --case=underscore

// Summer can make checksums of the item at the given path
type Summer interface {
Sum(path ...string) (string, error)
}

//go:generate mockery --name Runner --case=underscore

// Runner is something capable of running Cargo
Expand All @@ -24,7 +17,7 @@ type Runner interface {
}

// Build does the actual install of Rust
func Build(runner Runner, summer Summer, clock chronos.Clock, logger scribe.Emitter) packit.BuildFunc {
func Build(runner Runner, clock chronos.Clock, logger scribe.Emitter) packit.BuildFunc {
return func(context packit.BuildContext) (packit.BuildResult, error) {
logger.Title("%s %s", context.BuildpackInfo.Name, context.BuildpackInfo.Version)
logger.Process("Cargo is checking if your Rust project needs to be built")
Expand All @@ -34,7 +27,6 @@ func Build(runner Runner, summer Summer, clock chronos.Clock, logger scribe.Emit
return packit.BuildResult{}, err
}

cargoLayer.Build = true
cargoLayer.Cache = true

binaryLayer, err := context.Layers.Get("rust-bin")
Expand All @@ -44,35 +36,24 @@ func Build(runner Runner, summer Summer, clock chronos.Clock, logger scribe.Emit

binaryLayer.Launch = true

cargoLockHash, err := summer.Sum(filepath.Join(context.WorkingDir, "Cargo.lock"))
then := clock.Now()
preserver := mtimes.NewPreserver(logger)
preserver.Restore(cargoLayer.Path)
err = runner.Install(context.WorkingDir, cargoLayer, binaryLayer)
if err != nil {
return packit.BuildResult{}, err
}
preserver.Preserve(cargoLayer.Path)

if sha, ok := cargoLayer.Metadata["cache_sha"].(string); !ok || sha != cargoLockHash {
logger.Subprocess("Project needs to be built")
logger.Break()
logger.Action("Completed in %s", time.Since(then).Round(time.Millisecond))
logger.Break()

then := clock.Now()
err := runner.Install(context.WorkingDir, cargoLayer, binaryLayer)
if err != nil {
return packit.BuildResult{}, err
}

logger.Action("Completed in %s", time.Since(then).Round(time.Millisecond))
logger.Break()

cargoLayer.Metadata = map[string]interface{}{
"built_at": clock.Now().Format(time.RFC3339Nano),
"cache_sha": cargoLockHash,
}
cargoLayer.Metadata = map[string]interface{}{
"built_at": clock.Now().Format(time.RFC3339Nano),
}

binaryLayer.Metadata = map[string]interface{}{
"built_at": clock.Now().Format(time.RFC3339Nano),
}
} else {
logger.Subprocess("No change, reusing")
logger.Break()
binaryLayer.Metadata = map[string]interface{}{
"built_at": clock.Now().Format(time.RFC3339Nano),
}

return packit.BuildResult{
Expand Down
72 changes: 5 additions & 67 deletions cargo/build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"io/ioutil"
"os"
"path/filepath"
"strings"
"testing"
"time"

Expand Down Expand Up @@ -59,14 +58,9 @@ func testBuild(t *testing.T, context spec.G, it spec.S) {
mock.AnythingOfType("packit.Layer"),
mock.AnythingOfType("packit.Layer")).Return(nil)

mockSummer := mocks.Summer{}
mockSummer.On("Sum", mock.MatchedBy(func(s string) bool {
return strings.HasSuffix(s, "Cargo.lock")
})).Return("12345", nil)

logger := scribe.NewEmitter(buffer)

build = cargo.Build(&mockRunner, &mockSummer, clock, logger)
build = cargo.Build(&mockRunner, clock, logger)
})

it.After(func() {
Expand All @@ -76,7 +70,7 @@ func testBuild(t *testing.T, context spec.G, it spec.S) {
})

context("build cases", func() {
it("builds fresh", func() {
it("builds", func() {
result, err := build(packit.BuildContext{
WorkingDir: workingDir,
Layers: packit.Layers{Path: layersDir},
Expand All @@ -92,65 +86,15 @@ func testBuild(t *testing.T, context spec.G, it spec.S) {
{
Name: "rust-cargo",
Path: filepath.Join(layersDir, "rust-cargo"),
Build: true,
Cache: true,
Launch: false,
SharedEnv: packit.Environment{},
BuildEnv: packit.Environment{},
LaunchEnv: packit.Environment{},
ProcessLaunchEnv: map[string]packit.Environment{},
Metadata: map[string]interface{}{
"built_at": timestamp,
"cache_sha": "12345",
},
},
{
Name: "rust-bin",
Path: filepath.Join(layersDir, "rust-bin"),
Build: false,
Launch: true,
Cache: false,
SharedEnv: packit.Environment{},
BuildEnv: packit.Environment{},
LaunchEnv: packit.Environment{},
ProcessLaunchEnv: map[string]packit.Environment{},
Metadata: map[string]interface{}{
"built_at": timestamp,
},
},
},
}))
})

it("skips build", func() {
Expect(ioutil.WriteFile(filepath.Join(layersDir, "rust-cargo.toml"), []byte("launch = false\nbuild = true\ncache = true\n\n[metadata]\ncache_sha = \"12345\"\nbuilt_at = \"some_time\""), 0644)).To(Succeed())
Expect(ioutil.WriteFile(filepath.Join(layersDir, "rust-bin.toml"), []byte("launch = true\nbuild = false\ncache = false\n\n[metadata]\nbuilt_at = \"some_time\""), 0644)).To(Succeed())

result, err := build(packit.BuildContext{
WorkingDir: workingDir,
Layers: packit.Layers{Path: layersDir},
Plan: packit.BuildpackPlan{
Entries: []packit.BuildpackPlanEntry{
{Name: "rust"},
},
},
})
Expect(err).NotTo(HaveOccurred())
Expect(result).To(Equal(packit.BuildResult{
Layers: []packit.Layer{
{
Name: "rust-cargo",
Path: filepath.Join(layersDir, "rust-cargo"),
Build: true,
Cache: true,
Launch: false,
SharedEnv: packit.Environment{},
BuildEnv: packit.Environment{},
LaunchEnv: packit.Environment{},
ProcessLaunchEnv: map[string]packit.Environment{},
Metadata: map[string]interface{}{
"built_at": "some_time",
"cache_sha": "12345",
"built_at": timestamp,
},
},
{
Expand All @@ -164,12 +108,11 @@ func testBuild(t *testing.T, context spec.G, it spec.S) {
LaunchEnv: packit.Environment{},
ProcessLaunchEnv: map[string]packit.Environment{},
Metadata: map[string]interface{}{
"built_at": "some_time",
"built_at": timestamp,
},
},
},
}))
Expect(buffer.String()).ToNot(ContainSubstring("Running Cargo Build"))
})
})

Expand Down Expand Up @@ -203,14 +146,9 @@ func testBuild(t *testing.T, context spec.G, it spec.S) {
mock.AnythingOfType("packit.Layer"),
).Return(fmt.Errorf("expected"))

mockSummer := mocks.Summer{}
mockSummer.On("Sum", mock.MatchedBy(func(s string) bool {
return strings.HasSuffix(s, "Cargo.lock")
})).Return("12345", nil)

logger := scribe.NewEmitter(buffer)

build = cargo.Build(&mockRunner, &mockSummer, clock, logger)
build = cargo.Build(&mockRunner, clock, logger)
})

it("returns an error", func() {
Expand Down
67 changes: 66 additions & 1 deletion cargo/cli_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package cargo
import (
"fmt"
"os"
"path"
"path/filepath"
"strings"

Expand Down Expand Up @@ -33,7 +34,8 @@ func NewCLIRunner(exec Executable) CLIRunner {
// Install will build and install a project using `cargo install`
func (c CLIRunner) Install(srcDir string, workLayer packit.Layer, destLayer packit.Layer) error {
env := os.Environ()
env = append(env, fmt.Sprintf("CARGO_TARGET_DIR=%s", workLayer.Path))
env = append(env, fmt.Sprintf("CARGO_TARGET_DIR=%s", path.Join(workLayer.Path, "target")))
env = append(env, fmt.Sprintf("CARGO_HOME=%s", path.Join(workLayer.Path, "home")))

for i := 0; i < len(env); i++ {
if strings.HasPrefix(env[i], "PATH=") {
Expand All @@ -56,6 +58,69 @@ func (c CLIRunner) Install(srcDir string, workLayer packit.Layer, destLayer pack
if err != nil {
return fmt.Errorf("build failed: %w", err)
}

err = c.CleanCargoHomeCache(workLayer)
if err != nil {
return fmt.Errorf("cleanup failed: %w", err)
}
return nil
}

func (c CLIRunner) CleanCargoHomeCache(workLayer packit.Layer) error {
homeDir := filepath.Join(workLayer.Path, "home")
files, err := os.ReadDir(homeDir)
if err != nil {
if os.IsNotExist(err) {
return nil
}
return fmt.Errorf("unable to read directory\n%w", err)
}

for _, file := range files {
if file.IsDir() && file.Name() == "bin" ||
file.IsDir() && file.Name() == "registry" ||
file.IsDir() && file.Name() == "git" {
continue
}
err := os.RemoveAll(filepath.Join(homeDir, file.Name()))
if err != nil {
return fmt.Errorf("unable to remove files\n%w", err)
}
}

registryDir := filepath.Join(homeDir, "registry")
files, err = os.ReadDir(registryDir)
if err != nil && !os.IsNotExist(err) {
return fmt.Errorf("unable to read directory\n%w", err)
}

for _, file := range files {
if file.IsDir() && file.Name() == "index" ||
file.IsDir() && file.Name() == "cache" {
continue
}
err := os.RemoveAll(filepath.Join(registryDir, file.Name()))
if err != nil {
return fmt.Errorf("unable to remove files\n%w", err)
}
}

gitDir := filepath.Join(homeDir, "git")
files, err = os.ReadDir(gitDir)
if err != nil && !os.IsNotExist(err) {
return fmt.Errorf("unable to read directory\n%w", err)
}

for _, file := range files {
if file.IsDir() && file.Name() == "db" {
continue
}
err := os.RemoveAll(filepath.Join(gitDir, file.Name()))
if err != nil {
return fmt.Errorf("unable to remove files\n%w", err)
}
}

return nil
}

Expand Down
54 changes: 51 additions & 3 deletions cargo/cli_runner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cargo_test

import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
Expand Down Expand Up @@ -113,7 +114,8 @@ func testCLIRunner(t *testing.T, context spec.G, it spec.S) {
context("when there is a valid Rust project", func() {
it("builds correctly with defaults", func() {
env := os.Environ()
env = append(env, `CARGO_TARGET_DIR=/some/location/1`)
env = append(env, `CARGO_TARGET_DIR=/some/location/1/target`)
env = append(env, `CARGO_HOME=/some/location/1/home`)

for i := 0; i < len(env); i++ {
if strings.HasPrefix(env[i], "PATH=") {
Expand Down Expand Up @@ -152,7 +154,8 @@ func testCLIRunner(t *testing.T, context spec.G, it spec.S) {

it("builds correctly with custom args", func() {
env := os.Environ()
env = append(env, `CARGO_TARGET_DIR=/some/location/1`)
env = append(env, `CARGO_TARGET_DIR=/some/location/1/target`)
env = append(env, `CARGO_HOME=/some/location/1/home`)

for i := 0; i < len(env); i++ {
if strings.HasPrefix(env[i], "PATH=") {
Expand Down Expand Up @@ -187,7 +190,8 @@ func testCLIRunner(t *testing.T, context spec.G, it spec.S) {
context("failure cases", func() {
it("bubbles up failures", func() {
env := os.Environ()
env = append(env, `CARGO_TARGET_DIR=/some/location/1`)
env = append(env, `CARGO_TARGET_DIR=/some/location/1/target`)
env = append(env, `CARGO_HOME=/some/location/1/home`)

for i := 0; i < len(env); i++ {
if strings.HasPrefix(env[i], "PATH=") {
Expand Down Expand Up @@ -216,4 +220,48 @@ func testCLIRunner(t *testing.T, context spec.G, it spec.S) {
Expect(err).To(MatchError(Equal("build failed: expected")))
})
})

context("when cargo home has files", func() {
it("is cleaned up", func() {
workingDir, err := ioutil.TempDir("", "working-dir")
Expect(err).NotTo(HaveOccurred())

// To keep
Expect(os.MkdirAll(filepath.Join(workingDir, "home", "bin"), 0755)).ToNot(HaveOccurred())
Expect(os.MkdirAll(filepath.Join(workingDir, "home", "registry", "index"), 0755)).ToNot(HaveOccurred())
Expect(os.MkdirAll(filepath.Join(workingDir, "home", "registry", "cache"), 0755)).ToNot(HaveOccurred())
Expect(os.MkdirAll(filepath.Join(workingDir, "home", "git", "db"), 0755)).ToNot(HaveOccurred())

// To destroy
Expect(os.MkdirAll(filepath.Join(workingDir, "home", "registry", "foo"), 0755)).ToNot(HaveOccurred())
Expect(os.MkdirAll(filepath.Join(workingDir, "home", "git", "bar"), 0755)).ToNot(HaveOccurred())
Expect(os.MkdirAll(filepath.Join(workingDir, "home", "baz"), 0755)).ToNot(HaveOccurred())

err = cargo.NewCLIRunner(nil).CleanCargoHomeCache(packit.Layer{Name: "Cargo", Path: workingDir})
Expect(err).ToNot(HaveOccurred())
Expect(filepath.Join(workingDir, "home", "bin")).To(BeADirectory())
Expect(filepath.Join(workingDir, "home", "registry", "index")).To(BeADirectory())
Expect(filepath.Join(workingDir, "home", "registry", "cache")).To(BeADirectory())
Expect(filepath.Join(workingDir, "home", "git", "db")).To(BeADirectory())
Expect(filepath.Join(workingDir, "home", "registry", "foo")).ToNot(BeADirectory())
Expect(filepath.Join(workingDir, "home", "git", "bar")).ToNot(BeADirectory())
Expect(filepath.Join(workingDir, "home", "baz")).ToNot(BeADirectory())
})

it("handles when registry and git are not present", func() {
workingDir, err := ioutil.TempDir("", "working-dir")
Expect(err).NotTo(HaveOccurred())

// To keep
Expect(os.MkdirAll(filepath.Join(workingDir, "home", "bin"), 0755)).ToNot(HaveOccurred())

// To destroy
Expect(os.MkdirAll(filepath.Join(workingDir, "home", "baz"), 0755)).ToNot(HaveOccurred())

err = cargo.NewCLIRunner(nil).CleanCargoHomeCache(packit.Layer{Name: "Cargo", Path: workingDir})
Expect(err).ToNot(HaveOccurred())
Expect(filepath.Join(workingDir, "home", "bin")).To(BeADirectory())
Expect(filepath.Join(workingDir, "home", "baz")).ToNot(BeADirectory())
})
})
}
2 changes: 1 addition & 1 deletion cargo/mocks/executable.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 31537e2

Please sign in to comment.