Documentation and small changes.
This commit is contained in:
parent
e410df743a
commit
269cf53a68
10 changed files with 452 additions and 171 deletions
|
@ -1,6 +1,5 @@
|
|||
|
||||
from globals import *
|
||||
from utils import *
|
||||
from exception import *
|
||||
|
||||
from Package import Package, depends_on
|
||||
from package import Package, depends_on
|
||||
|
|
|
@ -3,11 +3,10 @@
|
|||
description = "Fetch archives for packages"
|
||||
|
||||
def setup_parser(subparser):
|
||||
subparser.add_argument('name', help="name of package to fetch")
|
||||
subparser.add_argument('-f', '--file', dest='file', default=None,
|
||||
help="supply an archive file instead of fetching from the package's URL.")
|
||||
subparser.add_argument('names', nargs='+', help="names of packages to fetch")
|
||||
|
||||
|
||||
def fetch(parser, args):
|
||||
package = packages.get(args.name)
|
||||
package.do_fetch()
|
||||
for name in args.names:
|
||||
package = packages.get(name)
|
||||
package.do_fetch()
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
description = "Build and install packages"
|
||||
|
||||
def setup_parser(subparser):
|
||||
subparser.add_argument('names', nargs='+', help="name(s) of package(s) to install")
|
||||
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.")
|
||||
|
|
|
@ -21,3 +21,19 @@ class CommandFailedException(SpackException):
|
|||
def __init__(self, command):
|
||||
super(CommandFailedException, self).__init__("Failed to execute command: " + command)
|
||||
self.command = command
|
||||
|
||||
|
||||
class VersionParseException(SpackException):
|
||||
def __init__(self, msg, spec):
|
||||
super(VersionParseException, self).__init__(msg)
|
||||
self.spec = spec
|
||||
|
||||
|
||||
class UndetectableVersionException(VersionParseException):
|
||||
def __init__(self, spec):
|
||||
super(UndetectableVersionException, self).__init__("Couldn't detect version in: " + spec, spec)
|
||||
|
||||
|
||||
class UndetectableNameException(VersionParseException):
|
||||
def __init__(self, spec):
|
||||
super(UndetectableNameException, self).__init__("Couldn't parse package name in: " + spec)
|
||||
|
|
|
@ -1,11 +1,6 @@
|
|||
import os
|
||||
import re
|
||||
import multiprocessing
|
||||
from version import Version
|
||||
|
||||
import tty
|
||||
from utils import *
|
||||
from spack.exception import *
|
||||
|
||||
# This lives in $prefix/lib/spac/spack/__file__
|
||||
prefix = ancestor(__file__, 4)
|
||||
|
|
|
@ -1,3 +1,14 @@
|
|||
"""
|
||||
This is where most of the action happens in Spack.
|
||||
See the Package docs for detailed instructions on how the class works
|
||||
and on how to write your own packages.
|
||||
|
||||
The spack package structure is based strongly on Homebrew
|
||||
(http://wiki.github.com/mxcl/homebrew/), mainly because
|
||||
Homebrew makes it very easy to create packages. For a complete
|
||||
rundown on spack and how it differs from homebrew, look at the
|
||||
README.
|
||||
"""
|
||||
import sys
|
||||
import inspect
|
||||
import os
|
||||
|
@ -16,64 +27,205 @@
|
|||
from stage import Stage
|
||||
|
||||
|
||||
DEPENDS_ON = "depends_on"
|
||||
|
||||
class Dependency(object):
|
||||
"""Represents a dependency from one package to another."""
|
||||
def __init__(self, name, **kwargs):
|
||||
self.name = name
|
||||
for key in kwargs:
|
||||
setattr(self, key, kwargs[key])
|
||||
|
||||
@property
|
||||
def package(self):
|
||||
return packages.get(self.name)
|
||||
|
||||
def __repr__(self):
|
||||
return "<dep: %s>" % self.name
|
||||
|
||||
def __str__(self):
|
||||
return self.__repr__()
|
||||
|
||||
|
||||
def depends_on(*args, **kwargs):
|
||||
"""Adds a depends_on local variable in the locals of
|
||||
the calling class, based on args.
|
||||
"""
|
||||
# This gets the calling frame so we can pop variables into it
|
||||
locals = sys._getframe(1).f_locals
|
||||
|
||||
# Put deps into the dependencies variable
|
||||
dependencies = locals.setdefault("dependencies", [])
|
||||
for name in args:
|
||||
dependencies.append(Dependency(name))
|
||||
|
||||
|
||||
class MakeExecutable(Executable):
|
||||
"""Special Executable for make so the user can specify parallel or
|
||||
not on a per-invocation basis. Using 'parallel' as a kwarg will
|
||||
override whatever the package's global setting is, so you can
|
||||
either default to true or false and override particular calls.
|
||||
|
||||
Note that if the SPACK_NO_PARALLEL_MAKE env var is set it overrides
|
||||
everything.
|
||||
"""
|
||||
def __init__(self, name, parallel):
|
||||
super(MakeExecutable, self).__init__(name)
|
||||
self.parallel = parallel
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
parallel = kwargs.get('parallel', self.parallel)
|
||||
disable_parallel = env_flag(SPACK_NO_PARALLEL_MAKE)
|
||||
|
||||
if parallel and not disable_parallel:
|
||||
jobs = "-j%d" % multiprocessing.cpu_count()
|
||||
args = (jobs,) + args
|
||||
|
||||
super(MakeExecutable, self).__call__(*args, **kwargs)
|
||||
|
||||
|
||||
class Package(object):
|
||||
"""This is the superclass for all spack packages.
|
||||
|
||||
The Package class
|
||||
==================
|
||||
Package is where the bulk of the work of installing packages is done.
|
||||
|
||||
A package defines how to fetch, verfiy (via, e.g., md5), build, and
|
||||
install a piece of software. A Package also defines what other
|
||||
packages it depends on, so that dependencies can be installed along
|
||||
with the package itself. Packages are written in pure python.
|
||||
|
||||
Packages are all submodules of spack.packages. If spack is installed
|
||||
in $prefix, all of its python files are in $prefix/lib/spack. Most
|
||||
of them are in the spack module, so all the packages live in
|
||||
$prefix/lib/spack/spack/packages.
|
||||
|
||||
All you have to do to create a package is make a new subclass of Package
|
||||
in this directory. Spack automatically scans the python files there
|
||||
and figures out which one to import when you invoke it.
|
||||
|
||||
An example package
|
||||
====================
|
||||
Let's look at the cmake package to start with. This package lives in
|
||||
$prefix/lib/spack/spack/packages/cmake.py:
|
||||
|
||||
from spack import *
|
||||
class Cmake(object):
|
||||
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,
|
||||
'--parallel=%s' % make_jobs)
|
||||
make()
|
||||
make('install')
|
||||
|
||||
Naming conventions
|
||||
---------------------
|
||||
There are two names you should care about:
|
||||
|
||||
1. The module name, 'cmake'.
|
||||
- User will refers to this name, e.g. 'spack install cmake'.
|
||||
- Corresponds to the name of the file, 'cmake.py', and it can
|
||||
include _, -, and numbers (it can even start with a number).
|
||||
|
||||
2. The class name, "Cmake". This is formed by converting -'s or _'s
|
||||
in the module name to camel case. If the name starts with a number,
|
||||
we prefix the class name with 'Num_'. Examples:
|
||||
|
||||
Module Name Class Name
|
||||
foo_bar FooBar
|
||||
docbook-xml DocbookXml
|
||||
FooBar Foobar
|
||||
3proxy Num_3proxy
|
||||
|
||||
The class name is what spack looks for when it loads a package module.
|
||||
|
||||
Required Attributes
|
||||
---------------------
|
||||
Aside from proper naming, here is the bare minimum set of things you
|
||||
need when you make a package:
|
||||
homepage informational URL, so that users know what they're
|
||||
installing.
|
||||
|
||||
url URL of the source archive that spack will fetch.
|
||||
|
||||
md5 md5 hash of the source archive, so that we can
|
||||
verify that it was downloaded securely and correctly.
|
||||
|
||||
install() This function tells spack how to build and install the
|
||||
software it downloaded.
|
||||
|
||||
Creating Packages
|
||||
===================
|
||||
As a package creator, you can probably ignore most of the preceding
|
||||
information, because you can use the 'spack create' command to do it
|
||||
all automatically.
|
||||
|
||||
You as the package creator generally only have to worry about writing
|
||||
your install function and specifying dependencies.
|
||||
|
||||
spack create
|
||||
----------------
|
||||
Most software comes in nicely packaged tarballs, like this one:
|
||||
http://www.cmake.org/files/v2.8/cmake-2.8.10.2.tar.gz
|
||||
|
||||
Taking a page from homebrew, spack deduces pretty much everything it
|
||||
needs to know from the URL above. If you simply type this:
|
||||
|
||||
spack create http://www.cmake.org/files/v2.8/cmake-2.8.10.2.tar.gz
|
||||
|
||||
Spack will download the tarball, generate an md5 hash, figure out the
|
||||
version and the name of the package from the URL, and create a new
|
||||
package file for you with all the names and attributes set correctly.
|
||||
|
||||
Once this skeleton code is generated, spack pops up the new package in
|
||||
your $EDITOR so that you can modify the parts that need changes.
|
||||
|
||||
Dependencies
|
||||
---------------
|
||||
If your package requires another in order to build, you can specify that
|
||||
like this:
|
||||
|
||||
class Stackwalker(Package):
|
||||
...
|
||||
depends_on("libdwarf")
|
||||
...
|
||||
|
||||
This tells spack that before it builds stackwalker, it needs to build
|
||||
the libdwarf package as well. Note that this is the module name, not
|
||||
the class name (The class name is really only used by spack to find
|
||||
your package).
|
||||
|
||||
Spack will download an install each dependency before it installs your
|
||||
package. In addtion, it will add -L, -I, and rpath arguments to your
|
||||
compiler and linker for each dependency. In most cases, this allows you
|
||||
to avoid specifying any dependencies in your configure or cmake line;
|
||||
you can just run configure or cmake without any additional arguments and
|
||||
it will find the dependencies automatically.
|
||||
|
||||
|
||||
The Install Function
|
||||
----------------------
|
||||
The install function is designed so that someone not too terribly familiar
|
||||
with Python could write a package installer. For example, we put a number
|
||||
of commands in install scope that you can use almost like shell commands.
|
||||
These include make, configure, cmake, rm, rmtree, mkdir, mkdirp, and others.
|
||||
|
||||
You can see above in the cmake script that these commands are used to run
|
||||
configure and make almost like they're used on the command line. The
|
||||
only difference is that they are python function calls and not shell
|
||||
commands.
|
||||
|
||||
It may be puzzling to you where the commands and functions in install live.
|
||||
They are NOT instance variables on the class; this would require us to
|
||||
type 'self.' all the time and it makes the install code unnecessarily long.
|
||||
Rather, spack puts these commands and variables in *module* scope for your
|
||||
Package subclass. Since each package has its own module, this doesn't
|
||||
pollute other namespaces, and it allows you to more easily implement an
|
||||
install function.
|
||||
|
||||
For a full list of commands and variables available in module scope, see the
|
||||
add_commands_to_module() function in this class. This is where most of
|
||||
them are created and set on the module.
|
||||
|
||||
|
||||
Parallel Builds
|
||||
-------------------
|
||||
By default, Spack will run make in parallel when you run make() in your
|
||||
install function. Spack figures out how many cores are available on
|
||||
your system and runs make with -j<cores>. If you do not want this behavior,
|
||||
you can explicitly mark a package not to use parallel make:
|
||||
|
||||
class SomePackage(Package):
|
||||
...
|
||||
parallel = False
|
||||
...
|
||||
|
||||
This changes thd default behavior so that make is sequential. If you still
|
||||
want to build some parts in parallel, you can do this in your install function:
|
||||
|
||||
make(parallel=True)
|
||||
|
||||
Likewise, if you do not supply parallel = True in your Package, you can keep
|
||||
the default parallel behavior and run make like this when you want a
|
||||
sequential build:
|
||||
|
||||
make(parallel=False)
|
||||
|
||||
Package Lifecycle
|
||||
==================
|
||||
This section is really only for developers of new spack commands.
|
||||
|
||||
A package's lifecycle over a run of Spack looks something like this:
|
||||
|
||||
packge p = new Package() # Done for you by spack
|
||||
|
||||
p.do_fetch() # called by spack commands in spack/cmd.
|
||||
p.do_stage() # see spack.stage.Stage docs.
|
||||
p.do_install() # calls package's install() function
|
||||
p.do_uninstall()
|
||||
|
||||
There are also some other commands that clean the build area:
|
||||
p.do_clean() # runs make clean
|
||||
p.do_clean_work() # removes the build directory and
|
||||
# re-expands the archive.
|
||||
p.do_clean_dist() # removes the stage directory entirely
|
||||
|
||||
The convention used here is that a do_* function is intended to be called
|
||||
internally by Spack commands (in spack.cmd). These aren't for package
|
||||
writers to override, and doing so may break the functionality of the Package
|
||||
class.
|
||||
|
||||
Package creators override functions like install() (all of them do this),
|
||||
clean() (some of them do this), and others to provide custom behavior.
|
||||
"""
|
||||
|
||||
def __init__(self, arch=arch.sys_type()):
|
||||
attr.required(self, 'homepage')
|
||||
attr.required(self, 'url')
|
||||
|
@ -101,7 +253,7 @@ def __init__(self, arch=arch.sys_type()):
|
|||
tty.die("Couldn't extract version from %s. " +
|
||||
"You must specify it explicitly for this URL." % self.url)
|
||||
|
||||
# This adds a bunch of convenient commands to the package's module scope.
|
||||
# This adds a bunch of convenience commands to the package's module scope.
|
||||
self.add_commands_to_module()
|
||||
|
||||
# Controls whether install and uninstall check deps before acting.
|
||||
|
@ -114,71 +266,65 @@ def __init__(self, arch=arch.sys_type()):
|
|||
self.dirty = False
|
||||
|
||||
# stage used to build this package.
|
||||
self.stage = Stage(self.stage_name, self.url)
|
||||
|
||||
|
||||
def make_make(self):
|
||||
"""Create a make command set up with the proper default arguments."""
|
||||
make = which('make', required=True)
|
||||
return make
|
||||
Self.stage = Stage(self.stage_name, self.url)
|
||||
|
||||
|
||||
def add_commands_to_module(self):
|
||||
"""Populate the module scope of install() with some useful functions.
|
||||
This makes things easier for package writers.
|
||||
"""
|
||||
self.module.make = MakeExecutable('make', self.parallel)
|
||||
self.module.gmake = MakeExecutable('gmake', self.parallel)
|
||||
m = self.module
|
||||
|
||||
m.make = MakeExecutable('make', self.parallel)
|
||||
m.gmake = MakeExecutable('gmake', self.parallel)
|
||||
|
||||
# number of jobs spack prefers to build with.
|
||||
self.module.make_jobs = multiprocessing.cpu_count()
|
||||
m.make_jobs = multiprocessing.cpu_count()
|
||||
|
||||
# Find the configure script in the archive path
|
||||
# Don't use which for this; we want to find it in the current dir.
|
||||
self.module.configure = Executable('./configure')
|
||||
self.module.cmake = which("cmake")
|
||||
m.configure = Executable('./configure')
|
||||
m.cmake = which("cmake")
|
||||
|
||||
# standard CMake arguments
|
||||
self.module.std_cmake_args = [
|
||||
'-DCMAKE_INSTALL_PREFIX=%s' % self.prefix,
|
||||
'-DCMAKE_BUILD_TYPE=None']
|
||||
m.std_cmake_args = ['-DCMAKE_INSTALL_PREFIX=%s' % self.prefix,
|
||||
'-DCMAKE_BUILD_TYPE=None']
|
||||
if platform.mac_ver()[0]:
|
||||
self.module.std_cmake_args.append('-DCMAKE_FIND_FRAMEWORK=LAST')
|
||||
m.std_cmake_args.append('-DCMAKE_FIND_FRAMEWORK=LAST')
|
||||
|
||||
# Emulate some shell commands for convenience
|
||||
self.module.cd = os.chdir
|
||||
self.module.mkdir = os.mkdir
|
||||
self.module.makedirs = os.makedirs
|
||||
self.module.removedirs = os.removedirs
|
||||
m.cd = os.chdir
|
||||
m.mkdir = os.mkdir
|
||||
m.makedirs = os.makedirs
|
||||
m.remove = os.remove
|
||||
m.removedirs = os.removedirs
|
||||
|
||||
self.module.mkdirp = mkdirp
|
||||
self.module.install = install
|
||||
self.module.rmtree = shutil.rmtree
|
||||
self.module.move = shutil.move
|
||||
self.module.remove = os.remove
|
||||
m.mkdirp = mkdirp
|
||||
m.install = install
|
||||
m.rmtree = shutil.rmtree
|
||||
m.move = shutil.move
|
||||
|
||||
# Useful directories within the prefix
|
||||
self.module.prefix = self.prefix
|
||||
self.module.bin = new_path(self.prefix, 'bin')
|
||||
self.module.sbin = new_path(self.prefix, 'sbin')
|
||||
self.module.etc = new_path(self.prefix, 'etc')
|
||||
self.module.include = new_path(self.prefix, 'include')
|
||||
self.module.lib = new_path(self.prefix, 'lib')
|
||||
self.module.lib64 = new_path(self.prefix, 'lib64')
|
||||
self.module.libexec = new_path(self.prefix, 'libexec')
|
||||
self.module.share = new_path(self.prefix, 'share')
|
||||
self.module.doc = new_path(self.module.share, 'doc')
|
||||
self.module.info = new_path(self.module.share, 'info')
|
||||
self.module.man = new_path(self.module.share, 'man')
|
||||
self.module.man1 = new_path(self.module.man, 'man1')
|
||||
self.module.man2 = new_path(self.module.man, 'man2')
|
||||
self.module.man3 = new_path(self.module.man, 'man3')
|
||||
self.module.man4 = new_path(self.module.man, 'man4')
|
||||
self.module.man5 = new_path(self.module.man, 'man5')
|
||||
self.module.man6 = new_path(self.module.man, 'man6')
|
||||
self.module.man7 = new_path(self.module.man, 'man7')
|
||||
self.module.man8 = new_path(self.module.man, 'man8')
|
||||
|
||||
m.prefix = self.prefix
|
||||
m.bin = new_path(self.prefix, 'bin')
|
||||
m.sbin = new_path(self.prefix, 'sbin')
|
||||
m.etc = new_path(self.prefix, 'etc')
|
||||
m.include = new_path(self.prefix, 'include')
|
||||
m.lib = new_path(self.prefix, 'lib')
|
||||
m.lib64 = new_path(self.prefix, 'lib64')
|
||||
m.libexec = new_path(self.prefix, 'libexec')
|
||||
m.share = new_path(self.prefix, 'share')
|
||||
m.doc = new_path(m.share, 'doc')
|
||||
m.info = new_path(m.share, 'info')
|
||||
m.man = new_path(m.share, 'man')
|
||||
m.man1 = new_path(m.man, 'man1')
|
||||
m.man2 = new_path(m.man, 'man2')
|
||||
m.man3 = new_path(m.man, 'man3')
|
||||
m.man4 = new_path(m.man, 'man4')
|
||||
m.man5 = new_path(m.man, 'man5')
|
||||
m.man6 = new_path(m.man, 'man6')
|
||||
m.man7 = new_path(m.man, 'man7')
|
||||
m.man8 = new_path(m.man, 'man8')
|
||||
|
||||
@property
|
||||
def dependents(self):
|
||||
|
@ -409,3 +555,58 @@ def do_clean_dist(self):
|
|||
if os.path.exists(self.stage.path):
|
||||
self.stage.destroy()
|
||||
tty.msg("Successfully cleaned %s" % self.name)
|
||||
|
||||
|
||||
class Dependency(object):
|
||||
"""Represents a dependency from one package to another."""
|
||||
def __init__(self, name, **kwargs):
|
||||
self.name = name
|
||||
for key in kwargs:
|
||||
setattr(self, key, kwargs[key])
|
||||
|
||||
@property
|
||||
def package(self):
|
||||
return packages.get(self.name)
|
||||
|
||||
def __repr__(self):
|
||||
return "<dep: %s>" % self.name
|
||||
|
||||
def __str__(self):
|
||||
return self.__repr__()
|
||||
|
||||
|
||||
def depends_on(*args, **kwargs):
|
||||
"""Adds a depends_on local variable in the locals of
|
||||
the calling class, based on args.
|
||||
"""
|
||||
# This gets the calling frame so we can pop variables into it
|
||||
locals = sys._getframe(1).f_locals
|
||||
|
||||
# Put deps into the dependencies variable
|
||||
dependencies = locals.setdefault("dependencies", [])
|
||||
for name in args:
|
||||
dependencies.append(Dependency(name))
|
||||
|
||||
|
||||
class MakeExecutable(Executable):
|
||||
"""Special Executable for make so the user can specify parallel or
|
||||
not on a per-invocation basis. Using 'parallel' as a kwarg will
|
||||
override whatever the package's global setting is, so you can
|
||||
either default to true or false and override particular calls.
|
||||
|
||||
Note that if the SPACK_NO_PARALLEL_MAKE env var is set it overrides
|
||||
everything.
|
||||
"""
|
||||
def __init__(self, name, parallel):
|
||||
super(MakeExecutable, self).__init__(name)
|
||||
self.parallel = parallel
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
parallel = kwargs.get('parallel', self.parallel)
|
||||
disable_parallel = env_flag(SPACK_NO_PARALLEL_MAKE)
|
||||
|
||||
if parallel and not disable_parallel:
|
||||
jobs = "-j%d" % multiprocessing.cpu_count()
|
||||
args = (jobs,) + args
|
||||
|
||||
super(MakeExecutable, self).__call__(*args, **kwargs)
|
|
@ -74,7 +74,7 @@ def class_for(pkg):
|
|||
# If a class starts with a number, prefix it with Number_ to make it a valid
|
||||
# Python class name.
|
||||
if re.match(r'^[0-9]', class_name):
|
||||
class_name = "Number_%s" % class_name
|
||||
class_name = "Num_%s" % class_name
|
||||
|
||||
return class_name
|
||||
|
||||
|
|
|
@ -5,47 +5,53 @@
|
|||
import getpass
|
||||
|
||||
import spack
|
||||
import packages
|
||||
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)
|
||||
|
||||
|
||||
def remove_linked_tree(path):
|
||||
"""Removes a directory and its contents. If the directory is a symlink,
|
||||
follows the link and reamoves the real directory before removing the link.
|
||||
"""
|
||||
if os.path.exists(path):
|
||||
if os.path.islink(path):
|
||||
shutil.rmtree(os.path.realpath(path), True)
|
||||
os.unlink(path)
|
||||
else:
|
||||
shutil.rmtree(path, True)
|
||||
|
||||
|
||||
def purge():
|
||||
"""Remove any build directories in the stage path."""
|
||||
if os.path.isdir(spack.stage_path):
|
||||
for stage_dir in os.listdir(spack.stage_path):
|
||||
stage_path = spack.new_path(spack.stage_path, stage_dir)
|
||||
remove_linked_tree(stage_path)
|
||||
|
||||
|
||||
class Stage(object):
|
||||
"""A Stage object manaages a directory where an archive is downloaded,
|
||||
expanded, and built before being installed. A stage's lifecycle looks
|
||||
like this:
|
||||
|
||||
setup() Create the stage directory.
|
||||
fetch() Fetch a source archive into the stage.
|
||||
expand_archive() Expand the source archive.
|
||||
<install> Build and install the archive. This is handled
|
||||
by the Package class.
|
||||
destroy() Remove the stage once the package has been installed.
|
||||
|
||||
If spack.use_tmp_stage is True, spack will attempt to create stages
|
||||
in a tmp directory. Otherwise, stages are created directly in
|
||||
spack.stage_path.
|
||||
"""
|
||||
|
||||
def __init__(self, stage_name, url):
|
||||
"""Create a stage object.
|
||||
Parameters:
|
||||
stage_name Name of the stage directory that will be created.
|
||||
url URL of the archive to be downloaded into this stage.
|
||||
"""
|
||||
self.stage_name = stage_name
|
||||
self.url = url
|
||||
|
||||
@property
|
||||
def path(self):
|
||||
"""Absolute path to the stage directory."""
|
||||
return spack.new_path(spack.stage_path, self.stage_name)
|
||||
|
||||
|
||||
def setup(self):
|
||||
# If we're using a stag in tmp that has since been deleted,
|
||||
"""Creates the stage directory.
|
||||
If spack.use_tmp_stage is False, the stage directory is created
|
||||
directly under spack.stage_path.
|
||||
|
||||
If spack.use_tmp_stage is True, this will attempt to create a
|
||||
stage in a temporary directory and link it into spack.stage_path.
|
||||
Spack will use the first writable location in spack.tmp_dirs to
|
||||
create a stage. If there is no valid location in tmp_dirs, fall
|
||||
back to making the stage inside spack.stage_path.
|
||||
"""
|
||||
# If we're using a stage in tmp that has since been deleted,
|
||||
# remove the stale symbolic link.
|
||||
if os.path.islink(self.path):
|
||||
real_path = os.path.realpath(self.path)
|
||||
|
@ -68,18 +74,23 @@ def setup(self):
|
|||
if not os.path.isdir(self.path):
|
||||
tty.die("Stage path %s is not a directory!" % self.path)
|
||||
else:
|
||||
# Now create the stage directory
|
||||
# Create the top-level stage directory
|
||||
spack.mkdirp(spack.stage_path)
|
||||
|
||||
# And the stage for this build within it
|
||||
if not spack.use_tmp_stage:
|
||||
# non-tmp stage is just a directory in spack.stage_path
|
||||
spack.mkdirp(self.path)
|
||||
else:
|
||||
# tmp stage is created in tmp but linked to spack.stage_path
|
||||
# Find a tmp_dir if we're supposed to use one.
|
||||
tmp_dir = None
|
||||
if spack.use_tmp_stage:
|
||||
tmp_dir = next((tmp for tmp in spack.tmp_dirs
|
||||
if os.access(tmp, os.R_OK|os.W_OK)), None)
|
||||
if can_access(tmp)), None)
|
||||
|
||||
if not tmp_dir:
|
||||
# If we couldn't find a tmp dir or if we're not using tmp
|
||||
# stages, create the stage directly in spack.stage_path.
|
||||
spack.mkdirp(self.path)
|
||||
|
||||
else:
|
||||
# Otherwise we found a tmp_dir, so create the stage there
|
||||
# and link it back to the prefix.
|
||||
username = getpass.getuser()
|
||||
if username:
|
||||
tmp_dir = spack.new_path(tmp_dir, username)
|
||||
|
@ -89,12 +100,13 @@ def setup(self):
|
|||
|
||||
os.symlink(tmp_dir, self.path)
|
||||
|
||||
# Finally make sure we can actually do something with the stage
|
||||
# Make sure we can actually do something with the stage we made.
|
||||
ensure_access(self.path)
|
||||
|
||||
|
||||
@property
|
||||
def archive_file(self):
|
||||
"""Path to the source archive within this stage directory."""
|
||||
path = os.path.join(self.path, os.path.basename(self.url))
|
||||
if os.path.exists(path):
|
||||
return path
|
||||
|
@ -155,6 +167,10 @@ def fetch(self):
|
|||
|
||||
|
||||
def expand_archive(self):
|
||||
"""Changes to the stage directory and attempt to expand the downloaded
|
||||
archive. Fail if the stage is not set up or if the archive is not yet
|
||||
downloaded.
|
||||
"""
|
||||
self.chdir()
|
||||
|
||||
if not self.archive_file:
|
||||
|
@ -165,8 +181,8 @@ def expand_archive(self):
|
|||
|
||||
|
||||
def chdir_to_archive(self):
|
||||
"""Changes directory to the expanded archive directory if it exists.
|
||||
Dies with an error otherwise.
|
||||
"""Changes directory to the expanded archive directory.
|
||||
Dies with an error if there was no expanded archive.
|
||||
"""
|
||||
path = self.expanded_archive_path
|
||||
if not path:
|
||||
|
@ -178,7 +194,9 @@ def chdir_to_archive(self):
|
|||
|
||||
|
||||
def restage(self):
|
||||
"""Removes the expanded archive path if it exists, then re-expands the archive."""
|
||||
"""Removes the expanded archive path if it exists, then re-expands
|
||||
the archive.
|
||||
"""
|
||||
if not self.archive_file:
|
||||
tty.die("Attempt to restage when not staged.")
|
||||
|
||||
|
@ -188,5 +206,38 @@ def restage(self):
|
|||
|
||||
|
||||
def destroy(self):
|
||||
"""Blows away the stage directory. Can always call setup() again."""
|
||||
"""Remove this stage directory."""
|
||||
remove_linked_tree(self.path)
|
||||
|
||||
|
||||
|
||||
def can_access(file=spack.stage_path):
|
||||
"""True if we have read/write access to the file."""
|
||||
return os.access(file, os.R_OK|os.W_OK)
|
||||
|
||||
|
||||
def ensure_access(file=spack.stage_path):
|
||||
"""Ensure we can access a directory and die with an error if we can't."""
|
||||
if not can_access(file):
|
||||
tty.die("Insufficient permissions for %s" % file)
|
||||
|
||||
|
||||
def remove_linked_tree(path):
|
||||
"""Removes a directory and its contents. If the directory is a symlink,
|
||||
follows the link and reamoves the real directory before removing the
|
||||
link.
|
||||
"""
|
||||
if os.path.exists(path):
|
||||
if os.path.islink(path):
|
||||
shutil.rmtree(os.path.realpath(path), True)
|
||||
os.unlink(path)
|
||||
else:
|
||||
shutil.rmtree(path, True)
|
||||
|
||||
|
||||
def purge():
|
||||
"""Remove all build directories in the top-level stage path."""
|
||||
if os.path.isdir(spack.stage_path):
|
||||
for stage_dir in os.listdir(spack.stage_path):
|
||||
stage_path = spack.new_path(spack.stage_path, stage_dir)
|
||||
remove_linked_tree(stage_path)
|
||||
|
|
|
@ -3,15 +3,15 @@
|
|||
This file has a bunch of versions tests taken from the excellent version
|
||||
detection in Homebrew.
|
||||
"""
|
||||
import spack.version as version
|
||||
import unittest
|
||||
import spack.version as version
|
||||
from spack.exception import *
|
||||
|
||||
|
||||
class VersionTest(unittest.TestCase):
|
||||
|
||||
def assert_not_detected(self, string):
|
||||
name, v = version.parse(string)
|
||||
self.assertIsNone(v)
|
||||
self.assertRaises(UndetectableVersionException, version.parse, string)
|
||||
|
||||
def assert_detected(self, name, v, string):
|
||||
parsed_name, parsed_v = version.parse(string)
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
import re
|
||||
|
||||
import utils
|
||||
from exception import *
|
||||
|
||||
class Version(object):
|
||||
"""Class to represent versions"""
|
||||
|
@ -45,12 +46,13 @@ def intify(part):
|
|||
return int(part)
|
||||
except:
|
||||
return part
|
||||
|
||||
return tuple(intify(v) for v in re.split(r'[_.-]+', v))
|
||||
|
||||
|
||||
def parse_version(spec):
|
||||
"""Try to extract a version from a filename or URL. This is taken
|
||||
largely from Homebrew's Version class."""
|
||||
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)
|
||||
|
@ -76,7 +78,7 @@ def parse_version(spec):
|
|||
(r'[-_](R\d+[AB]\d*(-\d+)?)', spec),
|
||||
|
||||
# e.g. boost_1_39_0
|
||||
(r'((\d+_)+\d+)$', stem, lambda s: s.replace('_', '.')),
|
||||
(r'((\d+_)+\d+)$', stem),
|
||||
|
||||
# e.g. foobar-4.5.1-1
|
||||
# e.g. ruby-1.9.1-p243
|
||||
|
@ -119,11 +121,29 @@ def parse_version(spec):
|
|||
regex, match_string = vtype[:2]
|
||||
match = re.search(regex, match_string)
|
||||
if match and match.group(1) is not None:
|
||||
if vtype[2:]:
|
||||
return Version(vtype[2](match.group(1)))
|
||||
else:
|
||||
return Version(match.group(1))
|
||||
return None
|
||||
return match.group(1), match.start(1), match.end(1)
|
||||
|
||||
raise UndetectableVersionException(spec)
|
||||
|
||||
|
||||
def parse_version(spec):
|
||||
"""Given a URL or archive name, extract a versino 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)
|
||||
|
||||
|
||||
def parse_name(spec, ver=None):
|
||||
|
@ -142,7 +162,7 @@ def parse_name(spec, ver=None):
|
|||
match = re.search(nt, spec)
|
||||
if match:
|
||||
return match.group(1)
|
||||
return None
|
||||
raise UndetectableNameException(spec)
|
||||
|
||||
def parse(spec):
|
||||
ver = parse_version(spec)
|
||||
|
|
Loading…
Reference in a new issue