3 # This Source Code Form is subject to the terms of the Mozilla Public
4 # License, v. 2.0. If a copy of the MPL was not distributed with this file,
5 # You can obtain one at http://mozilla.org/MPL/2.0/.
8 Setup mozbase packages for development.
10 Packages may be specified as command line arguments.
11 If no arguments are given, install all packages.
13 See https://wiki.mozilla.org/Auto-tools/Projects/Mozbase
19 from optparse
import OptionParser
20 from subprocess
import PIPE
22 from subprocess
import check_call
as call
24 from subprocess
import call
27 # directory containing this file
28 here
= os
.path
.dirname(os
.path
.abspath(__file__
))
31 mozbase_packages
= [i
for i
in os
.listdir(here
)
32 if os
.path
.exists(os
.path
.join(here
, i
, 'setup.py'))]
33 test_packages
= [ "mock" # testing: https://wiki.mozilla.org/Auto-tools/Projects/Mozbase#Tests
35 extra_packages
= [ "sphinx" # documentation: https://wiki.mozilla.org/Auto-tools/Projects/Mozbase#Documentation
38 def cycle_check(order
, dependencies
):
39 """ensure no cyclic dependencies"""
40 order_dict
= dict([(j
, i
) for i
, j
in enumerate(order
)])
41 for package
, deps
in dependencies
.items():
42 index
= order_dict
[package
]
44 assert index
> order_dict
[d
], "Cyclic dependencies detected"
47 "get the package setup.py information"
49 assert os
.path
.exists(os
.path
.join(directory
, 'setup.py'))
53 call([sys
.executable
, 'setup.py', 'egg_info'], cwd
=directory
, stdout
=PIPE
)
54 except subprocess
.CalledProcessError
:
55 print "Error running setup.py in %s" % directory
58 # get the .egg-info directory
59 egg_info
= [entry
for entry
in os
.listdir(directory
)
60 if entry
.endswith('.egg-info')]
61 assert len(egg_info
) == 1, 'Expected one .egg-info directory in %s, got: %s' % (directory
, egg_info
)
62 egg_info
= os
.path
.join(directory
, egg_info
[0])
63 assert os
.path
.isdir(egg_info
), "%s is not a directory" % egg_info
65 # read the package information
66 pkg_info
= os
.path
.join(egg_info
, 'PKG-INFO')
68 for line
in file(pkg_info
).readlines():
69 if not line
or line
[0].isspace():
70 continue # XXX neglects description
72 key
, value
= [i
.strip() for i
in line
.split(':', 1)]
73 info_dict
[key
] = value
77 def get_dependencies(directory
):
78 "returns the package name and dependencies given a package directory"
80 # get the package metadata
81 info_dict
= info(directory
)
83 # get the .egg-info directory
84 egg_info
= [entry
for entry
in os
.listdir(directory
)
85 if entry
.endswith('.egg-info')][0]
87 # read the dependencies
88 requires
= os
.path
.join(directory
, egg_info
, 'requires.txt')
89 if os
.path
.exists(requires
):
90 dependencies
= [line
.strip()
91 for line
in file(requires
).readlines()
96 # return the information
97 return info_dict
['Name'], dependencies
99 def dependency_info(dep
):
100 "return dictionary of dependency information from a dependency string"
101 retval
= dict(Name
=None, Type
=None, Version
=None)
102 for joiner
in ('==', '<=', '>='):
104 retval
['Type'] = joiner
105 name
, version
= [i
.strip() for i
in dep
.split(joiner
, 1)]
106 retval
['Name'] = name
107 retval
['Version'] = version
110 retval
['Name'] = dep
.strip()
113 def unroll_dependencies(dependencies
):
115 unroll a set of dependencies to a flat list
117 dependencies = {'packageA': set(['packageB', 'packageC', 'packageF']),
118 'packageB': set(['packageC', 'packageD', 'packageE', 'packageG']),
119 'packageC': set(['packageE']),
120 'packageE': set(['packageF', 'packageG']),
121 'packageF': set(['packageG']),
122 'packageX': set(['packageA', 'packageG'])}
128 packages
= set(dependencies
.keys())
129 for deps
in dependencies
.values():
130 packages
.update(deps
)
132 while len(order
) != len(packages
):
134 for package
in packages
.difference(order
):
135 if set(dependencies
.get(package
, set())).issubset(order
):
136 order
.append(package
)
139 raise AssertionError("Cyclic dependencies detected")
141 cycle_check(order
, dependencies
) # sanity check
146 def main(args
=sys
.argv
[1:]):
148 # parse command line options
149 usage
= '%prog [options] [package] [package] [...]'
150 parser
= OptionParser(usage
=usage
, description
=__doc__
)
151 parser
.add_option('-d', '--dependencies', dest
='list_dependencies',
152 action
='store_true', default
=False,
153 help="list dependencies for the packages")
154 parser
.add_option('--list', action
='store_true', default
=False,
155 help="list what will be installed")
156 parser
.add_option('--extra', '--install-extra-packages', action
='store_true', default
=False,
157 help="installs extra supporting packages as well as core mozbase ones")
158 options
, packages
= parser
.parse_args(args
)
161 # install all packages
162 packages
= sorted(mozbase_packages
)
164 # ensure specified packages are in the list
165 assert set(packages
).issubset(mozbase_packages
), "Packages should be in %s (You gave: %s)" % (mozbase_packages
, packages
)
167 if options
.list_dependencies
:
168 # list the package dependencies
169 for package
in packages
:
170 print '%s: %s' % get_dependencies(os
.path
.join(here
, package
))
173 # gather dependencies
174 # TODO: version conflict checking
177 mapping
= {} # mapping from subdir name to package name
179 for package
in packages
:
180 key
, value
= get_dependencies(os
.path
.join(here
, package
))
181 deps
[key
] = [dependency_info(dep
)['Name'] for dep
in value
]
182 mapping
[package
] = key
184 # keep track of all dependencies for non-mozbase packages
186 alldeps
[dependency_info(dep
)['Name']] = ''.join(dep
.split())
188 # indirect dependencies
192 for value
in deps
.values():
194 if dep
in mozbase_packages
and dep
not in deps
:
195 key
, value
= get_dependencies(os
.path
.join(here
, dep
))
196 deps
[key
] = [dep
for dep
in value
]
199 alldeps
[dep
] = ''.join(dep
.split())
200 mapping
[package
] = key
206 # get the remaining names for the mapping
207 for package
in mozbase_packages
:
208 if package
in mapping
:
210 key
, value
= get_dependencies(os
.path
.join(here
, package
))
211 mapping
[package
] = key
213 # unroll dependencies
214 unrolled
= unroll_dependencies(deps
)
216 # make a reverse mapping: package name -> subdirectory
217 reverse_mapping
= dict([(j
,i
) for i
, j
in mapping
.items()])
219 # we only care about dependencies in mozbase
220 unrolled
= [package
for package
in unrolled
if package
in reverse_mapping
]
223 # list what will be installed
224 for package
in unrolled
:
228 # set up the packages for development
229 for package
in unrolled
:
230 call([sys
.executable
, 'setup.py', 'develop', '--no-deps'],
231 cwd
=os
.path
.join(here
, reverse_mapping
[package
]))
233 # add the directory of sys.executable to path to aid the correct
234 # `easy_install` getting called
235 # https://bugzilla.mozilla.org/show_bug.cgi?id=893878
236 os
.environ
['PATH'] = '%s%s%s' % (os
.path
.dirname(os
.path
.abspath(sys
.executable
)),
238 os
.environ
.get('PATH', '').strip(os
.path
.pathsep
))
240 # install non-mozbase dependencies
241 # these need to be installed separately and the --no-deps flag
242 # subsequently used due to a bug in setuptools; see
243 # https://bugzilla.mozilla.org/show_bug.cgi?id=759836
244 pypi_deps
= dict([(i
, j
) for i
,j
in alldeps
.items()
245 if i
not in unrolled
])
246 for package
, version
in pypi_deps
.items():
247 # easy_install should be available since we rely on setuptools
248 call(['easy_install', version
])
250 # install packages required for unit testing
251 for package
in test_packages
:
252 call(['easy_install', package
])
254 # install extra non-mozbase packages if desired
256 for package
in extra_packages
:
257 call(['easy_install', package
])
259 if __name__
== '__main__':