var CS = ConstraintSolver;
var PV = PackageVersion;

var pvVar = function (p, v) {
  return p + ' ' + v;
};

// The "inner solver".  You construct it with a ConstraintSolver.Input object
// (which specifies the problem) and then call .getAnswer() on it.

CS.Solver = function (input, options) {
  var self = this;
  check(input, CS.Input);

  self.input = input;
  self.errors = []; // [String]

  self.pricer = new CS.VersionPricer();
  self.getConstraintFormula = _.memoize(_getConstraintFormula,
                                         function (p, vConstraint) {
                                           return p + "@" + vConstraint.raw;
                                         });

  self.options = options || {};
  self.Profile = (self.options.Profile || CS.DummyProfile);

  self.steps = [];
  self.stepsByName = {};

  self.analysis = {};

  self.Profile.time("Solver#analyze", function () {
    self.analyze();
  });

  self.logic = null; // Logic.Solver, initialized later
};

CS.Solver.prototype.throwAnyErrors = function () {
  if (this.errors.length) {
    var multiline = _.any(this.errors, function (e) {
      return /\n/.test(e);
    });
    CS.throwConstraintSolverError(this.errors.join(
      multiline ? '\n\n' : '\n'));
  }
};

CS.Solver.prototype.getVersions = function (pkg) {
  var self = this;
  if (_.has(self.analysis.allowedVersions, pkg)) {
    return self.analysis.allowedVersions[pkg];
  } else {
    return self.input.catalogCache.getPackageVersions(pkg);
  }
};

// Populates `self.analysis` with various data structures derived from the
// input.  May also throw errors, and may call methods that rely on
// analysis once that particular analysis is done (e.g. `self.getVersions`
// which relies on `self.analysis.allowedVersions`.
CS.Solver.prototype.analyze = function () {
  var self = this;
  var analysis = self.analysis;
  var input = self.input;
  var cache = input.catalogCache;
  var Profile = self.Profile;

  ////////// ANALYZE ALLOWED VERSIONS
  // (An "allowed version" is one that isn't ruled out by a top-level
  // constraint.)

  // package -> array of version strings.  If a package has an entry in
  // this map, then only the versions in the array are allowed for
  // consideration.
  analysis.allowedVersions = {};
  analysis.packagesWithNoAllowedVersions = {}; // package -> [constraints]

  // Process top-level constraints, applying them right now by
  // limiting what package versions we even consider.  This speeds up
  // solving, especially given the equality constraints on core
  // packages.  For versions we don't allow, we get to avoid generating
  // Constraint objects for their constraints, which saves us both
  // clause generation time and solver work up through the point where we
  // determine there are no conflicts between constraints.
  //
  // we can't throw any errors yet, because `input.constraints`
  // doesn't establish any dependencies (so we don't know if it's a
  // problem that some package has no legal versions), but we can
  // track such packages in packagesWithNoAllowedVersions so that we
  // throw a good error later.
  Profile.time("analyze allowed versions", function () {
    _.each(_.groupBy(input.constraints, 'package'), function (cs, p) {
      var versions = cache.getPackageVersions(p);
      if (! versions.length) {
        // deal with wholly unknown packages later
        return;
      }
      _.each(cs, function (constr) {
        versions = _.filter(versions, function (v) {
          return CS.isConstraintSatisfied(p, constr.versionConstraint, v);
        });
      });
      if (! versions.length) {
        analysis.packagesWithNoAllowedVersions[p] = _.filter(cs, function (c) {
          return !! c.constraintString;
        });
      }
      analysis.allowedVersions[p] = versions;
    });
  });

  ////////// ANALYZE ROOT DEPENDENCIES

  // Collect root dependencies that we've never heard of.
  analysis.unknownRootDeps = [];
  // Collect "previous solution" versions of root dependencies.
  analysis.previousRootDepVersions = [];

  Profile.time("analyze root dependencies", function () {
    _.each(input.dependencies, function (p) {
      if (! input.isKnownPackage(p)) {
        analysis.unknownRootDeps.push(p);
      } else if (input.isInPreviousSolution(p) &&
                 ! input.isUpgrading(p)) {
        analysis.previousRootDepVersions.push(new CS.PackageAndVersion(
          p, input.previousSolution[p]));
      }
    });

    // throw if there are unknown packages in root deps
    if (analysis.unknownRootDeps.length) {
      _.each(analysis.unknownRootDeps, function (p) {
        if (CS.isIsobuildFeaturePackage(p)) {
          self.errors.push(
            'unsupported Isobuild feature "' + p +
            '" in top-level dependencies; see ' +
            'https://docs.meteor.com/api/packagejs.html#isobuild-features ' +
            'for a list of features and the minimum Meteor release required'
          );
        } else {
          self.errors.push('unknown package in top-level dependencies: ' + p);
        }
      });
      self.throwAnyErrors();
    }
  });

  ////////// ANALYZE REACHABILITY

  // A "reachable" package is one that is either a root dependency or
  // a strong dependency of any "allowed" version of a reachable package.
  // In other words, we walk all strong dependencies starting
  // with the root dependencies, and visiting all allowed versions of each
  // package.
  //
  // This analysis is mainly done for performance, because if there are
  // extraneous packages in the CatalogCache (for whatever reason) we
  // want to spend as little time on them as possible.  It also establishes
  // the universe of possible "known" and "unknown" packages we might
  // come across.
  //
  // A more nuanced reachability analysis that takes versions into account
  // is probably possible.

  // package name -> true
  analysis.reachablePackages = {};
  // package name -> package versions asking for it (in pvVar form)
  analysis.unknownPackages = {};

  var markReachable = function (p) {
    analysis.reachablePackages[p] = true;

    _.each(self.getVersions(p), function (v) {
      _.each(cache.getDependencyMap(p, v), function (dep) {
        // `dep` is a CS.Dependency
        var p2 = dep.packageConstraint.package;
        if (! input.isKnownPackage(p2)) {
          // record this package so we will generate a variable
          // for it.  we'll try not to select it, and ultimately
          // throw an error if we are forced to.
          if (! _.has(analysis.unknownPackages, p2)) {
            analysis.unknownPackages[p2] = [];
          }
          analysis.unknownPackages[p2].push(pvVar(p, v));
        } else {
          if (! dep.isWeak) {
            if (! _.has(analysis.reachablePackages, p2)) {
              markReachable(p2);
            }
          }
        }
      });
    });
  };

  Profile.time("analyze reachability", function () {
    _.each(input.dependencies, markReachable);
  });

  ////////// ANALYZE CONSTRAINTS

  // Array of CS.Solver.Constraint
  analysis.constraints = [];
  // packages `foo` such that there's a simple top-level equality
  // constraint about `foo`.  package name -> true.
  analysis.topLevelEqualityConstrainedPackages = {};

  Profile.time("analyze constraints", function () {
    // top-level constraints
    _.each(input.constraints, function (c) {
      if (c.constraintString) {
        analysis.constraints.push(new CS.Solver.Constraint(
          null, c.package, c.versionConstraint,
          "constraint#" + analysis.constraints.length));

        if (c.versionConstraint.alternatives.length === 1 &&
            c.versionConstraint.alternatives[0].type === 'exactly') {
          analysis.topLevelEqualityConstrainedPackages[c.package] = true;
        }
      }
    });

    // constraints specified in package dependencies
    _.each(_.keys(analysis.reachablePackages), function (p) {
      _.each(self.getVersions(p), function (v) {
        var pv = pvVar(p, v);
        _.each(cache.getDependencyMap(p, v), function (dep) {
          // `dep` is a CS.Dependency
          var p2 = dep.packageConstraint.package;
          if (input.isKnownPackage(p2) &&
              dep.packageConstraint.constraintString) {
            analysis.constraints.push(new CS.Solver.Constraint(
              pv, p2, dep.packageConstraint.versionConstraint,
              "constraint#" + analysis.constraints.length));
          }
        });
      });
    });
  });

  ////////// ANALYZE PRE-RELEASES

  Profile.time("analyze pre-releases", function () {
    var unanticipatedPrereleases = [];
    _.each(_.keys(analysis.reachablePackages), function (p) {
      var anticipatedPrereleases = input.anticipatedPrereleases[p];
      _.each(self.getVersions(p), function (v) {
        if (/-/.test(v) && ! (anticipatedPrereleases &&
                              _.has(anticipatedPrereleases, v))) {
          unanticipatedPrereleases.push(pvVar(p, v));
        }
      });
    });
    analysis.unanticipatedPrereleases = unanticipatedPrereleases;
  });
};

var WholeNumber = Match.Where(Logic.isWholeNumber);

// A Step consists of a name, an array of terms, and an array of weights.
// Steps are optimized one by one.  Optimizing a Step means to find
// the minimum whole number value for the weighted sum of the terms,
// and then to enforce in the solver that the weighted sum be that number.
// Thus, when the Steps are optimized in sequence, earlier Steps take
// precedence and will stay minimized while later Steps are optimized.
//
// A term can be a package name, a package version, or any other variable
// name or Logic formula.
//
// A weight is a non-negative integer.  The weights array can be a single
// weight (which is used for all terms).
//
// The terms and weights arguments each default to [].  You can add terms
// with weights using addTerm.
//
// options is optional.
CS.Solver.Step = function (name, terms, weights) {
  check(name, String);
  terms = terms || [];
  check(terms, [String]);
  weights = (weights == null ? [] : weights);
  check(weights, Match.OneOf([WholeNumber], WholeNumber));

  this.name = name;

  // mutable:
  this.terms = terms;
  this.weights = weights;
  this.optimum = null; // set when optimized
};

// If weights is a single number, you can omit the weight argument.
// Adds a term.  If weight is 0, addTerm may skip it.
CS.Solver.Step.prototype.addTerm = function (term, weight) {
  if (weight == null) {
    if (typeof this.weights !== 'number') {
      throw new Error("Must specify a weight");
    }
    weight = this.weights;
  }
  check(weight, WholeNumber);
  if (weight !== 0) {
    this.terms.push(term);
    if (typeof this.weights === 'number') {
      if (weight !== this.weights) {
        throw new Error("Can't specify a different weight now: " +
                        weight + " != " + this.weights);
      }
    } else {
      this.weights.push(weight);
    }
  }
};

var DEBUG = false;

