/* * Copyright 2012-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.jvm.java; import static com.facebook.buck.rules.BuildableProperties.Kind.LIBRARY; import com.facebook.buck.android.AndroidPackageable; import com.facebook.buck.android.AndroidPackageableCollector; import com.facebook.buck.io.BuckPaths; import com.facebook.buck.io.ProjectFilesystem; import com.facebook.buck.model.BuildTarget; import com.facebook.buck.model.BuildTargets; import com.facebook.buck.rules.AbstractBuildRuleWithResolver; import com.facebook.buck.rules.AddToRuleKey; import com.facebook.buck.rules.ArchiveMemberSourcePath; import com.facebook.buck.rules.BuildContext; import com.facebook.buck.rules.BuildOutputInitializer; import com.facebook.buck.rules.BuildRule; import com.facebook.buck.rules.BuildRuleParams; import com.facebook.buck.rules.BuildRuleResolver; import com.facebook.buck.rules.BuildableContext; import com.facebook.buck.rules.BuildableProperties; import com.facebook.buck.rules.DefaultBuildTargetSourcePath; import com.facebook.buck.rules.ExplicitBuildTargetSourcePath; import com.facebook.buck.rules.ExportDependencies; 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.SupportsDependencyFileRuleKey; import com.facebook.buck.rules.keys.SupportsInputBasedRuleKey; import com.facebook.buck.step.Step; import com.facebook.buck.util.HumanReadableException; import com.facebook.buck.util.MoreCollectors; import com.google.common.base.Preconditions; import com.google.common.base.Supplier; import com.google.common.base.Suppliers; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSortedMap; import com.google.common.collect.ImmutableSortedSet; import com.google.common.collect.Sets; import com.google.common.hash.HashCode; import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Optional; import java.util.Set; import java.util.function.Predicate; import java.util.regex.Pattern; import javax.annotation.Nullable; /** * Suppose this were a rule defined in <code>src/com/facebook/feed/BUCK</code>: * * <pre> * java_library( * name = 'feed', * srcs = [ * 'FeedStoryRenderer.java', * ], * deps = [ * '//src/com/facebook/feed/model:model', * '//third-party/java/guava:guava', * ], * ) * </pre> * * Then this would compile {@code FeedStoryRenderer.java} against Guava and the classes generated * from the {@code //src/com/facebook/feed/model:model} rule. */ public class DefaultJavaLibrary extends AbstractBuildRuleWithResolver implements JavaLibrary, HasClasspathEntries, ExportDependencies, InitializableFromDisk<JavaLibrary.Data>, AndroidPackageable, SupportsInputBasedRuleKey, SupportsDependencyFileRuleKey, JavaLibraryWithTests { private static final BuildableProperties OUTPUT_TYPE = new BuildableProperties(LIBRARY); private static final Path METADATA_DIR = Paths.get("META-INF"); @AddToRuleKey private final ImmutableSortedSet<SourcePath> srcs; @AddToRuleKey private final ImmutableSortedSet<SourcePath> resources; @AddToRuleKey(stringify = true) private final Optional<Path> resourcesRoot; @AddToRuleKey private final Optional<SourcePath> manifestFile; @AddToRuleKey private final Optional<String> mavenCoords; private final Optional<Path> outputJar; private final JarContentsSupplier outputJarContentsSupplier; private final BuildTarget abiJar; @AddToRuleKey private final Optional<SourcePath> proguardConfig; @AddToRuleKey private final ImmutableList<String> postprocessClassesCommands; // It's very important that these deps are non-ABI rules, even if compiling against ABIs is turned // on. This is because various methods in this class perform dependency traversal that rely on // these deps being represented as their full-jar dependency form. private final ImmutableSortedSet<BuildRule> fullJarDeclaredDeps; private final ImmutableSortedSet<BuildRule> fullJarExportedDeps; private final ImmutableSortedSet<BuildRule> fullJarProvidedDeps; private final Supplier<ImmutableSet<SourcePath>> outputClasspathEntriesSupplier; private final Supplier<ImmutableSet<SourcePath>> transitiveClasspathsSupplier; private final Supplier<ImmutableSet<JavaLibrary>> transitiveClasspathDepsSupplier; private final boolean trackClassUsage; private final ImmutableSortedSet<SourcePath> compileTimeClasspathSourcePaths; @AddToRuleKey @SuppressWarnings("PMD.UnusedPrivateField") private final ZipArchiveDependencySupplier abiClasspath; @Nullable private Path depFileRelativePath; private final BuildOutputInitializer<Data> buildOutputInitializer; private final ImmutableSortedSet<BuildTarget> tests; private final Optional<Path> generatedSourceFolder; @SuppressWarnings("PMD.UnusedPrivateField") @AddToRuleKey private final ImmutableSet<Pattern> classesToRemoveFromJar; private final SourcePathRuleFinder ruleFinder; @AddToRuleKey private final CompileToJarStepFactory compileStepFactory; public static DefaultJavaLibraryBuilder builder( BuildRuleParams params, BuildRuleResolver buildRuleResolver, JavaBuckConfig javaBuckConfig) { return new DefaultJavaLibraryBuilder(params, buildRuleResolver, javaBuckConfig); } @Override public ImmutableSortedSet<BuildTarget> getTests() { return tests; } protected DefaultJavaLibrary( final BuildRuleParams params, SourcePathResolver resolver, SourcePathRuleFinder ruleFinder, Set<? extends SourcePath> srcs, Set<? extends SourcePath> resources, Optional<Path> generatedSourceFolder, Optional<SourcePath> proguardConfig, ImmutableList<String> postprocessClassesCommands, ImmutableSortedSet<BuildRule> fullJarDeclaredDeps, ImmutableSortedSet<BuildRule> fullJarExportedDeps, ImmutableSortedSet<BuildRule> fullJarProvidedDeps, ImmutableSortedSet<SourcePath> compileTimeClasspathSourcePaths, ImmutableSortedSet<SourcePath> abiInputs, BuildTarget abiJar, boolean trackClassUsage, CompileToJarStepFactory compileStepFactory, Optional<Path> resourcesRoot, Optional<SourcePath> manifestFile, Optional<String> mavenCoords, ImmutableSortedSet<BuildTarget> tests, ImmutableSet<Pattern> classesToRemoveFromJar) { super(params, resolver); this.ruleFinder = ruleFinder; this.compileStepFactory = compileStepFactory; // Exported deps are meant to be forwarded onto the CLASSPATH for dependents, // and so only make sense for java library types. for (BuildRule dep : fullJarExportedDeps) { if (!(dep instanceof JavaLibrary)) { throw new HumanReadableException( params.getBuildTarget() + ": exported dep " + dep.getBuildTarget() + " (" + dep.getType() + ") " + "must be a type of java library."); } } this.srcs = ImmutableSortedSet.copyOf(srcs); this.resources = ImmutableSortedSet.copyOf(resources); this.proguardConfig = proguardConfig; this.postprocessClassesCommands = postprocessClassesCommands; this.fullJarDeclaredDeps = fullJarDeclaredDeps; this.fullJarExportedDeps = fullJarExportedDeps; this.fullJarProvidedDeps = fullJarProvidedDeps; this.compileTimeClasspathSourcePaths = compileTimeClasspathSourcePaths; this.resourcesRoot = resourcesRoot; this.manifestFile = manifestFile; this.mavenCoords = mavenCoords; this.tests = tests; this.trackClassUsage = trackClassUsage; if (this.trackClassUsage) { depFileRelativePath = getUsedClassesFilePath(params.getBuildTarget(), params.getProjectFilesystem()); } this.abiClasspath = new ZipArchiveDependencySupplier(ruleFinder, abiInputs); if (!srcs.isEmpty() || !resources.isEmpty() || manifestFile.isPresent()) { this.outputJar = Optional.of(getOutputJarPath(getBuildTarget(), getProjectFilesystem())); } else { this.outputJar = Optional.empty(); } this.outputJarContentsSupplier = new JarContentsSupplier(resolver, getSourcePathToOutput()); this.abiJar = abiJar; this.outputClasspathEntriesSupplier = Suppliers.memoize( () -> JavaLibraryClasspathProvider.getOutputClasspathJars( DefaultJavaLibrary.this, sourcePathForOutputJar())); this.transitiveClasspathsSupplier = Suppliers.memoize( () -> JavaLibraryClasspathProvider.getClasspathsFromLibraries( getTransitiveClasspathDeps())); this.transitiveClasspathDepsSupplier = Suppliers.memoize( () -> JavaLibraryClasspathProvider.getTransitiveClasspathDeps(DefaultJavaLibrary.this)); this.buildOutputInitializer = new BuildOutputInitializer<>(params.getBuildTarget(), this); this.generatedSourceFolder = generatedSourceFolder; this.classesToRemoveFromJar = classesToRemoveFromJar; } public static Path getOutputJarDirPath(BuildTarget target, ProjectFilesystem filesystem) { return BuildTargets.getGenPath(filesystem, target, "lib__%s__output"); } private Optional<SourcePath> sourcePathForOutputJar() { return outputJar.map(input -> new ExplicitBuildTargetSourcePath(getBuildTarget(), input)); } static Path getOutputJarPath(BuildTarget target, ProjectFilesystem filesystem) { return Paths.get( String.format( "%s/%s.jar", getOutputJarDirPath(target, filesystem), target.getShortNameAndFlavorPostfix())); } static Path getUsedClassesFilePath(BuildTarget target, ProjectFilesystem filesystem) { return getOutputJarDirPath(target, filesystem).resolve("used-classes.json"); } /** * @return directory path relative to the project root where .class files will be generated. The * return value does not end with a slash. */ public static Path getClassesDir(BuildTarget target, ProjectFilesystem filesystem) { return BuildTargets.getScratchPath(filesystem, target, "lib__%s__classes"); } @Override public BuildableProperties getProperties() { return OUTPUT_TYPE; } @Override public ImmutableSortedSet<SourcePath> getJavaSrcs() { return srcs; } @Override public ImmutableSortedSet<SourcePath> getSources() { return srcs; } @Override public ImmutableSortedSet<SourcePath> getResources() { return resources; } @Override public Set<BuildRule> getDepsForTransitiveClasspathEntries() { return Sets.union(fullJarDeclaredDeps, fullJarExportedDeps); } @Override public ImmutableSet<SourcePath> getTransitiveClasspaths() { return transitiveClasspathsSupplier.get(); } @Override public ImmutableSet<JavaLibrary> getTransitiveClasspathDeps() { return transitiveClasspathDepsSupplier.get(); } @Override public ImmutableSet<SourcePath> getImmediateClasspaths() { ImmutableSet.Builder<SourcePath> builder = ImmutableSet.builder(); // Add any exported deps. for (BuildRule exported : getExportedDeps()) { if (exported instanceof JavaLibrary) { builder.addAll(((JavaLibrary) exported).getImmediateClasspaths()); } } // Add ourselves to the classpath if there's a jar to be built. Optional<SourcePath> sourcePathForOutputJar = sourcePathForOutputJar(); if (sourcePathForOutputJar.isPresent()) { builder.add(sourcePathForOutputJar.get()); } return builder.build(); } @Override public ImmutableSet<SourcePath> getOutputClasspaths() { return outputClasspathEntriesSupplier.get(); } public ImmutableSortedSet<SourcePath> getCompileTimeClasspathSourcePaths() { return compileTimeClasspathSourcePaths; } @Override public Optional<Path> getGeneratedSourcePath() { return generatedSourceFolder; } @Override public ImmutableSortedSet<BuildRule> getExportedDeps() { return fullJarExportedDeps; } /** * Building a java_library() rule entails compiling the .java files specified in the srcs * attribute. They are compiled into a directory under {@link BuckPaths#getScratchDir()}. */ @Override public final ImmutableList<Step> getBuildSteps( BuildContext context, BuildableContext buildableContext) { ImmutableList.Builder<Step> steps = ImmutableList.builder(); JavaLibraryRules.addCompileToJarSteps( context, buildableContext, this, outputJar, ruleFinder, srcs, resources, postprocessClassesCommands, compileTimeClasspathSourcePaths, trackClassUsage, depFileRelativePath, compileStepFactory, resourcesRoot, manifestFile, classesToRemoveFromJar, steps); JavaLibraryRules.addAccumulateClassNamesStep( this, buildableContext, context.getSourcePathResolver(), steps); return steps.build(); } @Override public ImmutableSortedSet<SourcePath> getJarContents() { return outputJarContentsSupplier.get(); } /** Instructs this rule to report the ABI it has on disk as its current ABI. */ @Override public JavaLibrary.Data 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 JavaLibraryRules.initializeFromDisk( getBuildTarget(), getProjectFilesystem(), onDiskBuildInfo); } @Override public BuildOutputInitializer<Data> getBuildOutputInitializer() { return buildOutputInitializer; } @Override public final Optional<BuildTarget> getAbiJar() { return outputJar.isPresent() ? Optional.of(abiJar) : Optional.empty(); } @Override public ImmutableSortedMap<String, HashCode> getClassNamesToHashes() { return buildOutputInitializer.getBuildOutput().getClassNamesToHashes(); } @Override @Nullable public SourcePath getSourcePathToOutput() { return outputJar.map(o -> new ExplicitBuildTargetSourcePath(getBuildTarget(), o)).orElse(null); } @Override public Iterable<AndroidPackageable> getRequiredPackageables() { return AndroidPackageableCollector.getPackageableRules( ImmutableSortedSet.copyOf( Sets.difference( Sets.union(fullJarDeclaredDeps, fullJarExportedDeps), fullJarProvidedDeps))); } @Override public Optional<String> getMavenCoords() { return mavenCoords; } @Override public void addToCollector(AndroidPackageableCollector collector) { if (outputJar.isPresent()) { collector.addClasspathEntry( this, new ExplicitBuildTargetSourcePath(getBuildTarget(), outputJar.get())); } if (proguardConfig.isPresent()) { collector.addProguardConfig(getBuildTarget(), proguardConfig.get()); } } @Override public boolean useDependencyFileRuleKeys() { return !getJavaSrcs().isEmpty() && trackClassUsage; } @Override public Predicate<SourcePath> getCoveredByDepFilePredicate() { // a hash set is intentionally used to achieve constant time look-up return abiClasspath.getArchiveMembers(getResolver()).collect(MoreCollectors.toImmutableSet()) ::contains; } @Override public Predicate<SourcePath> getExistenceOfInterestPredicate() { // Annotation processors might enumerate all files under a certain path and then generate // code based on that list (without actually reading the files), making the list of files // itself a used dependency that must be part of the dependency-based key. We don't // currently have the instrumentation to detect such enumeration perfectly, but annotation // processors are most commonly looking for files under META-INF, so as a stopgap we add // the listing of META-INF to the rule key. return (SourcePath path) -> (path instanceof ArchiveMemberSourcePath) && getResolver() .getRelativeArchiveMemberPath(path) .getMemberPath() .startsWith(METADATA_DIR); } @Override public ImmutableList<SourcePath> getInputsAfterBuildingLocally(BuildContext context) throws IOException { Preconditions.checkState(useDependencyFileRuleKeys()); return DefaultClassUsageFileReader.loadFromFile( getProjectFilesystem(), getProjectFilesystem() .getPathForRelativePath(Preconditions.checkNotNull(depFileRelativePath)), getDepOutputPathToAbiSourcePath(context.getSourcePathResolver())); } private ImmutableMap<Path, SourcePath> getDepOutputPathToAbiSourcePath( SourcePathResolver pathResolver) { ImmutableMap.Builder<Path, SourcePath> pathToSourcePathMapBuilder = ImmutableMap.builder(); for (SourcePath sourcePath : compileTimeClasspathSourcePaths) { BuildRule rule = ruleFinder.getRule(sourcePath).get(); Path path = pathResolver.getAbsolutePath(sourcePath); if (rule instanceof HasJavaAbi) { if (((HasJavaAbi) rule).getAbiJar().isPresent()) { BuildTarget buildTarget = ((HasJavaAbi) rule).getAbiJar().get(); pathToSourcePathMapBuilder.put(path, new DefaultBuildTargetSourcePath(buildTarget)); } } else if (rule instanceof CalculateAbi) { pathToSourcePathMapBuilder.put(path, sourcePath); } } return pathToSourcePathMapBuilder.build(); } }