diff --git a/spec/fixtures/atom-2048.json b/spec/fixtures/atom-2048.json index a3dc69bf8..aa1b5759a 100644 --- a/spec/fixtures/atom-2048.json +++ b/spec/fixtures/atom-2048.json @@ -8,7 +8,8 @@ "1.2.3": { "dist": { "tarball": "http://localhost:3000/tarball/test-module-1.0.0.tgz" - } + }, + "version": "1.2.3" } } } diff --git a/spec/fixtures/install-multi-version.json b/spec/fixtures/install-multi-version.json index 8bcf6134d..456eb89d7 100644 --- a/spec/fixtures/install-multi-version.json +++ b/spec/fixtures/install-multi-version.json @@ -15,7 +15,8 @@ }, "dist": { "tarball": "http://localhost:3000/tarball/test-module-1.0.0.tgz" - } + }, + "version": "0.3.0" }, "0.2.0": { "engines": { diff --git a/spec/fixtures/install-test-module-version-0.2.0.json b/spec/fixtures/install-test-module-version-0.2.0.json new file mode 100644 index 000000000..477cada49 --- /dev/null +++ b/spec/fixtures/install-test-module-version-0.2.0.json @@ -0,0 +1,6 @@ +{ + "dist": { + "tarball": "http://localhost:3000/tarball/test-module-0.2.0.tgz" + }, + "version": "0.2.0" +} diff --git a/spec/fixtures/install-test-module-with-bin.json b/spec/fixtures/install-test-module-with-bin.json index 78ba5ac45..b6c319e57 100644 --- a/spec/fixtures/install-test-module-with-bin.json +++ b/spec/fixtures/install-test-module-with-bin.json @@ -7,7 +7,8 @@ "2.0.0": { "dist": { "tarball": "http://localhost:3000/tarball/test-module-with-bin-2.0.0.tgz" - } + }, + "version": "2.0.0" } } } diff --git a/spec/fixtures/install-test-module-with-symlink.json b/spec/fixtures/install-test-module-with-symlink.json index 18f4da936..1782b2775 100644 --- a/spec/fixtures/install-test-module-with-symlink.json +++ b/spec/fixtures/install-test-module-with-symlink.json @@ -7,7 +7,8 @@ "5.0.0": { "dist": { "tarball": "http://localhost:3000/tarball/test-module-with-symlink-5.0.0.tgz" - } + }, + "version": "5.0.0" } } } diff --git a/spec/fixtures/install-test-module.json b/spec/fixtures/install-test-module.json index 613b5c789..be07f711b 100644 --- a/spec/fixtures/install-test-module.json +++ b/spec/fixtures/install-test-module.json @@ -6,8 +6,15 @@ "versions": { "0.4.0": { "dist": { - "tarball": "http://localhost:3000/tarball/test-module-1.0.0.tgz" - } + "tarball": "http://localhost:3000/tarball/test-module-0.4.0.tgz" + }, + "version": "0.4.0" + }, + "0.3.0": { + "dist": { + "tarball": "http://localhost:3000/tarball/test-module-0.3.0.tgz" + }, + "version": "0.3.0" } } } diff --git a/spec/fixtures/install-test-module2.json b/spec/fixtures/install-test-module2.json index 014c3d531..921f33e33 100644 --- a/spec/fixtures/install-test-module2.json +++ b/spec/fixtures/install-test-module2.json @@ -7,7 +7,8 @@ "2.0.0": { "dist": { "tarball": "http://localhost:3000/tarball/test-module2-2.0.0.tgz" - } + }, + "version": "2.0.0" } } } diff --git a/spec/fixtures/native-package.json b/spec/fixtures/native-package.json index 55356c355..186a5b25b 100644 --- a/spec/fixtures/native-package.json +++ b/spec/fixtures/native-package.json @@ -7,7 +7,8 @@ "1.0.0": { "dist": { "tarball": "http://localhost:3000/tarball/native-package-1.0.0.tgz" - } + }, + "version": "1.0.0" } } } diff --git a/spec/fixtures/test-module-0.2.0.tgz b/spec/fixtures/test-module-0.2.0.tgz new file mode 100644 index 000000000..8e05b166d Binary files /dev/null and b/spec/fixtures/test-module-0.2.0.tgz differ diff --git a/spec/fixtures/test-module-0.3.0.tgz b/spec/fixtures/test-module-0.3.0.tgz new file mode 100644 index 000000000..485a5a579 Binary files /dev/null and b/spec/fixtures/test-module-0.3.0.tgz differ diff --git a/spec/fixtures/test-module-0.4.0.tgz b/spec/fixtures/test-module-0.4.0.tgz new file mode 100644 index 000000000..9d2ccc694 Binary files /dev/null and b/spec/fixtures/test-module-0.4.0.tgz differ diff --git a/spec/install-spec.coffee b/spec/install-spec.coffee index f2a038ed2..a5070b060 100644 --- a/spec/install-spec.coffee +++ b/spec/install-spec.coffee @@ -37,12 +37,20 @@ describe 'apm install', -> response.sendfile path.join(__dirname, 'fixtures', 'node_x64.lib') app.get '/node/v0.10.3/SHASUMS256.txt', (request, response) -> response.sendfile path.join(__dirname, 'fixtures', 'SHASUMS256.txt') + app.get '/tarball/test-module-0.2.0.tgz', (request, response) -> + response.sendfile path.join(__dirname, 'fixtures', 'test-module-0.2.0.tgz') + app.get '/tarball/test-module-0.3.0.tgz', (request, response) -> + response.sendfile path.join(__dirname, 'fixtures', 'test-module-0.3.0.tgz') + app.get '/tarball/test-module-0.4.0.tgz', (request, response) -> + response.sendfile path.join(__dirname, 'fixtures', 'test-module-0.4.0.tgz') app.get '/tarball/test-module-1.0.0.tgz', (request, response) -> response.sendfile path.join(__dirname, 'fixtures', 'test-module-1.0.0.tgz') app.get '/tarball/test-module2-2.0.0.tgz', (request, response) -> response.sendfile path.join(__dirname, 'fixtures', 'test-module2-2.0.0.tgz') app.get '/packages/test-module', (request, response) -> response.sendfile path.join(__dirname, 'fixtures', 'install-test-module.json') + app.get '/packages/test-module/versions/0.2.0', (request, response) -> + response.sendfile path.join(__dirname, 'fixtures', 'install-test-module-version-0.2.0.json') app.get '/packages/test-module2', (request, response) -> response.sendfile path.join(__dirname, 'fixtures', 'install-test-module2.json') app.get '/packages/test-rename', (request, response) -> @@ -201,6 +209,43 @@ describe 'apm install', -> expect(fs.existsSync(path.join(testModuleDirectory, 'package.json'))).toBeTruthy() expect(callback.mostRecentCall.args[0]).toBeNull() + describe 'when an explicit version is specified', -> + it 'installs the version', -> + testModuleDirectory = path.join(atomHome, 'packages', 'test-module') + + callback = jasmine.createSpy('callback') + apm.run(['install', "test-module@0.3.0"], callback) + + waitsFor 'waiting for install to complete', 600000, -> + callback.callCount is 1 + + runs -> + expect(callback.mostRecentCall.args[0]).toBeNull() + expect(JSON.parse(fs.readFileSync(path.join(testModuleDirectory, 'package.json'))).version).toBe "0.3.0" + + it 'allows installing versions not in the package JSON', -> + testModuleDirectory = path.join(atomHome, 'packages', 'test-module') + + callback = jasmine.createSpy('callback') + apm.run(['install', "test-module@0.2.0"], callback) + + waitsFor 'waiting for install to complete', 600000, -> + callback.callCount is 1 + + runs -> + expect(callback.mostRecentCall.args[0]).toBeNull() + expect(JSON.parse(fs.readFileSync(path.join(testModuleDirectory, 'package.json'))).version).toBe "0.2.0" + + it 'gives an error when installing a nonexistent version', -> + callback = jasmine.createSpy('callback') + apm.run(['install', "test-module@0.1.0"], callback) + + waitsFor 'waiting for install to complete', 600000, -> + callback.callCount is 1 + + runs -> + expect(callback.mostRecentCall.args[0]).toBe 'Package version: 0.1.0 not found' + describe 'when multiple package names are specified', -> it 'installs all packages', -> testModuleDirectory = path.join(atomHome, 'packages', 'test-module') diff --git a/spec/stars-spec.coffee b/spec/stars-spec.coffee index 53f196be3..4afc07b0b 100644 --- a/spec/stars-spec.coffee +++ b/spec/stars-spec.coffee @@ -25,8 +25,8 @@ describe 'apm stars', -> response.sendfile path.join(__dirname, 'fixtures', 'node_x64.lib') app.get '/node/v0.10.3/SHASUMS256.txt', (request, response) -> response.sendfile path.join(__dirname, 'fixtures', 'SHASUMS256.txt') - app.get '/tarball/test-module-1.0.0.tgz', (request, response) -> - response.sendfile path.join(__dirname, 'fixtures', 'test-module-1.0.0.tgz') + app.get '/tarball/test-module-0.4.0.tgz', (request, response) -> + response.sendfile path.join(__dirname, 'fixtures', 'test-module-0.4.0.tgz') app.get '/tarball/test-module2-2.0.0.tgz', (request, response) -> response.sendfile path.join(__dirname, 'fixtures', 'test-module2-2.0.0.tgz') app.get '/packages/test-module', (request, response) -> diff --git a/src/install.coffee b/src/install.coffee index 10ebfff98..a66846925 100644 --- a/src/install.coffee +++ b/src/install.coffee @@ -235,6 +235,30 @@ class Install extends Command else callback("No releases available for #{packageName}") + # Request package version information from the atom.io API. + # + # packageName - The string name of the package to request. + # versionName - The string version of the package to request. + # callback - The function to invoke when the request completes with an error + # as the first argument and an object as the second. + requestPackageVersion: (packageName, versionName, callback) -> + requestSettings = + url: "#{config.getAtomPackagesUrl()}/#{packageName}/versions/#{versionName}" + json: true + retries: 4 + request.get requestSettings, (error, response, body={}) -> + if error? + message = "Request for package version information failed: #{error.message}" + message += " (#{error.code})" if error.code + callback(message) + else if response.statusCode is 404 + callback("Package version: #{versionName} not found") + else if response.statusCode isnt 200 + message = request.getErrorMessage(response, body) + callback("Request for package version information failed: #{message}") + else + callback(null, body) + # Download a package tarball. # # packageUrl - The string tarball URL to request @@ -271,13 +295,13 @@ class Install extends Command # Get the path to the package from the local cache. # # packageName - The string name of the package. - # packageVersion - The string version of the package. + # versionName - The string version of the package. # callback - The function to call with error and cachePath arguments. # # Returns a path to the cached tarball or undefined when not in the cache. - getPackageCachePath: (packageName, packageVersion, callback) -> + getPackageCachePath: (packageName, versionName, callback) -> cacheDir = config.getCacheDirectory() - cachePath = path.join(cacheDir, packageName, packageVersion, 'package.tgz') + cachePath = path.join(cacheDir, packageName, versionName, 'package.tgz') if fs.isFileSync(cachePath) tempPath = path.join(temp.mkdirSync(), path.basename(cachePath)) fs.cp cachePath, tempPath, (error) -> @@ -287,16 +311,16 @@ class Install extends Command callback(null, tempPath) else process.nextTick -> - callback(new Error("#{packageName}@#{packageVersion} is not in the cache")) + callback(new Error("#{packageName}@#{versionName} is not in the cache")) # Is the package at the specified version already installed? # # * packageName: The string name of the package. - # * packageVersion: The string version of the package. - isPackageInstalled: (packageName, packageVersion) -> + # * versionName: The string version of the package. + isPackageInstalled: (packageName, versionName) -> try {version} = CSON.readFileSync(CSON.resolve(path.join('node_modules', packageName, 'package'))) ? {} - packageVersion is version + versionName is version catch error false @@ -310,16 +334,16 @@ class Install extends Command # error as the first argument. installRegisteredPackage: (metadata, options, callback) -> packageName = metadata.name - packageVersion = metadata.version + versionName = metadata.version installGlobally = options.installGlobally ? true unless installGlobally - if packageVersion and @isPackageInstalled(packageName, packageVersion) + if versionName and @isPackageInstalled(packageName, versionName) callback(null, {}) return label = packageName - label += "@#{packageVersion}" if packageVersion + label += "@#{versionName}" if versionName unless options.argv.json process.stdout.write "Installing #{label} " if installGlobally @@ -330,50 +354,69 @@ class Install extends Command @logFailure() callback(error) else - packageVersion ?= @getLatestCompatibleVersion(pack) - unless packageVersion + versionName ?= @getLatestCompatibleVersion(pack) + unless versionName @logFailure() callback("No available version compatible with the installed Atom version: #{@installedAtomVersion}") return - {tarball} = pack.versions[packageVersion]?.dist ? {} - unless tarball - @logFailure() - callback("Package version: #{packageVersion} not found") - return - - commands = [] - commands.push (next) => - @getPackageCachePath packageName, packageVersion, (error, packagePath) => - if packagePath - next(null, packagePath) - else - @downloadPackage(tarball, installGlobally, next) - installNode = options.installNode ? true - if installNode - commands.push (packagePath, next) => - @installNode (error) -> next(error, packagePath) - commands.push (packagePath, next) => - @installModule(options, pack, packagePath, next) - if installGlobally and (packageName.localeCompare(pack.name, 'en', {sensitivity: 'accent'}) isnt 0) - commands.push (newPack, next) => # package was renamed; delete old package folder - fs.removeSync(path.join(@atomPackagesDirectory, packageName)) - next(null, newPack) - commands.push ({installPath}, next) -> - if installPath? - metadata = JSON.parse(fs.readFileSync(path.join(installPath, 'package.json'), 'utf8')) - json = {installPath, metadata} - next(null, json) - else - next(null, {}) # installed locally, no install path data - - async.waterfall commands, (error, json) => - unless installGlobally + version = pack.versions[versionName] + unless version + # The package information only has recent versions, so do another API + # request in case this is an older version. + @requestPackageVersion packageName, versionName, (error, version) => if error? @logFailure() + callback(error) else - @logSuccess() unless options.argv.json - callback(error, json) + @installPackageVersion(packageName, pack, version, options, callback) + return + @installPackageVersion(packageName, pack, version, options, callback) + + # Install the package with the given name and optional version + # + # packageName - The originally-requested package name. This might differ from + # pack.name if the package was renamed. + # pack - The package object returned by the API. + # version - The version object returned by the API. + # options - The installation options object. + # callback - The function to invoke when installation completes with an + # error as the first argument. + installPackageVersion: (packageName, pack, version, options, callback) -> + installGlobally = options.installGlobally ? true + tarball = version.dist.tarball + commands = [] + commands.push (next) => + @getPackageCachePath packageName, version.version, (error, packagePath) => + if packagePath + next(null, packagePath) + else + @downloadPackage(tarball, installGlobally, next) + installNode = options.installNode ? true + if installNode + commands.push (packagePath, next) => + @installNode (error) -> next(error, packagePath) + commands.push (packagePath, next) => + @installModule(options, pack, packagePath, next) + if installGlobally and (packageName.localeCompare(pack.name, 'en', {sensitivity: 'accent'}) isnt 0) + commands.push (newPack, next) => # package was renamed; delete old package folder + fs.removeSync(path.join(@atomPackagesDirectory, packageName)) + next(null, newPack) + commands.push ({installPath}, next) -> + if installPath? + metadata = JSON.parse(fs.readFileSync(path.join(installPath, 'package.json'), 'utf8')) + json = {installPath, metadata} + next(null, json) + else + next(null, {}) # installed locally, no install path data + + async.waterfall commands, (error, json) => + unless installGlobally + if error? + @logFailure() + else + @logSuccess() unless options.argv.json + callback(error, json) # Install all the package dependencies found in the package.json file. #