/*
 * Copyright 2008 Google 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.google.gwt.dev;

import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.linker.Artifact;
import com.google.gwt.core.ext.linker.ArtifactSet;
import com.google.gwt.core.ext.linker.EmittedArtifact;
import com.google.gwt.core.ext.linker.EmittedArtifact.Visibility;
import com.google.gwt.core.ext.linker.SelectionProperty;
import com.google.gwt.core.ext.linker.impl.BinaryOnlyArtifactWrapper;
import com.google.gwt.core.ext.linker.impl.JarEntryEmittedArtifact;
import com.google.gwt.core.ext.linker.impl.StandardCompilationResult;
import com.google.gwt.core.ext.linker.impl.StandardLinkerContext;
import com.google.gwt.dev.CompileTaskRunner.CompileTask;
import com.google.gwt.dev.cfg.BindingProperties;
import com.google.gwt.dev.cfg.BindingProperty;
import com.google.gwt.dev.cfg.ModuleDef;
import com.google.gwt.dev.cfg.ModuleDefLoader;
import com.google.gwt.dev.cfg.PropertyCombinations;
import com.google.gwt.dev.cfg.ResourceLoader;
import com.google.gwt.dev.cfg.ResourceLoaders;
import com.google.gwt.dev.jjs.PermutationResult;
import com.google.gwt.dev.jjs.impl.codesplitter.CodeSplitter;
import com.google.gwt.dev.resource.ResourceOracle;
import com.google.gwt.dev.util.NullOutputFileSet;
import com.google.gwt.dev.util.OutputFileSet;
import com.google.gwt.dev.util.OutputFileSetOnDirectory;
import com.google.gwt.dev.util.OutputFileSetOnJar;
import com.google.gwt.dev.util.PersistenceBackedObject;
import com.google.gwt.dev.util.StringInterningObjectInputStream;
import com.google.gwt.dev.util.arg.ArgHandlerDeployDir;
import com.google.gwt.dev.util.arg.ArgHandlerExtraDir;
import com.google.gwt.dev.util.arg.ArgHandlerSaveSourceOutput;
import com.google.gwt.dev.util.arg.ArgHandlerWarDir;
import com.google.gwt.dev.util.arg.OptionDeployDir;
import com.google.gwt.dev.util.arg.OptionExtraDir;
import com.google.gwt.dev.util.arg.OptionSaveSourceOutput;
import com.google.gwt.dev.util.arg.OptionWarDir;
import com.google.gwt.dev.util.log.perf.SimpleEvent;
import com.google.gwt.thirdparty.guava.common.collect.Sets;
import com.google.gwt.thirdparty.guava.common.io.MoreFiles;
import com.google.gwt.thirdparty.guava.common.io.RecursiveDeleteOption;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;

/**
 * Performs the last phase of compilation, merging the compilation outputs.
 */
public class Link {
  /**
   * Options for Link.
   */
  public interface LinkOptions extends OptionExtraDir,
      OptionWarDir, OptionDeployDir, OptionSaveSourceOutput, CompileTaskOptions {
  }

  static class ArgProcessor extends CompileArgProcessor {
    public ArgProcessor(LinkOptions options) {
      super(options);
      registerHandler(new ArgHandlerExtraDir(options));
      registerHandler(new ArgHandlerWarDir(options));
      registerHandler(new ArgHandlerDeployDir(options));
      registerHandler(new ArgHandlerSaveSourceOutput(options));
    }

    @Override
    protected String getName() {
      return Link.class.getName();
    }
  }

  /**
   * Concrete class to implement link options.
   */
  static class LinkOptionsImpl extends CompileTaskOptionsImpl implements
      LinkOptions {

    private File deployDir;
    private File extraDir;
    private File warDir;
    private File debugSourceDir;

    public LinkOptionsImpl() {
    }

    public LinkOptionsImpl(LinkOptions other) {
      copyFrom(other);
    }

    public void copyFrom(LinkOptions other) {
      super.copyFrom(other);
      setDeployDir(other.getDeployDir());
      setExtraDir(other.getExtraDir());
      setSaveSourceOutput(other.getSaveSourceOutput());
      setWarDir(other.getWarDir());
    }

    @Override
    public File getSaveSourceOutput() {
      return debugSourceDir;
    }

    @Override
    public File getDeployDir() {
      return (deployDir == null) ? new File(warDir, "WEB-INF/deploy")
          : deployDir;
    }

    @Override
    public File getExtraDir() {
      return extraDir;
    }

    @Override
    public File getWarDir() {
      return warDir;
    }

    @Override
    public void setSaveSourceOutput(File dest) {
      this.debugSourceDir = dest;
    }

    @Override
    public void setDeployDir(File dir) {
      deployDir = dir;
    }

    @Override
    public void setExtraDir(File extraDir) {
      this.extraDir = extraDir;
    }

    @Override
    public void setWarDir(File warDir) {
      this.warDir = warDir;
    }
  }