// Call as one of:
// * minimize(step, options)
// * minimize([step1, step2, ...], options)
// * minimize(stepName, costTerms, costWeights, options)
CS.Solver.prototype.minimize = function (step, options) {
  var self = this;

  if (_.isArray(step)) {
    // minimize([steps...], options)
    _.each(step, function (st) {
      self.minimize(st, options);
    });
    return;
  }

  if (typeof step === 'string') {
    // minimize(stepName, costTerms, costWeights, options)
    var stepName_ = arguments[0];
    var costTerms_ = arguments[1];
    var costWeights_ = arguments[2];
    var options_ = arguments[3];
    if (costWeights_ && typeof costWeights_ === 'object' &&
        ! _.isArray(costWeights_)) {
      options_ = costWeights_;
      costWeights_ = null;
    }
    var theStep = new CS.Solver.Step(
      stepName_, costTerms_, (costWeights_ == null ? 1 : costWeights_));
    self.minimize(theStep, options_);
    return;
  }

  // minimize(step, options);

  self.Profile.time("minimize " + step.name, function () {

    var logic = self.logic;

    self.steps.push(step);
    self.stepsByName[step.name] = step;

    if (DEBUG) {
      console.log("--- MINIMIZING " + step.name);
    }

    var costWeights = step.weights;
    var costTerms = step.terms;

    var optimized = groupMutuallyExclusiveTerms(costTerms, costWeights);

    self.setSolution(logic.minimizeWeightedSum(
      self.solution, optimized.costTerms, optimized.costWeights, {
        progress: function (status, cost) {
          if (self.options.nudge) {
            self.options.nudge();
          }
          if (DEBUG) {
            if (status === 'improving') {
              console.log(cost + " ... trying to improve ...");
            } else if (status === 'trying') {
              console.log("... trying " + cost + " ... ");
            }
          }
        },
        strategy: (options && options.strategy)
      }));

    step.optimum = self.solution.getWeightedSum(costTerms, costWeights);
    if (DEBUG) {
      console.log(step.optimum + " is optimal");

      if (step.optimum) {
        _.each(costTerms, function (t, i) {
          var w = (typeof costWeights === 'number' ? costWeights :
                   costWeights[i]);
          if (w && self.solution.evaluate(t)) {
            console.log("    " + w + ": " + t);
          }
        });
      }
    }
  });
};

// This is a correctness-preserving performance optimization.
//
// Cost functions often have many terms where both the package name
// and the weight are the same.  For example, when optimizing major
// version, we might have `(foo 3.0.0)*2 + (foo 3.0.1)*2 ...`.  It's
// more efficient to give the solver `((foo 3.0.0) OR (foo 3.0.1) OR
// ...)*2 + ...`, because it separates the question of whether to use
// ANY `foo 3.x.x` variable from the question of which one.  Other
// constraints already enforce the fact that `foo 3.0.0` and `foo 3.0.1`
// are mutually exclusive variables.  We can use that fact to "relax"
// that relationship for the purposes of the weighted sum.
//
// Note that shuffling up the order of terms unnecessarily seems to
// impact performance, so it's significant that we group by package
// first, then weight, rather than vice versa.
var groupMutuallyExclusiveTerms = function (costTerms, costWeights) {
  // Return a key for a term, such that terms with the same key are
  // guaranteed to be mutually exclusive.  We assume each term is
  // a variable representing either a package or a package version.
  // We take a prefix of the variable name up to and including the
  // first space.  So "foo 1.0.0" becomes "foo " and "foo" stays "foo".
  var getTermKey = function (t) {
    var firstSpace = t.indexOf(' ');
    return firstSpace < 0 ? t : t.slice(0, firstSpace+1);
  };

  // costWeights, as usual, may be a number or an array
  if (typeof costWeights === 'number') {
    return {
      costTerms: _.map(_.groupBy(costTerms, getTermKey), function (group) {
        return Logic.or(group);
      }),
      costWeights: costWeights
    };
  } else if (! costTerms.length) {
    return { costTerms: costTerms, costWeights: costWeights };
  } else {
    var weightedTerms = _.zip(costWeights, costTerms);
    var newWeightedTerms = _.map(_.groupBy(weightedTerms, function (wt) {
      // construct a string from the weight and term key, for grouping
      // purposes.  since the weight comes first, there's no ambiguity
      // and the separator char could be pretty much anything.
      return wt[0] + ' ' + getTermKey(wt[1]);
    }), function (wts) {
      return [wts[0][0], Logic.or(_.pluck(wts, 1))];
    });
    return {
      costTerms: _.pluck(newWeightedTerms, 1),
      costWeights: _.pluck(newWeightedTerms, 0)
    };
  }

};

// Determine the non-zero contributions to the cost function in `step`
// based on the current solution, returning a map from term (usually
// the name of a package or package version) to positive integer cost.
CS.Solver.prototype.getStepContributions = function (step) {
  var self = this;
  var solution = self.solution;
  var contributions = {};
  var weights = step.weights;
  _.each(step.terms, function (t, i) {
    var w = (typeof weights === 'number' ? weights : weights[i]);
    if (w && self.solution.evaluate(t)) {
      contributions[t] = w;
    }
  });
  return contributions;
};

var addCostsToSteps = function (pkg, versions, costs, steps) {
  var pvs = _.map(versions, function (v) {
    return pvVar(pkg, v);
  });
  for (var j = 0; j < steps.length; j++) {
    var step = steps[j];
    var costList = costs[j];
    if (costList.length !== versions.length) {
      throw new Error("Assertion failure: Bad lengths in addCostsToSteps");
    }
    for (var i = 0; i < versions.length; i++) {
      step.addTerm(pvs[i], costList[i]);
    }
  }
};

// Get an array of "Steps" that, when minimized in order, optimizes
// the package version costs of `packages` (an array of String package
// names) according to `pricerMode`, which may be
// `CS.VersionPricer.MODE_UPDATE` or a similar mode constant.
// Wraps `VersionPricer#priceVersions`, which is tasked with calculating
// the cost of every version of every package.  This function iterates
// over `packages` and puts the result into `Step` objects.
CS.Solver.prototype.getVersionCostSteps = function (stepBaseName, packages,
                                                    pricerMode) {
  var self = this;
  var major = new CS.Solver.Step(stepBaseName + '_major');
  var minor = new CS.Solver.Step(stepBaseName + '_minor');
  var patch = new CS.Solver.Step(stepBaseName + '_patch');
  var rest = new CS.Solver.Step(stepBaseName + '_rest');

  self.Profile.time(
    "calculate " + stepBaseName + " version costs",
    function () {
      _.each(packages, function (p) {
        var versions = self.getVersions(p);
        if (versions.length >= 2) {
          var costs = self.pricer.priceVersions(versions, pricerMode);
          addCostsToSteps(p, versions, costs, [major, minor, patch, rest]);
        }
      });
    });

  return [major, minor, patch, rest];
};

// Like `getVersionCostSteps`, but wraps
// `VersionPricer#priceVersionsWithPrevious` instead of `#priceVersions`.
// The cost function is "distance" from the previous versions passed in
// as `packageAndVersion`.  (Actually it's a complicated function of the
// previous and new version.)
CS.Solver.prototype.getVersionDistanceSteps = function (stepBaseName,
                                                        packageAndVersions,
                                                        takePatches) {
  var self = this;

  var incompat = new CS.Solver.Step(stepBaseName + '_incompat');
  var major = new CS.Solver.Step(stepBaseName + '_major');
  var minor = new CS.Solver.Step(stepBaseName + '_minor');
  var patch = new CS.Solver.Step(stepBaseName + '_patch');
  var rest = new CS.Solver.Step(stepBaseName + '_rest');

  self.Profile.time(
    "calculate " + stepBaseName + " distance costs",
    function () {
      _.each(packageAndVersions, function (pvArg) {
        var pkg = pvArg.package;
        var previousVersion = pvArg.version;
        var versions = self.getVersions(pkg);
        if (versions.length >= 2) {
          var costs = self.pricer.priceVersionsWithPrevious(
            versions, previousVersion, takePatches);
          addCostsToSteps(pkg, versions, costs,
                          [incompat, major, minor, patch, rest]);
        }
      });
    });

  return [incompat, major, minor, patch, rest];
};

CS.Solver.prototype.currentVersionMap = function () {
  var self = this;
  var pvs = [];
  _.each(self.solution.getTrueVars(), function (x) {
    if (x.indexOf(' ') >= 0) {
      // all variables with spaces in them are PackageAndVersions
      var pv = CS.PackageAndVersion.fromString(x);
      pvs.push(pv);
    }
  });

  var versionMap = {};
  _.each(pvs, function (pv) {
    if (_.has(versionMap, pv.package)) {
      throw new Error("Assertion failure: Selected two versions of " +
                      pv.package + ", " +versionMap[pv.package] +
                      " and " + pv.version);
    }
    versionMap[pv.package] = pv.version;
  });

  return versionMap;
};

// Called to re-assign `self.solution` after a call to `self.logic.solve()`,
// `solveAssuming`, or `minimize`.
CS.Solver.prototype.setSolution = function (solution) {
  var self = this;
  self.solution = solution;
  if (! self.solution) {
    throw new Error("Unexpected unsatisfiability");
  }
  // When we query a Solution, we always want to treat unknown variables
  // as "false".  Logic Solver normally throws an error if you ask it
  // to evaluate a formula containing a variable that isn't found in any
  // constraints, as a courtesy to help catch bugs, but we treat
  // variables as an open class of predicates ("foo" means package foo
  // is selected, for example), and we don't ensure that every package
  // or package version we might ask about is registered with the Solver.
  // For example, when we go to explain a conflict or generate an error
  // about an unknown package, we may ask about packages that were
  // forbidden in an early analysis of the problem and never entered
  // into the Solver.
  self.solution.ignoreUnknownVariables();
};

CS.Solver.prototype.getAnswer = function (options) {
  var self = this;
  return self.Profile.time("Solver#getAnswer", function () {
    return self._getAnswer(options);
  });
};

