diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..1e1cc11
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1 @@
+formula/COMMIT_INFO.txt export-subst
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..6cc140a
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+build/
+doc/_build/
+*.pyc
+dist/
diff --git a/COPYING b/COPYING
new file mode 100644
index 0000000..a1fff25
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,30 @@
+Copyright (c) 2005-2011, nipy developers
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+    * Redistributions of source code must retain the above copyright
+       notice, this list of conditions and the following disclaimer.
+
+    * Redistributions in binary form must reproduce the above
+       copyright notice, this list of conditions and the following
+       disclaimer in the documentation and/or other materials provided
+       with the distribution.
+
+    * Neither the name of the nipy developers nor the names of any
+       contributors may be used to endorse or promote products derived
+       from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..0453979
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,7 @@
+include AUTHOR COPYING Makefile* MANIFEST.in setup* README.*
+include Changelog TODO
+recursive-include doc *
+# put this stuff back into setup.py (package_data) once I'm enlightened
+# enough to accomplish this herculean task
+recursive-include formula/data *
+include formula/COMMIT_INFO.txt
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..f9d4968
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,40 @@
+PYTHON = python
+COVERAGE_REPORT=coverage
+
+clean:
+	$(MAKE) -C doc clean
+	-rm -rf build
+	-rm *-stamp
+
+distclean: clean
+	-rm MANIFEST
+	-rm $(COVERAGE_REPORT)
+	@find . -name '*.py[co]' \
+		 -o -name '*.a' \
+		 -o -name '*,cover' \
+		 -o -name '.coverage' \
+		 -o -iname '*~' \
+		 -o -iname '*.kcache' \
+		 -o -iname '*.pstats' \
+		 -o -iname '*.prof' \
+		 -o -iname '#*#' | xargs -L10 rm -f
+
+# Print out info for possible install methods
+check-version-info:
+	$(PYTHON) -c 'from nisext.testers import info_from_here; info_from_here("formula")'
+
+# Run tests from installed code
+installed-tests:
+	$(PYTHON) -c 'from nisext.testers import tests_installed; tests_installed("formula")'
+
+# Run tests from installed code
+sdist-tests:
+	$(PYTHON) -c 'from nisext.testers import sdist_tests; sdist_tests("formula")'
+
+bdist-egg-tests:
+	$(PYTHON) -c 'from nisext.testers import bdist_egg_tests; bdist_egg_tests("formula")'
+
+source-release: distclean
+	python -m compileall .
+	make distclean
+	python setup.py sdist --formats=gztar,zip
diff --git a/README.txt b/README.txt
new file mode 100644
index 0000000..7e6d8ff
--- /dev/null
+++ b/README.txt
@@ -0,0 +1,46 @@
+=======
+Formula
+=======
+
+This package contains an implementation of symbolic statistical models in
+Python.
+
+The implementation is of a full algebra of factors and numerical variables in
+statistical models.
+
+It has some similarities to model formulae in R, but differs in that the rules
+for the creation of design matrices are more completely defined by the model
+algebra.
+
+An earlier version of this package is in nipy_.
+
+Mailing Lists
+=============
+
+Please see the developer's list here::
+
+    http://mail.scipy.org/mailman/listinfo/nipy-devel
+
+Code
+====
+
+You can find our sources and single-click downloads:
+
+* `Main repository`_ on Github.
+* Documentation_ for all releases and current development tree.
+* Download as a tar/zip file the `current trunk`_.
+* Downloads of all `available releases`_.
+
+.. _nipy: http://nipy.org
+.. _main repository: http://github.com/jonathan-taylor/formula
+.. _documentation: http://github.com/jonathan-taylor/formula
+.. _current trunk: http://github.com/jonathan-taylor/formula/archives/master
+.. _available releases: http://github.com/jonathan-taylor/formula/downloads
+
+License
+=======
+
+formula is licensed under the terms of the BSD license.  Please the COPYING file
+in the formula distribution.
+
+.. vim:syntax=rst
diff --git a/formula/COMMIT_INFO.txt b/formula/COMMIT_INFO.txt
new file mode 100644
index 0000000..dcaee0b
--- /dev/null
+++ b/formula/COMMIT_INFO.txt
@@ -0,0 +1,6 @@
+# This is an ini file that may contain information about the code state
+[commit hash]
+# The line below may contain a valid hash if it has been substituted during 'git archive'
+archive_subst_hash=$Format:%h$
+# This line may be modified by the install process
+install_hash=
diff --git a/formula/__init__.py b/formula/__init__.py
index e69de29..ab820cb 100644
--- a/formula/__init__.py
+++ b/formula/__init__.py
@@ -0,0 +1,7 @@
+# Init for formula
+import os
+
+from .info import __version__, long_description as __doc__
+
+from .pkg_info import get_pkg_info as _get_pkg_info
+get_info = lambda : _get_pkg_info(os.path.dirname(__file__))
diff --git a/formula/info.py b/formula/info.py
index 2fbfc1e..d8c6d56 100644
--- a/formula/info.py
+++ b/formula/info.py
@@ -1,15 +1,16 @@
-""" This file contains defines parameters for nipy that we use to fill
-settings in setup.py, the nipy top-level docstring, and for building the
-docs.  In setup.py in particular, we exec this file, so it cannot import nipy
+""" This file contains defines parameters for formula that we use to fill
+settings in setup.py, the formula top-level docstring, and for building the
+docs.  In setup.py in particular, we exec this file, so it cannot import formula
 """
 
-# nipy version information.  An empty _version_extra corresponds to a
+# formula version information.  An empty _version_extra corresponds to a
 # full release.  '.dev' as a _version_extra string means this is a development
 # version
 _version_major = 0
-_version_minor = 0
-_version_micro = 1
+_version_minor = 1
+_version_micro = 0
 _version_extra = '.dev'
+# _version_extra = ''
 
 # Format expected by setup.py and doc/source/conf.py: string of form "X.Y.Z"
 __version__ = "%s.%s.%s%s" % (_version_major,
@@ -25,45 +26,80 @@
                "Programming Language :: Python",
                "Topic :: Scientific/Engineering"]
 
-description  = 'A package to build design matrices for regression models'
+description  = 'Symbolic statistical formulae'
 
 # Note: this long_description is actually a copy/paste from the top-level
 # README.txt, so that it shows up nicely on PyPI.  So please remember to edit
 # it only in one place and sync it correctly.
-long_description = \
-"""
+long_description = """
 =======
 Formula
 =======
 
-Formula is a python project for building
-design matrices for regression models.
+This package contains an implementation of symbolic statistical models in
+Python.
+
+The implementation is of a full algebra of factors and numerical variables in
+statistical models.
+
+It has some similarities to model formulae in R, but differs in that the rules
+for the creation of design matrices are more completely defined by the model
+algebra.
+
+An earlier version of this package is in nipy_.
+
+Mailing Lists
+=============
+
+Please see the developer's list here::
+
+    http://mail.scipy.org/mailman/listinfo/nipy-devel
+
+Code
+====
+
+You can find our sources and single-click downloads:
 
+* `Main repository`_ on Github.
+* Documentation_ for all releases and current development tree.
+* Download as a tar/zip file the `current trunk`_.
+* Downloads of all `available releases`_.
+
+.. _nipy: http://nipy.org
+.. _main repository: http://github.com/jonathan-taylor/formula
+.. _documentation: http://github.com/jonathan-taylor/formula
+.. _current trunk: http://github.com/jonathan-taylor/formula/archives/master
+.. _available releases: http://github.com/jonathan-taylor/formula/downloads
+
+License
+=======
+
+formula is licensed under the terms of the BSD license.  Please the COPYING file
+in the formula distribution.
 """
 
