Commits

Ronald Oussoren committed b1214b5

* Small tweaks to build_framework
* Run_tests now actually does something useful

The run_test script only runs the 2.6 and 2.7 tests
for now, I once again ran into what seems like distribute and/or 2to3
related issues when testing using python 3.x.

The run_test script generates an HTML file with a report
of the build and test results. This is basicly a summary
with the number of problems for every project and some
coloring to make it easier to check the state of the project
at a glance.

Comments (0)

Files changed (5)

build-support/Makefile

+default:
+	@echo "Usage:"
+	@echo " - build: Create test frameworks"
+	@echo " - test:  Build PyObjC and run tests"
+	@echo
+	@echo "WARNING: 'test' doesn't automaticly 'build'"
+	@echo
+
+build:
+	python3	build_frameworks.py
+
+test:
+	python3 run_tests.py
+
+.PHONY: default build test clean

build-support/build_frameworks.py

 """
 Script that builds a number of python frameworks as
 used by the run_tests.py script
+
+FIXME:
+- Both variants need to be build with simular options
+  to the official builds: 32-bit with SDK 10.4u and deployment
+  target 10.3, 3-way without SDK and depl. target 10.5.
+
+  This will have to wait until my sdkroot patches get committed,
+  without that patch I cannot build the 32-bit variant on 
+  SL.
+- get rid of the global variables
 """
-import subprocess, getopt, logging, os, sys, shutil
+import sys
+sys.dont_write_bytecode = True
+
+import subprocess, getopt, logging, os, shutil
 from urllib.request import urlopen
 
+
 gUsage="""\
 build_frameworks.py [-v versions] [--versions=versions] [-a archs] [--arch archs]
 
 - versions: comma seperated list of python versiosn, defaults to "2.6,2.7,3.1,3.2"
-- archs: comma seperated list of build variations, defaults to "32-bit,intel"
+- archs: comma seperated list of build variations, defaults to "32-bit,3-way"
 """
 
 gBaseDir = os.path.dirname(os.path.abspath(__file__))
 
-gArchs = ("32-bit", "intel")
+gArchs = ("32-bit", "3-way")
 
+
+# Name of the Python framework and any additional arguments
+# passed to the configure command.
+gFrameworkNameTemplate="DbgPython-{archs}"
+gExtraConfigureArgs=[
+    "--with-pydebug",
+]
+
+# Location of the SVN branches to be used
 gURLMap = {
     '2.6': 'http://svn.python.org/projects/python/branches/release26-maint',
     '2.7': 'http://svn.python.org/projects/python/trunk',
 }
 
 
+# Name of the OSX SDK used to build the framework, keyed of the architecture
+# variant.
+gSdkMap={
+    '32-bit': '/',
+    '3-way': '/',
+}
+
+# Name of the OSX Deployment Target used to build the framework, keyed of 
+# the architecture variant.
+gDeploymentTargetMap={
+    #'32-bit': '10.4',
+    '32-bit': '10.5',
+    '3-way': '10.5',
+}
+
+
+
 class ShellError (Exception):
+    """ An error occurred while running a shell command """
     pass
 
 def create_checkout(version):
+    """
+    Create or update the checkout of the given version
+    of Python.
+    """
     lg = logging.getLogger("create_checkout")
     lg.info("Create checkout for %s", version)
 
         raise ShellError(xit)
 
 def build_framework(version, archs):
+    """
+    Build the given version of Python in the given architecture
+    variant. 
+
+    This also installs distribute and virtualenv (the latter using
+    a local copy of the package).
+    """
     lg = logging.getLogger("build_framework")
     lg.info("Build framework version=%r archs=%r", version, archs)
 
     builddir = os.path.join(gBaseDir, "checkouts", version, "build")
-#    if os.path.exists(builddir):
-#        lg.debug("Remove existing build tree")
-#        shutil.rmtree(builddir)
-#
-#    lg.debug("Create build tree %r", builddir)
-#    os.mkdir(builddir)
-#
-#    lg.debug("Running 'configure'")
-#    p = subprocess.Popen([
-#        "../configure",
-#            "--enable-framework",
-#            "--with-framework-name=DbgPython-{0}".format(archs),
-#            "--enable-universalsdk=/",
-#            "--with-universal-archs={0}".format(archs),
-#            "--with-pydebug",
-#            "MACOSX_DEPLOYMENT_TARGET=10.6",
-#        ], cwd=builddir)
-#
-#    xit = p.wait()
-#    if xit != 0:
-#        lg.debug("Configure failed for %s", version)
-#        raise ShellError(xit)
-#    
-#    lg.debug("Running 'make'")
-#    p = subprocess.Popen([
-#            "make",
-#        ], cwd=builddir)
-#
-#    xit = p.wait()
-#    if xit != 0:
-#        lg.debug("Make failed for %s", version)
-#        raise ShellError(xit)
-#
-#    lg.debug("Running 'make install'")
-#    p = subprocess.Popen([
-#            "make",
-#            "install",
-#        ], cwd=builddir)
-#
-#    xit = p.wait()
-#    if xit != 0:
-#        lg.debug("Install failed for %r", version)
-#        raise ShellError(xit)
-#
+    if os.path.exists(builddir):
+        lg.debug("Remove existing build tree")
+        shutil.rmtree(builddir)
+
+    lg.debug("Create build tree %r", builddir)
+    os.mkdir(builddir)
+
+    lg.debug("Running 'configure'")
+    p = subprocess.Popen([
+        "../configure",
+            "--enable-framework",
+            "--with-framework-name={0}".format(gFrameworkNameTemplate.format(version=version, archs=archs)),
+            "--enable-universalsdk={0}".format(gSdkMap[archs]),
+            "--with-universal-archs={0}".format(archs),
+            ] + gExtraConfigureArgs + [
+            "MACOSX_DEPLOYMENT_TARGET={0}".format(gDeploymentTargetMap[archs]),
+            ], cwd=builddir)
+
+    xit = p.wait()
+    if xit != 0:
+        lg.debug("Configure failed for %s", version)
+        raise ShellError(xit)
+    
+    lg.debug("Running 'make'")
+    p = subprocess.Popen([
+            "make",
+        ], cwd=builddir)
+
+    xit = p.wait()
+    if xit != 0:
+        lg.debug("Make failed for %s", version)
+        raise ShellError(xit)
+
+    lg.debug("Running 'make install'")
+    p = subprocess.Popen([
+            "make",
+            "install",
+        ], cwd=builddir)
+
+    xit = p.wait()
+    if xit != 0:
+        lg.debug("Install failed for %r", version)
+        raise ShellError(xit)
+
+def install_distribute(version, archs):
+    lg = logging.getLogger("install_distribute")
     lg.debug("Installing distribute")
 
