ASwB aspect: parse java packages during execution -- MOS_MIGRATED_REVID=109305952
diff --git a/src/BUILD b/src/BUILD index 5f5f348..70aad12 100644 --- a/src/BUILD +++ b/src/BUILD
@@ -74,6 +74,7 @@ "//third_party:srcs", "//src/java_tools/buildjar/java/com/google/devtools/build/buildjar/jarhelper:srcs", "//src/tools/android/java/com/google/devtools/build/android:embedded_tools", + "//src/tools/android/java/com/google/devtools/build/android/ideinfo:embedded_tools", "//src/tools/android/java/com/google/devtools/build/android/idlclass:embedded_tools", "//src/tools/android/java/com/google/devtools/build/android/incrementaldeployment:srcs", "//src/tools/android/java/com/google/devtools/build/android/ziputils:embedded_tools",
diff --git a/src/main/java/com/google/devtools/build/lib/BUILD b/src/main/java/com/google/devtools/build/lib/BUILD index 4691a7a..c4c0435 100644 --- a/src/main/java/com/google/devtools/build/lib/BUILD +++ b/src/main/java/com/google/devtools/build/lib/BUILD
@@ -709,6 +709,7 @@ ":android-rules", ":build-base", ":collect", + ":common", ":concurrent", ":java-rules", ":packages-internal",
diff --git a/src/main/java/com/google/devtools/build/lib/actions/Artifact.java b/src/main/java/com/google/devtools/build/lib/actions/Artifact.java index 4857c21..bdf4639 100644 --- a/src/main/java/com/google/devtools/build/lib/actions/Artifact.java +++ b/src/main/java/com/google/devtools/build/lib/actions/Artifact.java
@@ -486,6 +486,14 @@ } }; + public static final Function<Artifact, String> ABSOLUTE_PATH_STRING = + new Function<Artifact, String>() { + @Override + public String apply(Artifact artifact) { + return artifact.getPath().getPathString(); + } + }; + /** * Converts a collection of artifacts into execution-time path strings, and * adds those to a given collection. Middleman artifacts are ignored by this @@ -510,6 +518,16 @@ } /** + * Lazily converts artifacts into absolute path strings. Middleman artifacts are ignored by + * this method. + */ + public static Iterable<String> toAbsolutePaths(Iterable<Artifact> artifacts) { + return Iterables.transform( + Iterables.filter(artifacts, MIDDLEMAN_FILTER), + ABSOLUTE_PATH_STRING); + } + + /** * Lazily converts artifacts into root-relative path strings. Middleman artifacts are ignored by * this method. */
diff --git a/src/main/java/com/google/devtools/build/lib/ideinfo/AndroidStudioInfoAspect.java b/src/main/java/com/google/devtools/build/lib/ideinfo/AndroidStudioInfoAspect.java index 0053bf5..cdb90eb 100644 --- a/src/main/java/com/google/devtools/build/lib/ideinfo/AndroidStudioInfoAspect.java +++ b/src/main/java/com/google/devtools/build/lib/ideinfo/AndroidStudioInfoAspect.java
@@ -15,12 +15,20 @@ package com.google.devtools.build.lib.ideinfo; import static com.google.common.collect.Iterables.transform; +import static com.google.devtools.build.lib.packages.Attribute.ConfigurationTransition.HOST; +import static com.google.devtools.build.lib.packages.Attribute.attr; +import static com.google.devtools.build.lib.packages.BuildType.LABEL; import com.google.common.base.Function; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Lists; import com.google.common.io.ByteSource; +import com.google.devtools.build.lib.Constants; +import com.google.devtools.build.lib.actions.Action; import com.google.devtools.build.lib.actions.ActionOwner; import com.google.devtools.build.lib.actions.Artifact; +import com.google.devtools.build.lib.actions.ParameterFile.ParameterFileType; import com.google.devtools.build.lib.actions.Root; import com.google.devtools.build.lib.analysis.AnalysisUtils; import com.google.devtools.build.lib.analysis.ConfiguredAspect; @@ -31,7 +39,9 @@ import com.google.devtools.build.lib.analysis.RuleContext; import com.google.devtools.build.lib.analysis.TransitiveInfoCollection; import com.google.devtools.build.lib.analysis.actions.BinaryFileWriteAction; +import com.google.devtools.build.lib.analysis.actions.CustomCommandLine; import com.google.devtools.build.lib.analysis.actions.FileWriteAction; +import com.google.devtools.build.lib.analysis.actions.SpawnAction; import com.google.devtools.build.lib.analysis.config.BuildConfiguration; import com.google.devtools.build.lib.cmdline.Label; import com.google.devtools.build.lib.collect.nestedset.NestedSet; @@ -66,6 +76,7 @@ import java.io.InputStream; import java.util.Collection; import java.util.List; +import java.util.Set; import javax.annotation.Nullable; @@ -108,10 +119,23 @@ } }; + /** White-list for rules potentially having .java srcs */ + private static final Set<Kind> JAVA_SRC_RULES = ImmutableSet.of( + Kind.JAVA_LIBRARY, + Kind.JAVA_TEST, + Kind.JAVA_BINARY, + Kind.ANDROID_LIBRARY, + Kind.ANDROID_BINARY, + Kind.ANDROID_TEST, + Kind.ANDROID_ROBOELECTRIC_TEST); + @Override public AspectDefinition getDefinition(AspectParameters aspectParameters) { AspectDefinition.Builder builder = new AspectDefinition.Builder(NAME) - .attributeAspect("runtime_deps", AndroidStudioInfoAspect.class); + .attributeAspect("runtime_deps", AndroidStudioInfoAspect.class) + .add(attr("$packageParser", LABEL).cfg(HOST).exec() + .value(Label.parseAbsoluteUnchecked( + Constants.TOOLS_REPOSITORY + "//tools/android:PackageParser"))); for (PrerequisiteAttr prerequisiteAttr : PREREQUISITE_ATTRS) { builder.attributeAspect(prerequisiteAttr.name, AndroidStudioInfoAspect.class); @@ -233,11 +257,14 @@ NestedSet<Label> directDependencies, AndroidStudioInfoFilesProvider.Builder providerBuilder) { - Artifact ideInfoFile = ideInfoArtifact(base, ruleContext, ASWB_BUILD_SUFFIX); - Artifact ideInfoTextFile = ideInfoArtifact(base, ruleContext, ASWB_BUILD_TEXT_SUFFIX); - + Artifact ideInfoFile = derivedArtifact(base, ruleContext, ASWB_BUILD_SUFFIX); + Artifact ideInfoTextFile = derivedArtifact(base, ruleContext, ASWB_BUILD_TEXT_SUFFIX); + Artifact packageManifest = createPackageManifest(base, ruleContext, ruleKind); providerBuilder.ideInfoFilesBuilder().add(ideInfoFile); providerBuilder.ideInfoTextFilesBuilder().add(ideInfoTextFile); + if (packageManifest != null) { + providerBuilder.ideInfoFilesBuilder().add(packageManifest); + } NestedSetBuilder<Artifact> ideResolveArtifacts = providerBuilder.ideResolveFilesBuilder(); RuleIdeInfo.Builder outputBuilder = RuleIdeInfo.newBuilder(); @@ -263,7 +290,9 @@ || ruleKind == Kind.ANDROID_TEST || ruleKind == Kind.ANDROID_ROBOELECTRIC_TEST || ruleKind == Kind.PROTO_LIBRARY) { - outputBuilder.setJavaRuleIdeInfo(makeJavaRuleIdeInfo(base, ruleContext, ideResolveArtifacts)); + JavaRuleIdeInfo javaRuleIdeInfo = makeJavaRuleIdeInfo( + base, ruleContext, ideResolveArtifacts, packageManifest); + outputBuilder.setJavaRuleIdeInfo(javaRuleIdeInfo); } if (ruleKind == Kind.ANDROID_LIBRARY || ruleKind == Kind.ANDROID_BINARY @@ -287,21 +316,58 @@ makeProtoWriteAction(ruleContext.getActionOwner(), ruleIdeInfo, ideInfoFile)); ruleContext.registerAction( makeProtoTextWriteAction(ruleContext.getActionOwner(), ruleIdeInfo, ideInfoTextFile)); + if (packageManifest != null) { + ruleContext.registerAction( + makePackageManifestAction(ruleContext, packageManifest, getJavaSources(ruleContext)) + ); + } return provider; } - private static Artifact ideInfoArtifact(ConfiguredTarget base, RuleContext ruleContext, + @Nullable private static Artifact createPackageManifest(ConfiguredTarget base, + RuleContext ruleContext, Kind ruleKind) { + if (!JAVA_SRC_RULES.contains(ruleKind)) { + return null; + } + Collection<Artifact> sourceFiles = getJavaSources(ruleContext); + if (sourceFiles.isEmpty()) { + return null; + } + return derivedArtifact(base, ruleContext, ".manifest"); + } + + private static Action[] makePackageManifestAction( + RuleContext ruleContext, + Artifact packageManifest, + Collection<Artifact> sourceFiles) { + + return new SpawnAction.Builder() + .addInputs(sourceFiles) + .addOutput(packageManifest) + .setExecutable(ruleContext.getExecutablePrerequisite("$packageParser", Mode.HOST)) + .setCommandLine(CustomCommandLine.builder() + .addExecPath("--output_manifest", packageManifest) + .addJoinStrings("--sources_absolute_paths", ":", Artifact.toAbsolutePaths(sourceFiles)) + .addJoinExecPaths("--sources_execution_paths", ":", sourceFiles) + .build()) + .useParameterFile(ParameterFileType.SHELL_QUOTED) + .setProgressMessage("Parsing java package strings for " + ruleContext.getRule()) + .setMnemonic("JavaPackageManifest") + .build(ruleContext); + } + + private static Artifact derivedArtifact(ConfiguredTarget base, RuleContext ruleContext, String suffix) { BuildConfiguration configuration = ruleContext.getConfiguration(); assert configuration != null; Root genfilesDirectory = configuration.getGenfilesDirectory(); - PathFragment ideBuildFilePath = + PathFragment derivedFilePath = getOutputFilePath(base, ruleContext, suffix); return ruleContext.getAnalysisEnvironment().getDerivedArtifact( - ideBuildFilePath, genfilesDirectory); + derivedFilePath, genfilesDirectory); } private static AndroidRuleIdeInfo makeAndroidRuleIdeInfo( @@ -401,7 +467,8 @@ private static JavaRuleIdeInfo makeJavaRuleIdeInfo( ConfiguredTarget base, RuleContext ruleContext, - NestedSetBuilder<Artifact> ideResolveArtifacts) { + NestedSetBuilder<Artifact> ideResolveArtifacts, + @Nullable Artifact packageManifest) { JavaRuleIdeInfo.Builder builder = JavaRuleIdeInfo.newBuilder(); JavaRuleOutputJarsProvider outputJarsProvider = base.getProvider(JavaRuleOutputJarsProvider.class); @@ -428,6 +495,10 @@ builder.addSources(makeArtifactLocation(sourceFile)); } + if (packageManifest != null) { + builder.setPackageManifest(makeArtifactLocation(packageManifest)); + } + return builder.build(); } @@ -522,6 +593,17 @@ } } + private static Collection<Artifact> getJavaSources(RuleContext ruleContext) { + Collection<Artifact> srcs = getSources(ruleContext); + List<Artifact> javaSrcs = Lists.newArrayList(); + for (Artifact src : srcs) { + if (src.getRootRelativePathString().endsWith(".java")) { + javaSrcs.add(src); + } + } + return javaSrcs; + } + private static Collection<Artifact> getSources(RuleContext ruleContext) { return ruleContext.attributes().has("srcs", BuildType.LABEL_LIST) ? ruleContext.getPrerequisiteArtifacts("srcs", Mode.TARGET).list()
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/PathFragment.java b/src/main/java/com/google/devtools/build/lib/vfs/PathFragment.java index 97221ab..e98f815 100644 --- a/src/main/java/com/google/devtools/build/lib/vfs/PathFragment.java +++ b/src/main/java/com/google/devtools/build/lib/vfs/PathFragment.java
@@ -43,7 +43,7 @@ * with advanced features like \\\\network\\paths and \\\\?\\unc\\paths. */ @Immutable @ThreadSafe -public final class PathFragment implements Comparable<PathFragment>, Serializable { +public final class PathFragment implements Comparable<PathFragment>, Serializable { public static final int INVALID_SEGMENT = -1;
diff --git a/src/main/protobuf/BUILD b/src/main/protobuf/BUILD index 6254b6a..7bff28a 100644 --- a/src/main/protobuf/BUILD +++ b/src/main/protobuf/BUILD
@@ -10,6 +10,7 @@ "crosstool_config", "extra_actions_base", "android_studio_ide_info", + "package_manifest", "test_status", "bundlemerge", "xcodegen",
diff --git a/src/main/protobuf/package_manifest.proto b/src/main/protobuf/package_manifest.proto new file mode 100644 index 0000000..dd5f4fc --- /dev/null +++ b/src/main/protobuf/package_manifest.proto
@@ -0,0 +1,31 @@ +// Copyright 2015 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. + +syntax = "proto3"; + +package blaze; + + +option java_package = "com.google.devtools.build.lib.ideinfo.androidstudio"; + +option java_generate_equals_and_hash = true; + +message JavaSourcePackage { + string absolute_path = 1; + string package_string = 2; +} + +message PackageManifest { + repeated JavaSourcePackage sources = 1; +}
diff --git a/src/test/java/com/google/devtools/build/android/ideinfo/BUILD b/src/test/java/com/google/devtools/build/android/ideinfo/BUILD new file mode 100644 index 0000000..79f75dd --- /dev/null +++ b/src/test/java/com/google/devtools/build/android/ideinfo/BUILD
@@ -0,0 +1,14 @@ +java_test( + name = "PackageParserTest", + size = "small", + srcs = glob(["*.java"]), + deps = [ + "//src/main/protobuf:package_manifest_proto", + "//src/tools/android/java/com/google/devtools/build/android/ideinfo:package_parser_lib", + "//third_party:guava", + "//third_party:jsr305", + "//third_party:junit4", + "//third_party:protobuf", + "//third_party:truth", + ], +)
diff --git a/src/test/java/com/google/devtools/build/android/ideinfo/PackageParserTest.java b/src/test/java/com/google/devtools/build/android/ideinfo/PackageParserTest.java new file mode 100644 index 0000000..a02ac30 --- /dev/null +++ b/src/test/java/com/google/devtools/build/android/ideinfo/PackageParserTest.java
@@ -0,0 +1,192 @@ +// Copyright 2015 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.android.ideinfo; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.protobuf.MessageLite; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.StringWriter; +import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.Map; + +import javax.annotation.Nonnull; + +/** + * Unit tests for {@link PackageParser} + */ +@RunWith(JUnit4.class) +public class PackageParserTest { + + private static class MockPackageParserIoProvider extends PackageParserIoProvider { + private final Map<Path, InputStream> sources = Maps.newHashMap(); + private StringWriter writer = new StringWriter(); + + public MockPackageParserIoProvider addSource(String filePath, String javaSrc) { + try { + sources.put(Paths.get(filePath), new ByteArrayInputStream(javaSrc.getBytes("UTF-8"))); + } catch (UnsupportedEncodingException e) { + fail(e.getMessage()); + } + return this; + } + + public void reset() { + sources.clear(); + writer = new StringWriter(); + } + + public List<Path> getPaths() { + return Lists.newArrayList(sources.keySet()); + } + + @Nonnull + @Override + public BufferedReader getReader(Path file) throws IOException { + InputStream input = sources.get(file); + return new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8)); + } + + @Override + public void writeProto(@Nonnull MessageLite message, @Nonnull Path file) throws IOException { + writer.write(message.toString()); + } + } + + private MockPackageParserIoProvider mockIoProvider; + private PackageParser parser; + + @Before + public void setUp() { + mockIoProvider = new MockPackageParserIoProvider(); + parser = new PackageParser(mockIoProvider); + } + + private Map<Path, String> parsePackageStrings() throws Exception { + List<Path> paths = mockIoProvider.getPaths(); + return parser.parsePackageStrings(paths, paths); + } + + @Test + public void testParseCommandLineArguments() throws Exception { + String[] args = new String[] { + "--output_manifest", + "/tmp/out.manifest", + "--sources_absolute_paths", + "/path/test1.java:/path/test2.java", + "--sources_execution_paths", + "/path/test1.java:/path/test2.java" + }; + PackageParser.PackageParserOptions options = PackageParser.parseArgs(args); + assertThat(options.outputManifest.toString()).isEqualTo("/tmp/out.manifest"); + assertThat(options.sourcesAbsolutePaths).hasSize(2); + assertThat(options.sourcesExecutionPaths).hasSize(2); + assertThat(options.sourcesAbsolutePaths.get(0).toString()).isEqualTo("/path/test1.java"); + assertThat(options.sourcesAbsolutePaths.get(1).toString()).isEqualTo("/path/test2.java"); + } + + @Test + public void testReadNoSources() throws Exception { + Map<Path, String> map = parsePackageStrings(); + assertThat(map).isEmpty(); + } + + @Test + public void testSingleRead() throws Exception { + mockIoProvider + .addSource("java/com/google/Bla.java", + "package com.test;\n public class Bla {}\""); + Map<Path, String> map = parsePackageStrings(); + assertThat(map).hasSize(1); + assertThat(map).containsEntry(Paths.get("java/com/google/Bla.java"), "com.test"); + } + + @Test + public void testMultiRead() throws Exception { + mockIoProvider + .addSource("java/com/google/Bla.java", + "package com.test;\n public class Bla {}\"") + .addSource("java/com/other/Foo.java", + "package com.other;\n public class Foo {}\""); + Map<Path, String> map = parsePackageStrings(); + assertThat(map).hasSize(2); + assertThat(map).containsEntry(Paths.get("java/com/google/Bla.java"), "com.test"); + assertThat(map).containsEntry(Paths.get("java/com/other/Foo.java"), "com.other"); + } + + @Test + public void testReadSomeInvalid() throws Exception { + mockIoProvider + .addSource("java/com/google/Bla.java", + "package %com.test;\n public class Bla {}\"") + .addSource("java/com/other/Foo.java", + "package com.other;\n public class Foo {}\""); + Map<Path, String> map = parsePackageStrings(); + assertThat(map).hasSize(1); + assertThat(map).containsEntry(Paths.get("java/com/other/Foo.java"), "com.other"); + } + + @Test + public void testReadAllInvalid() throws Exception { + mockIoProvider + .addSource("java/com/google/Bla.java", + "#package com.test;\n public class Bla {}\"") + .addSource("java/com/other/Foo.java", + "package com.other\n public class Foo {}\""); + Map<Path, String> map = parsePackageStrings(); + assertThat(map).isEmpty(); + } + + @Test + public void testWriteEmptyMap() throws Exception { + parser.writeManifest( + Maps.<Path, String> newHashMap(), Paths.get("/java/com/google/test.manifest")); + assertThat(mockIoProvider.writer.toString()).isEmpty(); + } + + @Test + public void testWriteMap() throws Exception { + Map<Path, String> map = ImmutableMap.of( + Paths.get("/java/com/google/Bla.java"), "com.google", + Paths.get("/java/com/other/Foo.java"), "com.other" + ); + parser.writeManifest(map, Paths.get("/java/com/google/test.manifest")); + + String writtenString = mockIoProvider.writer.toString(); + assertThat(writtenString).contains("absolute_path: \"/java/com/google/Bla.java\""); + assertThat(writtenString).contains("package_string: \"com.google\""); + assertThat(writtenString).contains("absolute_path: \"/java/com/other/Foo.java\""); + assertThat(writtenString).contains("package_string: \"com.other\""); + } + +}
diff --git a/src/test/java/com/google/devtools/build/lib/BUILD b/src/test/java/com/google/devtools/build/lib/BUILD index 707b33d..b02e782 100644 --- a/src/test/java/com/google/devtools/build/lib/BUILD +++ b/src/test/java/com/google/devtools/build/lib/BUILD
@@ -600,6 +600,7 @@ "//src/main/java/com/google/devtools/build/lib:bazel-rules", "//src/main/java/com/google/devtools/build/lib:build-base", "//src/main/java/com/google/devtools/build/lib:collect", + "//src/main/java/com/google/devtools/build/lib:common", "//src/main/java/com/google/devtools/build/lib:events", "//src/main/java/com/google/devtools/build/lib:ideinfo", "//src/main/java/com/google/devtools/build/lib:packages",
diff --git a/src/test/java/com/google/devtools/build/lib/analysis/mock/BazelAnalysisMock.java b/src/test/java/com/google/devtools/build/lib/analysis/mock/BazelAnalysisMock.java index 65f4c90..619ba2f 100644 --- a/src/test/java/com/google/devtools/build/lib/analysis/mock/BazelAnalysisMock.java +++ b/src/test/java/com/google/devtools/build/lib/analysis/mock/BazelAnalysisMock.java
@@ -181,7 +181,12 @@ .add("sh_binary(name = 'shuffle_jars', srcs = ['empty.sh'])") .add("sh_binary(name = 'strip_resources', srcs = ['empty.sh'])") .add("sh_binary(name = 'build_incremental_dexmanifest', srcs = ['empty.sh'])") - .add("sh_binary(name = 'incremental_install', srcs = ['empty.sh'])"); + .add("sh_binary(name = 'incremental_install', srcs = ['empty.sh'])") + .add("java_binary(name = 'PackageParser',") + .add(" runtime_deps = [ ':PackageParser_import'],") + .add(" main_class = 'com.google.devtools.build.android.ideinfo.PackageParser')") + .add("java_import(name = 'PackageParser_import',") + .add(" jars = [ 'package_parser_deploy.jar' ])"); for (Attribute attr : attrs) { if (attr.getType() == LABEL && attr.isMandatory() && !attr.getName().startsWith(":")) {
diff --git a/src/test/java/com/google/devtools/build/lib/ideinfo/AndroidStudioInfoAspectTest.java b/src/test/java/com/google/devtools/build/lib/ideinfo/AndroidStudioInfoAspectTest.java index bb8de48..27ec6a6 100644 --- a/src/test/java/com/google/devtools/build/lib/ideinfo/AndroidStudioInfoAspectTest.java +++ b/src/test/java/com/google/devtools/build/lib/ideinfo/AndroidStudioInfoAspectTest.java
@@ -57,7 +57,24 @@ "com/google/example/libsimple-src.jar" ); } - + + public void testPackageManifestCreated() throws Exception { + scratch.file( + "com/google/example/BUILD", + "java_library(", + " name = 'simple',", + " srcs = ['simple/Simple.java']", + ")"); + Map<String, RuleIdeInfo> ruleIdeInfos = buildRuleIdeInfo("//com/google/example:simple"); + assertThat(ruleIdeInfos.size()).isEqualTo(1); + RuleIdeInfo ruleIdeInfo = getRuleInfoAndVerifyLabel( + "//com/google/example:simple", ruleIdeInfos); + + ArtifactLocation packageManifest = ruleIdeInfo.getJavaRuleIdeInfo().getPackageManifest(); + assertNotNull(packageManifest); + assertEquals(packageManifest.getRelativePath(), "com/google/example/simple.manifest"); + } + public void testJavaLibraryProtoWithDependencies() throws Exception { scratch.file( "com/google/example/BUILD", @@ -83,7 +100,7 @@ assertThat(complexRuleIdeInfo.getDependenciesList()) .containsExactly("//com/google/example:simple"); } - + public void testJavaLibraryWithTransitiveDependencies() throws Exception { scratch.file( "com/google/example/BUILD", @@ -320,6 +337,33 @@ .containsExactly("//com/google/example:foobar", "//com/google/example:imp") .inOrder(); } + + public void testNoPackageManifestForExports() throws Exception { + scratch.file( + "com/google/example/BUILD", + "java_library(", + " name = 'foobar',", + " srcs = ['FooBar.java'],", + ")", + "java_import(", + " name = 'imp',", + " jars = ['a.jar', 'b.jar'],", + " deps = [':foobar'],", + " exports = [':foobar'],", + ")", + "java_library(", + " name = 'lib',", + " srcs = ['Lib.java'],", + " deps = [':imp'],", + ")"); + + Map<String, RuleIdeInfo> ruleIdeInfos = buildRuleIdeInfo("//com/google/example:lib"); + RuleIdeInfo libInfo = getRuleInfoAndVerifyLabel("//com/google/example:lib", ruleIdeInfos); + RuleIdeInfo impInfo = getRuleInfoAndVerifyLabel("//com/google/example:imp", ruleIdeInfos); + + assertThat(!impInfo.getJavaRuleIdeInfo().hasPackageManifest()).isTrue(); + assertThat(libInfo.getJavaRuleIdeInfo().hasPackageManifest()).isTrue(); + } public void testGeneratedJavaImportFilesAreAddedToOutputGroup() throws Exception { scratch.file(
diff --git a/src/test/java/com/google/devtools/build/lib/ideinfo/AndroidStudioInfoAspectTestBase.java b/src/test/java/com/google/devtools/build/lib/ideinfo/AndroidStudioInfoAspectTestBase.java index 4a26cc9..cb3ca8b 100644 --- a/src/test/java/com/google/devtools/build/lib/ideinfo/AndroidStudioInfoAspectTestBase.java +++ b/src/test/java/com/google/devtools/build/lib/ideinfo/AndroidStudioInfoAspectTestBase.java
@@ -21,11 +21,13 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; import com.google.common.eventbus.EventBus; +import com.google.devtools.build.lib.actions.Action; import com.google.devtools.build.lib.actions.Artifact; import com.google.devtools.build.lib.analysis.BuildView.AnalysisResult; import com.google.devtools.build.lib.analysis.ConfiguredAspect; import com.google.devtools.build.lib.analysis.OutputGroupProvider; import com.google.devtools.build.lib.analysis.actions.BinaryFileWriteAction; +import com.google.devtools.build.lib.analysis.actions.SpawnAction; import com.google.devtools.build.lib.analysis.util.BuildViewTestCase; import com.google.devtools.build.lib.collect.nestedset.NestedSet; import com.google.devtools.build.lib.ideinfo.androidstudio.AndroidStudioIdeInfo.ArtifactLocation; @@ -136,14 +138,24 @@ Iterable<Artifact> artifacts = provider.getIdeInfoFiles(); ImmutableMap.Builder<String, RuleIdeInfo> builder = ImmutableMap.builder(); for (Artifact artifact : artifacts) { - BinaryFileWriteAction generatingAction = - (BinaryFileWriteAction) getGeneratingAction(artifact); - RuleIdeInfo ruleIdeInfo = RuleIdeInfo.parseFrom(generatingAction.getSource().openStream()); - builder.put(ruleIdeInfo.getLabel(), ruleIdeInfo); + Action generatingAction = getGeneratingAction(artifact); + if (generatingAction instanceof BinaryFileWriteAction) { + BinaryFileWriteAction writeAction = (BinaryFileWriteAction) generatingAction; + RuleIdeInfo ruleIdeInfo = RuleIdeInfo.parseFrom(writeAction.getSource().openStream()); + builder.put(ruleIdeInfo.getLabel(), ruleIdeInfo); + } else { + verifyPackageManifestSpawnAction(generatingAction); + } } return builder.build(); } - + + protected final void verifyPackageManifestSpawnAction(Action genAction) { + assertEquals(genAction.getMnemonic(), "JavaPackageManifest"); + SpawnAction action = (SpawnAction) genAction; + assertFalse(action.isShellCommand()); + } + protected List<String> getOutputGroupResult(String outputGroup) { OutputGroupProvider outputGroupProvider = this.configuredAspect.getProvider(OutputGroupProvider.class);
diff --git a/src/test/shell/bazel/test-setup.sh b/src/test/shell/bazel/test-setup.sh index d5393e7..bf80071 100755 --- a/src/test/shell/bazel/test-setup.sh +++ b/src/test/shell/bazel/test-setup.sh
@@ -120,6 +120,23 @@ data = ["//src/tools/android/java/com/google/devtools/build/android/idlclass:IdlClass"], ) +filegroup( + name = "package_parser", + srcs = ["//src/tools/android/java/com/google/devtools/build/android/ideinfo:PackageParser_deploy.jar"], +) + +java_binary( + name = "PackageParser", + main_class = "com.google.devtools.build.android.ideinfo.PackageParser", + visibility = ["//visibility:public"], + runtime_deps = [":package_parser_import"], +) + +java_import( + name = "package_parser_import", + jars = [":package_parser"], +) + sh_binary( name = "merge_manifests", srcs = ["fail.sh"],
diff --git a/src/tools/android/java/com/google/devtools/build/android/BUILD b/src/tools/android/java/com/google/devtools/build/android/BUILD index 74c1255..9abd062 100644 --- a/src/tools/android/java/com/google/devtools/build/android/BUILD +++ b/src/tools/android/java/com/google/devtools/build/android/BUILD
@@ -37,10 +37,12 @@ srcs = glob(["*.java"]), deps = [ "//src/main/java/com/google/devtools/common/options", + "//src/main/protobuf:package_manifest_proto", "//third_party:android_common", "//third_party:apache_commons_compress", "//third_party:asm", "//third_party:guava", "//third_party:jsr305", + "//third_party:protobuf", ], )
diff --git a/src/tools/android/java/com/google/devtools/build/android/Converters.java b/src/tools/android/java/com/google/devtools/build/android/Converters.java index 8a10c52..f69aac1 100644 --- a/src/tools/android/java/com/google/devtools/build/android/Converters.java +++ b/src/tools/android/java/com/google/devtools/build/android/Converters.java
@@ -26,6 +26,8 @@ import java.nio.file.Files; import java.nio.file.InvalidPathException; import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; /** @@ -154,4 +156,30 @@ super(VariantConfiguration.Type.class, "variant configuration type"); } } + + /** + * Validating converter for a list of Paths. + * A Path is considered valid if it resolves to a file. + */ + public static class PathListConverter implements Converter<List<Path>> { + + final PathConverter baseConverter = new PathConverter(); + + @Override + public List<Path> convert(String input) throws OptionsParsingException { + List<Path> list = new ArrayList<>(); + for (String piece : input.split(":")) { + if (!piece.isEmpty()) { + list.add(baseConverter.convert(piece)); + } + } + return Collections.unmodifiableList(list); + } + + @Override + public String getTypeDescription() { + return "a colon-separated list of paths"; + } + } + }
diff --git a/src/tools/android/java/com/google/devtools/build/android/ideinfo/BUILD b/src/tools/android/java/com/google/devtools/build/android/ideinfo/BUILD new file mode 100644 index 0000000..b520a81 --- /dev/null +++ b/src/tools/android/java/com/google/devtools/build/android/ideinfo/BUILD
@@ -0,0 +1,38 @@ +filegroup( + name = "embedded_tools", + srcs = [ + "BUILD.tools", + "classes_deploy.jar", + ], + visibility = ["//src:__pkg__"], +) + +java_binary( + name = "classes", + main_class = "does.not.exist", + runtime_deps = [":package_parser_lib"], +) + +java_binary( + name = "PackageParser", + main_class = "com.google.devtools.build.android.ideinfo.PackageParser", + visibility = ["//visibility:public"], + runtime_deps = [":package_parser_lib"], +) + +java_library( + name = "package_parser_lib", + srcs = glob(["*.java"]), + visibility = [ + "//devtools/blaze/integration:__pkg__", + "//src/test/java/com/google/devtools/build/android/ideinfo:__pkg__", + ], + deps = [ + "//src/main/java/com/google/devtools/common/options", + "//src/main/protobuf:package_manifest_proto", + "//src/tools/android/java/com/google/devtools/build/android:android_builder_lib", + "//third_party:guava", + "//third_party:jsr305", + "//third_party:protobuf", + ], +)
diff --git a/src/tools/android/java/com/google/devtools/build/android/ideinfo/BUILD.tools b/src/tools/android/java/com/google/devtools/build/android/ideinfo/BUILD.tools new file mode 100644 index 0000000..efa64fb --- /dev/null +++ b/src/tools/android/java/com/google/devtools/build/android/ideinfo/BUILD.tools
@@ -0,0 +1,12 @@ +package(default_visibility = ["//visibility:public"]) + +java_import( + name = "classes", + jars = [":classes_deploy.jar"], +) + +java_binary( + name = "PackageParser", + main_class = "com.google.devtools.build.android.ideinfo.PackageParser", + runtime_deps = [":classes"], +)
diff --git a/src/tools/android/java/com/google/devtools/build/android/ideinfo/PackageParser.java b/src/tools/android/java/com/google/devtools/build/android/ideinfo/PackageParser.java new file mode 100644 index 0000000..5e84d3c --- /dev/null +++ b/src/tools/android/java/com/google/devtools/build/android/ideinfo/PackageParser.java
@@ -0,0 +1,206 @@ +// Copyright 2015 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.android.ideinfo; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import com.google.common.collect.Maps; +import com.google.common.io.Files; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; +import com.google.devtools.build.android.Converters.PathConverter; +import com.google.devtools.build.android.Converters.PathListConverter; +import com.google.devtools.build.lib.ideinfo.androidstudio.PackageManifestOuterClass.JavaSourcePackage; +import com.google.devtools.build.lib.ideinfo.androidstudio.PackageManifestOuterClass.PackageManifest; +import com.google.devtools.common.options.Option; +import com.google.devtools.common.options.OptionsBase; +import com.google.devtools.common.options.OptionsParser; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.Callable; +import java.util.concurrent.Executors; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * Parses the package string from each of the source .java files + */ +public class PackageParser { + + /** The options for a {@PackageParser} action. */ + public static final class PackageParserOptions extends OptionsBase { + @Option(name = "sources_absolute_paths", + defaultValue = "null", + converter = PathListConverter.class, + category = "input", + help = "The absolute paths of the java source files. The expected format is a " + + "colon-separated list.") + public List<Path> sourcesAbsolutePaths; + + @Option(name = "sources_execution_paths", + defaultValue = "null", + converter = PathListConverter.class, + category = "input", + help = "The execution paths of the java source files. The expected format is a " + + "colon-separated list.") + public List<Path> sourcesExecutionPaths; + + @Option(name = "output_manifest", + defaultValue = "null", + converter = PathConverter.class, + category = "output", + help = "The path to the manifest file this parser writes to.") + public Path outputManifest; + } + + private static final Logger logger = Logger.getLogger(PackageParser.class.getName()); + + private static final Pattern JAVA_PACKAGE_PATTERN = + Pattern.compile("^\\s*package\\s+([\\w\\.]+);"); + + public static void main(String[] args) throws Exception { + PackageParserOptions options = parseArgs(args); + Preconditions.checkNotNull(options.sourcesAbsolutePaths); + Preconditions.checkNotNull(options.sourcesExecutionPaths); + Preconditions.checkState( + options.sourcesAbsolutePaths.size() == options.sourcesExecutionPaths.size()); + Preconditions.checkNotNull(options.outputManifest); + + try { + PackageParser parser = new PackageParser(PackageParserIoProvider.INSTANCE); + Map<Path, String> outputMap = parser.parsePackageStrings(options.sourcesAbsolutePaths, + options.sourcesExecutionPaths); + parser.writeManifest(outputMap, options.outputManifest); + } catch (Throwable e) { + logger.log(Level.SEVERE, "Error parsing package strings", e); + System.exit(1); + } + System.exit(0); + } + + @VisibleForTesting + public static PackageParserOptions parseArgs(String[] args) { + args = parseParamFileIfUsed(args); + OptionsParser optionsParser = OptionsParser.newOptionsParser(PackageParserOptions.class); + optionsParser.parseAndExitUponError(args); + return optionsParser.getOptions(PackageParserOptions.class); + } + + private static String[] parseParamFileIfUsed(@Nonnull String[] args) { + if (args.length != 1 || !args[0].startsWith("@")) { + return args; + } + File paramFile = new File(args[0].substring(1)); + try { + return Files.readLines(paramFile, StandardCharsets.UTF_8).toArray(new String[0]); + } catch (IOException e) { + throw new RuntimeException("Error parsing param file: " + args[0], e); + } + } + + private final PackageParserIoProvider ioProvider; + + @VisibleForTesting + public PackageParser(@Nonnull PackageParserIoProvider ioProvider) { + this.ioProvider = ioProvider; + } + + @VisibleForTesting + public void writeManifest(@Nonnull Map<Path, String> sourceToPackageMap, Path outputFile) + throws IOException { + if (sourceToPackageMap.isEmpty()) { + return; + } + PackageManifest.Builder builder = PackageManifest.newBuilder(); + for (Entry<Path, String> entry : sourceToPackageMap.entrySet()) { + builder.addSources(JavaSourcePackage.newBuilder() + .setAbsolutePath(entry.getKey().toAbsolutePath().toString()) + .setPackageString(entry.getValue())); + } + + try { + ioProvider.writeProto(builder.build(), outputFile); + } catch (IOException e) { + logger.log(Level.SEVERE, "Error writing package manifest", e); + throw e; + } + } + + @Nonnull + @VisibleForTesting + public Map<Path, String> parsePackageStrings(@Nonnull List<Path> absolutePaths, + @Nonnull List<Path> executionPaths) throws Exception { + + ListeningExecutorService executorService = MoreExecutors.listeningDecorator( + Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors())); + + Map<Path, ListenableFuture<String>> futures = Maps.newHashMap(); + for (int i = 0; i < absolutePaths.size(); i++) { + final Path source = executionPaths.get(i); + futures.put(absolutePaths.get(i), executorService.submit(new Callable<String>() { + @Override + public String call() throws Exception { + return getDeclaredPackageOfJavaFile(source); + } + })); + } + Map<Path, String> map = Maps.newHashMap(); + for (Entry<Path, ListenableFuture<String>> entry : futures.entrySet()) { + String value = entry.getValue().get(); + if (value != null) { + map.put(entry.getKey(), value); + } + } + return map; + } + + @Nullable + private String getDeclaredPackageOfJavaFile(@Nonnull Path source) { + try (BufferedReader reader = ioProvider.getReader(source)) { + return parseDeclaredPackage(reader); + + } catch (IOException e) { + logger.log(Level.WARNING, "Error parsing package string from java source: " + source, e); + return null; + } + } + + @VisibleForTesting + @Nullable + public static String parseDeclaredPackage(@Nonnull BufferedReader reader) throws IOException { + String line; + while ((line = reader.readLine()) != null) { + Matcher packageMatch = JAVA_PACKAGE_PATTERN.matcher(line); + if (packageMatch.find()) { + return packageMatch.group(1); + } + } + return null; + } + +}
diff --git a/src/tools/android/java/com/google/devtools/build/android/ideinfo/PackageParserIoProvider.java b/src/tools/android/java/com/google/devtools/build/android/ideinfo/PackageParserIoProvider.java new file mode 100644 index 0000000..be555c8 --- /dev/null +++ b/src/tools/android/java/com/google/devtools/build/android/ideinfo/PackageParserIoProvider.java
@@ -0,0 +1,49 @@ +// Copyright 2015 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.android.ideinfo; + +import com.google.common.annotations.VisibleForTesting; +import com.google.protobuf.MessageLite; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + +import javax.annotation.Nonnull; + +/** + * Provides a BufferedReader for the source java files, + * and a writer for the output proto + */ +@VisibleForTesting +public class PackageParserIoProvider { + + public static final PackageParserIoProvider INSTANCE = new PackageParserIoProvider(); + + public void writeProto(@Nonnull MessageLite message, @Nonnull Path file) throws IOException { + try (OutputStream out = Files.newOutputStream(file)) { + message.writeTo(out); + } + } + + @Nonnull + public BufferedReader getReader(Path file) throws IOException { + return Files.newBufferedReader(file, StandardCharsets.UTF_8); + } + +}