| 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 |