+    builddir = os.path.join(gBaseDir, "checkouts", version, "build")
+
     lg.debug("Download distribute_setup script")
     fd = urlopen("http://python-distribute.org/distribute_setup.py")
     data = fd.read()
     fd.write(data)
     fd.close()
 
-    python = "/Library/Frameworks/DbgPython-{0}.framework/Versions/{1}/bin/python".format(
-            archs, version)
+    frameworkName=gFrameworkNameTemplate.format(archs=archs, version=version)
+
+    python = "/Library/Frameworks/{0}.framework/Versions/{1}/bin/python".format(
+            frameworkName, version)
     if version[0] == '3':
         python += '3'
 
             lg.warning("Running 2to3 failed")
             raise ShellError(xit)
 
-    lg.debug("Run distribute_setup script")
+    lg.debug("Run distribute_setup script '%s' with '%s'", scriptfn, python)
     p = subprocess.Popen([
         python,
-        scriptfn])
+        scriptfn],
+        cwd=os.path.join(gBaseDir, "checkouts"))
     xit = p.wait()
     if xit != 0:
         lg.warning("Installing 'distribute' failed")
         raise ShellError(xit)
 
-    lg.debug("Installing virtualenv")
+
+def install_virtualenv(version, archs):
+    lg = logging.getLogger("install_virtualenv")
+
+    lg.info("Installing virtualenv from local source")
+
+    frameworkName=gFrameworkNameTemplate.format(archs=archs, version=version)
+
+    python = "/Library/Frameworks/{0}.framework/Versions/{1}/bin/python".format(
+            frameworkName, version)
+    if version[0] == '3':
+        python += '3'
 
     # Sadly enough plain virtualenv doens't support 
     # python3 yet, but there is a fork that does.
 
     xit = p.wait()
     if xit != 0:
-        lg.warning("Installing 'distribute' failed")
+        lg.warning("Installing 'virtualenv' failed")
         raise ShellError(xit)
 
-    lg.info("Installation of %r done", version)
 
 def main():
     logging.basicConfig(level=logging.DEBUG)
         print(gUsage, file=sys.stderr)
         sys.exit(1)
 
+
+
     for k, v in opts:
         if k in ('-h', '-?', '--help'):
             print(gUsage)
                     file=sys.stderr)
             sys.exit(2)
 
+    lg = logging.getLogger("build_frameworks")
+    lg.info("Building versions: %s", versions)
+    lg.info("Building architectures: %s", archs)
     try:
         for version in sorted(versions):
             create_checkout(version)
 
             for arch in sorted(archs):
+                lg.info('Building framework for python %s (%s)', version, arch)
                 build_framework(version, arch)
+                lg.info('Installing distribute for python %s (%s)', version, arch)
+                install_distribute(version, arch)
+                lg.info('Installing virtualenv for python %s (%s)', version, arch)
+                install_virtualenv(version, arch)
+                lg.info('Done python %s (%s)', version, arch)
     
     except ShellError:
         sys.exit(1)

build-support/distribute_setup.py

