Skip to content

Commit

Permalink
rust-toolchain.toml support
Browse files Browse the repository at this point in the history
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 specified, then the buildpack does not use 'BP_RUST_PROFILE' or 'BP_RUST_TOOLCHAIN'. The two are mutually exclusive. The '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 <[email protected]>
  • Loading branch information
Daniel Mikusa committed Jul 3, 2022
1 parent 8076e76 commit 710c25c
Show file tree
Hide file tree
Showing 5 changed files with 185 additions and 43 deletions.
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 4 additions & 3 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand All @@ -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=
Expand Down
29 changes: 28 additions & 1 deletion rustup/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@
package rustup

import (
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"runtime"

"github.com/buildpacks/libcnb"
Expand Down Expand Up @@ -88,9 +92,14 @@ func (b Build) Build(context libcnb.BuildContext) (libcnb.BuildResult, error) {
result.Layers = append(result.Layers, rustup)

// install rust
rustToolChainFilePath, err := rustToolChainFilePath(context.Application.Path)
if err != nil {
return libcnb.BuildResult{}, fmt.Errorf("unable to find dependency\n%w", err)
}

rustVersion, _ := cr.Resolve("BP_RUST_TOOLCHAIN")
additionalTarget := AdditionalTarget(cr, context.StackID)
rust := NewRust(profile, rustVersion, additionalTarget)
rust := NewRust(profile, rustVersion, additionalTarget, rustToolChainFilePath)
rust.Logger = b.Logger

result.Layers = append(result.Layers, rust)
Expand All @@ -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
}
129 changes: 94 additions & 35 deletions rustup/rust.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ package rustup

import (
"bytes"
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"strings"
Expand All @@ -28,6 +30,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
Expand All @@ -39,9 +42,10 @@ type Rust struct {
Toolchain string
Target string
Profile string
ToolchainFile string
}

func NewRust(profile, toolchain, target string) Rust {
func NewRust(profile, toolchain, target, toolchainFile string) Rust {
return Rust{
LayerContributor: libpak.NewLayerContributor(
"Rust",
Expand All @@ -54,10 +58,11 @@ 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,
Target: target,
Toolchain: toolchain,
ToolchainFile: toolchainFile,
}
}

Expand All @@ -76,6 +81,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")

Expand All @@ -96,38 +108,18 @@ 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)
if _, err := os.Stat(r.ToolchainFile); errors.Is(err, fs.ErrNotExist) {
if err := r.installRust(layer); err != nil {
return libcnb.Layer{}, fmt.Errorf("unable to install rust\n%w", err)
}
} else {
if err := r.installFromRustToolChainFile(layer); err != nil {
return libcnb.Layer{}, fmt.Errorf("unable to install rust from toolchain file\n%w", err)
}
}

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 err := r.installAdditionalTarget(layer); err != nil {
return libcnb.Layer{}, fmt.Errorf("unable to install additional rust target\n%w", err)
}

buf := &bytes.Buffer{}
Expand Down Expand Up @@ -180,9 +172,76 @@ 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 {
// 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
}
59 changes: 57 additions & 2 deletions rustup/rust_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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))
Expand All @@ -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() {
Expand All @@ -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", "", "")
r.Executor = executor

layer, err = r.Contribute(layer)
Expand Down Expand Up @@ -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", "")
r.Executor = executor

layer, err = r.Contribute(layer)
Expand Down Expand Up @@ -154,4 +159,54 @@ 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)
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"}))

execToolchain := executor.Calls[1].Arguments[0].(effect.Execution)
Expect(execToolchain.Command).To(Equal("rustup"))
Expect(execToolchain.Args).To(Equal([]string{"-q", "show"}))
Expect(execToolchain.Dir).To(Equal(layer.Path))

execTarget := executor.Calls[2].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[3].Arguments[0].(effect.Execution)
Expect(execVer.Command).To(Equal("rustc"))
Expect(execVer.Args).To(Equal([]string{"--version"}))

Expect(layer.SBOMPath(libcnb.SyftJSON)).To(BeARegularFile())
})
}

0 comments on commit 710c25c

Please sign in to comment.