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
16 from __future__
import absolute_import
, print_function
21 from optparse
import OptionParser
22 from subprocess
import PIPE
25 from subprocess
import check_call
as call
27 from subprocess
import call
30 # directory containing this file
31 here
= os
.path
.dirname(os
.path
.abspath(__file__
))
35 i
for i
in os
.listdir(here
) if os
.path
.exists(os
.path
.join(here
, i
, "setup.py"))
38 # testing: https://wiki.mozilla.org/Auto-tools/Projects/Mozbase#Tests
39 test_packages
= ["mock"]
41 # documentation: https://wiki.mozilla.org/Auto-tools/Projects/Mozbase#Documentation
42 extra_packages
= ["sphinx"]
45 def cycle_check(order
, dependencies
):
46 """ensure no cyclic dependencies"""
47 order_dict
= dict([(j
, i
) for i
, j
in enumerate(order
)])
48 for package
, deps
in dependencies
.items():
49 index
= order_dict
[package
]
51 assert index
> order_dict
[d
], "Cyclic dependencies detected"
55 "get the package setup.py information"
57 assert os
.path
.exists(os
.path
.join(directory
, "setup.py"))
61 call([sys
.executable
, "setup.py", "egg_info"], cwd
=directory
, stdout
=PIPE
)
62 except subprocess
.CalledProcessError
:
63 print("Error running setup.py in %s" % directory
)
66 # get the .egg-info directory
67 egg_info
= [entry
for entry
in os
.listdir(directory
) if entry
.endswith(".egg-info")]
68 assert len(egg_info
) == 1, "Expected one .egg-info directory in %s, got: %s" % (
72 egg_info
= os
.path
.join(directory
, egg_info
[0])
73 assert os
.path
.isdir(egg_info
), "%s is not a directory" % egg_info
75 # read the package information
76 pkg_info
= os
.path
.join(egg_info
, "PKG-INFO")
78 for line
in open(pkg_info
).readlines():
79 if not line
or line
[0].isspace():
80 continue # XXX neglects description
82 key
, value
= [i
.strip() for i
in line
.split(":", 1)]
83 info_dict
[key
] = value
88 def get_dependencies(directory
):
89 "returns the package name and dependencies given a package directory"
91 # get the package metadata
92 info_dict
= info(directory
)
94 # get the .egg-info directory
96 entry
for entry
in os
.listdir(directory
) if entry
.endswith(".egg-info")
99 # read the dependencies
100 requires
= os
.path
.join(directory
, egg_info
, "requires.txt")
102 if os
.path
.exists(requires
):
103 for line
in open(requires
):
105 # in requires.txt file, a dependency is a non empty line
106 # Also lines like [device] are sections to mark optional
107 # dependencies, we don't want those sections.
108 if line
and not (line
.startswith("[") and line
.endswith("]")):
109 dependencies
.append(line
)
111 # return the information
112 return info_dict
["Name"], dependencies
115 def dependency_info(dep
):
116 "return dictionary of dependency information from a dependency string"
117 retval
= dict(Name
=None, Type
=None, Version
=None)
118 for joiner
in ("==", "<=", ">="):
120 retval
["Type"] = joiner
121 name
, version
= [i
.strip() for i
in dep
.split(joiner
, 1)]
122 retval
["Name"] = name
123 retval
["Version"] = version
126 retval
["Name"] = dep
.strip()
130 def unroll_dependencies(dependencies
):
132 unroll a set of dependencies to a flat list
134 dependencies = {'packageA': set(['packageB', 'packageC', 'packageF']),
135 'packageB': set(['packageC', 'packageD', 'packageE', 'packageG']),
136 'packageC': set(['packageE']),
137 'packageE': set(['packageF', 'packageG']),
138 'packageF': set(['packageG']),
139 'packageX': set(['packageA', 'packageG'])}
145 packages
= set(dependencies
.keys())
146 for deps
in dependencies
.values():
147 packages
.update(deps
)
149 while len(order
) != len(packages
):
151 for package
in packages
.difference(order
):
152 if set(dependencies
.get(package
, set())).issubset(order
):
153 order
.append(package
)
156 raise AssertionError("Cyclic dependencies detected")
158 cycle_check(order
, dependencies
) # sanity check
163 def main(args
=sys
.argv
[1:]):
165 # parse command line options
166 usage
= "%prog [options] [package] [package] [...]"
167 parser
= OptionParser(usage
=usage
, description
=__doc__
)
171 dest
="list_dependencies",
174 help="list dependencies for the packages",
177 "--list", action
="store_true", default
=False, help="list what will be installed"
181 "--install-extra-packages",
184 help="installs extra supporting packages as well as core mozbase ones",
186 options
, packages
= parser
.parse_args(args
)
189 # install all packages
190 packages
= sorted(mozbase_packages
)
192 # ensure specified packages are in the list
193 assert set(packages
).issubset(
195 ), "Packages should be in %s (You gave: %s)" % (mozbase_packages
, packages
)
197 if options
.list_dependencies
:
198 # list the package dependencies
199 for package
in packages
:
200 print("%s: %s" % get_dependencies(os
.path
.join(here
, package
)))
203 # gather dependencies
204 # TODO: version conflict checking
207 mapping
= {} # mapping from subdir name to package name
209 for package
in packages
:
210 key
, value
= get_dependencies(os
.path
.join(here
, package
))
211 deps
[key
] = [dependency_info(dep
)["Name"] for dep
in value
]
212 mapping
[package
] = key
214 # keep track of all dependencies for non-mozbase packages
216 alldeps
[dependency_info(dep
)["Name"]] = "".join(dep
.split())
218 # indirect dependencies
222 for value
in deps
.values():
224 if dep
in mozbase_packages
and dep
not in deps
:
225 key
, value
= get_dependencies(os
.path
.join(here
, dep
))
226 deps
[key
] = [dep
for dep
in value
]
229 alldeps
[dep
] = "".join(dep
.split())
230 mapping
[package
] = key
236 # get the remaining names for the mapping
237 for package
in mozbase_packages
:
238 if package
in mapping
:
240 key
, value
= get_dependencies(os
.path
.join(here
, package
))
241 mapping
[package
] = key
243 # unroll dependencies
244 unrolled
= unroll_dependencies(deps
)
246 # make a reverse mapping: package name -> subdirectory
247 reverse_mapping
= dict([(j
, i
) for i
, j
in mapping
.items()])
249 # we only care about dependencies in mozbase
250 unrolled
= [package
for package
in unrolled
if package
in reverse_mapping
]
253 # list what will be installed
254 for package
in unrolled
:
258 # set up the packages for development
259 for package
in unrolled
:
261 [sys
.executable
, "setup.py", "develop", "--no-deps"],
262 cwd
=os
.path
.join(here
, reverse_mapping
[package
]),
265 # add the directory of sys.executable to path to aid the correct
266 # `easy_install` getting called
267 # https://bugzilla.mozilla.org/show_bug.cgi?id=893878
268 os
.environ
["PATH"] = "%s%s%s" % (
269 os
.path
.dirname(os
.path
.abspath(sys
.executable
)),
271 os
.environ
.get("PATH", "").strip(os
.path
.pathsep
),
274 # install non-mozbase dependencies
275 # these need to be installed separately and the --no-deps flag
276 # subsequently used due to a bug in setuptools; see
277 # https://bugzilla.mozilla.org/show_bug.cgi?id=759836
278 pypi_deps
= dict([(i
, j
) for i
, j
in alldeps
.items() if i
not in unrolled
])
279 for package
, version
in pypi_deps
.items():
280 # easy_install should be available since we rely on setuptools
281 call(["easy_install", version
])
283 # install packages required for unit testing
284 for package
in test_packages
:
285 call(["easy_install", package
])
287 # install extra non-mozbase packages if desired
289 for package
in extra_packages
:
290 call(["easy_install", package
])
293 if __name__
== "__main__":