+# versions for dependencies
+NUMPY_MIN_VERSION='1.2'
+SYMPY_MIN_VERSION='0.7.0'
+
+# Main setup parameters
 NAME                = 'formula'
-MAINTAINER          = "formula developers"
-MAINTAINER_EMAIL    = ""
+MAINTAINER          = "Jonathan Taylor"
+MAINTAINER_EMAIL    = "nipy-devel@neuroimaging.scipy.org"
 DESCRIPTION         = description
 LONG_DESCRIPTION    = long_description
-URL                 = ""
-DOWNLOAD_URL        = ""
+URL                 = "http://github.com/jonathan-taylor/formula"
+DOWNLOAD_URL        = "http://github.com/jonathan-taylor/formula/downloads"
 LICENSE             = "BSD license"
 CLASSIFIERS         = CLASSIFIERS
-AUTHOR              = "formula developers"
-AUTHOR_EMAIL        = ""
+AUTHOR              = "Jonathan Taylor"
+AUTHOR_EMAIL        = "nipy-devel@neuroimaging.scipy.org"
 PLATFORMS           = "OS Independent"
 MAJOR               = _version_major
 MINOR               = _version_minor
 MICRO               = _version_micro
 ISRELEASE           = _version_extra == ''
 VERSION             = __version__