  public static void link(TreeLogger logger, ModuleDef module, ResourceOracle publicResourceOracle,
      ArtifactSet generatedArtifacts, Permutation[] permutations,
      List<PersistenceBackedObject<PermutationResult>> resultFiles,
      Set<PermutationResult> libraries, PrecompileTaskOptions precompileOptions,
      LinkOptions linkOptions)
      throws UnableToCompleteException, IOException {
    StandardLinkerContext linkerContext =
        new StandardLinkerContext(logger, module, publicResourceOracle,
            precompileOptions.getOutput());
    ArtifactSet artifacts = doSimulatedShardingLink(
        logger, module, linkerContext, generatedArtifacts, permutations, resultFiles);

    doProduceOutput(logger, artifacts, linkerContext, module, precompileOptions.shouldSaveSource(),
        linkOptions);
  }

  /**
   * This link operation is performed on a CompilePerms shard for one
   * permutation. It sees the generated artifacts for one permutation compile,
   * and it runs the per-permutation part of each shardable linker.
   */
  public static void linkOnePermutationToJar(TreeLogger logger,
      ModuleDef module, ResourceOracle publicResourceOracle, ArtifactSet generatedArtifacts,
      PermutationResult permResult, File jarFile,
      PrecompileTaskOptions precompileOptions) throws UnableToCompleteException {
    try {
      if (jarFile.exists()) {
        boolean success = jarFile.delete();
        if (!success) {
          logger.log(TreeLogger.ERROR, "Linker output file " + jarFile.getName()
              + " already exists and can't be deleted.");
        }
      }
      JarOutputStream jar = new JarOutputStream(new FileOutputStream(jarFile));

      StandardLinkerContext linkerContext = new StandardLinkerContext(logger,
          module, publicResourceOracle, precompileOptions.getOutput());

      StandardCompilationResult compilation = new StandardCompilationResult(
          permResult);
      addSelectionPermutations(compilation, permResult.getPermutation(),
          linkerContext);
      ArtifactSet permArtifacts = new ArtifactSet(generatedArtifacts);
      permArtifacts.addAll(permResult.getArtifacts());
      permArtifacts.add(compilation);

      ArtifactSet linkedArtifacts = linkerContext.invokeLinkForOnePermutation(
          logger, compilation, permArtifacts);

      // Write the data of emitted artifacts
      for (EmittedArtifact art : linkedArtifacts.find(EmittedArtifact.class)) {
        Visibility visibility = art.getVisibility();
        String jarEntryPath = visibility.name() + "/";
        if (visibility == Visibility.Public) {
          jarEntryPath += art.getPartialPath();
        } else {
          jarEntryPath += prefixArtifactPath(art, linkerContext);
        }
        ZipEntry ze = new ZipEntry(jarEntryPath);
        ze.setTime(OutputFileSetOnJar.normalizeTimestamps ? 0 : art.getLastModified());
        jar.putNextEntry(ze);
        art.writeTo(logger, jar);
        jar.closeEntry();
      }

      // Serialize artifacts marked as Transferable
      int numSerializedArtifacts = 0;
      // The raw type Artifact is to work around a Java compiler bug:
      // http://bugs.sun.com/view_bug.do?bug_id=6548436
      for (Artifact art : linkedArtifacts) {
        if (art.isTransferableFromShards() && !(art instanceof EmittedArtifact)) {
          String jarEntryPath = "arts/" + numSerializedArtifacts++;
          ZipEntry ze = new ZipEntry(jarEntryPath);
          if (OutputFileSetOnJar.normalizeTimestamps) {
            ze.setTime(0);
          }
          jar.putNextEntry(ze);
          ObjectOutputStream objectStream = new ObjectOutputStream(jar);
          objectStream.writeObject(art);
          // Explicitly not closing, only flushing, so that closeEntry() can close
          objectStream.flush();

          jar.closeEntry();
        }
      }

      jar.close();
    } catch (IOException e) {
      logger.log(TreeLogger.ERROR, "Error linking", e);
      throw new UnableToCompleteException();
    }
  }

