#!/usr/bin/env python
# Copyright 2018 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
#
# This script builds the credential provider installer that is used to install
# all required components of the Google Credential Provider for Windows. The
# installer is a 7-zip self extracting executable file that wraps three main
# parts:
#
# - the Credential Provider COM DLL
# - a DLL that contains Windows EventLog message formatting
# - a setup exe that performs action required during install and uninstall
#
# In this description "installer" refers to the self extracting executable that
# wraps all the parts, whereas "setup" refers to an exe inside the installer
# that runs specific actions at install and uninstall time.
#
# When run, the installer extracts the wrapped files into a new empty
# directory under %TEMP%. The setup exe is then run to register the COM
# objects, install the message format dll, and properly register the credential
# provider with Windows. Once installation completes, the new directory
# containing the extracted files is automatically deleted.
#
# The installer can be run multiple times on the same machine. On an already
# working computer this is essentially a noop. On a damaged computer the files
# will be overwritten and the parts registered, so can be used to correct
# problems.
#
# Running a new version of the installer will replace the existing install with
# a newer one. It is not required to first uninstall the old version.
# Installation of the newer version will attempt to delete older versions if
# possible.
#
# The installer is not needed for uninstall and may be removed after initial
# install. To uninstall the Google Credential Provider for Windows, run the
# setup exe with the command line argument: /uninstall
"""Creates the GCPW self extracting installer. This script is not run manually,
it is called when building the //credential_provider:gcp_installer GN target.
All paths can be absolute or relative to $root_build_dir.
"""
import argparse
import os
import shutil
import subprocess
import sys
def GetLZMAExec(src_path):
"""Gets the path to the 7zip compression command line tool.
Args:
src_path: Full path to the source root
Returns:
The executable command to run the 7zip compressor.
"""
executable = '7zr'
if sys.platform == 'win32':
executable += '.exe'
return os.path.join(src_path, 'third_party', 'lzma_sdk', 'bin',
'host_platform', executable)
def GetCmdLine(command, sz_fn, gcp_7z_fn):
"""Builds the command line for the given archive.
Args:
command: 7Zip command such as 'u', 'rn'..
sz_fn: The executable command to run 7zip.
gcp_7z_fn: 7zip file for the archive.
Returns:
Returns the command line for the provided command and 7zip archive. Command
needs to be one of the supported 7zip commands.
"""
return [
sz_fn, # Path to 7z executable.
command,
# The follow options are equivalent to -mx9 with bcj2 turned on.
# Because //third_party/lzma_sdk is only partial copy of the ful sdk
# it does not support all forms of compression. Make sure to use
# compression that is compatible. These same options are used when
# building the chrome install compressed files.
'-m0=BCJ2',
'-m1=LZMA:d27:fb128',
'-m2=LZMA:d22:fb128:mf=bt2',
'-m3=LZMA:d22:fb128:mf=bt2',
'-mb0:1',
'-mb0s1:2',
'-mb0s2:3',
# Full path to archive.
gcp_7z_fn,
]
def main():
parser = argparse.ArgumentParser(
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument('src_path', help='Path to the source root')
parser.add_argument('cp_path',
help='Path to the credential provider directory')
parser.add_argument('root_build_path', help='$root_build_dir GN variable')
parser.add_argument('target_gen_path', help='$target_gen_dir GN variable')
args = parser.parse_args()
# Make sure all arguments are converted to absolute paths for use below.
args.src_path = os.path.abspath(args.src_path)
args.cp_path = os.path.abspath(args.cp_path)
args.root_build_path = os.path.abspath(args.root_build_path)
args.target_gen_path = os.path.abspath(args.target_gen_path)
if not os.path.isdir(args.cp_path):
parser.error('Invalid cp_path: "%s"' % args.cp_path)
if not os.path.isdir(args.src_path):
parser.error('Invalid src_path: "%s"' % args.src_path)
# Absolute path to gcp installer.
gcp_installer_fn = os.path.join(args.root_build_path, 'gcp_installer.exe')
gcp_7z_fn = os.path.join(args.root_build_path, 'gcp.7z')
sz_fn = GetLZMAExec(args.src_path)
sfx_fn = os.path.join(args.root_build_path, 'gcp_sfx.exe')
# Build the command line for updating files in the GCP 7z archive.
u_cmd = GetCmdLine('u', sz_fn, gcp_7z_fn)
# 7Zip CLI doesn't provide a direct way of adding files into a custom
# folder. As suggested by the developer of 7Zip the method is to rename
# a file with a specific subfolder to achieve the same.
# https://sourceforge.net/p/sevenzip/discussion/45798/thread/5856d980/
# For instance, if there is a file called "a.txt" in the parent folder of
# archive, when it is renamed with "f\a.txt", the "a.txt" file is actually
# placed in a folder called "f".
rn_cmd = GetCmdLine('rn', sz_fn, gcp_7z_fn)
# Builds the command line for deleting files in the archive.
d_cmd = GetCmdLine('d', sz_fn, gcp_7z_fn)
# Because of the way that 7zS2.sfx determine what program to run after
# extraction, only gcp_setup.exe should be placed in the root of the archive.
# Other "executable" type files (bat, cmd, exe, inf, msi, html, htm) should
# be located only in subfolders. That's why all the files initially added in
# the top folder. Then the ones need to move to subfolders are renamed. 7z
# doesn't have a method to achieve the same directly.
# Add the credential provider dll, credential provider extension and setup
# programs to the archive. If the files added to the archive are changed,
# make sure to update the kFilenames array in setup_lib.cc.
try:
gcpw_log_file = 'gcpw_archive_log_file'
if os.path.exists(gcpw_log_file):
os.remove(gcpw_log_file)
# Redirecting output of 7zip and copy commands to a file and only printing
# if any of the subprocess commands fail.
with open(gcpw_log_file, "w+") as output_file:
os.chdir(args.root_build_path)
subprocess.check_call(d_cmd + ['*'], stdout=output_file)
subprocess.check_call(u_cmd + ['gaia1_0.dll'], stdout=output_file)
subprocess.check_call(u_cmd + ['gcp_setup.exe'], stdout=output_file)
subprocess.check_call(u_cmd + ['gcp_eventlog_provider.dll'],
stdout=output_file)
subprocess.check_call(u_cmd + ['gcpw_extension.exe'], stdout=output_file)
# Move the executable into a subfolder as there needs to be only one
# executable in the parent folder.
subprocess.check_call(rn_cmd +
[
'gcpw_extension.exe',
os.path.join('extension', 'gcpw_extension.exe')
],
stdout=output_file)
except subprocess.CalledProcessError as e:
print(e.output)
with open(gcpw_log_file, "r") as output_file:
print(output_file.read())
raise e
# Combine the SFX module with the archive to make a self extracting
# executable.
with open(gcp_installer_fn, 'wb') as output:
with open (sfx_fn, 'rb') as input:
shutil.copyfileobj(input, output)
with open (gcp_7z_fn, 'rb') as input:
shutil.copyfileobj(input, output)
return 0
if __name__ == '__main__':
sys.exit(main())