From 89e84dedc72dd5c337c6f0f151cd678efbef8ced Mon Sep 17 00:00:00 2001 From: Daniel Mikusa Date: Sun, 3 Jul 2022 01:12:56 -0400 Subject: [PATCH] rust-toolchain.toml support This PR will optionally install Rust as specified in a 'rust-toolchain.toml' file. If a 'rust-toolchain' file is present, that file is used first. If not present, then 'rust-toolchain.toml' will be used. If both are present then 'rust-toolchain' is used. When a 'rust-toolchain' or 'rust-toolchain.toml' is included, then the buildpack will run 'rustup' and install Rust as specified in the file. If 'BP_RUST_PROFILE' or 'BP_RUST_TOOLCHAIN' are also set (non-default values), the buildpack will install them as well. If 'rust-toolchain' or 'rust-toolchain.toml' are not present then the buildpack will install 'BP_RUST_PROFILE' or 'BP_RUST_TOOLCHAIN'. The additional target as set in 'BP_RUST_TARGET' is still installed, as this is necessary if you are running on the Tiny stack. Resolves #56 Signed-off-by: Daniel Mikusa --- README.md | 2 + go.mod | 4 +- go.sum | 7 ++- rustup/build.go | 33 +++++++++- rustup/rust.go | 150 ++++++++++++++++++++++++++++++++++---------- rustup/rust_test.go | 125 +++++++++++++++++++++++++++++++++++- 6 files changed, 277 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index 722f450..2267918 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,8 @@ The buildpack will do the following: * Contributes `rustup-init` to a layer marked `cache` with command on `$PATH` * Executes `rustup-init` with the output written to a layer marked `build` and `cache` with installed commands on `$PATH` * Executes `rustup` to install a Rust toolchain to a layer marked `build` and `cache` with installed commands on `$PATH` + * If `rust-toolchain` or `rust-toolchain.toml` exists, `rustup` will install as configured in the file. If `$BP_RUST_TOOLCHAIN` / `$BP_RUST_PROFILE` are also set to non-default values, they will also be installed. + * If `rust-toolchain` or `rust-toolchain.toml` do not exist, `rustup` will install `$BP_RUST_TOOLCHAIN` / `$BP_RUST_PROFILE`. * If `$BP_RUST_TARGET` is set, executes `rustup target add` to install an additional Rust target. * If `$BP_RUST_TARGET` is not set and the build is running on the Paketo Tiny stack, then the Rust Linux musl target will be automatically added. diff --git a/go.mod b/go.mod index f101b69..eb71f75 100644 --- a/go.mod +++ b/go.mod @@ -23,8 +23,8 @@ require ( github.com/pelletier/go-toml v1.9.5 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/stretchr/objx v0.4.0 // indirect - golang.org/x/net v0.0.0-20220524220425-1d687d428aca // indirect - golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect + golang.org/x/net v0.0.0-20220630215102-69896b714898 // indirect + golang.org/x/sys v0.0.0-20220702020025-31831981b65f // indirect golang.org/x/text v0.3.7 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 0515fbb..2238d54 100644 --- a/go.sum +++ b/go.sum @@ -90,8 +90,8 @@ golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220524220425-1d687d428aca h1:xTaFYiPROfpPhqrfTIDXj0ri1SpfueYT951s4bAuDO8= -golang.org/x/net v0.0.0-20220524220425-1d687d428aca/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220630215102-69896b714898 h1:K7wO6V1IrczY9QOQ2WkVpw4JQSwCd52UsxVEirZUfiw= +golang.org/x/net v0.0.0-20220630215102-69896b714898/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -113,8 +113,9 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220702020025-31831981b65f h1:xdsejrW/0Wf2diT5CPp3XmKUNbr7Xvw8kYilQ+6qjRY= +golang.org/x/sys v0.0.0-20220702020025-31831981b65f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/rustup/build.go b/rustup/build.go index 6c82694..8a01dfd 100644 --- a/rustup/build.go +++ b/rustup/build.go @@ -17,7 +17,11 @@ package rustup import ( + "errors" "fmt" + "io/fs" + "os" + "path/filepath" "runtime" "github.com/buildpacks/libcnb" @@ -81,16 +85,21 @@ func (b Build) Build(context libcnb.BuildContext) (libcnb.BuildResult, error) { result.Layers = append(result.Layers, cargo) // install rustup - profile, _ := cr.Resolve("BP_RUST_PROFILE") + profile, profileSet := cr.Resolve("BP_RUST_PROFILE") rustup := NewRustup(rustupInitDependency.Version, profile) rustup.Logger = b.Logger result.Layers = append(result.Layers, rustup) // install rust - rustVersion, _ := cr.Resolve("BP_RUST_TOOLCHAIN") + rustToolChainFilePath, err := rustToolChainFilePath(context.Application.Path) + if err != nil { + return libcnb.BuildResult{}, fmt.Errorf("unable to find dependency\n%w", err) + } + + rustVersion, rustVersionSet := cr.Resolve("BP_RUST_TOOLCHAIN") additionalTarget := AdditionalTarget(cr, context.StackID) - rust := NewRust(profile, rustVersion, additionalTarget) + rust := NewRust(profile, rustVersion, additionalTarget, rustToolChainFilePath, profileSet, rustVersionSet) rust.Logger = b.Logger result.Layers = append(result.Layers, rust) @@ -117,3 +126,21 @@ func AdditionalTarget(cr libpak.ConfigurationResolver, stack string) string { return fmt.Sprintf("%s-unknown-linux-%s", arch, libc) } + +func rustToolChainFilePath(appPath string) (string, error) { + toolchainFilePath := filepath.Join(appPath, "rust-toolchain") + if _, err := os.Stat(toolchainFilePath); err == nil { + return toolchainFilePath, nil + } else if err != nil && !errors.Is(err, fs.ErrNotExist) { + return "", err + } + + toolchainFilePath = filepath.Join(appPath, "rust-toolchain.toml") + if _, err := os.Stat(toolchainFilePath); err == nil { + return toolchainFilePath, nil + } else if err != nil && !errors.Is(err, fs.ErrNotExist) { + return "", err + } + + return "", nil +} diff --git a/rustup/rust.go b/rustup/rust.go index 11e3028..c1791da 100644 --- a/rustup/rust.go +++ b/rustup/rust.go @@ -28,6 +28,7 @@ import ( "github.com/paketo-buildpacks/libpak/bard" "github.com/paketo-buildpacks/libpak/effect" "github.com/paketo-buildpacks/libpak/sbom" + "github.com/paketo-buildpacks/libpak/sherpa" ) // Rust will run `rustup` from the PATH to install a given toolchain @@ -37,11 +38,14 @@ type Rust struct { Arguments []string Executor effect.Executor Toolchain string + ToolchainSet bool Target string Profile string + ProfileSet bool + ToolchainFile string } -func NewRust(profile, toolchain, target string) Rust { +func NewRust(profile, toolchain, target, toolchainFile string, profileSet, toolchainSet bool) Rust { return Rust{ LayerContributor: libpak.NewLayerContributor( "Rust", @@ -54,10 +58,13 @@ func NewRust(profile, toolchain, target string) Rust { Build: true, Cache: true, }), - Executor: effect.NewExecutor(), - Profile: profile, - Toolchain: toolchain, - Target: target, + Executor: effect.NewExecutor(), + Profile: profile, + ProfileSet: profileSet, + Target: target, + Toolchain: toolchain, + ToolchainSet: toolchainSet, + ToolchainFile: toolchainFile, } } @@ -76,6 +83,13 @@ func (r Rust) Contribute(layer libcnb.Layer) (libcnb.Layer, error) { } r.LayerContributor.ExpectedMetadata.(map[string]interface{})["installed"] = strings.TrimSpace(buf.String()) + // add hash of rust toolchain file (rust-toolchain.toml or rust-toolchain) so it re-runs if that file changes + if hash, err := sherpa.NewFileListingHash(r.ToolchainFile); err != nil { + return libcnb.Layer{}, fmt.Errorf("unable to hash: %s\n%w", r.ToolchainFile, err) + } else { + r.LayerContributor.ExpectedMetadata.(map[string]interface{})["rust-toolchain"] = hash + } + layer, err := r.LayerContributor.Contribute(layer, func() (libcnb.Layer, error) { r.Logger.Body("Installing Rust") @@ -96,40 +110,27 @@ func (r Rust) Contribute(layer libcnb.Layer) (libcnb.Layer, error) { } } - if err := r.Executor.Execute(effect.Execution{ - Command: "rustup", - Args: []string{ - "-q", - "toolchain", - "install", - fmt.Sprintf("--profile=%s", r.Profile), - r.Toolchain, - }, - Dir: layer.Path, - Stdout: bard.NewWriter(r.Logger.Logger.InfoWriter(), bard.WithIndent(3)), - Stderr: bard.NewWriter(r.Logger.Logger.InfoWriter(), bard.WithIndent(3)), - }); err != nil { - return libcnb.Layer{}, fmt.Errorf("unable to run `rustup`\n%w", err) + rustToolChainFileExists := false + if _, err := os.Stat(r.ToolchainFile); err == nil { + rustToolChainFileExists = true } - if r.Target != "" { - if err := r.Executor.Execute(effect.Execution{ - Command: "rustup", - Args: []string{ - "-q", - "target", - "add", - fmt.Sprintf("--toolchain=%s", r.Toolchain), - r.Target, - }, - Dir: layer.Path, - Stdout: bard.NewWriter(r.Logger.Logger.InfoWriter(), bard.WithIndent(3)), - Stderr: bard.NewWriter(r.Logger.Logger.InfoWriter(), bard.WithIndent(3)), - }); err != nil { - return libcnb.Layer{}, fmt.Errorf("unable to run `rustup`\n%w", err) + if rustToolChainFileExists { + if err := r.installFromRustToolChainFile(layer); err != nil { + return libcnb.Layer{}, fmt.Errorf("unable to install rust from toolchain file\n%w", err) + } + } + + if !rustToolChainFileExists || r.ProfileSet || r.ToolchainSet { + if err := r.installRust(layer); err != nil { + return libcnb.Layer{}, fmt.Errorf("unable to install rust\n%w", err) } } + if err := r.installAdditionalTarget(layer); err != nil { + return libcnb.Layer{}, fmt.Errorf("unable to install additional rust target\n%w", err) + } + buf := &bytes.Buffer{} if err := r.Executor.Execute(effect.Execution{ Command: "rustc", @@ -180,9 +181,90 @@ func (r Rust) Contribute(layer libcnb.Layer) (libcnb.Layer, error) { } layer.Metadata["installed"] = strings.TrimSpace(buf.String()) + if hash, err := sherpa.NewFileListingHash(r.ToolchainFile); err != nil { + return libcnb.Layer{}, fmt.Errorf("unable to hash: %s\n%w", r.ToolchainFile, err) + } else { + layer.Metadata["rust-toolchain"] = hash + } + return layer, nil } func (r Rust) Name() string { return r.LayerContributor.Name } + +func (r Rust) installRust(layer libcnb.Layer) error { + if err := r.Executor.Execute(effect.Execution{ + Command: "rustup", + Args: []string{ + "-q", + "toolchain", + "install", + fmt.Sprintf("--profile=%s", r.Profile), + r.Toolchain, + }, + Dir: layer.Path, + Stdout: bard.NewWriter(r.Logger.Logger.InfoWriter(), bard.WithIndent(3)), + Stderr: bard.NewWriter(r.Logger.Logger.InfoWriter(), bard.WithIndent(3)), + }); err != nil { + return fmt.Errorf("unable to run `rustup toolchain install`\n%w", err) + } + + return nil +} + +func (r Rust) installAdditionalTarget(layer libcnb.Layer) error { + if r.Target != "" { + if err := r.Executor.Execute(effect.Execution{ + Command: "rustup", + Args: []string{ + "-q", + "target", + "add", + fmt.Sprintf("--toolchain=%s", r.Toolchain), + r.Target, + }, + Dir: layer.Path, + Stdout: bard.NewWriter(r.Logger.Logger.InfoWriter(), bard.WithIndent(3)), + Stderr: bard.NewWriter(r.Logger.Logger.InfoWriter(), bard.WithIndent(3)), + }); err != nil { + return fmt.Errorf("unable to run `rustup target add`\n%w", err) + } + } + + return nil +} + +func (r Rust) installFromRustToolChainFile(layer libcnb.Layer) error { + if err := r.Executor.Execute(effect.Execution{ + Command: "rustup", + Args: []string{ + "-q", + "default", + r.Toolchain, + }, + Dir: layer.Path, + Stdout: bard.NewWriter(r.Logger.Logger.InfoWriter(), bard.WithIndent(3)), + Stderr: bard.NewWriter(r.Logger.Logger.InfoWriter(), bard.WithIndent(3)), + }); err != nil { + return fmt.Errorf("unable to run `rustup default`\n%w", err) + } + + // This seems weird, but `rustup show` will actually read rust-toolchain.toml or rust-toolchain + // and install anything missing. + if err := r.Executor.Execute(effect.Execution{ + Command: "rustup", + Args: []string{ + "-q", + "show", + }, + Dir: layer.Path, + Stdout: bard.NewWriter(r.Logger.Logger.InfoWriter(), bard.WithIndent(3)), + Stderr: bard.NewWriter(r.Logger.Logger.InfoWriter(), bard.WithIndent(3)), + }); err != nil { + return fmt.Errorf("unable to run `rustup show`\n%w", err) + } + + return nil +} diff --git a/rustup/rust_test.go b/rustup/rust_test.go index 74729c1..976ce41 100644 --- a/rustup/rust_test.go +++ b/rustup/rust_test.go @@ -39,6 +39,7 @@ func testRust(t *testing.T, context spec.G, it spec.S) { ctx libcnb.BuildContext executor *mocks.Executor cargoHome string + appPath string ) it.Before(func() { @@ -47,6 +48,9 @@ func testRust(t *testing.T, context spec.G, it spec.S) { ctx.Layers.Path, err = ioutil.TempDir("", "rust-layers") Expect(err).NotTo(HaveOccurred()) + appPath, err = ioutil.TempDir("", "app-dir") + Expect(err).NotTo(HaveOccurred()) + cargoHome, err = ioutil.TempDir("", "cargoHome") Expect(err).NotTo(HaveOccurred()) Expect(os.MkdirAll(filepath.Join(cargoHome, "bin"), 0755)) @@ -61,6 +65,7 @@ func testRust(t *testing.T, context spec.G, it spec.S) { it.After(func() { Expect(os.Unsetenv("CARGO_HOME")).To(Succeed()) Expect(os.RemoveAll(ctx.Layers.Path)).To(Succeed()) + Expect(os.RemoveAll(appPath)).To(Succeed()) }) it("contributes rust", func() { @@ -80,7 +85,7 @@ func testRust(t *testing.T, context spec.G, it spec.S) { Expect(ioutil.WriteFile(filepath.Join(layer.Path, "env"), nil, 0644)).To(Succeed()) }) - r := rustup.NewRust("minimal", "1.2.3", "") + r := rustup.NewRust("minimal", "1.2.3", "", "", false, false) r.Executor = executor layer, err = r.Contribute(layer) @@ -123,7 +128,7 @@ func testRust(t *testing.T, context spec.G, it spec.S) { Expect(ioutil.WriteFile(filepath.Join(layer.Path, "env"), nil, 0644)).To(Succeed()) }) - r := rustup.NewRust("minimal", "1.2.3", "foo") + r := rustup.NewRust("minimal", "1.2.3", "foo", "", false, false) r.Executor = executor layer, err = r.Contribute(layer) @@ -154,4 +159,120 @@ func testRust(t *testing.T, context spec.G, it spec.S) { Expect(layer.SBOMPath(libcnb.SyftJSON)).To(BeARegularFile()) }) + it("contributes rust and a target from rust-toolchain.toml", func() { + layer, err := ctx.Layers.Layer("test-layer") + Expect(err).NotTo(HaveOccurred()) + + toolchainFilePath := filepath.Join(appPath, "rust-toolchain.toml") + Expect(os.WriteFile(toolchainFilePath, []byte("foo"), 0644)).To(Succeed()) + + executor.On("Execute", mock.MatchedBy(func(ex effect.Execution) bool { + return ex.Args[0] == "--version" && ex.Command == "rustc" + })).Return(func(ex effect.Execution) error { + _, err := ex.Stdout.Write([]byte("rustc 1.2.3 (53cb7b09b 2021-06-17)\n")) + Expect(err).ToNot(HaveOccurred()) + return nil + }) + + executor.On("Execute", mock.Anything).Return(nil).Run(func(args mock.Arguments) { + Expect(os.MkdirAll(layer.Path, 0755)).To(Succeed()) + Expect(ioutil.WriteFile(filepath.Join(layer.Path, "env"), nil, 0644)).To(Succeed()) + }) + + r := rustup.NewRust("minimal", "1.2.3", "foo", toolchainFilePath, false, false) + r.Executor = executor + + layer, err = r.Contribute(layer) + Expect(err).NotTo(HaveOccurred()) + + Expect(layer.LayerTypes.Build).To(BeTrue()) + Expect(layer.LayerTypes.Cache).To(BeTrue()) + Expect(layer.LayerTypes.Launch).To(BeFalse()) + + execCheck := executor.Calls[0].Arguments[0].(effect.Execution) + Expect(execCheck.Command).To(Equal("rustup")) + Expect(execCheck.Args).To(Equal([]string{"check"})) + + execDefault := executor.Calls[1].Arguments[0].(effect.Execution) + Expect(execDefault.Command).To(Equal("rustup")) + Expect(execDefault.Args).To(Equal([]string{"-q", "default", "1.2.3"})) + Expect(execDefault.Dir).To(Equal(layer.Path)) + + execShow := executor.Calls[2].Arguments[0].(effect.Execution) + Expect(execShow.Command).To(Equal("rustup")) + Expect(execShow.Args).To(Equal([]string{"-q", "show"})) + Expect(execShow.Dir).To(Equal(layer.Path)) + + execTarget := executor.Calls[3].Arguments[0].(effect.Execution) + Expect(execTarget.Command).To(Equal("rustup")) + Expect(execTarget.Args).To(Equal([]string{"-q", "target", "add", "--toolchain=1.2.3", "foo"})) + Expect(execTarget.Dir).To(Equal(layer.Path)) + + execVer := executor.Calls[4].Arguments[0].(effect.Execution) + Expect(execVer.Command).To(Equal("rustc")) + Expect(execVer.Args).To(Equal([]string{"--version"})) + + Expect(layer.SBOMPath(libcnb.SyftJSON)).To(BeARegularFile()) + }) + + it("contributes rust and a target from rust-toolchain.toml and from env variable", func() { + layer, err := ctx.Layers.Layer("test-layer") + Expect(err).NotTo(HaveOccurred()) + + toolchainFilePath := filepath.Join(appPath, "rust-toolchain.toml") + Expect(os.WriteFile(toolchainFilePath, []byte("foo"), 0644)).To(Succeed()) + + executor.On("Execute", mock.MatchedBy(func(ex effect.Execution) bool { + return ex.Args[0] == "--version" && ex.Command == "rustc" + })).Return(func(ex effect.Execution) error { + _, err := ex.Stdout.Write([]byte("rustc 1.2.3 (53cb7b09b 2021-06-17)\n")) + Expect(err).ToNot(HaveOccurred()) + return nil + }) + + executor.On("Execute", mock.Anything).Return(nil).Run(func(args mock.Arguments) { + Expect(os.MkdirAll(layer.Path, 0755)).To(Succeed()) + Expect(ioutil.WriteFile(filepath.Join(layer.Path, "env"), nil, 0644)).To(Succeed()) + }) + + r := rustup.NewRust("minimal", "1.2.3", "foo", toolchainFilePath, true, true) + r.Executor = executor + + layer, err = r.Contribute(layer) + Expect(err).NotTo(HaveOccurred()) + + Expect(layer.LayerTypes.Build).To(BeTrue()) + Expect(layer.LayerTypes.Cache).To(BeTrue()) + Expect(layer.LayerTypes.Launch).To(BeFalse()) + + execCheck := executor.Calls[0].Arguments[0].(effect.Execution) + Expect(execCheck.Command).To(Equal("rustup")) + Expect(execCheck.Args).To(Equal([]string{"check"})) + + execDefault := executor.Calls[1].Arguments[0].(effect.Execution) + Expect(execDefault.Command).To(Equal("rustup")) + Expect(execDefault.Args).To(Equal([]string{"-q", "default", "1.2.3"})) + Expect(execDefault.Dir).To(Equal(layer.Path)) + + execShow := executor.Calls[2].Arguments[0].(effect.Execution) + Expect(execShow.Command).To(Equal("rustup")) + Expect(execShow.Args).To(Equal([]string{"-q", "show"})) + Expect(execShow.Dir).To(Equal(layer.Path)) + + execToolchain := executor.Calls[3].Arguments[0].(effect.Execution) + Expect(execToolchain.Command).To(Equal("rustup")) + Expect(execToolchain.Args).To(Equal([]string{"-q", "toolchain", "install", "--profile=minimal", "1.2.3"})) + Expect(execToolchain.Dir).To(Equal(layer.Path)) + + execTarget := executor.Calls[4].Arguments[0].(effect.Execution) + Expect(execTarget.Command).To(Equal("rustup")) + Expect(execTarget.Args).To(Equal([]string{"-q", "target", "add", "--toolchain=1.2.3", "foo"})) + Expect(execTarget.Dir).To(Equal(layer.Path)) + + execVer := executor.Calls[5].Arguments[0].(effect.Execution) + Expect(execVer.Command).To(Equal("rustc")) + Expect(execVer.Args).To(Equal([]string{"--version"})) + + Expect(layer.SBOMPath(libcnb.SyftJSON)).To(BeARegularFile()) + }) }