  public static void main(String[] args) {
    boolean success = false;
    try (SimpleEvent ignored = new SimpleEvent("Link")) {
      /*
       * NOTE: main always exits with a call to System.exit to terminate any
       * non-daemon threads that were started in Generators. Typically, this is to
       * shutdown AWT related threads, since the contract for their termination is
       * still implementation-dependent.
       */
      final LinkOptions options = new LinkOptionsImpl();

      if (new ArgProcessor(options).processArgs(args)) {
        CompileTask task = new CompileTask() {
          @Override
          public boolean run(TreeLogger logger) throws UnableToCompleteException {
            return new Link(options).run(logger);
          }
        };
        if (CompileTaskRunner.runWithAppropriateLogger(options, task)) {
          success = true;
        }
      }
    }
    System.exit(success ? 0 : 1);
  }

  /**
   * In a parallel build, artifact sets are thinned down in transit between
   * compilation and linking. All emitted artifacts are changed to binary
   * emitted artifacts, and all other artifacts are dropped except @Transferable
   * ones. This method simulates the thinning that happens in a parallel build.
   */
  @SuppressWarnings("rawtypes")
  public static ArtifactSet simulateTransferThinning(ArtifactSet artifacts,
      StandardLinkerContext context) {
    ArtifactSet thinnedArtifacts = new ArtifactSet();
    // The raw type Artifact is to work around a compiler bug:
    // http://bugs.sun.com/view_bug.do?bug_id=6548436
    for (Artifact artifact : artifacts) {
      if (artifact instanceof EmittedArtifact) {
        EmittedArtifact emittedArtifact = (EmittedArtifact) artifact;
        String path = getFullArtifactPath(emittedArtifact, context);
        thinnedArtifacts.add(new BinaryOnlyArtifactWrapper(path,
            emittedArtifact));
      } else if (artifact.isTransferableFromShards()) {
        thinnedArtifacts.add(artifact);
      }
    }

    return thinnedArtifacts;
  }

  /**
   * Add to a compilation result all of the selection permutations from its
   * associated permutation.
   */
  private static void addSelectionPermutations(
      StandardCompilationResult compilation, Permutation permutation,
      StandardLinkerContext linkerContext) {
    for (BindingProperties properties : permutation.getProperties().getSoftProperties()) {
      compilation.addSelectionPermutation(computeSelectionPermutation(linkerContext, properties));
      compilation.addSoftPermutation(computeSoftPermutation(linkerContext, properties));
    }
  }

  /**
   * Choose an output file set for the given <code>dirOrJar</code> based on its
   * name, whether it's null, and whether it already exists as a directory.
   */
  static OutputFileSet chooseOutputFileSet(File dirOrJar,
      String pathPrefix) throws IOException {
    if (dirOrJar == null) {
      return new NullOutputFileSet();
    }

    String name = dirOrJar.getName();
    if (!dirOrJar.isDirectory() && (name.endsWith(".war")
        || name.endsWith(".jar") || name.endsWith(".zip"))) {
      return new OutputFileSetOnJar(dirOrJar, pathPrefix);
    } else {
      File target = new File(dirOrJar, pathPrefix);
      if (target.exists()) {
        MoreFiles.deleteDirectoryContents(target.toPath(), RecursiveDeleteOption.ALLOW_INSECURE);
      }
      return new OutputFileSetOnDirectory(dirOrJar, pathPrefix);
    }
  }

  /**
   * Return a map giving the value of each non-trivial selection property.
   */
  private static Map<SelectionProperty, String> computeSelectionPermutation(
      StandardLinkerContext linkerContext, BindingProperties properties) {
    BindingProperty[] orderedProps = properties.getOrderedProps();
    String[] orderedPropValues = properties.getOrderedPropValues();
    Map<SelectionProperty, String> unboundProperties = new HashMap<SelectionProperty, String>();
    for (int i = 0; i < orderedProps.length; i++) {
      SelectionProperty key = linkerContext.getProperty(orderedProps[i].getName());
      if (key.tryGetValue() != null) {
        /*
         * The view of the Permutation doesn't include properties with defined
         * values.
         */
        continue;
      } else if (key.isDerived()) {
        /*
         * The property provider does not need to be invoked, because the value
         * is determined entirely by other properties.
         */
        continue;
      }
      unboundProperties.put(key, orderedPropValues[i]);
    }
    return unboundProperties;
  }

