From cc76c0f5f9f8021cfb7423a226bd431c00d791ce Mon Sep 17 00:00:00 2001 From: Todd Gamblin Date: Wed, 13 Feb 2013 17:50:44 -0800 Subject: Initial version of spack with one package: cmake --- .gitignore | 4 + bin/spack | 41 ++++++++ lib/spack/spack/Package.py | 168 ++++++++++++++++++++++++++++++ lib/spack/spack/__init__.py | 5 + lib/spack/spack/attr.py | 8 ++ lib/spack/spack/cmd/__init__.py | 36 +++++++ lib/spack/spack/cmd/arch.py | 12 +++ lib/spack/spack/cmd/clean.py | 16 +++ lib/spack/spack/cmd/create.py | 57 ++++++++++ lib/spack/spack/cmd/edit.py | 50 +++++++++ lib/spack/spack/cmd/fetch.py | 9 ++ lib/spack/spack/cmd/install.py | 11 ++ lib/spack/spack/cmd/stage.py | 9 ++ lib/spack/spack/cmd/uninstall.py | 9 ++ lib/spack/spack/fileutils.py | 112 ++++++++++++++++++++ lib/spack/spack/globals.py | 42 ++++++++ lib/spack/spack/packages/__init__.py | 35 +++++++ lib/spack/spack/packages/cmake.py | 11 ++ lib/spack/spack/stage.py | 68 ++++++++++++ lib/spack/spack/test/test_versions.py | 189 ++++++++++++++++++++++++++++++++++ lib/spack/spack/tty.py | 63 ++++++++++++ lib/spack/spack/validate.py | 13 +++ lib/spack/spack/version.py | 126 +++++++++++++++++++++++ 23 files changed, 1094 insertions(+) create mode 100644 .gitignore create mode 100755 bin/spack create mode 100644 lib/spack/spack/Package.py create mode 100644 lib/spack/spack/__init__.py create mode 100644 lib/spack/spack/attr.py create mode 100644 lib/spack/spack/cmd/__init__.py create mode 100644 lib/spack/spack/cmd/arch.py create mode 100644 lib/spack/spack/cmd/clean.py create mode 100644 lib/spack/spack/cmd/create.py create mode 100644 lib/spack/spack/cmd/edit.py create mode 100644 lib/spack/spack/cmd/fetch.py create mode 100644 lib/spack/spack/cmd/install.py create mode 100644 lib/spack/spack/cmd/stage.py create mode 100644 lib/spack/spack/cmd/uninstall.py create mode 100644 lib/spack/spack/fileutils.py create mode 100644 lib/spack/spack/globals.py create mode 100644 lib/spack/spack/packages/__init__.py create mode 100644 lib/spack/spack/packages/cmake.py create mode 100644 lib/spack/spack/stage.py create mode 100755 lib/spack/spack/test/test_versions.py create mode 100644 lib/spack/spack/tty.py create mode 100644 lib/spack/spack/validate.py create mode 100644 lib/spack/spack/version.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..5527bf8f05 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.pyc +/opt/ +/var/ +*~ diff --git a/bin/spack b/bin/spack new file mode 100755 index 0000000000..1fd47d5e8c --- /dev/null +++ b/bin/spack @@ -0,0 +1,41 @@ +#!/usr/bin/env python + +import os +import sys +import argparse + +# Find spack's location and its prefix. +SPACK_FILE = os.environ["SPACK_FILE"] = os.path.expanduser(__file__) +SPACK_PREFIX = os.path.dirname(os.path.dirname(SPACK_FILE)) + +# Allow spack libs to be imported in our scripts +SPACK_LIB_PATH = os.path.join(SPACK_PREFIX, "lib", "spack") +sys.path.insert(0, SPACK_LIB_PATH) + +# clean up the scope and start using spack package instead. +del SPACK_FILE, SPACK_PREFIX, SPACK_LIB_PATH +import spack + +# Command parsing +parser = argparse.ArgumentParser( + description='Spack: the Supercomputing PACKage Manager.') +parser.add_argument('-V', '--version', action='version', version="%s" % spack.version) +parser.add_argument('-v', '--verbose', action='store_true', dest='verbose') + +# each command module implements a parser() function, to which we pass its +# subparser for setup. +subparsers = parser.add_subparsers(title="subcommands", dest="command") + +import spack.cmd +for cmd in spack.cmd.commands: + subparser = subparsers.add_parser(cmd) + module = spack.cmd.get_module(cmd) + module.setup_parser(subparser) +args = parser.parse_args() + +# Set up environment based on args. +spack.verbose = args.verbose + +# Try to load the particular command asked for and run it +command = spack.cmd.get_command(args.command) +command(args) diff --git a/lib/spack/spack/Package.py b/lib/spack/spack/Package.py new file mode 100644 index 0000000000..3fdbbe16f8 --- /dev/null +++ b/lib/spack/spack/Package.py @@ -0,0 +1,168 @@ +import inspect +import os +import re +import subprocess + +from spack import * +import tty +import attr +import validate +import version +import shutil +import platform +from stage import Stage + +def depends_on(*args, **kwargs): + """Adds a depends_on local variable in the locals of + the calling class, based on args. + """ + stack = inspect.stack() + try: + locals = stack[1][0].f_locals + finally: + del stack + print locals + + locals["depends_on"] = kwargs + + +class Package(object): + def __init__(self): + attr.required(self, 'homepage') + attr.required(self, 'url') + attr.required(self, 'md5') + + # Name of package is just the classname lowercased + self.name = self.__class__.__name__.lower() + + # Make sure URL is an allowed type + validate.url(self.url) + + v = version.parse(self.url) + if not v: + tty.die("Couldn't extract version from '%s'. " + + "You must specify it explicitly for this URL." % self.url) + self.version = v + + @property + def stage(self): + return Stage(self.stage_name) + + @property + def stage_name(self): + return "%s-%s" % (self.name, self.version) + + @property + def prefix(self): + return new_path(install_path, self.stage_name) + + def do_fetch(self): + """Creates a stage directory and downloads the taball for this package. + Working directory will be set to the stage directory. + """ + stage = self.stage + stage.setup() + stage.chdir() + + archive_file = os.path.basename(self.url) + if not os.path.exists(archive_file): + tty.msg("Fetching %s" % self.url) + + # Run curl but grab the mime type from the http headers + headers = curl('-#', '-O', '-D', '-', self.url, return_output=True) + + # output this if we somehow got an HTML file rather than the archive we + # asked for. + if re.search(r'Content-Type: text/html', headers): + tty.warn("The contents of '%s' look like HTML. The checksum will "+ + "likely fail. Use 'spack clean %s' to delete this file. " + "The fix the gateway issue and install again." % (archive_file, self.name)) + + if not os.path.exists(archive_file): + tty.die("Failed to download '%s'!" % self.url) + else: + tty.msg("Already downloaded %s." % self.name) + + archive_md5 = md5(archive_file) + if archive_md5 != self.md5: + tty.die("MD5 Checksum failed for %s. Expected %s but got %s." + % (self.name, self.md5, archive_md5)) + + return archive_file + + def do_stage(self): + """Unpacks the fetched tarball, then changes into the expanded tarball directory.""" + archive_file = self.do_fetch() + stage = self.stage + + archive_dir = stage.archive_path + if not archive_dir: + tty.msg("Staging archive: '%s'" % archive_file) + decompress = decompressor_for(archive_file) + decompress(archive_file) + else: + tty.msg("Alredy staged %s" % self.name) + + stage.chdir_to_archive() + + def do_install(self): + """This class should call this version of the install method. + Package implementations should override install(). + """ + if os.path.exists(self.prefix): + tty.msg("%s is already installed." % self.name) + tty.pkg(self.prefix) + return + + self.do_stage() + + # Populate the module scope of install() with some useful functions. + # This makes things easier for package writers. + self.module.configure = which("configure", [self.stage.archive_path]) + self.module.cmake = which("cmake") + + self.install(self.prefix) + tty.msg("Successfully installed %s" % self.name) + tty.pkg(self.prefix) + + @property + def module(self): + """Use this to add variables to the class's module's scope. + This lets us use custom syntax in the install method. + """ + return __import__(self.__class__.__module__, + fromlist=[self.__class__.__name__]) + + def install(self, prefix): + """Package implementations override this with their own build configuration.""" + tty.die("Packages must provide an install method!") + + def do_uninstall(self): + self.uninstall(self.prefix) + tty.msg("Successfully uninstalled %s." % self.name) + + def uninstall(self, prefix): + """By default just blows the install dir away.""" + shutil.rmtree(self.prefix, True) + + def do_clean(self): + self.clean() + + def clean(self): + """By default just runs make clean. Override if this isn't good.""" + stage = self.stage + if stage.archive_path: + stage.chdir_to_archive() + try: + make("clean") + tty.msg("Successfully cleaned %s" % self.name) + except subprocess.CalledProcessError: + # Might not be configured. Ignore. + pass + + def do_clean_all(self): + if os.path.exists(self.stage.path): + self.stage.destroy() + tty.msg("Successfully cleaned %s" % self.name) + + diff --git a/lib/spack/spack/__init__.py b/lib/spack/spack/__init__.py new file mode 100644 index 0000000000..15dc99c866 --- /dev/null +++ b/lib/spack/spack/__init__.py @@ -0,0 +1,5 @@ + +from globals import * +from fileutils import * + +from Package import Package, depends_on diff --git a/lib/spack/spack/attr.py b/lib/spack/spack/attr.py new file mode 100644 index 0000000000..03fc6e1463 --- /dev/null +++ b/lib/spack/spack/attr.py @@ -0,0 +1,8 @@ +import spack.tty as tty + +def required(obj, attr_name): + """Ensure that a class has a required attribute.""" + if not hasattr(obj, attr_name): + tty.die("No required attribute '%s' in class '%s'" + % (attr_name, obj.__class__.__name__)) + diff --git a/lib/spack/spack/cmd/__init__.py b/lib/spack/spack/cmd/__init__.py new file mode 100644 index 0000000000..20c4685e1b --- /dev/null +++ b/lib/spack/spack/cmd/__init__.py @@ -0,0 +1,36 @@ +import os +import re + +import spack +import spack.tty as tty + +SETUP_PARSER = "setup_parser" +command_path = os.path.join(spack.lib_path, "spack", "cmd") + +commands = [] +for file in os.listdir(command_path): + if file.endswith(".py") and not file == "__init__.py": + cmd = re.sub(r'.py$', '', file) + commands.append(cmd) +commands.sort() + +def null_op(*args): + pass + + +def get_module(name): + """Imports the module for a particular command name and returns it.""" + module_name = "%s.%s" % (__name__, name) + module = __import__(module_name, fromlist=[name, SETUP_PARSER], level=0) + module.setup_parser = getattr(module, SETUP_PARSER, null_op) + + if not hasattr(module, name): + tty.die("Command module %s (%s) must define function '%s'." + % (module.__name__, module.__file__, name)) + + return module + + +def get_command(name): + """Imports the command's function from a module and returns it.""" + return getattr(get_module(name), name) diff --git a/lib/spack/spack/cmd/arch.py b/lib/spack/spack/cmd/arch.py new file mode 100644 index 0000000000..fdbed800b1 --- /dev/null +++ b/lib/spack/spack/cmd/arch.py @@ -0,0 +1,12 @@ +from spack import * +import spack.version as version + +import multiprocessing +import platform + +def arch(args): + print multiprocessing.cpu_count() + print platform.mac_ver() + + + print version.canonical(platform.mac_ver()[0]) diff --git a/lib/spack/spack/cmd/clean.py b/lib/spack/spack/cmd/clean.py new file mode 100644 index 0000000000..eecfb011e3 --- /dev/null +++ b/lib/spack/spack/cmd/clean.py @@ -0,0 +1,16 @@ +import spack.packages as packages + +def setup_parser(subparser): + subparser.add_argument('name', help="name of package to clean") + subparser.add_argument('-a', "--all", action="store_true", dest="all", + help="delete the entire stage directory") + +def clean(args): + package_class = packages.get(args.name) + package = package_class() + if args.all: + package.do_clean_all() + else: + package.do_clean() + + diff --git a/lib/spack/spack/cmd/create.py b/lib/spack/spack/cmd/create.py new file mode 100644 index 0000000000..3354831508 --- /dev/null +++ b/lib/spack/spack/cmd/create.py @@ -0,0 +1,57 @@ +import string + +import spack +import spack.packages as packages +import spack.tty as tty +import spack.version + +pacakge_tempate = string.Template("""\ +from spack import * + +class $name(Package): + homepage = "${homepage}" + url = "${url}" + md5 = "${md5}" + + def install(self): + # Insert your installation code here. + pass +""") + +def create_template(name): + class_name = name.capitalize() + return new_pacakge_tempate % class_name + + +def setup_parser(subparser): + subparser.add_argument('url', nargs='?', help="url of package archive") + + +def create(args): + url = args.url + + version = spack.version.parse(url) + if not version: + tty.die("Couldn't figure out a version string from '%s'." % url) + + + + # By default open the directory where packages live. + if not name: + path = spack.packages_path + else: + path = packages.filename_for(name) + + if os.path.exists(path): + if not os.path.isfile(path): + tty.die("Something's wrong. '%s' is not a file!" % path) + if not os.access(path, os.R_OK|os.W_OK): + tty.die("Insufficient permissions on '%s'!" % path) + else: + tty.msg("Editing new file: '%s'." % path) + file = open(path, "w") + file.write(create_template(name)) + file.close() + + # If everything checks out, go ahead and edit. + spack.editor(path) diff --git a/lib/spack/spack/cmd/edit.py b/lib/spack/spack/cmd/edit.py new file mode 100644 index 0000000000..6efcc5487e --- /dev/null +++ b/lib/spack/spack/cmd/edit.py @@ -0,0 +1,50 @@ +import os +import spack +import spack.packages as packages +import spack.tty as tty + +new_pacakge_tempate = """\ +from spack import * + +class %s(Package): + homepage = "https://www.example.com" + url = "https://www.example.com/download/example-1.0.tar.gz" + md5 = "nomd5" + + def install(self): + # Insert your installation code here. + pass + +""" + +def create_template(name): + class_name = name.capitalize() + return new_pacakge_tempate % class_name + + +def setup_parser(subparser): + subparser.add_argument( + 'name', nargs='?', default=None, help="name of package to edit") + +def edit(args): + name = args.name + + # By default open the directory where packages live. + if not name: + path = spack.packages_path + else: + path = packages.filename_for(name) + + if os.path.exists(path): + if not os.path.isfile(path): + tty.die("Something's wrong. '%s' is not a file!" % path) + if not os.access(path, os.R_OK|os.W_OK): + tty.die("Insufficient permissions on '%s'!" % path) + else: + tty.msg("Editing new file: '%s'." % path) + file = open(path, "w") + file.write(create_template(name)) + file.close() + + # If everything checks out, go ahead and edit. + spack.editor(path) diff --git a/lib/spack/spack/cmd/fetch.py b/lib/spack/spack/cmd/fetch.py new file mode 100644 index 0000000000..8682a76a1b --- /dev/null +++ b/lib/spack/spack/cmd/fetch.py @@ -0,0 +1,9 @@ +import spack.packages as packages + +def setup_parser(subparser): + subparser.add_argument('name', help="name of package to fetch") + +def fetch(args): + package_class = packages.get(args.name) + package = package_class() + package.do_fetch() diff --git a/lib/spack/spack/cmd/install.py b/lib/spack/spack/cmd/install.py new file mode 100644 index 0000000000..f946e49ba3 --- /dev/null +++ b/lib/spack/spack/cmd/install.py @@ -0,0 +1,11 @@ +import spack.packages as packages + +def setup_parser(subparser): + subparser.add_argument('name', help="name of package to install") + +def install(args): + package_class = packages.get(args.name) + package = package_class() + package.do_install() + + diff --git a/lib/spack/spack/cmd/stage.py b/lib/spack/spack/cmd/stage.py new file mode 100644 index 0000000000..da7cb4a636 --- /dev/null +++ b/lib/spack/spack/cmd/stage.py @@ -0,0 +1,9 @@ +import spack.packages as packages + +def setup_parser(subparser): + subparser.add_argument('name', help="name of package to stage") + +def stage(args): + package_class = packages.get(args.name) + package = package_class() + package.do_stage() diff --git a/lib/spack/spack/cmd/uninstall.py b/lib/spack/spack/cmd/uninstall.py new file mode 100644 index 0000000000..d618845e55 --- /dev/null +++ b/lib/spack/spack/cmd/uninstall.py @@ -0,0 +1,9 @@ +import spack.packages as packages + +def setup_parser(subparser): + subparser.add_argument('name', help="name of package to uninstall") + +def uninstall(args): + package_class = packages.get(args.name) + package = package_class() + package.do_uninstall() diff --git a/lib/spack/spack/fileutils.py b/lib/spack/spack/fileutils.py new file mode 100644 index 0000000000..6827e5d1f0 --- /dev/null +++ b/lib/spack/spack/fileutils.py @@ -0,0 +1,112 @@ +import os +import subprocess +import re +from itertools import product +from contextlib import closing + +import tty + +# Supported archvie extensions. +PRE_EXTS = ["tar"] +EXTS = ["gz", "bz2", "xz", "Z", "zip", "tgz"] + +# Add EXTS last so that .tar.gz is matched *before* tar.gz +ALLOWED_ARCHIVE_TYPES = [".".join(l) for l in product(PRE_EXTS, EXTS)] + EXTS + + +def has_whitespace(string): + return re.search(r'\s', string) + + +def new_path(prefix, *args): + path=prefix + for elt in args: + path = os.path.join(path, elt) + + if has_whitespace(path): + tty.die("Invalid path: '%s'. Use a path without whitespace.") + + return path + + +def ancestor(dir, n=1): + """Get the nth ancestor of a directory.""" + parent = os.path.abspath(dir) + for i in range(n): + parent = os.path.dirname(parent) + return parent + + +class Executable(object): + """Class representing a program that can be run on the command line.""" + def __init__(self, name): + self.exe = name.split(' ') + + def add_default_arg(self, arg): + self.exe.append(arg) + + def __call__(self, *args, **kwargs): + """Run the executable with subprocess.check_output, return output.""" + return_output = kwargs.get("return_output", False) + + quoted_args = [arg for arg in args if re.search(r'^"|^\'|"$|\'$', arg)] + if quoted_args: + tty.warn("Quotes in package command arguments can confuse shell scripts like configure.", + "The following arguments may cause problems when executed:", + str("\n".join([" "+arg for arg in quoted_args])), + "Quotes aren't needed because spack doesn't use a shell. Consider removing them") + + cmd = self.exe + list(args) + tty.verbose(cmd) + + if return_output: + return subprocess.check_output(cmd) + else: + return subprocess.check_call(cmd) + + def __repr__(self): + return "" % self.exe + + +def which(name, path=None): + """Finds an executable in the path like command-line which.""" + if not path: + path = os.environ.get('PATH', '').split(os.pathsep) + if not path: + return None + + for dir in path: + exe = os.path.join(dir, name) + if os.access(exe, os.X_OK): + return Executable(exe) + return None + + +def stem(path): + """Get the part of a path that does not include its compressed + type extension.""" + for type in ALLOWED_ARCHIVE_TYPES: + suffix = r'\.%s$' % type + if re.search(suffix, path): + return re.sub(suffix, "", path) + return path + + +def decompressor_for(path): + """Get the appropriate decompressor for a path.""" + if which("tar"): + return Executable("tar -xf") + else: + tty.die("spack requires tar. Make sure it's on your path.") + + +def md5(filename, block_size=2**20): + import hashlib + md5 = hashlib.md5() + with closing(open(filename)) as file: + while True: + data = file.read(block_size) + if not data: + break + md5.update(data) + return md5.hexdigest() diff --git a/lib/spack/spack/globals.py b/lib/spack/spack/globals.py new file mode 100644 index 0000000000..66cc6ce157 --- /dev/null +++ b/lib/spack/spack/globals.py @@ -0,0 +1,42 @@ +import os +import re +import multiprocessing +from version import Version + +import tty +from fileutils import * + +# This lives in $prefix/lib/spac/spack/__file__ +prefix = ancestor(__file__, 4) + +# The spack script itself +spack_file = new_path(prefix, "bin", "spack") + +# spack directory hierarchy +lib_path = new_path(prefix, "lib", "spack") +module_path = new_path(lib_path, "spack") +packages_path = new_path(module_path, "packages") + +var_path = new_path(prefix, "var", "spack") +stage_path = new_path(var_path, "stage") + +install_path = new_path(prefix, "opt") + +# Version information +version = Version("0.1") + +# User's editor from the environment +editor = Executable(os.environ.get("EDITOR", "")) + +# Curl tool for fetching files. +curl = which("curl") +if not curl: + tty.die("spack requires curl. Make sure it is in your path.") + +make = which("make") +make.add_default_arg("-j%d" % multiprocessing.cpu_count()) +if not make: + tty.die("spack requires make. Make sure it is in your path.") + +verbose = False +debug = False diff --git a/lib/spack/spack/packages/__init__.py b/lib/spack/spack/packages/__init__.py new file mode 100644 index 0000000000..2aadfc612f --- /dev/null +++ b/lib/spack/spack/packages/__init__.py @@ -0,0 +1,35 @@ +import spack +from spack.fileutils import * + +import re +import inspect + + +def filename_for(package): + """Get the filename where a package name should be stored.""" + return new_path(spack.packages_path, "%s.py" % package.lower()) + + +def get(name): + file = filename_for(name) + + if os.path.exists(file): + if not os.path.isfile(file): + tty.die("Something's wrong. '%s' is not a file!" % file) + if not os.access(file, os.R_OK): + tty.die("Cannot read '%s'!" % file) + + class_name = name.capitalize() + try: + module_name = "%s.%s" % (__name__, name) + module = __import__(module_name, fromlist=[class_name]) + except ImportError, e: + tty.die("Error while importing %s.%s:\n%s" % (name, class_name, e.message)) + + klass = getattr(module, class_name) + if not inspect.isclass(klass): + tty.die("%s.%s is not a class" % (name, class_name)) + + return klass + + diff --git a/lib/spack/spack/packages/cmake.py b/lib/spack/spack/packages/cmake.py new file mode 100644 index 0000000000..57cb0d9cf9 --- /dev/null +++ b/lib/spack/spack/packages/cmake.py @@ -0,0 +1,11 @@ +from spack import * + +class Cmake(Package): + homepage = 'https://www.cmake.org' + url = 'http://www.cmake.org/files/v2.8/cmake-2.8.10.2.tar.gz' + md5 = '097278785da7182ec0aea8769d06860c' + + def install(self, prefix): + configure('--prefix=%s' % prefix) + make() + make('install') diff --git a/lib/spack/spack/stage.py b/lib/spack/spack/stage.py new file mode 100644 index 0000000000..8585c75976 --- /dev/null +++ b/lib/spack/spack/stage.py @@ -0,0 +1,68 @@ +import os +import shutil + +import spack +import tty + + +def ensure_access(dir=spack.stage_path): + if not os.access(dir, os.R_OK|os.W_OK): + tty.die("Insufficient permissions on directory '%s'" % dir) + + +class Stage(object): + def __init__(self, stage_name): + self.stage_name = stage_name + + @property + def path(self): + return spack.new_path(spack.stage_path, self.stage_name) + + + def setup(self): + if os.path.exists(self.path): + if not os.path.isdir(self.path): + tty.die("Stage path '%s' is not a directory!" % self.path) + else: + os.makedirs(self.path) + + ensure_access(self.path) + + + @property + def archive_path(self): + """"Returns the path to the expanded archive directory if it's expanded; + None if the archive hasn't been expanded. + """ + for file in os.listdir(self.path): + archive_path = spack.new_path(self.path, file) + if os.path.isdir(archive_path): + return archive_path + return None + + + def chdir(self): + """Changes directory to the stage path. Or dies if it is not set up.""" + if os.path.isdir(self.path): + os.chdir(self.path) + else: + tty.die("Attempt to chdir to stage before setup.") + + + def chdir_to_archive(self): + """Changes directory to the expanded archive directory if it exists. + Dies with an error otherwise. + """ + path = self.archive_path + if not path: + tty.die("Attempt to chdir before expanding archive.") + else: + os.chdir(path) + if not os.listdir(path): + tty.die("Archive was empty for '%s'" % self.name) + + + def destroy(self): + """Blows away the stage directory. Can always call setup() again.""" + if os.path.exists(self.path): + shutil.rmtree(self.path, True) diff --git a/lib/spack/spack/test/test_versions.py b/lib/spack/spack/test/test_versions.py new file mode 100755 index 0000000000..e7ebae630b --- /dev/null +++ b/lib/spack/spack/test/test_versions.py @@ -0,0 +1,189 @@ +#!/usr/bin/env python +"""\ +This file has a bunch of versions tests taken from the excellent version +detection in Homebrew. +""" +import spack.version as version +import unittest + + +class VersionTest(unittest.TestCase): + + def assert_not_detected(self, string): + self.assertIsNone(version.parse(string)) + + def assert_detected(self, v, string): + self.assertEqual(v, version.parse(string)) + + def test_wwwoffle_version(self): + self.assert_detected( + '2.9h', 'http://www.gedanken.demon.co.uk/download-wwwoffle/wwwoffle-2.9h.tgz') + + def test_version_sourceforge_download(self): + self.assert_detected( + '1.21', 'http://sourceforge.net/foo_bar-1.21.tar.gz/download') + self.assert_detected( + '1.21', 'http://sf.net/foo_bar-1.21.tar.gz/download') + + def test_no_version(self): + self.assert_not_detected('http://example.com/blah.tar') + self.assert_not_detected('foo') + + def test_version_all_dots(self): + self.assert_detected( + '1.14','http://example.com/foo.bar.la.1.14.zip') + + def test_version_underscore_separator(self): + self.assert_detected( + '1.1', 'http://example.com/grc_1.1.tar.gz') + + def test_boost_version_style(self): + self.assert_detected( + '1.39.0', 'http://example.com/boost_1_39_0.tar.bz2') + + def test_erlang_version_style(self): + self.assert_detected( + 'R13B', 'http://erlang.org/download/otp_src_R13B.tar.gz') + + def test_another_erlang_version_style(self): + self.assert_detected( + 'R15B01', 'https://github.com/erlang/otp/tarball/OTP_R15B01') + + def test_yet_another_erlang_version_style(self): + self.assert_detected( + 'R15B03-1', 'https://github.com/erlang/otp/tarball/OTP_R15B03-1') + + def test_p7zip_version_style(self): + self.assert_detected( + '9.04', + 'http://kent.dl.sourceforge.net/sourceforge/p7zip/p7zip_9.04_src_all.tar.bz2') + + def test_new_github_style(self): + self.assert_detected( + '1.1.4', 'https://github.com/sam-github/libnet/tarball/libnet-1.1.4') + + def test_gloox_beta_style(self): + self.assert_detected( + '1.0-beta7', 'http://camaya.net/download/gloox-1.0-beta7.tar.bz2') + + def test_sphinx_beta_style(self): + self.assert_detected( + '1.10-beta', 'http://sphinxsearch.com/downloads/sphinx-1.10-beta.tar.gz') + + def test_astyle_verson_style(self): + self.assert_detected( + '1.23', 'http://kent.dl.sourceforge.net/sourceforge/astyle/astyle_1.23_macosx.tar.gz') + + def test_version_dos2unix(self): + self.assert_detected( + '3.1', 'http://www.sfr-fresh.com/linux/misc/dos2unix-3.1.tar.gz') + + def test_version_internal_dash(self): + self.assert_detected( + '1.1-2', 'http://example.com/foo-arse-1.1-2.tar.gz') + + def test_version_single_digit(self): + self.assert_detected( + '45', 'http://example.com/foo_bar.45.tar.gz') + + def test_noseparator_single_digit(self): + self.assert_detected( + '45', 'http://example.com/foo_bar45.tar.gz') + + def test_version_developer_that_hates_us_format(self): + self.assert_detected( + '1.2.3', 'http://example.com/foo-bar-la.1.2.3.tar.gz') + + def test_version_regular(self): + self.assert_detected( + '1.21', 'http://example.com/foo_bar-1.21.tar.gz') + + def test_version_github(self): + self.assert_detected( + '1.0.5', 'http://github.com/lloyd/yajl/tarball/1.0.5') + + def test_version_github_with_high_patch_number(self): + self.assert_detected( + '1.2.34', 'http://github.com/lloyd/yajl/tarball/v1.2.34') + + def test_yet_another_version(self): + self.assert_detected( + '0.15.1b', 'http://example.com/mad-0.15.1b.tar.gz') + + def test_lame_version_style(self): + self.assert_detected( + '398-2', 'http://kent.dl.sourceforge.net/sourceforge/lame/lame-398-2.tar.gz') + + def test_ruby_version_style(self): + self.assert_detected( + '1.9.1-p243', 'ftp://ftp.ruby-lang.org/pub/ruby/1.9/ruby-1.9.1-p243.tar.gz') + + def test_omega_version_style(self): + self.assert_detected( + '0.80.2', 'http://www.alcyone.com/binaries/omega/omega-0.80.2-src.tar.gz') + + def test_rc_style(self): + self.assert_detected( + '1.2.2rc1', 'http://downloads.xiph.org/releases/vorbis/libvorbis-1.2.2rc1.tar.bz2') + + def test_dash_rc_style(self): + self.assert_detected( + '1.8.0-rc1', 'http://ftp.mozilla.org/pub/mozilla.org/js/js-1.8.0-rc1.tar.gz') + + def test_angband_version_style(self): + self.assert_detected( + '3.0.9b', 'http://rephial.org/downloads/3.0/angband-3.0.9b-src.tar.gz') + + def test_stable_suffix(self): + self.assert_detected( + '1.4.14b', 'http://www.monkey.org/~provos/libevent-1.4.14b-stable.tar.gz') + + def test_debian_style_1(self): + self.assert_detected( + '3.03', 'http://ftp.de.debian.org/debian/pool/main/s/sl/sl_3.03.orig.tar.gz') + + def test_debian_style_2(self): + self.assert_detected( + '1.01b', 'http://ftp.de.debian.org/debian/pool/main/m/mmv/mmv_1.01b.orig.tar.gz') + + def test_imagemagick_style(self): + self.assert_detected( + '6.7.5-7', 'http://downloads.sf.net/project/machomebrew/mirror/ImageMagick-6.7.5-7.tar.bz2') + + def test_dash_version_dash_style(self): + self.assert_detected( + '3.4', 'http://www.antlr.org/download/antlr-3.4-complete.jar') + + def test_apache_version_style(self): + self.assert_detected( + '1.2.0-rc2', 'http://www.apache.org/dyn/closer.cgi?path=/cassandra/1.2.0/apache-cassandra-1.2.0-rc2-bin.tar.gz') + + def test_jpeg_style(self): + self.assert_detected( + '8d', 'http://www.ijg.org/files/jpegsrc.v8d.tar.gz') + + def test_more_versions(self): + self.assert_detected( + '1.4.1', 'http://pypy.org/download/pypy-1.4.1-osx.tar.bz2') + self.assert_detected( + '0.9.8s', 'http://www.openssl.org/source/openssl-0.9.8s.tar.gz') + self.assert_detected( + '1.5E', 'ftp://ftp.visi.com/users/hawkeyd/X/Xaw3d-1.5E.tar.gz') + self.assert_detected( + '2.1.0beta', 'http://downloads.sourceforge.net/project/fann/fann/2.1.0beta/fann-2.1.0beta.zip') + self.assert_detected( + '2.0.1', 'ftp://iges.org/grads/2.0/grads-2.0.1-bin-darwin9.8-intel.tar.gz') + self.assert_detected( + '2.08', 'http://haxe.org/file/haxe-2.08-osx.tar.gz') + self.assert_detected( + '2007f', 'ftp://ftp.cac.washington.edu/imap/imap-2007f.tar.gz') + self.assert_detected( + '3.3.12ga7', 'http://sourceforge.net/projects/x3270/files/x3270/3.3.12ga7/suite3270-3.3.12ga7-src.tgz') + self.assert_detected( + '1.3.6p2', 'http://synergy.googlecode.com/files/synergy-1.3.6p2-MacOSX-Universal.zip') + + + + +if __name__ == "__main__": + unittest.main() diff --git a/lib/spack/spack/tty.py b/lib/spack/spack/tty.py new file mode 100644 index 0000000000..b7b27d2e3b --- /dev/null +++ b/lib/spack/spack/tty.py @@ -0,0 +1,63 @@ +import sys +import spack + +indent = " " + +def escape(s): + """Returns a TTY escape code if stdout is a tty, otherwise empty string""" + if sys.stdout.isatty(): + return "\033[{}m".format(s) + return '' + +def color(n): + return escape("0;{}".format(n)) + +def bold(n): + return escape("1;{}".format(n)) + +def underline(n): + return escape("4;{}".format(n)) + +blue = bold(34) +white = bold(39) +red = bold(31) +yellow = underline(33) +green = bold(92) +gray = bold(30) +em = underline(39) +reset = escape(0) + +def msg(msg, *args, **kwargs): + color = kwargs.get("color", blue) + print "{}==>{} {}{}".format(color, white, str(msg), reset) + for arg in args: print indent + str(arg) + +def verbose(*args): + if spack.verbose: msg(*args, color=green) + +def debug(*args): + if spack.debug: msg(*args, color=red) + +def error(msg, *args): + print "{}Error{}: {}".format(red, reset, str(msg)) + for arg in args: print indent + str(arg) + +def warn(msg, *args): + print "{}Warning{}: {}".format(yellow, reset, str(msg)) + for arg in args: print indent + str(arg) + +def die(msg): + error(msg) + sys.exit(1) + +def pkg(msg): + """Outputs a message with a package icon.""" + import platform + from version import Version + + mac_version = platform.mac_ver()[0] + if Version(mac_version) >= Version("10.7"): + print u"\U0001F4E6" + indent, + else: + print '[%] ', + print msg diff --git a/lib/spack/spack/validate.py b/lib/spack/spack/validate.py new file mode 100644 index 0000000000..312517f42a --- /dev/null +++ b/lib/spack/spack/validate.py @@ -0,0 +1,13 @@ +import tty +from fileutils import ALLOWED_ARCHIVE_TYPES +from urlparse import urlparse + +ALLOWED_SCHEMES = ["http", "https", "ftp"] + +def url(url_string): + url = urlparse(url_string) + if url.scheme not in ALLOWED_SCHEMES: + tty.die("Invalid protocol in URL: '%s'" % url_string) + + if not any(url_string.endswith(t) for t in ALLOWED_ARCHIVE_TYPES): + tty.die("Invalid file type in URL: '%s'" % url_string) diff --git a/lib/spack/spack/version.py b/lib/spack/spack/version.py new file mode 100644 index 0000000000..e3fdd1c826 --- /dev/null +++ b/lib/spack/spack/version.py @@ -0,0 +1,126 @@ +import os +import re + +import fileutils + +class Version(object): + """Class to represent versions""" + def __init__(self, version_string): + self.version_string = version_string + self.version = canonical(version_string) + + def __cmp__(self, other): + return cmp(self.version, other.version) + + @property + def major(self): + return self.component(0) + + @property + def minor(self): + return self.component(1) + + @property + def patch(self): + return self.component(2) + + def component(self, i): + """Returns the ith version component""" + if len(self.version) > i: + return self.version[i] + else: + return None + + def __repr__(self): + return self.version_string + + def __str__(self): + return self.version_string + + +def canonical(v): + """Get a "canonical" version of a version string, as a tuple.""" + def intify(part): + try: + return int(part) + except: + return part + return tuple(intify(v) for v in re.split(r'[_.-]+', v)) + + +def parse(spec): + """Try to extract a version from a filename. 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 = fileutils.stem(os.path.dirname(spec)) + else: + stem = fileutils.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, lambda s: s.replace('_', '.')), + + # 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 type in version_types: + regex, match_string = type[:2] + match = re.search(regex, match_string) + if match and match.group(1) is not None: + if type[2:]: + return Version(type[2](match.group(1))) + else: + return Version(match.group(1)) + return None -- cgit v1.2.3-70-g09d2