diff --git a/Gruntfile.coffee b/Gruntfile.coffee index a1a4b8520..73ca6e420 100644 --- a/Gruntfile.coffee +++ b/Gruntfile.coffee @@ -38,6 +38,6 @@ module.exports = (grunt) -> grunt.file.delete('bin/node_darwin_x64') if grunt.file.exists('bin/node_darwin_x64') grunt.registerTask('lint', ['coffeelint']) - grunt.registerTask('default', ['coffee', 'coffeelint:src']) - grunt.registerTask('test', ['clean', 'default', 'coffeelint:test', 'shell:test']) + grunt.registerTask('default', ['coffee', 'lint']) + grunt.registerTask('test', ['clean', 'default', 'shell:test']) grunt.registerTask('prepublish', ['clean', 'coffee', 'lint']) diff --git a/spec/fixtures/install-multi-version.json b/spec/fixtures/install-multi-version.json new file mode 100644 index 000000000..8bcf6134d --- /dev/null +++ b/spec/fixtures/install-multi-version.json @@ -0,0 +1,26 @@ +{ + "releases": { + "latest": "0.4.0" + }, + "name": "multi-module", + "versions": { + "0.4.0": { + "engines": { + "atom": ">=2.0" + } + }, + "0.3.0": { + "engines": { + "atom": ">=1.0" + }, + "dist": { + "tarball": "http://localhost:3000/tarball/test-module-1.0.0.tgz" + } + }, + "0.2.0": { + "engines": { + "atom": ">=1.0" + } + } + } +} diff --git a/spec/install-spec.coffee b/spec/install-spec.coffee index 14bfe0ef0..a6cdbfb70 100644 --- a/spec/install-spec.coffee +++ b/spec/install-spec.coffee @@ -1,4 +1,5 @@ path = require 'path' +CSON = require 'season' fs = require 'fs-plus' temp = require 'temp' express = require 'express' @@ -7,7 +8,7 @@ wrench = require 'wrench' apm = require '../lib/apm-cli' describe 'apm install', -> - atomHome = null + [atomHome, resourcePath] = [] beforeEach -> spyOnToken() @@ -16,6 +17,9 @@ describe 'apm install', -> atomHome = temp.mkdirSync('apm-home-dir-') process.env.ATOM_HOME = atomHome + resourcePath = temp.mkdirSync('atom-resource-path-') + process.env.ATOM_RESOURCE_PATH = resourcePath + describe "when installing an atom package", -> server = null @@ -41,6 +45,8 @@ describe 'apm install', -> response.sendfile path.join(__dirname, 'fixtures', 'test-module-with-symlink-5.0.0.tgz') app.get '/tarball/test-module-with-bin-2.0.0.tgz', (request, response) -> response.sendfile path.join(__dirname, 'fixtures', 'test-module-with-bin-2.0.0.tgz') + app.get '/packages/multi-module', (request, response) -> + response.sendfile path.join(__dirname, 'fixtures', 'install-multi-version.json') server = http.createServer(app) server.listen(3000) @@ -86,6 +92,35 @@ describe 'apm install', -> expect(fs.existsSync(path.join(testModuleDirectory, 'package.json'))).toBeTruthy() expect(callback.mostRecentCall.args[0]).toBeNull() + describe 'when multiple releases are available', -> + it 'installs the latest compatible version', -> + CSON.writeFileSync(path.join(resourcePath, 'package.json'), version: '1.5.0') + packageDirectory = path.join(atomHome, 'packages', 'test-module') + + callback = jasmine.createSpy('callback') + apm.run(['install', 'multi-module'], callback) + + waitsFor 'waiting for install to complete', 600000, -> + callback.callCount is 1 + + runs -> + expect(JSON.parse(fs.readFileSync(path.join(packageDirectory, 'package.json'))).version).toBe "1.0.0" + expect(callback.mostRecentCall.args[0]).toBeNull() + + it 'logs an error when no compatible versions are available', -> + CSON.writeFileSync(path.join(resourcePath, 'package.json'), version: '0.9.0') + packageDirectory = path.join(atomHome, 'packages', 'test-module') + + callback = jasmine.createSpy('callback') + apm.run(['install', 'multi-module'], callback) + + waitsFor 'waiting for install to complete', 600000, -> + callback.callCount is 1 + + runs -> + expect(fs.existsSync(packageDirectory)).toBeFalsy() + expect(callback.mostRecentCall.args[0]).not.toBeNull() + describe 'when multiple package names are specified', -> it 'installs all packages', -> testModuleDirectory = path.join(atomHome, 'packages', 'test-module') @@ -200,3 +235,16 @@ describe 'apm install', -> runs -> expect(callback.mostRecentCall.args[0]).not.toBeNull() + + describe 'when the package is bundled with Atom', -> + it 'logs a message to standard error', -> + CSON.writeFileSync(path.join(resourcePath, 'package.json'), packageDependencies: 'test-module': '1.0') + + callback = jasmine.createSpy('callback') + apm.run(['install', 'test-module'], callback) + + waitsFor 'waiting for install to complete', 600000, -> + callback.callCount is 1 + + runs -> + expect(console.error.mostRecentCall.args[0].length).toBeGreaterThan 0 diff --git a/src/apm-cli.coffee b/src/apm-cli.coffee index d5d905396..ee66381a1 100644 --- a/src/apm-cli.coffee +++ b/src/apm-cli.coffee @@ -35,6 +35,7 @@ commandClasses = [ require './login' require './publish' require './rebuild' + require './rebuild-module-cache' require './search' require './star' require './stars' diff --git a/src/dedupe.coffee b/src/dedupe.coffee index 3c42f8a16..9e4b88041 100644 --- a/src/dedupe.coffee +++ b/src/dedupe.coffee @@ -96,8 +96,9 @@ class Dedupe extends Command fs.makeTreeSync(@atomNodeDirectory) run: (options) -> - {callback} = options + {callback, cwd} = options options = @parseOptions(options.commandArgs) + options.cwd = cwd @createAtomDirectories() diff --git a/src/install.coffee b/src/install.coffee index 51374669c..d382a2daf 100644 --- a/src/install.coffee +++ b/src/install.coffee @@ -4,11 +4,13 @@ async = require 'async' _ = require 'underscore-plus' optimist = require 'optimist' CSON = require 'season' +semver = require 'semver' temp = require 'temp' config = require './config' Command = require './command' fs = require './fs' +RebuildModuleCache = require './rebuild-module-cache' request = require './request' module.exports = @@ -125,6 +127,10 @@ class Install extends Command destination = path.join(@atomPackagesDirectory, child) do (source, destination) -> commands.push (callback) -> fs.cp(source, destination, callback) + + commands.push (callback) => @buildModuleCache(pack.name, callback) + commands.push (callback) => @warmCompileCache(pack.name, callback) + async.waterfall commands, (error) => if error? @logFailure() @@ -184,7 +190,7 @@ class Install extends Command message = body.message ? body.error ? body callback("Request for package information failed: #{message}") else - if latestVersion = body.releases.latest + if body.releases.latest callback(null, body) else callback("No releases available for #{packageName}") @@ -273,14 +279,18 @@ class Install extends Command @logFailure() callback(error) else - commands = [] - packageVersion ?= pack.releases.latest + packageVersion ?= @getLatestCompatibleVersion(pack) + unless packageVersion + @logFailure() + callback("No available version compatible with the installed Atom version: #{@installedAtomVersion}") + {tarball} = pack.versions[packageVersion]?.dist ? {} unless tarball @logFailure() callback("Package version: #{packageVersion} not found") return + commands = [] commands.push (callback) => if packagePath = @getPackageCachePath(packageName, packageVersion) callback(null, packagePath) @@ -376,6 +386,68 @@ class Install extends Command packages = fs.readFileSync(filePath, 'utf8') @sanitizePackageNames(packages.split(/\s/)) + getResourcePath: (callback) -> + if @resourcePath + process.nextTick => callback(@resourcePath) + else + config.getResourcePath (@resourcePath) => callback(@resourcePath) + + buildModuleCache: (packageName, callback) -> + packageDirectory = path.join(@atomPackagesDirectory, packageName) + rebuildCacheCommand = new RebuildModuleCache() + rebuildCacheCommand.rebuild packageDirectory, -> + # Ignore cache errors and just finish the install + callback() + + warmCompileCache: (packageName, callback) -> + packageDirectory = path.join(@atomPackagesDirectory, packageName) + + @getResourcePath (resourcePath) -> + try + CoffeeCache = require(path.join(resourcePath, 'src', 'coffee-cache')) + + onDirectory = (directoryPath) -> + path.basename(directoryPath) isnt 'node_modules' + + onFile = (filePath) -> + CoffeeCache.addPathToCache(filePath) + + fs.traverseTreeSync(packageDirectory, onFile, onDirectory) + callback(null) + + isBundledPackage: (packageName, callback) -> + @getResourcePath (resourcePath) -> + try + atomMetadata = JSON.parse(fs.readFileSync(path.join(resourcePath, 'package.json'))) + catch error + return callback(false) + + callback(atomMetadata?.packageDependencies?.hasOwnProperty(packageName)) + + getLatestCompatibleVersion: (pack) -> + return pack.releases.latest unless @installedAtomVersion + + latestVersion = null + for version, metadata of pack.versions ? {} + continue unless semver.valid(version) + continue unless metadata + + engine = metadata.engines?.atom ? '*' + continue unless semver.validRange(engine) + continue unless semver.satisfies(@installedAtomVersion, engine) + + latestVersion ?= version + latestVersion = version if semver.gt(version, latestVersion) + + latestVersion + + loadInstalledAtomVersion: (callback) -> + @getResourcePath (resourcePath) => + try + {version} = require(path.join(resourcePath, 'package.json')) ? {} + @installedAtomVersion = version if semver.valid(version) + callback() + run: (options) -> {callback} = options options = @parseOptions(options.commandArgs) @@ -393,9 +465,16 @@ class Install extends Command if atIndex > 0 version = name.substring(atIndex + 1) name = name.substring(0, atIndex) - @installPackage({name, version}, options, callback) - commands = [] + @isBundledPackage name, (isBundledPackage) => + if isBundledPackage + console.error """ + The #{name} package is bundled with Atom and should not be explicitly installed. + You can run `apm uninstall #{name}` to uninstall it and then the version bundled + with Atom will be used. + """.yellow + @installPackage({name, version}, options, callback) + if packagesFilePath try packageNames = @packageNamesFromPath(packagesFilePath) @@ -404,6 +483,9 @@ class Install extends Command else packageNames = @packageNamesFromArgv(options.argv) packageNames.push('.') if packageNames.length is 0 + + commands = [] + commands.push (callback) => @loadInstalledAtomVersion(callback) packageNames.forEach (packageName) -> commands.push (callback) -> installPackage(packageName, callback) async.waterfall(commands, callback) diff --git a/src/links.coffee b/src/links.coffee index ec8f0cfe7..55e36e984 100644 --- a/src/links.coffee +++ b/src/links.coffee @@ -47,6 +47,9 @@ class Links extends Command realpath = '???'.red "#{path.basename(link).yellow} -> #{realpath}" - run: -> + run: (options) -> + {callback} = options + @logLinks(@devPackagesPath) @logLinks(@packagesPath) + callback() diff --git a/src/rebuild-module-cache.coffee b/src/rebuild-module-cache.coffee new file mode 100644 index 000000000..6d7341116 --- /dev/null +++ b/src/rebuild-module-cache.coffee @@ -0,0 +1,48 @@ +path = require 'path' +async = require 'async' +Command = require './command' +config = require './config' +fs = require './fs' + +module.exports = +class RebuildModuleCache extends Command + @commandNames: ['rebuild-module-cache'] + + constructor: -> + @atomPackagesDirectory = path.join(config.getAtomDirectory(), 'packages') + + getResourcePath: (callback) -> + if @resourcePath + process.nextTick => callback(@resourcePath) + else + config.getResourcePath (@resourcePath) => callback(@resourcePath) + + rebuild: (packageDirectory, callback) -> + @getResourcePath (resourcePath) => + try + @moduleCache ?= require(path.join(resourcePath, 'src', 'module-cache')) + @moduleCache.create(packageDirectory) + catch error + return callback(error) + + callback() + + run: (options) -> + {callback} = options + + commands = [] + fs.list(@atomPackagesDirectory).forEach (packageName) => + packageDirectory = path.join(@atomPackagesDirectory, packageName) + return if fs.isSymbolicLinkSync(packageDirectory) + return unless fs.isDirectorySync(packageDirectory) + + commands.push (callback) => + process.stdout.write "Rebuilding #{packageName} module cache " + @rebuild packageDirectory, (error) => + if error? + @logFailure() + else + @logSuccess() + callback(error) + + async.waterfall(commands, callback)