#!/usr/bin/env python3
# Copyright 2023 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Builds a test MSI with the supplied parameters.
For example, to create `TestMsiInstaller.msi`:
```
python3 chrome/updater/test/test_installer/create_test_msi_installer.py
--candle_path third_party/wix/candle.exe
--light_path third_party/wix/light.exe
--product_name "Test MSI Installer"
--product_version 1.0.0.0
--appid {8A898FC4-7309-4D27-8BC8-7A5152227458}
--msi_base_name TestMsiInstaller
--msi_template_path chrome/updater/test/test_installer/test_installer.wxs.xml
--per_user_install
--output_dir out/Default
```
"""
import binascii
from datetime import date
import argparse
import filecmp
import os
import shutil
import subprocess
import sys
import tempfile
import uuid
def optional_flag(key, value):
return (f'-d{key}={value}', ) if value else ()
class TestInstaller:
"""Creates a test installer."""
def __init__(self, candle_path, light_path, product_name, product_version,
appid, msi_base_name, msi_template_path, company_name,
company_full_name, per_user_install, checked_in_msi,
output_dir):
self._candle_path = candle_path
self._light_path = light_path
self._product_name = product_name
self._product_version = product_version
self._appid = appid
self._msi_base_name = msi_base_name
self._msi_template_path = msi_template_path
self._company_name = company_name
self._company_full_name = company_full_name
self._per_user_install = per_user_install
self._checked_in_msi = checked_in_msi
self._output_dir = output_dir
def BuildInstaller(self):
output_directory_name = os.path.join(
self._output_dir, self._appid + '.' + self._product_version)
if not os.path.exists(output_directory_name):
os.makedirs(output_directory_name)
msi_output_path = os.path.join(output_directory_name,
self._msi_base_name + '.msi')
msi_base_file_path = os.path.splitext(self._checked_in_msi)[0]
target_wxs = msi_base_file_path + '.wxs'
if sys.platform == 'win32' and os.path.isfile(
self._candle_path) and os.path.isfile(self._light_path) and (
not os.path.isfile(target_wxs) or not filecmp.cmp(
self._msi_template_path, target_wxs, shallow=False)):
checked_in_dir = os.path.dirname(self._checked_in_msi)
if not os.path.exists(checked_in_dir):
os.makedirs(checked_in_dir)
test_installer_namespace = '{A2091DEA-AF86-4C00-8AE0-ECC38FDE6533}'
namespace_uuid = uuid.UUID(test_installer_namespace)
names_plus_version = '%s %s %s' % (
self._product_name, self._msi_base_name, self._product_version)
wix_candle_flags = [
*optional_flag('ProductName', self._product_name),
*optional_flag('ProductNameLegalIdentifier',
self._product_name.replace(' ', '')),
*optional_flag('ProductVersion', self._product_version),
*optional_flag('ProductOriginalVersionString',
self._product_version),
*optional_flag('ProductBuildYear', str(date.today().year)),
*optional_flag('ProductGuid', self._appid),
*optional_flag('CompanyName', self._company_name),
*optional_flag('CompanyFullName', self._company_full_name),
*optional_flag('PerUserInstall', self._per_user_install),
*optional_flag(
'MsiProductId',
str(
uuid.uuid5(namespace_uuid, 'Product %s' %
names_plus_version)).upper()),
*optional_flag(
'MsiUpgradeCode',
str(
uuid.uuid5(namespace_uuid,
'Upgrade ' + self._product_name)).upper()),
*optional_flag(
'ComponentGuidInstallerResultSet',
str(
uuid.uuid5(
namespace_uuid,
'Component InstallerResult Set %s' %
names_plus_version)).upper()),
*optional_flag(
'ComponentGuidInstallerErrorSet',
str(
uuid.uuid5(
namespace_uuid, 'Component InstallerError Set %s' %
names_plus_version)).upper()),
*optional_flag(
'ComponentGuidInstallerResultUIStringSet',
str(
uuid.uuid5(
namespace_uuid,
'Component InstallerResultUIString Set %s' %
names_plus_version)).upper()),
*optional_flag(
'ComponentGuidRegisterLaunchCommandSet',
str(
uuid.uuid5(
namespace_uuid,
'Component RegisterLaunchCommand Set %s' %
names_plus_version)).upper()),
]
# Disable warning LGHT1076 and internal check ICE61 on light.exe.
wix_light_flags = ['-sw1076', '-sice:ICE61']
shutil.copyfile(self._msi_template_path, target_wxs)
with tempfile.TemporaryDirectory() as temp_dir:
msi_wixobj = os.path.join(temp_dir,
self._msi_base_name + '.wixobj')
msi_pdb = os.path.join(temp_dir,
self._msi_base_name + '.wixpdb')
candle_cmd = [self._candle_path] + wix_candle_flags + [
'-nologo', '-o', msi_wixobj, target_wxs
]
subprocess.run(candle_cmd, check=True)
light_cmd = [self._light_path] + wix_light_flags + [
'-nologo', '-pdbout', msi_pdb, '-out',
self._checked_in_msi, msi_wixobj
]
subprocess.run(light_cmd, check=True)
# Copy the checked-in files to the final output path.
shutil.copyfile(self._checked_in_msi, msi_output_path)
def main():
parser = argparse.ArgumentParser(
description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument('--candle_path',
required=True,
help='path to the WiX candle.exe')
parser.add_argument('--light_path',
required=True,
help='path to the WiX light.exe')
parser.add_argument('--product_name',
required=True,
help='name of the product being built')
parser.add_argument('--product_version',
required=True,
help='product version to be installed')
parser.add_argument('--appid',
required=True,
help='updater application ID for product')
parser.add_argument('--msi_base_name',
required=True,
help='root of name for the MSI')
parser.add_argument('--msi_template_path',
required=True,
help='path to `test_installer.wxs.xml`')
parser.add_argument('--company_name',
default='Google',
help='company name for the application')
parser.add_argument('--company_full_name',
default='Google LLC',
help='company full name for the application')
parser.add_argument('--per_user_install',
action='store_true',
help='specifies that the MSI is a per-user installer')
parser.add_argument(
'--checked_in_msi',
required=True,
help='specifies the location where the MSI will live in the source tree'
)
parser.add_argument(
'--output_dir',
required=True,
help='path to the directory that will contain the resulting MSI')
args = parser.parse_args()
TestInstaller(args.candle_path, args.light_path, args.product_name,
args.product_version, args.appid, args.msi_base_name,
args.msi_template_path, args.company_name,
args.company_full_name, args.per_user_install,
args.checked_in_msi, args.output_dir).BuildInstaller()
if __name__ == '__main__':
main()