+#!python
+"""Bootstrap distribute installation
+
+If you want to use setuptools in your package's setup.py, just include this
+file in the same directory with it, and add this to the top of your setup.py::
+
+    from distribute_setup import use_setuptools
+    use_setuptools()
+
+If you want to require a specific version of setuptools, set a download
+mirror, or use an alternate download directory, you can do so by supplying
+the appropriate options to ``use_setuptools()``.
+
+This file can also be run as a script to install or upgrade setuptools.
+"""
+import os
+import sys
+import time
+import fnmatch
+import tempfile
+import tarfile
+from distutils import log
+
+try:
+    from site import USER_SITE
+except ImportError:
+    USER_SITE = None
+
+try:
+    import subprocess
+
+    def _python_cmd(*args):
+        args = (sys.executable,) + args
+        return subprocess.call(args) == 0
+
+except ImportError:
+    # will be used for python 2.3
+    def _python_cmd(*args):
+        args = (sys.executable,) + args
+        # quoting arguments if windows
+        if sys.platform == 'win32':
+            def quote(arg):
+                if ' ' in arg:
+                    return '"%s"' % arg
+                return arg
+            args = [quote(arg) for arg in args]
+        return os.spawnl(os.P_WAIT, sys.executable, *args) == 0
+
+DEFAULT_VERSION = "0.6.8"
+DEFAULT_URL = "http://pypi.python.org/packages/source/d/distribute/"
+SETUPTOOLS_PKG_INFO = """\
+Metadata-Version: 1.0
+Name: setuptools
+Version: 0.6c9
+Summary: xxxx
+Home-page: xxx
+Author: xxx
+Author-email: xxx
+License: xxx
+Description: xxx
+"""
+
+
+def _install(tarball):
+    # extracting the tarball
+    tmpdir = tempfile.mkdtemp()
+    log.warn('Extracting in %s', tmpdir)
+    old_wd = os.getcwd()
+    try:
+        os.chdir(tmpdir)
+        tar = tarfile.open(tarball)
+        _extractall(tar)
+        tar.close()
+
+        # going in the directory
+        subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0])
+        os.chdir(subdir)
+        log.warn('Now working in %s', subdir)
+
+        # installing
+        log.warn('Installing Distribute')
+        assert _python_cmd('setup.py', 'install')
+    finally:
+        os.chdir(old_wd)
+
+
+def _build_egg(egg, tarball, to_dir):
+    # extracting the tarball
+    tmpdir = tempfile.mkdtemp()
+    log.warn('Extracting in %s', tmpdir)
+    old_wd = os.getcwd()
+    try:
+        os.chdir(tmpdir)
+        tar = tarfile.open(tarball)
+        _extractall(tar)
+        tar.close()
+
+        # going in the directory
+        subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0])
+        os.chdir(subdir)
+        log.warn('Now working in %s', subdir)
+
+        # building an egg
+        log.warn('Building a Distribute egg in %s', to_dir)
+        _python_cmd('setup.py', '-q', 'bdist_egg', '--dist-dir', to_dir)
+
+    finally:
+        os.chdir(old_wd)
+    # returning the result
+    log.warn(egg)
+    if not os.path.exists(egg):
+        raise IOError('Could not build the egg.')
+
+
+def _do_download(version, download_base, to_dir, download_delay):
+    egg = os.path.join(to_dir, 'distribute-%s-py%d.%d.egg'
+                       % (version, sys.version_info[0], sys.version_info[1]))
+    if not os.path.exists(egg):
+        tarball = download_setuptools(version, download_base,
+                                      to_dir, download_delay)
+        _build_egg(egg, tarball, to_dir)
+    sys.path.insert(0, egg)
+    import setuptools
+    setuptools.bootstrap_install_from = egg
+
+
+def use_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL,
+                   to_dir=os.curdir, download_delay=15, no_fake=True):
+    # making sure we use the absolute path
+    to_dir = os.path.abspath(to_dir)
+    was_imported = 'pkg_resources' in sys.modules or \
+        'setuptools' in sys.modules
+    try:
+        try:
+            import pkg_resources
+            if not hasattr(pkg_resources, '_distribute'):
+                if not no_fake:
+                    _fake_setuptools()
+                raise ImportError
+        except ImportError:
+            return _do_download(version, download_base, to_dir, download_delay)
+        try:
+            pkg_resources.require("distribute>="+version)
+            return
+        except pkg_resources.VersionConflict:
+            e = sys.exc_info()[1]
+            if was_imported:
+                sys.stderr.write(
+                "The required version of distribute (>=%s) is not available,\n"
+                "and can't be installed while this script is running. Please\n"
+                "install a more recent version first, using\n"
+                "'easy_install -U distribute'."
+                "\n\n(Currently using %r)\n" % (version, e.args[0]))
+                sys.exit(2)
+            else:
+                del pkg_resources, sys.modules['pkg_resources']    # reload ok
+                return _do_download(version, download_base, to_dir,
+                                    download_delay)
+        except pkg_resources.DistributionNotFound:
+            return _do_download(version, download_base, to_dir,
+                                download_delay)
+    finally:
+        if not no_fake:
+            _create_fake_setuptools_pkg_info(to_dir)
+
+def download_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL,
+                        to_dir=os.curdir, delay=15):
+    """Download distribute from a specified location and return its filename
+
+    `version` should be a valid distribute version number that is available
+    as an egg for download under the `download_base` URL (which should end
+    with a '/'). `to_dir` is the directory where the egg will be downloaded.
+    `delay` is the number of seconds to pause before an actual download
+    attempt.
+    """
+    # making sure we use the absolute path
+    to_dir = os.path.abspath(to_dir)
+    try:
+        from urllib.request import urlopen
+    except ImportError:
+        from urllib2 import urlopen
+    tgz_name = "distribute-%s.tar.gz" % version
+    url = download_base + tgz_name
+    saveto = os.path.join(to_dir, tgz_name)
+    src = dst = None
+    if not os.path.exists(saveto):  # Avoid repeated downloads
+        try:
+            log.warn("Downloading %s", url)
+            src = urlopen(url)
+            # Read/write all in one block, so we don't create a corrupt file
+            # if the download is interrupted.
+            data = src.read()
+            dst = open(saveto, "wb")
+            dst.write(data)
+        finally:
+            if src:
+                src.close()
+            if dst:
+                dst.close()
+    return os.path.realpath(saveto)
+
+
+def _patch_file(path, content):
+    """Will backup the file then patch it"""
+    existing_content = open(path).read()
+    if existing_content == content:
+        # already patched
+        log.warn('Already patched.')
+        return False
+    log.warn('Patching...')
+    _rename_path(path)
+    f = open(path, 'w')
+    try:
+        f.write(content)
+    finally:
+        f.close()
+    return True
+
+
+def _same_content(path, content):
+    return open(path).read() == content
+
+
+def _rename_path(path):
+    new_name = path + '.OLD.%s' % time.time()
+    log.warn('Renaming %s into %s', path, new_name)
+    try:
+        from setuptools.sandbox import DirectorySandbox
+        def _violation(*args):
+            pass
+        DirectorySandbox._violation = _violation
+    except ImportError:
+        pass
+
+    os.rename(path, new_name)
+    return new_name
+
+
+def _remove_flat_installation(placeholder):
+    if not os.path.isdir(placeholder):
+        log.warn('Unkown installation at %s', placeholder)
+        return False
+    found = False
+    for file in os.listdir(placeholder):
+        if fnmatch.fnmatch(file, 'setuptools*.egg-info'):
+            found = True
+            break
+    if not found:
+        log.warn('Could not locate setuptools*.egg-info')
+        return
+
+    log.warn('Removing elements out of the way...')
+    pkg_info = os.path.join(placeholder, file)
+    if os.path.isdir(pkg_info):
+        patched = _patch_egg_dir(pkg_info)
+    else:
+        patched = _patch_file(pkg_info, SETUPTOOLS_PKG_INFO)
+
+    if not patched:
+        log.warn('%s already patched.', pkg_info)
+        return False
+    # now let's move the files out of the way
+    for element in ('setuptools', 'pkg_resources.py', 'site.py'):
+        element = os.path.join(placeholder, element)
+        if os.path.exists(element):
+            _rename_path(element)
+        else:
+            log.warn('Could not find the %s element of the '
+                     'Setuptools distribution', element)
+    return True
+
+
+def _after_install(dist):
+    log.warn('After install bootstrap.')
+    placeholder = dist.get_command_obj('install').install_purelib
+    _create_fake_setuptools_pkg_info(placeholder)
+
+def _create_fake_setuptools_pkg_info(placeholder):
+    if not placeholder or not os.path.exists(placeholder):
+        log.warn('Could not find the install location')
+        return
+    pyver = '%s.%s' % (sys.version_info[0], sys.version_info[1])
+    setuptools_file = 'setuptools-0.6c9-py%s.egg-info' % pyver
+    pkg_info = os.path.join(placeholder, setuptools_file)
+    if os.path.exists(pkg_info):
+        log.warn('%s already exists', pkg_info)
+        return
+    log.warn('Creating %s', pkg_info)
+    f = open(pkg_info, 'w')
+    try:
+        f.write(SETUPTOOLS_PKG_INFO)
+    finally:
+        f.close()
+    pth_file = os.path.join(placeholder, 'setuptools.pth')
+    log.warn('Creating %s', pth_file)
+    f = open(pth_file, 'w')
+    try:
+        f.write(os.path.join(os.curdir, setuptools_file))
+    finally:
+        f.close()
+
+
+def _patch_egg_dir(path):
+    # let's check if it's already patched
+    pkg_info = os.path.join(path, 'EGG-INFO', 'PKG-INFO')
+    if os.path.exists(pkg_info):
+        if _same_content(pkg_info, SETUPTOOLS_PKG_INFO):
+            log.warn('%s already patched.', pkg_info)
+            return False
+    _rename_path(path)
+    os.mkdir(path)
+    os.mkdir(os.path.join(path, 'EGG-INFO'))
+    pkg_info = os.path.join(path, 'EGG-INFO', 'PKG-INFO')
+    f = open(pkg_info, 'w')
+    try:
+        f.write(SETUPTOOLS_PKG_INFO)
+    finally:
+        f.close()
+    return True
+
+
+def _before_install():
+    log.warn('Before install bootstrap.')
+    _fake_setuptools()
+
+
+def _under_prefix(location):
+    if 'install' not in sys.argv:
+        return True
+    args = sys.argv[sys.argv.index('install')+1:]
+    for index, arg in enumerate(args):
+        for option in ('--root', '--prefix'):
+            if arg.startswith('%s=' % option):
+                top_dir = arg.split('root=')[-1]
+                return location.startswith(top_dir)
+            elif arg == option:
+                if len(args) > index:
+                    top_dir = args[index+1]
+                    return location.startswith(top_dir)
+            elif option == '--user' and USER_SITE is not None:
+                return location.startswith(USER_SITE)
+    return True
+
+
+def _fake_setuptools():
+    log.warn('Scanning installed packages')
+    try:
+        import pkg_resources
+    except ImportError:
+        # we're cool
+        log.warn('Setuptools or Distribute does not seem to be installed.')
+        return
+    ws = pkg_resources.working_set
+    try:
+        setuptools_dist = ws.find(pkg_resources.Requirement.parse('setuptools',
+                                  replacement=False))
+    except TypeError:
+        # old distribute API
+        setuptools_dist = ws.find(pkg_resources.Requirement.parse('setuptools'))
+
+    if setuptools_dist is None:
+        log.warn('No setuptools distribution found')
+        return
+    # detecting if it was already faked
+    setuptools_location = setuptools_dist.location
+    log.warn('Setuptools installation detected at %s', setuptools_location)
+
+    # if --root or --preix was provided, and if
+    # setuptools is not located in them, we don't patch it
+    if not _under_prefix(setuptools_location):
+        log.warn('Not patching, --root or --prefix is installing Distribute'
+                 ' in another location')
+        return
+
+    # let's see if its an egg
+    if not setuptools_location.endswith('.egg'):
+        log.warn('Non-egg installation')
+        res = _remove_flat_installation(setuptools_location)
+        if not res:
+            return
+    else:
+        log.warn('Egg installation')
+        pkg_info = os.path.join(setuptools_location, 'EGG-INFO', 'PKG-INFO')
+        if (os.path.exists(pkg_info) and
+            _same_content(pkg_info, SETUPTOOLS_PKG_INFO)):
+            log.warn('Already patched.')
+            return
+        log.warn('Patching...')
+        # let's create a fake egg replacing setuptools one
+        res = _patch_egg_dir(setuptools_location)
+        if not res:
+            return
+    log.warn('Patched done.')
+    _relaunch()
+
+
+def _relaunch():
+    log.warn('Relaunching...')
+    # we have to relaunch the process
+    args = [sys.executable] + sys.argv
+    sys.exit(subprocess.call(args))
+
+
+def _extractall(self, path=".", members=None):
+    """Extract all members from the archive to the current working
+       directory and set owner, modification time and permissions on
+       directories afterwards. `path' specifies a different directory
+       to extract to. `members' is optional and must be a subset of the
+       list returned by getmembers().
+    """
+    import copy
+    import operator
+    from tarfile import ExtractError
+    directories = []
+
+    if members is None:
+        members = self
+
+    for tarinfo in members:
+        if tarinfo.isdir():
+            # Extract directories with a safe mode.
+            directories.append(tarinfo)
+            tarinfo = copy.copy(tarinfo)
+            tarinfo.mode = 448 # decimal for oct 0700
+        self.extract(tarinfo, path)
+
+    # Reverse sort directories.
+    if sys.version_info < (2, 4):
+        def sorter(dir1, dir2):
+            return cmp(dir1.name, dir2.name)
+        directories.sort(sorter)
+        directories.reverse()
+    else:
+        directories.sort(key=operator.attrgetter('name'), reverse=True)
+
+    # Set correct owner, mtime and filemode on directories.
+    for tarinfo in directories:
+        dirpath = os.path.join(path, tarinfo.name)
+        try:
+            self.chown(tarinfo, dirpath)
+            self.utime(tarinfo, dirpath)
+            self.chmod(tarinfo, dirpath)
+        except ExtractError:
+            e = sys.exc_info()[1]
+            if self.errorlevel > 1:
+                raise
+            else:
+                self._dbg(1, "tarfile: %s" % e)
+
+
+def main(argv, version=DEFAULT_VERSION):
+    """Install or upgrade setuptools and EasyInstall"""
+    tarball = download_setuptools()
+    _install(tarball)
+
+
+if __name__ == '__main__':
+    main(sys.argv[1:])

