]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blob - src/allmydata/__init__.py
Refactor _auto_deps.py and __init__.py, adding more robust checking of dependency...
[tahoe-lafs/tahoe-lafs.git] / src / allmydata / __init__.py
1 """
2 Decentralized storage grid.
3
4 community web site: U{http://tahoe-lafs.org/}
5 """
6
7 class PackagingError(EnvironmentError):
8     """
9     Raised when there is an error in packaging of Tahoe-LAFS or its
10     dependencies which makes it impossible to proceed safely.
11     """
12     pass
13
14 __version__ = "unknown"
15 try:
16     from allmydata._version import __version__
17 except ImportError:
18     # We're running in a tree that hasn't run "./setup.py darcsver", and didn't
19     # come with a _version.py, so we don't know what our version is. This should
20     # not happen very often.
21     pass
22
23 __appname__ = "unknown"
24 try:
25     from allmydata._appname import __appname__
26 except ImportError:
27     # We're running in a tree that hasn't run "./setup.py".  This shouldn't happen.
28     pass
29
30 # __full_version__ is the one that you ought to use when identifying yourself in the
31 # "application" part of the Tahoe versioning scheme:
32 # http://allmydata.org/trac/tahoe/wiki/Versioning
33 __full_version__ = __appname__ + '/' + str(__version__)
34
35 import os, platform, re, subprocess, sys
36 _distributor_id_cmdline_re = re.compile("(?:Distributor ID:)\s*(.*)", re.I)
37 _release_cmdline_re = re.compile("(?:Release:)\s*(.*)", re.I)
38
39 _distributor_id_file_re = re.compile("(?:DISTRIB_ID\s*=)\s*(.*)", re.I)
40 _release_file_re = re.compile("(?:DISTRIB_RELEASE\s*=)\s*(.*)", re.I)
41
42 global _distname,_version
43 _distname = None
44 _version = None
45
46 def get_linux_distro():
47     """ Tries to determine the name of the Linux OS distribution name.
48
49     First, try to parse a file named "/etc/lsb-release".  If it exists, and
50     contains the "DISTRIB_ID=" line and the "DISTRIB_RELEASE=" line, then return
51     the strings parsed from that file.
52
53     If that doesn't work, then invoke platform.dist().
54
55     If that doesn't work, then try to execute "lsb_release", as standardized in
56     2001:
57
58     http://refspecs.freestandards.org/LSB_1.0.0/gLSB/lsbrelease.html
59
60     The current version of the standard is here:
61
62     http://refspecs.freestandards.org/LSB_3.2.0/LSB-Core-generic/LSB-Core-generic/lsbrelease.html
63
64     that lsb_release emitted, as strings.
65
66     Returns a tuple (distname,version). Distname is what LSB calls a
67     "distributor id", e.g. "Ubuntu".  Version is what LSB calls a "release",
68     e.g. "8.04".
69
70     A version of this has been submitted to python as a patch for the standard
71     library module "platform":
72
73     http://bugs.python.org/issue3937
74     """
75     global _distname,_version
76     if _distname and _version:
77         return (_distname, _version)
78
79     try:
80         etclsbrel = open("/etc/lsb-release", "rU")
81         for line in etclsbrel:
82             m = _distributor_id_file_re.search(line)
83             if m:
84                 _distname = m.group(1).strip()
85                 if _distname and _version:
86                     return (_distname, _version)
87             m = _release_file_re.search(line)
88             if m:
89                 _version = m.group(1).strip()
90                 if _distname and _version:
91                     return (_distname, _version)
92     except EnvironmentError:
93         pass
94
95     (_distname, _version) = platform.dist()[:2]
96     if _distname and _version:
97         return (_distname, _version)
98
99     try:
100         p = subprocess.Popen(["lsb_release", "--all"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
101         rc = p.wait()
102         if rc == 0:
103             for line in p.stdout.readlines():
104                 m = _distributor_id_cmdline_re.search(line)
105                 if m:
106                     _distname = m.group(1).strip()
107                     if _distname and _version:
108                         return (_distname, _version)
109
110                 m = _release_cmdline_re.search(p.stdout.read())
111                 if m:
112                     _version = m.group(1).strip()
113                     if _distname and _version:
114                         return (_distname, _version)
115     except EnvironmentError:
116         pass
117
118     if os.path.exists("/etc/arch-release"):
119         return ("Arch_Linux", "")
120
121     return (_distname,_version)
122
123 def get_platform():
124     # Our version of platform.platform(), telling us both less and more than the
125     # Python Standard Library's version does.
126     # We omit details such as the Linux kernel version number, but we add a
127     # more detailed and correct rendition of the Linux distribution and
128     # distribution-version.
129     if "linux" in platform.system().lower():
130         return platform.system()+"-"+"_".join(get_linux_distro())+"-"+platform.machine()+"-"+"_".join([x for x in platform.architecture() if x])
131     else:
132         return platform.platform()
133
134
135 from allmydata.util import verlib
136 def normalized_version(verstr):
137     return verlib.NormalizedVersion(verlib.suggest_normalized_version(verstr))
138
139
140 def get_package_versions_and_locations():
141     import warnings
142     from _auto_deps import package_imports, deprecation_messages, deprecation_imports
143
144     def package_dir(srcfile):
145         return os.path.dirname(os.path.dirname(os.path.normcase(os.path.realpath(srcfile))))
146
147     # pkg_resources.require returns the distribution that pkg_resources attempted to put
148     # on sys.path, which can differ from the one that we actually import due to #1258,
149     # or any other bug that causes sys.path to be set up incorrectly. Therefore we
150     # must import the packages in order to check their versions and paths.
151
152     # This warning is generated by twisted, PyRex, and possibly other packages,
153     # but can happen at any time, not only when they are imported. See
154     # http://tahoe-lafs.org/trac/tahoe-lafs/ticket/1129 .
155     warnings.filterwarnings("ignore", category=DeprecationWarning,
156         message="BaseException.message has been deprecated as of Python 2.6",
157         append=True)
158
159     # This is to suppress various DeprecationWarnings that occur when modules are imported.
160     # See http://allmydata.org/trac/tahoe/ticket/859 and http://divmod.org/trac/ticket/2994 .
161
162     for msg in deprecation_messages:
163         warnings.filterwarnings("ignore", category=DeprecationWarning, message=msg, append=True)
164     try:
165         for modulename in deprecation_imports:
166             try:
167                 __import__(modulename)
168             except ImportError:
169                 pass
170     finally:
171         for ign in deprecation_messages:
172             warnings.filters.pop()
173
174     packages = []
175
176     def get_version(module, attr):
177         return str(getattr(module, attr, 'unknown'))
178
179     for pkgname, modulename in [(__appname__, 'allmydata')] + package_imports:
180         if modulename:
181             try:
182                 __import__(modulename)
183                 module = sys.modules[modulename]
184             except ImportError:
185                 packages.append((pkgname, (None, modulename)))
186             else:
187                 if 'sqlite' in pkgname:
188                     packages.append( (pkgname,  (get_version(module, 'version'),        package_dir(module.__file__))) )
189                     packages.append( ('sqlite', (get_version(module, 'sqlite_version'), package_dir(module.__file__))) )
190                 else:
191                     packages.append( (pkgname,  (get_version(module, '__version__'),    package_dir(module.__file__))) )
192         elif pkgname == 'python':
193             packages.append( (pkgname, (platform.python_version(), sys.executable)) )
194         elif pkgname == 'platform':
195             packages.append( (pkgname, (get_platform(), None)) )
196
197     return packages
198
199
200 def check_requirement(req, vers_and_locs):
201     # TODO: check [] options
202     # We support only disjunctions of >= and ==
203
204     reqlist = req.split(',')
205     name = reqlist[0].split('>=')[0].split('==')[0].strip(' ').split('[')[0]
206     if name not in vers_and_locs:
207         raise PackagingError("no version info for %s" % (name,))
208     if req.strip(' ') == name:
209         return
210     (actual, location) = vers_and_locs[name]
211     if actual is None:
212         raise ImportError("could not import %r for requirement %r" % (location, req))
213     if actual == 'unknown':
214         return
215     actualver = normalized_version(actual)
216
217     for r in reqlist:
218         s = r.split('>=')
219         if len(s) == 2:
220             required = s[1].strip(' ')
221             if actualver >= normalized_version(required):
222                 return  # minimum requirement met
223         else:
224             s = r.split('==')
225             if len(s) == 2:
226                 required = s[1].strip(' ')
227                 if actualver == normalized_version(required):
228                     return  # exact requirement met
229             else:
230                 raise PackagingError("no version info or could not understand requirement %r" % (req,))
231
232     msg = ("We require %s, but could only find version %s.\n" % (req, actual))
233     if location and location != 'unknown':
234         msg += "The version we found is from %r.\n" % (location,)
235     msg += ("To resolve this problem, uninstall that version, either using your\n"
236             "operating system's package manager or by moving aside the directory.")
237     raise PackagingError(msg)
238
239
240 _vers_and_locs_list = get_package_versions_and_locations()
241
242
243 def cross_check_pkg_resources_versus_import():
244     """This function returns a list of errors due to any failed cross-checks."""
245
246     import pkg_resources
247     from _auto_deps import install_requires
248
249     errors = []
250     not_pkg_resourceable = set(['sqlite', 'sqlite3', 'python', 'platform', __appname__.lower()])
251     not_import_versionable = set(['zope.interface', 'mock', 'pyasn1'])
252     ignorable = set(['argparse', 'pyutil', 'zbase32'])
253
254     pkg_resources_vers_and_locs = dict([(p.project_name.lower(), (str(p.version), p.location))
255                                         for p in pkg_resources.require(install_requires)])
256
257     for name, (imp_ver, imp_loc) in _vers_and_locs_list:
258         name = name.lower()
259         if name not in not_pkg_resourceable:
260             if name not in pkg_resources_vers_and_locs:
261                 errors.append("Warning: dependency %s (version %s imported from %r) was not found by pkg_resources."
262                               % (name, imp_ver, imp_loc))
263
264             pr_ver, pr_loc = pkg_resources_vers_and_locs[name]
265             try:
266                 pr_normver = normalized_version(pr_ver)
267             except Exception, e:
268                 errors.append("Warning: version number %s found for dependency %s by pkg_resources could not be parsed. "
269                               "The version found by import was %s from %r. "
270                               "pkg_resources thought it should be found at %r. "
271                               "The exception was %s: %s"
272                               % (pr_ver, name, imp_ver, imp_loc, pr_loc, e.__class__.name, e))
273             else:
274                 if imp_ver == 'unknown':
275                     if name not in not_import_versionable:
276                         errors.append("Warning: unexpectedly could not find a version number for dependency %s imported from %r. "
277                                       "pkg_resources thought it should be version %s at %r."
278                                       % (name, imp_loc, pr_ver, pr_loc))
279                 else:
280                     try:
281                         imp_normver = normalized_version(imp_ver)
282                     except Exception, e:
283                         errors.append("Warning: version number %s found for dependency %s (imported from %r) could not be parsed. "
284                                       "pkg_resources thought it should be version %s at %r. "
285                                       "The exception was %s: %s"
286                                       % (imp_ver, name, imp_loc, pr_ver, pr_loc, e.__class__.name, e))
287                     else:
288                         if pr_ver == 'unknown' or (pr_normver != imp_normver):
289                             if not os.path.normpath(os.path.realpath(pr_loc)) == os.path.normpath(os.path.realpath(imp_loc)):
290                                 errors.append("Warning: dependency %s found to have version number %s (normalized to %s, from %r) "
291                                               "by pkg_resources, but version %s (normalized to %s, from %r) by import."
292                                               % (name, pr_ver, str(pr_normver), pr_loc, imp_ver, str(imp_normver), imp_loc))
293
294     imported_packages = set([p.lower() for (p, _) in _vers_and_locs_list])
295     for pr_name, (pr_ver, pr_loc) in pkg_resources_vers_and_locs.iteritems():
296         if pr_name not in imported_packages and pr_name not in ignorable:
297             errors.append("Warning: dependency %s (version %s) found by pkg_resources not found by import."
298                           % (pr_name, pr_ver))
299
300     return errors
301
302
303 def get_error_string(errors):
304     from allmydata._auto_deps import install_requires
305
306     return ("\n%s\n\n"
307             "For debugging purposes, the PYTHONPATH was\n"
308             "  %r\n"
309             "install_requires was\n"
310             "  %r\n"
311             "sys.path after importing pkg_resources was\n"
312             "  %s\n"
313             % ("\n".join(errors), os.environ.get('PYTHONPATH'), install_requires, (os.pathsep+"\n  ").join(sys.path)) )
314
315 def check_all_requirements():
316     """This function returns a list of errors due to any failed checks."""
317
318     from allmydata._auto_deps import install_requires
319
320     errors = []
321
322     # we require 2.4.4 on non-UCS-2, non-Redhat builds to avoid <http://www.python.org/news/security/PSF-2006-001/>
323     # we require 2.4.3 on non-UCS-2 Redhat, because 2.4.3 is common on Redhat-based distros and will have patched the above bug
324     # we require at least 2.4.2 in any case to avoid a bug in the base64 module: <http://bugs.python.org/issue1171487>
325     if sys.maxunicode == 65535:
326         if sys.version_info < (2, 4, 2) or sys.version_info[0] > 2:
327             errors.append("Tahoe-LAFS current requires Python v2.4.2 or greater "
328                           "for a UCS-2 build (but less than v3), not %r" %
329                           (sys.version_info,))
330     elif platform.platform().lower().find('redhat') >= 0:
331         if sys.version_info < (2, 4, 3) or sys.version_info[0] > 2:
332             errors.append("Tahoe-LAFS current requires Python v2.4.3 or greater "
333                           "on Redhat-based distributions (but less than v3), not %r" %
334                           (sys.version_info,))
335     else:
336         if sys.version_info < (2, 4, 4) or sys.version_info[0] > 2:
337             errors.append("Tahoe-LAFS current requires Python v2.4.4 or greater "
338                           "for a non-UCS-2 build (but less than v3), not %r" %
339                           (sys.version_info,))
340
341     vers_and_locs = dict(_vers_and_locs_list)
342     for requirement in install_requires:
343         try:
344             check_requirement(requirement, vers_and_locs)
345         except Exception, e:
346             errors.append("%s: %s" % (e.__class__.__name__, e))
347
348     if errors:
349         raise PackagingError(get_error_string(errors))
350
351 check_all_requirements()
352
353
354 def get_package_versions():
355     return dict([(k, v) for k, (v, l) in _vers_and_locs_list])
356
357 def get_package_locations():
358     return dict([(k, l) for k, (v, l) in _vers_and_locs_list])
359
360 def get_package_versions_string(show_paths=False):
361     res = []
362     for p, (v, loc) in _vers_and_locs_list:
363         info = str(p) + ": " + str(v)
364         if show_paths:
365             info = info + " (%s)" % str(loc)
366         res.append(info)
367
368     output = ",\n".join(res) + "\n"
369
370     if not hasattr(sys, 'frozen'):
371         errors = cross_check_pkg_resources_versus_import()
372         if errors:
373             output += get_error_string(errors)
374
375     return output