AccumulateClassNames makes the discovered class names and their hashes available.
Summary:
In turn, this makes the class names and hashes available to
`DexProducedFromJavaLibraryThatContainsClassFiles`, which is important for:
(1) Avoiding a re-dex when the .class files have not changed.
(2) Making it possible for split-dex to determine which intermediate .dex
files need to be in the primary classes.dex.
Test Plan: Sandcastle builds.
diff --git a/src/com/facebook/buck/java/AccumulateClassNames.java b/src/com/facebook/buck/java/AccumulateClassNames.java
index 84bfced..142bfad 100644
--- a/src/com/facebook/buck/java/AccumulateClassNames.java
+++ b/src/com/facebook/buck/java/AccumulateClassNames.java
@@ -25,29 +25,50 @@
import com.facebook.buck.rules.BuildRuleType;
import com.facebook.buck.rules.Buildable;
import com.facebook.buck.rules.BuildableContext;
+import com.facebook.buck.rules.OnDiskBuildInfo;
import com.facebook.buck.step.Step;
import com.facebook.buck.step.fs.MkdirStep;
import com.facebook.buck.step.fs.RmStep;
import com.facebook.buck.util.BuckConstant;
+import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
+import com.google.common.base.Splitter;
+import com.google.common.base.Supplier;
+import com.google.common.base.Suppliers;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSortedMap;
import com.google.common.collect.ImmutableSortedSet;
+import com.google.common.hash.HashCode;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
+import javax.annotation.Nullable;
+
/**
* {@link Buildable} that writes the list of {@code .class} files found in a zip or directory to a
* file.
*/
public class AccumulateClassNames extends AbstractBuildable {
+ private static final Splitter CLASS_NAME_AND_HASH_SPLITTER = Splitter.on(
+ AccumulateClassNamesStep.CLASS_NAME_HASH_CODE_SEPARATOR);
+
private final JavaLibraryRule javaLibraryRule;
private final Path pathToOutputFile;
- private AccumulateClassNames(BuildTarget buildTarget, JavaLibraryRule javaLibraryRule) {
+ /**
+ * This will contain the classes info discovered in the course of building this {@link Buildable}.
+ * This {@link Supplier} will be defined and determined after this buildable is built.
+ */
+ @VisibleForTesting
+ @Nullable
+ Supplier<ImmutableSortedMap<String, HashCode>> classNames;
+
+ @VisibleForTesting
+ AccumulateClassNames(BuildTarget buildTarget, JavaLibraryRule javaLibraryRule) {
Preconditions.checkNotNull(buildTarget);
this.javaLibraryRule = Preconditions.checkNotNull(javaLibraryRule);
this.pathToOutputFile = Paths.get(
@@ -81,13 +102,47 @@
// Make sure that the output directory exists for the output file.
steps.add(new MkdirStep(pathToOutputFile.getParent()));
- steps.add(new AccumulateClassNamesStep(
+ AccumulateClassNamesStep accumulateClassNamesStep = new AccumulateClassNamesStep(
Paths.get(javaLibraryRule.getPathToOutputFile()),
- Paths.get(getPathToOutputFile())));
+ Paths.get(getPathToOutputFile()));
+ classNames = accumulateClassNamesStep;
+ steps.add(accumulateClassNamesStep);
return steps.build();
}
+ public ImmutableSortedMap<String, HashCode> getClassNames() {
+ // TODO(mbolin): Assert that this Buildable has been built. Currently, there is no way to do
+ // that from a Buildable (but there is from an AbstractCachingBuildRule).
+ Preconditions.checkNotNull(classNames);
+ return classNames.get();
+ }
+
+ @Override
+ protected void initializeFromDisk(OnDiskBuildInfo onDiskBuildInfo) {
+ // Assign this.classNames when instantiating this Buildable when pulled from cache.
+
+ // Read the output file, which should now be in place because this rule was downloaded from
+ // cache.
+ List<String> lines;
+ try {
+ lines = onDiskBuildInfo.getOutputFileContentsByLine(this);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+
+ // Use the contents of the file to create the ImmutableSortedMap<String, HashCode>.
+ ImmutableSortedMap.Builder<String, HashCode> classNamesBuilder = ImmutableSortedMap.naturalOrder();
+ for (String line : lines) {
+ List<String> parts = CLASS_NAME_AND_HASH_SPLITTER.splitToList(line);
+ Preconditions.checkState(parts.size() == 2);
+ String key = parts.get(0);
+ HashCode value = HashCode.fromString(parts.get(1));
+ classNamesBuilder.put(key, value);
+ }
+ this.classNames = Suppliers.ofInstance(classNamesBuilder.build());
+ }
+
public static Builder newAccumulateClassNamesBuilder(AbstractBuildRuleBuilderParams params) {
return new Builder(params);
}
diff --git a/src/com/facebook/buck/java/AccumulateClassNamesStep.java b/src/com/facebook/buck/java/AccumulateClassNamesStep.java
index fd1ccbc..e2e0cb5 100644
--- a/src/com/facebook/buck/java/AccumulateClassNamesStep.java
+++ b/src/com/facebook/buck/java/AccumulateClassNamesStep.java
@@ -25,6 +25,7 @@
import com.facebook.buck.step.Step;
import com.google.common.base.Function;
import com.google.common.base.Preconditions;
+import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.collect.Iterables;
import com.google.common.hash.HashCode;
@@ -39,11 +40,14 @@
import java.util.Map;
import java.util.Map.Entry;
+import javax.annotation.Nullable;
+
/**
* {@link Step} that takes a directory or zip of {@code .class} files and traverses it to get the
* total set of {@code .class} files included by the directory or zip.
*/
-public class AccumulateClassNamesStep extends AbstractExecutionStep {
+public class AccumulateClassNamesStep extends AbstractExecutionStep
+ implements Supplier<ImmutableSortedMap<String, HashCode>> {
/**
* In the generated {@code classes.txt} file, each line will contain the path to a {@code .class}
@@ -61,6 +65,14 @@
private final Path pathToJarOrClassesDirectory;
private final Path whereClassNamesShouldBeWritten;
+ /**
+ * Map of names of {@code .class} files to hashes of their contents.
+ * <p>
+ * This is not set until this step is executed.
+ */
+ @Nullable
+ private ImmutableSortedMap<String, HashCode> classNames;
+
public AccumulateClassNamesStep(Path pathToJarOrClassesDirectory,
Path whereClassNamesShouldBeWritten) {
super("get_class_names " + pathToJarOrClassesDirectory + " > " + whereClassNamesShouldBeWritten);
@@ -119,7 +131,15 @@
return 1;
}
+ this.classNames = classNames;
return 0;
}
+ @Override
+ public ImmutableSortedMap<String, HashCode> get() {
+ Preconditions.checkNotNull(classNames,
+ "Step must be executed successfully before invoking this method.");
+ return classNames;
+ }
+
}
diff --git a/src/com/facebook/buck/rules/OnDiskBuildInfo.java b/src/com/facebook/buck/rules/OnDiskBuildInfo.java
index 06555d5..cc84282 100644
--- a/src/com/facebook/buck/rules/OnDiskBuildInfo.java
+++ b/src/com/facebook/buck/rules/OnDiskBuildInfo.java
@@ -21,7 +21,10 @@
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
+import java.io.IOException;
import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.List;
/**
* Utility for reading the metadata associated with a build rule's output. This is metadata that
@@ -73,4 +76,16 @@
return getValue(BuildInfo.METADATA_KEY_FOR_RULE_KEY_WITHOUT_DEPS)
.transform(RuleKey.TO_RULE_KEY);
}
+
+ /**
+ * Invokes the {@link Buildable#getPathToOutputFile()} method of the specified {@link Buildable},
+ * reads the file at the specified path, and returns the list of lines in the file.
+ */
+ public List<String> getOutputFileContentsByLine(Buildable buildable) throws IOException {
+ Preconditions.checkNotNull(buildable);
+ String pathToOutputFile = buildable.getPathToOutputFile();
+ Preconditions.checkNotNull(pathToOutputFile);
+ Path path = Paths.get(pathToOutputFile);
+ return projectFilesystem.readLines(path);
+ }
}
diff --git a/test/com/facebook/buck/java/AccumulateClassNamesStepTest.java b/test/com/facebook/buck/java/AccumulateClassNamesStepTest.java
index 60de63a..3512cd0 100644
--- a/test/com/facebook/buck/java/AccumulateClassNamesStepTest.java
+++ b/test/com/facebook/buck/java/AccumulateClassNamesStepTest.java
@@ -23,6 +23,8 @@
import com.facebook.buck.util.ProjectFilesystem;
import com.google.common.base.Charsets;
import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableSortedMap;
+import com.google.common.hash.HashCode;
import com.google.common.io.Files;
import org.junit.Rule;
@@ -80,6 +82,14 @@
"com/example/Foo" + separator + SHA1_FOR_EMPTY_STRING,
"com/example/subpackage/Baz" + separator + SHA1_FOR_EMPTY_STRING) + '\n',
contents);
+ assertEquals(
+ "get() should return the class name/hash mappings.",
+ ImmutableSortedMap.of(
+ "com/example/Bar", HashCode.fromString(SHA1_FOR_EMPTY_STRING),
+ "com/example/Foo", HashCode.fromString(SHA1_FOR_EMPTY_STRING),
+ "com/example/subpackage/Baz", HashCode.fromString(SHA1_FOR_EMPTY_STRING)
+ ),
+ accumulateClassNamesStep.get());
}
@Test
@@ -115,5 +125,21 @@
"com/example/Foo" + separator + SHA1_FOR_EMPTY_STRING,
"com/example/subpackage/Baz" + separator + SHA1_FOR_EMPTY_STRING) + '\n',
contents);
+ assertEquals(
+ "get() should return the class name/hash mappings.",
+ ImmutableSortedMap.of(
+ "com/example/Bar", HashCode.fromString(SHA1_FOR_EMPTY_STRING),
+ "com/example/Foo", HashCode.fromString(SHA1_FOR_EMPTY_STRING),
+ "com/example/subpackage/Baz", HashCode.fromString(SHA1_FOR_EMPTY_STRING)
+ ),
+ accumulateClassNamesStep.get());
+ }
+
+ @Test(expected = NullPointerException.class)
+ public void testGetClassesBeforeExecuteThrowsException() {
+ AccumulateClassNamesStep accumulateClassNamesStep = new AccumulateClassNamesStep(
+ Paths.get("pathToJarOrClassesDirectory"),
+ Paths.get("whereClassNamesShouldBeWritten"));
+ accumulateClassNamesStep.get();
}
}
diff --git a/test/com/facebook/buck/java/AccumulateClassNamesTest.java b/test/com/facebook/buck/java/AccumulateClassNamesTest.java
index f04c5d2..d9987a6 100644
--- a/test/com/facebook/buck/java/AccumulateClassNamesTest.java
+++ b/test/com/facebook/buck/java/AccumulateClassNamesTest.java
@@ -16,7 +16,9 @@
package com.facebook.buck.java;
+import static org.easymock.EasyMock.expect;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
import com.facebook.buck.model.BuildTarget;
import com.facebook.buck.rules.AbstractBuildRuleBuilderParams;
@@ -26,13 +28,16 @@
import com.facebook.buck.rules.BuildRuleType;
import com.facebook.buck.rules.BuildableContext;
import com.facebook.buck.rules.FakeAbstractBuildRuleBuilderParams;
+import com.facebook.buck.rules.OnDiskBuildInfo;
import com.facebook.buck.step.ExecutionContext;
import com.facebook.buck.step.Step;
import com.facebook.buck.step.TestExecutionContext;
import com.facebook.buck.testutil.MoreAsserts;
import com.facebook.buck.util.ProjectFilesystem;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSortedMap;
import com.google.common.collect.ImmutableSortedSet;
+import com.google.common.hash.HashCode;
import org.easymock.EasyMockSupport;
import org.junit.Test;
@@ -85,6 +90,9 @@
List<Step> steps = accumulateClassNames.getBuildSteps(buildContext, buildableContext);
verifyAll();
+ assertNotNull("The Supplier should be set as a side-effect of creating the steps.",
+ accumulateClassNames.classNames);
+
// Verify the build steps.
ProjectFilesystem projectFilesystem = new ProjectFilesystem(new File("."));
ExecutionContext context = TestExecutionContext
@@ -100,4 +108,36 @@
steps,
context);
}
+
+ @Test
+ public void testInitializeFromDisk() throws IOException {
+ BuildTarget buildTarget = new BuildTarget("//foo", "bar");
+ JavaLibraryRule javaRule = createMock(JavaLibraryRule.class);
+
+ replayAll();
+ AccumulateClassNames accumulateClassNames = new AccumulateClassNames(buildTarget, javaRule);
+ verifyAll();
+ resetAll();
+
+ OnDiskBuildInfo onDiskBuildInfo = createMock(OnDiskBuildInfo.class);
+ List<String> lines = ImmutableList.of(
+ "com/example/Bar 087b7707a5f8e0a2adf5652e3cd2072d89a197dc",
+ "com/example/Baz 62b1c2510840c0de55c13f66065a98a719be0f19",
+ "com/example/Foo e4fccb7520b7795e632651323c63217c9f59f72a");
+ expect(onDiskBuildInfo.getOutputFileContentsByLine(accumulateClassNames)).andReturn(lines);
+
+ replayAll();
+ accumulateClassNames.initializeFromDisk(onDiskBuildInfo);
+ verifyAll();
+
+ ImmutableSortedMap<String, HashCode> observedClasses = accumulateClassNames.getClassNames();
+ assertEquals(
+ "initializeFromDisk() should read the lines and use them to create an ImmutableSortedMap.",
+ ImmutableSortedMap.of(
+ "com/example/Bar", HashCode.fromString("087b7707a5f8e0a2adf5652e3cd2072d89a197dc"),
+ "com/example/Baz", HashCode.fromString("62b1c2510840c0de55c13f66065a98a719be0f19"),
+ "com/example/Foo", HashCode.fromString("e4fccb7520b7795e632651323c63217c9f59f72a")
+ ),
+ observedClasses);
+ }
}
diff --git a/test/com/facebook/buck/rules/OnDiskBuildInfoTest.java b/test/com/facebook/buck/rules/OnDiskBuildInfoTest.java
new file mode 100644
index 0000000..6c9d0ee
--- /dev/null
+++ b/test/com/facebook/buck/rules/OnDiskBuildInfoTest.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2013-present Facebook, Inc.
+ *
+ * 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.facebook.buck.rules;
+
+import static org.easymock.EasyMock.expect;
+import static org.junit.Assert.assertEquals;
+
+import com.facebook.buck.model.BuildTarget;
+import com.facebook.buck.util.ProjectFilesystem;
+import com.google.common.collect.ImmutableList;
+
+import org.easymock.EasyMockSupport;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.nio.file.Paths;
+import java.util.List;
+
+public class OnDiskBuildInfoTest extends EasyMockSupport {
+
+ @Test
+ public void testGetOutputFileContentsByLine() throws IOException {
+ String pathToOutputFile = "buck-out/gen/java/com/example/classes.txt";
+
+ ProjectFilesystem projectFilesystem = createMock(ProjectFilesystem.class);
+ List<String> lines = ImmutableList.of(
+ "com/example/Bar.class 087b7707a5f8e0a2adf5652e3cd2072d89a197dc",
+ "com/example/Foo.class e4fccb7520b7795e632651323c63217c9f59f72a");
+ expect(projectFilesystem.readLines(Paths.get(pathToOutputFile))).andReturn(lines);
+
+ Buildable buildable = createMock(Buildable.class);
+ expect(buildable.getPathToOutputFile()).andReturn(pathToOutputFile);
+
+ replayAll();
+
+ OnDiskBuildInfo onDiskBuildInfo = new OnDiskBuildInfo(
+ new BuildTarget("//java/com/example", "ex"),
+ projectFilesystem);
+ List<String> observedLines = onDiskBuildInfo.getOutputFileContentsByLine(buildable);
+ assertEquals(lines, observedLines);
+
+ verifyAll();
+ }
+
+ @Test(expected = NullPointerException.class)
+ public void testGetOutputFileContentsByLineRejectsNullBuildable() throws IOException {
+ ProjectFilesystem projectFilesystem = createMock(ProjectFilesystem.class);
+ OnDiskBuildInfo onDiskBuildInfo = new OnDiskBuildInfo(
+ new BuildTarget("//java/com/example", "ex"),
+ projectFilesystem);
+
+ replayAll();
+
+ onDiskBuildInfo.getOutputFileContentsByLine(null);
+ }
+
+ @Test(expected = NullPointerException.class)
+ public void testGetOutputFileContentsByLineRejectsNullOutputPath() throws IOException {
+ ProjectFilesystem projectFilesystem = createMock(ProjectFilesystem.class);
+ OnDiskBuildInfo onDiskBuildInfo = new OnDiskBuildInfo(
+ new BuildTarget("//java/com/example", "ex"),
+ projectFilesystem);
+
+ Buildable buildable = createMock(Buildable.class);
+ expect(buildable.getPathToOutputFile()).andReturn(null);
+
+ replayAll();
+
+ onDiskBuildInfo.getOutputFileContentsByLine(buildable);
+ }
+
+}