build-support/run_tests.py

 - there are python frameworks for the various architectures: DbgPython-VARIANT.framework
 - those frameworks contain distribute and virtualenv
 
-(TODO: create script that builds a fresh copy of these frameworks from svn checkouts)
 """
-import getopt, sys, os, shutil, logging, subprocess
+import sys
+sys.dont_write_bytecode = True
+
+import distribute_setup
+distribute_setup.use_setuptools()
+
+import pkg_resources
+# Use Jinja2 for templating because that's the
+# only one that supports Python3 at this time.
+pkg_resources.require('Jinja2')
+
+import getopt, os, shutil, logging, subprocess, time
 from topsort import topological_sort
 
+from jinja2 import Template
+
+gBaseDir = '.'
+gIndexTemplate = os.path.join(gBaseDir, 'templates', 'index.html')
+gTestResults = os.path.join(gBaseDir, "testresults")
+
+
 gUsage = """\
 run_tests.py [-a archs] [--archs=archs] [-v versions] [--versions,versions]
 
 
 gBaseDir = os.path.dirname(os.path.abspath(__file__))
 gRootDir = os.path.dirname(gBaseDir)
+gTestResults = os.path.join(gBaseDir, "testresults")
+
+gFrameworkNameTemplate="DbgPython-{archs}"
 
 gVersions=["2.6", "2.7", "3.1", "3.2"]
 gArchs=["32-bit", "3-way"]
 