-REQUIRES            = ["numpy", "sympy"]
-STATUS              = 'alpha'
-
-# versions
-NUMPY_MIN_VERSION='1.3'
-SYMPY_MIN_VERSION = '0.6.7'
-
-
-
+PROVIDES            = ["formula"]
+REQUIRES            = ["numpy (>=%s)" % NUMPY_MIN_VERSION,
+                       "sympy (>=%s)" % SYMPY_MIN_VERSION]
diff --git a/formula/pkg_info.py b/formula/pkg_info.py
new file mode 100644
index 0000000..2ce4d23
--- /dev/null
+++ b/formula/pkg_info.py
@@ -0,0 +1,83 @@
+import os
+import sys
+import subprocess
+from ConfigParser import ConfigParser
+
+COMMIT_INFO_FNAME = 'COMMIT_INFO.txt'
+
+def pkg_commit_hash(pkg_path):
+    ''' Get short form of commit hash given directory `pkg_path`
+
+    There should be a file called 'COMMIT_INFO.txt' in `pkg_path`.  This is a
+    file in INI file format, with at least one section: ``commit hash``, and two
+    variables ``archive_subst_hash`` and ``install_hash``.  The first has a
+    substitution pattern in it which may have been filled by the execution of
+    ``git archive`` if this is an archive generated that way.  The second is
+    filled in by the installation, if the installation is from a git archive.
+
+    We get the commit hash from (in order of preference):
+
+    * A substituted value in ``archive_subst_hash``
+    * A written commit hash value in ``install_hash`
+    * git's output, if we are in a git repository
+
+    If all these fail, we return a not-found placeholder tuple
+
+    Parameters
+    ----------
+    pkg_path : str
+       directory containing package
+
+    Returns
+    -------
+    hash_from : str
+       Where we got the hash from - description
+    hash_str : str
+       short form of hash
+    '''
+    # Try and get commit from written commit text file
+    pth = os.path.join(pkg_path, COMMIT_INFO_FNAME)
+    if not os.path.isfile(pth):
+        raise IOError('Missing commit info file %s' % pth)
+    cfg_parser = ConfigParser()
+    cfg_parser.read(pth)
+    archive_subst = cfg_parser.get('commit hash', 'archive_subst_hash')
+    if not archive_subst.startswith('$Format'): # it has been substituted
+        return 'archive substitution', archive_subst
+    install_subst = cfg_parser.get('commit hash', 'install_hash')
+    if install_subst != '':
+        return 'installation', install_subst
+    # maybe we are in a repository
+    proc = subprocess.Popen('git rev-parse --short HEAD',
+                            stdout=subprocess.PIPE,
+                            stderr=subprocess.PIPE,
+                            cwd=pkg_path, shell=True)
+    repo_commit, _ = proc.communicate()
+    if repo_commit:
+        return 'repository', repo_commit.strip()
+    return '(none found)', '<not found>'
+
+
+def get_pkg_info(pkg_path):
+    ''' Return dict describing the context of this package
+
+    Parameters
+    ----------
+    pkg_path : str
+       path containing __init__.py for package
+
+    Returns
+    -------
+    context : dict
+       with named parameters of interest
+    '''
+    src, hsh = pkg_commit_hash(pkg_path)
+    import numpy
+    return dict(
+        pkg_path=pkg_path,
+        commit_source=src,
+        commit_hash=hsh,
+        sys_version=sys.version,
+        sys_executable=sys.executable,
+        sys_platform=sys.platform,
+        np_version=numpy.__version__)
diff --git a/formula/tests/__init__.py b/formula/tests/__init__.py
new file mode 100644
index 0000000..7a8947f
--- /dev/null
+++ b/formula/tests/__init__.py
@@ -0,0 +1 @@
+# Make tests a package
diff --git a/nisext/__init__.py b/nisext/__init__.py
new file mode 100644
index 0000000..2163c3b
--- /dev/null
+++ b/nisext/__init__.py
@@ -0,0 +1,28 @@
+# init for sext package
+""" Setuptools extensions that can be shared across projects
+
+Typical use for these routines is as a git subtree merge
+
+For example::
+
+    # Add a remote pointing to repository
+    git remote add nisext git://github.com/nipy/nisext.git
+    git fetch nisext
+    # Label nisext history as merged
+    git merge -s ours --no-commit nisext/master
+    # Read nisext contents as nisext subdirectory
+    git read-tree --prefix=nisext/ -u nisext/master
+    git commit -m "Merge nisext project as subtree"
+
+Then you would typically add a makefile target like::
+
+    # Update nisext subtree from remote
+    update-nisext:
+            git fetch nisext
+            git merge --squash -s subtree --no-commit nisext/master
+
+and commit when you have changes you want. This allows you to keep the nisext
+tree updated from the upstream repository, but the tree will be there and ready
+for someone without this machinery or remote.
+"""
+
diff --git a/nisext/sexts.py b/nisext/sexts.py
new file mode 100644
index 0000000..838bb42
--- /dev/null
+++ b/nisext/sexts.py
@@ -0,0 +1,141 @@
+''' Distutils / setuptools helpers '''
+
+from os.path import join as pjoin
+try:
+    from ConfigParser import ConfigParser
+except ImportError:
+    from configparser import ConfigParser
+
+
+from distutils.version import LooseVersion
+from distutils.command.build_py import build_py
+
+from distutils import log
+
+def get_comrec_build(pkg_dir, build_cmd=build_py):
+    """ Return extended build command class for recording commit
+
+    The extended command tries to run git to find the current commit, getting
+    the empty string if it fails.  It then writes the commit hash into a file
+    in the `pkg_dir` path, named ``COMMIT_INFO.txt``.
+
+    In due course this information can be used by the package after it is
+    installed, to tell you what commit it was installed from if known.
+
+    To make use of this system, you need a package with a COMMIT_INFO.txt file -
+    e.g. ``myproject/COMMIT_INFO.txt`` - that might well look like this::
+
+        # This is an ini file that may contain information about the code state
+        [commit hash]
+        # The line below may contain a valid hash if it has been substituted during 'git archive'
+        archive_subst_hash=$Format:%h$
+        # This line may be modified by the install process
+        install_hash=
+
+    The COMMIT_INFO file above is also designed to be used with git substitution
+    - so you probably also want a ``.gitattributes`` file in the root directory
+    of your working tree that contains something like this::
+
+       myproject/COMMIT_INFO.txt export-subst
+
+    That will cause the ``COMMIT_INFO.txt`` file to get filled in by ``git
+    archive`` - useful in case someone makes such an archive - for example with
+    via the github 'download source' button.
+
+    Although all the above will work as is, you might consider having something
+    like a ``get_info()`` function in your package to display the commit
+    information at the terminal.  See the ``pkg_info.py`` module in the nipy
+    package for an example.
+    """
+    class MyBuildPy(build_cmd):
+        ''' Subclass to write commit data into installation tree '''
+        def run(self):
+            build_cmd.run(self)
+            import subprocess
+            proc = subprocess.Popen('git rev-parse --short HEAD',
+                                    stdout=subprocess.PIPE,
+                                    stderr=subprocess.PIPE,
+                                    shell=True)
+            repo_commit, _ = proc.communicate()
+            # Fix for python 3
+            repo_commit = str(repo_commit)
+            # We write the installation commit even if it's empty
+            cfg_parser = ConfigParser()
+            cfg_parser.read(pjoin(pkg_dir, 'COMMIT_INFO.txt'))
+            cfg_parser.set('commit hash', 'install_hash', repo_commit)
+            out_pth = pjoin(self.build_lib, pkg_dir, 'COMMIT_INFO.txt')
+            cfg_parser.write(open(out_pth, 'wt'))
+    return MyBuildPy
+
+
+# Dependency checks
+def package_check(pkg_name, version=None,
+                  optional=False,
+                  checker=LooseVersion,
+                  version_getter=None,
+                  messages=None
+                  ):
+    ''' Check if package `pkg_name` is present, and correct version
+
+    Parameters
+    ----------
+    pkg_name : str
+       name of package as imported into python
+    version : {None, str}, optional
+       minimum version of the package that we require. If None, we don't
+       check the version.  Default is None
+    optional : {False, True}, optional
+       If False, raise error for absent package or wrong version;
+       otherwise warn
+    checker : callable, optional
+       callable with which to return comparable thing from version
+       string.  Default is ``distutils.version.LooseVersion``
+    version_getter : {None, callable}:
+       Callable that takes `pkg_name` as argument, and returns the
+       package version string - as in::
+
+          ``version = version_getter(pkg_name)``
+
+       If None, equivalent to::
+
+          mod = __import__(pkg_name); version = mod.__version__``
+    messages : None or dict, optional
+       dictionary giving output messages
+    '''
+    if version_getter is None:
+        def version_getter(pkg_name):
+            mod = __import__(pkg_name)
+            return mod.__version__
+    if messages is None:
+        messages = {}
+    msgs = {
+         'missing': 'Cannot import package "%s" - is it installed?',
+         'missing opt': 'Missing optional package "%s"',
+         'opt suffix' : '; you may get run-time errors',
+         'version too old': 'You have version %s of package "%s"'
+                            ' but we need version >= %s', }
+    msgs.update(messages)
+    try:
+        __import__(pkg_name)
+    except ImportError:
+        if not optional:
+            raise RuntimeError(msgs['missing'] % pkg_name)
+        log.warn(msgs['missing opt'] % pkg_name +
+                 msgs['opt suffix'])
+        return
+    if not version:
+        return
+    try:
+        have_version = version_getter(pkg_name)
+    except AttributeError:
+        raise RuntimeError('Cannot find version for %s' % pkg_name)
+    if checker(have_version) < checker(version):
+        if optional:
+            log.warn(msgs['version too old'] % (have_version,
+                                                pkg_name,
+                                                version)
+                     + msgs['opt suffix'])
+        else:
+            raise RuntimeError(msgs['version too old'] % (have_version,
+                                                          pkg_name,
+                                                          version))
diff --git a/nisext/testers.py b/nisext/testers.py
new file mode 100644
index 0000000..e51278f
--- /dev/null
+++ b/nisext/testers.py
@@ -0,0 +1,252 @@
+''' Test package information in various install settings
+
+The routines here install the package in various settings and print out the
+corresponding version info from the installation.
+
+The typical use for this module is as a makefile target, as in::
+
+    # Print out info for possible install methods
+    check-version-info:
+        python -c 'from nisext.testers import info_from_here; info_from_here("mypackage")'
+
+'''
+
+import os
+from os.path import join as pjoin
+from glob import glob
+import shutil
+import tempfile
+import zipfile
+from subprocess import call
+from functools import partial
+
+my_call = partial(call, shell=True)
+
+PY_LIB_SDIR = 'pylib'
+
+def run_mod_cmd(mod_name, pkg_path, cmd):
+    ''' Run command in own process in anonymous path
+
+    Parameters
+    ----------
+    mod_name : str
+        Name of module to import - e.g. 'nibabel'
+    pkg_path : str
+        directory containing `mod_name` package.  Typically that will be the
+        directory containing the e.g. 'nibabel' directory.
+    '''
+    cwd = os.getcwd()
+    tmpdir = tempfile.mkdtemp()
+    try:
+        os.chdir(tmpdir)
+        my_call('python -c "import sys; sys.path.insert(1,\'%s\'); '
+                'import %s;'
+                'print %s.__file__;'
+                '%s"' % (pkg_path,
+                         mod_name,
+                         mod_name,
+                         cmd))
+    finally:
+        os.chdir(cwd)
+        shutil.rmtree(tmpdir)
+
+
+def zip_extract_all(fname, path=None):
+    ''' Extract all members from zipfile
+
+    Deals with situation where the directory is stored in the zipfile as a name,
+    as well as files that have to go into this directory.
+    '''
+    zf = zipfile.ZipFile(fname)
+    members = zf.namelist()
+    # Remove members that are just bare directories
+    members = [m for m in members if not m.endswith('/')]
+    for zipinfo in members:
+        zf.extract(zipinfo, path, None)
+
+
+def install_from_to(from_dir, to_dir, py_lib_sdir):
+    """ Install package in `from_dir` to standard location in `to_dir`
+
+    Return path to directory containing package directory. The package directory
+    is the directory containing __init__.py
+    """
+    site_pkgs_path = os.path.join(to_dir, py_lib_sdir)
+    py_lib_locs = ' --install-purelib=%s --install-platlib=%s' % (
+        site_pkgs_path, site_pkgs_path)
+    pwd = os.path.abspath(os.getcwd())
+    try:
+        os.chdir(from_dir)
+        my_call('python setup.py --quiet install --prefix=%s %s' % (to_dir,
+                                                                    py_lib_locs))
+    finally:
+        os.chdir(pwd)
+    return site_pkgs_path
+
+
+def check_installed_files(repo_mod_path, install_mod_path):
+    """ Check files in `repo_mod_path` are installed at `install_mod_path`
+
+    At the moment, all this does is check that all the ``*.py`` files in
+    `repo_mod_path` are installed at `install_mod_path`.
+
+    Parameters
+    ----------
+    repo_mod_path : str
+        repository path containing package files, e.g. <nibabel-repo>/nibabel>
+    install_mod_path : str
+        path at which package has been installed.  This is the path where the
+        root package ``__init__.py`` lives.
+
+    Return
+    ------
+    uninstalled : list
+        list of files that should have been installed, but have not been
+        installed
+    """
+    repo_mod_path = os.path.abspath(repo_mod_path)
+    uninstalled = []
+    # Walk directory tree to get py files
+    for dirpath, dirnames, filenames in os.walk(repo_mod_path):
+        out_dirpath = dirpath.replace(repo_mod_path, install_mod_path)
+        for fname in filenames:
+            if not fname.lower().endswith('.py'):
+                continue
+            equiv_fname = os.path.join(out_dirpath, fname)
+            if not os.path.isfile(equiv_fname):
+                uninstalled.append(pjoin(dirpath, fname))
+    return uninstalled
+
+
+def contexts_print_info(mod_name, repo_path, install_path):
+    ''' Print result of get_info from different installation routes
+
+    Runs installation from:
+
+    * git archive zip file
+    * with setup.py install from repository directory
+    * just running code from repository directory
+
+    and prints out result of get_info in each case.  There will be many files
+    written into `install_path` that you may want to clean up somehow.
+
+    Parameters
+    ----------
+    mod_name : str
+       package name that will be installed, and tested
+    repo_path : str
+       path to location of git repository
+    install_path : str
+       path into which to install temporary installations
+    '''
+    site_pkgs_path = os.path.join(install_path, PY_LIB_SDIR)
+    # first test archive
+    pwd = os.path.abspath(os.getcwd())
+    out_fname = pjoin(install_path, 'test.zip')
+    try:
+        os.chdir(repo_path)
+        my_call('git archive --format zip -o %s HEAD' % out_fname)
+    finally:
+        os.chdir(pwd)
+    install_from = pjoin(install_path, mod_name)
+    zip_extract_all(out_fname, install_from)
+    site_pkgs_path = install_from_to(install_from,
+                                     install_path,
+                                     PY_LIB_SDIR)
+    cmd_str = 'print %s.get_info()' % mod_name
+    run_mod_cmd(mod_name, site_pkgs_path, cmd_str)
+    # now test install into a directory from the repository
+    site_pkgs_path = install_from_to(repo_path,
+                                     install_path,
+                                     PY_LIB_SDIR)
+    run_mod_cmd(mod_name, site_pkgs_path, cmd_str)
+    # Take the opportunity to audit the py files
+    repo_mod_path = os.path.join(repo_path, mod_name)
+    install_mod_path = os.path.join(site_pkgs_path, mod_name)
+    print 'Files not taken across by the installation:'
+    print check_installed_files(repo_mod_path, install_mod_path)
+    # test from development tree
+    run_mod_cmd(mod_name, repo_path, cmd_str)
+    return
+
+
+def info_from_here(mod_name):
+    ''' Run info context checks starting in working directory
+
+    Runs checks from current working directory, installing temporary
+    installations into a new temporary directory
+
+    Parameters
+    ----------
+    mod_name : str
+       package name that will be installed, and tested
+    '''
+    repo_path = os.path.abspath(os.getcwd())
+    install_path = tempfile.mkdtemp()
+    try:
+        contexts_print_info(mod_name, repo_path, install_path)
+    finally:
+        shutil.rmtree(install_path)
+
+
+def tests_installed(mod_name, source_path=None):
+    """ Install from `source_path` into temporary directory; run tests
+
+    Parameters
+    ----------
+    mod_name : str
+        name of module - e.g. 'nibabel'
+    source_path : None or str
+        Path from which to install.  If None, defaults to working directory
+    """
+    if source_path is None:
+        source_path = os.path.abspath(os.getcwd())
+    install_path = tempfile.mkdtemp()
+    try:
+        site_pkgs_path = install_from_to(source_path,
+                                         install_path,
+                                         PY_LIB_SDIR)
+        run_mod_cmd(mod_name, site_pkgs_path, mod_name + '.test()')
+    finally:
+        shutil.rmtree(install_path)
+
+# Tell nose this is not a test
+tests_installed.__test__ = False
+
+
+def tests_from_zip(mod_name, zip_fname):
+    """ Runs test from sdist zip source archive """
+    install_path = tempfile.mkdtemp()
+    try:
+        zip_extract_all(zip_fname, install_path)
+        pkg_dirs = glob(pjoin(install_path, mod_name + "*"))
+        if len(pkg_dirs) != 1:
+            raise OSError('There must be one and only one package dir')
+        pkg_contents = pjoin(install_path, pkg_dirs[0])
+        tests_installed(mod_name, pkg_contents)
+    finally:
+        shutil.rmtree(install_path)
+
+tests_from_zip.__test__ = False
+
+
+def sdist_tests(mod_name, repo_path=None):
+    """ Make sdist zip, install from it, and run tests """
+    pwd = os.path.abspath(os.getcwd())
+    if repo_path is None:
+        repo_path = pwd
+    install_path = tempfile.mkdtemp()
+    try:
+        os.chdir(repo_path)
+        my_call('python setup.py sdist --formats=zip --dist-dir='
+                + install_path)
+        zip_fnames = glob(pjoin(install_path, mod_name + "*.zip"))
+        if len(zip_fnames) != 1:
+            raise OSError('There must be one and only one zip file, '
+                          'but I found "%s"' % ': '.join(zip_fnames))
+        tests_from_zip(mod_name, zip_fnames[0])
+    finally:
+        os.chdir(pwd)
+        shutil.rmtree(install_path)
+
+sdist_tests.__test__ = False
diff --git a/setup.py b/setup.py
index bb0a1c8..7b25aae 100755
--- a/setup.py
+++ b/setup.py
@@ -1,28 +1,84 @@
 """
-A package to solve separable convex optimization problems, based on 
+Symbolic formulae for statistical models
 """
 
