|
1 #!/usr/bin/env python |
|
2 # |
|
3 # Copyright 2007 Google Inc. |
|
4 # |
|
5 # Licensed under the Apache License, Version 2.0 (the "License"); |
|
6 # you may not use this file except in compliance with the License. |
|
7 # You may obtain a copy of the License at |
|
8 # |
|
9 # http://www.apache.org/licenses/LICENSE-2.0 |
|
10 # |
|
11 # Unless required by applicable law or agreed to in writing, software |
|
12 # distributed under the License is distributed on an "AS IS" BASIS, |
|
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|
14 # See the License for the specific language governing permissions and |
|
15 # limitations under the License. |
|
16 # |
|
17 |
|
18 """Code to exist off of google.appengine.dist. |
|
19 |
|
20 Kept in a separate file from the __init__ module for testing purposes. |
|
21 """ |
|
22 |
|
23 |
|
24 __all__ = ['use_library'] |
|
25 |
|
26 |
|
27 import distutils.version |
|
28 import os |
|
29 import sys |
|
30 |
|
31 server_software = os.getenv('SERVER_SOFTWARE') |
|
32 USING_SDK = not server_software or server_software.startswith('Dev') |
|
33 del server_software |
|
34 |
|
35 if not USING_SDK: |
|
36 import google |
|
37 this_version = os.path.dirname(os.path.dirname(google.__file__)) |
|
38 versions = os.path.dirname(this_version) |
|
39 PYTHON_LIB = os.path.dirname(versions) |
|
40 del google, this_version, versions |
|
41 else: |
|
42 PYTHON_LIB = '/base/python_lib' |
|
43 |
|
44 installed = {} |
|
45 |
|
46 |
|
47 def SetAllowedModule(_): |
|
48 pass |
|
49 |
|
50 |
|
51 class UnacceptableVersionError(Exception): |
|
52 """Raised when a version of a package that is unacceptable is requested.""" |
|
53 pass |
|
54 |
|
55 |
|
56 def DjangoVersion(): |
|
57 """Discover the version of Django installed. |
|
58 |
|
59 Returns: |
|
60 A distutils.version.LooseVersion. |
|
61 """ |
|
62 import django |
|
63 return distutils.version.LooseVersion('.'.join(map(str, django.VERSION))) |
|
64 |
|
65 |
|
66 def PylonsVersion(): |
|
67 """Discover the version of Pylons installed. |
|
68 |
|
69 Returns: |
|
70 A distutils.version.LooseVersion. |
|
71 """ |
|
72 import pylons |
|
73 return distutils.version.LooseVersion(pylons.__version__) |
|
74 |
|
75 |
|
76 PACKAGES = { |
|
77 'django': (DjangoVersion, |
|
78 {'1.0': None, '0.96': None}), |
|
79 |
|
80 |
|
81 |
|
82 |
|
83 |
|
84 |
|
85 |
|
86 '_test': (lambda: distutils.version.LooseVersion('1.0'), {'1.0': None}), |
|
87 '_testpkg': (lambda: distutils.version.LooseVersion('1.0'), |
|
88 {'1.0': set([('_test', '1.0')])}), |
|
89 } |
|
90 |
|
91 |
|
92 def EqualVersions(version, baseline): |
|
93 """Test that a version is acceptable as compared to the baseline. |
|
94 |
|
95 Meant to be used to compare version numbers as returned by a package itself |
|
96 and not user input. |
|
97 |
|
98 Args: |
|
99 version: distutils.version.LooseVersion. |
|
100 The version that is being checked. |
|
101 baseline: distutils.version.LooseVersion. |
|
102 The version that one hopes version compares equal to. |
|
103 |
|
104 Returns: |
|
105 A bool indicating whether the versions are considered equal. |
|
106 """ |
|
107 baseline_tuple = baseline.version |
|
108 truncated_tuple = version.version[:len(baseline_tuple)] |
|
109 if truncated_tuple == baseline_tuple: |
|
110 return True |
|
111 else: |
|
112 return False |
|
113 |
|
114 |
|
115 def AllowInstalledLibrary(name, desired): |
|
116 """Allow the use of a package without performing a version check. |
|
117 |
|
118 Needed to clear a package's dependencies in case the dependencies need to be |
|
119 imported in order to perform a version check. The version check is skipped on |
|
120 the dependencies because the assumption is that the package that triggered |
|
121 the call would not be installed without the proper dependencies (which might |
|
122 be a different version than what the package explicitly requires). |
|
123 |
|
124 Args: |
|
125 name: Name of package. |
|
126 desired: Desired version. |
|
127 |
|
128 Raises: |
|
129 UnacceptableVersion Error if the installed version of a package is |
|
130 unacceptable. |
|
131 """ |
|
132 if name == 'django' and desired != '0.96': |
|
133 tail = os.path.join('lib', 'django') |
|
134 sys.path[:] = [dirname |
|
135 for dirname in sys.path |
|
136 if not dirname.endswith(tail)] |
|
137 CallSetAllowedModule(name, desired) |
|
138 dependencies = PACKAGES[name][1][desired] |
|
139 if dependencies: |
|
140 for dep_name, dep_version in dependencies: |
|
141 AllowInstalledLibrary(dep_name, dep_version) |
|
142 installed[name] = desired, False |
|
143 |
|
144 |
|
145 def CheckInstalledLibrary(name, desired): |
|
146 """Check that the library and its dependencies are installed. |
|
147 |
|
148 Args: |
|
149 name: Name of the library that should be installed. |
|
150 desired: The desired version. |
|
151 |
|
152 Raises: |
|
153 UnacceptableVersionError if the installed version of a package is |
|
154 unacceptable. |
|
155 """ |
|
156 dependencies = PACKAGES[name][1][desired] |
|
157 if dependencies: |
|
158 for dep_name, dep_version in dependencies: |
|
159 AllowInstalledLibrary(dep_name, dep_version) |
|
160 CheckInstalledVersion(name, desired, explicit=True) |
|
161 |
|
162 |
|
163 def CheckInstalledVersion(name, desired, explicit): |
|
164 """Check that the installed version of a package is acceptable. |
|
165 |
|
166 Args: |
|
167 name: Name of package. |
|
168 desired: Desired version string. |
|
169 explicit: Explicitly requested by the user or implicitly because of a |
|
170 dependency. |
|
171 |
|
172 Raises: |
|
173 UnacceptableVersionError if the installed version of a package is |
|
174 unacceptable. |
|
175 """ |
|
176 CallSetAllowedModule(name, desired) |
|
177 find_version = PACKAGES[name][0] |
|
178 installed_version = find_version() |
|
179 desired_version = distutils.version.LooseVersion(desired) |
|
180 if not EqualVersions(installed_version, desired_version): |
|
181 raise UnacceptableVersionError( |
|
182 '%s %s was requested, but %s is already in use' % |
|
183 (name, desired_version, installed_version)) |
|
184 installed[name] = desired, explicit |
|
185 |
|
186 |
|
187 def CallSetAllowedModule(name, desired): |
|
188 """Helper to call SetAllowedModule(name), after special-casing Django.""" |
|
189 if name == 'django' and desired != '0.96': |
|
190 tail = os.path.join('lib', 'django') |
|
191 sys.path[:] = [dirname |
|
192 for dirname in sys.path |
|
193 if not dirname.endswith(tail)] |
|
194 SetAllowedModule(name) |
|
195 |
|
196 |
|
197 def CreatePath(name, version): |
|
198 """Create the path to a package.""" |
|
199 package_dir = '%s-%s' % (name, version) |
|
200 return os.path.join(PYTHON_LIB, 'versions', 'third_party', package_dir) |
|
201 |
|
202 |
|
203 def RemoveLibrary(name): |
|
204 """Remove a library that has been installed.""" |
|
205 installed_version, _ = installed[name] |
|
206 path = CreatePath(name, installed_version) |
|
207 try: |
|
208 sys.path.remove(path) |
|
209 except ValueError: |
|
210 pass |
|
211 del installed[name] |
|
212 |
|
213 |
|
214 def AddLibrary(name, version, explicit): |
|
215 """Add a library to sys.path and 'installed'.""" |
|
216 sys.path.insert(1, CreatePath(name, version)) |
|
217 installed[name] = version, explicit |
|
218 |
|
219 |
|
220 def InstallLibrary(name, version, explicit=True): |
|
221 """Install a package. |
|
222 |
|
223 If the installation is explicit then the user made the installation request, |
|
224 not a package as a dependency. Explicit installation leads to stricter |
|
225 version checking. |
|
226 |
|
227 Args: |
|
228 name: Name of the requested package (already validated as available). |
|
229 version: The desired version (already validated as available). |
|
230 explicit: Explicitly requested by the user or implicitly because of a |
|
231 dependency. |
|
232 """ |
|
233 installed_version, explicitly_installed = installed.get(name, [None] * 2) |
|
234 if name in sys.modules: |
|
235 if explicit: |
|
236 CheckInstalledVersion(name, version, explicit=True) |
|
237 return |
|
238 elif installed_version: |
|
239 if version == installed_version: |
|
240 return |
|
241 if explicit: |
|
242 if explicitly_installed: |
|
243 raise ValueError('%s %s requested, but %s already in use' % |
|
244 (name, version, installed_version)) |
|
245 RemoveLibrary(name) |
|
246 else: |
|
247 version_ob = distutils.version.LooseVersion(version) |
|
248 installed_ob = distutils.version.LooseVersion(installed_version) |
|
249 if version_ob <= installed_ob: |
|
250 return |
|
251 else: |
|
252 RemoveLibrary(name) |
|
253 AddLibrary(name, version, explicit) |
|
254 dep_details = PACKAGES[name][1][version] |
|
255 if not dep_details: |
|
256 return |
|
257 for dep_name, dep_version in dep_details: |
|
258 InstallLibrary(dep_name, dep_version, explicit=False) |
|
259 |
|
260 |
|
261 def use_library(name, version): |
|
262 """Specify a third-party package to use. |
|
263 |
|
264 Args: |
|
265 name: Name of package to use. |
|
266 version: Version of the package to use (string). |
|
267 """ |
|
268 if name not in PACKAGES: |
|
269 raise ValueError('%s is not a supported package' % name) |
|
270 versions = PACKAGES[name][1].keys() |
|
271 if version not in versions: |
|
272 raise ValueError('%s is not a supported version for %s; ' |
|
273 'supported versions are %s' % (version, name, versions)) |
|
274 if USING_SDK: |
|
275 CheckInstalledLibrary(name, version) |
|
276 else: |
|
277 InstallLibrary(name, version, explicit=True) |
|
278 |
|
279 |
|
280 if not USING_SDK: |
|
281 InstallLibrary('django', '0.96', explicit=False) |