Commands take specs as input instead of names.

modified clean, create, fetch, install, and uninstall
This commit is contained in:
Todd Gamblin 2013-05-12 14:17:38 -07:00
parent ad4411bc9e
commit b2a5fef6ad
15 changed files with 265 additions and 180 deletions

View File

@ -1,7 +1,9 @@
import os import os
import re import re
import sys
import spack import spack
import spack.spec
import spack.tty as tty import spack.tty as tty
import spack.attr as attr import spack.attr as attr
@ -21,10 +23,6 @@
commands.sort() commands.sort()
def null_op(*args):
pass
def get_cmd_function_name(name): def get_cmd_function_name(name):
return name.replace("-", "_") return name.replace("-", "_")
@ -36,7 +34,7 @@ def get_module(name):
module_name, fromlist=[name, SETUP_PARSER, DESCRIPTION], module_name, fromlist=[name, SETUP_PARSER, DESCRIPTION],
level=0) level=0)
attr.setdefault(module, SETUP_PARSER, null_op) attr.setdefault(module, SETUP_PARSER, lambda *args: None) # null-op
attr.setdefault(module, DESCRIPTION, "") attr.setdefault(module, DESCRIPTION, "")
fn_name = get_cmd_function_name(name) fn_name = get_cmd_function_name(name)
@ -50,3 +48,22 @@ def get_module(name):
def get_command(name): def get_command(name):
"""Imports the command's function from a module and returns it.""" """Imports the command's function from a module and returns it."""
return getattr(get_module(name), get_cmd_function_name(name)) return getattr(get_module(name), get_cmd_function_name(name))
def parse_specs(args):
"""Convenience function for parsing arguments from specs. Handles common
exceptions and dies if there are errors.
"""
if type(args) == list:
args = " ".join(args)
try:
return spack.spec.parse(" ".join(args))
except spack.parse.ParseError, e:
e.print_error(sys.stdout)
sys.exit(1)
except spack.spec.SpecError, e:
tty.error(e.message)
sys.exit(1)

View File

@ -1,3 +1,6 @@
import argparse
import spack.cmd
import spack.packages as packages import spack.packages as packages
import spack.tty as tty import spack.tty as tty
import spack.stage as stage import spack.stage as stage
@ -5,21 +8,22 @@
description = "Remove staged files for packages" description = "Remove staged files for packages"
def setup_parser(subparser): def setup_parser(subparser):
subparser.add_argument('names', nargs='+', help="name(s) of package(s) to clean")
subparser.add_argument('-c', "--clean", action="store_true", dest='clean', subparser.add_argument('-c', "--clean", action="store_true", dest='clean',
help="run make clean in the stage directory (default)") help="run make clean in the stage directory (default)")
subparser.add_argument('-w', "--work", action="store_true", dest='work', subparser.add_argument('-w', "--work", action="store_true", dest='work',
help="delete and re-expand the entire stage directory") help="delete and re-expand the entire stage directory")
subparser.add_argument('-d', "--dist", action="store_true", dest='dist', subparser.add_argument('-d', "--dist", action="store_true", dest='dist',
help="delete the downloaded archive.") help="delete the downloaded archive.")
subparser.add_argument('packages', nargs=argparse.REMAINDER, help="specs of packages to clean")
def clean(parser, args): def clean(parser, args):
if not args.names: if not args.packages:
tty.die("spack clean requires at least one package name.") tty.die("spack clean requires at least one package argument")
for name in args.names: specs = spack.cmd.parse_specs(args.packages)
package = packages.get(name) for spec in specs:
package = packages.get(spec.name)
if args.dist: if args.dist:
package.do_clean_dist() package.do_clean_dist()
elif args.work: elif args.work:

View File

@ -4,7 +4,7 @@
import spack import spack
import spack.packages as packages import spack.packages as packages
import spack.tty as tty import spack.tty as tty
import spack.version import spack.url
from spack.stage import Stage from spack.stage import Stage
from contextlib import closing from contextlib import closing
@ -38,7 +38,7 @@ def create(parser, args):
url = args.url url = args.url
# Try to deduce name and version of the new package from the URL # Try to deduce name and version of the new package from the URL
name, version = spack.version.parse(url) name, version = spack.url.parse_name_and_version(url)
if not name: if not name:
print "Couldn't guess a name for this package." print "Couldn't guess a name for this package."
while not name: while not name:

View File

@ -1,12 +1,18 @@
import argparse
import spack.cmd
import spack.packages as packages import spack.packages as packages
description = "Fetch archives for packages" description = "Fetch archives for packages"
def setup_parser(subparser): def setup_parser(subparser):
subparser.add_argument('names', nargs='+', help="names of packages to fetch") subparser.add_argument('packages', nargs=argparse.REMAINDER, help="specs of packages to fetch")
def fetch(parser, args): def fetch(parser, args):
for name in args.names: if not args.packages:
package = packages.get(name) tty.die("fetch requires at least one package argument")
specs = spack.cmd.parse_specs(args.packages)
for spec in specs:
package = packages.get(spec.name)
package.do_fetch() package.do_fetch()

View File

@ -1,19 +1,28 @@
import sys
import argparse
import spack import spack
import spack.packages as packages import spack.packages as packages
import spack.cmd
description = "Build and install packages" description = "Build and install packages"
def setup_parser(subparser): def setup_parser(subparser):
subparser.add_argument('names', nargs='+', help="names of packages to install")
subparser.add_argument('-i', '--ignore-dependencies', subparser.add_argument('-i', '--ignore-dependencies',
action='store_true', dest='ignore_dependencies', action='store_true', dest='ignore_dependencies',
help="Do not try to install dependencies of requested packages.") help="Do not try to install dependencies of requested packages.")
subparser.add_argument('-d', '--dirty', action='store_true', dest='dirty', subparser.add_argument('-d', '--dirty', action='store_true', dest='dirty',
help="Don't clean up partially completed build/installation on error.") help="Don't clean up partially completed build/installation on error.")
subparser.add_argument('packages', nargs=argparse.REMAINDER, help="specs of packages to install")
def install(parser, args): def install(parser, args):
if not args.packages:
tty.die("install requires at least one package argument")
spack.ignore_dependencies = args.ignore_dependencies spack.ignore_dependencies = args.ignore_dependencies
for name in args.names: specs = spack.cmd.parse_specs(args.packages)
package = packages.get(name) for spec in specs:
package = packages.get(spec.name)
package.dirty = args.dirty package.dirty = args.dirty
package.do_install() package.do_install()

View File

@ -1,15 +1,22 @@
import spack.cmd
import spack.packages as packages import spack.packages as packages
import argparse
description="Remove an installed package" description="Remove an installed package"
def setup_parser(subparser): def setup_parser(subparser):
subparser.add_argument('names', nargs='+', help="name(s) of package(s) to uninstall")
subparser.add_argument('-f', '--force', action='store_true', dest='force', subparser.add_argument('-f', '--force', action='store_true', dest='force',
help="Ignore installed packages that depend on this one and remove it anyway.") help="Ignore installed packages that depend on this one and remove it anyway.")
subparser.add_argument('packages', nargs=argparse.REMAINDER, help="specs of packages to uninstall")
def uninstall(parser, args): def uninstall(parser, args):
if not args.packages:
tty.die("uninstall requires at least one package argument.")
specs = spack.cmd.parse_specs(args.packages)
# get packages to uninstall as a list. # get packages to uninstall as a list.
pkgs = [packages.get(name) for name in args.names] pkgs = [packages.get(spec.name) for spec in specs]
# Sort packages to be uninstalled by the number of installed dependents # Sort packages to be uninstalled by the number of installed dependents
# This ensures we do things in the right order # This ensures we do things in the right order

View File

@ -9,7 +9,7 @@
class Dependency(object): class Dependency(object):
"""Represents a dependency from one package to another. """Represents a dependency from one package to another.
""" """
def __init__(self, name, version): def __init__(self, name):
self.name = name self.name = name
@property @property

View File

@ -22,7 +22,7 @@
import tty import tty
import attr import attr
import validate import validate
import version import url
import arch import arch
from multi_function import platform from multi_function import platform
@ -261,7 +261,7 @@ def __init__(self, sys_type = arch.sys_type()):
validate.url(self.url) validate.url(self.url)
# Set up version # Set up version
attr.setdefault(self, 'version', version.parse_version(self.url)) attr.setdefault(self, 'version', url.parse_version(self.url))
if not self.version: if not self.version:
tty.die("Couldn't extract version from %s. " + tty.die("Couldn't extract version from %s. " +
"You must specify it explicitly for this URL." % self.url) "You must specify it explicitly for this URL." % self.url)