+gVersions=["2.6", "2.7", ] #"3.2"]
+gArchs=["3-way"]
+
+gArchMap={
+    '3-way': ['ppc', 'i386', 'x86_64'],
+    '32-bit': ['ppc', 'i386'],
+    'intel': ['i386', 'x86_64'],
+}
+
+# XXX: Temporary workaround for structure of 
+# my local work environment.
+gPyObjCCore="pyobjc-core"
+if os.path.exists(os.path.join(gRootDir, "pyobjc-core-py3k")):
+    gPyObjCCore="pyobjc-core-py3k"
+
+
+def supports_arch_command(version):
+    major, minor = map(int, version.split('.'))
+    if major == 2:
+        return minor >= 7
+    else:
+        return minor >= 2
+
 def main():
     logging.basicConfig(level=logging.DEBUG)
 
     all_results = []
     for ver in versions:
         for arch in archs:
-            test_results = run_tests(ver, arch)
-            all_results.append([ver, arch, test_results])
+            run_tests(ver, arch)
 
-    report_results(all_results)
-
-def report_results(all_results):
-    for version, archs, test_results in all_results:
-        title = "Architectures {1} for Python {0}".format(version, archs)
-        print(title)
-        print("="*len(title))
-        print()
-        
-        for pkg, stdout, stderr in test_results:
-            summary = stdout.splitlines()[-1]
-            print("{0:>20}: {1}".format(pkg, summary))
-
-        print()
+    gen_summary(versions, archs)
 
 
 def detect_frameworks():
             partial_order.append((dep, subdir))
 
     frameworks = topological_sort(frameworks, partial_order)
