| #!/usr/bin/env python |
| |
| # This Source Code Form is subject to the terms of the Mozilla Public |
| # License, v. 2.0. If a copy of the MPL was not distributed with this file, |
| # You can obtain one at http://mozilla.org/MPL/2.0/. |
| |
| """ |
| Setup mozbase packages for development. |
| |
| Packages may be specified as command line arguments. |
| If no arguments are given, install all packages. |
| |
| See https://wiki.mozilla.org/Auto-tools/Projects/Mozbase |
| """ |
| |
| import os |
| import subprocess |
| import sys |
| from optparse import OptionParser |
| from subprocess import PIPE |
| try: |
| from subprocess import check_call as call |
| except ImportError: |
| from subprocess import call |
| |
| |
| # directory containing this file |
| here = os.path.dirname(os.path.abspath(__file__)) |
| |
| # all python packages |
| mozbase_packages = [i for i in os.listdir(here) |
| if os.path.exists(os.path.join(here, i, 'setup.py'))] |
| test_packages = [ "mock" # testing: https://wiki.mozilla.org/Auto-tools/Projects/Mozbase#Tests |
| ] |
| extra_packages = [ "sphinx" # documentation: https://wiki.mozilla.org/Auto-tools/Projects/Mozbase#Documentation |
| ] |
| |
| def cycle_check(order, dependencies): |
| """ensure no cyclic dependencies""" |
| order_dict = dict([(j, i) for i, j in enumerate(order)]) |
| for package, deps in dependencies.items(): |
| index = order_dict[package] |
| for d in deps: |
| assert index > order_dict[d], "Cyclic dependencies detected" |
| |
| def info(directory): |
| "get the package setup.py information" |
| |
| assert os.path.exists(os.path.join(directory, 'setup.py')) |
| |
| # setup the egg info |
| try: |
| call([sys.executable, 'setup.py', 'egg_info'], cwd=directory, stdout=PIPE) |
| except subprocess.CalledProcessError: |
| print "Error running setup.py in %s" % directory |
| raise |
| |
| # get the .egg-info directory |
| egg_info = [entry for entry in os.listdir(directory) |
| if entry.endswith('.egg-info')] |
| assert len(egg_info) == 1, 'Expected one .egg-info directory in %s, got: %s' % (directory, egg_info) |
| egg_info = os.path.join(directory, egg_info[0]) |
| assert os.path.isdir(egg_info), "%s is not a directory" % egg_info |
| |
| # read the package information |
| pkg_info = os.path.join(egg_info, 'PKG-INFO') |
| info_dict = {} |
| for line in file(pkg_info).readlines(): |
| if not line or line[0].isspace(): |
| continue # XXX neglects description |
| assert ':' in line |
| key, value = [i.strip() for i in line.split(':', 1)] |
| info_dict[key] = value |
| |
| return info_dict |
| |
| def get_dependencies(directory): |
| "returns the package name and dependencies given a package directory" |
| |
| # get the package metadata |
| info_dict = info(directory) |
| |
| # get the .egg-info directory |
| egg_info = [entry for entry in os.listdir(directory) |
| if entry.endswith('.egg-info')][0] |
| |
| # read the dependencies |
| requires = os.path.join(directory, egg_info, 'requires.txt') |
| dependencies = [] |
| if os.path.exists(requires): |
| for line in file(requires): |
| line = line.strip() |
| # in requires.txt file, a dependency is a non empty line |
| # Also lines like [device] are sections to mark optional |
| # dependencies, we don't want those sections. |
| if line and not (line.startswith('[') and line.endswith(']')): |
| dependencies.append(line) |
| |
| # return the information |
| return info_dict['Name'], dependencies |
| |
| def dependency_info(dep): |
| "return dictionary of dependency information from a dependency string" |
| retval = dict(Name=None, Type=None, Version=None) |
| for joiner in ('==', '<=', '>='): |
| if joiner in dep: |
| retval['Type'] = joiner |
| name, version = [i.strip() for i in dep.split(joiner, 1)] |
| retval['Name'] = name |
| retval['Version'] = version |
| break |
| else: |
| retval['Name'] = dep.strip() |
| return retval |
| |
| def unroll_dependencies(dependencies): |
| """ |
| unroll a set of dependencies to a flat list |
| |
| dependencies = {'packageA': set(['packageB', 'packageC', 'packageF']), |
| 'packageB': set(['packageC', 'packageD', 'packageE', 'packageG']), |
| 'packageC': set(['packageE']), |
| 'packageE': set(['packageF', 'packageG']), |
| 'packageF': set(['packageG']), |
| 'packageX': set(['packageA', 'packageG'])} |
| """ |
| |
| order = [] |
| |
| # flatten all |
| packages = set(dependencies.keys()) |
| for deps in dependencies.values(): |
| packages.update(deps) |
| |
| while len(order) != len(packages): |
| |
| for package in packages.difference(order): |
| if set(dependencies.get(package, set())).issubset(order): |
| order.append(package) |
| break |
| else: |
| raise AssertionError("Cyclic dependencies detected") |
| |
| cycle_check(order, dependencies) # sanity check |
| |
| return order |
| |
| |
| def main(args=sys.argv[1:]): |
| |
| # parse command line options |
| usage = '%prog [options] [package] [package] [...]' |
| parser = OptionParser(usage=usage, description=__doc__) |
| parser.add_option('-d', '--dependencies', dest='list_dependencies', |
| action='store_true', default=False, |
| help="list dependencies for the packages") |
| parser.add_option('--list', action='store_true', default=False, |
| help="list what will be installed") |
| parser.add_option('--extra', '--install-extra-packages', action='store_true', default=False, |
| help="installs extra supporting packages as well as core mozbase ones") |
| options, packages = parser.parse_args(args) |
| |
| if not packages: |
| # install all packages |
| packages = sorted(mozbase_packages) |
| |
| # ensure specified packages are in the list |
| assert set(packages).issubset(mozbase_packages), "Packages should be in %s (You gave: %s)" % (mozbase_packages, packages) |
| |
| if options.list_dependencies: |
| # list the package dependencies |
| for package in packages: |
| print '%s: %s' % get_dependencies(os.path.join(here, package)) |
| parser.exit() |
| |
| # gather dependencies |
| # TODO: version conflict checking |
| deps = {} |
| alldeps = {} |
| mapping = {} # mapping from subdir name to package name |
| # core dependencies |
| for package in packages: |
| key, value = get_dependencies(os.path.join(here, package)) |
| deps[key] = [dependency_info(dep)['Name'] for dep in value] |
| mapping[package] = key |
| |
| # keep track of all dependencies for non-mozbase packages |
| for dep in value: |
| alldeps[dependency_info(dep)['Name']] = ''.join(dep.split()) |
| |
| # indirect dependencies |
| flag = True |
| while flag: |
| flag = False |
| for value in deps.values(): |
| for dep in value: |
| if dep in mozbase_packages and dep not in deps: |
| key, value = get_dependencies(os.path.join(here, dep)) |
| deps[key] = [dep for dep in value] |
| |
| for dep in value: |
| alldeps[dep] = ''.join(dep.split()) |
| mapping[package] = key |
| flag = True |
| break |
| if flag: |
| break |
| |
| # get the remaining names for the mapping |
| for package in mozbase_packages: |
| if package in mapping: |
| continue |
| key, value = get_dependencies(os.path.join(here, package)) |
| mapping[package] = key |
| |
| # unroll dependencies |
| unrolled = unroll_dependencies(deps) |
| |
| # make a reverse mapping: package name -> subdirectory |
| reverse_mapping = dict([(j,i) for i, j in mapping.items()]) |
| |
| # we only care about dependencies in mozbase |
| unrolled = [package for package in unrolled if package in reverse_mapping] |
| |
| if options.list: |
| # list what will be installed |
| for package in unrolled: |
| print package |
| parser.exit() |
| |
| # set up the packages for development |
| for package in unrolled: |
| call([sys.executable, 'setup.py', 'develop', '--no-deps'], |
| cwd=os.path.join(here, reverse_mapping[package])) |
| |
| # add the directory of sys.executable to path to aid the correct |
| # `easy_install` getting called |
| # https://bugzilla.mozilla.org/show_bug.cgi?id=893878 |
| os.environ['PATH'] = '%s%s%s' % (os.path.dirname(os.path.abspath(sys.executable)), |
| os.path.pathsep, |
| os.environ.get('PATH', '').strip(os.path.pathsep)) |
| |
| # install non-mozbase dependencies |
| # these need to be installed separately and the --no-deps flag |
| # subsequently used due to a bug in setuptools; see |
| # https://bugzilla.mozilla.org/show_bug.cgi?id=759836 |
| pypi_deps = dict([(i, j) for i,j in alldeps.items() |
| if i not in unrolled]) |
| for package, version in pypi_deps.items(): |
| # easy_install should be available since we rely on setuptools |
| call(['easy_install', version]) |
| |
| # install packages required for unit testing |
| for package in test_packages: |
| call(['easy_install', package]) |
| |
| # install extra non-mozbase packages if desired |
| if options.extra: |
| for package in extra_packages: |
| call(['easy_install', package]) |
| |
| if __name__ == '__main__': |
| main() |