From b2a5fef6ad11c9f975d3ddb4a6e34b03bb8051f5 Mon Sep 17 00:00:00 2001 From: Todd Gamblin Date: Sun, 12 May 2013 14:17:38 -0700 Subject: Commands take specs as input instead of names. modified clean, create, fetch, install, and uninstall --- lib/spack/spack/cmd/__init__.py | 27 ++++- lib/spack/spack/cmd/clean.py | 14 ++- lib/spack/spack/cmd/create.py | 4 +- lib/spack/spack/cmd/fetch.py | 12 ++- lib/spack/spack/cmd/install.py | 15 ++- lib/spack/spack/cmd/uninstall.py | 11 +- lib/spack/spack/dependency.py | 2 +- lib/spack/spack/package.py | 4 +- lib/spack/spack/parse.py | 7 +- lib/spack/spack/relations.py | 1 + lib/spack/spack/stage.py | 3 + lib/spack/spack/test/compare_versions.py | 140 -------------------------- lib/spack/spack/test/url_parse.py | 9 +- lib/spack/spack/test/versions.py | 141 ++++++++++++++++++++++++++ lib/spack/spack/url.py | 166 +++++++++++++++++++++++++++++++ lib/spack/spack/version.py | 165 ++++-------------------------- 16 files changed, 403 insertions(+), 318 deletions(-) delete mode 100644 lib/spack/spack/test/compare_versions.py create mode 100644 lib/spack/spack/test/versions.py create mode 100644 lib/spack/spack/url.py (limited to 'lib') diff --git a/lib/spack/spack/cmd/__init__.py b/lib/spack/spack/cmd/__init__.py index a175ce7cfb..9b034b2a60 100644 --- a/lib/spack/spack/cmd/__init__.py +++ b/lib/spack/spack/cmd/__init__.py @@ -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 @@ for file in os.listdir(command_path): 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) diff --git a/lib/spack/spack/cmd/clean.py b/lib/spack/spack/cmd/clean.py index 33f1d68a46..d827768e6e 100644 --- a/lib/spack/spack/cmd/clean.py +++ b/lib/spack/spack/cmd/clean.py @@ -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 @@ import spack.stage as stage 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: diff --git a/lib/spack/spack/cmd/create.py b/lib/spack/spack/cmd/create.py index 36b6d0e571..e0d087bb9d 100644 --- a/lib/spack/spack/cmd/create.py +++ b/lib/spack/spack/cmd/create.py @@ -4,7 +4,7 @@ import os 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: diff --git a/lib/spack/spack/cmd/fetch.py b/lib/spack/spack/cmd/fetch.py index df5173fdaa..2c8098b673 100644 --- a/lib/spack/spack/cmd/fetch.py +++ b/lib/spack/spack/cmd/fetch.py @@ -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() diff --git a/lib/spack/spack/cmd/install.py b/lib/spack/spack/cmd/install.py index 766be9a3ea..1af74e0f60 100644 --- a/lib/spack/spack/cmd/install.py +++ b/lib/spack/spack/cmd/install.py @@ -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() diff --git a/lib/spack/spack/cmd/uninstall.py b/lib/spack/spack/cmd/uninstall.py index 2cc5aeb2c8..e76d91f123 100644 --- a/lib/spack/spack/cmd/uninstall.py +++ b/lib/spack/spack/cmd/uninstall.py @@ -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 diff --git a/lib/spack/spack/dependency.py b/lib/spack/spack/dependency.py index ef87de7ce1..7b8517b035 100644 --- a/lib/spack/spack/dependency.py +++ b/lib/spack/spack/dependency.py @@ -9,7 +9,7 @@ import packages class Dependency(object): """Represents a dependency from one package to another. """ - def __init__(self, name, version): + def __init__(self, name): self.name = name @property diff --git a/lib/spack/spack/package.py b/lib/spack/spack/package.py index 13da76c934..81b64ccc4e 100644 --- a/lib/spack/spack/package.py +++ b/lib/spack/spack/package.py @@ -22,7 +22,7 @@ import packages import tty import attr import validate -import version +import url import arch from multi_function import platform @@ -261,7 +261,7 @@ class Package(object): 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) diff --git a/lib/spack/spack/parse.py b/lib/spack/spack/parse.py index 1d10bda6af..e34f833bf6 100644 --- a/lib/spack/spack/parse.py +++ b/lib/spack/spack/parse.py @@ -1,5 +1,6 @@ import re import spack.error as err +import spack.tty as tty import itertools @@ -11,9 +12,7 @@ class ParseError(err.SpackError): 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 @@ class Parser(object): 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): diff --git a/lib/spack/spack/relations.py b/lib/spack/spack/relations.py index 052a509cb7..0442314e5e 100644 --- a/lib/spack/spack/relations.py +++ b/lib/spack/spack/relations.py @@ -44,6 +44,7 @@ provides spack install mpileaks ^mvapich spack install mpileaks ^mpich """ +import sys from dependency import Dependency diff --git a/lib/spack/spack/stage.py b/lib/spack/spack/stage.py index c8ffa915bc..54d5eb7e6a 100644 --- a/lib/spack/spack/stage.py +++ b/lib/spack/spack/stage.py @@ -126,6 +126,9 @@ class Stage(object): """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): diff --git a/lib/spack/spack/test/compare_versions.py b/lib/spack/spack/test/compare_versions.py deleted file mode 100644 index 5513da6915..0000000000 --- a/lib/spack/spack/test/compare_versions.py +++ /dev/null @@ -1,140 +0,0 @@ -""" -These version tests were taken from the RPM source code. -We try to maintain compatibility with RPM's version semantics -where it makes sense. -""" -import unittest -from spack.version import * - - -class CompareVersionsTest(unittest.TestCase): - - def assert_ver_lt(self, a, b): - a, b = ver(a), ver(b) - self.assertTrue(a < b) - self.assertTrue(a <= b) - self.assertTrue(a != b) - self.assertFalse(a == b) - self.assertFalse(a > b) - self.assertFalse(a >= b) - - - def assert_ver_gt(self, a, b): - a, b = ver(a), ver(b) - self.assertTrue(a > b) - self.assertTrue(a >= b) - self.assertTrue(a != b) - self.assertFalse(a == b) - self.assertFalse(a < b) - self.assertFalse(a <= b) - - - def assert_ver_eq(self, a, b): - a, b = ver(a), ver(b) - self.assertFalse(a > b) - self.assertTrue(a >= b) - self.assertFalse(a != b) - self.assertTrue(a == b) - self.assertFalse(a < b) - self.assertTrue(a <= b) - - - def test_two_segments(self): - self.assert_ver_eq('1.0', '1.0') - self.assert_ver_lt('1.0', '2.0') - self.assert_ver_gt('2.0', '1.0') - - - def test_three_segments(self): - self.assert_ver_eq('2.0.1', '2.0.1') - self.assert_ver_lt('2.0', '2.0.1') - self.assert_ver_gt('2.0.1', '2.0') - - def test_alpha(self): - # TODO: not sure whether I like this. 2.0.1a is *usually* - # TODO: less than 2.0.1, but special-casing it makes version - # TODO: comparison complicated. See version.py - self.assert_ver_eq('2.0.1a', '2.0.1a') - self.assert_ver_gt('2.0.1a', '2.0.1') - self.assert_ver_lt('2.0.1', '2.0.1a') - - def test_patch(self): - self.assert_ver_eq('5.5p1', '5.5p1') - self.assert_ver_lt('5.5p1', '5.5p2') - self.assert_ver_gt('5.5p2', '5.5p1') - self.assert_ver_eq('5.5p10', '5.5p10') - self.assert_ver_lt('5.5p1', '5.5p10') - self.assert_ver_gt('5.5p10', '5.5p1') - - def test_num_alpha_with_no_separator(self): - self.assert_ver_lt('10xyz', '10.1xyz') - self.assert_ver_gt('10.1xyz', '10xyz') - self.assert_ver_eq('xyz10', 'xyz10') - self.assert_ver_lt('xyz10', 'xyz10.1') - self.assert_ver_gt('xyz10.1', 'xyz10') - - def test_alpha_with_dots(self): - self.assert_ver_eq('xyz.4', 'xyz.4') - self.assert_ver_lt('xyz.4', '8') - self.assert_ver_gt('8', 'xyz.4') - self.assert_ver_lt('xyz.4', '2') - self.assert_ver_gt('2', 'xyz.4') - - def test_nums_and_patch(self): - self.assert_ver_lt('5.5p2', '5.6p1') - self.assert_ver_gt('5.6p1', '5.5p2') - self.assert_ver_lt('5.6p1', '6.5p1') - self.assert_ver_gt('6.5p1', '5.6p1') - - def test_rc_versions(self): - self.assert_ver_gt('6.0.rc1', '6.0') - self.assert_ver_lt('6.0', '6.0.rc1') - - def test_alpha_beta(self): - self.assert_ver_gt('10b2', '10a1') - self.assert_ver_lt('10a2', '10b2') - - def test_double_alpha(self): - self.assert_ver_eq('1.0aa', '1.0aa') - self.assert_ver_lt('1.0a', '1.0aa') - self.assert_ver_gt('1.0aa', '1.0a') - - def test_padded_numbers(self): - self.assert_ver_eq('10.0001', '10.0001') - self.assert_ver_eq('10.0001', '10.1') - self.assert_ver_eq('10.1', '10.0001') - self.assert_ver_lt('10.0001', '10.0039') - self.assert_ver_gt('10.0039', '10.0001') - - def test_close_numbers(self): - self.assert_ver_lt('4.999.9', '5.0') - self.assert_ver_gt('5.0', '4.999.9') - - def test_date_stamps(self): - self.assert_ver_eq('20101121', '20101121') - self.assert_ver_lt('20101121', '20101122') - self.assert_ver_gt('20101122', '20101121') - - def test_underscores(self): - self.assert_ver_eq('2_0', '2_0') - self.assert_ver_eq('2.0', '2_0') - self.assert_ver_eq('2_0', '2.0') - - def test_rpm_oddities(self): - self.assert_ver_eq('1b.fc17', '1b.fc17') - self.assert_ver_lt('1b.fc17', '1.fc17') - self.assert_ver_gt('1.fc17', '1b.fc17') - self.assert_ver_eq('1g.fc17', '1g.fc17') - self.assert_ver_gt('1g.fc17', '1.fc17') - self.assert_ver_lt('1.fc17', '1g.fc17') - - - # Stuff below here is not taken from RPM's tests. - 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') - self.assert_ver_eq('1.2:1.4', '1.2:1.4') - self.assertNotEqual(ver('1.2:1.4'), ver('1.2:1.6')) - - self.assert_ver_lt('1.2:1.4', '1.5:1.6') - self.assert_ver_gt('1.5:1.6', '1.2:1.4') diff --git a/lib/spack/spack/test/url_parse.py b/lib/spack/spack/test/url_parse.py index 73fe5796ed..819e6fefd2 100644 --- a/lib/spack/spack/test/url_parse.py +++ b/lib/spack/spack/test/url_parse.py @@ -3,18 +3,19 @@ This file has a bunch of versions tests taken from the excellent version 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( diff --git a/lib/spack/spack/test/versions.py b/lib/spack/spack/test/versions.py new file mode 100644 index 0000000000..96666b36db --- /dev/null +++ b/lib/spack/spack/test/versions.py @@ -0,0 +1,141 @@ +""" +These version tests were taken from the RPM source code. +We try to maintain compatibility with RPM's version semantics +where it makes sense. +""" +import unittest +from spack.version import * + + +class VersionsTest(unittest.TestCase): + + def assert_ver_lt(self, a, b): + a, b = ver(a), ver(b) + self.assertTrue(a < b) + self.assertTrue(a <= b) + self.assertTrue(a != b) + self.assertFalse(a == b) + self.assertFalse(a > b) + self.assertFalse(a >= b) + + + def assert_ver_gt(self, a, b): + a, b = ver(a), ver(b) + self.assertTrue(a > b) + self.assertTrue(a >= b) + self.assertTrue(a != b) + self.assertFalse(a == b) + self.assertFalse(a < b) + self.assertFalse(a <= b) + + + def assert_ver_eq(self, a, b): + a, b = ver(a), ver(b) + self.assertFalse(a > b) + self.assertTrue(a >= b) + self.assertFalse(a != b) + self.assertTrue(a == b) + self.assertFalse(a < b) + self.assertTrue(a <= b) + + + def test_two_segments(self): + self.assert_ver_eq('1.0', '1.0') + self.assert_ver_lt('1.0', '2.0') + self.assert_ver_gt('2.0', '1.0') + + + def test_three_segments(self): + self.assert_ver_eq('2.0.1', '2.0.1') + self.assert_ver_lt('2.0', '2.0.1') + self.assert_ver_gt('2.0.1', '2.0') + + def test_alpha(self): + # TODO: not sure whether I like this. 2.0.1a is *usually* + # TODO: less than 2.0.1, but special-casing it makes version + # TODO: comparison complicated. See version.py + self.assert_ver_eq('2.0.1a', '2.0.1a') + self.assert_ver_gt('2.0.1a', '2.0.1') + self.assert_ver_lt('2.0.1', '2.0.1a') + + def test_patch(self): + self.assert_ver_eq('5.5p1', '5.5p1') + self.assert_ver_lt('5.5p1', '5.5p2') + self.assert_ver_gt('5.5p2', '5.5p1') + self.assert_ver_eq('5.5p10', '5.5p10') + self.assert_ver_lt('5.5p1', '5.5p10') + self.assert_ver_gt('5.5p10', '5.5p1') + + def test_num_alpha_with_no_separator(self): + self.assert_ver_lt('10xyz', '10.1xyz') + self.assert_ver_gt('10.1xyz', '10xyz') + self.assert_ver_eq('xyz10', 'xyz10') + self.assert_ver_lt('xyz10', 'xyz10.1') + self.assert_ver_gt('xyz10.1', 'xyz10') + + def test_alpha_with_dots(self): + self.assert_ver_eq('xyz.4', 'xyz.4') + self.assert_ver_lt('xyz.4', '8') + self.assert_ver_gt('8', 'xyz.4') + self.assert_ver_lt('xyz.4', '2') + self.assert_ver_gt('2', 'xyz.4') + + def test_nums_and_patch(self): + self.assert_ver_lt('5.5p2', '5.6p1') + self.assert_ver_gt('5.6p1', '5.5p2') + self.assert_ver_lt('5.6p1', '6.5p1') + self.assert_ver_gt('6.5p1', '5.6p1') + + def test_rc_versions(self): + self.assert_ver_gt('6.0.rc1', '6.0') + self.assert_ver_lt('6.0', '6.0.rc1') + + def test_alpha_beta(self): + self.assert_ver_gt('10b2', '10a1') + self.assert_ver_lt('10a2', '10b2') + + def test_double_alpha(self): + self.assert_ver_eq('1.0aa', '1.0aa') + self.assert_ver_lt('1.0a', '1.0aa') + self.assert_ver_gt('1.0aa', '1.0a') + + def test_padded_numbers(self): + self.assert_ver_eq('10.0001', '10.0001') + self.assert_ver_eq('10.0001', '10.1') + self.assert_ver_eq('10.1', '10.0001') + self.assert_ver_lt('10.0001', '10.0039') + self.assert_ver_gt('10.0039', '10.0001') + + def test_close_numbers(self): + self.assert_ver_lt('4.999.9', '5.0') + self.assert_ver_gt('5.0', '4.999.9') + + def test_date_stamps(self): + self.assert_ver_eq('20101121', '20101121') + self.assert_ver_lt('20101121', '20101122') + self.assert_ver_gt('20101122', '20101121') + + def test_underscores(self): + self.assert_ver_eq('2_0', '2_0') + self.assert_ver_eq('2.0', '2_0') + self.assert_ver_eq('2_0', '2.0') + + def test_rpm_oddities(self): + self.assert_ver_eq('1b.fc17', '1b.fc17') + self.assert_ver_lt('1b.fc17', '1.fc17') + self.assert_ver_gt('1.fc17', '1b.fc17') + self.assert_ver_eq('1g.fc17', '1g.fc17') + self.assert_ver_gt('1g.fc17', '1.fc17') + self.assert_ver_lt('1.fc17', '1g.fc17') + + + # 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') + self.assert_ver_eq('1.2:1.4', '1.2:1.4') + self.assertNotEqual(ver('1.2:1.4'), ver('1.2:1.6')) + + self.assert_ver_lt('1.2:1.4', '1.5:1.6') + self.assert_ver_gt('1.5:1.6', '1.2:1.4') diff --git a/lib/spack/spack/url.py b/lib/spack/spack/url.py new file mode 100644 index 0000000000..3a6c9a5158 --- /dev/null +++ b/lib/spack/spack/url.py @@ -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:] diff --git a/lib/spack/spack/version.py b/lib/spack/spack/version.py index 9a0390451c..1dfd3718b5 100644 --- a/lib/spack/spack/version.py +++ b/lib/spack/spack/version.py @@ -3,7 +3,10 @@ import re 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 @@ class VersionRange(object): 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) -- cgit v1.2.3-70-g09d2