/*
* 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.android;
import com.facebook.buck.io.ProjectFilesystem;
import com.facebook.buck.jvm.java.CompileToJarStepFactory;
import com.facebook.buck.jvm.java.HasJavaAbi;
import com.facebook.buck.jvm.java.JarDirectoryStep;
import com.facebook.buck.jvm.java.NoOpClassUsageFileWriter;
import com.facebook.buck.model.BuildTarget;
import com.facebook.buck.model.BuildTargets;
import com.facebook.buck.rules.AbstractBuildRule;
import com.facebook.buck.rules.AddToRuleKey;
import com.facebook.buck.rules.BuildContext;
import com.facebook.buck.rules.BuildOutputInitializer;
import com.facebook.buck.rules.BuildRuleParams;
import com.facebook.buck.rules.BuildableContext;
import com.facebook.buck.rules.ExplicitBuildTargetSourcePath;
import com.facebook.buck.rules.InitializableFromDisk;
import com.facebook.buck.rules.OnDiskBuildInfo;
import com.facebook.buck.rules.SourcePath;
import com.facebook.buck.rules.SourcePathResolver;
import com.facebook.buck.rules.SourcePathRuleFinder;
import com.facebook.buck.rules.keys.SupportsInputBasedRuleKey;
import com.facebook.buck.step.ExecutionContext;
import com.facebook.buck.step.Step;
import com.facebook.buck.step.StepExecutionResult;
import com.facebook.buck.step.fs.MakeCleanDirectoryStep;
import com.facebook.buck.step.fs.MkdirStep;
import com.facebook.buck.step.fs.WriteFileStep;
import com.facebook.buck.util.MoreCollectors;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Collections;
import java.util.Comparator;
import java.util.Optional;
import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
/**
* Buildable that takes in a list of {@link HasAndroidResourceDeps} and for each of these rules,
* first creates an {@code R.java} file using {@link MergeAndroidResourcesStep} and compiles it to
* generate a corresponding {@code R.class} file. These are called "dummy" {@code R.java} files
* since these are later merged together into a single {@code R.java} file by {@link AaptStep}.
*/
public class DummyRDotJava extends AbstractBuildRule
implements SupportsInputBasedRuleKey, InitializableFromDisk<Object>, HasJavaAbi {
private final ImmutableList<HasAndroidResourceDeps> androidResourceDeps;
private final Path outputJar;
private final JarContentsSupplier outputJarContentsSupplier;
private final SourcePathRuleFinder ruleFinder;
@AddToRuleKey CompileToJarStepFactory compileStepFactory;
@AddToRuleKey private final boolean forceFinalResourceIds;
@AddToRuleKey private final Optional<String> unionPackage;
@AddToRuleKey private final Optional<String> finalRName;
@AddToRuleKey private final boolean useOldStyleableFormat;
@AddToRuleKey
@SuppressWarnings("PMD.UnusedPrivateField")
private final ImmutableList<SourcePath> abiInputs;
public DummyRDotJava(
BuildRuleParams params,
SourcePathRuleFinder ruleFinder,
Set<HasAndroidResourceDeps> androidResourceDeps,
CompileToJarStepFactory compileStepFactory,
boolean forceFinalResourceIds,
Optional<String> unionPackage,
Optional<String> finalRName,
boolean useOldStyleableFormat) {
this(
params,
ruleFinder,
androidResourceDeps,
compileStepFactory,
forceFinalResourceIds,
unionPackage,
finalRName,
useOldStyleableFormat,
abiPaths(androidResourceDeps));
}
private DummyRDotJava(
BuildRuleParams params,
SourcePathRuleFinder ruleFinder,
Set<HasAndroidResourceDeps> androidResourceDeps,
CompileToJarStepFactory compileStepFactory,
boolean forceFinalResourceIds,
Optional<String> unionPackage,
Optional<String> finalRName,
boolean useOldStyleableFormat,
ImmutableList<SourcePath> abiInputs) {
super(params.copyAppendingExtraDeps(() -> ruleFinder.filterBuildRuleInputs(abiInputs)));
SourcePathResolver resolver = new SourcePathResolver(ruleFinder);
this.ruleFinder = ruleFinder;
// Sort the input so that we get a stable ABI for the same set of resources.
this.androidResourceDeps =
androidResourceDeps
.stream()
.sorted(Comparator.comparing(HasAndroidResourceDeps::getBuildTarget))
.collect(MoreCollectors.toImmutableList());
this.useOldStyleableFormat = useOldStyleableFormat;
this.outputJar = getOutputJarPath(getBuildTarget(), getProjectFilesystem());
this.compileStepFactory = compileStepFactory;
this.forceFinalResourceIds = forceFinalResourceIds;
this.unionPackage = unionPackage;
this.finalRName = finalRName;
this.abiInputs = abiInputs;
this.outputJarContentsSupplier = new JarContentsSupplier(resolver, getSourcePathToOutput());
}
private static ImmutableList<SourcePath> abiPaths(Iterable<HasAndroidResourceDeps> deps) {
FluentIterable<HasAndroidResourceDeps> iter = FluentIterable.from(deps);
return iter.transform(HasAndroidResourceDeps::getPathToTextSymbolsFile)
.append(iter.transform(HasAndroidResourceDeps::getPathToRDotJavaPackageFile))
.toList();
}
@Override
public ImmutableList<Step> getBuildSteps(
BuildContext context, final BuildableContext buildableContext) {
ImmutableList.Builder<Step> steps = ImmutableList.builder();
final Path rDotJavaSrcFolder = getRDotJavaSrcFolder(getBuildTarget(), getProjectFilesystem());
steps.addAll(MakeCleanDirectoryStep.of(getProjectFilesystem(), rDotJavaSrcFolder));
// Generate the .java files and record where they will be written in javaSourceFilePaths.
ImmutableSortedSet<Path> javaSourceFilePaths;
if (androidResourceDeps.isEmpty()) {
// In this case, the user is likely running a Robolectric test that does not happen to
// depend on any resources. However, if Robolectric doesn't find an R.java file, it flips
// out, so we have to create one, anyway.
// TODO(mbolin): Stop hardcoding com.facebook. This should match the package in the
// associated TestAndroidManifest.xml file.
Path emptyRDotJava = rDotJavaSrcFolder.resolve("com/facebook/R.java");
steps.addAll(MakeCleanDirectoryStep.of(getProjectFilesystem(), emptyRDotJava.getParent()));
steps.add(
new WriteFileStep(
getProjectFilesystem(),
"package com.facebook;\n public class R {}\n",
emptyRDotJava,
/* executable */ false));
javaSourceFilePaths = ImmutableSortedSet.of(emptyRDotJava);
} else {
MergeAndroidResourcesStep mergeStep =
MergeAndroidResourcesStep.createStepForDummyRDotJava(
getProjectFilesystem(),
context.getSourcePathResolver(),
androidResourceDeps,
rDotJavaSrcFolder,
forceFinalResourceIds,
unionPackage,
/* rName */ Optional.empty(),
useOldStyleableFormat);
steps.add(mergeStep);
if (!finalRName.isPresent()) {
javaSourceFilePaths = mergeStep.getRDotJavaFiles();
} else {
MergeAndroidResourcesStep mergeFinalRStep =
MergeAndroidResourcesStep.createStepForDummyRDotJava(
getProjectFilesystem(),
context.getSourcePathResolver(),
androidResourceDeps,
rDotJavaSrcFolder,
/* forceFinalResourceIds */ true,
unionPackage,
finalRName,
useOldStyleableFormat);
steps.add(mergeFinalRStep);
javaSourceFilePaths =
ImmutableSortedSet.<Path>naturalOrder()
.addAll(mergeStep.getRDotJavaFiles())
.addAll(mergeFinalRStep.getRDotJavaFiles())
.build();
}
}
// Clear out the directory where the .class files will be generated.
final Path rDotJavaClassesFolder = getRDotJavaBinFolder();
steps.addAll(MakeCleanDirectoryStep.of(getProjectFilesystem(), rDotJavaClassesFolder));
Path pathToJarOutputDir = outputJar.getParent();
steps.addAll(MakeCleanDirectoryStep.of(getProjectFilesystem(), pathToJarOutputDir));
Path pathToSrcsList =
BuildTargets.getGenPath(getProjectFilesystem(), getBuildTarget(), "__%s__srcs");
steps.add(MkdirStep.of(getProjectFilesystem(), pathToSrcsList.getParent()));
// Compile the .java files.
compileStepFactory.createCompileStep(
context,
javaSourceFilePaths,
getBuildTarget(),
context.getSourcePathResolver(),
ruleFinder,
getProjectFilesystem(),
/* declared classpath */ ImmutableSortedSet.of(),
rDotJavaClassesFolder,
Optional.empty(),
pathToSrcsList,
NoOpClassUsageFileWriter.instance(),
steps,
buildableContext);
buildableContext.recordArtifact(rDotJavaClassesFolder);
steps.add(
new JarDirectoryStep(
getProjectFilesystem(),
outputJar,
ImmutableSortedSet.of(rDotJavaClassesFolder),
/* mainClass */ null,
/* manifestFile */ null,
/* mergeManifests */ true,
/* hashEntries */ true,
/* blacklist */ ImmutableSet.of()));
buildableContext.recordArtifact(outputJar);
steps.add(new CheckDummyRJarNotEmptyStep(javaSourceFilePaths));
return steps.build();
}
@Override
public Object initializeFromDisk(OnDiskBuildInfo onDiskBuildInfo) throws IOException {
// Warm up the jar contents. We just wrote the thing, so it should be in the filesystem cache
outputJarContentsSupplier.load();
return new Object();
}
@Override
public BuildOutputInitializer<Object> getBuildOutputInitializer() {
return new BuildOutputInitializer<>(getBuildTarget(), this);
}
private class CheckDummyRJarNotEmptyStep implements Step {
private final ImmutableSortedSet<Path> javaSourceFilePaths;
CheckDummyRJarNotEmptyStep(ImmutableSortedSet<Path> javaSourceFilePaths) {
this.javaSourceFilePaths = javaSourceFilePaths;
}
@Override
public StepExecutionResult execute(ExecutionContext context)
throws IOException, InterruptedException {
try (ZipFile jar = new ZipFile(getProjectFilesystem().resolve(outputJar).toFile())) {
for (ZipEntry zipEntry : Collections.list(jar.entries())) {
if (zipEntry.getName().endsWith(".class")) {
// We found a class, so the jar is probably fine.
return StepExecutionResult.SUCCESS;
}
}
}
StringBuilder sb = new StringBuilder();
for (Path file : javaSourceFilePaths) {
BasicFileAttributes attrs =
getProjectFilesystem().readAttributes(file, BasicFileAttributes.class);
sb.append(file);
sb.append(' ');
sb.append(attrs.size());
sb.append('\n');
}
throw new RuntimeException(
String.format(
"Dummy R.java JAR %s has no classes. Possible corrupt output. Is disk full? "
+ "Source files:\n%s",
outputJar, sb));
}
@Override
public String getShortName() {
return "check_dummy_r_jar_not_empty";
}
@Override
public String getDescription(ExecutionContext context) {
return "check_dummy_r_jar_not_empty " + outputJar;
}
}
public static Path getRDotJavaSrcFolder(BuildTarget buildTarget, ProjectFilesystem filesystem) {
return BuildTargets.getScratchPath(filesystem, buildTarget, "__%s_rdotjava_src__");
}
public static Path getRDotJavaBinFolder(BuildTarget buildTarget, ProjectFilesystem filesystem) {
return BuildTargets.getScratchPath(filesystem, buildTarget, "__%s_rdotjava_bin__");
}
private static Path getPathToOutputDir(BuildTarget buildTarget, ProjectFilesystem filesystem) {
return BuildTargets.getGenPath(filesystem, buildTarget, "__%s_dummyrdotjava_output__");
}
private static Path getOutputJarPath(BuildTarget buildTarget, ProjectFilesystem filesystem) {
return getPathToOutputDir(buildTarget, filesystem)
.resolve(String.format("%s.jar", buildTarget.getShortNameAndFlavorPostfix()));
}
@Override
public SourcePath getSourcePathToOutput() {
return new ExplicitBuildTargetSourcePath(getBuildTarget(), outputJar);
}
@Override
public ImmutableSortedSet<SourcePath> getJarContents() {
return outputJarContentsSupplier.get();
}
@Override
public Optional<BuildTarget> getAbiJar() {
return Optional.of(getBuildTarget());
}
public Path getRDotJavaBinFolder() {
return getRDotJavaBinFolder(getBuildTarget(), getProjectFilesystem());
}
public ImmutableList<HasAndroidResourceDeps> getAndroidResourceDeps() {
return androidResourceDeps;
}
@VisibleForTesting
CompileToJarStepFactory getCompileStepFactory() {
return compileStepFactory;
}
}