/*
* Copyright 2017-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.js;
import com.facebook.buck.android.Aapt2Compile;
import com.facebook.buck.android.AndroidLibraryDescription;
import com.facebook.buck.android.AndroidResource;
import com.facebook.buck.android.AndroidResourceDescription;
import com.facebook.buck.apple.AppleBundleResources;
import com.facebook.buck.apple.AppleLibraryDescription;
import com.facebook.buck.apple.HasAppleBundleResourcesDescription;
import com.facebook.buck.graph.AbstractBreadthFirstTraversal;
import com.facebook.buck.io.ProjectFilesystem;
import com.facebook.buck.model.BuildTarget;
import com.facebook.buck.model.Either;
import com.facebook.buck.model.Flavor;
import com.facebook.buck.model.FlavorDomain;
import com.facebook.buck.model.Flavored;
import com.facebook.buck.parser.NoSuchBuildTargetException;
import com.facebook.buck.rules.BuildRule;
import com.facebook.buck.rules.BuildRuleParams;
import com.facebook.buck.rules.BuildRuleResolver;
import com.facebook.buck.rules.CellPathResolver;
import com.facebook.buck.rules.CommonDescriptionArg;
import com.facebook.buck.rules.Description;
import com.facebook.buck.rules.HasDeclaredDeps;
import com.facebook.buck.rules.SourcePathResolver;
import com.facebook.buck.rules.SourcePathRuleFinder;
import com.facebook.buck.rules.TargetGraph;
import com.facebook.buck.rules.TargetNode;
import com.facebook.buck.shell.ExportFile;
import com.facebook.buck.shell.ExportFileDescription;
import com.facebook.buck.shell.WorkerTool;
import com.facebook.buck.util.HumanReadableException;
import com.facebook.buck.util.MoreCollectors;
import com.facebook.buck.util.immutables.BuckStyleImmutable;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.collect.ImmutableSortedSet;
import java.util.Collection;
import java.util.Optional;
import org.immutables.value.Value;
public class JsBundleDescription
implements Description<JsBundleDescriptionArg>,
Flavored,
HasAppleBundleResourcesDescription<JsBundleDescriptionArg> {
private static final ImmutableSet<FlavorDomain<?>> FLAVOR_DOMAINS =
ImmutableSet.of(
JsFlavors.PLATFORM_DOMAIN,
JsFlavors.OPTIMIZATION_DOMAIN,
JsFlavors.RAM_BUNDLE_DOMAIN,
JsFlavors.SOURCE_MAP_DOMAIN);
@Override
public boolean hasFlavors(ImmutableSet<Flavor> flavors) {
return JsFlavors.validateFlavors(flavors, FLAVOR_DOMAINS);
}
@Override
public Optional<ImmutableSet<FlavorDomain<?>>> flavorDomains() {
return Optional.of(FLAVOR_DOMAINS);
}
@Override
public Class<JsBundleDescriptionArg> getConstructorArgType() {
return JsBundleDescriptionArg.class;
}
@Override
public BuildRule createBuildRule(
TargetGraph targetGraph,
BuildRuleParams params,
BuildRuleResolver resolver,
CellPathResolver cellRoots,
JsBundleDescriptionArg args)
throws NoSuchBuildTargetException {
final ImmutableSortedSet<Flavor> flavors = params.getBuildTarget().getFlavors();
// Source maps are exposed individually using a special flavor
if (flavors.contains(JsFlavors.SOURCE_MAP)) {
BuildTarget bundleTarget = params.getBuildTarget().withoutFlavors(JsFlavors.SOURCE_MAP);
resolver.requireRule(bundleTarget);
JsBundleOutputs bundleOutputs = resolver.getRuleWithType(bundleTarget, JsBundleOutputs.class);
SourcePathRuleFinder ruleFinder = new SourcePathRuleFinder(resolver);
return new ExportFile(
JsUtil.copyParamsWithDependencies(params),
ruleFinder,
new SourcePathResolver(ruleFinder),
bundleOutputs.getBundleName() + ".map",
ExportFileDescription.Mode.REFERENCE,
bundleOutputs.getSourcePathToSourceMap());
}
// For Android, we bundle JS output as assets, and images etc. as resources.
// To facilitate this, we return a build rule that in turn depends on a `JsBundle` and
// an `AndroidResource`. The `AndroidResource` rule also depends on the `JsBundle`
// if the `FORCE_JS_BUNDLE` flavor is present, we create the `JsBundle` instance itself.
if (flavors.contains(JsFlavors.ANDROID) && !flavors.contains(JsFlavors.FORCE_JS_BUNDLE)) {
return createAndroidRule(params.copyInvalidatingDeps(), resolver, args.getAndroidPackage());
}
// Flavors are propagated from js_bundle targets to their js_library dependencies
// for that reason, dependencies of libraries are handled manually, and as a first step,
// all dependencies to libraries are removed
params = JsUtil.withWorkerDependencyOnly(params, resolver, args.getWorker());
final Either<ImmutableSet<String>, String> entryPoint = args.getEntry();
ImmutableSortedSet<JsLibrary> libraryDeps =
new TransitiveLibraryDependencies(params.getBuildTarget(), targetGraph, resolver)
.collect(args.getDeps());
return new JsBundle(
params.copyAppendingExtraDeps(libraryDeps),
libraryDeps
.stream()
.map(JsLibrary::getSourcePathToOutput)
.collect(MoreCollectors.toImmutableSortedSet()),
entryPoint.isLeft() ? entryPoint.getLeft() : ImmutableSet.of(entryPoint.getRight()),
args.getBundleName(),
resolver.getRuleWithType(args.getWorker(), WorkerTool.class));
}
private static BuildRule createAndroidRule(
BuildRuleParams params, BuildRuleResolver resolver, Optional<String> rDotJavaPackage)
throws NoSuchBuildTargetException {
final BuildTarget bundleTarget =
params
.getBuildTarget()
.withAppendedFlavors(JsFlavors.FORCE_JS_BUNDLE)
.withoutFlavors(JsFlavors.ANDROID_RESOURCES)
.withoutFlavors(AndroidResourceDescription.AAPT2_COMPILE_FLAVOR);
resolver.requireRule(bundleTarget);
final JsBundle jsBundle = resolver.getRuleWithType(bundleTarget, JsBundle.class);
if (params.getBuildTarget().getFlavors().contains(JsFlavors.ANDROID_RESOURCES)) {
final String rDot =
rDotJavaPackage.orElseThrow(
() ->
new HumanReadableException(
"Specify `android_package` when building %s for Android.",
params.getBuildTarget().getUnflavoredBuildTarget()));
return createAndroidResources(params, resolver, jsBundle, rDot);
} else {
return createAndroidBundle(params, resolver, jsBundle);
}
}
private static JsBundleAndroid createAndroidBundle(
BuildRuleParams params, BuildRuleResolver resolver, JsBundle jsBundle)
throws NoSuchBuildTargetException {
final BuildTarget resourceTarget =
params.getBuildTarget().withAppendedFlavors(JsFlavors.ANDROID_RESOURCES);
final BuildRule resource = resolver.requireRule(resourceTarget);
return new JsBundleAndroid(
params.copyReplacingDeclaredAndExtraDeps(
ImmutableSortedSet::of, () -> ImmutableSortedSet.of(jsBundle, resource)),
jsBundle,
resolver.getRuleWithType(resourceTarget, AndroidResource.class));
}
private static BuildRule createAndroidResources(
BuildRuleParams params, BuildRuleResolver resolver, JsBundle jsBundle, String rDotJavaPackage)
throws NoSuchBuildTargetException {
if (params
.getBuildTarget()
.getFlavors()
.contains(AndroidResourceDescription.AAPT2_COMPILE_FLAVOR)) {
return new Aapt2Compile(
params.copyReplacingDeclaredAndExtraDeps(
ImmutableSortedSet::of, () -> ImmutableSortedSet.of(jsBundle)),
jsBundle.getSourcePathToResources());
}
return new AndroidResource(
params.copyReplacingDeclaredAndExtraDeps(
ImmutableSortedSet::of, () -> ImmutableSortedSet.of(jsBundle)),
new SourcePathRuleFinder(resolver),
ImmutableSortedSet.of(), // deps
jsBundle.getSourcePathToResources(),
ImmutableSortedMap.of(), // resSrcs
rDotJavaPackage,
null,
ImmutableSortedMap.of(),
null,
false);
}
@Override
public void addAppleBundleResources(
AppleBundleResources.Builder builder,
TargetNode<JsBundleDescriptionArg, ?> targetNode,
ProjectFilesystem filesystem,
BuildRuleResolver resolver) {
JsBundleOutputs bundle =
resolver.getRuleWithType(targetNode.getBuildTarget(), JsBundleOutputs.class);
builder.addDirsContainingResourceDirs(
bundle.getSourcePathToOutput(), bundle.getSourcePathToResources());
}
@BuckStyleImmutable
@Value.Immutable
interface AbstractJsBundleDescriptionArg extends CommonDescriptionArg, HasDeclaredDeps {
Either<ImmutableSet<String>, String> getEntry();
@Value.Default
default String getBundleName() {
return getName() + ".js";
}
BuildTarget getWorker();
/** For R.java */
Optional<String> getAndroidPackage();
}
private static class TransitiveLibraryDependencies {
private final ImmutableSortedSet<Flavor> extraFlavors;
private final BuildRuleResolver resolver;
private final SourcePathRuleFinder ruleFinder;
private final TargetGraph targetGraph;
private TransitiveLibraryDependencies(
BuildTarget bundleTarget, TargetGraph targetGraph, BuildRuleResolver resolver) {
this.targetGraph = targetGraph;
this.resolver = resolver;
final ImmutableSortedSet<Flavor> bundleFlavors = bundleTarget.getFlavors();
extraFlavors =
bundleFlavors
.stream()
.filter(
flavor ->
JsLibraryDescription.FLAVOR_DOMAINS
.stream()
.anyMatch(domain -> domain.contains(flavor)))
.collect(MoreCollectors.toImmutableSortedSet());
ruleFinder = new SourcePathRuleFinder(resolver);
}
ImmutableSortedSet<JsLibrary> collect(Collection<BuildTarget> deps) {
ImmutableSortedSet.Builder<JsLibrary> jsLibraries = ImmutableSortedSet.naturalOrder();
new AbstractBreadthFirstTraversal<BuildTarget>(deps) {
@Override
public Iterable<BuildTarget> visit(BuildTarget target) throws RuntimeException {
final TargetNode<?, ?> targetNode = targetGraph.get(target);
final Description<?> description = targetNode.getDescription();
if (description instanceof JsLibraryDescription) {
final JsLibrary library = requireLibrary(target);
jsLibraries.add(library);
return getLibraryDependencies(library);
} else if (description instanceof AndroidLibraryDescription
|| description instanceof AppleLibraryDescription) {
return targetNode.getDeclaredDeps();
}
return ImmutableList.of();
}
}.start();
return jsLibraries.build();
}
private JsLibrary requireLibrary(BuildTarget target) {
try {
BuildRule rule = resolver.requireRule(target.withAppendedFlavors(extraFlavors));
Preconditions.checkState(rule instanceof JsLibrary);
return (JsLibrary) rule;
} catch (NoSuchBuildTargetException e) {
throw new HumanReadableException(e);
}
}
private Iterable<BuildTarget> getLibraryDependencies(JsLibrary library) {
return library
.getLibraryDependencies()
.stream()
.map(
sourcePath ->
ruleFinder
.getRule(sourcePath)
.<HumanReadableException>orElseThrow(
() ->
new HumanReadableException(
"js_library %s has '%s' as a lib, but js_library can only have other "
+ "js_library targets as lib",
library.getBuildTarget(), sourcePath)))
.map(BuildRule::getBuildTarget)
.collect(MoreCollectors.toImmutableList());
}
}
}