diff --git a/tools/bundler.js b/tools/bundler.js index d99f9617bf..cba9f8333e 100644 --- a/tools/bundler.js +++ b/tools/bundler.js @@ -1784,12 +1784,10 @@ exports.bundle = function (options) { // Pick up any additional targets in /programs // Step 1: scan for targets and make a list. We will reload if you create a // new subdir in 'programs', or create 'programs' itself. - var programsDir = path.join(appDir, 'programs'); var programs = []; - var programsSubdirs = watch.readAndWatchDirectory(watchSet, { - absPath: programsDir, - include: [/\/$/], - exclude: [/^\./] + var programsDir = project.getProgramsDirectory(appDir); + var programsSubdirs = project.getProgramsSubdirs(appDir, { + watchSet: watchSet }); _.each(programsSubdirs, function (item) { @@ -1882,7 +1880,7 @@ exports.bundle = function (options) { // it var pkg = packageCache.packageCache. - loadPackageAtPath(p.name, p.loadPath); + loadPackageAtPath(p.name, p.path); var target; switch (p.type) { case "server": diff --git a/tools/commands.js b/tools/commands.js index 472412c11b..63f2e0728e 100644 --- a/tools/commands.js +++ b/tools/commands.js @@ -189,7 +189,7 @@ main.registerCommand({ var allPackages = project.getIndirectDependencies(options.appDir); var messages = buildmessage.capture(function () { - _.forEach(allPackages, function (version, name) { + _.forEach(allPackages, function (versions, name) { // Calling getPackage on the loader will return a unipackage object, which // means that the package will be compiled/downloaded. That we throw the // package variable away afterwards is immaterial. @@ -602,7 +602,6 @@ main.registerCommand({ failed = true; return; } - console.log(constraint); // If the version was specified, check that the version exists. if ( constraint.versionConstraint !== "none") { @@ -617,16 +616,16 @@ main.registerCommand({ } } // Check that the constraint is new. If we are already using the package at - // the same constraint, return from this function. - if (_.has(packages, constraint.name) && - constraint.versionConstraint === packages[constraint.name] ) { + // the same constraint in the app, return from this function. + if (_.has(packages.appDeps, constraint.name) && + packages.appDeps[constraint.name] === constraint.versionConstraint) { process.stderr.write(constraint.name + " with version constraint " + constraint.versionConstraint + " has already been added.\n"); failed = true; } // Add the package to our direct dependency constraints that we get from .meteor/packages. - packages[constraint.name] = constraint.versionConstraint; + packages.appDeps[constraint.name] = constraint.versionConstraint; }); // If the user asked for invalid packages, then the user probably expects a @@ -644,9 +643,12 @@ main.registerCommand({ // Call the constraint solver. var constraintSolver = require('./constraint-solver.js'); var resolver = new constraintSolver.Resolver; + // Combine into one object mapping package name to list of + // constraints, to pass in to the constraint solver. + var allPackages = project.combineAppAndProgramDependencies(packages); // XXX: constraint solver currently ignores versions, but it should not. // XXX: this would also be the place to add no-update options. - var newVersions = resolver.resolve(packages); + var newVersions = resolver.resolve(allPackages); if ( ! newVersions) { // XXX: Better error handling. process.stderr.write("Cannot resolve package dependencies."); @@ -663,31 +665,36 @@ main.registerCommand({ messageLog.push("removed dependency on " + packageName); }); - // Install the new versions. + // Install the new versions. If all new versions were installed + // successfully, then `setDependencies` also records dependency + // changes in the .meteor/versions file. + // + // Makes sure we have enough builds of the package downloaded such that + // we can load a browser slice and a slice that will run on this + // system. (Later we may also need to download more builds to be able to + // deploy to another architecture.) + var downloaded = project.setDependencies( + options.appDir, + packages.appDeps, + newVersions + ); + _.each(newVersions, function(version, packageName) { - if ( failed ) + if (failed) return; + if (_.has(versions, packageName) && - versions[packageName] == version ) { + versions[packageName] === version) { // Nothing changed. Skip this. return; - } + } - // Make sure we have enough builds of the package downloaded such that - // we can load a browser slice and a slice that will run on this - // system. (Later we may also need to download more builds to be able to - // deploy to another architecture.) - var available = tropohouse.maybeDownloadPackageForArchitectures( - catalog.getVersion(packageName, version), - // XXX we also download the deploy arch now, because we don't run the - // constraint solver / downloader anywhere other than add-package yet. - ['browser', archinfo.host(), XXX_DEPLOY_ARCH]); - if (! available) { + if (! downloaded[packageName] || downloaded[packageName] !== version) { // XXX maybe we shouldn't be letting the constraint solver choose // things that don't have the right arches? process.stderr.write("Package " + packageName + " has no compatible build for version " + - version); + version + "\n"); failed = true; return; } @@ -700,7 +707,7 @@ main.registerCommand({ // If the previous versions file had this, then we are upgrading, if it did // not, then we must be adding this package anew. - if ( _.has(versions, packageName )) { + if (_.has(versions, packageName)) { messageLog.push("upgraded " + packageName + " from version " + versions[packageName] + " to version " + newVersions[packageName]); @@ -713,9 +720,6 @@ main.registerCommand({ if (failed) return 1; - // Record dependency changes. - project.rewriteDependencies(options.appDir, packages, newVersions); - // Show the user the messageLog of packages we added. _.each(messageLog, function (msg) { process.stdout.write(msg + "\n"); @@ -762,12 +766,12 @@ main.registerCommand({ _.each(options.args, function (packageName) { // Check that we are using the package. We don't check if the package // exists. You should be able to remove non-existent packages. - if (! _.has(packages, packageName)) { + if (! _.has(packages.appDeps, packageName)) { process.stderr.write( packageName + " is not in this project \n"); } // Remove the package from our dependency list. - delete packages[packageName]; + delete packages.appDeps[packageName]; }); // Get the contents of our versions file. We need to pass them to the @@ -780,7 +784,8 @@ main.registerCommand({ var resolver = new constraintSolver.Resolver; // XXX: constraint solver currently ignores versions, but it should not. // XXX: this would also be the place to add no-update options. - var newVersions = resolver.resolve(packages); + var newVersions = resolver.resolve( + project.combineAppAndProgramDependencies(packages)); if ( ! newVersions) { // This should never really happen. process.stderr.write("Cannot resolve package dependencies."); @@ -798,7 +803,7 @@ main.registerCommand({ }); // Write the dependency files with the right versions - project.rewriteDependencies(options.appDir, packages, newVersions); + project.setDependencies(options.appDir, packages.appDeps, newVersions); // Show the user the messageLog of everything we removed. _.each(messageLog, function (msg) { @@ -831,7 +836,7 @@ main.registerCommand({ var items = []; if (options.using) { var packages = project.getDirectDependencies(options.appDir); - _.each(packages, function (version, name) { + _.each(packages.appDeps, function (version, name) { var versionInfo = catalog.getVersion(name, version); if (!versionInfo) { process.stderr.write("Cannot process package list. Unknown: " + name + @@ -1362,16 +1367,9 @@ main.registerCommand({ // The catalog doesn't know about other programs in your app. Let's blow // away their .build directories if they have them, and not rebuild // them. Sort of hacky, but eh. - var programsDir = path.join(options.appDir, 'programs'); - try { - var programs = fs.readdirSync(programsDir); - } catch (e) { - // OK if the programs directory doesn't exist; that'll just leave - // 'programs' empty. - if (e.code !== "ENOENT") - throw e; - } - _.each(programs, function (program) { + var programsDir = project.getProgramsDirectory(options.appDir); + var programsSubdirs = project.getProgramsSubdirs(options.appDir); + _.each(programsSubdirs, function (program) { files.rm_recursive(path.join(programsDir, program, '.build')); }); } diff --git a/tools/constraint-solver.js b/tools/constraint-solver.js index 8e7686de1d..1579c7d976 100644 --- a/tools/constraint-solver.js +++ b/tools/constraint-solver.js @@ -28,7 +28,7 @@ var deepClone = function (v) { // For some reason, _.map doesn't work in this context on Opera (weird test // failures). ret = []; - for (i = 0; i < v.length; i++) + for (var i = 0; i < v.length; i++) ret[i] = deepClone(v[i]); return ret; } @@ -244,7 +244,8 @@ var rejectExactDeps = function (deps) { return _.reject(deps, isExact); }; // converts dependencies from simple format to the structured format var toStructuredDeps = function (dependencies) { var structuredDeps = []; - _.each(dependencies, function (details, packageName) { + + var addStructuredDep = function (packageName, details) { // if details is null, it means 'no constraint' if (typeof details === "string" || details === null) { structuredDeps.push(_.extend( @@ -253,6 +254,16 @@ var toStructuredDeps = function (dependencies) { } else { structuredDeps.push(_.extend({ packageName: packageName }, details)); } + }; + + _.each(dependencies, function (details, packageName) { + if (_.isArray(details)) { + _.each(details, function (constraint) { + addStructuredDep(packageName, constraint); + }); + } else { + addStructuredDep(packageName, details); + } }); return structuredDeps; }; diff --git a/tools/project.js b/tools/project.js index 13dfdacad4..0ec62d862d 100644 --- a/tools/project.js +++ b/tools/project.js @@ -3,6 +3,9 @@ var path = require('path'); var _ = require('underscore'); var files = require('./files.js'); var utils = require('./utils.js'); +var tropohouse = require('./tropohouse.js'); +var archinfo = require('./archinfo.js'); +var watch = require('./watch.js'); var project = exports; @@ -74,9 +77,64 @@ project.processPerConstraintLines = function(lines) { }; -// Read in the .meteor/packages file. +project.getProgramsDirectory = function (appDir) { + return path.join(appDir, "programs"); +}; + +// Return the list of subdirectories containing programs in the +// app. Options can include: +// - watchSet: if provided, the app's programs directory will be added to it +project.getProgramsSubdirs = function (appDir, options) { + options = options || {}; + var programsDir = project.getProgramsDirectory(appDir); + var readOptions = { + absPath: programsDir, + include: [/\/$/], + exclude: [/^\./] + }; + if (options.watchSet) { + return watch.readAndWatchDirectory(options.watchSet, readOptions); + } else { + return watch.readDirectory(readOptions); + } +}; + +// Read direct dependencies from the .meteor/packages file and from +// programs in this app. +// +// Returns an object with keys: +// - appDeps: object mapping package names to version constraints +// - programsDeps: an object mapping program name to program deps, +// where program deps is an object mapping package names to version +// constraints. project.getDirectDependencies = function(appDir) { - return project.processPerConstraintLines(getPackagesLines(appDir)); + var appDeps = project.processPerConstraintLines(getPackagesLines(appDir)); + + var programsDeps = {}; + var programsSubdirs = project.getProgramsSubdirs(appDir); + var PackageSource; + _.each(programsSubdirs, function (item) { + if (! PackageSource) { + PackageSource = require('./package-source.js'); + } + + var programName = item.substr(0, item.length - 1); + programsDeps[programName] = {}; + + var programSubdir = path.join(project.getProgramsDirectory(appDir), item); + var programSource = new PackageSource(programSubdir); + programSource.initFromPackageDir(programName, programSubdir); + _.each(programSource.slices, function (sourceSlice) { + _.each(sourceSlice.uses, function (use) { + programsDeps[programName][use["package"]] = use.constraint || "none"; + }); + }); + }); + + return { + appDeps: appDeps, + programsDeps: programsDeps + }; }; // Get a list of constraints from the .meteor/versions file. @@ -85,7 +143,7 @@ project.getIndirectDependencies = function(appDir) { }; // Write the .meteor/versions file after running the constraint solver. -project.rewriteDependencies = function (appDir, deps, versions) { +var rewriteDependencies = function (appDir, deps, versions) { // Rewrite the packages file. Do this first, since the versions file is // derived from the packages file. @@ -112,10 +170,58 @@ project.rewriteDependencies = function (appDir, deps, versions) { lines.join(''), 'utf8'); }; + +// Call this after running the constraint solver. Downloads the +// necessary package builds and writes the .meteor/versions and +// .meteor/packages files with the results of the constraint solver. +// +// Only writes to .meteor/versions if all the requested versions were +// available from the package server. +// +// Returns an object whose keys are package names and values are +// versions that were successfully downloaded. +project.setDependencies = function (appDir, deps, versions) { + var downloadedPackages = {}; + _.each(versions, function (version, name) { + var packageVersionInfo = { packageName: name, version: version }; + // XXX error handling + var available = tropohouse.maybeDownloadPackageForArchitectures( + packageVersionInfo, + ['browser', archinfo.host()] + ); + if (available) { + downloadedPackages[name] = version; + } + }); + + if (_.keys(downloadedPackages).length === _.keys(versions).length) { + rewriteDependencies(appDir, deps, versions); + } + return downloadedPackages; +}; + var meteorReleaseFilePath = function (appDir) { return path.join(appDir, '.meteor', 'release'); }; +// Helper function. Given an object `deps` as returned from +// `getDirectDependencies`, combine all the direct dependencies (for the +// app and its programs) into a single object mapping package name to a +// list of version constraints, which can be passed into the constraint +// solver. +project.combineAppAndProgramDependencies = function (deps) { + var allDeps = {}; + _.each(deps.appDeps, function (constraint, packageName) { + allDeps[packageName] = [constraint]; + }); + _.each(deps.programsDeps, function (deps, programName) { + _.each(deps, function (constraint, packageName) { + allDeps[packageName] = allDeps[packageName] || []; + allDeps[packageName].push(constraint); + }); + }); + return allDeps; +}; // Run the constraint solver to determine the package versions to use. // @@ -127,20 +233,23 @@ project.generatePackageLoader = function (appDir) { var versions = project.getIndirectDependencies(appDir); var packages = project.getDirectDependencies(appDir); + // package name -> list of version constraints + var allPackages = project.combineAppAndProgramDependencies(packages); + // XXX: We are manually adding ctl here, but we should do this in a more // principled manner. var constraintSolver = require('./constraint-solver.js'); var resolver = new constraintSolver.Resolver; // XXX: constraint solver currently ignores versions, but it should not. var newVersions = resolver.resolve( - _.extend(packages, { "ctl" : "none" })); + _.extend(allPackages, { "ctl" : ["none"] })); if ( ! newVersions) { return { outcome: 'conflicting-versions' }; } - // Write out the new versions file. + // Download any necessary package builds and write out the new versions file. delete packages["ctl"]; - project.rewriteDependencies(appDir, packages, newVersions); + project.setDependencies(appDir, packages.appDeps, newVersions); var newVersionsReform = {}; _.each(newVersions, function (version, name) {