CS.Solver.prototype._getAnswer = function (options) {
  var self = this;
  var input = self.input;
  var analysis = self.analysis;
  var cache = input.catalogCache;
  var allAnswers = (options && options.allAnswers); // for tests
  var Profile = self.Profile;

  var logic;
  Profile.time("new Logic.Solver (MiniSat start-up)", function () {
    logic = self.logic = new Logic.Solver();
  });

  // require root dependencies
  Profile.time("require root dependencies", function () {
    _.each(input.dependencies, function (p) {
      logic.require(p);
    });
  });

  // generate package version variables for known, reachable packages
  Profile.time("generate package variables", function () {
    _.each(_.keys(analysis.reachablePackages), function (p) {
      if (! _.has(analysis.packagesWithNoAllowedVersions, p)) {
        var versionVars = _.map(self.getVersions(p),
                                function (v) {
                                  return pvVar(p, v);
                                });
        // At most one of ["foo 1.0.0", "foo 1.0.1", ...] is true.
        logic.require(Logic.atMostOne(versionVars));
        // The variable "foo" is true if and only if at least one of the
        // variables ["foo 1.0.0", "foo 1.0.1", ...] is true.
        logic.require(Logic.equiv(p, Logic.or(versionVars)));
      }
    });
  });

  // generate strong dependency requirements
  Profile.time("generate dependency requirements", function () {
    _.each(_.keys(analysis.reachablePackages), function (p) {
      _.each(self.getVersions(p), function (v) {
        _.each(cache.getDependencyMap(p, v), function (dep) {
          // `dep` is a CS.Dependency
          if (! dep.isWeak) {
            var p2 = dep.packageConstraint.package;
            logic.require(Logic.implies(pvVar(p, v), p2));
          }
        });
      });
    });
  });

  // generate constraints -- but technically don't enforce them, because
  // we haven't forced the conflictVars to be false
  Profile.time("generate constraints", function () {
    _.each(analysis.constraints, function (c) {
      // We logically require that EITHER a constraint is marked as a
      // conflict OR it comes from a package version that is not selected
      // OR its constraint formula must be true.
      // (The constraint formula says that if toPackage is selected,
      // then a version of it that satisfies our constraint must be true.)
      logic.require(
        Logic.or(c.conflictVar,
                 c.fromVar ? Logic.not(c.fromVar) : [],
                 self.getConstraintFormula(c.toPackage, c.vConstraint)));
    });
  });

  // Establish the invariant of self.solution being a valid solution.
  // From now on, if we add some new logical requirement to the solver
  // that isn't necessarily true of `self.solution`, we must
  // recalculate `self.solution` and pass the new value to
  // self.setSolution.  It is our job to obtain the new solution in a
  // way that ensures the solution exists and doesn't put the solver
  // in an unsatisfiable state.  There are several ways to do this:
  //
  // * Calling `logic.solve()` and immediately throwing a fatal error
  //   if there's no solution (not calling `setSolution` at all)
  // * Calling `logic.solve()` in a situation where we know we have
  //   not made the problem unsatisfiable
  // * Calling `logic.solveAssuming(...)` and checking the result, only
  //   using the solution if it exists
  // * Calling `minimize()`, which always maintains satisfiability

  Profile.time("pre-solve", function () {
    self.setSolution(logic.solve());
  });
  // There is always a solution at this point, namely,
  // select all packages (including unknown packages), select
  // any version of each known package (excluding packages with
  // "no allowed versions"), and set all conflictVars
  // to true.

  // Forbid packages with no versions allowed by top-level constraints,
  // which we didn't do earlier because we needed to establish an
  // initial solution before asking the solver if it's possible to
  // not use these packages.
  Profile.time("forbid packages with no matching versions", function () {
    _.each(analysis.packagesWithNoAllowedVersions, function (constrs, p) {
      var newSolution = logic.solveAssuming(Logic.not(p));
      if (newSolution) {
        self.setSolution(newSolution);
        logic.forbid(p);
      } else {
        var error =
          'No version of ' + p + ' satisfies all constraints: ' +
            _.map(constrs, function (constr) {
              return '@' + constr.constraintString;
            }).join(', ');
        error += '\n' + self.listConstraintsOnPackage(p);
        self.errors.push(error);
      }
    });
    self.throwAnyErrors();
  });

  // try not to use any unknown packages.  If the minimum is greater
  // than 0, we'll throw an error later, after we apply the constraints
  // and the cost function, so that we can explain the problem to the
  // user in a convincing way.
  self.minimize('unknown_packages', _.keys(analysis.unknownPackages));

  // try not to set the conflictVar on any constraint.  If the minimum
  // is greater than 0, we'll throw an error later, after we've run the
  // cost function, so we can show a better error.
  // If there are conflicts, this minimization can be time-consuming
  // (several seconds or more).  The strategy 'bottom-up' helps by
  // looking for solutions with few conflicts first.
  self.minimize('conflicts', _.pluck(analysis.constraints, 'conflictVar'),
                { strategy: 'bottom-up' });

  // Try not to use "unanticipated" prerelease versions
  self.minimize('unanticipated_prereleases',
                analysis.unanticipatedPrereleases);

  var previousRootSteps = self.getVersionDistanceSteps(
    'previous_root', analysis.previousRootDepVersions);
  // the "previous_root_incompat" step
  var previousRootIncompat = previousRootSteps[0];
  // the "previous_root_major", "previous_root_minor", etc. steps
  var previousRootVersionParts = previousRootSteps.slice(1);

  var toUpdate = _.filter(input.upgrade, function (p) {
    return analysis.reachablePackages[p] === true;
  });

  // make sure packages that are being updated can still count as
  // a previous_root for the purposes of previous_root_incompat
  Profile.time("add terms to previous_root_incompat", function () {
    _.each(toUpdate, function (p) {
      if (input.isRootDependency(p) && input.isInPreviousSolution(p)) {
        var parts = self.pricer.partitionVersions(
          self.getVersions(p), input.previousSolution[p]);
        _.each(parts.older.concat(parts.higherMajor), function (v) {
          previousRootIncompat.addTerm(pvVar(p, v), 1);
        });
      }
    });
  });

  if (! input.allowIncompatibleUpdate) {
    // Enforce that we don't make breaking changes to your root dependencies,
    // unless you pass --allow-incompatible-update.  It will actually be enforced
    // farther down, but for now, we want to apply this constraint before handling
    // updates.
    self.minimize(previousRootIncompat);
  }

  self.minimize(self.getVersionCostSteps(
    'update', toUpdate, CS.VersionPricer.MODE_UPDATE));

  if (input.allowIncompatibleUpdate) {
    // If you pass `--allow-incompatible-update`, we will still try to minimize
    // version changes to root deps that break compatibility, but with a lower
    // priority than taking as-new-as-possible versions for `meteor update`.
    self.minimize(previousRootIncompat);
  }

  self.minimize(previousRootVersionParts);

  var otherPrevious = _.filter(_.map(input.previousSolution, function (v, p) {
    return new CS.PackageAndVersion(p, v);
  }), function (pv) {
    var p = pv.package;
    return analysis.reachablePackages[p] === true &&
      ! input.isRootDependency(p);
  });

  self.minimize(self.getVersionDistanceSteps(
    'previous_indirect', otherPrevious,
    input.upgradeIndirectDepPatchVersions));

  var newRootDeps = _.filter(input.dependencies, function (p) {
    return ! input.isInPreviousSolution(p);
  });

  self.minimize(self.getVersionCostSteps(
    'new_root', newRootDeps, CS.VersionPricer.MODE_UPDATE));

  // Lock down versions of all root, previous, and updating packages that
  // are currently selected.  The reason to do this is to save the solver
  // a bunch of work (i.e. improve performance) by not asking it to
  // optimize the "unimportant" packages while also twiddling the versions
  // of the "important" packages, which would just multiply the search space.
  //
  // The important packages are root deps, packages in the previous solution,
  // and packages being upgraded.  At this point, we either have unique
  // versions for them, or else there is some kind of trade-off, like a
  // situation where raising the version of one package and lowering the
  // version of another produces the same cost -- a tie between two solutions.
  // If we have a tie, it probably won't be broken by the unimportant
  // packages, so we'll end up going with whatever we picked anyway.  (Note
  // that we have already taken the unimportant packages into account in that
  // we are only considering solutions where SOME versions can be chosen for
  // them.)  Even if optimizing the unimportant packages (coming up next)
  // was able to break a tie in the important packages, we care so little
  // about the versions of the unimportant packages that it's a very weak
  // signal.  In other words, the user might be better off with some tie-breaker
  // that looks only at the important packages anyway.
  Profile.time("lock down important versions", function () {
    _.each(self.currentVersionMap(), function (v, pkg) {
      if (input.isRootDependency(pkg) ||
          input.isInPreviousSolution(pkg) ||
          input.isUpgrading(pkg)) {
        logic.require(Logic.implies(pkg, pvVar(pkg, v)));
      }
    });
  });

  // new, indirect packages are the lowest priority
  var otherPackages = [];
  _.each(_.keys(analysis.reachablePackages), function (p) {
    if (! (input.isRootDependency(p) ||
           input.isInPreviousSolution(p) ||
           input.isUpgrading(p))) {
      otherPackages.push(p);
    }
  });

  self.minimize(self.getVersionCostSteps(
    'new_indirect', otherPackages,
    CS.VersionPricer.MODE_GRAVITY_WITH_PATCHES));

  self.minimize('total_packages', _.keys(analysis.reachablePackages));

  // throw errors about unknown packages
  if (self.stepsByName['unknown_packages'].optimum > 0) {
    Profile.time("generate error for unknown packages", function () {
      var unknownPackages = _.keys(analysis.unknownPackages);
      var unknownPackagesNeeded = _.filter(unknownPackages, function (p) {
        return self.solution.evaluate(p);
      });
      _.each(unknownPackagesNeeded, function (p) {
        var requirers = _.filter(analysis.unknownPackages[p], function (pv) {
          return self.solution.evaluate(pv);
        });
        var errorStr;
        if (CS.isIsobuildFeaturePackage(p)) {
          errorStr = 'unsupported Isobuild feature "' + p + '"; see ' +
            'https://docs.meteor.com/api/packagejs.html#isobuild-features ' +
            'for a list of features and the minimum Meteor release required';
        } else {
          errorStr = 'unknown package: ' + p;
        }
        _.each(requirers, function (pv) {
          errorStr += '\nRequired by: ' + pv;
        });
        self.errors.push(errorStr);
      });
    });
    self.throwAnyErrors();
  }

  // throw errors about conflicts
  if (self.stepsByName['conflicts'].optimum > 0) {
    self.throwConflicts();
  }

  if ((! input.allowIncompatibleUpdate) &&
      self.stepsByName['previous_root_incompat'].optimum > 0) {
    // we have some "incompatible root changes", where we needed to change a
    // version of a root dependency to a new version incompatible with the
    // original, but --allow-incompatible-update hasn't been passed in.
    // these are in the form of PackageAndVersion strings that we need.
    var incompatRootChanges = _.keys(self.getStepContributions(
      self.stepsByName['previous_root_incompat']));

    Profile.time("generate errors for incompatible root change", function () {
      var numActualErrors = 0;
      _.each(incompatRootChanges, function (pvStr) {
        var pv = CS.PackageAndVersion.fromString(pvStr);
        // exclude packages with top-level equality constraints (added by user
        // or by the tool pinning a version)
        if (! _.has(analysis.topLevelEqualityConstrainedPackages, pv.package)) {
          var prevVersion = input.previousSolution[pv.package];
          self.errors.push(
            'Potentially incompatible change required to ' +
              'top-level dependency: ' +
              pvStr + ', was ' + prevVersion + '.\n' +
              self.listConstraintsOnPackage(pv.package));
          numActualErrors++;
        }
      });
      if (numActualErrors) {
        self.errors.push(
          'To allow potentially incompatible changes to top-level ' +
            'dependencies, you must pass --allow-incompatible-update ' +
            'on the command line.');
      }
    });
    self.throwAnyErrors();
  }

  var result = {
    neededToUseUnanticipatedPrereleases: (
      self.stepsByName['unanticipated_prereleases'].optimum > 0),
    answer: Profile.time("generate version map", function () {
      return self.currentVersionMap();
    })
  };

  if (allAnswers) {
    Profile.time("generate all answers", function () {
      var allAnswersList = [result.answer];
      var nextAnswer = function () {
        var formula = self.solution.getFormula();
        var newSolution = logic.solveAssuming(Logic.not(formula));
        if (newSolution) {
          self.setSolution(newSolution);
          logic.forbid(formula);
        }
        return newSolution;
      };
      while (nextAnswer()) {
        allAnswersList.push(self.currentVersionMap());
      }
      result.allAnswers = allAnswersList;
    });
  };

  return result;
};