-    return frameworks[:2]
+    return frameworks
 
 
 
 def run_tests(version, archs):
-    test_results = []
 
     lg = logging.getLogger("run_tests")
 
     lg.info("Run tests for Python %s with archs %s", version, archs)
 
-    subdir = os.path.join(gBaseDir, "virtualenvs", "{0}.{1}".format(version, archs))
+    subdir = os.path.join(gBaseDir, "virtualenvs", "{0}--{1}".format(version, archs))
     if os.path.exists(subdir):
         lg.debug("Remove existing virtualenv")
         shutil.rmtree(subdir)
 
-    base_python = "/Library/Frameworks/DbgPython-{0}.framework/Versions/{1}/bin/python".format(
-            archs, version)
+    if not os.path.exists(os.path.dirname(subdir)):
+        os.mkdir(os.path.dirname(subdir))
+
+    resultdir = os.path.join(gTestResults, "{0}--{1}".format(version, archs))
+    if os.path.exists(resultdir):
+        lg.debug("Remove existing results directory")
+        shutil.rmtree(resultdir)
+
+    if not os.path.exists(os.path.dirname(resultdir)):
+        os.mkdir(os.path.dirname(resultdir))
+
+    base_python = "/Library/Frameworks/{0}.framework/Versions/{1}/bin/python".format(
+            gFrameworkNameTemplate.format(archs=archs, version=version), version)
     if version[0] == '3':
         base_python += '3'
 
             subdir])
 
     xit = p.wait()
-    if p != 0:
+    if xit != 0:
         lg.warning("Cannot create virtualenv in %s", subdir)
         raise RuntimeError(subdir)
 
     # There are circular dependencies w.r.t. testing the Cocoa and Quartz wrappers,
     # install pyobjc-core, pyobjc-framework-Cocoa and pyobjc-framework-Quartz
     # to ensure we avoid those problems.
-    for pkg in ["pyobjc-core-py3k", "pyobjc-framework-Cocoa", "pyobjc-framework-Quartz"]:
-        pkgroot = os.path.join(gRootDir, pkg)
+    for pkg in ["pyobjc-core", "pyobjc-framework-Cocoa", "pyobjc-framework-Quartz"]:
+        if not os.path.exists(os.path.join(resultdir, pkg)):
+            os.makedirs(os.path.join(resultdir, pkg))
+
+        if pkg == "pyobjc-core":
+            pkgroot = os.path.join(gRootDir, gPyObjCCore)
+        else:
+            pkgroot = os.path.join(gRootDir, pkg)
         pkgbuild = os.path.join(pkgroot, "build")
         if os.path.exists(pkgbuild):
             lg.debug("Remove build directory for %s", pkg)
         p = subprocess.Popen([
             os.path.join(subdir, "bin", "python"),
             "setup.py", "install"],
-            cwd=pkgroot)
+            cwd=pkgroot, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
+
+        stdout, _ = p.communicate()
+
+        with open(os.path.join(resultdir, pkg, "pre-build-stdout.txt"), "wb") as fd:
+            fd.write(stdout)
 
         xit = p.wait()
         if xit != 0:
             raise RuntimeError(pkg)
 
     lg.debug("Start testing cycle")
-    for pkg in ["pyobjc-core-py3k"] + detect_frameworks():
-        pkgroot = os.path.join(gRootDir, pkg)
+    for pkg in ["pyobjc-core"] + detect_frameworks():
+        if not os.path.exists(os.path.join(resultdir, pkg)):
+            os.makedirs(os.path.join(resultdir, pkg))
+
+        if pkg == "pyobjc-core":
+            pkgroot = os.path.join(gRootDir, gPyObjCCore)
+        else:
+            pkgroot = os.path.join(gRootDir, pkg)
+
         pkgbuild = os.path.join(pkgroot, "build")
         if os.path.exists(pkgbuild):
             lg.debug("Remove build directory for %s", pkg)
             shutil.rmtree(pkgbuild)
 
-        lg.debug("Build %s for %s", pkg.os.path.basename(subdir))
+        lg.debug("Build %s for %s", pkg, os.path.basename(subdir))
         p = subprocess.Popen([
             os.path.join(subdir, "bin", "python"),
-            "setup.py", "build"],
-            cwd=pkgroot)
+            "setup.py", "install"],
+            cwd=pkgroot, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
+
+        stdout, _ = p.communicate()
+        with open(os.path.join(resultdir, pkg, "build-stdout.txt"), "wb") as fd:
+            fd.write(stdout)
 
         xit = p.wait()
         if xit != 0:
+            print(stdout)
             lg.warning("Build %s failed", pkg)
-            raise RuntimeError(pkg)
+            #raise RuntimeError(pkg)
+            continue
 
         # TODO: 
         # - For python2.7/3.2: use `arch` to run tests with all architectures
         # - For python2.6/3.1: run tests using 'python-32' and 'python-64' 
         #   when those are available
 
