#!/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()