// Get a list of package-version variables that satisfy a given constraint.
var getOkVersions = function (toPackage, vConstraint, targetVersions) {
  return _.compact(_.map(targetVersions, function (v) {
    if (CS.isConstraintSatisfied(toPackage, vConstraint, v)) {
      return pvVar(toPackage, v);
    } else {
      return null;
    }
  }));
};

// The CS.Solver constructor turns this into a memoized method.
// Memoizing the Formula object reduces clause generation a lot.
var _getConstraintFormula = function (toPackage, vConstraint) {
  var self = this;

  var targetVersions = self.getVersions(toPackage);
  var okVersions = getOkVersions(toPackage, vConstraint, targetVersions);

  if (okVersions.length === targetVersions.length) {
    return Logic.TRUE;
  } else {
    return Logic.or(Logic.not(toPackage), okVersions);
  }
};

CS.Solver.prototype.listConstraintsOnPackage = function (pkg) {
  var self = this;
  var constraints = self.analysis.constraints;

  var result = 'Constraints on package "' + pkg + '":';

  _.each(constraints, function (c) {
    if (c.toPackage === pkg) {
      var paths;
      if (c.fromVar) {
        paths = self.getPathsToPackageVersion(
          CS.PackageAndVersion.fromString(c.fromVar));
      } else {
        paths = [['top level']];
      }
      _.each(paths, function (path) {
        result += '\n* ' + (new PV.PackageConstraint(
          pkg, c.vConstraint.raw)) + ' <- ' + path.join(' <- ');
      });
    }
  });

  return result;
};

CS.Solver.prototype.throwConflicts = function () {
  var self = this;

  var solution = self.solution;
  var constraints = self.analysis.constraints;

  self.Profile.time("generate error about conflicts", function () {
    _.each(constraints, function (c) {
      // c is a CS.Solver.Constraint
      if (solution.evaluate(c.conflictVar)) {
        // skipped this constraint
        var possibleVersions = self.getVersions(c.toPackage);
        var chosenVersion = _.find(possibleVersions, function (v) {
          return solution.evaluate(pvVar(c.toPackage, v));
        });
        if (! chosenVersion) {
          // this can't happen, because for a constraint to be a problem,
          // we must have chosen some version of the package it applies to!
          throw new Error("Internal error: Version not found");
        }
        var error = (
          'Conflict: Constraint ' + (new PV.PackageConstraint(
            c.toPackage, c.vConstraint)) +
            ' is not satisfied by ' + c.toPackage + ' ' + chosenVersion + '.');

        error += '\n' + self.listConstraintsOnPackage(c.toPackage);

        // Avoid printing exactly the same error twice.  eg, if we have two
        // different packages which have the same unsatisfiable constraint.
        if (self.errors.indexOf(error) === -1) {
          self.errors.push(error);
        }
      }
    });
  });

  // always throws, never returns
  self.throwAnyErrors();

  throw new Error("Internal error: conflicts could not be explained");
};

// Takes a PackageVersion and returns an array of arrays of PackageVersions.
// If the `packageVersion` is not selected in `self.solution`, returns
// an empty array.  Otherwise, returns an array of all paths from
// root dependencies to the package, in reverse order.  In other words,
// the first element of each path is `packageVersion`,
// and the last element is the selected version of a root dependency.
//
// Ok, it isn't all paths.  Because that would be crazy (combinatorial
// explosion).  It stops at root dependencies and tries to filter out
// ones that are definitely longer than another.
CS.Solver.prototype.getPathsToPackageVersion = function (packageAndVersion) {
  check(packageAndVersion, CS.PackageAndVersion);
  var self = this;
  var input = self.input;
  var cache = input.catalogCache;
  var solution = self.solution;

  var versionMap = self.currentVersionMap();
  var hasDep = function (p1, p2) {
    // Include weak dependencies, because their constraints matter.
    return _.has(cache.getDependencyMap(p1, versionMap[p1]), p2);
  };
  var allPackages = _.keys(versionMap);

  var getPaths = function (pv, _ignorePackageSet) {
    if (! solution.evaluate(pv.toString())) {
      return [];
    }
    var pkg = pv.package;

    if (input.isRootDependency(pkg)) {
      return [[pv]];
    }

    var newIgnorePackageSet = _.clone(_ignorePackageSet);
    newIgnorePackageSet[pkg] = true;

    var paths = [];
    var shortestLength = null;

    _.each(allPackages, function (p) {
      if ((! _.has(newIgnorePackageSet, p)) &&
          solution.evaluate(p) &&
          hasDep(p, pkg)) {
        var newPV = new CS.PackageAndVersion(p, versionMap[p]);
        _.each(getPaths(newPV, newIgnorePackageSet), function (path) {
          var newPath = [pv].concat(path);
          if ((! paths.length) || newPath.length < shortestLength) {
            paths.push(newPath);
            shortestLength = newPath.length;
          }
        });
      }
    });

    return paths;
  };

  return getPaths(packageAndVersion, {});
};

CS.Solver.Constraint = function (fromVar, toPackage, vConstraint, conflictVar) {
  this.fromVar = fromVar;
  this.toPackage = toPackage;
  this.vConstraint = vConstraint;
  this.conflictVar = conflictVar;

  // this.fromVar is a return value of pvVar(p, v), or null for a
  // top-level constraint
  check(this.fromVar, Match.OneOf(String, null));
  check(this.toPackage, String); // package name
  check(this.vConstraint, PV.VersionConstraint);
  check(this.conflictVar, String);
};


const installResult = runNpmCommand(["install"], appDir);

"Could not install npm dependencies for test-packages: " +

const { runNpmCommand } = require("../isobuild/meteor-npm.js");

const installResult = runNpmCommand(npmCommand, appDir);


less                    # Leaner CSS language

meteortesting:mocha               # A package for writing and running your meteor app and package tests with mocha

# Meteor packages used by this project, one per line.
# Check this file (and the other files in this directory) into your repository.
#
# 'meteor add' and 'meteor remove' will edit this file for you,
# but you can also edit it by hand.

meteor-base             # Packages every Meteor app needs to have
mobile-experience       # Packages for a great mobile UX
mongo                   # The database Meteor supports right now
blaze-html-templates    # Compile .html files into Meteor Blaze views
reactive-var            # Reactive variable for tracker
tracker                 # Meteor's client-side reactive programming library

standard-minifier-css   # CSS minifier run for production mode
standard-minifier-js    # JS minifier run for production mode
es5-shim                # ECMAScript 5 compatibility for older browsers
ecmascript              # Enable ECMAScript2015+ syntax in app code
shell-server            # Server-side component of the `meteor shell` command

kadira:flow-router      # FlowRouter is a very simple router for Meteor
kadira:blaze-layout     # Layout manager for blaze (works well with FlowRouter)
less                    # Leaner CSS language

practicalmeteor:mocha             # A package for writing and running your meteor app and package tests with mocha
johanbrook:publication-collector  # Test a Meteor publication by collecting its output


autoupdate@1.2.2-plugins.0
babel-compiler@5.8.3-plugins.0_5
babel-runtime@0.1.2
base64@1.0.4-plugins.0
binary-heap@1.0.4-plugins.0
blaze@2.1.3-plugins.0
blaze-tools@1.0.4-plugins.0
boilerplate-generator@1.0.4-plugins.0
caching-compiler@1.0.0-plugins.1
caching-html-compiler@1.0.0
callback-hook@1.0.4-plugins.0
check@1.0.6-plugins.0
ddp@1.2.0-plugins.0
ddp-client@1.2.0-plugins.0
ddp-common@1.2.0-plugins.0
ddp-server@1.2.0-plugins.1
deps@1.0.8-plugins.0
diff-sequence@1.0.0-plugins.0
ecmascript@0.1.3-plugins.1
ejson@1.0.7-plugins.0
geojson-utils@1.0.4-plugins.0
html-tools@1.0.5-plugins.0
htmljs@1.0.5-plugins.0
http@1.1.1-plugins.0
id-map@1.0.4-plugins.0
jquery@1.11.3-plugins.0_3
json@1.0.4-plugins.0
logging@1.0.8-plugins.0
meteor@1.1.7-plugins.1
minifiers@1.1.6-plugins.0
minimongo@1.0.9-plugins.0
mongo@1.1.1-plugins.0
mongo-id@1.0.0-plugins.0
npm-mongo@1.4.32-plugins.0_1
observe-sequence@1.0.7-plugins.0
ordered-dict@1.0.4-plugins.0
promise@0.4.2-plugins.0
random@1.0.4-plugins.0
reactive-var@1.0.6-plugins.0
reload@1.1.4-plugins.0
retry@1.0.4-plugins.0
routepolicy@1.0.6-plugins.0
spacebars@1.0.7-plugins.0
spacebars-compiler@1.0.7-plugins.1
static-html@1.0.12
templating@1.1.2-plugins.1
templating-tools@0.0.1
tracker@1.0.8-plugins.0
ui@1.0.7-plugins.0
underscore@1.0.4-plugins.0
url@1.0.5-plugins.0
webapp@1.2.1-plugins.0
webapp-hashing@1.0.4-plugins.0