-        lg.debug("Test %s for %s", pkg.os.path.basename(subdir))
-        p = subprocess.Popen([
-            os.path.join(subdir, "bin", "python"),
-            "setup.py", "test"],
-            cwd=pkgroot, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
-        stdout, stderr = p.communicate()
+        if supports_arch_command(version):
+            for a in gArchMap[archs]:
+                lg.info("Test %s for %s (%s)", pkg, os.path.basename(subdir), a)
+                p = subprocess.Popen([
+                    '/usr/bin/arch', '-' + a,
+                    os.path.join(subdir, "bin", "python"),
+                    "setup.py", "test"],
+                    cwd=pkgroot, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
+                stdout, _ = p.communicate()
 
-        print("====STDOUT===")
-        print(stdout)
-        print("====STDERR===")
-        print(stderr)
-        print("====ENDEND===")
+                with open(os.path.join(resultdir, pkg, "test-stdout-{0}.txt".format(a)), "wb") as fd:
+                    fd.write(stdout)
 
-        test_results.append((pkg, stdout, stderr))
+                status = stdout.splitlines()
+                if status[-1].startswith(b'['):
+                    status = status[-2]
+                else:
+                    status = status[-1]
+                status = status.decode('UTF-8')
+                lg.info("Test %s for %s (%s): %s", pkg, os.path.basename(subdir), a, status)
 
-        xit = p.wait()
-        if xit != 0:
-            lg.warning("Test %s failed", pkg)
-            raise RuntimeError(pkg)
+                xit = p.wait()
+                if xit != 0:
+                    lg.warning("Test %s failed", pkg)
+                
+        else:
+            lg.debug("Test %s for %s", pkg, os.path.basename(subdir))
+            p = subprocess.Popen([
+                os.path.join(subdir, "bin", "python"),
+                "setup.py", "test"],
+                cwd=pkgroot, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
+            stdout, _ = p.communicate()
+
+            with open(os.path.join(resultdir, pkg, "test-stdout.txt"), "wb") as fd:
+                fd.write(stdout)
+
+            status = stdout.splitlines()[-1]
+            lg.info("Test %s for %s: %s", pkg, os.path.basename(subdir), status)
+
+            xit = p.wait()
+            if xit != 0:
+                lg.warning("Test %s failed", pkg)
+                continue
+                #raise RuntimeError(pkg)
 
         
         lg.debug("Install %s into %s", pkg, os.path.basename(subdir))
             raise RuntimeError(pkg)
 
        
-    return test_results
+
+def parse_tests(inputfile):
+    result = {
+        'test_pass':  0,
+        'test_fail':  0,
+        'test_error': 0,
+    }
+    with open(inputfile) as stream:
+        for ln in stream:
+            ln = ln.rstrip()
+            if ln.endswith('... ok'):
+                result['test_pass'] += 1
+            elif ln.endswith('... FAIL'):
+                result['test_fail'] += 1
+            elif ln.endswith('... ERROR'):
+                result['test_error'] += 1
+
+    result['class_pass'] = ''
+    result['class_fail'] = ''
+    result['class_error'] = ''
+    if result['test_pass'] == 0  \
+        and result['test_fail'] == 0 \
+        and result['test_error'] == 0:
+            result['class_pass'] = 'error'
+    if result['test_fail']:
+        result['class_fail'] = 'warning'
+    if result['test_error']:
+        result['class_error'] = 'error'
+
+    if result['test_error'] + result['test_fail'] == 0:
+        result['class_pass'] = 'ok'
+                
+    return result
+
+def parse_build(inputfile):
+    result = {
+        'build_warnings': 0,
+        'build_errors':   0,
+    }
+    with open(inputfile) as stream:
+        for ln in stream:
+            if 'error:' in ln:
+                result['build_errors'] += 1
+
+            elif 'warning:' in ln:
+                result['build_warnings'] += 1
+
+    result['class_warnings'] = ''
+    result['class_errors'] = ''
+    if result['build_warnings']:
+        result['class_warnings'] = 'warning'
+    if result['build_errors']:
+        result['class_errors'] = 'error'
+
+    if result['build_warnings'] + result['build_errors'] == 0:
+        result['class_warnings'] = 'ok'
+        result['class_errors'] = 'ok'
+
+    return result
+
+def get_svnversion():
+    p = subprocess.Popen([
+        'svnversion',
+        ], cwd='..', stdout=subprocess.PIPE)
+    stdout, _ = p.communicate()
+    xit = p.wait()
+    if xit != 0:
+        raise RuntimeError(xit)
+
+    return stdout.decode('UTF-8')
+
+def get_svnurl():
+    p = subprocess.Popen([
+        'svn', 'info', '.'
+        ], cwd='..', stdout=subprocess.PIPE)
+    stdout, _ = p.communicate()
+    xit = p.wait()
+    if xit != 0:
+        raise RuntimeError(xit)
+
+    for ln in stdout.splitlines():
+        if ln.startswith(b'URL'):
+            return ln.split(None, 1)[1].decode('UTF-8')
+
+def get_osx_version():
+    p = subprocess.Popen([
+        'sw_vers',
+        ], cwd='..', stdout=subprocess.PIPE)
+    stdout, _ = p.communicate()
+    xit = p.wait()
+    if xit != 0:
+        raise RuntimeError(xit)
+
+    r = {}
+    for ln in stdout.splitlines():
+        k, v = ln.decode('UTF-8').split(':', 1)
+        r[k.strip()] = v.strip()
+
+    return "{ProductName} {ProductVersion} ({BuildVersion})".format(**r)
+
+def gen_summary(versions, archs):
+    with open(gIndexTemplate) as fp:
+        tmpl = Template(fp.read())
+
+    svn={}
+    svn['revision'] = get_svnversion()
+    svn['url'] = get_svnurl()
+
+    osx={}
+    osx['version'] = get_osx_version()
+
+    versions = {}
+
+    for subdir in os.listdir(gTestResults):
+        if subdir == 'index.html': continue
+        version, style = subdir.split('--')
+
+        if version not in versions: continue
+        if style not in archs: continue
+
+        versions[(version, style)] = modules = []
+
+        for mod in os.listdir(os.path.join(gTestResults, subdir)):
+            moddir = os.path.join(gTestResults, subdir, mod)
+            info = parse_build(os.path.join(moddir, 'build-stdout.txt'))
+            info['name'] = mod
+            modules.append(info)
+            info['archs'] = []
+            info['class'] = None
+
+            if info['build_errors']:
+                info['class'] = 'error'
+            #elif info['build_warnings']:
+                #info['class'] = 'warning'
+
+            for fn in os.listdir(moddir):
+                if not fn.startswith('test'): continue
+
+                if fn == 'test-stdout.txt':
+                    a = 'all'
+
+                else:
+                    a = fn.split('-')[-1].split('.')[0]
+
+                info['archs'].append(a)
+                info[a] =  parse_tests(os.path.join(moddir, fn))
+
+                if info[a]['test_fail'] and (info['class'] is None):
+                    info['class'] = 'warning'
+
+                if info[a]['test_error']:
+                    info['class'] = 'error'
+
+
+            if info['class'] is None:
+                info['class'] = 'ok'
+
+    with open(os.path.join(gTestResults, 'index.html'), 'w') as fp:
+        fp.write(tmpl.render(
+            svn=svn,
+            osx=osx,
+            versions=versions,
+            sorted=sorted,
+            timestamp=time.ctime(),
+            ))
 
 if __name__ == "__main__":
     try:

build-support/templates/index.html

+<html>
+  <head>
+    <title>PyObjC Test results</title>
+    <style type="text/css">
+      table#summary {
+        border-collapse: collapse;
+      }
+      table#summary th {
+        border-left: 1px solid black;
+        border-right: 1px solid black;
+      }
+      table#summary th.bottom {
+        border-left: 1px solid black;
+        border-right: 1px solid black;
+        border-bottom: 3px solid black;
+      }
+      table#summary th.bottom-left {
+        border-left: 1px solid black;
+        border-right: none;
+        border-bottom: 3px solid black;
+      }
+      table#summary th.bottom-mid {
+        border-left: none;
+        border-right: none;
+        border-bottom: 3px solid black;
+      }
+      table#summary th.bottom-right {
+        border-right: 1px solid black;
+        border-left: none;
+        border-bottom: 3px solid black;
+      }
+      table#summary td {
+        border: 1px solid black;
+      }
+      /*
+      table#summary tr.ok td { 
+        background-color: green;
+      }
+      table#summary tr.warning td { 
+        background-color: orange;
+      }
+      table#summary tr.error td { 
+        background-color: red;
+      }
+      */
+
+      table#summary td.ok { 
+        background-color: green;
+      }
+      table#summary td.warning { 
+        background-color: orange;
+      }
+      table#summary td.error { 
+        background-color: red;
+      }
+
+    </style>
+  </head>
+  <body>
+    <h1>PyObjC Test results</h1>
+
+    <ul>
+      <li>Repository:  {{ svn['url'] }}</li>
+      <li>Checkout:    {{ svn['revision'] }}</li>
+      <li>OSX Version: {{ osx['version'] }}</li>
+      <li>Timestamp:   {{ timestamp }}</li>
+    {% for ver, style in sorted(versions) %}
+      <li><a href="#{{ver}}--{{style}}">Python {{ver}} ({{style}})</a></li>
+    {% endfor %}
+    </ul>
+
+    {% for ver, style in sorted(versions) %}
+
+    <a name="${ver}--${style}"><h2>Python {{ ver }} ({{ style }})</h2>
+
+    <table id="summary">
+      <tr>
+        <th></th>
+        <th colspan="2">Build</th>
+        {% for a in versions[(ver, style)][0]['archs'] %}
+        <th colspan="3">Test ({{ a }})</th>
+        {% endfor %}
+      </tr>
+      <tr>
+        <th class="bottom">Subproject</th>
+        <th class="bottom-left">Warnings</th>
+        <th class="bottom-right">Errors</th>
+        {% for a in versions[(ver, style)][0]['archs'] %}
+        <th class="bottom-left">Pass</th>
+        <th class="bottom-mid">Fail</th>
+        <th class="bottom-right">Error</th>
+        {% endfor %}
+      </tr>
+      {% for item in versions[(ver, style)] %}
+      <tr class="{{ item['class'] }}">
+        <td>{{ item['name'] }}</td>
+        <td class="{{ item['class_warnings'] }}">{{ item['build_warnings'] }}</td>
+        <td class="{{ item['class_errors'] }}">{{ item['build_errors'] }}</td>
+        {% for a in item['archs'] %}
+        <td class="{{ item[a]['class_pass']}}">{{ item[a]['test_pass'] }}</td>
+        <td class="{{ item[a]['class_fail']}}">{{ item[a]['test_fail'] }}</td>
+        <td class="{{ item[a]['class_error']}}">{{ item[a]['test_error'] }}</td>
+        {% endfor %}
+      </tr>
+      {% endfor %}
+    </table>
+
+    {% endfor %}
+  </body>
+</html>
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.