OLD | NEW |
| (Empty) |
1 #!/usr/bin/python2.4 | |
2 # | |
3 # Copyright 2009-2010 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 """Build an installer for use in enterprise situations. | |
19 | |
20 This module contains the functionality required to build enterprise | |
21 installers (MSIs) for Omaha's various customers. | |
22 | |
23 The supplied wxs templates need to have an XML extension because SCons | |
24 tries to apply WiX building rules to any input file with the .wxs suffix. | |
25 | |
26 BuildGoogleUpdateFragment(): Build an update fragment into a .wixobj. | |
27 GenerateNameBasedGUID(): Generate a GUID based on the names supplied. | |
28 BuildEnterpriseInstaller(): Build an MSI installer for use in enterprises. | |
29 """ | |
30 | |
31 import binascii | |
32 import md5 | |
33 | |
34 _GOOGLE_UPDATE_NAMESPACE_GUID = 'BE19B3E4502845af8B3E67A99FCDCFB1' | |
35 | |
36 | |
37 def BuildGoogleUpdateFragment(env, | |
38 metainstaller_path, | |
39 product_name, | |
40 product_version, | |
41 product_guid, | |
42 product_custom_params, | |
43 wixobj_base_name, | |
44 google_update_wxs_template_path): | |
45 """Build an update fragment into a WiX object. | |
46 | |
47 Takes a supplied wix fragment, and turns it into a .wixobj object for later | |
48 inclusion into an MSI. | |
49 | |
50 Args: | |
51 env: environment to build with | |
52 metainstaller_path: path to the Omaha metainstaller to include | |
53 product_name: name of the product the fragment is being built for | |
54 product_version: product version to be installed | |
55 product_guid: Omaha application ID of the product the fragment is being | |
56 built for | |
57 product_custom_params: custom values to be appended to the Omaha tag | |
58 wixobj_base_name: root of name for the wixobj | |
59 google_update_wxs_template_path: path to the fragment source | |
60 | |
61 Returns: | |
62 Output object for the built wixobj. | |
63 | |
64 Raises: | |
65 Nothing. | |
66 """ | |
67 | |
68 product_name_legal_identifier = product_name.replace(' ', '') | |
69 | |
70 intermediate_base_name = wixobj_base_name + '_google_update_fragment' | |
71 | |
72 copy_target = env.Command( | |
73 target=intermediate_base_name + '.wxs', | |
74 source=google_update_wxs_template_path, | |
75 action='@copy /y $SOURCE $TARGET', | |
76 ) | |
77 | |
78 wix_defines = [ | |
79 '-dProductName="%s"' % product_name, | |
80 '-dProductNameLegalIdentifier="%s"' % product_name_legal_identifier, | |
81 '-dProductVersion=' + product_version, | |
82 '-dProductGuid="%s"' % product_guid, | |
83 '-dProductCustomParams="%s"' % product_custom_params, | |
84 '-dGoogleUpdateMetainstallerPath="%s"' % ( | |
85 env.File(metainstaller_path).abspath), | |
86 ] | |
87 | |
88 wixobj_output = env.Command( | |
89 target=intermediate_base_name + '.wixobj', | |
90 source=copy_target, | |
91 action='@candle.exe -nologo -out $TARGET $SOURCE ' + ' '.join(wix_defines) | |
92 ) | |
93 | |
94 # Force a rebuild of the .wixobj file when the metainstaller changes. | |
95 # Does not necessarily force rebuild of the MSI because hash does not change. | |
96 env.Depends(wixobj_output, metainstaller_path) | |
97 | |
98 return wixobj_output | |
99 | |
100 | |
101 def _BuildMsiForExe(env, | |
102 product_name, | |
103 product_version, | |
104 product_guid, | |
105 product_installer_path, | |
106 product_installer_install_command, | |
107 product_installer_disable_update_registration_arg, | |
108 product_uninstaller_additional_args, | |
109 msi_base_name, | |
110 google_update_wixobj_output, | |
111 enterprise_installer_dir, | |
112 show_error_action_dll_path, | |
113 metainstaller_path, | |
114 output_dir): | |
115 """Build an MSI installer for use in enterprise situations. | |
116 | |
117 Builds an MSI for the executable installer at product_installer_path using | |
118 the supplied details. Requires an existing Google Update installer fragment | |
119 as well as a path to a custom action DLL containing the logic to launch the | |
120 product's uninstaller. | |
121 | |
122 This is intended to enable enterprise installation scenarios. | |
123 | |
124 Args: | |
125 env: environment to build with | |
126 product_name: name of the product being built | |
127 product_version: product version to be installed | |
128 product_guid: product's Omaha application ID | |
129 product_installer_path: path to specific product installer | |
130 product_installer_install_command: command line args used to run product | |
131 installer in 'install' mode | |
132 product_installer_disable_update_registration_arg: command line args used | |
133 to run product installer in 'do not register' mode | |
134 product_uninstaller_additional_args: extra command line parameters that the | |
135 custom action dll will pass on to the product uninstaller, typically | |
136 you'll want to pass any extra arguments that will force the uninstaller | |
137 to run silently here. | |
138 msi_base_name: root of name for the MSI | |
139 google_update_wixobj_output: the MSI fragment containing the Omaha | |
140 installer. | |
141 enterprise_installer_dir: path to dir which contains | |
142 enterprise_installer.wxs.xml | |
143 show_error_action_dll_path: path to the error display custom action dll that | |
144 exports a ShowInstallerResultUIString method. This CA method will read | |
145 the LastInstallerResultUIString from the product's ClientState key in | |
146 the registry and display the string via MsiProcessMessage. | |
147 metainstaller_path: path to the Omaha metainstaller. Should be same file | |
148 used for google_update_wixobj_output. Used only to force rebuilds. | |
149 output_dir: path to the directory that will contain the resulting MSI | |
150 | |
151 Returns: | |
152 Nothing. | |
153 | |
154 Raises: | |
155 Nothing. | |
156 """ | |
157 | |
158 product_name_legal_identifier = product_name.replace(' ', '') | |
159 msi_name = msi_base_name + '.msi' | |
160 | |
161 omaha_installer_namespace = binascii.a2b_hex(_GOOGLE_UPDATE_NAMESPACE_GUID) | |
162 | |
163 # Include the .msi filename in the Product Code generation because "the | |
164 # product code must be changed if... the name of the .msi file has been | |
165 # changed" according to http://msdn.microsoft.com/en-us/library/aa367850.aspx. | |
166 msi_product_id = GenerateNameBasedGUID( | |
167 omaha_installer_namespace, | |
168 'Product %s %s' % (product_name, msi_base_name) | |
169 ) | |
170 msi_upgradecode_guid = GenerateNameBasedGUID( | |
171 omaha_installer_namespace, | |
172 'Upgrade ' + product_name | |
173 ) | |
174 | |
175 copy_target = env.Command( | |
176 target=msi_base_name + '.wxs', | |
177 source=enterprise_installer_dir + '/enterprise_installer.wxs.xml', | |
178 action='@copy /y $SOURCE $TARGET', | |
179 ) | |
180 | |
181 # Disable warning LGHT1076 and internal check ICE61 on light.exe. Details: | |
182 # http://blogs.msdn.com/astebner/archive/2007/02/13/building-an-msi-using-wix-
v3-0-that-includes-the-vc-8-0-runtime-merge-modules.aspx | |
183 # http://windows-installer-xml-wix-toolset.687559.n2.nabble.com/ICE61-Upgrade-
VersionMax-format-is-wrong-td4396813.html # pylint: disable-msg=C6310 | |
184 wix_env = env.Clone() | |
185 wix_env.Append( | |
186 WIXCANDLEFLAGS=[ | |
187 '-dProductName=' + product_name, | |
188 '-dProductNameLegalIdentifier=' + product_name_legal_identifier, | |
189 '-dProductVersion=' + product_version, | |
190 '-dProductGuid=' + product_guid, | |
191 '-dProductInstallerPath=' + env.File(product_installer_path).abspath, | |
192 '-dProductInstallerInstallCommand=' + ( | |
193 product_installer_install_command), | |
194 '-dProductInstallerDisableUpdateRegistrationArg=' + ( | |
195 product_installer_disable_update_registration_arg), | |
196 '-dShowErrorCADll=' + env.File(show_error_action_dll_path).abspath, | |
197 '-dProductUninstallerAdditionalArgs=' + ( | |
198 product_uninstaller_additional_args), | |
199 '-dMsiProductId=' + msi_product_id, | |
200 '-dMsiUpgradeCode=' + msi_upgradecode_guid, | |
201 ], | |
202 WIXLIGHTFLAGS=[ | |
203 '-sw1076', | |
204 '-sice:ICE61', | |
205 ], | |
206 ) | |
207 | |
208 wix_output = wix_env.WiX( | |
209 target='unsigned_' + msi_name, | |
210 source=[copy_target, google_update_wixobj_output], | |
211 ) | |
212 | |
213 # Force a rebuild when the installer or metainstaller changes. | |
214 # The metainstaller change does not get passed through even though the .wixobj | |
215 # file is rebuilt because the hash of the .wixobj does not change. | |
216 # Also force a dependency on the CA DLL. Otherwise, it might not be built | |
217 # before the MSI. | |
218 wix_env.Depends(wix_output, [product_installer_path, | |
219 metainstaller_path, | |
220 show_error_action_dll_path]) | |
221 | |
222 sign_output = wix_env.SignedBinary( | |
223 target=msi_name, | |
224 source=wix_output, | |
225 ) | |
226 | |
227 env.Replicate(output_dir, sign_output) | |
228 | |
229 | |
230 def GenerateNameBasedGUID(namespace, name): | |
231 """Generate a GUID based on the names supplied. | |
232 | |
233 Follows a methodology recommended in Section 4.3 of RFC 4122 to generate | |
234 a "name-based UUID," which basically means that you want to control the | |
235 inputs to the GUID so that you can generate the same valid GUID each time | |
236 given the same inputs. | |
237 | |
238 Args: | |
239 namespace: First part of identifier used to generate GUID | |
240 name: Second part of identifier used to generate GUID | |
241 | |
242 Returns: | |
243 String representation of the generated GUID. | |
244 | |
245 Raises: | |
246 Nothing. | |
247 """ | |
248 | |
249 # Generate 128 unique bits. | |
250 mymd5 = md5.new() | |
251 mymd5.update(namespace + name) | |
252 md5_hash = mymd5.digest() | |
253 | |
254 # Set various reserved bits to make this a valid GUID. | |
255 | |
256 # "Set the four most significant bits (bits 12 through 15) of the | |
257 # time_hi_and_version field to the appropriate 4-bit version number | |
258 # from Section 4.1.3." | |
259 version = ord(md5_hash[6]) | |
260 version = 0x30 | (version & 0x0f) | |
261 | |
262 # "Set the two most significant bits (bits 6 and 7) of the | |
263 # clock_seq_hi_and_reserved to zero and one, respectively." | |
264 clock_seq_hi_and_reserved = ord(md5_hash[8]) | |
265 clock_seq_hi_and_reserved = 0x80 | (clock_seq_hi_and_reserved & 0x3f) | |
266 | |
267 return ( | |
268 '%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x' % ( | |
269 ord(md5_hash[0]), ord(md5_hash[1]), ord(md5_hash[2]), | |
270 ord(md5_hash[3]), | |
271 ord(md5_hash[4]), ord(md5_hash[5]), | |
272 version, ord(md5_hash[7]), | |
273 clock_seq_hi_and_reserved, ord(md5_hash[9]), | |
274 ord(md5_hash[10]), ord(md5_hash[11]), ord(md5_hash[12]), | |
275 ord(md5_hash[13]), ord(md5_hash[14]), ord(md5_hash[15]))) | |
276 | |
277 | |
278 def ConvertToMSIVersionNumberIfNeeded(product_version): | |
279 """Change product_version to fit in an MSI version number if needed. | |
280 | |
281 Some products use a 4-field version numbering scheme whereas MSI looks only | |
282 at the first three fields when considering version numbers. Furthermore, MSI | |
283 version fields have documented width restrictions of 8bits.8bits.16bits as | |
284 per http://msdn.microsoft.com/en-us/library/aa370859(VS.85).aspx | |
285 | |
286 As such, the following scheme is used: | |
287 | |
288 Product a.b.c.d -> a.(c>>8).(((c & 0xFF) << 8) + d) | |
289 | |
290 So eg. 6.1.420.8 would become 6.1.41992. | |
291 | |
292 This assumes: | |
293 1) we don't care about the product minor number, e.g. we will never reset | |
294 the 'c' number after an increase in 'b'. | |
295 2) 'd' will always be <= 255 | |
296 3) 'c' is <= 65535 | |
297 | |
298 As a final note, if product_version is not of the format a.b.c.d then | |
299 this function returns the original product_version value. | |
300 """ | |
301 | |
302 try: | |
303 version_field_strings = product_version.split('.') | |
304 (major, minor, build, patch) = [int(x) for x in version_field_strings] | |
305 except: | |
306 # Couldn't parse the version number as a 4-term period-separated number, | |
307 # just return the original string. | |
308 return product_version | |
309 | |
310 # Input version number was out of range. Return the original string. | |
311 if patch > 255 or build > 65535: | |
312 return product_string | |
313 | |
314 msi_major = major | |
315 msi_minor = build >> 8 | |
316 msi_build = ((build & 0xff) << 8) + patch | |
317 | |
318 return str(msi_major) + '.' + str(msi_minor) + '.' + str(msi_build) | |
319 | |
320 | |
321 def BuildEnterpriseInstaller(env, | |
322 product_name, | |
323 product_version, | |
324 product_guid, | |
325 product_custom_params, | |
326 product_installer_path, | |
327 product_installer_install_command, | |
328 product_installer_disable_update_registration_arg, | |
329 product_uninstaller_additional_args, | |
330 msi_base_name, | |
331 enterprise_installer_dir, | |
332 show_error_action_dll_path, | |
333 metainstaller_path, | |
334 output_dir='$STAGING_DIR'): | |
335 """Build an installer for use in enterprise situations. | |
336 | |
337 Builds an MSI using the supplied details and binaries. This MSI is | |
338 intended to enable enterprise installation scenarios. | |
339 | |
340 Args: | |
341 env: environment to build with | |
342 product_name: name of the product being built | |
343 product_version: product version to be installed | |
344 product_guid: product's Omaha application ID | |
345 product_custom_params: custom values to be appended to the Omaha tag | |
346 product_installer_path: path to specific product installer | |
347 product_installer_install_command: command line args used to run product | |
348 installer in 'install' mode | |
349 product_installer_disable_update_registration_arg: command line args used | |
350 to run product installer in 'do not register' mode | |
351 product_uninstaller_additional_args: extra command line parameters that the | |
352 custom action dll will pass on to the product uninstaller, typically | |
353 you'll want to pass any extra arguments that will force the uninstaller | |
354 to run silently here. | |
355 msi_base_name: root of name for the MSI | |
356 enterprise_installer_dir: path to dir which contains | |
357 enterprise_installer.wxs.xml | |
358 show_error_action_dll_path: path to the error display custom action dll that | |
359 exports a ShowInstallerResultUIString method. This CA method will read | |
360 the LastInstallerResultUIString from the product's ClientState key in | |
361 the registry and display the string via MsiProcessMessage. | |
362 metainstaller_path: path to the Omaha metainstaller to include | |
363 output_dir: path to the directory that will contain the resulting MSI | |
364 | |
365 Returns: | |
366 Nothing. | |
367 | |
368 Raises: | |
369 Nothing. | |
370 """ | |
371 product_version = ConvertToMSIVersionNumberIfNeeded(product_version) | |
372 | |
373 google_update_wixobj_output = BuildGoogleUpdateFragment( | |
374 env, | |
375 metainstaller_path, | |
376 product_name, | |
377 product_version, | |
378 product_guid, | |
379 product_custom_params, | |
380 msi_base_name, | |
381 enterprise_installer_dir + '/google_update_installer_fragment.wxs.xml') | |
382 | |
383 _BuildMsiForExe( | |
384 env, | |
385 product_name, | |
386 product_version, | |
387 product_guid, | |
388 product_installer_path, | |
389 product_installer_install_command, | |
390 product_installer_disable_update_registration_arg, | |
391 product_uninstaller_additional_args, | |
392 msi_base_name, | |
393 google_update_wixobj_output, | |
394 enterprise_installer_dir, | |
395 show_error_action_dll_path, | |
396 metainstaller_path, | |
397 output_dir) | |
398 | |
399 | |
400 def BuildEnterpriseInstallerFromStandaloneInstaller( | |
401 env, | |
402 product_name, | |
403 product_version, | |
404 product_guid, | |
405 product_custom_params, | |
406 product_uninstaller_additional_args, | |
407 product_installer_data, | |
408 standalone_installer_path, | |
409 show_error_action_dll_path, | |
410 msi_base_name, | |
411 enterprise_installer_dir, | |
412 output_dir='$STAGING_DIR'): | |
413 """Build an installer for use in enterprise situations. | |
414 | |
415 Builds an MSI around the supplied standalone installer. This MSI is | |
416 intended to enable enterprise installation scenarios while being as close | |
417 to a normal install as possible. It does not suffer from the separation of | |
418 Omaha and application install like the other methods do. | |
419 | |
420 This method only works for installers that do not use an MSI. | |
421 | |
422 Args: | |
423 env: environment to build with | |
424 product_name: name of the product being built | |
425 product_version: product version to be installed | |
426 product_guid: product's Omaha application ID | |
427 product_custom_params: custom values to be appended to the Omaha tag | |
428 product_uninstaller_additional_args: extra command line parameters that the | |
429 custom action dll will pass on to the product uninstaller, typically | |
430 you'll want to pass any extra arguments that will force the uninstaller | |
431 to run silently here. | |
432 product_installer_data: installer data to be passed to the | |
433 product installer at run time. This is useful as an alternative to | |
434 the product_installer_install_command parameter accepted by | |
435 BuildEnterpriseInstaller() since command line parameters can't be | |
436 passed to the product installer when it is wrapped in a standalone | |
437 installer. | |
438 standalone_installer_path: path to product's standalone installer | |
439 show_error_action_dll_path: path to the error display custom action dll that | |
440 exports a ShowInstallerResultUIString method. This CA method will read | |
441 the LastInstallerResultUIString from the product's ClientState key in | |
442 the registry and display the string via MsiProcessMessage. | |
443 msi_base_name: root of name for the MSI | |
444 enterprise_installer_dir: path to dir which contains | |
445 enterprise_standalone_installer.wxs.xml | |
446 output_dir: path to the directory that will contain the resulting MSI | |
447 | |
448 Returns: | |
449 Target nodes. | |
450 | |
451 Raises: | |
452 Nothing. | |
453 """ | |
454 product_name_legal_identifier = product_name.replace(' ', '') | |
455 msi_name = msi_base_name + '.msi' | |
456 product_version = ConvertToMSIVersionNumberIfNeeded(product_version) | |
457 | |
458 omaha_installer_namespace = binascii.a2b_hex(_GOOGLE_UPDATE_NAMESPACE_GUID) | |
459 | |
460 # Include the .msi filename in the Product Code generation because "the | |
461 # product code must be changed if... the name of the .msi file has been | |
462 # changed" according to http://msdn.microsoft.com/en-us/library/aa367850.aspx. | |
463 msi_product_id = GenerateNameBasedGUID( | |
464 omaha_installer_namespace, | |
465 'Product %s %s' % (product_name, msi_base_name) | |
466 ) | |
467 msi_upgradecode_guid = GenerateNameBasedGUID( | |
468 omaha_installer_namespace, | |
469 'Upgrade ' + product_name | |
470 ) | |
471 | |
472 # To allow for multiple versions of the same product to be generated, | |
473 # stick output in a subdirectory. | |
474 output_directory_name = product_guid + '.' + product_version | |
475 | |
476 copy_target = env.Command( | |
477 target=output_directory_name + msi_base_name + '.wxs', | |
478 source=(enterprise_installer_dir + | |
479 '/enterprise_standalone_installer.wxs.xml'), | |
480 action='@copy /y $SOURCE $TARGET', | |
481 ) | |
482 | |
483 wix_env = env.Clone() | |
484 wix_candle_flags = [ | |
485 '-dProductName=' + product_name, | |
486 '-dProductNameLegalIdentifier=' + product_name_legal_identifier, | |
487 '-dProductVersion=' + product_version, | |
488 '-dProductGuid="%s"' % product_guid, | |
489 '-dProductCustomParams="%s"' % product_custom_params, | |
490 '-dStandaloneInstallerPath=' + ( | |
491 env.File(standalone_installer_path).abspath), | |
492 '-dShowErrorCADll=' + env.File(show_error_action_dll_path).abspath, | |
493 '-dProductUninstallerAdditionalArgs=' + ( | |
494 product_uninstaller_additional_args), | |
495 '-dMsiProductId=' + msi_product_id, | |
496 '-dMsiUpgradeCode=' + msi_upgradecode_guid, | |
497 ] | |
498 | |
499 if product_installer_data: | |
500 wix_candle_flags.append('-dProductInstallerData=' + product_installer_data) | |
501 | |
502 wix_light_flags = [ | |
503 '-sw1076', | |
504 '-sice:ICE61', | |
505 ] | |
506 | |
507 wix_env.Append( | |
508 WIXCANDLEFLAGS=wix_candle_flags, | |
509 WIXLIGHTFLAGS=wix_light_flags | |
510 ) | |
511 | |
512 wix_output = wix_env.WiX( | |
513 target = output_directory_name + '/' + 'unsigned_' + msi_name, | |
514 source = [copy_target], | |
515 ) | |
516 | |
517 # Force a rebuild when the standalone installer changes. | |
518 # The metainstaller change does not get passed through even though the .wixobj | |
519 # file is rebuilt because the hash of the .wixobj does not change. | |
520 # Also force a dependency on the CA DLL. Otherwise, it might not be built | |
521 # before the MSI. | |
522 wix_env.Depends(wix_output, [standalone_installer_path, | |
523 show_error_action_dll_path]) | |
524 | |
525 sign_output = wix_env.SignedBinary( | |
526 target=output_directory_name + '/' + msi_name, | |
527 source=wix_output, | |
528 ) | |
529 | |
530 return env.Replicate(output_dir + '/' + output_directory_name, sign_output) | |
OLD | NEW |