View File

@ -1,5 +1,6 @@
import re import re
import spack.error as err import spack.error as err
import spack.tty as tty
import itertools import itertools
@ -11,9 +12,7 @@ def __init__(self, message, string, pos):
self.pos = pos self.pos = pos
def print_error(self, out): def print_error(self, out):
out.write(self.message + ":\n\n") tty.error(self.message, self.string, self.pos * " " + "^")
out.write(" " + self.string + "\n")
out.write(" " + self.pos * " " + "^\n\n")
class LexError(ParseError): class LexError(ParseError):
@ -107,7 +106,7 @@ def expect(self, id):
if self.next: if self.next:
self.unexpected_token() self.unexpected_token()
else: else:
self.next_token_error("Unexpected end of file") self.next_token_error("Unexpected end of input")
sys.exit(1) sys.exit(1)
def parse(self, text): def parse(self, text):

View File

@ -44,6 +44,7 @@ class Mpileaks(Package):
spack install mpileaks ^mvapich spack install mpileaks ^mvapich
spack install mpileaks ^mpich spack install mpileaks ^mpich
""" """
import sys
from dependency import Dependency from dependency import Dependency

View File

@ -126,6 +126,9 @@ def expanded_archive_path(self):
"""Returns the path to the expanded archive directory if it's expanded; """Returns the path to the expanded archive directory if it's expanded;
None if the archive hasn't been expanded. None if the archive hasn't been expanded.
""" """
if not self.archive_file:
return None
for file in os.listdir(self.path): for file in os.listdir(self.path):
archive_path = spack.new_path(self.path, file) archive_path = spack.new_path(self.path, file)
if os.path.isdir(archive_path): if os.path.isdir(archive_path):

View File

