com.facebook.buck.features.js.JsBundleDescription.java Source code

Java tutorial

Introduction

Here is the source code for com.facebook.buck.features.js.JsBundleDescription.java

Source

/*
 * 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.features.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.android.toolchain.AndroidPlatformTarget;
import com.facebook.buck.apple.AppleBundleResources;
import com.facebook.buck.apple.AppleLibraryDescription;
import com.facebook.buck.apple.HasAppleBundleResourcesDescription;
import com.facebook.buck.core.description.BaseDescription;
import com.facebook.buck.core.description.arg.CommonDescriptionArg;
import com.facebook.buck.core.description.arg.HasDeclaredDeps;
import com.facebook.buck.core.exceptions.HumanReadableException;
import com.facebook.buck.core.model.BuildTarget;
import com.facebook.buck.core.model.Flavor;
import com.facebook.buck.core.model.FlavorDomain;
import com.facebook.buck.core.model.Flavored;
import com.facebook.buck.core.model.targetgraph.BuildRuleCreationContextWithTargetGraph;
import com.facebook.buck.core.model.targetgraph.DescriptionWithTargetGraph;
import com.facebook.buck.core.model.targetgraph.TargetGraph;
import com.facebook.buck.core.model.targetgraph.TargetNode;
import com.facebook.buck.core.rules.ActionGraphBuilder;
import com.facebook.buck.core.rules.BuildRule;
import com.facebook.buck.core.rules.BuildRuleParams;
import com.facebook.buck.core.rules.BuildRuleResolver;
import com.facebook.buck.core.rules.SourcePathRuleFinder;
import com.facebook.buck.core.sourcepath.SourcePath;
import com.facebook.buck.core.toolchain.ToolchainProvider;
import com.facebook.buck.core.util.graph.AbstractBreadthFirstTraversal;
import com.facebook.buck.core.util.immutables.BuckStyleImmutable;
import com.facebook.buck.io.filesystem.ProjectFilesystem;
import com.facebook.buck.rules.args.Arg;
import com.facebook.buck.shell.ExportFile;
import com.facebook.buck.shell.ExportFileDescription;
import com.facebook.buck.shell.ExportFileDirectoryAction;
import com.facebook.buck.shell.WorkerTool;
import com.facebook.buck.util.types.Either;
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 com.google.common.collect.Ordering;
import java.util.Collection;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Stream;
import org.immutables.value.Value;

public class JsBundleDescription implements DescriptionWithTargetGraph<JsBundleDescriptionArg>, Flavored,
        HasAppleBundleResourcesDescription<JsBundleDescriptionArg>,
        JsBundleOutputsDescription<JsBundleDescriptionArg> {

    static final ImmutableSet<FlavorDomain<?>> FLAVOR_DOMAINS = ImmutableSet.of(JsFlavors.PLATFORM_DOMAIN,
            JsFlavors.OPTIMIZATION_DOMAIN, JsFlavors.RAM_BUNDLE_DOMAIN, JsFlavors.OUTPUT_OPTIONS_DOMAIN);

    private final ToolchainProvider toolchainProvider;

    public JsBundleDescription(ToolchainProvider toolchainProvider) {
        this.toolchainProvider = toolchainProvider;
    }

    @Override
    public boolean hasFlavors(ImmutableSet<Flavor> flavors) {
        return supportsFlavors(flavors);
    }

    static boolean supportsFlavors(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(BuildRuleCreationContextWithTargetGraph context, BuildTarget buildTarget,
            BuildRuleParams params, JsBundleDescriptionArg args) {
        ActionGraphBuilder graphBuilder = context.getActionGraphBuilder();
        ProjectFilesystem projectFilesystem = context.getProjectFilesystem();
        ImmutableSortedSet<Flavor> flavors = buildTarget.getFlavors();
        SourcePathRuleFinder ruleFinder = new SourcePathRuleFinder(graphBuilder);

        // Source maps are exposed individually using a special flavor
        if (flavors.contains(JsFlavors.SOURCE_MAP)) {
            BuildTarget bundleTarget = buildTarget.withoutFlavors(JsFlavors.SOURCE_MAP);
            graphBuilder.requireRule(bundleTarget);
            JsBundleOutputs bundleOutputs = graphBuilder.getRuleWithType(bundleTarget, JsBundleOutputs.class);

            return new ExportFile(buildTarget, projectFilesystem, ruleFinder,
                    bundleOutputs.getBundleName() + ".map", ExportFileDescription.Mode.REFERENCE,
                    bundleOutputs.getSourcePathToSourceMap(), ExportFileDirectoryAction.FAIL);
        }

        if (flavors.contains(JsFlavors.MISC)) {
            BuildTarget bundleTarget = buildTarget.withoutFlavors(JsFlavors.MISC);
            graphBuilder.requireRule(bundleTarget);
            JsBundleOutputs bundleOutputs = graphBuilder.getRuleWithType(bundleTarget, JsBundleOutputs.class);

            return new ExportFile(buildTarget, projectFilesystem, ruleFinder,
                    bundleOutputs.getBundleName() + "-misc", ExportFileDescription.Mode.REFERENCE,
                    bundleOutputs.getSourcePathToMisc(),
                    // TODO(27131551): temporary allow directory export until a proper fix is implemented
                    ExportFileDirectoryAction.ALLOW);
        }

        // 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)
                && !flavors.contains(JsFlavors.DEPENDENCY_FILE)) {
            return createAndroidRule(toolchainProvider, buildTarget, projectFilesystem, graphBuilder, ruleFinder,
                    args.getAndroidPackage());
        }

        Either<ImmutableSet<String>, String> entryPoint = args.getEntry();
        TransitiveLibraryDependencies libsResolver = new TransitiveLibraryDependencies(buildTarget,
                context.getTargetGraph(), graphBuilder, ruleFinder);
        ImmutableSet<JsLibrary> flavoredLibraryDeps = libsResolver.collect(args.getDeps());
        Stream<BuildRule> generatedDeps = findGeneratedSources(ruleFinder, flavoredLibraryDeps.stream())
                .map(graphBuilder::requireRule);

        // 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 replaced with dependencies to flavored library targets.
        BuildRuleParams paramsWithFlavoredLibraries = params.withoutDeclaredDeps()
                .copyAppendingExtraDeps(Stream.concat(flavoredLibraryDeps.stream(), generatedDeps)::iterator);
        ImmutableSortedSet<SourcePath> libraries = flavoredLibraryDeps.stream()
                .map(JsLibrary::getSourcePathToOutput)
                .collect(ImmutableSortedSet.toImmutableSortedSet(Ordering.natural()));
        ImmutableSet<String> entryPoints = entryPoint.isLeft() ? entryPoint.getLeft()
                : ImmutableSet.of(entryPoint.getRight());

        Optional<Arg> extraJson = JsUtil.getExtraJson(args, buildTarget, graphBuilder,
                context.getCellPathResolver());

        // If {@link JsFlavors.DEPENDENCY_FILE} is specified, the worker will output a file containing
        // all dependencies between files that go into the final bundle
        if (flavors.contains(JsFlavors.DEPENDENCY_FILE)) {
            return new JsDependenciesFile(buildTarget, projectFilesystem, paramsWithFlavoredLibraries, libraries,
                    entryPoints, extraJson, graphBuilder.getRuleWithType(args.getWorker(), WorkerTool.class));
        }

        String bundleName = args.computeBundleName(buildTarget.getFlavors(), () -> args.getName() + ".js");

        return new JsBundle(buildTarget, projectFilesystem, paramsWithFlavoredLibraries, libraries, entryPoints,
                extraJson, bundleName, graphBuilder.getRuleWithType(args.getWorker(), WorkerTool.class));
    }

    private static BuildRule createAndroidRule(ToolchainProvider toolchainProvider, BuildTarget buildTarget,
            ProjectFilesystem projectFilesystem, ActionGraphBuilder graphBuilder, SourcePathRuleFinder ruleFinder,
            Optional<String> rDotJavaPackage) {
        BuildTarget bundleTarget = buildTarget.withAppendedFlavors(JsFlavors.FORCE_JS_BUNDLE)
                .withoutFlavors(JsFlavors.ANDROID_RESOURCES)
                .withoutFlavors(AndroidResourceDescription.AAPT2_COMPILE_FLAVOR);
        graphBuilder.requireRule(bundleTarget);

        JsBundle jsBundle = graphBuilder.getRuleWithType(bundleTarget, JsBundle.class);
        if (buildTarget.getFlavors().contains(JsFlavors.ANDROID_RESOURCES)) {
            String rDot = rDotJavaPackage.orElseThrow(
                    () -> new HumanReadableException("Specify `android_package` when building %s for Android.",
                            buildTarget.getUnflavoredBuildTarget()));
            return createAndroidResources(toolchainProvider, buildTarget, projectFilesystem, ruleFinder, jsBundle,
                    rDot);
        } else {
            return createAndroidBundle(buildTarget, projectFilesystem, graphBuilder, jsBundle);
        }
    }

    private static JsBundleAndroid createAndroidBundle(BuildTarget buildTarget, ProjectFilesystem projectFilesystem,
            ActionGraphBuilder graphBuilder, JsBundle jsBundle) {

        BuildTarget resourceTarget = buildTarget.withAppendedFlavors(JsFlavors.ANDROID_RESOURCES);
        BuildRule resource = graphBuilder.requireRule(resourceTarget);

        return new JsBundleAndroid(buildTarget, projectFilesystem,
                new BuildRuleParams(() -> ImmutableSortedSet.of(), () -> ImmutableSortedSet.of(jsBundle, resource),
                        ImmutableSortedSet.of()),
                jsBundle, graphBuilder.getRuleWithType(resourceTarget, AndroidResource.class));
    }

    private static BuildRule createAndroidResources(ToolchainProvider toolchainProvider, BuildTarget buildTarget,
            ProjectFilesystem projectFilesystem, SourcePathRuleFinder ruleFinder, JsBundle jsBundle,
            String rDotJavaPackage) {
        if (buildTarget.getFlavors().contains(AndroidResourceDescription.AAPT2_COMPILE_FLAVOR)) {
            AndroidPlatformTarget androidPlatformTarget = toolchainProvider
                    .getByName(AndroidPlatformTarget.DEFAULT_NAME, AndroidPlatformTarget.class);
            return new Aapt2Compile(buildTarget, projectFilesystem, androidPlatformTarget,
                    ImmutableSortedSet.of(jsBundle), jsBundle.getSourcePathToResources());
        }

        BuildRuleParams params = new BuildRuleParams(() -> ImmutableSortedSet.of(),
                () -> ImmutableSortedSet.of(jsBundle), ImmutableSortedSet.of());

        return new AndroidResource(buildTarget, projectFilesystem, params, ruleFinder, ImmutableSortedSet.of(), // deps
                jsBundle.getSourcePathToResources(), ImmutableSortedMap.of(), // resSrcs
                rDotJavaPackage, null, ImmutableSortedMap.of(), null, false);
    }

    /**
     * Finds all build targets that are inputs to any transitive JsFile dependency of any of the
     * passed in JsLibrary instances.
     */
    private static Stream<BuildTarget> findGeneratedSources(SourcePathRuleFinder ruleFinder,
            Stream<JsLibrary> libraries) {
        return libraries.map(lib -> lib.getJsFiles(ruleFinder)).flatMap(Function.identity())
                .map(jsFile -> jsFile.getSourceBuildTarget(ruleFinder)).filter(Objects::nonNull);
    }

    @Override
    public void addAppleBundleResources(AppleBundleResources.Builder builder,
            TargetNode<JsBundleDescriptionArg> targetNode, ProjectFilesystem filesystem,
            BuildRuleResolver resolver) {
        JsBundleOutputs bundle = resolver.getRuleWithType(targetNode.getBuildTarget(), JsBundleOutputs.class);
        addAppleBundleResources(builder, bundle);
    }

    static void addAppleBundleResourcesJSOutputOnly(AppleBundleResources.Builder builder, JsBundleOutputs bundle) {
        builder.addDirsContainingResourceDirs(bundle.getSourcePathToOutput());
    }

    static void addAppleBundleResources(AppleBundleResources.Builder builder, JsBundleOutputs bundle) {
        addAppleBundleResourcesJSOutputOnly(builder, bundle);
        builder.addDirsContainingResourceDirs(bundle.getSourcePathToResources());
    }

    @BuckStyleImmutable
    @Value.Immutable
    interface AbstractJsBundleDescriptionArg
            extends CommonDescriptionArg, HasDeclaredDeps, HasExtraJson, HasBundleName {

        Either<ImmutableSet<String>, String> getEntry();

        BuildTarget getWorker();

        /** For R.java */
        Optional<String> getAndroidPackage();
    }

    private static class TransitiveLibraryDependencies {
        private final ImmutableSortedSet<Flavor> extraFlavors;
        private final ActionGraphBuilder graphBuilder;
        private final SourcePathRuleFinder ruleFinder;
        private final TargetGraph targetGraph;

        private TransitiveLibraryDependencies(BuildTarget bundleTarget, TargetGraph targetGraph,
                ActionGraphBuilder graphBuilder, SourcePathRuleFinder ruleFinder) {
            this.targetGraph = targetGraph;
            this.graphBuilder = graphBuilder;
            this.ruleFinder = ruleFinder;

            ImmutableSortedSet<Flavor> bundleFlavors = bundleTarget.getFlavors();
            extraFlavors = bundleFlavors.stream()
                    .filter(flavor -> JsLibraryDescription.FLAVOR_DOMAINS.stream()
                            .anyMatch(domain -> domain.contains(flavor)))
                    .collect(ImmutableSortedSet.toImmutableSortedSet(Ordering.natural()));
        }

        ImmutableSet<JsLibrary> collect(Collection<BuildTarget> deps) {
            ImmutableSet.Builder<JsLibrary> jsLibraries = ImmutableSet.builder();

            new AbstractBreadthFirstTraversal<BuildTarget>(deps) {
                @Override
                public Iterable<BuildTarget> visit(BuildTarget target) throws RuntimeException {
                    TargetNode<?> targetNode = targetGraph.get(target);
                    BaseDescription<?> description = targetNode.getDescription();

                    if (description instanceof JsLibraryDescription) {
                        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) {
            BuildRule rule = graphBuilder.requireRule(target.withAppendedFlavors(extraFlavors));
            Preconditions.checkState(rule instanceof JsLibrary);
            return (JsLibrary) rule;
        }

        private Iterable<BuildTarget> getLibraryDependencies(JsLibrary library) {
            ImmutableSortedSet<SourcePath> libraryDependencies = library.getLibraryDependencies();
            ImmutableList.Builder<BuildTarget> libraryTargets = ImmutableList
                    .builderWithExpectedSize(libraryDependencies.size());
            for (SourcePath sourcePath : libraryDependencies) {
                Optional<BuildRule> rule = ruleFinder.getRule(sourcePath);
                if (rule.isPresent()) {
                    libraryTargets.add(rule.get().getBuildTarget());
                } else {
                    throw 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);
                }
            }
            return libraryTargets.build();
        }
    }
}