-import os, sys
-import string
+#!/usr/bin/env python
+# emacs: -*- mode: python-mode; py-indent-offset: 4; indent-tabs-mode: nil -*-
+# vi: set ft=python sts=4 ts=4 sw=4 et:
+### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##
+#
+#   See COPYING file distributed along with the NiBabel package for the
+#   copyright and license terms.
+#
+### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##
+"""Build helper."""
 
-from Cython.Compiler import Main
+import os
+from os.path import join as pjoin
+import sys
 
-def cython_extension(srcfile):
-    options = Main.CompilationOptions(include_path=[os.path.join(os.path.abspath(os.path.dirname(__file__)), 'include')])
-    Main.compile(srcfile, options=options)
+# BEFORE importing distutils, remove MANIFEST. distutils doesn't properly
+# update it when the contents of directories change.
+if os.path.exists('MANIFEST'): os.remove('MANIFEST')
 
-def configuration(parent_package='',top_path=None):
-    from numpy.distutils.misc_util import Configuration
-    config = Configuration(None,parent_package,top_path)
+from distutils.core import setup
 
-    config.add_subpackage('formula')
-    config.add_data_dir("formula/data")
-    return config
+# For some commands, use setuptools.  
+if len(set(('develop', 'bdist_egg', 'bdist_rpm', 'bdist', 'bdist_dumb',
+            'bdist_wininst', 'install_egg_info', 'egg_info', 'easy_install',
+            )).intersection(sys.argv)) > 0:
+    # setup_egg imports setuptools setup, thus monkeypatching distutils. 
+    import setup_egg
 
