Introduce AccumulateClassNames rule to produce the list of .class files in a Java artifact. Summary: This is needed for the upcoming pre-dexing diff. Test Plan: Sandcastle builds.
diff --git a/src/com/facebook/buck/java/AccumulateClassNames.java b/src/com/facebook/buck/java/AccumulateClassNames.java new file mode 100644 index 0000000..880012a --- /dev/null +++ b/src/com/facebook/buck/java/AccumulateClassNames.java
@@ -0,0 +1,117 @@ +/* + * 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.java; + +import com.facebook.buck.model.BuildTarget; +import com.facebook.buck.rules.AbstractBuildRuleBuilderParams; +import com.facebook.buck.rules.AbstractBuildable; +import com.facebook.buck.rules.BuildContext; +import com.facebook.buck.rules.BuildRuleParams; +import com.facebook.buck.rules.BuildRuleResolver; +import com.facebook.buck.rules.BuildRuleType; +import com.facebook.buck.rules.Buildable; +import com.facebook.buck.rules.BuildableContext; +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.base.Preconditions; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSortedSet; + +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; + +/** + * {@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 final JavaLibraryRule javaLibraryRule; + private final Path pathToOutputFile; + + private AccumulateClassNames(BuildTarget buildTarget, JavaLibraryRule javaLibraryRule) { + Preconditions.checkNotNull(buildTarget); + this.javaLibraryRule = Preconditions.checkNotNull(javaLibraryRule); + this.pathToOutputFile = Paths.get( + BuckConstant.GEN_DIR, + buildTarget.getBasePath(), + buildTarget.getShortName() + ".classes.txt"); + } + + @Override + public Iterable<String> getInputsToCompareToOutput() { + // The deps of this rule already capture all of the inputs that should affect the cache key. + return ImmutableSortedSet.of(); + } + + @Override + public String getPathToOutputFile() { + return pathToOutputFile.toString(); + } + + @Override + public List<Step> getBuildSteps(BuildContext context, BuildableContext buildableContext) + throws IOException { + ImmutableList.Builder<Step> steps = ImmutableList.builder(); + + steps.add(new RmStep(getPathToOutputFile(), /* shouldForceDeletion */ true)); + + // Make sure that the output directory exists for the output file. + steps.add(new MkdirStep(pathToOutputFile.getParent())); + + steps.add(new AccumulateClassNamesStep( + Paths.get(javaLibraryRule.getPathToOutputFile()), + Paths.get(getPathToOutputFile()))); + + return steps.build(); + } + + public static Builder newAccumulateClassNamesBuilder(AbstractBuildRuleBuilderParams params) { + return new Builder(params); + } + + public static class Builder extends AbstractBuildable.Builder { + + private JavaLibraryRule javaLibraryRule; + + private Builder(AbstractBuildRuleBuilderParams params) { + super(params); + } + + @Override + protected BuildRuleType getType() { + return BuildRuleType._CLASS_NAMES; + } + + @Override + protected AccumulateClassNames newBuildable(BuildRuleParams params, + BuildRuleResolver resolver) { + return new AccumulateClassNames(params.getBuildTarget(), javaLibraryRule); + } + + public Builder setJavaLibraryToDex(JavaLibraryRule javaLibraryRule) { + this.javaLibraryRule = javaLibraryRule; + this.addDep(javaLibraryRule.getBuildTarget()); + return this; + } + + } +}
diff --git a/src/com/facebook/buck/java/AccumulateClassNamesStep.java b/src/com/facebook/buck/java/AccumulateClassNamesStep.java new file mode 100644 index 0000000..0eb7c2f --- /dev/null +++ b/src/com/facebook/buck/java/AccumulateClassNamesStep.java
@@ -0,0 +1,93 @@ +/* + * 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.java; + +import com.facebook.buck.event.ThrowableLogEvent; +import com.facebook.buck.java.classes.ClasspathTraversal; +import com.facebook.buck.java.classes.DefaultClasspathTraverser; +import com.facebook.buck.java.classes.FileLike; +import com.facebook.buck.step.AbstractExecutionStep; +import com.facebook.buck.step.ExecutionContext; +import com.facebook.buck.step.Step; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableSortedSet; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Collections; + +/** + * {@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 { + + /** + * Entries in the {@link #pathToJarOrClassesDirectory} that end with this suffix will be written + * to {@link #whereClassNamesShouldBeWritten}. Since they all share the same suffix, the suffix + * will be stripped when written to {@link #whereClassNamesShouldBeWritten}. + */ + private static final String CLASS_NAME_SUFFIX = ".class"; + + private final Path pathToJarOrClassesDirectory; + private final Path whereClassNamesShouldBeWritten; + + public AccumulateClassNamesStep(Path pathToJarOrClassesDirectory, + Path whereClassNamesShouldBeWritten) { + super("get_class_names " + pathToJarOrClassesDirectory + " > " + whereClassNamesShouldBeWritten); + this.pathToJarOrClassesDirectory = Preconditions.checkNotNull(pathToJarOrClassesDirectory); + this.whereClassNamesShouldBeWritten = Preconditions.checkNotNull(whereClassNamesShouldBeWritten); + } + + @Override + public int execute(ExecutionContext context) { + final ImmutableSortedSet.Builder<String> classNamesBuilder = ImmutableSortedSet.naturalOrder(); + Path path = context.getProjectFilesystem().resolve(pathToJarOrClassesDirectory); + ClasspathTraversal traversal = new ClasspathTraversal(Collections.singleton(path)) { + @Override + public void visit(FileLike fileLike) throws IOException { + String name = fileLike.getRelativePath(); + + // When traversing a JAR file, it may have resources or directory entries that do not end + // in .class, which should be ignored. + if (name.endsWith(CLASS_NAME_SUFFIX)) { + classNamesBuilder.add(name.substring(0, name.length() - CLASS_NAME_SUFFIX.length())); + } + } + }; + + try { + new DefaultClasspathTraverser().traverse(traversal); + } catch (IOException e) { + e.printStackTrace(context.getStdErr()); + return 1; + } + + ImmutableSortedSet<String> classNames = classNamesBuilder.build(); + try { + context.getProjectFilesystem().writeLinesToPath(classNames, whereClassNamesShouldBeWritten); + } catch (IOException e) { + context.getBuckEventBus().post(ThrowableLogEvent.create(e, + "There was an error writing the list of .class files to %s.", + whereClassNamesShouldBeWritten)); + return 1; + } + + return 0; + } + +}
diff --git a/src/com/facebook/buck/java/BUCK b/src/com/facebook/buck/java/BUCK index 909d9eb..3adf317 100644 --- a/src/com/facebook/buck/java/BUCK +++ b/src/com/facebook/buck/java/BUCK
@@ -26,6 +26,7 @@ java_library( name = 'rules', srcs = [ + 'AccumulateClassNames.java', 'Classpaths.java', 'DefaultJavaLibraryRule.java', 'HasJavaSrcs.java', @@ -68,6 +69,7 @@ java_library( name = 'steps', srcs = [ + 'AccumulateClassNamesStep.java', 'GenerateCodeCoverageReportStep.java', 'InstrumentStep.java', 'JarDirectoryStep.java', @@ -81,6 +83,7 @@ '//lib:jsr305', '//src/com/facebook/buck/android:exceptions', '//src/com/facebook/buck/java/abi:protocol', + '//src/com/facebook/buck/java/classes:classes', '//src/com/facebook/buck/event:event', '//src/com/facebook/buck/model:model', '//src/com/facebook/buck/rules:build_rule',
diff --git a/src/com/facebook/buck/rules/BuildRuleType.java b/src/com/facebook/buck/rules/BuildRuleType.java index e85c016..385eba5 100644 --- a/src/com/facebook/buck/rules/BuildRuleType.java +++ b/src/com/facebook/buck/rules/BuildRuleType.java
@@ -44,6 +44,9 @@ public static BuildRuleType SH_BINARY = new BuildRuleType("sh_binary"); public static BuildRuleType SH_TEST = new BuildRuleType("sh_test"); + // Internal rule types. Denoted by leading trailing underscore. + public static BuildRuleType _CLASS_NAMES = new BuildRuleType("_class_names"); + private final String name; private final boolean isTestRule;
diff --git a/test/com/facebook/buck/java/AccumulateClassNamesStepTest.java b/test/com/facebook/buck/java/AccumulateClassNamesStepTest.java new file mode 100644 index 0000000..85e46b8 --- /dev/null +++ b/test/com/facebook/buck/java/AccumulateClassNamesStepTest.java
@@ -0,0 +1,115 @@ +/* + * 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.java; + +import static org.junit.Assert.assertEquals; + +import com.facebook.buck.step.ExecutionContext; +import com.facebook.buck.step.TestExecutionContext; +import com.facebook.buck.util.ProjectFilesystem; +import com.google.common.base.Charsets; +import com.google.common.base.Joiner; +import com.google.common.io.Files; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Paths; +import java.util.jar.JarOutputStream; +import java.util.zip.ZipEntry; + +public class AccumulateClassNamesStepTest { + + @Rule + public TemporaryFolder tmp = new TemporaryFolder(); + + @Test + public void testExecuteAccumulateClassNamesStepOnJarFile() throws IOException { + // Create a JAR file. + String name = "example.jar"; + File jarFile = tmp.newFile(name); + try (JarOutputStream out = new JarOutputStream( + new BufferedOutputStream( + new FileOutputStream(jarFile)))) { + out.putNextEntry(new ZipEntry("com/example/Foo.class")); + out.closeEntry(); + out.putNextEntry(new ZipEntry("com/example/Bar.class")); + out.closeEntry(); + out.putNextEntry(new ZipEntry("com/example/not_a_class.png")); + out.closeEntry(); + out.putNextEntry(new ZipEntry("com/example/subpackage/Baz.class")); + out.closeEntry(); + } + + // Create the AccumulateClassNamesStep and execute it. + AccumulateClassNamesStep accumulateClassNamesStep = new AccumulateClassNamesStep( + Paths.get(name), Paths.get("output.txt")); + ExecutionContext context = TestExecutionContext + .newBuilder() + .setProjectFilesystem(new ProjectFilesystem(tmp.getRoot())) + .build(); + accumulateClassNamesStep.execute(context); + + String contents = Files.toString(new File(tmp.getRoot(), "output.txt"), Charsets.UTF_8); + assertEquals( + "Verify that the contents are sorted alphabetically and ignore non-.class files.", + Joiner.on('\n').join( + "com/example/Bar", + "com/example/Foo", + "com/example/subpackage/Baz") + '\n', + contents); + } + + @Test + public void testExecuteAccumulateClassNamesStepOnDirectory() throws IOException { + // Create a directory. + String name = "dir"; + tmp.newFolder(name); + + tmp.newFolder("dir/com"); + tmp.newFolder("dir/com/example"); + tmp.newFolder("dir/com/example/subpackage"); + + tmp.newFile("dir/com/example/Foo.class"); + tmp.newFile("dir/com/example/Bar.class"); + tmp.newFile("dir/com/example/not_a_class.png"); + tmp.newFile("dir/com/example/subpackage/Baz.class"); + + // Create the AccumulateClassNamesStep and execute it. + AccumulateClassNamesStep accumulateClassNamesStep = new AccumulateClassNamesStep( + Paths.get(name), Paths.get("output.txt")); + ExecutionContext context = TestExecutionContext + .newBuilder() + .setProjectFilesystem(new ProjectFilesystem(tmp.getRoot())) + .build(); + accumulateClassNamesStep.execute(context); + + String contents = Files.toString(new File(tmp.getRoot(), "output.txt"), Charsets.UTF_8); + assertEquals( + "Verify that the contents are sorted alphabetically and ignore non-.class files.", + Joiner.on('\n').join( + "com/example/Bar", + "com/example/Foo", + "com/example/subpackage/Baz") + '\n', + contents); + } +}
diff --git a/test/com/facebook/buck/java/AccumulateClassNamesTest.java b/test/com/facebook/buck/java/AccumulateClassNamesTest.java new file mode 100644 index 0000000..f04c5d2 --- /dev/null +++ b/test/com/facebook/buck/java/AccumulateClassNamesTest.java
@@ -0,0 +1,103 @@ +/* + * 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.java; + +import static org.junit.Assert.assertEquals; + +import com.facebook.buck.model.BuildTarget; +import com.facebook.buck.rules.AbstractBuildRuleBuilderParams; +import com.facebook.buck.rules.BuildContext; +import com.facebook.buck.rules.BuildRule; +import com.facebook.buck.rules.BuildRuleResolver; +import com.facebook.buck.rules.BuildRuleType; +import com.facebook.buck.rules.BuildableContext; +import com.facebook.buck.rules.FakeAbstractBuildRuleBuilderParams; +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.ImmutableSortedSet; + +import org.easymock.EasyMockSupport; +import org.junit.Test; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; + +public class AccumulateClassNamesTest extends EasyMockSupport { + + @Test + public void testObserversForAccumulateClassNames() throws IOException { + // Create a JavaLibraryRule with classes to accumulate. + BuildRuleResolver buildRuleResolver = new BuildRuleResolver(); + JavaLibraryRule javaLibraryRule = buildRuleResolver.buildAndAddToIndex(PrebuiltJarRule + .newPrebuiltJarRuleBuilder(new FakeAbstractBuildRuleBuilderParams()) + .setBuildTarget(new BuildTarget("//foo/bar", "jar")) + .setBinaryJar("foo/bar/example.jar")); + + // Create the Builder. + AbstractBuildRuleBuilderParams params = new FakeAbstractBuildRuleBuilderParams(); + AccumulateClassNames.Builder builder = AccumulateClassNames.newAccumulateClassNamesBuilder( + params); + assertEquals(BuildRuleType._CLASS_NAMES, builder.getType()); + + + // Construct the Buildable. + BuildTarget buildTarget = new BuildTarget("//foo/bar", "baz", "class_names"); + builder.setBuildTarget(buildTarget); + builder.setJavaLibraryToDex(javaLibraryRule); + BuildRule buildRule = buildRuleResolver.buildAndAddToIndex(builder); + AccumulateClassNames accumulateClassNames = (AccumulateClassNames) buildRule.getBuildable(); + + // Test the observers. + String pathToOutput = "buck-out/gen/foo/bar/baz#class_names.classes.txt"; + assertEquals(pathToOutput, + accumulateClassNames.getPathToOutputFile()); + assertEquals("There should not be any input files that factor into the cache key.", + ImmutableSortedSet.of(), + accumulateClassNames.getInputsToCompareToOutput()); + + // Mock out objects so getBuildSteps() can be invoked. + BuildContext buildContext = createMock(BuildContext.class); + BuildableContext buildableContext = createMock(BuildableContext.class); + + // Create the build steps. + replayAll(); + List<Step> steps = accumulateClassNames.getBuildSteps(buildContext, buildableContext); + verifyAll(); + + // Verify the build steps. + ProjectFilesystem projectFilesystem = new ProjectFilesystem(new File(".")); + ExecutionContext context = TestExecutionContext + .newBuilder() + .setProjectFilesystem(projectFilesystem) + .build(); + Path absolutePathToOutput = projectFilesystem.resolve(Paths.get(pathToOutput)); + MoreAsserts.assertSteps("Delete old classes.txt file, if present, and then write a new one.", + ImmutableList.of( + "rm -f " + absolutePathToOutput, + "mkdir -p " + absolutePathToOutput.getParent(), + "get_class_names foo/bar/example.jar > " + pathToOutput), + steps, + context); + } +}