Skip to content
This repository was archived by the owner on Dec 15, 2022. It is now read-only.

Allow installing old versions not listed in the package info #773

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion spec/fixtures/atom-2048.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
"1.2.3": {
"dist": {
"tarball": "http://localhost:3000/tarball/test-module-1.0.0.tgz"
}
},
"version": "1.2.3"
}
}
}
3 changes: 2 additions & 1 deletion spec/fixtures/install-multi-version.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
},
"dist": {
"tarball": "http://localhost:3000/tarball/test-module-1.0.0.tgz"
}
},
"version": "0.3.0"
},
"0.2.0": {
"engines": {
Expand Down
6 changes: 6 additions & 0 deletions spec/fixtures/install-test-module-version-0.2.0.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"dist": {
"tarball": "http://localhost:3000/tarball/test-module-0.2.0.tgz"
},
"version": "0.2.0"
}
3 changes: 2 additions & 1 deletion spec/fixtures/install-test-module-with-bin.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
}
3 changes: 2 additions & 1 deletion spec/fixtures/install-test-module-with-symlink.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
}
11 changes: 9 additions & 2 deletions spec/fixtures/install-test-module.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
}
3 changes: 2 additions & 1 deletion spec/fixtures/install-test-module2.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"2.0.0": {
"dist": {
"tarball": "http://localhost:3000/tarball/test-module2-2.0.0.tgz"
}
},
"version": "2.0.0"
}
}
}
3 changes: 2 additions & 1 deletion spec/fixtures/native-package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"1.0.0": {
"dist": {
"tarball": "http://localhost:3000/tarball/native-package-1.0.0.tgz"
}
},
"version": "1.0.0"
}
}
}
Binary file added spec/fixtures/test-module-0.2.0.tgz
Binary file not shown.
Binary file added spec/fixtures/test-module-0.3.0.tgz
Binary file not shown.
Binary file added spec/fixtures/test-module-0.4.0.tgz
Binary file not shown.
45 changes: 45 additions & 0 deletions spec/install-spec.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -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) ->
Expand Down Expand Up @@ -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', "[email protected]"], 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', "[email protected]"], 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', "[email protected]"], 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')
Expand Down
4 changes: 2 additions & 2 deletions spec/stars-spec.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -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) ->
Expand Down
137 changes: 90 additions & 47 deletions src/install.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) ->
Expand All @@ -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

Expand All @@ -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
Expand All @@ -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.
#
Expand Down