/* * 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.io.MorePaths; 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.model.Pair; import com.facebook.buck.model.UnflavoredBuildTarget; 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.Hint; import com.facebook.buck.rules.SourcePath; import com.facebook.buck.rules.SourcePathResolver; import com.facebook.buck.rules.SourcePathRuleFinder; import com.facebook.buck.rules.TargetGraph; 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.ImmutableBiMap; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSortedSet; import com.google.common.collect.Iterables; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Optional; import java.util.function.Function; import javax.annotation.Nullable; import org.immutables.value.Value; public class JsLibraryDescription implements Description<JsLibraryDescriptionArg>, Flavored { static final ImmutableSet<FlavorDomain<?>> FLAVOR_DOMAINS = ImmutableSet.of(JsFlavors.PLATFORM_DOMAIN, JsFlavors.OPTIMIZATION_DOMAIN); @Override public Class<JsLibraryDescriptionArg> getConstructorArgType() { return JsLibraryDescriptionArg.class; } @Override public BuildRule createBuildRule( TargetGraph targetGraph, BuildRuleParams params, BuildRuleResolver resolver, CellPathResolver cellRoots, JsLibraryDescriptionArg args) throws NoSuchBuildTargetException { params.getBuildTarget().getBasePath(); // this params object is used as base for the JsLibrary build rule, but also for all dynamically // created JsFile rules. // For the JsLibrary case, we want to propagate flavors to library dependencies // For the JsFile case, we only want to depend on the worker, not on any libraries params = JsUtil.withWorkerDependencyOnly(params, resolver, args.getWorker()); final WorkerTool worker = resolver.getRuleWithType(args.getWorker(), WorkerTool.class); final SourcePathRuleFinder ruleFinder = new SourcePathRuleFinder(resolver); final SourcePathResolver sourcePathResolver = new SourcePathResolver(ruleFinder); final ImmutableBiMap<Either<SourcePath, Pair<SourcePath, String>>, Flavor> sourcesToFlavors = mapSourcesToFlavors(sourcePathResolver, args.getSrcs()); final Optional<Either<SourcePath, Pair<SourcePath, String>>> file = JsFlavors.extractSourcePath( sourcesToFlavors.inverse(), params.getBuildTarget().getFlavors().stream()); if (file.isPresent()) { return params.getBuildTarget().getFlavors().contains(JsFlavors.RELEASE) ? createReleaseFileRule(params, resolver, args, worker) : createDevFileRule(params, ruleFinder, sourcePathResolver, args, file.get(), worker); } else { return new LibraryBuilder(targetGraph, resolver, params, sourcesToFlavors) .setSources(args.getSrcs()) .setLibraryDependencies(args.getLibs()) .build(worker); } } @Override public boolean hasFlavors(ImmutableSet<Flavor> flavors) { return JsFlavors.validateFlavors(flavors, FLAVOR_DOMAINS); } @Override public Optional<ImmutableSet<FlavorDomain<?>>> flavorDomains() { return Optional.of(FLAVOR_DOMAINS); } @BuckStyleImmutable @Value.Immutable interface AbstractJsLibraryDescriptionArg extends CommonDescriptionArg { Optional<String> getExtraArgs(); ImmutableSet<Either<SourcePath, Pair<SourcePath, String>>> getSrcs(); @Value.NaturalOrder ImmutableSortedSet<BuildTarget> getLibs(); BuildTarget getWorker(); @Hint(isDep = false, isInput = false) Optional<String> getBasePath(); } private static class LibraryBuilder { private final TargetGraph targetGraph; private final BuildRuleResolver resolver; private final ImmutableBiMap<Either<SourcePath, Pair<SourcePath, String>>, Flavor> sourcesToFlavors; private final BuildRuleParams baseParams; private final BuildTarget fileBaseTarget; @Nullable private ImmutableList<JsFile> sourceFiles; @Nullable private ImmutableList<BuildRule> libraryDependencies; private LibraryBuilder( TargetGraph targetGraph, BuildRuleResolver resolver, BuildRuleParams baseParams, ImmutableBiMap<Either<SourcePath, Pair<SourcePath, String>>, Flavor> sourcesToFlavors) { this.targetGraph = targetGraph; this.baseParams = baseParams; this.resolver = resolver; this.sourcesToFlavors = sourcesToFlavors; BuildTarget buildTarget = baseParams.getBuildTarget(); // Platform information is only relevant when building release-optimized files. // Stripping platform targets from individual files allows us to use the base version of // every file in the build for all supported platforms, leading to improved cache reuse. this.fileBaseTarget = !buildTarget.getFlavors().contains(JsFlavors.RELEASE) ? buildTarget.withFlavors() : buildTarget; } private LibraryBuilder setSources( ImmutableSet<Either<SourcePath, Pair<SourcePath, String>>> sources) throws NoSuchBuildTargetException { final ImmutableList.Builder<JsFile> builder = ImmutableList.builder(); for (Either<SourcePath, Pair<SourcePath, String>> source : sources) { builder.add(this.requireJsFile(source)); } this.sourceFiles = builder.build(); return this; } private LibraryBuilder setLibraryDependencies( ImmutableSortedSet<BuildTarget> libraryDependencies) throws NoSuchBuildTargetException { BuildTarget buildTarget = baseParams.getBuildTarget(); final BuildTarget[] targets = libraryDependencies .stream() .map(t -> JsUtil.verifyIsJsLibraryTarget(t, buildTarget, targetGraph)) .map(hasFlavors() ? this::addFlavorsToLibraryTarget : Function.identity()) .toArray(BuildTarget[]::new); final ImmutableList.Builder<BuildRule> builder = ImmutableList.builder(); for (BuildTarget target : targets) { // `requireRule()` needed for dependencies to flavored versions builder.add(resolver.requireRule(target)); } this.libraryDependencies = builder.build(); return this; } private JsLibrary build(WorkerTool worker) { Preconditions.checkNotNull(sourceFiles, "No source files set"); Preconditions.checkNotNull(libraryDependencies, "No library dependencies set"); return new JsLibrary( baseParams.copyAppendingExtraDeps(Iterables.concat(sourceFiles, libraryDependencies)), sourceFiles .stream() .map(BuildRule::getSourcePathToOutput) .collect(MoreCollectors.toImmutableSortedSet()), libraryDependencies .stream() .map(BuildRule::getSourcePathToOutput) .collect(MoreCollectors.toImmutableSortedSet()), worker); } private boolean hasFlavors() { return !baseParams.getBuildTarget().getFlavors().isEmpty(); } private JsFile requireJsFile(Either<SourcePath, Pair<SourcePath, String>> file) throws NoSuchBuildTargetException { final Flavor fileFlavor = sourcesToFlavors.get(file); final BuildTarget target = fileBaseTarget.withAppendedFlavors(fileFlavor); resolver.requireRule(target); return resolver.getRuleWithType(target, JsFile.class); } private BuildTarget addFlavorsToLibraryTarget(BuildTarget unflavored) { return unflavored.withAppendedFlavors(baseParams.getBuildTarget().getFlavors()); } } private static BuildRule createReleaseFileRule( BuildRuleParams params, BuildRuleResolver resolver, JsLibraryDescriptionArg args, WorkerTool worker) throws NoSuchBuildTargetException { final BuildTarget devTarget = withFileFlavorOnly(params.getBuildTarget()); final BuildRule devFile = resolver.requireRule(devTarget); return new JsFile.JsFileRelease( params.copyAppendingExtraDeps(devFile), resolver.getRuleWithType(devTarget, JsFile.class).getSourcePathToOutput(), args.getExtraArgs(), worker); } private static <A extends AbstractJsLibraryDescriptionArg> BuildRule createDevFileRule( BuildRuleParams params, SourcePathRuleFinder ruleFinder, SourcePathResolver sourcePathResolver, A args, Either<SourcePath, Pair<SourcePath, String>> source, WorkerTool worker) { final SourcePath sourcePath = source.transform(x -> x, Pair::getFirst); final Optional<String> subPath = Optional.ofNullable(source.transform(x -> null, Pair::getSecond)); final Optional<Path> virtualPath = args.getBasePath() .map( basePath -> changePathPrefix( sourcePath, basePath, params, sourcePathResolver, params.getBuildTarget().getUnflavoredBuildTarget()) .resolve(subPath.orElse(""))); return new JsFile.JsFileDev( ruleFinder.getRule(sourcePath).map(params::copyAppendingExtraDeps).orElse(params), sourcePath, subPath, virtualPath, args.getExtraArgs(), worker); } private static BuildTarget withFileFlavorOnly(BuildTarget target) { return target.withFlavors( target.getFlavors().stream().filter(JsFlavors::isFileFlavor).toArray(Flavor[]::new)); } private static ImmutableBiMap<Either<SourcePath, Pair<SourcePath, String>>, Flavor> mapSourcesToFlavors( SourcePathResolver sourcePathResolver, ImmutableSet<Either<SourcePath, Pair<SourcePath, String>>> sources) { final ImmutableBiMap.Builder<Either<SourcePath, Pair<SourcePath, String>>, Flavor> builder = ImmutableBiMap.builder(); for (Either<SourcePath, Pair<SourcePath, String>> source : sources) { final Path relativePath = source.isLeft() ? sourcePathResolver.getRelativePath(source.getLeft()) : Paths.get(source.getRight().getSecond()); builder.put(source, JsFlavors.fileFlavorForSourcePath(relativePath)); } return builder.build(); } private static Path changePathPrefix( SourcePath sourcePath, String basePath, BuildRuleParams params, SourcePathResolver sourcePathResolver, UnflavoredBuildTarget target) { final Path directoryOfBuildFile = target.getCellPath().resolve(target.getBasePath()); final Path transplantTo = MorePaths.normalize(directoryOfBuildFile.resolve(basePath)); final Path absolutePath = sourcePathResolver .getPathSourcePath(sourcePath) .map( pathSourcePath -> // for sub paths, replace the leading directory with the base path transplantTo.resolve( MorePaths.relativize( directoryOfBuildFile, sourcePathResolver.getAbsolutePath(sourcePath)))) .orElse(transplantTo); // build target output paths are replaced completely return params .getProjectFilesystem() .getPathRelativeToProjectRoot(absolutePath) .orElseThrow( () -> new HumanReadableException( "%s: Using '%s' as base path for '%s' would move the file " + "out of the project root.", target, basePath, sourcePathResolver.getRelativePath(sourcePath))); } }