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 globals import *
|
||||||
from utils import *
|
from utils import *
|
||||||
from exception 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"
|
description = "Fetch archives for packages"
|
||||||
|
|
||||||
def setup_parser(subparser):
|
def setup_parser(subparser):
|
||||||
subparser.add_argument('name', help="name of package to fetch")
|
subparser.add_argument('names', nargs='+', help="names of packages to fetch")
|
||||||
subparser.add_argument('-f', '--file', dest='file', default=None,
|
|
||||||
help="supply an archive file instead of fetching from the package's URL.")
|
|
||||||
|
|
||||||
|
|
||||||
def fetch(parser, args):
|
def fetch(parser, args):
|
||||||
package = packages.get(args.name)
|
for name in args.names:
|
||||||
package.do_fetch()
|
package = packages.get(name)
|
||||||
|
package.do_fetch()
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
description = "Build and install packages"
|
description = "Build and install packages"
|
||||||
|
|
||||||
def setup_parser(subparser):
|
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',
|
subparser.add_argument('-i', '--ignore-dependencies',
|
||||||
action='store_true', dest='ignore_dependencies',
|
action='store_true', dest='ignore_dependencies',
|
||||||
help="Do not try to install dependencies of requested packages.")
|
help="Do not try to install dependencies of requested packages.")
|
||||||
|
|
|
@ -21,3 +21,19 @@ class CommandFailedException(SpackException):
|
||||||
def __init__(self, command):
|
def __init__(self, command):
|
||||||
super(CommandFailedException, self).__init__("Failed to execute command: " + command)
|
super(CommandFailedException, self).__init__("Failed to execute command: " + command)
|
||||||
self.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 os
|
||||||
import re
|
|
||||||
import multiprocessing
|
|
||||||
from version import Version
|
from version import Version
|
||||||
|
|
||||||
import tty
|
|
||||||
from utils import *
|
from utils import *
|
||||||
from spack.exception import *
|
|
||||||
|
|
||||||
# This lives in $prefix/lib/spac/spack/__file__
|
# This lives in $prefix/lib/spac/spack/__file__
|
||||||
prefix = ancestor(__file__, 4)
|
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 sys
|
||||||
import inspect
|
import inspect
|
||||||
import os
|
import os
|
||||||
|
@ -16,64 +27,205 @@
|
||||||
from stage import Stage
|
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):
|
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()):
|
def __init__(self, arch=arch.sys_type()):
|
||||||
attr.required(self, 'homepage')
|
attr.required(self, 'homepage')
|
||||||
attr.required(self, 'url')
|
attr.required(self, 'url')
|
||||||
|
@ -101,7 +253,7 @@ def __init__(self, arch=arch.sys_type()):
|
||||||
tty.die("Couldn't extract version from %s. " +
|
tty.die("Couldn't extract version from %s. " +
|
||||||
"You must specify it explicitly for this URL." % self.url)
|
"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()
|
self.add_commands_to_module()
|
||||||
|
|
||||||
# Controls whether install and uninstall check deps before acting.
|
# Controls whether install and uninstall check deps before acting.
|
||||||
|
@ -114,71 +266,65 @@ def __init__(self, arch=arch.sys_type()):
|
||||||
self.dirty = False
|
self.dirty = False
|
||||||
|
|
||||||
# stage used to build this package.
|
# stage used to build this package.
|
||||||
self.stage = Stage(self.stage_name, self.url)
|
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
|
|
||||||
|
|
||||||
|
|
||||||
def add_commands_to_module(self):
|
def add_commands_to_module(self):
|
||||||
"""Populate the module scope of install() with some useful functions.
|
"""Populate the module scope of install() with some useful functions.
|
||||||
This makes things easier for package writers.
|
This makes things easier for package writers.
|
||||||
"""
|
"""
|
||||||
self.module.make = MakeExecutable('make', self.parallel)
|
m = self.module
|
||||||
self.module.gmake = MakeExecutable('gmake', self.parallel)
|
|
||||||
|
m.make = MakeExecutable('make', self.parallel)
|
||||||
|
m.gmake = MakeExecutable('gmake', self.parallel)
|
||||||
|
|
||||||
# number of jobs spack prefers to build with.
|
# 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
|
# Find the configure script in the archive path
|
||||||
# Don't use which for this; we want to find it in the current dir.
|
# Don't use which for this; we want to find it in the current dir.
|
||||||
self.module.configure = Executable('./configure')
|
m.configure = Executable('./configure')
|
||||||
self.module.cmake = which("cmake")
|
m.cmake = which("cmake")
|
||||||
|
|
||||||
# standard CMake arguments
|
# standard CMake arguments
|
||||||
self.module.std_cmake_args = [
|
m.std_cmake_args = ['-DCMAKE_INSTALL_PREFIX=%s' % self.prefix,
|
||||||
'-DCMAKE_INSTALL_PREFIX=%s' % self.prefix,
|
'-DCMAKE_BUILD_TYPE=None']
|
||||||
'-DCMAKE_BUILD_TYPE=None']
|
|
||||||
if platform.mac_ver()[0]:
|
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
|
# Emulate some shell commands for convenience
|
||||||
self.module.cd = os.chdir
|
m.cd = os.chdir
|
||||||
self.module.mkdir = os.mkdir
|
m.mkdir = os.mkdir
|
||||||
self.module.makedirs = os.makedirs
|
m.makedirs = os.makedirs
|
||||||
self.module.removedirs = os.removedirs
|
m.remove = os.remove
|
||||||
|
m.removedirs = os.removedirs
|
||||||
|
|
||||||
self.module.mkdirp = mkdirp
|
m.mkdirp = mkdirp
|
||||||
self.module.install = install
|
m.install = install
|
||||||
self.module.rmtree = shutil.rmtree
|
m.rmtree = shutil.rmtree
|
||||||
self.module.move = shutil.move
|
m.move = shutil.move
|
||||||
self.module.remove = os.remove
|
|
||||||
|
|
||||||
# Useful directories within the prefix
|
# Useful directories within the prefix
|
||||||
self.module.prefix = self.prefix
|
m.prefix = self.prefix
|
||||||
self.module.bin = new_path(self.prefix, 'bin')
|
m.bin = new_path(self.prefix, 'bin')
|
||||||
self.module.sbin = new_path(self.prefix, 'sbin')
|
m.sbin = new_path(self.prefix, 'sbin')
|
||||||
self.module.etc = new_path(self.prefix, 'etc')
|
m.etc = new_path(self.prefix, 'etc')
|
||||||
self.module.include = new_path(self.prefix, 'include')
|
m.include = new_path(self.prefix, 'include')
|
||||||
self.module.lib = new_path(self.prefix, 'lib')
|
m.lib = new_path(self.prefix, 'lib')
|
||||||
self.module.lib64 = new_path(self.prefix, 'lib64')
|
m.lib64 = new_path(self.prefix, 'lib64')
|
||||||
self.module.libexec = new_path(self.prefix, 'libexec')
|
m.libexec = new_path(self.prefix, 'libexec')
|
||||||
self.module.share = new_path(self.prefix, 'share')
|
m.share = new_path(self.prefix, 'share')
|
||||||
self.module.doc = new_path(self.module.share, 'doc')
|
m.doc = new_path(m.share, 'doc')
|
||||||
self.module.info = new_path(self.module.share, 'info')
|
m.info = new_path(m.share, 'info')
|
||||||
self.module.man = new_path(self.module.share, 'man')
|
m.man = new_path(m.share, 'man')
|
||||||
self.module.man1 = new_path(self.module.man, 'man1')
|
m.man1 = new_path(m.man, 'man1')
|
||||||
self.module.man2 = new_path(self.module.man, 'man2')
|
m.man2 = new_path(m.man, 'man2')
|
||||||
self.module.man3 = new_path(self.module.man, 'man3')
|
m.man3 = new_path(m.man, 'man3')
|
||||||
self.module.man4 = new_path(self.module.man, 'man4')
|
m.man4 = new_path(m.man, 'man4')
|
||||||
self.module.man5 = new_path(self.module.man, 'man5')
|
m.man5 = new_path(m.man, 'man5')
|
||||||
self.module.man6 = new_path(self.module.man, 'man6')
|
m.man6 = new_path(m.man, 'man6')
|
||||||
self.module.man7 = new_path(self.module.man, 'man7')
|
m.man7 = new_path(m.man, 'man7')
|
||||||
self.module.man8 = new_path(self.module.man, 'man8')
|
m.man8 = new_path(m.man, 'man8')
|
||||||
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def dependents(self):
|
def dependents(self):
|
||||||
|
@ -409,3 +555,58 @@ def do_clean_dist(self):
|
||||||
if os.path.exists(self.stage.path):
|
if os.path.exists(self.stage.path):
|
||||||
self.stage.destroy()
|
self.stage.destroy()
|
||||||
tty.msg("Successfully cleaned %s" % self.name)
|
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
|
# If a class starts with a number, prefix it with Number_ to make it a valid
|
||||||
# Python class name.
|
# Python class name.
|
||||||
if re.match(r'^[0-9]', 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
|
return class_name
|
||||||
|
|
||||||
|
|
|
@ -5,47 +5,53 @@
|
||||||
import getpass
|
import getpass
|
||||||
|
|
||||||
import spack
|
import spack
|
||||||
import packages
|
|
||||||
import tty
|
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):
|
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):
|
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.stage_name = stage_name
|
||||||
self.url = url
|
self.url = url
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def path(self):
|
def path(self):
|
||||||
|
"""Absolute path to the stage directory."""
|
||||||
return spack.new_path(spack.stage_path, self.stage_name)
|
return spack.new_path(spack.stage_path, self.stage_name)
|
||||||
|
|
||||||
|
|
||||||
def setup(self):
|
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.
|
# remove the stale symbolic link.
|
||||||
if os.path.islink(self.path):
|
if os.path.islink(self.path):
|
||||||
real_path = os.path.realpath(self.path)
|
real_path = os.path.realpath(self.path)
|
||||||
|
@ -68,18 +74,23 @@ def setup(self):
|
||||||
if not os.path.isdir(self.path):
|
if not os.path.isdir(self.path):
|
||||||
tty.die("Stage path %s is not a directory!" % self.path)
|
tty.die("Stage path %s is not a directory!" % self.path)
|
||||||
else:
|
else:
|
||||||
# Now create the stage directory
|
# Create the top-level stage directory
|
||||||
spack.mkdirp(spack.stage_path)
|
spack.mkdirp(spack.stage_path)
|
||||||
|
|
||||||
# And the stage for this build within it
|
# Find a tmp_dir if we're supposed to use one.
|
||||||
if not spack.use_tmp_stage:
|
tmp_dir = None
|
||||||
# non-tmp stage is just a directory in spack.stage_path
|
if spack.use_tmp_stage:
|
||||||
spack.mkdirp(self.path)
|
|
||||||
else:
|
|
||||||
# tmp stage is created in tmp but linked to spack.stage_path
|
|
||||||
tmp_dir = next((tmp for tmp in spack.tmp_dirs
|
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()
|
username = getpass.getuser()
|
||||||
if username:
|
if username:
|
||||||
tmp_dir = spack.new_path(tmp_dir, username)
|
tmp_dir = spack.new_path(tmp_dir, username)
|
||||||
|
@ -89,12 +100,13 @@ def setup(self):
|
||||||
|
|
||||||
os.symlink(tmp_dir, self.path)
|
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)
|
ensure_access(self.path)
|
||||||
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def archive_file(self):
|
def archive_file(self):
|
||||||
|
"""Path to the source archive within this stage directory."""
|
||||||
path = os.path.join(self.path, os.path.basename(self.url))
|
path = os.path.join(self.path, os.path.basename(self.url))
|
||||||
if os.path.exists(path):
|
if os.path.exists(path):
|
||||||
return path
|
return path
|
||||||
|
@ -155,6 +167,10 @@ def fetch(self):
|
||||||
|
|
||||||
|
|
||||||
def expand_archive(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()
|
self.chdir()
|
||||||
|
|
||||||
if not self.archive_file:
|
if not self.archive_file:
|
||||||
|
@ -165,8 +181,8 @@ def expand_archive(self):
|
||||||
|
|
||||||
|
|
||||||
def chdir_to_archive(self):
|
def chdir_to_archive(self):
|
||||||
"""Changes directory to the expanded archive directory if it exists.
|
"""Changes directory to the expanded archive directory.
|
||||||
Dies with an error otherwise.
|
Dies with an error if there was no expanded archive.
|
||||||
"""
|
"""
|
||||||
path = self.expanded_archive_path
|
path = self.expanded_archive_path
|
||||||
if not path:
|
if not path:
|
||||||
|
@ -178,7 +194,9 @@ def chdir_to_archive(self):
|
||||||
|
|
||||||
|
|
||||||
def restage(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:
|
if not self.archive_file:
|
||||||
tty.die("Attempt to restage when not staged.")
|
tty.die("Attempt to restage when not staged.")
|
||||||
|
|
||||||
|
@ -188,5 +206,38 @@ def restage(self):
|
||||||
|
|
||||||
|
|
||||||
def destroy(self):
|
def destroy(self):
|
||||||
"""Blows away the stage directory. Can always call setup() again."""
|
"""Remove this stage directory."""
|
||||||
remove_linked_tree(self.path)
|
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
|
This file has a bunch of versions tests taken from the excellent version
|
||||||
detection in Homebrew.
|
detection in Homebrew.
|
||||||
"""
|
"""
|
||||||
import spack.version as version
|
|
||||||
import unittest
|
import unittest
|
||||||
|
import spack.version as version
|
||||||
|
from spack.exception import *
|
||||||
|
|
||||||
|
|
||||||
class VersionTest(unittest.TestCase):
|
class VersionTest(unittest.TestCase):
|
||||||
|
|
||||||
def assert_not_detected(self, string):
|
def assert_not_detected(self, string):
|
||||||
name, v = version.parse(string)
|
self.assertRaises(UndetectableVersionException, version.parse, string)
|
||||||
self.assertIsNone(v)
|
|
||||||
|
|
||||||
def assert_detected(self, name, v, string):
|
def assert_detected(self, name, v, string):
|
||||||
parsed_name, parsed_v = version.parse(string)
|
parsed_name, parsed_v = version.parse(string)
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
import re
|
import re
|
||||||
|
|
||||||
import utils
|
import utils
|
||||||
|
from exception import *
|
||||||
|
|
||||||
class Version(object):
|
class Version(object):
|
||||||
"""Class to represent versions"""
|
"""Class to represent versions"""
|
||||||
|
@ -45,12 +46,13 @@ def intify(part):
|
||||||
return int(part)
|
return int(part)
|
||||||
except:
|
except:
|
||||||
return part
|
return part
|
||||||
|
|
||||||
return tuple(intify(v) for v in re.split(r'[_.-]+', v))
|
return tuple(intify(v) for v in re.split(r'[_.-]+', v))
|
||||||
|
|
||||||
|
|
||||||
def parse_version(spec):
|
def parse_version_string_with_indices(spec):
|
||||||
"""Try to extract a version from a filename or URL. This is taken
|
"""Try to extract a version string from a filename or URL. This is taken
|
||||||
largely from Homebrew's Version class."""
|
largely from Homebrew's Version class."""
|
||||||
|
|
||||||
if os.path.isdir(spec):
|
if os.path.isdir(spec):
|
||||||
stem = os.path.basename(spec)
|
stem = os.path.basename(spec)
|
||||||
|
@ -76,7 +78,7 @@ def parse_version(spec):
|
||||||
(r'[-_](R\d+[AB]\d*(-\d+)?)', spec),
|
(r'[-_](R\d+[AB]\d*(-\d+)?)', spec),
|
||||||
|
|
||||||
# e.g. boost_1_39_0
|
# 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. foobar-4.5.1-1
|
||||||
# e.g. ruby-1.9.1-p243
|
# e.g. ruby-1.9.1-p243
|
||||||
|
@ -119,11 +121,29 @@ def parse_version(spec):
|
||||||
regex, match_string = vtype[:2]
|
regex, match_string = vtype[:2]
|
||||||
match = re.search(regex, match_string)
|
match = re.search(regex, match_string)
|
||||||
if match and match.group(1) is not None:
|
if match and match.group(1) is not None:
|
||||||
if vtype[2:]:
|
return match.group(1), match.start(1), match.end(1)
|
||||||
return Version(vtype[2](match.group(1)))
|
|
||||||
else:
|
raise UndetectableVersionException(spec)
|
||||||
return Version(match.group(1))
|
|
||||||
return None
|
|
||||||
|
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):
|
def parse_name(spec, ver=None):
|
||||||
|
@ -142,7 +162,7 @@ def parse_name(spec, ver=None):
|
||||||
match = re.search(nt, spec)
|
match = re.search(nt, spec)
|
||||||
if match:
|
if match:
|
||||||
return match.group(1)
|
return match.group(1)
|
||||||
return None
|
raise UndetectableNameException(spec)
|
||||||
|
|
||||||
def parse(spec):
|
def parse(spec):
|
||||||
ver = parse_version(spec)
|
ver = parse_version(spec)
|
||||||
|
|
Loading…
Reference in a new issue