diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/DirectoryBuildpack.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/DirectoryBuildpack.java index 6316dd99c134..bac39277e399 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/DirectoryBuildpack.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/DirectoryBuildpack.java @@ -24,7 +24,6 @@ import java.nio.file.Paths; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; -import java.nio.file.attribute.PosixFileAttributeView; import org.springframework.boot.buildpack.platform.docker.type.Layer; import org.springframework.boot.buildpack.platform.io.Content; @@ -82,9 +81,18 @@ public void apply(IOConsumer layers) throws IOException { private void addLayerContent(Layout layout) throws IOException { String id = this.coordinates.getSanitizedId(); Path cnbPath = Paths.get("/cnb/buildpacks/", id, this.coordinates.getVersion()); + writeBasePathEntries(layout, cnbPath); Files.walkFileTree(this.path, new LayoutFileVisitor(this.path, cnbPath, layout)); } + private void writeBasePathEntries(Layout layout, Path basePath) throws IOException { + int pathCount = basePath.getNameCount(); + for (int pathIndex = 1; pathIndex < pathCount + 1; pathIndex++) { + String name = "/" + basePath.subpath(0, pathIndex) + "/"; + layout.directory(name, Owner.ROOT); + } + } + /** * A {@link BuildpackResolver} compatible method to resolve directory buildpacks. * @param context the resolver context @@ -116,16 +124,30 @@ private static class LayoutFileVisitor extends SimpleFileVisitor { this.layout = layout; } + @Override + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { + if (!dir.equals(this.basePath)) { + this.layout.directory(relocate(dir), Owner.ROOT, getMode(dir)); + } + return FileVisitResult.CONTINUE; + } + @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { - PosixFileAttributeView attributeView = Files.getFileAttributeView(file, PosixFileAttributeView.class); - Assert.state(attributeView != null, - "Buildpack content in a directory is not supported on this operating system"); - int mode = FilePermissions.posixPermissionsToUmask(attributeView.readAttributes().permissions()); - this.layout.file(relocate(file), Owner.ROOT, mode, Content.of(file.toFile())); + this.layout.file(relocate(file), Owner.ROOT, getMode(file), Content.of(file.toFile())); return FileVisitResult.CONTINUE; } + private int getMode(Path path) throws IOException { + try { + return FilePermissions.umaskForPath(path); + } + catch (IllegalStateException ex) { + throw new IllegalStateException( + "Buildpack content in a directory is not supported on this operating system"); + } + } + private String relocate(Path path) { Path node = path.subpath(this.basePath.getNameCount(), path.getNameCount()); return Paths.get(this.layerPath.toString(), node.toString()).toString(); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/ImageBuildpack.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/ImageBuildpack.java index c64d5c24f551..7c26f9da5121 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/ImageBuildpack.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/ImageBuildpack.java @@ -127,11 +127,9 @@ private void copyLayerTar(Path path, OutputStream out) throws IOException { tarOut.setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX); TarArchiveEntry entry = tarIn.getNextTarEntry(); while (entry != null) { - if (entry.isFile()) { - tarOut.putArchiveEntry(entry); - StreamUtils.copy(tarIn, tarOut); - tarOut.closeArchiveEntry(); - } + tarOut.putArchiveEntry(entry); + StreamUtils.copy(tarIn, tarOut); + tarOut.closeArchiveEntry(); entry = tarIn.getNextTarEntry(); } tarOut.finish(); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/TarGzipBuildpack.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/TarGzipBuildpack.java index 6a0cce88436c..735c76619f6a 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/TarGzipBuildpack.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/TarGzipBuildpack.java @@ -89,6 +89,7 @@ private void copyAndRebaseEntries(OutputStream outputStream) throws IOException try (TarArchiveInputStream tar = new TarArchiveInputStream( new GzipCompressorInputStream(Files.newInputStream(this.path))); TarArchiveOutputStream output = new TarArchiveOutputStream(outputStream)) { + writeBasePathEntries(output, basePath); TarArchiveEntry entry = tar.getNextTarEntry(); while (entry != null) { entry.setName(basePath + "/" + entry.getName()); @@ -101,6 +102,16 @@ private void copyAndRebaseEntries(OutputStream outputStream) throws IOException } } + private void writeBasePathEntries(TarArchiveOutputStream output, Path basePath) throws IOException { + int pathCount = basePath.getNameCount(); + for (int pathIndex = 1; pathIndex < pathCount + 1; pathIndex++) { + String name = "/" + basePath.subpath(0, pathIndex) + "/"; + TarArchiveEntry entry = new TarArchiveEntry(name); + output.putArchiveEntry(entry); + output.closeArchiveEntry(); + } + } + /** * A {@link BuildpackResolver} compatible method to resolve tar-gzip buildpacks. * @param context the resolver context diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/FilePermissions.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/FilePermissions.java index 4d3457920353..7f63c012d82e 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/FilePermissions.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/FilePermissions.java @@ -16,6 +16,10 @@ package org.springframework.boot.buildpack.platform.io; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.PosixFileAttributeView; import java.nio.file.attribute.PosixFilePermission; import java.util.Collection; @@ -32,6 +36,21 @@ public final class FilePermissions { private FilePermissions() { } + /** + * Return the integer representation of the file permissions for a path, where the + * integer value conforms to the + * umask octal notation. + * @param path the file path + * @return the integer representation + * @throws IOException if path permissions cannot be read + */ + public static int umaskForPath(Path path) throws IOException { + Assert.notNull(path, "Path must not be null"); + PosixFileAttributeView attributeView = Files.getFileAttributeView(path, PosixFileAttributeView.class); + Assert.state(attributeView != null, "Unsupported file type for retrieving Posix attributes"); + return posixPermissionsToUmask(attributeView.readAttributes().permissions()); + } + /** * Return the integer representation of a set of Posix file permissions, where the * integer value conforms to the diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/Layout.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/Layout.java index 2e838d86ce70..1d4041efb2e7 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/Layout.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/Layout.java @@ -33,7 +33,18 @@ public interface Layout { * @param owner the owner of the directory * @throws IOException on IO error */ - void directory(String name, Owner owner) throws IOException; + default void directory(String name, Owner owner) throws IOException { + directory(name, owner, 0755); + } + + /** + * Add a directory to the content. + * @param name the full name of the directory to add + * @param owner the owner of the directory + * @param mode the permissions for the file + * @throws IOException on IO error + */ + void directory(String name, Owner owner, int mode) throws IOException; /** * Write a file to the content. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/TarLayoutWriter.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/TarLayoutWriter.java index da9140abdd12..1f8db8280414 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/TarLayoutWriter.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/io/TarLayoutWriter.java @@ -44,8 +44,8 @@ class TarLayoutWriter implements Layout, Closeable { } @Override - public void directory(String name, Owner owner) throws IOException { - this.outputStream.putArchiveEntry(createDirectoryEntry(name, owner)); + public void directory(String name, Owner owner, int mode) throws IOException { + this.outputStream.putArchiveEntry(createDirectoryEntry(name, owner, mode)); this.outputStream.closeArchiveEntry(); } @@ -56,8 +56,8 @@ public void file(String name, Owner owner, int mode, Content content) throws IOE this.outputStream.closeArchiveEntry(); } - private TarArchiveEntry createDirectoryEntry(String name, Owner owner) { - return createEntry(name, owner, TarConstants.LF_DIR, 0755, 0); + private TarArchiveEntry createDirectoryEntry(String name, Owner owner, int mode) { + return createEntry(name, owner, TarConstants.LF_DIR, mode, 0); } private TarArchiveEntry createFileEntry(String name, Owner owner, int mode, int size) { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/DirectoryBuildpackTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/DirectoryBuildpackTests.java index 390e04cd2ffa..25c61a47b4e6 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/DirectoryBuildpackTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/DirectoryBuildpackTests.java @@ -133,8 +133,11 @@ private void assertHasExpectedLayers(Buildpack buildpack) throws IOException { entries.add(entry); entry = tar.getNextTarEntry(); } - assertThat(entries).extracting("name", "mode").containsExactlyInAnyOrder( + assertThat(entries).extracting("name", "mode").containsExactlyInAnyOrder(tuple("/cnb/", 0755), + tuple("/cnb/buildpacks/", 0755), tuple("/cnb/buildpacks/example_buildpack1/", 0755), + tuple("/cnb/buildpacks/example_buildpack1/0.0.1/", 0755), tuple("/cnb/buildpacks/example_buildpack1/0.0.1/buildpack.toml", 0644), + tuple("/cnb/buildpacks/example_buildpack1/0.0.1/bin/", 0755), tuple("/cnb/buildpacks/example_buildpack1/0.0.1/bin/detect", 0744), tuple("/cnb/buildpacks/example_buildpack1/0.0.1/bin/build", 0744)); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/ImageBuildpackTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/ImageBuildpackTests.java index 648dbb3a9c28..bbe213a247a1 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/ImageBuildpackTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/ImageBuildpackTests.java @@ -38,6 +38,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.fail; +import static org.assertj.core.api.Assertions.tuple; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.willAnswer; @@ -126,6 +127,10 @@ private Object withMockLayers(InvocationOnMock invocation) { TarArchive archive = (out) -> { try (TarArchiveOutputStream tarOut = new TarArchiveOutputStream(out)) { tarOut.setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX); + writeTarEntry(tarOut, "/cnb/"); + writeTarEntry(tarOut, "/cnb/buildpacks/"); + writeTarEntry(tarOut, "/cnb/buildpacks/example_buildpack/"); + writeTarEntry(tarOut, "/cnb/buildpacks/example_buildpack/0.0.1/"); writeTarEntry(tarOut, "/cnb/buildpacks/example_buildpack/0.0.1/buildpack.toml"); writeTarEntry(tarOut, "/cnb/buildpacks/example_buildpack/0.0.1/" + this.longFilePath); tarOut.finish(); @@ -154,16 +159,22 @@ private void assertHasExpectedLayers(Buildpack buildpack) throws IOException { }); assertThat(layers).hasSize(1); byte[] content = layers.get(0).toByteArray(); - List names = new ArrayList<>(); + List entries = new ArrayList<>(); try (TarArchiveInputStream tar = new TarArchiveInputStream(new ByteArrayInputStream(content))) { TarArchiveEntry entry = tar.getNextTarEntry(); while (entry != null) { - names.add(entry.getName()); + entries.add(entry); entry = tar.getNextTarEntry(); } } - assertThat(names).containsExactlyInAnyOrder("cnb/buildpacks/example_buildpack/0.0.1/buildpack.toml", - "cnb/buildpacks/example_buildpack/0.0.1/" + this.longFilePath); + assertThat(entries).extracting("name", "mode").containsExactlyInAnyOrder( + tuple("cnb/", TarArchiveEntry.DEFAULT_DIR_MODE), + tuple("cnb/buildpacks/", TarArchiveEntry.DEFAULT_DIR_MODE), + tuple("cnb/buildpacks/example_buildpack/", TarArchiveEntry.DEFAULT_DIR_MODE), + tuple("cnb/buildpacks/example_buildpack/0.0.1/", TarArchiveEntry.DEFAULT_DIR_MODE), + tuple("cnb/buildpacks/example_buildpack/0.0.1/buildpack.toml", TarArchiveEntry.DEFAULT_FILE_MODE), + tuple("cnb/buildpacks/example_buildpack/0.0.1/" + this.longFilePath, + TarArchiveEntry.DEFAULT_FILE_MODE)); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/TestTarGzip.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/TestTarGzip.java index 94bae1cd5be1..4c50d71c4d1a 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/TestTarGzip.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/TestTarGzip.java @@ -87,12 +87,19 @@ private void writeBuildpackContentToArchive(Path archive) throws Exception { String buildScript = "#!/usr/bin/env bash\n" + "echo \"---> build\"\n"; try (TarArchiveOutputStream tar = new TarArchiveOutputStream(Files.newOutputStream(archive))) { writeEntry(tar, "buildpack.toml", buildpackToml.toString()); + writeEntry(tar, "bin/"); writeEntry(tar, "bin/detect", detectScript); writeEntry(tar, "bin/build", buildScript); tar.finish(); } } + private void writeEntry(TarArchiveOutputStream tar, String entryName) throws IOException { + TarArchiveEntry entry = new TarArchiveEntry(entryName); + tar.putArchiveEntry(entry); + tar.closeArchiveEntry(); + } + private void writeEntry(TarArchiveOutputStream tar, String entryName, String content) throws IOException { TarArchiveEntry entry = new TarArchiveEntry(entryName); entry.setSize(content.length()); @@ -111,8 +118,13 @@ void assertHasExpectedLayers(Buildpack buildpack) throws IOException { assertThat(layers).hasSize(1); byte[] content = layers.get(0).toByteArray(); try (TarArchiveInputStream tar = new TarArchiveInputStream(new ByteArrayInputStream(content))) { + assertThat(tar.getNextEntry().getName()).isEqualTo("cnb/"); + assertThat(tar.getNextEntry().getName()).isEqualTo("cnb/buildpacks/"); + assertThat(tar.getNextEntry().getName()).isEqualTo("cnb/buildpacks/example_buildpack1/"); + assertThat(tar.getNextEntry().getName()).isEqualTo("cnb/buildpacks/example_buildpack1/0.0.1/"); assertThat(tar.getNextEntry().getName()) .isEqualTo("cnb/buildpacks/example_buildpack1/0.0.1/buildpack.toml"); + assertThat(tar.getNextEntry().getName()).isEqualTo("cnb/buildpacks/example_buildpack1/0.0.1/bin/"); assertThat(tar.getNextEntry().getName()).isEqualTo("cnb/buildpacks/example_buildpack1/0.0.1/bin/detect"); assertThat(tar.getNextEntry().getName()).isEqualTo("cnb/buildpacks/example_buildpack1/0.0.1/bin/build"); assertThat(tar.getNextEntry()).isNull(); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/io/FilePermissionsTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/io/FilePermissionsTests.java index 0f05c177dfa4..e5b83f1340c8 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/io/FilePermissionsTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/io/FilePermissionsTests.java @@ -16,14 +16,21 @@ package org.springframework.boot.buildpack.platform.io; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.attribute.FileAttribute; import java.nio.file.attribute.PosixFilePermission; import java.nio.file.attribute.PosixFilePermissions; import java.util.Collections; import java.util.Set; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIOException; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; /** @@ -33,6 +40,28 @@ */ class FilePermissionsTests { + @TempDir + Path tempDir; + + @Test + void umaskForPath() throws IOException { + FileAttribute> fileAttribute = PosixFilePermissions + .asFileAttribute(PosixFilePermissions.fromString("rw-r-----")); + Path tempFile = Files.createTempFile(this.tempDir, "umask", null, fileAttribute); + assertThat(FilePermissions.umaskForPath(tempFile)).isEqualTo(0640); + } + + @Test + void umaskForPathWithNonExistentFile() throws IOException { + assertThatIOException() + .isThrownBy(() -> FilePermissions.umaskForPath(Paths.get(this.tempDir.toString(), "does-not-exist"))); + } + + @Test + void umaskForPathWithNullPath() throws IOException { + assertThatIllegalArgumentException().isThrownBy(() -> FilePermissions.umaskForPath(null)); + } + @Test void posixPermissionsToUmask() { Set permissions = PosixFilePermissions.fromString("rwxrw-r--"); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests.java index e8868a8be394..14742f09a77d 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests.java @@ -42,6 +42,7 @@ import org.springframework.boot.buildpack.platform.docker.DockerApi; import org.springframework.boot.buildpack.platform.docker.type.ImageName; import org.springframework.boot.buildpack.platform.docker.type.ImageReference; +import org.springframework.boot.buildpack.platform.io.FilePermissions; import org.springframework.boot.gradle.junit.GradleCompatibility; import org.springframework.boot.gradle.testkit.GradleBuild; import org.springframework.boot.testsupport.testcontainers.DisabledIfDockerUnavailable; @@ -312,8 +313,14 @@ private void writeLongNameResource() throws IOException { } private void writeBuildpackContent() throws IOException { + FileAttribute> dirAttribute = PosixFilePermissions + .asFileAttribute(PosixFilePermissions.fromString("rwxr-xr-x")); + FileAttribute> execFileAttribute = PosixFilePermissions + .asFileAttribute(PosixFilePermissions.fromString("rwxrwxrwx")); File buildpackDir = new File(this.gradleBuild.getProjectDir(), "buildpack/hello-world"); - buildpackDir.mkdirs(); + Files.createDirectories(buildpackDir.toPath(), dirAttribute); + File binDir = new File(buildpackDir, "bin"); + Files.createDirectories(binDir.toPath(), dirAttribute); File descriptor = new File(buildpackDir, "buildpack.toml"); try (PrintWriter writer = new PrintWriter(new FileWriter(descriptor))) { writer.println("api = \"0.2\""); @@ -325,17 +332,13 @@ private void writeBuildpackContent() throws IOException { writer.println("[[stacks]]\n"); writer.println("id = \"io.buildpacks.stacks.bionic\""); } - File binDir = new File(buildpackDir, "bin"); - binDir.mkdirs(); - FileAttribute> attribute = PosixFilePermissions - .asFileAttribute(PosixFilePermissions.fromString("rwxrwxrwx")); - File detect = Files.createFile(Paths.get(binDir.getAbsolutePath(), "detect"), attribute).toFile(); + File detect = Files.createFile(Paths.get(binDir.getAbsolutePath(), "detect"), execFileAttribute).toFile(); try (PrintWriter writer = new PrintWriter(new FileWriter(detect))) { writer.println("#!/usr/bin/env bash"); writer.println("set -eo pipefail"); writer.println("exit 0"); } - File build = Files.createFile(Paths.get(binDir.getAbsolutePath(), "build"), attribute).toFile(); + File build = Files.createFile(Paths.get(binDir.getAbsolutePath(), "build"), execFileAttribute).toFile(); try (PrintWriter writer = new PrintWriter(new FileWriter(build))) { writer.println("#!/usr/bin/env bash"); writer.println("set -eo pipefail"); @@ -349,16 +352,33 @@ private void tarGzipBuildpackContent() throws IOException { Path tarGzipPath = Paths.get(this.gradleBuild.getProjectDir().getAbsolutePath(), "hello-world.tgz"); try (TarArchiveOutputStream tar = new TarArchiveOutputStream( new GzipCompressorOutputStream(Files.newOutputStream(Files.createFile(tarGzipPath))))) { - writeFileToTar(tar, new File(this.gradleBuild.getProjectDir(), "buildpack/hello-world/buildpack.toml"), - "buildpack.toml", 0644); - writeFileToTar(tar, new File(this.gradleBuild.getProjectDir(), "buildpack/hello-world/bin/detect"), - "bin/detect", 0777); - writeFileToTar(tar, new File(this.gradleBuild.getProjectDir(), "buildpack/hello-world/bin/build"), - "bin/build", 0777); + File buildpackDir = new File(this.gradleBuild.getProjectDir(), "buildpack/hello-world"); + writeDirectoryToTar(tar, buildpackDir, buildpackDir.getAbsolutePath()); + } + } + + private void writeDirectoryToTar(TarArchiveOutputStream tar, File dir, String baseDirPath) throws IOException { + for (File file : dir.listFiles()) { + String name = file.getAbsolutePath().replace(baseDirPath, ""); + int mode = FilePermissions.umaskForPath(file.toPath()); + if (file.isDirectory()) { + writeTarEntry(tar, name + "/", mode); + writeDirectoryToTar(tar, file, baseDirPath); + } + else { + writeTarEntry(tar, file, name, mode); + } } } - private void writeFileToTar(TarArchiveOutputStream tar, File file, String name, int mode) throws IOException { + private void writeTarEntry(TarArchiveOutputStream tar, String name, int mode) throws IOException { + TarArchiveEntry entry = new TarArchiveEntry(name); + entry.setMode(mode); + tar.putArchiveEntry(entry); + tar.closeArchiveEntry(); + } + + private void writeTarEntry(TarArchiveOutputStream tar, File file, String name, int mode) throws IOException { TarArchiveEntry entry = new TarArchiveEntry(file, name); entry.setMode(mode); tar.putArchiveEntry(entry);