diff --git a/README.md b/README.md index 4f4732b..1326da4 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,11 @@ The buildpack will do the following: ## Bindings The buildpack optionally accepts the following bindings: +### Type: `gradle` +|Secret | Description +|-----|-------------- +|`gradle.properties` | If present, the contents of the file are copied to `$GRADLE_USER_HOME/gradle.properties` which is [picked up by gradle and merged](https://docs.gradle.org/current/userguide/build_environment.html#sec:gradle_configuration_properties) when it runs. + ### Type: `dependency-mapping` |Key | Value | Description |----------------------|---------|------------ diff --git a/cmd/main/main.go b/cmd/main/main.go index 081ccfe..efa21f1 100644 --- a/cmd/main/main.go +++ b/cmd/main/main.go @@ -30,8 +30,9 @@ func main() { libpak.Main( gradle.Detect{}, gradle.Build{ - ApplicationFactory: libbs.NewApplicationFactory(), - Logger: bard.NewLogger(os.Stdout), + ApplicationFactory: libbs.NewApplicationFactory(), + Logger: bard.NewLogger(os.Stdout), + HomeDirectoryResolver: gradle.OSHomeDirectoryResolver{}, }, ) } diff --git a/gradle/build.go b/gradle/build.go index 969ca3a..8564cf3 100644 --- a/gradle/build.go +++ b/gradle/build.go @@ -22,6 +22,8 @@ import ( "os/user" "path/filepath" + "github.com/paketo-buildpacks/libpak/bindings" + "github.com/buildpacks/libcnb" "github.com/paketo-buildpacks/libbs" "github.com/paketo-buildpacks/libpak" @@ -29,8 +31,9 @@ import ( ) type Build struct { - Logger bard.Logger - ApplicationFactory ApplicationFactory + Logger bard.Logger + ApplicationFactory ApplicationFactory + HomeDirectoryResolver HomeDirectoryResolver } type ApplicationFactory interface { @@ -38,6 +41,21 @@ type ApplicationFactory interface { cache libbs.Cache, command string, bom *libcnb.BOM, applicationPath string) (libbs.Application, error) } +type HomeDirectoryResolver interface { + Location() (string, error) +} + +type OSHomeDirectoryResolver struct{} + +func (p OSHomeDirectoryResolver) Location() (string, error) { + u, err := user.Current() + if err != nil { + return "", fmt.Errorf("unable to determine user home directory\n%w", err) + } + + return u.HomeDir, nil +} + func (b Build) Build(context libcnb.BuildContext) (libcnb.BuildResult, error) { b.Logger.Title(context.Buildpack) result := libcnb.NewBuildResult() @@ -79,12 +97,13 @@ func (b Build) Build(context libcnb.BuildContext) (libcnb.BuildResult, error) { } } - u, err := user.Current() + homeDir, err := b.HomeDirectoryResolver.Location() if err != nil { - return libcnb.BuildResult{}, fmt.Errorf("unable to determine user home directory\n%w", err) + return libcnb.BuildResult{}, fmt.Errorf("unable to resolve home directory\n%w", err) } + gradleHome := filepath.Join(homeDir, ".gradle") - c := libbs.Cache{Path: filepath.Join(u.HomeDir, ".gradle")} + c := libbs.Cache{Path: gradleHome} c.Logger = b.Logger result.Layers = append(result.Layers, c) @@ -93,6 +112,15 @@ func (b Build) Build(context libcnb.BuildContext) (libcnb.BuildResult, error) { return libcnb.BuildResult{}, fmt.Errorf("unable to resolve build arguments\n%w", err) } + if binding, ok, err := bindings.ResolveOne(context.Platform.Bindings, bindings.OfType("gradle")); err != nil { + return libcnb.BuildResult{}, fmt.Errorf("unable to resolve binding\n%w", err) + } else if ok { + result.Layers = append(result.Layers, PropertiesFile{ + binding, + gradleHome, + }) + } + art := libbs.ArtifactResolver{ ArtifactConfigurationKey: "BP_GRADLE_BUILT_ARTIFACT", ConfigurationResolver: cr, diff --git a/gradle/build_test.go b/gradle/build_test.go index e2b04d0..4b1e9c0 100644 --- a/gradle/build_test.go +++ b/gradle/build_test.go @@ -37,6 +37,7 @@ func testBuild(t *testing.T, context spec.G, it spec.S) { ctx libcnb.BuildContext gradleBuild gradle.Build + homeDir string ) it.Before(func() { @@ -48,14 +49,19 @@ func testBuild(t *testing.T, context spec.G, it spec.S) { ctx.Layers.Path, err = ioutil.TempDir("", "build-layers") Expect(err).NotTo(HaveOccurred()) + homeDir, err = ioutil.TempDir("", "home-dir") + Expect(err).NotTo(HaveOccurred()) + gradleBuild = gradle.Build{ - ApplicationFactory: &FakeApplicationFactory{}, + ApplicationFactory: &FakeApplicationFactory{}, + HomeDirectoryResolver: FakeHomeDirectoryResolver{path: homeDir}, } }) it.After(func() { Expect(os.RemoveAll(ctx.Application.Path)).To(Succeed()) Expect(os.RemoveAll(ctx.Layers.Path)).To(Succeed()) + Expect(os.RemoveAll(homeDir)).To(Succeed()) }) it("does not contribute distribution if wrapper exists", func() { @@ -101,6 +107,47 @@ func testBuild(t *testing.T, context spec.G, it spec.S) { Expect(result.BOM.Entries[0].Build).To(BeTrue()) Expect(result.BOM.Entries[0].Launch).To(BeFalse()) }) + + context("gradle properties bindings exists", func() { + var bindingPath string + + it.Before(func() { + var err error + ctx.StackID = "test-stack-id" + ctx.Platform.Path, err = ioutil.TempDir("", "gradle-test-platform") + Expect(err).NotTo(HaveOccurred()) + Expect(ioutil.WriteFile(filepath.Join(ctx.Application.Path, "gradlew"), []byte{}, 0644)).To(Succeed()) + bindingPath = filepath.Join(ctx.Platform.Path, "bindings", "some-gradle") + ctx.Platform.Bindings = libcnb.Bindings{ + { + Name: "some-gradle", + Type: "gradle", + Secret: map[string]string{"gradle.properties": "gradle-properties-content"}, + Path: bindingPath, + }, + } + gradlePropertiesPath, ok := ctx.Platform.Bindings[0].SecretFilePath("gradle.properties") + Expect(os.MkdirAll(filepath.Dir(gradlePropertiesPath), 0777)).To(Succeed()) + Expect(ok).To(BeTrue()) + Expect(ioutil.WriteFile( + gradlePropertiesPath, + []byte("gradle-properties-content"), + 0644, + )).To(Succeed()) + }) + + it.After(func() { + Expect(os.RemoveAll(ctx.Platform.Path)).To(Succeed()) + }) + + it("provides gradle.properties under $GRADLE_USER_HOME", func() { + result, err := gradleBuild.Build(ctx) + Expect(err).NotTo(HaveOccurred()) + + Expect(result.Layers).To(HaveLen(3)) + Expect(result.Layers[1].Name()).To(Equal("gradle-properties")) + }) + }) } type FakeApplicationFactory struct{} @@ -116,3 +163,11 @@ func (f *FakeApplicationFactory) NewApplication( ) (libbs.Application, error) { return libbs.Application{Command: command}, nil } + +type FakeHomeDirectoryResolver struct { + path string +} + +func (f FakeHomeDirectoryResolver) Location() (string, error) { + return f.path, nil +} diff --git a/gradle/gradle_properties.go b/gradle/gradle_properties.go new file mode 100644 index 0000000..f2bbf1d --- /dev/null +++ b/gradle/gradle_properties.go @@ -0,0 +1,42 @@ +package gradle + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/buildpacks/libcnb" +) + +type PropertiesFile struct { + Binding libcnb.Binding + GradleHome string +} + +func (p PropertiesFile) Contribute(layer libcnb.Layer) (libcnb.Layer, error) { + path, ok := p.Binding.SecretFilePath("gradle.properties") + if !ok { + return layer, nil + } + + gradlePropertiesPath := filepath.Join(p.GradleHome, "gradle.properties") + if err := os.Symlink(path, gradlePropertiesPath); os.IsExist(err) { + err = os.Remove(gradlePropertiesPath) + if err != nil { + return layer, fmt.Errorf("unable to remove old symlink for gradle.properties\n%w", err) + } + + err = os.Symlink(path, gradlePropertiesPath) + if err != nil { + return layer, fmt.Errorf("unable to create symlink for gradle.properties on retry\n%w", err) + } + } else if err != nil { + return layer, fmt.Errorf("unable to symlink bound gradle.properties\n%w", err) + } + + return layer, nil +} + +func (p PropertiesFile) Name() string { + return "gradle-properties" +} diff --git a/gradle/gradle_properties_test.go b/gradle/gradle_properties_test.go new file mode 100644 index 0000000..c5377d5 --- /dev/null +++ b/gradle/gradle_properties_test.go @@ -0,0 +1,143 @@ +package gradle_test + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" + + "github.com/buildpacks/libcnb" + . "github.com/onsi/gomega" + "github.com/sclevine/spec" + + "github.com/paketo-buildpacks/gradle/gradle" +) + +func testGradleProperties(t *testing.T, context spec.G, it spec.S) { + var ( + Expect = NewWithT(t).Expect + + ctx libcnb.BuildContext + gradleProps gradle.PropertiesFile + gradleLayer libcnb.Layer + gradleHome string + gradleTargetPropsPath string + bindingPath string + homeDir string + ) + + it.Before(func() { + var err error + + ctx.Platform.Path, err = ioutil.TempDir("", "gradle-test-platform") + Expect(err).NotTo(HaveOccurred()) + + ctx.Application.Path, err = ioutil.TempDir("", "build-application") + Expect(err).NotTo(HaveOccurred()) + + ctx.Layers.Path, err = ioutil.TempDir("", "build-layers") + Expect(err).NotTo(HaveOccurred()) + + homeDir, err = ioutil.TempDir("", "home-dir") + Expect(err).NotTo(HaveOccurred()) + + gradleHome = filepath.Join(homeDir, ".gradle") + gradleTargetPropsPath = filepath.Join(gradleHome, "gradle.properties") + }) + + it.After(func() { + Expect(os.RemoveAll(ctx.Platform.Path)).To(Succeed()) + Expect(os.RemoveAll(ctx.Application.Path)).To(Succeed()) + Expect(os.RemoveAll(ctx.Layers.Path)).To(Succeed()) + Expect(os.RemoveAll(homeDir)).To(Succeed()) + }) + + context("no binding is present", func() { + it("does nothing ", func() { + layer, err := gradleProps.Contribute(gradleLayer) + Expect(err).NotTo(HaveOccurred()) + Expect(layer).To(Equal(gradleLayer)) + + Expect(gradleHome).ToNot(BeADirectory()) + Expect(gradleTargetPropsPath).ToNot(BeAnExistingFile()) + }) + }) + + context("a binding is present", func() { + it.Before(func() { + var err error + + bindingPath = filepath.Join(ctx.Platform.Path, "bindings", "some-gradle") + ctx.Platform.Bindings = libcnb.Bindings{ + { + Name: "some-gradle", + Type: "gradle", + Secret: map[string]string{"gradle.properties": "gradle-properties-content"}, + Path: bindingPath, + }, + } + gradleSrcPropsPath, ok := ctx.Platform.Bindings[0].SecretFilePath("gradle.properties") + Expect(os.MkdirAll(filepath.Dir(gradleSrcPropsPath), 0777)).To(Succeed()) + Expect(ok).To(BeTrue()) + Expect(ioutil.WriteFile( + gradleSrcPropsPath, + []byte("gradle-properties-content"), + 0644, + )).To(Succeed()) + + // normally done by cache layer + Expect(os.MkdirAll(gradleHome, 0755)).ToNot(HaveOccurred()) + + gradleLayer, err = ctx.Layers.Layer("gradle-properties") + Expect(err).NotTo(HaveOccurred()) + + gradleProps = gradle.PropertiesFile{ + Binding: ctx.Platform.Bindings[0], + GradleHome: gradleHome, + } + }) + + it("creates a symlink for gradle.properties under $GRADLE_USER_HOME", func() { + layer, err := gradleProps.Contribute(gradleLayer) + Expect(err).NotTo(HaveOccurred()) + Expect(layer).To(Equal(gradleLayer)) + + info, err := os.Lstat(gradleTargetPropsPath) + Expect(err).NotTo(HaveOccurred()) + Expect(info.Mode()&os.ModeSymlink != 0).To(BeTrue()) // is symlink bit set + + target, err := os.Readlink(gradleTargetPropsPath) + Expect(err).NotTo(HaveOccurred()) + Expect(target).To(Equal(filepath.Join(bindingPath, "gradle.properties"))) + + data, err := ioutil.ReadFile(gradleTargetPropsPath) + Expect(err).ToNot(HaveOccurred()) + Expect(string(data)).To(Equal("gradle-properties-content")) + }) + + it("recreates symlink for gradle.properties under $GRADLE_USER_HOME", func() { + Expect(os.MkdirAll(filepath.Dir(gradleTargetPropsPath), 0755)).ToNot(HaveOccurred()) + err := os.Symlink("/dev/null", gradleTargetPropsPath) + Expect(err).NotTo(HaveOccurred()) + Expect(gradleTargetPropsPath).To(BeAnExistingFile()) + + layer, err := gradleProps.Contribute(gradleLayer) + Expect(err).NotTo(HaveOccurred()) + Expect(layer).To(Equal(gradleLayer)) + + info, err := os.Lstat(gradleTargetPropsPath) + Expect(err).NotTo(HaveOccurred()) + Expect(info.Mode()&os.ModeSymlink != 0).To(BeTrue()) // is symlink bit set + + target, err := os.Readlink(gradleTargetPropsPath) + Expect(err).NotTo(HaveOccurred()) + // symlink should point to our binding, not /dev/null + Expect(target).To(Equal(filepath.Join(bindingPath, "gradle.properties"))) + + data, err := ioutil.ReadFile(gradleTargetPropsPath) + Expect(err).ToNot(HaveOccurred()) + Expect(string(data)).To(Equal("gradle-properties-content")) + }) + + }) +} diff --git a/gradle/init_test.go b/gradle/init_test.go index a576b55..399d085 100644 --- a/gradle/init_test.go +++ b/gradle/init_test.go @@ -28,5 +28,6 @@ func TestUnit(t *testing.T) { suite("Build", testBuild) suite("Detect", testDetect) suite("Distribution", testDistribution) + suite("Properties", testGradleProperties) suite.Run(t) }