summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTodd Gamblin <tgamblin@llnl.gov>2013-02-13 17:50:44 -0800
committerTodd Gamblin <tgamblin@llnl.gov>2013-02-13 17:50:44 -0800
commitcc76c0f5f9f8021cfb7423a226bd431c00d791ce (patch)
tree44bdf9954ab913a17ca3bd01ec6042fb34a18694
downloadspack-cc76c0f5f9f8021cfb7423a226bd431c00d791ce.tar.gz
spack-cc76c0f5f9f8021cfb7423a226bd431c00d791ce.tar.bz2
spack-cc76c0f5f9f8021cfb7423a226bd431c00d791ce.tar.xz
spack-cc76c0f5f9f8021cfb7423a226bd431c00d791ce.zip
Initial version of spack with one package: cmake
-rw-r--r--.gitignore4
-rwxr-xr-xbin/spack41
-rw-r--r--lib/spack/spack/Package.py168
-rw-r--r--lib/spack/spack/__init__.py5
-rw-r--r--lib/spack/spack/attr.py8
-rw-r--r--lib/spack/spack/cmd/__init__.py36
-rw-r--r--lib/spack/spack/cmd/arch.py12
-rw-r--r--lib/spack/spack/cmd/clean.py16
-rw-r--r--lib/spack/spack/cmd/create.py57
-rw-r--r--lib/spack/spack/cmd/edit.py50
-rw-r--r--lib/spack/spack/cmd/fetch.py9
-rw-r--r--lib/spack/spack/cmd/install.py11
-rw-r--r--lib/spack/spack/cmd/stage.py9
-rw-r--r--lib/spack/spack/cmd/uninstall.py9
-rw-r--r--lib/spack/spack/fileutils.py112
-rw-r--r--lib/spack/spack/globals.py42
-rw-r--r--lib/spack/spack/packages/__init__.py35
-rw-r--r--lib/spack/spack/packages/cmake.py11
-rw-r--r--lib/spack/spack/stage.py68
-rwxr-xr-xlib/spack/spack/test/test_versions.py189
-rw-r--r--lib/spack/spack/tty.py63
-rw-r--r--lib/spack/spack/validate.py13
-rw-r--r--lib/spack/spack/version.py126
23 files changed, 1094 insertions, 0 deletions
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 "<exe: %s>" % 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