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 re
import sys
import spack
import spack.spec
import spack.tty as tty
import spack.attr as attr
@ -21,10 +23,6 @@
commands.sort()
def null_op(*args):
pass
def get_cmd_function_name(name):
return name.replace("-", "_")
@ -36,7 +34,7 @@ def get_module(name):
module_name, fromlist=[name, SETUP_PARSER, DESCRIPTION],
level=0)
attr.setdefault(module, SETUP_PARSER, null_op)
attr.setdefault(module, SETUP_PARSER, lambda *args: None) # null-op
attr.setdefault(module, DESCRIPTION, "")
fn_name = get_cmd_function_name(name)
@ -50,3 +48,22 @@ def get_module(name):
def get_command(name):
"""Imports the command's function from a module and returns it."""
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.tty as tty
import spack.stage as stage
@ -5,21 +8,22 @@
description = "Remove staged files for packages"
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',
help="run make clean in the stage directory (default)")
subparser.add_argument('-w', "--work", action="store_true", dest='work',
help="delete and re-expand the entire stage directory")
subparser.add_argument('-d', "--dist", action="store_true", dest='dist',
help="delete the downloaded archive.")
subparser.add_argument('packages', nargs=argparse.REMAINDER, help="specs of packages to clean")
def clean(parser, args):
if not args.names:
tty.die("spack clean requires at least one package name.")
if not args.packages:
tty.die("spack clean requires at least one package argument")
for name in args.names:
package = packages.get(name)
specs = spack.cmd.parse_specs(args.packages)
for spec in specs:
package = packages.get(spec.name)
if args.dist:
package.do_clean_dist()
elif args.work:

View file

@ -4,7 +4,7 @@
import spack
import spack.packages as packages
import spack.tty as tty
import spack.version
import spack.url
from spack.stage import Stage
from contextlib import closing
@ -38,7 +38,7 @@ def create(parser, args):
url = args.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:
print "Couldn't guess a name for this package."
while not name:

View file

@ -1,12 +1,18 @@
import argparse
import spack.cmd
import spack.packages as packages
description = "Fetch archives for packages"
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):
for name in args.names:
package = packages.get(name)
if not args.packages:
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()

View file

@ -1,19 +1,28 @@
import sys
import argparse
import spack
import spack.packages as packages
import spack.cmd
description = "Build and install packages"
def setup_parser(subparser):
subparser.add_argument('names', nargs='+', help="names of packages to install")
subparser.add_argument('-i', '--ignore-dependencies',
action='store_true', dest='ignore_dependencies',
help="Do not try to install dependencies of requested packages.")
subparser.add_argument('-d', '--dirty', action='store_true', dest='dirty',
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):
if not args.packages:
tty.die("install requires at least one package argument")
spack.ignore_dependencies = args.ignore_dependencies
for name in args.names:
package = packages.get(name)
specs = spack.cmd.parse_specs(args.packages)
for spec in specs:
package = packages.get(spec.name)
package.dirty = args.dirty
package.do_install()

View file

@ -1,15 +1,22 @@
import spack.cmd
import spack.packages as packages
import argparse
description="Remove an installed package"
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',
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):
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.
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
# This ensures we do things in the right order

View file

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

View file

@ -22,7 +22,7 @@
import tty
import attr
import validate
import version
import url
import arch
from multi_function import platform
@ -261,7 +261,7 @@ def __init__(self, sys_type = arch.sys_type()):
validate.url(self.url)
# 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:
tty.die("Couldn't extract version from %s. " +
"You must specify it explicitly for this URL." % self.url)

View file

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

View file

@ -44,6 +44,7 @@ class Mpileaks(Package):
spack install mpileaks ^mvapich
spack install mpileaks ^mpich
"""
import sys
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;
None if the archive hasn't been expanded.
"""
if not self.archive_file:
return None
for file in os.listdir(self.path):
archive_path = spack.new_path(self.path, file)
if os.path.isdir(archive_path):

View file

@ -3,18 +3,19 @@
detection in Homebrew.
"""
import unittest
import spack.version as ver
import spack.url as url
from pprint import pprint
class UrlParseTest(unittest.TestCase):
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):
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_v, ver.Version(v))
self.assertEqual(parsed_v, url.Version(v))
def test_wwwoffle_version(self):
self.assert_detected(

View file

@ -7,7 +7,7 @@
from spack.version import *
class CompareVersionsTest(unittest.TestCase):
class VersionsTest(unittest.TestCase):
def assert_ver_lt(self, a, b):
a, b = ver(a), ver(b)
@ -129,7 +129,8 @@ def test_rpm_oddities(self):
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):
self.assert_ver_lt('1.2:1.4', '1.6')
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
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):
@ -26,28 +29,37 @@ def ver(string):
@total_ordering
class Version(object):
"""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
self.version_string = version_string
self.string = string
# 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)
def up_to(self, index):
"""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'.
"""
return '.'.join(str(x) for x in self[:index])
def __iter__(self):
for v in self.version:
yield v
def __getitem__(self, idx):
return tuple(self.version[idx])
def __repr__(self):
return self.version_string
return self.string
def __str__(self):
return self.version_string
return self.string
def __lt__(self, other):
"""Version comparison is designed for consistency with the way RPM
@ -145,144 +157,3 @@ def __str__(self):
if self.end:
out += str(self.end)
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)