-if __name__ == '__main__':
+from nisext.sexts import get_comrec_build, package_check
+cmdclass = {'build_py': get_comrec_build('formula')}
 
-    from numpy.distutils.core import setup
+# Get version and release info, which is all stored in formula/info.py
+ver_file = os.path.join('formula', 'info.py')
+execfile(ver_file)
 
-    c = configuration(top_path='',
-                      ).todict()
-    setup(**c)
+# Do dependency checking
+package_check('numpy', NUMPY_MIN_VERSION)
+package_check('sympy', SYMPY_MIN_VERSION)
+extra_setuptools_args = {}
+if 'setuptools' in sys.modules:
+    extra_setuptools_args['extras_require'] = dict(
+        doc='Sphinx>=0.3',
+        test='nose>=0.10.1',
+    )
+
+def main(**extra_args):
+    setup(name=NAME,
+          maintainer=MAINTAINER,
+          maintainer_email=MAINTAINER_EMAIL,
+          description=DESCRIPTION,
+          long_description=LONG_DESCRIPTION,
+          url=URL,
+          download_url=DOWNLOAD_URL,
+          license=LICENSE,
+          classifiers=CLASSIFIERS,
+          author=AUTHOR,
+          author_email=AUTHOR_EMAIL,
+          platforms=PLATFORMS,
+          version=VERSION,
+          requires=REQUIRES,
+          provides=PROVIDES,
+          packages     = ['formula',
+                          'formula.tests'],
+          # The package_data spec has no effect for me (on python 2.6) -- even
+          # changing to data_files doesn't get this stuff included in the source
+          # distribution -- not sure if it has something to do with the magic
+          # above, but distutils is surely the worst piece of code in all of
+          # python -- duplicating things into MANIFEST.in but this is admittedly
+          # only a workaround to get things started -- not a solution
+          package_data = {'formula':
+                          [pjoin('data', '*'),
+                          ]},
+          scripts      = [],
+          cmdclass = cmdclass,
+          **extra_args
+         )
+
+
+if __name__ == "__main__":
+    main(**extra_setuptools_args)
diff --git a/setup_egg.py b/setup_egg.py
new file mode 100644
index 0000000..48aafbc
--- /dev/null
+++ b/setup_egg.py
@@ -0,0 +1,13 @@
+#!/usr/bin/env python
+# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*-
+# vi: set ft=python sts=4 ts=4 sw=4 et:
+"""Wrapper to run setup.py using setuptools."""
+
+import setuptools
+
+################################################################################
+# Call the setup.py script, injecting the setuptools-specific arguments.
+
+if __name__ == '__main__':
+    exec(open('setup.py', 'rt').read(), dict(__name__='__main__'))
+