  private static Map<SelectionProperty, String> computeSoftPermutation(
      StandardLinkerContext linkerContext, BindingProperties properties) {
    BindingProperty[] orderedProps = properties.getOrderedProps();
    String[] orderedPropValues = properties.getOrderedPropValues();
    Map<SelectionProperty, String> softProperties = new HashMap<SelectionProperty, String>();
    for (int i = 0; i < orderedProps.length; i++) {
      if (orderedProps[i].getCollapsedValuesSets().isEmpty()) {
        continue;
      }

      SelectionProperty key = linkerContext.getProperty(orderedProps[i].getName());
      softProperties.put(key, orderedPropValues[i]);
    }
    return softProperties;
  }

  /**
   * Emit final output.
   */
  private static void doProduceOutput(TreeLogger logger, ArtifactSet artifacts,
      StandardLinkerContext linkerContext, ModuleDef module, boolean saveSources,
      LinkOptions options) throws IOException, UnableToCompleteException {

    // == create output filesets ==

    String destPrefix = module.getName() + "/";

    OutputFileSet outFileSet = chooseOutputFileSet(options.getWarDir(), destPrefix);
    OutputFileSet extraFileSet = chooseOutputFileSet(options.getExtraDir(), destPrefix);

    // allow -deploy and -extra to point to the same directory/jar
    OutputFileSet deployFileSet;
    if (options.getDeployDir().equals(options.getExtraDir())) {
      deployFileSet = extraFileSet;
    } else {
      deployFileSet = chooseOutputFileSet(options.getDeployDir(), destPrefix);
    }

    // == write the output ==

    linkerContext.produceOutput(logger, artifacts, Visibility.Public,
        outFileSet);
    linkerContext.produceOutput(logger, artifacts, Visibility.Deploy,
        deployFileSet);
    linkerContext.produceOutput(logger, artifacts, Visibility.Private,
        extraFileSet);

    boolean embedSourcesContent = linkerContext.getModule()
        .shouldEmbedSourceMapContents();

    if (saveSources && !embedSourcesContent) {
      // Assume that all source code is available in the compiler's classpath.
      // (This will have to be adjusted to work with Super Dev Mode.)
      ResourceLoader loader = ResourceLoaders.fromContextClassLoader();
      SourceSaver.save(logger, artifacts, loader, options, destPrefix, extraFileSet);
    }

    outFileSet.close();
    extraFileSet.close();
    if (deployFileSet != extraFileSet) {
      deployFileSet.close();
    }

    logger.log(TreeLogger.INFO, "Link succeeded");
  }

  /**
   * This link operation simulates sharded linking even though all generating
   * and linking is happening on the same computer. It can tolerate
   * non-shardable linkers.
   */
  private static ArtifactSet doSimulatedShardingLink(TreeLogger logger, ModuleDef module,
      StandardLinkerContext linkerContext, ArtifactSet generatedArtifacts, Permutation[] perms,
      List<PersistenceBackedObject<PermutationResult>> resultFiles)
      throws UnableToCompleteException {
    ArtifactSet combinedArtifacts = new ArtifactSet();
    for (int i = 0; i < perms.length; ++i) {
      ArtifactSet newArtifacts = finishPermutation(
          logger, perms[i], resultFiles.get(i), linkerContext, generatedArtifacts);
      combinedArtifacts.addAll(newArtifacts);
    }

    combinedArtifacts.addAll(linkerContext.getArtifactsForPublicResources(
        logger, module));

    ArtifactSet legacyLinkedArtifacts = linkerContext.invokeLegacyLinkers(
        logger, combinedArtifacts);

    ArtifactSet thinnedArtifacts = simulateTransferThinning(
        legacyLinkedArtifacts, linkerContext);

    return linkerContext.invokeFinalLink(logger, thinnedArtifacts);
  }

  /**
   * Add a compilation to a linker context. Also runs the shardable part of all
   * linkers that support sharding.
   *
   * @return the new artifacts generated by the shardable part of this link
   *         operation
   */
  private static ArtifactSet finishPermutation(TreeLogger logger, Permutation perm,
      PersistenceBackedObject<PermutationResult> resultFile, StandardLinkerContext linkerContext,
      ArtifactSet generatedArtifacts)
      throws UnableToCompleteException {
    PermutationResult permResult = resultFile.newInstance(logger);
    StandardCompilationResult compilation =
        new StandardCompilationResult(permResult);
    addSelectionPermutations(compilation, perm, linkerContext);
    logScriptSize(logger, perm.getId(), compilation);

    ArtifactSet permArtifacts = new ArtifactSet(generatedArtifacts);
    permArtifacts.addAll(permResult.getArtifacts());
    permArtifacts.add(compilation);
    permArtifacts.freeze();
    return linkerContext.invokeLinkForOnePermutation(logger, compilation,
        permArtifacts);
  }

