blob: d09d47d6a6fdb84a6802de58b55586a0182eb40d [file] [log] [blame]
#!/usr/bin/python2.4
#
# Copyright 2009-2010 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========================================================================
"""Build an installer for use in enterprise situations.
This module contains the functionality required to build enterprise
installers (MSIs) for Omaha's various customers.
The supplied wxs templates need to have an XML extension because SCons
tries to apply WiX building rules to any input file with the .wxs suffix.
BuildGoogleUpdateFragment(): Build an update fragment into a .wixobj.
GenerateNameBasedGUID(): Generate a GUID based on the names supplied.
BuildEnterpriseInstaller(): Build an MSI installer for use in enterprises.
"""
import binascii
import md5
_GOOGLE_UPDATE_NAMESPACE_GUID = 'BE19B3E4502845af8B3E67A99FCDCFB1'
def BuildGoogleUpdateFragment(env,
metainstaller_path,
product_name,
product_version,
product_guid,
product_custom_params,
wixobj_base_name,
google_update_wxs_template_path):
"""Build an update fragment into a WiX object.
Takes a supplied wix fragment, and turns it into a .wixobj object for later
inclusion into an MSI.
Args:
env: environment to build with
metainstaller_path: path to the Omaha metainstaller to include
product_name: name of the product the fragment is being built for
product_version: product version to be installed
product_guid: Omaha application ID of the product the fragment is being
built for
product_custom_params: custom values to be appended to the Omaha tag
wixobj_base_name: root of name for the wixobj
google_update_wxs_template_path: path to the fragment source
Returns:
Output object for the built wixobj.
Raises:
Nothing.
"""
product_name_legal_identifier = product_name.replace(' ', '')
intermediate_base_name = wixobj_base_name + '_google_update_fragment'
copy_target = env.Command(
target=intermediate_base_name + '.wxs',
source=google_update_wxs_template_path,
action='@copy /y $SOURCE $TARGET',
)
wix_defines = [
'-dProductName="%s"' % product_name,
'-dProductNameLegalIdentifier="%s"' % product_name_legal_identifier,
'-dProductVersion=' + product_version,
'-dProductGuid="%s"' % product_guid,
'-dProductCustomParams="%s"' % product_custom_params,
'-dGoogleUpdateMetainstallerPath="%s"' % (
env.File(metainstaller_path).abspath),
]
wixobj_output = env.Command(
target=intermediate_base_name + '.wixobj',
source=copy_target,
action='@candle.exe -nologo -out $TARGET $SOURCE ' + ' '.join(wix_defines)
)
# Force a rebuild of the .wixobj file when the metainstaller changes.
# Does not necessarily force rebuild of the MSI because hash does not change.
env.Depends(wixobj_output, metainstaller_path)
return wixobj_output
def _BuildMsiForExe(env,
product_name,
product_version,
product_guid,
product_installer_path,
product_installer_install_command,
product_installer_disable_update_registration_arg,
product_uninstaller_additional_args,
msi_base_name,
google_update_wixobj_output,
enterprise_installer_dir,
show_error_action_dll_path,
metainstaller_path,
output_dir):
"""Build an MSI installer for use in enterprise situations.
Builds an MSI for the executable installer at product_installer_path using
the supplied details. Requires an existing Google Update installer fragment
as well as a path to a custom action DLL containing the logic to launch the
product's uninstaller.
This is intended to enable enterprise installation scenarios.
Args:
env: environment to build with
product_name: name of the product being built
product_version: product version to be installed
product_guid: product's Omaha application ID
product_installer_path: path to specific product installer
product_installer_install_command: command line args used to run product
installer in 'install' mode
product_installer_disable_update_registration_arg: command line args used
to run product installer in 'do not register' mode
product_uninstaller_additional_args: extra command line parameters that the
custom action dll will pass on to the product uninstaller, typically
you'll want to pass any extra arguments that will force the uninstaller
to run silently here.
msi_base_name: root of name for the MSI
google_update_wixobj_output: the MSI fragment containing the Omaha
installer.
enterprise_installer_dir: path to dir which contains
enterprise_installer.wxs.xml
show_error_action_dll_path: path to the error display custom action dll that
exports a ShowInstallerResultUIString method. This CA method will read
the LastInstallerResultUIString from the product's ClientState key in
the registry and display the string via MsiProcessMessage.
metainstaller_path: path to the Omaha metainstaller. Should be same file
used for google_update_wixobj_output. Used only to force rebuilds.
output_dir: path to the directory that will contain the resulting MSI
Returns:
Nothing.
Raises:
Nothing.
"""
product_name_legal_identifier = product_name.replace(' ', '')
msi_name = msi_base_name + '.msi'
omaha_installer_namespace = binascii.a2b_hex(_GOOGLE_UPDATE_NAMESPACE_GUID)
# Include the .msi filename in the Product Code generation because "the
# product code must be changed if... the name of the .msi file has been
# changed" according to http://msdn.microsoft.com/en-us/library/aa367850.aspx.
msi_product_id = GenerateNameBasedGUID(
omaha_installer_namespace,
'Product %s %s' % (product_name, msi_base_name)
)
msi_upgradecode_guid = GenerateNameBasedGUID(
omaha_installer_namespace,
'Upgrade ' + product_name
)
copy_target = env.Command(
target=msi_base_name + '.wxs',
source=enterprise_installer_dir + '/enterprise_installer.wxs.xml',
action='@copy /y $SOURCE $TARGET',
)
# Disable warning LGHT1076 and internal check ICE61 on light.exe. Details:
# 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
# http://windows-installer-xml-wix-toolset.687559.n2.nabble.com/ICE61-Upgrade-VersionMax-format-is-wrong-td4396813.html # pylint: disable-msg=C6310
wix_env = env.Clone()
wix_env.Append(
WIXCANDLEFLAGS=[
'-dProductName=' + product_name,
'-dProductNameLegalIdentifier=' + product_name_legal_identifier,
'-dProductVersion=' + product_version,
'-dProductGuid=' + product_guid,
'-dProductInstallerPath=' + env.File(product_installer_path).abspath,
'-dProductInstallerInstallCommand=' + (
product_installer_install_command),
'-dProductInstallerDisableUpdateRegistrationArg=' + (
product_installer_disable_update_registration_arg),
'-dShowErrorCADll=' + env.File(show_error_action_dll_path).abspath,
'-dProductUninstallerAdditionalArgs=' + (
product_uninstaller_additional_args),
'-dMsiProductId=' + msi_product_id,
'-dMsiUpgradeCode=' + msi_upgradecode_guid,
],
WIXLIGHTFLAGS=[
'-sw1076',
'-sice:ICE61',
],
)
wix_output = wix_env.WiX(
target='unsigned_' + msi_name,
source=[copy_target, google_update_wixobj_output],
)
# Force a rebuild when the installer or metainstaller changes.
# The metainstaller change does not get passed through even though the .wixobj
# file is rebuilt because the hash of the .wixobj does not change.
# Also force a dependency on the CA DLL. Otherwise, it might not be built
# before the MSI.
wix_env.Depends(wix_output, [product_installer_path,
metainstaller_path,
show_error_action_dll_path])
sign_output = wix_env.SignedBinary(
target=msi_name,
source=wix_output,
)
env.Replicate(output_dir, sign_output)
def GenerateNameBasedGUID(namespace, name):
"""Generate a GUID based on the names supplied.
Follows a methodology recommended in Section 4.3 of RFC 4122 to generate
a "name-based UUID," which basically means that you want to control the
inputs to the GUID so that you can generate the same valid GUID each time
given the same inputs.
Args:
namespace: First part of identifier used to generate GUID
name: Second part of identifier used to generate GUID
Returns:
String representation of the generated GUID.
Raises:
Nothing.
"""
# Generate 128 unique bits.
mymd5 = md5.new()
mymd5.update(namespace + name)
md5_hash = mymd5.digest()
# Set various reserved bits to make this a valid GUID.
# "Set the four most significant bits (bits 12 through 15) of the
# time_hi_and_version field to the appropriate 4-bit version number
# from Section 4.1.3."
version = ord(md5_hash[6])
version = 0x30 | (version & 0x0f)
# "Set the two most significant bits (bits 6 and 7) of the
# clock_seq_hi_and_reserved to zero and one, respectively."
clock_seq_hi_and_reserved = ord(md5_hash[8])
clock_seq_hi_and_reserved = 0x80 | (clock_seq_hi_and_reserved & 0x3f)
return (
'%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x' % (
ord(md5_hash[0]), ord(md5_hash[1]), ord(md5_hash[2]),
ord(md5_hash[3]),
ord(md5_hash[4]), ord(md5_hash[5]),
version, ord(md5_hash[7]),
clock_seq_hi_and_reserved, ord(md5_hash[9]),
ord(md5_hash[10]), ord(md5_hash[11]), ord(md5_hash[12]),
ord(md5_hash[13]), ord(md5_hash[14]), ord(md5_hash[15])))
def ConvertToMSIVersionNumberIfNeeded(product_version):
"""Change product_version to fit in an MSI version number if needed.
Some products use a 4-field version numbering scheme whereas MSI looks only
at the first three fields when considering version numbers. Furthermore, MSI
version fields have documented width restrictions of 8bits.8bits.16bits as
per http://msdn.microsoft.com/en-us/library/aa370859(VS.85).aspx
As such, the following scheme is used:
Product a.b.c.d -> a.(c>>8).(((c & 0xFF) << 8) + d)
So eg. 6.1.420.8 would become 6.1.41992.
This assumes:
1) we don't care about the product minor number, e.g. we will never reset
the 'c' number after an increase in 'b'.
2) 'd' will always be <= 255
3) 'c' is <= 65535
As a final note, if product_version is not of the format a.b.c.d then
this function returns the original product_version value.
"""
try:
version_field_strings = product_version.split('.')
(major, minor, build, patch) = [int(x) for x in version_field_strings]
except:
# Couldn't parse the version number as a 4-term period-separated number,
# just return the original string.
return product_version
# Input version number was out of range. Return the original string.
if patch > 255 or build > 65535:
return product_string
msi_major = major
msi_minor = build >> 8
msi_build = ((build & 0xff) << 8) + patch
return str(msi_major) + '.' + str(msi_minor) + '.' + str(msi_build)
def BuildEnterpriseInstaller(env,
product_name,
product_version,
product_guid,
product_custom_params,
product_installer_path,
product_installer_install_command,
product_installer_disable_update_registration_arg,
product_uninstaller_additional_args,
msi_base_name,
enterprise_installer_dir,
show_error_action_dll_path,
metainstaller_path,
output_dir='$STAGING_DIR'):
"""Build an installer for use in enterprise situations.
Builds an MSI using the supplied details and binaries. This MSI is
intended to enable enterprise installation scenarios.
Args:
env: environment to build with
product_name: name of the product being built
product_version: product version to be installed
product_guid: product's Omaha application ID
product_custom_params: custom values to be appended to the Omaha tag
product_installer_path: path to specific product installer
product_installer_install_command: command line args used to run product
installer in 'install' mode
product_installer_disable_update_registration_arg: command line args used
to run product installer in 'do not register' mode
product_uninstaller_additional_args: extra command line parameters that the
custom action dll will pass on to the product uninstaller, typically
you'll want to pass any extra arguments that will force the uninstaller
to run silently here.
msi_base_name: root of name for the MSI
enterprise_installer_dir: path to dir which contains
enterprise_installer.wxs.xml
show_error_action_dll_path: path to the error display custom action dll that
exports a ShowInstallerResultUIString method. This CA method will read
the LastInstallerResultUIString from the product's ClientState key in
the registry and display the string via MsiProcessMessage.
metainstaller_path: path to the Omaha metainstaller to include
output_dir: path to the directory that will contain the resulting MSI
Returns:
Nothing.
Raises:
Nothing.
"""
product_version = ConvertToMSIVersionNumberIfNeeded(product_version)
google_update_wixobj_output = BuildGoogleUpdateFragment(
env,
metainstaller_path,
product_name,
product_version,
product_guid,
product_custom_params,
msi_base_name,
enterprise_installer_dir + '/google_update_installer_fragment.wxs.xml')
_BuildMsiForExe(
env,
product_name,
product_version,
product_guid,
product_installer_path,
product_installer_install_command,
product_installer_disable_update_registration_arg,
product_uninstaller_additional_args,
msi_base_name,
google_update_wixobj_output,
enterprise_installer_dir,
show_error_action_dll_path,
metainstaller_path,
output_dir)
def BuildEnterpriseInstallerFromStandaloneInstaller(
env,
product_name,
product_version,
product_guid,
product_custom_params,
product_uninstaller_additional_args,
product_installer_data,
standalone_installer_path,
show_error_action_dll_path,
msi_base_name,
enterprise_installer_dir,
output_dir='$STAGING_DIR'):
"""Build an installer for use in enterprise situations.
Builds an MSI around the supplied standalone installer. This MSI is
intended to enable enterprise installation scenarios while being as close
to a normal install as possible. It does not suffer from the separation of
Omaha and application install like the other methods do.
This method only works for installers that do not use an MSI.
Args:
env: environment to build with
product_name: name of the product being built
product_version: product version to be installed
product_guid: product's Omaha application ID
product_custom_params: custom values to be appended to the Omaha tag
product_uninstaller_additional_args: extra command line parameters that the
custom action dll will pass on to the product uninstaller, typically
you'll want to pass any extra arguments that will force the uninstaller
to run silently here.
product_installer_data: installer data to be passed to the
product installer at run time. This is useful as an alternative to
the product_installer_install_command parameter accepted by
BuildEnterpriseInstaller() since command line parameters can't be
passed to the product installer when it is wrapped in a standalone
installer.
standalone_installer_path: path to product's standalone installer
show_error_action_dll_path: path to the error display custom action dll that
exports a ShowInstallerResultUIString method. This CA method will read
the LastInstallerResultUIString from the product's ClientState key in
the registry and display the string via MsiProcessMessage.
msi_base_name: root of name for the MSI
enterprise_installer_dir: path to dir which contains
enterprise_standalone_installer.wxs.xml
output_dir: path to the directory that will contain the resulting MSI
Returns:
Target nodes.
Raises:
Nothing.
"""
product_name_legal_identifier = product_name.replace(' ', '')
msi_name = msi_base_name + '.msi'
product_version = ConvertToMSIVersionNumberIfNeeded(product_version)
omaha_installer_namespace = binascii.a2b_hex(_GOOGLE_UPDATE_NAMESPACE_GUID)
# Include the .msi filename in the Product Code generation because "the
# product code must be changed if... the name of the .msi file has been
# changed" according to http://msdn.microsoft.com/en-us/library/aa367850.aspx.
msi_product_id = GenerateNameBasedGUID(
omaha_installer_namespace,
'Product %s %s' % (product_name, msi_base_name)
)
msi_upgradecode_guid = GenerateNameBasedGUID(
omaha_installer_namespace,
'Upgrade ' + product_name
)
# To allow for multiple versions of the same product to be generated,
# stick output in a subdirectory.
output_directory_name = product_guid + '.' + product_version
copy_target = env.Command(
target=output_directory_name + msi_base_name + '.wxs',
source=(enterprise_installer_dir +
'/enterprise_standalone_installer.wxs.xml'),
action='@copy /y $SOURCE $TARGET',
)
wix_env = env.Clone()
wix_candle_flags = [
'-dProductName=' + product_name,
'-dProductNameLegalIdentifier=' + product_name_legal_identifier,
'-dProductVersion=' + product_version,
'-dProductGuid="%s"' % product_guid,
'-dProductCustomParams="%s"' % product_custom_params,
'-dStandaloneInstallerPath=' + (
env.File(standalone_installer_path).abspath),
'-dShowErrorCADll=' + env.File(show_error_action_dll_path).abspath,
'-dProductUninstallerAdditionalArgs=' + (
product_uninstaller_additional_args),
'-dMsiProductId=' + msi_product_id,
'-dMsiUpgradeCode=' + msi_upgradecode_guid,
]
if product_installer_data:
wix_candle_flags.append('-dProductInstallerData=' + product_installer_data)
wix_light_flags = [
'-sw1076',
'-sice:ICE61',
]
wix_env.Append(
WIXCANDLEFLAGS=wix_candle_flags,
WIXLIGHTFLAGS=wix_light_flags
)
wix_output = wix_env.WiX(
target = output_directory_name + '/' + 'unsigned_' + msi_name,
source = [copy_target],
)
# Force a rebuild when the standalone installer changes.
# The metainstaller change does not get passed through even though the .wixobj
# file is rebuilt because the hash of the .wixobj does not change.
# Also force a dependency on the CA DLL. Otherwise, it might not be built
# before the MSI.
wix_env.Depends(wix_output, [standalone_installer_path,
show_error_action_dll_path])
sign_output = wix_env.SignedBinary(
target=output_directory_name + '/' + msi_name,
source=wix_output,
)
return env.Replicate(output_dir + '/' + output_directory_name, sign_output)