UI.body.items = Items.find({}, { sort: { rank: 1 } });

SimpleRationalRanks = {
  beforeFirst: function (firstRank) { return firstRank - 1; },
  between: function (beforeRank, afterRank) { return (beforeRank + afterRank) / 2; },
  afterLast: function (lastRank) { return lastRank + 1; }
};

UI.body.rendered = function () {
  $(this.find('#list')).sortable({ // uses the 'sortable' interaction from jquery ui
    stop: function (event, ui) { // fired when an item is dropped
      var el = ui.item.get(0), before = ui.item.prev().get(0), after = ui.item.next().get(0);

      var newRank;
      if (!before) { // moving to the top of the list
        newRank = SimpleRationalRanks.beforeFirst(UI.getElementData(after).rank);

      } else if (!after) { // moving to the bottom of the list
        newRank = SimpleRationalRanks.afterLast(UI.getElementData(before).rank);

      } else {
        newRank = SimpleRationalRanks.between(
          UI.getElementData(before).rank,
          UI.getElementData(after).rank);
      }

      Items.update(UI.getElementData(el)._id, {$set: {rank: newRank}});
    }
  });
};


// Normally, a var should be file-local, but this file is loaded with {bare:
// true}, so it should be readable by bare_tests.js
var VariableSetByBareTestSetup = 1234;


// Tests for the behavior of the links collection

import { assert } from 'meteor/practicalmeteor:chai';

