Add support for .ar archives (and .deb files)
This implements #[15130](https://github.com/bazelbuild/bazel/issues/15130).
As I was updating the docs for .ar and .deb formats, I also addressed some previous formats that had been added but not propagated through to all the documentation places.
Closes #15132.
PiperOrigin-RevId: 439569440
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/repository/ArFunction.java b/src/main/java/com/google/devtools/build/lib/bazel/repository/ArFunction.java
new file mode 100644
index 0000000..dc4ac44
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/repository/ArFunction.java
@@ -0,0 +1,81 @@
+// Copyright 2022 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.build.lib.bazel.repository;
+
+import com.google.common.io.ByteStreams;
+import com.google.devtools.build.lib.bazel.repository.DecompressorValue.Decompressor;
+import com.google.devtools.build.lib.vfs.Path;
+import java.io.BufferedInputStream;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import org.apache.commons.compress.archivers.ar.ArArchiveEntry;
+import org.apache.commons.compress.archivers.ar.ArArchiveInputStream;
+
+/**
+ * Opens a .ar archive file. It ignores the prefix setting because these archives cannot contain
+ * directories.
+ */
+public class ArFunction implements Decompressor {
+
+ public static final Decompressor INSTANCE = new ArFunction();
+
+ // This is the same value as picked for .tar files, which appears to have worked well.
+ private static final int BUFFER_SIZE = 32 * 1024;
+
+ private InputStream getDecompressorStream(DecompressorDescriptor descriptor) throws IOException {
+ return new BufferedInputStream(
+ new FileInputStream(descriptor.archivePath().getPathFile()), BUFFER_SIZE);
+ }
+ ;
+
+ @Override
+ public Path decompress(DecompressorDescriptor descriptor)
+ throws InterruptedException, IOException {
+ if (Thread.interrupted()) {
+ throw new InterruptedException();
+ }
+
+ try (InputStream decompressorStream = getDecompressorStream(descriptor)) {
+ ArArchiveInputStream arStream = new ArArchiveInputStream(decompressorStream);
+ ArArchiveEntry entry;
+ while ((entry = arStream.getNextArEntry()) != null) {
+ Path filePath = descriptor.repositoryPath().getRelative(entry.getName());
+ filePath.getParentDirectory().createDirectoryAndParents();
+ if (entry.isDirectory()) {
+ // ar archives don't contain any directory information, so this should never
+ // happen
+ continue;
+ } else {
+ // We do not have to worry about symlinks in .ar files - it's not supported
+ // by the .ar file format.
+ try (OutputStream out = filePath.getOutputStream()) {
+ ByteStreams.copy(arStream, out);
+ }
+ filePath.chmod(entry.getMode());
+ // entry.getLastModified() appears to be in seconds, so we need to convert
+ // it into milliseconds for setLastModifiedTime
+ filePath.setLastModifiedTime(entry.getLastModified() * 1000L);
+ }
+ if (Thread.interrupted()) {
+ throw new InterruptedException();
+ }
+ }
+ }
+
+ return descriptor.repositoryPath();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/repository/DecompressorValue.java b/src/main/java/com/google/devtools/build/lib/bazel/repository/DecompressorValue.java
index 49ff20b..a651c2c 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/repository/DecompressorValue.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/repository/DecompressorValue.java
@@ -107,11 +107,13 @@
return TarZstFunction.INSTANCE;
} else if (baseName.endsWith(".tar.bz2")) {
return TarBz2Function.INSTANCE;
+ } else if (baseName.endsWith(".ar") || baseName.endsWith(".deb")) {
+ return ArFunction.INSTANCE;
} else {
throw new RepositoryFunctionException(
Starlark.errorf(
"Expected a file with a .zip, .jar, .war, .aar, .tar, .tar.gz, .tgz, .tar.xz, .txz,"
- + " .tar.zst, .tzst, or .tar.bz2 suffix (got %s)",
+ + " .tar.zst, .tzst, .tar.bz2, .ar or .deb suffix (got %s)",
archivePath),
Transience.PERSISTENT);
}
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/repository/starlark/StarlarkRepositoryContext.java b/src/main/java/com/google/devtools/build/lib/bazel/repository/starlark/StarlarkRepositoryContext.java
index 217f8b0..1ef1d77 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/repository/starlark/StarlarkRepositoryContext.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/repository/starlark/StarlarkRepositoryContext.java
@@ -825,7 +825,8 @@
+ " By default, the archive type is determined from the file extension of"
+ " the URL."
+ " If the file has no extension, you can explicitly specify either \"zip\","
- + " \"jar\", \"war\", \"aar\", \"tar.gz\", \"tgz\", \"tar.bz2\", or \"tar.xz\""
+ + " \"jar\", \"war\", \"aar\", \"tar\", \"tar.gz\", \"tgz\", \"tar.xz\","
+ + " \"txz\", \".tar.zst\", \".tzst\", \"tar.bz2\", \".ar\", or \".deb\""
+ " here."),
@Param(
name = "stripPrefix",
diff --git a/src/test/java/com/google/devtools/build/lib/bazel/repository/ArFunctionTest.java b/src/test/java/com/google/devtools/build/lib/bazel/repository/ArFunctionTest.java
new file mode 100644
index 0000000..96077f0
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/bazel/repository/ArFunctionTest.java
@@ -0,0 +1,88 @@
+// Copyright 2022 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.build.lib.bazel.repository;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.devtools.build.lib.testutil.TestConstants;
+import com.google.devtools.build.lib.testutil.TestUtils;
+import com.google.devtools.build.lib.unix.UnixFileSystem;
+import com.google.devtools.build.lib.util.OS;
+import com.google.devtools.build.lib.vfs.DigestHashFunction;
+import com.google.devtools.build.lib.vfs.FileSystem;
+import com.google.devtools.build.lib.vfs.JavaIoFileSystem;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.runfiles.Runfiles;
+import java.io.File;
+import java.io.IOException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests decompressing archives. */
+@RunWith(JUnit4.class)
+public class ArFunctionTest {
+ /*
+ * .ar archive created with ar cr test_files.ar archived_first.txt archived_second.md
+ * The files contain short UTF-8 encoded strings.
+ */
+ private static final String ARCHIVE_NAME = "test_files.ar";
+ private static final String PATH_TO_TEST_ARCHIVE =
+ "/com/google/devtools/build/lib/bazel/repository/";
+ private static final String FIRST_FILE_NAME = "archived_first.txt";
+ private static final String SECOND_FILE_NAME = "archived_second.md";
+
+ @Test
+ public void testDecompress() throws Exception {
+ Path outputDir = decompress(createDescriptorBuilder());
+
+ assertThat(outputDir.exists()).isTrue();
+ Path firstFile = outputDir.getRelative(FIRST_FILE_NAME);
+ assertThat(firstFile.exists()).isTrue();
+ // There are 20 bytes in the content "this is test file 1"
+ assertThat(firstFile.getFileSize()).isEqualTo(20);
+ assertThat(firstFile.isSymbolicLink()).isFalse();
+
+ Path secondFile = outputDir.getRelative(SECOND_FILE_NAME);
+ assertThat(secondFile.exists()).isTrue();
+ // There are 20 bytes in the content "this is the second test file"
+ assertThat(secondFile.getFileSize()).isEqualTo(29);
+ assertThat(secondFile.isSymbolicLink()).isFalse();
+ }
+
+ private Path decompress(DecompressorDescriptor.Builder descriptorBuilder) throws Exception {
+ descriptorBuilder.setDecompressor(ArFunction.INSTANCE);
+ return new ArFunction().decompress(descriptorBuilder.build());
+ }
+
+ private DecompressorDescriptor.Builder createDescriptorBuilder() throws IOException {
+ // This was cribbed from TestArchiveDescriptor
+ FileSystem testFS =
+ OS.getCurrent() == OS.WINDOWS
+ ? new JavaIoFileSystem(DigestHashFunction.SHA256)
+ : new UnixFileSystem(DigestHashFunction.SHA256, /*hashAttributeName=*/ "");
+
+ // do not rely on TestConstants.JAVATESTS_ROOT end with slash, but ensure separators
+ // are not duplicated
+ String path =
+ (TestConstants.JAVATESTS_ROOT + PATH_TO_TEST_ARCHIVE + ARCHIVE_NAME).replace("//", "/");
+ Path tarballPath = testFS.getPath(Runfiles.create().rlocation(path));
+
+ Path workingDir = testFS.getPath(new File(TestUtils.tmpDir()).getCanonicalPath());
+ Path outDir = workingDir.getRelative("out");
+
+ return DecompressorDescriptor.builder().setRepositoryPath(outDir).setArchivePath(tarballPath);
+ }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/bazel/repository/BUILD b/src/test/java/com/google/devtools/build/lib/bazel/repository/BUILD
index 2397b27..f9cf05b 100644
--- a/src/test/java/com/google/devtools/build/lib/bazel/repository/BUILD
+++ b/src/test/java/com/google/devtools/build/lib/bazel/repository/BUILD
@@ -27,6 +27,7 @@
data = [
"test_decompress_archive.tar.gz",
"test_decompress_archive.zip",
+ "test_files.ar",
],
deps = [
"//src/main/java/com/google/devtools/build/lib/bazel/repository",
diff --git a/src/test/java/com/google/devtools/build/lib/bazel/repository/DecompressorValueTest.java b/src/test/java/com/google/devtools/build/lib/bazel/repository/DecompressorValueTest.java
index 032da6f..f457f2b 100644
--- a/src/test/java/com/google/devtools/build/lib/bazel/repository/DecompressorValueTest.java
+++ b/src/test/java/com/google/devtools/build/lib/bazel/repository/DecompressorValueTest.java
@@ -56,6 +56,10 @@
DecompressorDescriptor.builder().setArchivePath(path).build();
path = fs.getPath("/foo/.external-repositories/some-repo/bar.baz.tar.bz2");
DecompressorDescriptor.builder().setArchivePath(path).build();
+ path = fs.getPath("/foo/.external-repositories/some-repo/bar.baz.ar");
+ DecompressorDescriptor.builder().setArchivePath(path).build();
+ path = fs.getPath("/foo/.external-repositories/some-repo/bar.baz.deb");
+ DecompressorDescriptor.builder().setArchivePath(path).build();
}
@Test
diff --git a/src/test/java/com/google/devtools/build/lib/bazel/repository/test_files.ar b/src/test/java/com/google/devtools/build/lib/bazel/repository/test_files.ar
new file mode 100644
index 0000000..8aaddbe
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/bazel/repository/test_files.ar
@@ -0,0 +1,9 @@
+!<arch>
+// 40 `
+archived_first.txt/
+archived_second.md/
+/0 0 0 0 644 20 `
+this is test file 1
+/20 0 0 0 644 29 `
+this is the second test file
+
diff --git a/tools/build_defs/repo/http.bzl b/tools/build_defs/repo/http.bzl
index 1bbd2f9..fd40a5b 100644
--- a/tools/build_defs/repo/http.bzl
+++ b/tools/build_defs/repo/http.bzl
@@ -269,7 +269,7 @@
By default, the archive type is determined from the file extension of the
URL. If the file has no extension, you can explicitly specify one of the
following: `"zip"`, `"jar"`, `"war"`, `"aar"`, `"tar"`, `"tar.gz"`, `"tgz"`,
-`"tar.xz"`, or `tar.bz2`.""",
+`"tar.xz"`, `"txz"`, `"tar.zst"`, `"tzst"`, `tar.bz2`, `"ar"`, or `"deb"`.""",
),
"patches": attr.label_list(
default = [],
@@ -357,8 +357,9 @@
"""Downloads a Bazel repository as a compressed archive file, decompresses it,
and makes its targets available for binding.
-It supports the following file extensions: `"zip"`, `"jar"`, `"war"`, `"aar"`,
-`"tar"`, `"tar.gz"`, `"tgz"`, `"tar.xz"`, and `tar.bz2`.
+It supports the following file extensions: `"zip"`, `"jar"`, `"war"`, `"aar"`, `"tar"`,
+`"tar.gz"`, `"tgz"`, `"tar.xz"`, `"txz"`, `"tar.zst"`, `"tzst"`, `tar.bz2`, `"ar"`,
+or `"deb"`.
Examples:
Suppose the current repository contains the source code for a chat program,