  private static String getFullArtifactPath(EmittedArtifact emittedArtifact,
      StandardLinkerContext context) {
    String path = emittedArtifact.getPartialPath();
    if (emittedArtifact.getVisibility() != Visibility.Public) {
      path = prefixArtifactPath(emittedArtifact, context);
    }
    return path;
  }

  /**
   * Logs the total script size for this permutation, as calculated by
   * CodeSplitter2#totalScriptSize(int[]).
   */
  private static void logScriptSize(TreeLogger logger, int permId,
      StandardCompilationResult compilation) {
    if (!logger.isLoggable(TreeLogger.TRACE)) {
      return;
    }

    String[] javaScript = compilation.getJavaScript();

    int[] jsLengths = new int[javaScript.length];
    for (int i = 0; i < javaScript.length; i++) {
      jsLengths[i] = javaScript[i].length();
    }

    // TODO(acleung): This is broken for CodeSplitter2.
    int totalSize = CodeSplitter.computeTotalSize(jsLengths);

    if (logger.isLoggable(TreeLogger.TRACE)) {
      logger.log(TreeLogger.TRACE, "Permutation " + permId + " (strong name "
          + compilation.getStrongName() + ") has an initial download size of "
          + javaScript[0].length() + " and total script size of " + totalSize);
    }
  }

  /**
   * Prefix an artifact's partial path with the linker name and make sure it is
   * a relative pathname.
   *
   * @param art
   * @param linkerContext
   * @return prefixed path
   */
  private static String prefixArtifactPath(EmittedArtifact art,
      StandardLinkerContext linkerContext) {
    String pathWithLinkerName = linkerContext.getExtraPathForLinker(
        art.getLinker(), art.getPartialPath());
    if (pathWithLinkerName.startsWith("/")) {
      // This happens if the linker has no extra path
      pathWithLinkerName = pathWithLinkerName.substring(1);
    }
    return pathWithLinkerName;
  }

  private static ArtifactSet scanCompilePermResults(TreeLogger logger,
      List<File> resultFiles) throws IOException, UnableToCompleteException {
    final ArtifactSet artifacts = new ArtifactSet();

    for (File resultFile : resultFiles) {
      JarFile jarFile = null;
      try {
        jarFile = new JarFile(resultFile);
      } catch (ZipException ze) {
        logger.log(TreeLogger.ERROR, "Error opening " + resultFile
            + " as jar file.", ze);
        throw new UnableToCompleteException();
      }

      Enumeration<JarEntry> entries = jarFile.entries();
      while (entries.hasMoreElements()) {
        JarEntry entry = entries.nextElement();
        if (entry.isDirectory()) {
          continue;
        }

        String path;
        Artifact<?> artForEntry = null;

        String entryName = entry.getName();
        if (entryName.startsWith("arts/")) {
          try {
            try (InputStream inputStream = new BufferedInputStream(jarFile.getInputStream(entry));
                 ObjectInputStream objectInputStream =
                     new StringInterningObjectInputStream(inputStream)) {
              artForEntry = (Artifact<?>) objectInputStream.readObject();
            }
            assert artForEntry.isTransferableFromShards();
          } catch (ClassNotFoundException e) {
            logger.log(TreeLogger.ERROR,
                "Failed trying to deserialize an artifact", e);
            throw new UnableToCompleteException();
          }
        } else {
          int slash = entryName.indexOf('/');
          if (slash >= 0) {
            try {
              Visibility visibility = Visibility.valueOf(entryName.substring(0,
                  slash));
              path = entryName.substring(slash + 1);
              JarEntryEmittedArtifact jarArtifact = new JarEntryEmittedArtifact(
                  path, resultFile, entry);
              jarArtifact.setVisibility(visibility);
              artForEntry = jarArtifact;
            } catch (IllegalArgumentException e) {
              // silently ignore paths with invalid visibilities
              continue;
            }
          }
        }

        artifacts.add(artForEntry);
      }

      jarFile.close();
    }

    return artifacts;
  }

  private final LinkOptionsImpl options;

  private final CompilerContext.Builder compilerContextBuilder = new CompilerContext.Builder();

  private CompilerContext compilerContext;

