OLD | NEW |
| (Empty) |
1 #!/usr/bin/python2.4 | |
2 # Copyright 2009 Google Inc. | |
3 # | |
4 # Licensed under the Apache License, Version 2.0 (the "License"); | |
5 # you may not use this file except in compliance with the License. | |
6 # You may obtain a copy of the License at | |
7 # | |
8 # http://www.apache.org/licenses/LICENSE-2.0 | |
9 # | |
10 # Unless required by applicable law or agreed to in writing, software | |
11 # distributed under the License is distributed on an "AS IS" BASIS, | |
12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 # See the License for the specific language governing permissions and | |
14 # limitations under the License. | |
15 # ======================================================================== | |
16 | |
17 """Builds standalone installers and MSI wrappers around them. | |
18 | |
19 This is very close to the logic within installers\build.scons. The difference | |
20 is that we have an additional file standalone_installers.txt. This file | |
21 contains a list of standalone installers to create along with necessary values. | |
22 For each entry in standalone_installers.txt, we create a corresponding | |
23 standalone installer, which is the meta-installer, app installer binaries, and | |
24 update response tarred together. | |
25 MSI installers that wrap the standalone installer may also be created. | |
26 """ | |
27 | |
28 import array | |
29 import base64 | |
30 import codecs | |
31 import os | |
32 import sha | |
33 | |
34 from enterprise.installer import build_enterprise_installer | |
35 from installers import build_metainstaller | |
36 from installers import tag_meta_installers | |
37 from installers import tagged_installer | |
38 | |
39 | |
40 class OfflineInstaller(object): | |
41 """Represents the information for a bundle.""" | |
42 | |
43 def __init__(self, | |
44 friendly_product_name, | |
45 exe_base_name, | |
46 binaries, | |
47 msi_base_name, | |
48 custom_tag_params, | |
49 silent_uninstall_args, | |
50 should_build_enterprise_msi, | |
51 msi_installer_data, | |
52 installers_txt_filename): | |
53 self.friendly_product_name = friendly_product_name | |
54 self.exe_base_name = exe_base_name | |
55 self.binaries = binaries | |
56 self.msi_base_name = msi_base_name | |
57 self.custom_tag_params = custom_tag_params | |
58 self.silent_uninstall_args = silent_uninstall_args | |
59 self.should_build_enterprise_msi = should_build_enterprise_msi | |
60 self.msi_installer_data = msi_installer_data | |
61 self.installers_txt_filename = installers_txt_filename | |
62 | |
63 | |
64 def ReadOfflineInstallersFile(env, offline_installers_file_path): | |
65 """Enumerates the entries in the offline installers file. | |
66 | |
67 Args: | |
68 env: Environment. | |
69 offline_installers_file_path: Path to file specifying installers to build. | |
70 | |
71 Returns: | |
72 Returns a list of structures used for creating the prestamped binaries. | |
73 """ | |
74 | |
75 offline_installers = [] | |
76 offline_abs_path = env.File(offline_installers_file_path).abspath | |
77 installer_file = codecs.open(offline_abs_path, 'r') | |
78 for line in installer_file: | |
79 line = line.strip() | |
80 if len(line) and not line.startswith('#'): | |
81 (friendly_product_name, | |
82 exe_base_name, | |
83 binaries, | |
84 msi_base_name, | |
85 custom_tag_params, | |
86 silent_uninstall_args, | |
87 should_build_enterprise_msi, | |
88 msi_installer_data, | |
89 installers_txt_filename) = eval(line) | |
90 installer = OfflineInstaller(friendly_product_name, | |
91 exe_base_name, | |
92 binaries, | |
93 msi_base_name, | |
94 custom_tag_params, | |
95 silent_uninstall_args, | |
96 should_build_enterprise_msi, | |
97 msi_installer_data, | |
98 installers_txt_filename) | |
99 offline_installers.append(installer) | |
100 return offline_installers | |
101 | |
102 | |
103 def BuildOfflineInstallersVersion(env, | |
104 omaha_version_info, | |
105 omaha_files_path, | |
106 empty_metainstaller_path, | |
107 offline_installers_file_path, | |
108 manifest_files_path, | |
109 prefix='', | |
110 is_official=False): | |
111 """Builds all standalone installers specified in offline_installers_file_path. | |
112 | |
113 Args: | |
114 env: Environment. | |
115 omaha_version_info: info about the version of the Omaha files | |
116 omaha_files_path: Path to the directory containing the Omaha binaries. | |
117 empty_metainstaller_path: Path to empty (no tarball) metainstaller binary. | |
118 offline_installers_file_path: Path to file specifying installers to build. | |
119 manifest_files_path: Path to the directory containing the manifests for the | |
120 apps specified in offline_installers_file_path. | |
121 prefix: Optional prefix for the resulting installer. | |
122 is_official: Whether to build official (vs. test) standalone installers. | |
123 """ | |
124 | |
125 offline_installers = ReadOfflineInstallersFile(env, | |
126 offline_installers_file_path) | |
127 | |
128 for offline_installer in offline_installers: | |
129 BuildOfflineInstaller( | |
130 env, | |
131 offline_installer, | |
132 omaha_version_info, | |
133 omaha_files_path, | |
134 empty_metainstaller_path, | |
135 offline_installers_file_path, | |
136 manifest_files_path, | |
137 prefix, | |
138 is_official | |
139 ) | |
140 | |
141 | |
142 def _GenerateUpdateResponseFile(target, source, env): | |
143 """Generate GUP file based on a list of sources. | |
144 | |
145 Don't call function directly from this script. source may be | |
146 generated as part of build. Use function as action in env.Command. | |
147 | |
148 Args: | |
149 target: Target GUP file name. | |
150 source: A list of source files. Source files should be listed as manifest1, | |
151 binary1, manifest2, binary2 and so on. Order is important so that | |
152 manifests and installers can be differentiated and 'INSTALLER_VERSIONS' | |
153 can be applied properly. | |
154 env: Construct environment. This environment must contain environment | |
155 variable 'INSTALLER_VERSIONS', which contains a list of versions for | |
156 corresponding binaries in source and should be in same order. | |
157 | |
158 Raises: | |
159 Exception: When build encounters error. | |
160 """ | |
161 xml_header = '<?xml version="1.0" encoding="UTF-8"?>\n' | |
162 response_header = '<response protocol="3.0">' | |
163 response_footer = '</response>' | |
164 | |
165 local_env = env.Clone() | |
166 | |
167 version_list = local_env['INSTALLER_VERSIONS'] | |
168 if not version_list: | |
169 raise Exception('INSTALLER_VERSIONS is missing from environment.') | |
170 | |
171 manifest_content_list = [xml_header, response_header] | |
172 for file_index in xrange(0, len(source), 2): | |
173 source_manifest_path = source[file_index] | |
174 binary_path = source[file_index + 1] | |
175 size = os.stat(binary_path.abspath).st_size | |
176 installer_file = open(binary_path.abspath, mode='rb') | |
177 data = array.array('B') | |
178 data.fromfile(installer_file, size) | |
179 installer_file.close() | |
180 s = sha.new(data) | |
181 hash_value = base64.b64encode(s.digest()) | |
182 | |
183 manifest_file = open(source_manifest_path.abspath) | |
184 manifest_content = manifest_file.read() | |
185 response_body_start_index = manifest_content.find('<response') | |
186 if response_body_start_index < 0: | |
187 raise Exception('GUP file does not contain response element.') | |
188 # + 1 to include the closing > in header | |
189 response_body_start_index = manifest_content.find( | |
190 '>', response_body_start_index) | |
191 if response_body_start_index < 0: | |
192 raise Exception('GUP file does not contain response element.') | |
193 response_body_start_index += 1 | |
194 response_body_end_index = manifest_content.find( | |
195 '</response>', response_body_start_index) | |
196 if response_body_end_index < 0: | |
197 raise Exception('GUP file is not in valid response format.') | |
198 local_env['INSTALLER_SIZE'] = str(size) | |
199 local_env['INSTALLER_HASH'] = hash_value | |
200 local_env['INSTALLER_VERSION'] = version_list[file_index/2] | |
201 manifest_content_list.append(local_env.subst( | |
202 manifest_content[response_body_start_index:response_body_end_index], | |
203 raw=1)) | |
204 manifest_file.close() | |
205 manifest_content_list.append(response_footer) | |
206 | |
207 manifest_content_str = ''.join(manifest_content_list) | |
208 output_file = open(target[0].abspath, 'w') | |
209 output_file.write(manifest_content_str) | |
210 output_file.close() | |
211 | |
212 | |
213 def BuildOfflineInstaller( | |
214 env, | |
215 offline_installer, | |
216 omaha_version_info, | |
217 omaha_files_path, | |
218 empty_metainstaller_path, | |
219 offline_installers_file_path, | |
220 manifest_files_path, | |
221 prefix='', | |
222 is_official=False, | |
223 installers_sources_path='$MAIN_DIR/installers', | |
224 enterprise_installers_sources_path='$MAIN_DIR/enterprise/installer', | |
225 lzma_path='$MAIN_DIR/third_party/lzma/v4_65/files/lzma.exe', | |
226 resmerge_path='$MAIN_DIR/tools/resmerge'): | |
227 """Builds the standalone installers specified by offline_installer. | |
228 | |
229 Args: | |
230 env: Environment. | |
231 offline_installer: OfflineInstaller containing the information about the | |
232 standalone installer to build. | |
233 omaha_version_info: info about the version of the Omaha files | |
234 omaha_files_path: Path to the directory containing the Omaha binaries. | |
235 empty_metainstaller_path: Path to empty (no tarball) metainstaller binary. | |
236 offline_installers_file_path: Path to file specifying installers to build. | |
237 manifest_files_path: Path to the directory containing the manifests for the | |
238 apps specified in offline_installers_file_path. | |
239 prefix: Optional prefix for the resulting installer. | |
240 is_official: Whether to build official (vs. test) standalone installers. | |
241 installers_sources_path: path to the directory containing the source files | |
242 for building the metainstaller | |
243 enterprise_installers_sources_path: path to the directory containing the | |
244 source files for building enterprise installers | |
245 lzma_path: path to lzma.exe | |
246 resmerge_path: path to resmerge.exe | |
247 | |
248 Returns: | |
249 Target nodes. | |
250 | |
251 Raises: | |
252 Exception: Missing or invalid data specified in offline_installer. | |
253 """ | |
254 standalone_installer_base_name = offline_installer.exe_base_name | |
255 if not standalone_installer_base_name: | |
256 raise Exception('Product name not specified.') | |
257 | |
258 output_dir = '$STAGING_DIR' | |
259 if not is_official: | |
260 standalone_installer_base_name = ('UNOFFICIAL_' + | |
261 standalone_installer_base_name) | |
262 output_dir = '$TARGET_ROOT/Test_Installers' | |
263 | |
264 target_base = prefix + standalone_installer_base_name | |
265 target_name = target_base + '.exe' | |
266 log_name = target_base + '_Contents.txt' | |
267 | |
268 # Write Omaha's version. | |
269 log_text = '*** Omaha Version ***\n\n' | |
270 log_text += omaha_version_info.GetVersionString() + '\n' | |
271 | |
272 # Rename the checked in binaries by adding the application guid as the | |
273 # extension. This is needed as the meta-installer expects the | |
274 # extension. | |
275 # Also, log information about each app. | |
276 additional_payload_contents = [] | |
277 if not offline_installer.binaries: | |
278 raise Exception('No binaries specified.') | |
279 | |
280 manifest_target = '' | |
281 manifest_source = [] | |
282 version_list = [] | |
283 for binary in offline_installer.binaries: | |
284 (version, installer_path, guid) = binary | |
285 if not installer_path or not guid or not version: | |
286 raise Exception('Application specification is incomplete.') | |
287 | |
288 installer_path_modified = os.path.basename(installer_path) + '.' + guid | |
289 # Have to use Command('copy') here instead of replicate, as the | |
290 # file is being renamed in the process. | |
291 env.Command( | |
292 target=installer_path_modified, | |
293 source=installer_path, | |
294 action='@copy /y $SOURCES $TARGET' | |
295 ) | |
296 | |
297 manifest_source.extend([ | |
298 manifest_files_path + '/' + guid + '.gup', installer_path_modified]) | |
299 version_list.append(version) | |
300 additional_payload_contents.append(installer_path_modified) | |
301 | |
302 # TODO(omaha): Use full guid and version to generate unique string, use | |
303 # hash of the unique string as target directory name. | |
304 manifest_target += guid[0:4] + version | |
305 | |
306 # Log info about the app. | |
307 log_text += '\n\n*** App: ' + guid + ' ***\n' | |
308 log_text += '\nVersion:' + version + '\n' | |
309 log_text += '\nINSTALLER:\n' + installer_path + '\n' | |
310 | |
311 # Place the generated manifests in a subdirectory. This allows a single | |
312 # build to generate installers for multiple versions of the same app. | |
313 manifest_target += '/OfflineManifest.gup' | |
314 manifest_file_path = env.Command( | |
315 target=manifest_target, | |
316 source=manifest_source, | |
317 action=[_GenerateUpdateResponseFile], | |
318 INSTALLER_VERSIONS=version_list | |
319 ) | |
320 | |
321 # Use the BCJ2 tool from the official build we're using to generate this | |
322 # metainstaller, not the current build directory. | |
323 bcj2_path = omaha_files_path + '/bcj2.exe' | |
324 | |
325 additional_payload_contents.append(manifest_file_path) | |
326 | |
327 def WriteLog(target, source, env): | |
328 """Writes the log of what is being built.""" | |
329 dump_data = '' | |
330 for f in source: | |
331 file_to_dump = open(env.File(f).abspath, 'r', -1) | |
332 content = file_to_dump.read() | |
333 file_to_dump.close() | |
334 dump_data += '\nMANIFEST:\n' | |
335 dump_data += str(f) | |
336 dump_data += '\n' | |
337 dump_data += content | |
338 source = source # Avoid PyLint warning. | |
339 f = open(env.File(target[0]).abspath, 'w') | |
340 f.write(env['write_data']) | |
341 f.write(dump_data) | |
342 f.close() | |
343 return 0 | |
344 | |
345 env.Command( | |
346 target='%s/%s' % (output_dir, log_name), | |
347 source=manifest_file_path, | |
348 action=WriteLog, | |
349 write_data=log_text | |
350 ) | |
351 | |
352 results = [] | |
353 results += build_metainstaller.BuildMetaInstaller( | |
354 env=env, | |
355 target_name=target_name, | |
356 omaha_version_info=omaha_version_info, | |
357 empty_metainstaller_path=empty_metainstaller_path, | |
358 omaha_files_path=omaha_files_path, | |
359 prefix=prefix, | |
360 suffix='_' + standalone_installer_base_name, | |
361 additional_payload_contents=additional_payload_contents, | |
362 additional_payload_contents_dependencies=offline_installers_file_path, | |
363 output_dir=output_dir, | |
364 installers_sources_path=installers_sources_path, | |
365 lzma_path=lzma_path, | |
366 resmerge_path=resmerge_path, | |
367 bcj2_path=bcj2_path | |
368 ) | |
369 | |
370 standalone_installer_path = '%s/%s' % (output_dir, target_name) | |
371 | |
372 # Build an enterprise installer. | |
373 if offline_installer.should_build_enterprise_msi: | |
374 # TODO(omaha): Add support for bundles here and to | |
375 # BuildEnterpriseInstallerFromStandaloneInstaller(). | |
376 # TODO(omaha): Determine how product_version should be decided for MSI in | |
377 # bundle scenarios. | |
378 # TODO(omaha): custom tag, silent uninstall args, distribution data may need | |
379 # to be made per-app. | |
380 if 1 < len(offline_installer.binaries): | |
381 raise Exception('Enterprise installers do not currently support bundles.') | |
382 (product_version, installer_path, product_guid) = offline_installer.binaries
[0] | |
383 | |
384 # Note: msi_base_name should not include version info and cannot change! | |
385 friendly_product_name = offline_installer.friendly_product_name | |
386 msi_base_name = offline_installer.msi_base_name | |
387 custom_tag_params = offline_installer.custom_tag_params | |
388 silent_uninstall_args = offline_installer.silent_uninstall_args | |
389 msi_installer_data = offline_installer.msi_installer_data | |
390 | |
391 # custom_tag_params and msi_installer_data are optional. | |
392 if (not product_version or not friendly_product_name or not msi_base_name or | |
393 not silent_uninstall_args): | |
394 raise Exception('Field required to build enterprise MSI is missing.') | |
395 | |
396 if not is_official: | |
397 msi_base_name = ('UNOFFICIAL_' + msi_base_name) | |
398 | |
399 results += (build_enterprise_installer. | |
400 BuildEnterpriseInstallerFromStandaloneInstaller( | |
401 env, | |
402 friendly_product_name, | |
403 product_version, | |
404 product_guid, | |
405 custom_tag_params, | |
406 silent_uninstall_args, | |
407 msi_installer_data, | |
408 standalone_installer_path, | |
409 omaha_files_path + '/show_error_action.dll', | |
410 prefix + msi_base_name, | |
411 enterprise_installers_sources_path, | |
412 output_dir=output_dir | |
413 )) | |
414 | |
415 # Tag the meta-installer if an installers.txt file was specified. | |
416 if offline_installer.installers_txt_filename: | |
417 installers_txt_path = env.File( | |
418 offline_installer.installers_txt_filename).abspath | |
419 app_bundles = tag_meta_installers.ReadBundleInstallerFile( | |
420 installers_txt_path) | |
421 | |
422 bundles = {} | |
423 for (key, bundle_list) in app_bundles.items(): | |
424 if not bundle_list or not key: | |
425 continue | |
426 if not key in bundles: | |
427 bundles[key] = bundle_list | |
428 else: | |
429 new_bundles_list = bundles[key] + bundle_list | |
430 bundles[key] = new_bundles_list | |
431 | |
432 tag_meta_installers.SetOutputFileNames(target_name, bundles, '') | |
433 for bundles_lang in bundles.itervalues(): | |
434 for bundle in bundles_lang: | |
435 results += tagged_installer.TagOneBundle( | |
436 env=env, | |
437 bundle=bundle, | |
438 untagged_binary_path=standalone_installer_path, | |
439 output_dir='$TARGET_ROOT/Tagged_Offline_Installers', | |
440 ) | |
441 | |
442 return results | |
OLD | NEW |