describe('links collection', function () {

keep keep keep keep keep replace keep keep keep keep keep

import buildmessage from "../utils/buildmessage.js";
import {
  pathJoin,
  statOrNull,
  writeFile,
  unlink,
} from "../fs/files.js";

const INSTALL_JOB_MESSAGE = "installing npm dependencies";

export function install(appDir) {
  const packageJsonPath = pathJoin(appDir, "package.json");
  const needTempPackageJson = ! statOrNull(packageJsonPath);

  if (needTempPackageJson) {
    const { dependencies } = require("../static-assets/skel/package.json");

    // Write a minimial package.json with the same dependencies as the
    // default new-app package.json file.
    writeFile(
      packageJsonPath,
      JSON.stringify({ dependencies }, null, 2) + "\n",
      "utf8",
    );
  }

  const ok = buildmessage.enterJob(INSTALL_JOB_MESSAGE, function () {
    const { runNpmCommand } = require("../isobuild/meteor-npm.js");

    const installResult = runNpmCommand(["install"], appDir);
    if (! installResult.success) {
      buildmessage.error(
        "Could not install npm dependencies for test-packages: " +
          installResult.error);

      return false;
    }

    return true;
  });

  if (needTempPackageJson) {
    // Clean up the temporary package.json file created above.
    unlink(packageJsonPath);
  }

  return ok;
}


var selftest = require('../tool-testing/selftest.js');
var Sandbox = selftest.Sandbox;

selftest.define("help", function () {
  var s = new Sandbox;

  // Top-level help
  var checkTopLevelHelp = function (run) {
    run.read("Usage: meteor");
    run.match("Commands:");
    run.match(/create\s*Create a new project/);
    run.match(/\s*admin\s/);
    run.expectExit(0);
    run.forbidAll(/^\s*maintainers\s/); // no subcommands
    run.forbidAll(/^\s*dummy\s/); // no hidden commands
  };

  checkTopLevelHelp(s.run("help"));
  checkTopLevelHelp(s.run("--help"));

  // Command help
  var checkCommandHelp = function (run) {
    run.read("Usage: meteor create");
    run.match("create a new Meteor app");
    run.match("Options:");
    run.match(/--list\s*Show list/);
    run.expectExit(0);
  };

  checkCommandHelp(s.run("help", "create"));
  checkCommandHelp(s.run("create", "--help"));
  checkCommandHelp(s.run("--help", "create"));

  // List of subcommands
  var checkSubcommandList = function (run) {
    run.read("Usage: meteor admin <command>");
    run.match("Commands:");
    run.match(/recommend-release\s*Recommend a previously published/);
    run.expectExit(0);
  };
  checkSubcommandList(s.run("help", "admin"));
  checkSubcommandList(s.run("admin", "help"));
  checkSubcommandList(s.run("admin", "--help"));
  checkSubcommandList(s.run("--help", "admin"));

  // Subcommand help
  var checkSubcommandHelp = function (run) {
    run.match("Usage: meteor admin make-bootstrap-tarballs");
    run.match("For internal use only.");
    run.expectExit(0);
  };

  var comm = "make-bootstrap-tarballs";
  checkSubcommandHelp(s.run("help", "admin", comm));
  checkSubcommandHelp(s.run("admin", "help", comm));
  checkSubcommandHelp(s.run("admin", comm, "--help"));
  checkSubcommandHelp(s.run("--help", "admin", comm));
});


var _ = require('underscore');

var config = require('../meteor-services/config.js');
var httpHelpers = require('../utils/http-helpers.js');
var release = require('./release.js');
var files = require('../fs/files.js');
var utils = require('../utils/utils.js');
var buildmessage = require('../utils/buildmessage.js');
var compiler = require('../isobuild/compiler.js');
var authClient = require('../meteor-services/auth-client.js');
var catalog = require('./catalog/catalog.js');
var projectContextModule = require('../project-context.js');
var colonConverter = require('../utils/colon-converter.js');
var Profile = require('../tool-env/profile.js').Profile;

import { requestGarbageCollection } from "../utils/gc.js";

// Opens a DDP connection to a package server. Loads the packages needed for a
// DDP connection, then calls DDP connect to the package server URL in config,
// using a current user-agent header composed by http-helpers.js.
var openPackageServerConnection = function (packageServerUrl) {
  var serverUrl = packageServerUrl || config.getPackageServerUrl();
  return authClient.openServiceConnection(serverUrl);
};

// We don't let the user upload a blank README for UX reasons, but we would
// prefer that the server move to a world with 'readme' files for everything in
// the future. As a way to breach these interfaces, for now, we are going to
// upload blank documentation files when null docs are requested.
//
// This function generates a Readme object for a blank readme file, as well as
// the file itself.
var generateBlankReadme = function () {
  return {
    contents: "",
    excerpt: "",
    hash: files.blankHash
  };
};

// Save a readme file to a temporary path.
var saveReadmeToTmp = function (readmeInfo) {
  var tempReadmeDir = files.mkdtemp('readme');
  var readmePath = files.pathJoin(tempReadmeDir, "Readme.md");
  files.writeFileAtomically(readmePath, readmeInfo.contents);
  return readmePath;
};

// Given a connection, makes a call to the package server.  (Checks to see if
// the connection is connected, and reconnects if needed -- a workaround for
// the fact that connections in the tool do not reconnect)
exports.callPackageServer = function (conn, ...args) {
  // XXX This is broken since it doesn't actually replace the conn in the
  // caller, so it'll happen on every subsequent call
  if (!conn.connected) {
    conn.close();
    conn = exports.loggedInPackagesConnection();
  }
  return conn.call(...args);
};

var callPackageServerBM = exports.callPackageServerBM = function (...args) {
  buildmessage.assertInJob();
  try {
    return exports.callPackageServer.apply(null, args);
  } catch (e) {
    buildmessage.error(e.reason || e.message);
    return null;
  }
};

// Requests and returns one page of new package data that we haven't cached on
// disk. We assume that data is cached chronologically, so essentially, we are
// asking for a diff from the last time that we did this.
// Takes in:
// - conn: the connection to use (does not have to be logged in)
// - syncToken: a syncToken object to be sent to the server that
//   represents the last time that we talked to the server.
// - options:
//    - useShortPages (Boolean). Ask the server for pages of ~3 records
//      instead of ~100, for testing pagination.
//
// Returns an object, containing the following fields:
//  - syncToken: a new syncToken object, that we can pass to the server in the future.
//  - collections: an object keyed by the name of server collections, with the
//    records as an array of javascript objects.
var loadRemotePackageData = function (conn, syncToken, options) {
  options = options || {};

  // Did we get disconnected between retries somehow? Then we should open a new
  // connection. We shouldn't use the callPackageServer method here though,
  // since we don't need to authenticate.
  if (!conn.connected) {
    conn.close();
    conn =  openPackageServerConnection();
  }

  var syncOpts = {};
  if (options && options.useShortPages) {
    syncOpts.shortPagesForTest = options.useShortPages;
  }
  if (options && options.compressCollections) {
    syncOpts.compressCollections = options.compressCollections;
  }
  return conn.call('syncNewPackageData', syncToken, syncOpts);
};

// Contacts the package server to get the latest diff and writes changes to
// disk.
//
// Takes in the dataStore, which is an example of the remote catalog. Contacts
// the package server and updates the sql database with the most recent
// information.
//
// Returns null if contacting the server times out, or an object with the
// following keys:
//     resetData : true if we should reset the database, otherwise false.
//     connectionFailed: true if we failed to connect to the server.
//
// options can include:
//  - packageStorageFile: String. The file to write the data to (overrides
//    `config.getPackageStorage()`)
//  - packageServerUrl: String. The package server (overrides
//    `config.getPackageServerUrl()`)
//  - useShortPages: Boolean. Request short pages of ~3 records from the
//    server, instead of ~100 that it would send otherwise
exports.updateServerPackageData = function (dataStore, options) {
  return buildmessage.enterJob('updating package catalog', function () {
    return _updateServerPackageData(dataStore, options);
  });
};

var _updateServerPackageData = function (dataStore, options) {
  var self = this;
  options = options || {};
  if (dataStore === null) {
    throw Error("Data store expected");
  }

  var done = false;
  var ret = {resetData: false};

  // For now, we don't have a great progress metric, so just use a spinner
  var useProgressbar = false;

  var start = undefined;
  // Guess that we're about an hour behind, as an opening guess
  var state = { current: 0, end: 60 * 60 * 1000, done: false};
  useProgressbar && buildmessage.reportProgress(state);

  var conn = openPackageServerConnection(options.packageServerUrl);

  // Provide some progress indication for connection
  // XXX though it is just a hack
  state.current = 1;
  useProgressbar && buildmessage.reportProgress(state);

  var getSomeData = function () {
    var syncToken = dataStore.getSyncToken() || {format: "1.1"};

    if (!start) {
      start = {};
      start.builds = syncToken.builds;
      start.versions = syncToken.versions;
      state.end = (Date.now() - start.builds) + (Date.now() - start.versions);
    }
    // XXX: This is a hack... syncToken should have a % done
    state.current =
      (syncToken.builds - start.builds) +
      (syncToken.versions - start.versions);
    useProgressbar && buildmessage.reportProgress(state);

    var compress = !!process.env.METEOR_CATALOG_COMPRESS_RPCS;

    // (loadRemotePackageData may throw)
    var remoteData = loadRemotePackageData(conn, syncToken, {
      useShortPages: options.useShortPages,
      compressCollections: compress
    });

    // Is the remote server telling us to ignore everything we've heard before?
    // OK, we can do that.
    if (remoteData.resetData) {
      dataStore.reset();
      // The caller may want to take this as a cue to delete packages from the
      // tropohouse.
      ret.resetData = true;
    }

    if (remoteData.collectionsCompressed) {
      var zlib = require('zlib');
      var colsGzippedBuffer = Buffer.from(
        remoteData.collectionsCompressed, 'base64');
      var colsJSON = new Promise((resolve, reject) => {
        zlib.gunzip(colsGzippedBuffer, (err, res) => {
          err ? reject(err) : resolve(res);
        });
      }).await();
      remoteData.collections = JSON.parse(colsJSON);
      delete remoteData.collectionsCompressed;
    }

    // We always write to the data store; the fact there is no data is itself
    // data!  e.g. the last-refresh timestamp
    var syncComplete =
          _.isEqual(remoteData.collections, {}) || remoteData.upToDate;
    dataStore.insertData(remoteData, syncComplete);

    // If there is no new data from the server, don't bother writing things to
    // disk (unless we were just told to reset everything).
    if (!remoteData.resetData && _.isEqual(remoteData.collections, {})) {
      done = true;
      return;
    }

    if (remoteData.upToDate) {
      done = true;
    }
  };

  try {
    while (!done) {
      getSomeData();
      requestGarbageCollection();
    }
  } finally {
    conn.close();
  }

  return ret;
};

_updateServerPackageData = Profile('package-client _updateServerPackageData',
                                   _updateServerPackageData);

// Returns a logged-in DDP connection to the package server, or null if
// we cannot log in. If an error unrelated to login occurs
// (e.g. connection to package server times out), then it will be
// thrown.
exports.loggedInPackagesConnection = function () {
  return authClient.loggedInConnection(
    config.getPackageServerUrl(),
    config.getPackageServerDomain(),
    "package-server"
  );
};

// XXX this is missing a few things. In retrospect a better approach here might
//     be to actually make "save source somewhere else" or perhaps "add source
//     to tarball" be part of the package build itself...
var bundleSource = function (isopack, includeSources, packageDir) {
  buildmessage.assertInJob();

  var name = isopack.name;

  var tempDir = files.mkdtemp('build-source-package-');
  var packageTarName = name + '-' + isopack.version + '-source';
  var dirToTar = files.pathJoin(tempDir, 'source',
    colonConverter.convert(packageTarName));
  // XXX name probably needs to be escaped for windows?
  // XXX note that publish-for-arch thinks it knows how this tarball is laid
  //     out, which is a bit of a shame
  var sourcePackageDir = files.pathJoin(dirToTar, colonConverter.convert(name));
  if (! files.mkdir_p(sourcePackageDir)) {
    buildmessage.error('Failed to create temporary source directory: ' +
                       sourcePackageDir);
    return null;
  }

  // We copy source files into a temp directory and then tar up the temp
  // directory. It would be great if we could avoid the copy, but as far
  // as we can tell, this is the only way to get a tarball with the
  // directory structure that we want (<package name>-<version-source/
  // at the top level).
  _.each(includeSources, function (f) {
    const from = files.pathJoin(packageDir, f);
    const to = files.pathJoin(sourcePackageDir, f);
    if (files.exists(from)) {
      files.copyFile(from, to);
    }
  });

  // Write a package map to `.versions` inside the source tarball.  Note that
  // this differs in two ways from the `.versions` file that is maintained
  // inside standalone packages by 'meteor publish':
  //  (a) It only contains the direct, directly implied, and linked-into-plugin
  //      dependencies of the package, not all transitive dependencies.
  //  (b) It is ALWAYS put into the source tarball, even if the package came
  //      from inside an app, whereas the package-source-tree .versions file
  //      is only used for standalone packages
  var packageMapFilename = files.pathJoin(sourcePackageDir, '.versions');
  if (files.exists(packageMapFilename)) {
    throw Error(".versions file already exists? " + packageMapFilename);
  }
  var pluginProviderPackageMap = isopack.pluginProviderPackageMap;
  if (! pluginProviderPackageMap) {
    throw Error("no pluginProviderPackageMap on isopack?");
  }
  var packageMapFile = new projectContextModule.PackageMapFile({
    filename: packageMapFilename
  });
  packageMapFile.write(pluginProviderPackageMap);

  // We put this inside the temp dir because mkdtemp makes sure that the
  // temp dir gets cleaned up on process exit, so we don't have to worry
  // about cleaning up our tarball (or our copied source files)
  // ourselves.
  var sourceTarball = files.pathJoin(tempDir, packageTarName + '.tgz');
  files.createTarball(dirToTar, sourceTarball);

  var tarballHash = files.fileHash(sourceTarball);
  var treeHash = files.treeHash(dirToTar);

  return {
    sourceTarball: sourceTarball,
    tarballHash: tarballHash,
    treeHash: treeHash
  };
};

// Uploads a file at a filepath to the HTTP put URL.
//
// Returns true on success and false on failure.
var uploadFile = function (putUrl, filepath) {
  buildmessage.assertInJob();
  var size = files.stat(filepath).size;
  var rs = files.createReadStream(filepath);
  try {
    // Use getUrl instead of request, to throw on 4xx/5xx.
    httpHelpers.getUrl({
      method: 'PUT',
      url: putUrl,
      headers: {
        'content-length': size,
        'content-type': 'application/octet-stream',
        'x-amz-acl': 'public-read'
      },
      bodyStream: rs,
      bodyStreamLength: size
    });
  } catch (err) {
    // XXX: getUrl's error handling is terrible and we should fix it there.
    buildmessage.error(typeof err === "string" ? err : err.error.toString());
    return false;
  } finally {
    rs.close();
  }
  return true;
};

exports.uploadFile = uploadFile;

export function bundleBuild(isopack, isopackCache) {
  buildmessage.assertInJob();

  var tempDir = files.mkdtemp('bp-');
  var packageTarName = isopack.tarballName();
  var tarInputDir = files.pathJoin(tempDir, packageTarName);

  // Note that we do need to do this even though we already have the isopack on
  // disk in an IsopackCache, because we don't want to include
  // isopack-buildinfo.json. (We don't include it because we're not passing
  // includeIsopackBuildInfo to saveToPath here.)
  isopack.saveToPath(tarInputDir, {
    // When publishing packages that don't use new registerCompiler plugins,
    // make sure that old Meteors can use it too
    includePreCompilerPluginIsopackVersions: true,
    isopackCache,
  });

  var buildTarball = files.pathJoin(tempDir, packageTarName + '.tgz');

  files.createTarball(tarInputDir, buildTarball);

  var tarballHash = files.fileHash(buildTarball);
  var treeHash = files.treeHash(tarInputDir, {
    // We don't include any package.json from an npm module in the tree hash,
    // because npm isn't super consistent about what it puts in there (eg, does
    // it include the "readme" field)? This ends up leading to spurious
    // differences. The tree hash will still notice any actual CODE changes in
    // the npm packages.
    ignore: function (relativePath) {
      var pieces = relativePath.split(files.pathSep);
      return pieces.length && _.last(pieces) === 'package.json'
        && _.contains(pieces, 'npm');
    }
  });

  return {
    buildTarball: buildTarball,
    tarballHash: tarballHash,
    treeHash: treeHash
  };
}

function createBuiltPackage(isopack, isopackCache) {
  buildmessage.assertInJob();
  var name = isopack.name;

  // Note: we really want to do this before createPackageBuild, because the URL
  // we get from createPackageBuild will expire!
  var bundleResult;
  buildmessage.enterJob("bundling build for " + name, function () {
    bundleResult = bundleBuild(isopack, isopackCache);
  });
  if (buildmessage.jobHasMessages()) {
    return;
  }

  return bundleResult;
}

var publishBuiltPackage = function (conn, isopack, bundleResult) {
  buildmessage.assertInJob();
  var name = isopack.name;

  var uploadInfo;
  buildmessage.enterJob('creating package build for ' + name, function () {
    uploadInfo = callPackageServerBM(conn, 'createPackageBuild', {
      packageName: isopack.name,
      version: isopack.version,
      buildArchitectures: isopack.buildArchitectures()
    });
  });
  if (buildmessage.jobHasMessages()) {
    return;
  }

  buildmessage.enterJob("uploading build", function () {
    uploadFile(uploadInfo.uploadUrl,
               bundleResult.buildTarball);
  });
  if (buildmessage.jobHasMessages()) {
    return;
  }

  buildmessage.enterJob('publishing package build for ' + name, function () {
    callPackageServerBM(conn, 'publishPackageBuild',
                        uploadInfo.uploadToken,
                        bundleResult.tarballHash,
                        bundleResult.treeHash);
  });
  if (buildmessage.jobHasMessages()) {
    return;
  }
};

export function createAndPublishBuiltPackage(conn, isopack, isopackCache) {
  publishBuiltPackage(
    conn,
    isopack,
    createBuiltPackage(isopack, isopackCache),
  );
}

// Handle an error thrown on trying to connect to the package server.
exports.handlePackageServerConnectionError = function (error) {
  authClient.handleConnectionError(error, "package server");
};

// Update the package metdata in the server catalog. Chane the docs,
// descriptions and the Git URL to new values.
//
// options:
// - packageSource: the packageSource for this package.
// - readmeInfo: null, or an object containing docs information for this package.
// - connection: the open, logged-in connection over which we should talk to the
//   package server. DO NOT CLOSE this connection here.
//
// Return true on success and an error code otherwise.
exports.updatePackageMetadata = function (options) {
  buildmessage.assertInJob();

  var packageSource = options.packageSource;
  var conn = options.connection;
  var readmeInfo = options.readmeInfo;

  var name = packageSource.name;
  var version = packageSource.version;

  if (! version) {
    buildmessage.error(
      "Package cannot be updated because it doesn't have a version");
    return;
  }

  // For now, documentation is optional on the client, so we have to give people
  // a way to remove it with 'documentation: null'.
  if (! readmeInfo) {
    readmeInfo = generateBlankReadme();
  }

  var dataToUpdate = {
    git: packageSource.metadata.git || "",
    description: packageSource.metadata.summary,
    longDescription: readmeInfo.excerpt
  };

  // Check that the metadata fits under the established limits, and give helpful
  // feedback.
  if (! dataToUpdate["description"]) {
    buildmessage.error("Please provide a short description to use in 'meteor search'");
    return;
  }

  if (dataToUpdate["description"] &&
      dataToUpdate["description"].length > 100) {
    buildmessage.error("Summary must be under 100 chars.");
    return;
  }

  if (dataToUpdate["longDescription"].length > 1500) {
    buildmessage.error(
      "Longform package description is too long. Meteor uses the section of " +
      "the Markdown documentation file between the first and second " +
      "headings. That section must be less than 1500 characters long.");
    return;
  }

  // Update the general metadata.
  var versionIdentifier = { packageName: name, version: version };
  buildmessage.enterJob('updating metadata', function () {
    callPackageServerBM(
      conn, "changeVersionMetadata", versionIdentifier, dataToUpdate);
  });
  if (buildmessage.jobHasMessages()) {
    return;
  }

  // Upload the new Readme.
  buildmessage.enterJob('uploading documentation', function () {
    var readmePath = saveReadmeToTmp(readmeInfo);
    var uploadInfo =
          callPackageServerBM(conn, "createReadme", versionIdentifier);
    if (! uploadInfo) {
      return;
    }
    if (! uploadFile(uploadInfo.url, readmePath)) {
      return;
    }
    callPackageServerBM(
      conn, "publishReadme", uploadInfo.uploadToken, { hash: readmeInfo.hash });
  });
  if (buildmessage.jobHasMessages()) {
    return;
  }

};

// Publish the package information into the server catalog. Create new records
// for the package (if needed), the version and the build; upload source and
// isopack.
//
// options:
// - packageSource: the packageSource for this package.
// - connection: the open, logged-in connection over which we should talk to the
//   package server. DO NOT CLOSE this connection here.
// - projectContext: the (probably temporary) ProjectContext to use. Must have\
//   already built local packages
// - new: this package is new, we should call createPackage to create a new
//   package record.
// - existingVersion: we expect the version to exist already, and for us
//   to merely be providing a new build of the same source
// - doNotPublishBuild: do not publish the build of this package.
//
// Return true on success and an error code otherwise.
exports.publishPackage = function (options) {
  buildmessage.assertInJob();
  var packageSource = options.packageSource;
  var conn = options.connection;
  var projectContext = options.projectContext;

  var name = packageSource.name;
  var version = packageSource.version;

  if (options.new && options.existingVersion) {
    throw Error("is it new or does it exist?!?");
  }

  // Check that the package name is valid.
  utils.validatePackageName(name, { useBuildmessage: true });
  if (buildmessage.jobHasMessages()) {
    return;
  }

  // Check that we have a version.
  if (! version) {
    buildmessage.error(
      "Package cannot be published because it doesn't have a version");
    return;
  }

  // Check that the version description is under the character limit. (We check
  // all string limits on the server, but this is the one that is mostly likely
  // to be wrong)
  if (! packageSource.metadata.summary) {
    buildmessage.error(
      "Please describe what your package does. Set a summary " +
        "in Package.describe in package.js.");
    return;
  }

  if (packageSource.metadata.summary.length > 100) {
    buildmessage.error("Summary must be under 100 chars.");
    return;
  }

  // Check that we are an authorized maintainer of this package.
  if (!options['new']) {
    var packRecord = catalog.official.getPackage(name);
    if (! packRecord) {
      buildmessage.error(
        'There is no package named ' + name +
          '. If you are creating a new package, use the --create flag.');
      return;
    }

    if (!exports.amIAuthorized(name, conn, false)) {
      buildmessage.error(
        'You are not an authorized maintainer of ' + name + '.  Only ' +
          'authorized maintainers may publish new versions.');
    }
  }

  // Check that our documentation exists (or we know that it doesn't) and has
  // been filled out.
  var readmeInfo = buildmessage.enterJob(
    "processing documentation",
    function () {
      return packageSource.processReadme();
  });
  if (buildmessage.jobHasMessages()) {
    return;
  }
  if (readmeInfo && (readmeInfo.hash === files.blankHash)) {
    buildmessage.error(
      "Your documentation file is blank, so users may have trouble figuring " +
      "out how to use your package. Please fill it out, or " +
      "set 'documentation: null' in your Package.describe");
    return;
  }

  if (readmeInfo && readmeInfo.excerpt.length > 1500) {
    buildmessage.error(
      "Longform package description is too long. Meteor uses the section of " +
      "the Markdown documentation file between the first and second " +
      "headings. That section must be less than 1500 characters long.");
    return;
  }

  // We don't let the user upload a blank README for UX reasons, but we would
  // prefer that the server move to a world with 'readme' files for everything
  // in the future. This helps unite these interfaces, and makes our code easier
  // to reason about in the future.
  if (! readmeInfo) {
    readmeInfo = generateBlankReadme();
  }
  var readmePath = saveReadmeToTmp(readmeInfo);

  var packageDeps = packageSource.getDependencyMetadata();

  // Check that the package does not have any unconstrained references.
  _.each(packageDeps, function(refs, label) {
    if (refs.constraint == null) {
      if (packageSource.isCore && files.inCheckout() &&
          projectContext.localCatalog.getPackage(label)) {
        // Core package is using or implying another core package,
        // without a version number.  We fill in the version number.
        // (Well, we're assuming that the other package is core and
        // not some other sort of local package.)
        var versionString =
              projectContext.localCatalog.getLatestVersion(label).version;
        // modify the constraint on this dep that will be sent to troposphere
        refs.constraint = versionString;
      } else if (label === "meteor") {
        // HACK: We are willing to publish a package with a "null"
        // constraint on the "meteor" package to troposphere.  This
        // happens for non-core packages when not running from a
        // checkout, because all packages implicitly depend on the
        // "meteor" package, but do not necessarily specify an
        // explicit version for it, and we don't have a great way to
        // choose one here.
        // XXX come back to this, especially if we are incrementing the
        // major version of "meteor".  hopefully we will have more data
        // about the package system by then.
      } else {
        buildmessage.error(
          "You must specify a version constraint for package " + label);
      }
    }
  });
  if (buildmessage.jobHasMessages()) {
    return;
  }

  var isopack = projectContext.isopackCache.getIsopack(name);
  if (! isopack) {
    throw Error("no isopack " + name);
  }

  // If we aren't able to include legacy builds in this version, make sure that
  // it has a fake dependency on isobuild:isopack-2 so that old versions of
  // Isobuild won't accidentally use it.
  if (!isopack.canWriteLegacyBuilds()
      && !_.has(packageDeps, 'isobuild:isopack-2')) {
    packageDeps['isobuild:isopack-2'] = {
      constraint: '1.0.0',
      // Arbitrary arch here; nothing other than meteor show really pays
      // attention to reference arch anymore, because we no longer pay attention
      // to the arch with the constraint in Version Solver.
      references: [{arch: 'os'}],
    };
  }

  var sourceFiles = isopack.getSourceFilesUnderSourceRoot(
    packageSource.sourceRoot);
  if (! sourceFiles) {
    throw Error("isopack doesn't know what its source files are?");
  }

  // We need to have built the test package to get all of its sources, even
  // though we're not publishing a BUILD for the test package.
  if (packageSource.testName) {
    var testIsopack = projectContext.isopackCache.getIsopack(
      packageSource.testName);
    if (! testIsopack) {
      throw Error("no testIsopack " + packageSource.testName);
    }
    var testSourceFiles = testIsopack.getSourceFilesUnderSourceRoot(
      packageSource.sourceRoot);
    if (! testSourceFiles) {
      throw Error("test isopack doesn't know what its source files are?");
    }
    sourceFiles = _.union(sourceFiles, testSourceFiles);
  }

  var sourceBundleResult;
  buildmessage.enterJob("bundling source for " + name, function () {
    sourceBundleResult = bundleSource(
      isopack, sourceFiles, packageSource.sourceRoot);
  });
  if (buildmessage.jobHasMessages()) {
    return;
  }

  // Create the package. Check that the metadata exists.
  if (options.new) {
    buildmessage.enterJob("creating package " + name, function () {
      callPackageServerBM(conn, 'createPackage', {
        name: packageSource.name
      });
    });
    if (buildmessage.jobHasMessages()) {
      return;
    }
  }

  if (options.existingVersion) {
    var existingRecord = catalog.official.getVersion(name, version);
    if (! existingRecord) {
      buildmessage.error("Version does not exist.");
      return;
    }
    if (existingRecord.source.treeHash !== sourceBundleResult.treeHash) {
      buildmessage.error("Package source differs from the existing version.");
      return;
    }

    if (! options.doNotPublishBuild) {
      createAndPublishBuiltPackage(
        conn, isopack, projectContext.isopackCache);

      if (buildmessage.jobHasMessages()) {
        return;
      }
    }

    // XXX check that we're actually providing something new?
  } else {
    var uploadInfo;
    buildmessage.enterJob("pre-publishing package " + name, function () {
      var uploadRec = {
        packageName: packageSource.name,
        version: version,
        description: packageSource.metadata.summary,
        longDescription: readmeInfo.excerpt,
        git: packageSource.metadata.git,
        compilerVersion: compiler.BUILT_BY,
        containsPlugins: packageSource.containsPlugins(),
        debugOnly: packageSource.debugOnly,
        prodOnly: packageSource.prodOnly,
        testOnly: packageSource.testOnly,
        exports: packageSource.getExports(),
        releaseName: release.current.name,
        dependencies: packageDeps
      };
      uploadInfo = callPackageServerBM(conn, 'createPackageVersion', uploadRec);
    });
    if (buildmessage.jobHasMessages()) {
      return;
    }

    // XXX If package version already exists, print a nice error message
    // telling them to try 'meteor publish-for-arch' if they want to
    // publish a new build.

    // Documentation is smaller than the source. Upload it first, to minimize
    // the chances of PUT URLs expiring. (XXX: in the far future, parallelize this)
    buildmessage.enterJob("uploading documentation", function () {
      uploadFile(uploadInfo.readmeUrl, readmePath);
    });
    if (buildmessage.jobHasMessages()) {
      return;
    }

    buildmessage.enterJob("uploading source", function () {
      uploadFile(uploadInfo.uploadUrl, sourceBundleResult.sourceTarball);
    });
    if (buildmessage.jobHasMessages()) {
      return;
    }

    if (! options.doNotPublishBuild) {
      var bundleResult = createBuiltPackage(
        isopack,
        projectContext.isopackCache,
      );

      if (buildmessage.jobHasMessages()) {
        return;
      }
    }

    var hashes = {
      tarballHash: sourceBundleResult.tarballHash,
      treeHash: sourceBundleResult.treeHash,
      readmeHash: readmeInfo.hash
    };
    buildmessage.enterJob("publishing package version", function () {
      callPackageServerBM(
        conn, 'publishPackageVersion', uploadInfo.uploadToken, hashes);
    });
    if (buildmessage.jobHasMessages()) {
      return;
    }

    if (! options.doNotPublishBuild) {
      publishBuiltPackage(conn, isopack, bundleResult);
      if (buildmessage.jobHasMessages()) {
        return;
      }
    }
  }

  return;
};

  // Call the server to ask if we are authorized to update this release or
// package. This is a way to save time before sending data to the server. It
// will mostly ignore most errors (just in case we have a flaky network connection or
// something) and let the method deal with those.
//
// If this returns FALSE, then we are NOT authorized.
// Otherwise, return true.
exports.amIAuthorized = function (name, conn, isRelease) {
  var methodName = "amIAuthorized" +
    (isRelease ? "Release" : "Package");

  try {
    exports.callPackageServer(conn, methodName, name);
  } catch (err) {
    if (err.error === 401) {
      return false;
    }

    // We don't know what this error is. Probably we can't contact the server,
    // or the like. It would be a pity to fail all operations with the server
    // just because a preliminary check fails, so return true for now.
    return true;
  }
  return true;
};


var crypto = Npm.require('crypto');
var fs = Npm.require('fs');
var path = Npm.require('path');

var _disableSizeCheck = false;

Meteor.AppCache = {
  config: function (options) {
    _.each(options, function (value, option) {
      if (option === 'browsers') {
        disabledBrowsers = {};
        _.each(value, function (browser) {
          disabledBrowsers[browser] = false;
        });
      }
      else if (option === 'onlineOnly') {
        _.each(value, function (urlPrefix) {
          RoutePolicy.declare(urlPrefix, 'static-online');
        });
      }
      // option to suppress warnings for tests.
      else if (option === '_disableSizeCheck') {
        _disableSizeCheck = value;
      }
      else if (value === false) {
        disabledBrowsers[option] = true;
      }
      else if (value === true) {
        disabledBrowsers[option] = false;
      } else {
        throw new Error('Invalid AppCache config option: ' + option);
      }
    });
  }
};

var disabledBrowsers = {};
var browserDisabled = function (request) {
  return disabledBrowsers[request.browser.name];
};

function isDynamic(resource) {
  return resource.type === 'dynamic js' ||
    (resource.type === 'json' &&
     // TODO Update this test with PR #9439.
     resource.url.startsWith('/dynamic/') &&
     resource.url.endsWith('.map'))
}

WebApp.addHtmlAttributeHook(function (request) {
  if (browserDisabled(request))
    return null;
  else
    return { manifest: "/app.manifest" };
});

WebApp.connectHandlers.use(function (req, res, next) {
  if (req.url !== '/app.manifest') {
    return next();
  }

  // Browsers will get confused if we unconditionally serve the
  // manifest and then disable the app cache for that browser.  If
  // the app cache had previously been enabled for a browser, it
  // will continue to fetch the manifest as long as it's available,
  // even if we now are not including the manifest attribute in the
  // app HTML.  (Firefox for example will continue to display "this
  // website is asking to store data on your computer for offline
  // use").  Returning a 404 gets the browser to really turn off the
  // app cache.

  if (browserDisabled(WebApp.categorizeRequest(req))) {
    res.writeHead(404);
    res.end();
    return;
  }

  var manifest = "CACHE MANIFEST\n\n";

  // After the browser has downloaded the app files from the server and
  // has populated the browser's application cache, the browser will
  // *only* connect to the server and reload the application if the
  // *contents* of the app manifest file has changed.
  //
  // So to ensure that the client updates if client resources change,
  // include a hash of client resources in the manifest.

  manifest += "# " + WebApp.clientHash() + "\n";

  // When using the autoupdate package, also include
  // AUTOUPDATE_VERSION.  Otherwise the client will get into an
  // infinite loop of reloads when the browser doesn't fetch the new
  // app HTML which contains the new version, and autoupdate will
  // reload again trying to get the new code.

  if (Package.autoupdate) {
    var version = Package.autoupdate.Autoupdate.autoupdateVersion;
    if (version !== WebApp.clientHash())
      manifest += "# " + version + "\n";
  }

  manifest += "\n";

  manifest += "CACHE:" + "\n";
  manifest += "/" + "\n";
  _.each(WebApp.clientPrograms[WebApp.defaultArch].manifest, function (resource) {
    if (resource.where === 'client' &&
        ! RoutePolicy.classify(resource.url) &&
        ! isDynamic(resource)) {
      manifest += resource.url;
      // If the resource is not already cacheable (has a query
      // parameter, presumably with a hash or version of some sort),
      // put a version with a hash in the cache.
      //
      // Avoid putting a non-cacheable asset into the cache, otherwise
      // the user can't modify the asset until the cache headers
      // expire.
      if (!resource.cacheable)
        manifest += "?" + resource.hash;

      manifest += "\n";
    }
  });
  manifest += "\n";

  manifest += "FALLBACK:\n";
  manifest += "/ /" + "\n";
  // Add a fallback entry for each uncacheable asset we added above.
  //
  // This means requests for the bare url ("/image.png" instead of
  // "/image.png?hash") will work offline. Online, however, the browser
  // will send a request to the server. Users can remove this extra
  // request to the server and have the asset served from cache by
  // specifying the full URL with hash in their code (manually, with
  // some sort of URL rewriting helper)
  _.each(WebApp.clientPrograms[WebApp.defaultArch].manifest, function (resource) {
    if (resource.where === 'client' &&
        ! RoutePolicy.classify(resource.url) &&
        ! resource.cacheable &&
        ! isDynamic(resource)) {
      manifest += resource.url + " " + resource.url +
        "?" + resource.hash + "\n";
    }
  });

  manifest += "\n";

  manifest += "NETWORK:\n";
  // TODO adding the manifest file to NETWORK should be unnecessary?
  // Want more testing to be sure.
  manifest += "/app.manifest" + "\n";
  _.each(
    [].concat(
      RoutePolicy.urlPrefixesFor('network'),
      RoutePolicy.urlPrefixesFor('static-online')
    ),
    function (urlPrefix) {
      manifest += urlPrefix + "\n";
    }
  );
  manifest += "*" + "\n";

  // content length needs to be based on bytes
  var body = Buffer.from(manifest);

  res.setHeader('Content-Type', 'text/cache-manifest');
  res.setHeader('Content-Length', body.length);
  return res.end(body);
});

var sizeCheck = function () {
  var totalSize = 0;
  _.each(WebApp.clientPrograms[WebApp.defaultArch].manifest, function (resource) {
    if (resource.where === 'client' &&
        ! RoutePolicy.classify(resource.url) &&
        ! isDynamic(resource)) {
      totalSize += resource.size;
    }
  });
  if (totalSize > 5 * 1024 * 1024) {
    Meteor._debug(
      "** You are using the appcache package but the total size of the\n" +
      "** cached resources is " +
      (totalSize / 1024 / 1024).toFixed(1) + "MB.\n" +
      "**\n" +
      "** This is over the recommended maximum of 5 MB and may break your\n" +
      "** app in some browsers! See http://docs.meteor.com/#appcache\n" +
      "** for more information and fixes.\n"
    );
  }
};

// Run the size check after user code has had a chance to run. That way,
// the size check can take into account files that the user does not
// want cached. Otherwise, the size check warning will still print even
// if the user excludes their large files with
// `Meteor.AppCache.config({onlineOnly: files})`.
Meteor.startup(function () {
  if (! _disableSizeCheck)
    sizeCheck();
});


keep keep keep keep keep replace keep keep keep


var selftest = require('../tool-testing/selftest.js');
var Sandbox = selftest.Sandbox;
var files = require('../fs/files.js');

selftest.define("add cordova platforms", ["cordova"], function () {
  var s = new Sandbox();
  let run;

  // Starting a run
  s.createApp("myapp", "package-tests");
  s.cd("myapp");

  run = s.run("run", "android");
  run.matchErr("Please add the Android platform to your project first");
  run.match("meteor add-platform android");
  run.expectExit(1);

  run = s.run("add-platform", "android");
  // Cordova may need to download cordova-android if it's not already
  // cached (in ~/.cordova).
  run.waitSecs(30);
  run.match("added platform");
  run.expectExit(0);

  run = s.run("add-platform", "android");
  run.matchErr("android: platform is already added");
  run.expectExit(1);

  run = s.run("remove-platform", "foo");
  run.matchErr("foo: platform is not");
  run.expectExit(1);

  run = s.run("remove-platform", "android");
  run.match("removed");
  run = s.run("run", "android");
  run.matchErr("Please add the Android platform to your project first");
  run.match("meteor add-platform android");
  run.expectExit(1);

  if (process.platform !== 'win32') {
    const originalAndroidHome = process.env.ANDROID_HOME;
    const originalPath = process.env.PATH;

    // Hide the fact that Android is installed (as it is on CircleCI) by providing
    // access to only bare system functionality. Android is installed globally in /usr/local/
    // on CircleCI and on Mac.
    s.set("ANDROID_HOME", undefined);
    s.set("PATH", "/usr/bin:/bin:/usr/sbin:/sbin");

    run = s.run("add-platform", "android");
    run.match("added platform");
    run.match("Your system does not yet seem to fulfill all requirements to build apps for Android");
    run.expectExit(0);

    run = s.run("remove-platform", "android");
    run.match("removed");
    run.expectExit(0);

    s.set("ANDROID_HOME", originalAndroidHome);
    s.set("PATH", originalPath);
  }
});


// Server entry point, imports all server code

import '/imports/startup/server';
import '/imports/startup/both';


Meteor.startup(() => {
  WebAppLocalServer.onError((error) => {
    console.error(error);
  });
});


import { PublicationCollector } from 'meteor/johanbrook:publication-collector';

keep keep keep keep replace keep keep keep keep