@ -3,18 +3,19 @@
detection in Homebrew. detection in Homebrew.
""" """
import unittest import unittest
import spack.version as ver import spack.url as url
from pprint import pprint from pprint import pprint
class UrlParseTest(unittest.TestCase): class UrlParseTest(unittest.TestCase):
def assert_not_detected(self, string): def assert_not_detected(self, string):
self.assertRaises(ver.UndetectableVersionError, ver.parse, string) self.assertRaises(
url.UndetectableVersionError, url.parse_name_and_version, string)
def assert_detected(self, name, v, string): def assert_detected(self, name, v, string):
parsed_name, parsed_v = ver.parse(string) parsed_name, parsed_v = url.parse_name_and_version(string)
self.assertEqual(parsed_name, name) self.assertEqual(parsed_name, name)
self.assertEqual(parsed_v, ver.Version(v)) self.assertEqual(parsed_v, url.Version(v))
def test_wwwoffle_version(self): def test_wwwoffle_version(self):
self.assert_detected( self.assert_detected(

View File

@ -7,7 +7,7 @@
from spack.version import * from spack.version import *
class CompareVersionsTest(unittest.TestCase): class VersionsTest(unittest.TestCase):
def assert_ver_lt(self, a, b): def assert_ver_lt(self, a, b):
a, b = ver(a), ver(b) a, b = ver(a), ver(b)
@ -129,7 +129,8 @@ def test_rpm_oddities(self):
self.assert_ver_lt('1.fc17', '1g.fc17') self.assert_ver_lt('1.fc17', '1g.fc17')
# Stuff below here is not taken from RPM's tests. # Stuff below here is not taken from RPM's tests and is
# unique to spack
def test_version_ranges(self): def test_version_ranges(self):
self.assert_ver_lt('1.2:1.4', '1.6') self.assert_ver_lt('1.2:1.4', '1.6')
self.assert_ver_gt('1.6', '1.2:1.4') self.assert_ver_gt('1.6', '1.2:1.4')

166
lib/spack/spack/url.py Normal file
View File

@ -0,0 +1,166 @@
"""
This module has methods for parsing names and versions of packages from URLs.
The idea is to allow package creators to supply nothing more than the
download location of the package, and figure out version and name information
from there.
Example: when spack is given the following URL:
ftp://ftp.ruby-lang.org/pub/ruby/1.9/ruby-1.9.1-p243.tar.gz
It can figure out that the package name is ruby, and that it is at version
1.9.1-p243. This is useful for making the creation of packages simple: a user
just supplies a URL and skeleton code is generated automatically.
Spack can also figure out that it can most likely download 1.8.1 at this URL:
ftp://ftp.ruby-lang.org/pub/ruby/1.9/ruby-1.8.1.tar.gz
This is useful if a user asks for a package at a particular version number;
spack doesn't need anyone to tell it where to get the tarball even though
it's never been told about that version before.
"""
import os
import re
import spack.error
import spack.utils
from spack.version import Version
class UrlParseError(spack.error.SpackError):
"""Raised when the URL module can't parse something correctly."""
def __init__(self, msg, spec):
super(UrlParseError, self).__init__(msg)
self.spec = spec
class UndetectableVersionError(UrlParseError):
"""Raised when we can't parse a version from a string."""
def __init__(self, spec):
super(UndetectableVersionError, self).__init__(
"Couldn't detect version in: " + spec, spec)
class UndetectableNameError(UrlParseError):
"""Raised when we can't parse a package name from a string."""
def __init__(self, spec):
super(UndetectableNameError, self).__init__(
"Couldn't parse package name in: " + spec)
def parse_version_string_with_indices(spec):
"""Try to extract a version string from a filename or URL. This is taken
largely from Homebrew's Version class."""
if os.path.isdir(spec):
stem = os.path.basename(spec)
elif re.search(r'((?:sourceforge.net|sf.net)/.*)/download$', spec):
stem = spack.utils.stem(os.path.dirname(spec))
else:
stem = spack.utils.stem(spec)
version_types = [
# GitHub tarballs, e.g. v1.2.3
(r'github.com/.+/(?:zip|tar)ball/v?((\d+\.)+\d+)$', spec),
# e.g. https://github.com/sam-github/libnet/tarball/libnet-1.1.4
(r'github.com/.+/(?:zip|tar)ball/.*-((\d+\.)+\d+)$', spec),
# e.g. https://github.com/isaacs/npm/tarball/v0.2.5-1
(r'github.com/.+/(?:zip|tar)ball/v?((\d+\.)+\d+-(\d+))$', spec),
# e.g. https://github.com/petdance/ack/tarball/1.93_02
(r'github.com/.+/(?:zip|tar)ball/v?((\d+\.)+\d+_(\d+))$', spec),
# e.g. https://github.com/erlang/otp/tarball/OTP_R15B01 (erlang style)
(r'[-_](R\d+[AB]\d*(-\d+)?)', spec),
# e.g. boost_1_39_0
(r'((\d+_)+\d+)$', stem),
# e.g. foobar-4.5.1-1
# e.g. ruby-1.9.1-p243
(r'-((\d+\.)*\d\.\d+-(p|rc|RC)?\d+)(?:[-._](?:bin|dist|stable|src|sources))?$', stem),
# e.g. lame-398-1
(r'-((\d)+-\d)', stem),
# e.g. foobar-4.5.1
(r'-((\d+\.)*\d+)$', stem),
# e.g. foobar-4.5.1b
(r'-((\d+\.)*\d+([a-z]|rc|RC)\d*)$', stem),
# e.g. foobar-4.5.0-beta1, or foobar-4.50-beta
(r'-((\d+\.)*\d+-beta(\d+)?)$', stem),
# e.g. foobar4.5.1
(r'((\d+\.)*\d+)$', stem),
# e.g. foobar-4.5.0-bin
(r'-((\d+\.)+\d+[a-z]?)[-._](bin|dist|stable|src|sources?)$', stem),
# e.g. dash_0.5.5.1.orig.tar.gz (Debian style)
(r'_((\d+\.)+\d+[a-z]?)[.]orig$', stem),
# e.g. http://www.openssl.org/source/openssl-0.9.8s.tar.gz
(r'-([^-]+)', stem),
# e.g. astyle_1.23_macosx.tar.gz
(r'_([^_]+)', stem),
# e.g. http://mirrors.jenkins-ci.org/war/1.486/jenkins.war
(r'\/(\d\.\d+)\/', spec),
# e.g. http://www.ijg.org/files/jpegsrc.v8d.tar.gz
(r'\.v(\d+[a-z]?)', stem)]
for vtype in version_types:
regex, match_string = vtype[:2]
match = re.search(regex, match_string)
if match and match.group(1) is not None:
return match.group(1), match.start(1), match.end(1)
raise UndetectableVersionError(spec)
def parse_version(spec):
"""Given a URL or archive name, extract a version from it and return
a version object.
"""
ver, start, end = parse_version_string_with_indices(spec)
return Version(ver)
def parse_name(spec, ver=None):
if ver is None:
ver = parse_version(spec)
ntypes = (r'/sourceforge/([^/]+)/',
r'/([^/]+)/(tarball|zipball)/',
r'/([^/]+)[_.-](bin|dist|stable|src|sources)[_.-]%s' % ver,
r'/([^/]+)[_.-]v?%s' % ver,
r'/([^/]+)%s' % ver,
r'^([^/]+)[_.-]v?%s' % ver,
r'^([^/]+)%s' % ver)
for nt in ntypes:
match = re.search(nt, spec)
if match:
return match.group(1)
raise UndetectableNameError(spec)
def parse_name_and_version(spec):
ver = parse_version(spec)
name = parse_name(spec, ver)
return (name, ver)
def create_version_format(spec):
"""Given a URL or archive name, find the version and create a format string
that will allow another version to be substituted.
"""
ver, start, end = parse_version_string_with_indices(spec)
return spec[:start] + '%s' + spec[end:]

View File

@ -3,7 +3,10 @@
from functools import total_ordering from functools import total_ordering
import utils import utils
import spack.error as serr import spack.error
# Valid version characters
VALID_VERSION = r'[A-Za-z0-9_.-]'
def int_if_int(string): def int_if_int(string):
@ -26,28 +29,37 @@ def ver(string):
@total_ordering @total_ordering
class Version(object): class Version(object):
"""Class to represent versions""" """Class to represent versions"""
def __init__(self, version_string): def __init__(self, string):
if not re.match(VALID_VERSION, string):
raise ValueError("Bad characters in version string: %s" % string)
# preserve the original string # preserve the original string
self.version_string = version_string self.string = string
# Split version into alphabetical and numeric segments # Split version into alphabetical and numeric segments
segments = re.findall(r'[a-zA-Z]+|[0-9]+', version_string) segment_regex = r'[a-zA-Z]+|[0-9]+'
segments = re.findall(segment_regex, string)
self.version = tuple(int_if_int(seg) for seg in segments) self.version = tuple(int_if_int(seg) for seg in segments)
def up_to(self, index): def up_to(self, index):
"""Return a version string up to the specified component, exclusive. """Return a version string up to the specified component, exclusive.
e.g., if this is 10.8.2, self.up_to(2) will return '10.8'. e.g., if this is 10.8.2, self.up_to(2) will return '10.8'.
""" """
return '.'.join(str(x) for x in self[:index]) return '.'.join(str(x) for x in self[:index])
def __iter__(self):
for v in self.version:
yield v
def __getitem__(self, idx): def __getitem__(self, idx):
return tuple(self.version[idx]) return tuple(self.version[idx])
def __repr__(self): def __repr__(self):
return self.version_string return self.string
def __str__(self): def __str__(self):
return self.version_string return self.string
def __lt__(self, other): def __lt__(self, other):
"""Version comparison is designed for consistency with the way RPM """Version comparison is designed for consistency with the way RPM
@ -145,144 +157,3 @@ def __str__(self):
if self.end: if self.end:
out += str(self.end) out += str(self.end)
return out return out
class VersionParseError(serr.SpackError):
"""Raised when the version module can't parse something."""
def __init__(self, msg, spec):
super(VersionParseError, self).__init__(msg)
self.spec = spec
class UndetectableVersionError(VersionParseError):
"""Raised when we can't parse a version from a string."""
def __init__(self, spec):
super(UndetectableVersionError, self).__init__(
"Couldn't detect version in: " + spec, spec)
class UndetectableNameError(VersionParseError):
"""Raised when we can't parse a package name from a string."""
def __init__(self, spec):
super(UndetectableNameError, self).__init__(
"Couldn't parse package name in: " + spec)
def parse_version_string_with_indices(spec):
"""Try to extract a version string from a filename or URL. This is taken
largely from Homebrew's Version class."""
if os.path.isdir(spec):
stem = os.path.basename(spec)
elif re.search(r'((?:sourceforge.net|sf.net)/.*)/download$', spec):
stem = utils.stem(os.path.dirname(spec))
else:
stem = utils.stem(spec)
version_types = [
# GitHub tarballs, e.g. v1.2.3
(r'github.com/.+/(?:zip|tar)ball/v?((\d+\.)+\d+)$', spec),
# e.g. https://github.com/sam-github/libnet/tarball/libnet-1.1.4
(r'github.com/.+/(?:zip|tar)ball/.*-((\d+\.)+\d+)$', spec),
# e.g. https://github.com/isaacs/npm/tarball/v0.2.5-1
(r'github.com/.+/(?:zip|tar)ball/v?((\d+\.)+\d+-(\d+))$', spec),
# e.g. https://github.com/petdance/ack/tarball/1.93_02
(r'github.com/.+/(?:zip|tar)ball/v?((\d+\.)+\d+_(\d+))$', spec),
# e.g. https://github.com/erlang/otp/tarball/OTP_R15B01 (erlang style)
(r'[-_](R\d+[AB]\d*(-\d+)?)', spec),
# e.g. boost_1_39_0
(r'((\d+_)+\d+)$', stem),
# e.g. foobar-4.5.1-1
# e.g. ruby-1.9.1-p243
(r'-((\d+\.)*\d\.\d+-(p|rc|RC)?\d+)(?:[-._](?:bin|dist|stable|src|sources))?$', stem),
# e.g. lame-398-1
(r'-((\d)+-\d)', stem),
# e.g. foobar-4.5.1
(r'-((\d+\.)*\d+)$', stem),
# e.g. foobar-4.5.1b
(r'-((\d+\.)*\d+([a-z]|rc|RC)\d*)$', stem),
# e.g. foobar-4.5.0-beta1, or foobar-4.50-beta
(r'-((\d+\.)*\d+-beta(\d+)?)$', stem),
# e.g. foobar4.5.1
(r'((\d+\.)*\d+)$', stem),
# e.g. foobar-4.5.0-bin
(r'-((\d+\.)+\d+[a-z]?)[-._](bin|dist|stable|src|sources?)$', stem),
# e.g. dash_0.5.5.1.orig.tar.gz (Debian style)
(r'_((\d+\.)+\d+[a-z]?)[.]orig$', stem),
# e.g. http://www.openssl.org/source/openssl-0.9.8s.tar.gz
(r'-([^-]+)', stem),
# e.g. astyle_1.23_macosx.tar.gz
(r'_([^_]+)', stem),
# e.g. http://mirrors.jenkins-ci.org/war/1.486/jenkins.war
(r'\/(\d\.\d+)\/', spec),
# e.g. http://www.ijg.org/files/jpegsrc.v8d.tar.gz
(r'\.v(\d+[a-z]?)', stem)]
for vtype in version_types:
regex, match_string = vtype[:2]
match = re.search(regex, match_string)
if match and match.group(1) is not None:
return match.group(1), match.start(1), match.end(1)
raise UndetectableVersionError(spec)
def parse_version(spec):
"""Given a URL or archive name, extract a version from it and return
a version object.
"""
ver, start, end = parse_version_string_with_indices(spec)
return Version(ver)
def create_version_format(spec):
"""Given a URL or archive name, find the version and create a format string
that will allow another version to be substituted.
"""
ver, start, end = parse_version_string_with_indices(spec)
return spec[:start] + '%s' + spec[end:]
def replace_version(spec, new_version):
version = create_version_format(spec)
# TODO: finish this function.
def parse_name(spec, ver=None):
if ver is None:
ver = parse_version(spec)
ntypes = (r'/sourceforge/([^/]+)/',
r'/([^/]+)/(tarball|zipball)/',
r'/([^/]+)[_.-](bin|dist|stable|src|sources)[_.-]%s' % ver,
r'/([^/]+)[_.-]v?%s' % ver,
r'/([^/]+)%s' % ver,
r'^([^/]+)[_.-]v?%s' % ver,
r'^([^/]+)%s' % ver)
for nt in ntypes:
match = re.search(nt, spec)
if match:
return match.group(1)
raise UndetectableNameError(spec)
def parse(spec):
ver = parse_version(spec)
name = parse_name(spec, ver)
return (name, ver)