  public Link(LinkOptions options) {
    this.options = new LinkOptionsImpl(options);
    compilerContext =
        compilerContextBuilder.options(new PrecompileTaskOptionsImpl(options)).build();
  }

  public boolean run(TreeLogger logger) throws UnableToCompleteException {
    loop_modules : for (String moduleName : options.getModuleNames()) {
      ModuleDef module = ModuleDefLoader.loadFromClassPath(logger, moduleName);
      compilerContext = compilerContextBuilder.module(module).build();
      ResourceOracle publicResourceOracle = compilerContext.getPublicResourceOracle();

      File compilerWorkDir = options.getCompilerWorkDir(moduleName);

      // Look for the compilerOptions file output from running AnalyzeModule
      PrecompileTaskOptions precompileOptions = AnalyzeModule.readAnalyzeModuleOptionsFile(
          logger, compilerWorkDir);

      PrecompilationResult precompileResults = null;
      if (precompileOptions == null) {
        // Check for the output from Precompile where precompiling has
        // been delegated to shards.
        File precompilationFile = new File(compilerWorkDir,
            Precompile.PRECOMPILE_FILENAME);
        precompileResults = CompilePerms.readPrecompilationFile(logger,
            precompilationFile);
        if (precompileResults == null) {
          return false;
        }
        if (precompileResults instanceof PrecompileTaskOptions) {
          precompileOptions = (PrecompileTaskOptions) precompileResults;
        }
      }

      if (precompileOptions != null) {
        /**
         * Precompiling happened on the shards.
         */
        if (!doLinkFinal(
            logger, compilerWorkDir, module, publicResourceOracle, precompileOptions)) {
          return false;
        }
        continue loop_modules;
      } else {
        /**
         * Precompiling happened on the start node.
         */
        Precompilation precomp = (Precompilation) precompileResults;
        Permutation[] perms = precomp.getPermutations();
        List<PersistenceBackedObject<PermutationResult>> resultFiles =
            CompilePerms.makeResultFiles(compilerWorkDir, perms, compilerContext.getOptions());

        // Check that all files are present
        for (PersistenceBackedObject<PermutationResult> file : resultFiles) {
          if (!file.exists()) {
            logger.log(TreeLogger.ERROR,
                "File not found '" + file.getPath() + "'; please compile all permutations");
            return false;
          }
        }

        TreeLogger branch = logger.branch(TreeLogger.INFO, "Linking module "
            + module.getName());

        try {
          link(branch, module, publicResourceOracle, precomp.getGeneratedArtifacts(), perms,
              resultFiles, Sets.<PermutationResult> newHashSet(),
              precomp.getUnifiedAst().getOptions(), options);
        } catch (IOException e) {
          logger.log(TreeLogger.ERROR,
              "Unexpected exception while producing output", e);
          throw new UnableToCompleteException();
        }
      }
    }
    return true;
  }

  /**
   * Do a final link, assuming the precompiles were done on the CompilePerms
   * shards. Returns true if successful.
   */
  private boolean doLinkFinal(TreeLogger logger, File compilerWorkDir, ModuleDef module,
      ResourceOracle publicResourceOracle, PrecompileTaskOptions precompileOptions)
      throws UnableToCompleteException {
    int numPermutations = new PropertyCombinations(module.getProperties(),
        module.getActiveLinkerNames()).collapseProperties().size();
    List<File> resultFiles = new ArrayList<File>(numPermutations);
    for (int i = 0; i < numPermutations; ++i) {
      File f = CompilePerms.makePermFilename(compilerWorkDir, i);
      if (!f.exists()) {
        logger.log(TreeLogger.ERROR, "File not found '" + f.getAbsolutePath()
            + "'; please compile all permutations");
        return false;
      }
      resultFiles.add(f);
    }

    TreeLogger branch = logger.branch(TreeLogger.INFO, "Linking module "
        + module.getName());
    StandardLinkerContext linkerContext = new StandardLinkerContext(branch,
        module, publicResourceOracle, precompileOptions.getOutput());

    try {
      ArtifactSet artifacts = scanCompilePermResults(logger, resultFiles);
      artifacts.addAll(linkerContext.getArtifactsForPublicResources(logger,
          module));
      artifacts = linkerContext.invokeFinalLink(logger, artifacts);

      doProduceOutput(logger, artifacts, linkerContext, module,
          precompileOptions.shouldSaveSource(), options);
    } catch (IOException e) {
      logger.log(TreeLogger.ERROR, "Exception during final linking", e);
      throw new UnableToCompleteException();
    }

    return true;
  }
}
