#!/usr/bin/python # Copyright 2008, Google Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of Google Inc. nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """Script to create Chrome Installer archive. This script is used to create an archive of all the files required for a Chrome install in appropriate directory structure. It reads chrome.release file as input, creates chrome.7z archive, compresses setup.exe and generates packed_files.txt for mini_installer project. """ import ConfigParser import glob import md5 import optparse import os import shutil import sys ARCHIVE_DIR = "installer_archive" FULL_ARCHIVE_FILE = "chrome.7z" # uncompresed full archive file C_FULL_ARCHIVE_FILE = "chrome.packed.7z" # compressed full archive file PATCH_FILE_NAME = "patch" # patch archive file name PATCH_FILE_EXT = ".packed.7z" # extension of patch archive file CHROME_DIR = "Chrome-bin" MINI_INSTALLER_INPUT_FILE = "packed_files.txt" SETUP_EXEC = "setup.exe" BSDIFF_EXEC = "bsdiff.exe" VERSION_FILE = "VERSION" PACKED_FILE_COMMENTS = """ // This file is automatically generated by create_installer_archive.py. // It contains the resource entries that are going to be linked inside // mini_installer.exe. For each file to be linked there should be two // lines: // - The first line contains the output filename (without path) and the // type of the resource ('BN' means the file is not compressed and // 'BL' means the file is compressed. // - The second line contains the path to the input file. Uses '/' to // separate path components. """ def BuildVersion(output_dir): """Returns the full build version string constructed from information in VERSION_FILE. Any segment not found in that file will default to '0'. """ major = 0 minor = 0 build = 0 patch = 0 # TODO(rahulk): find a better way to locate VERSION file for line in open(os.path.join(output_dir, "..", VERSION_FILE), 'r'): line = line.rstrip() if line.startswith('MAJOR='): major = line[6:] elif line.startswith('MINOR='): minor = line[6:] elif line.startswith('BUILD='): build = line[6:] elif line.startswith('PATCH='): patch = line[6:] return '%s.%s.%s.%s' % (major, minor, build, patch) def Readconfig(output_dir, input_file, current_version): """Reads config information from input file after setting default value of global variabes. """ variables = {} variables['ChromeDir'] = CHROME_DIR variables['VersionDir'] = os.path.join(variables['ChromeDir'], current_version) config = ConfigParser.SafeConfigParser(variables) config.read(input_file) return config def MakeStagingDirectory(output_dir): """Creates a staging path for installer archive. If directory exists already, deletes the existing directory. """ file_path = os.path.join(output_dir, ARCHIVE_DIR) if os.path.exists(file_path): shutil.rmtree(file_path) os.makedirs(file_path) return file_path def CopyFilesToStagingDir(config, staging_dir, output_dir): """Copies files required for installer archive to staging dir. """ for option in config.options('FILES'): if option.endswith('dir'): continue dst = os.path.join(staging_dir, config.get('FILES', option)) if not os.path.exists(dst): os.makedirs(dst) for file in glob.glob(os.path.join(output_dir, option)): shutil.copy(file, dst) def RunSystemCommand(cmd): if (os.system(cmd) != 0): raise "Error while running cmd: %s" % cmd def CreateArchiveFile(output_dir, staging_dir, current_version, prev_version_dir, prev_version, rebuild_archive): """Creates a new installer archive file after deleting any existing old file. """ # First create an uncompressed archive file for the current build # TODO(rahulk): find a better way to locate 7za.exe lzma_exec = os.path.join(output_dir, "..", "..", "third_party", "lzma_sdk", "Executable", "7za.exe") archive_file = os.path.join(output_dir, FULL_ARCHIVE_FILE) cmd = '%s a -t7z "%s" "%s" -mx0' % (lzma_exec, archive_file, os.path.join(staging_dir, CHROME_DIR)) # There doesnt seem to be any way in 7za.exe to override existing file so # we always delete before creating a new one. if not os.path.exists(archive_file): RunSystemCommand(cmd) elif rebuild_archive: os.remove(archive_file) RunSystemCommand(cmd) # If we are generating a patch, run bsdiff against previous build and # compress the resulting patch file. If this is not a patch just compress the # uncompressed archive file. if (prev_version_dir): prev_archive_file = os.path.join(prev_version_dir, FULL_ARCHIVE_FILE) patch_file = os.path.join(output_dir, "patch.7z") cmd = '%s "%s" "%s" "%s"' % (os.path.join(output_dir, BSDIFF_EXEC), prev_archive_file, archive_file, patch_file) RunSystemCommand(cmd) archive_file_name = PATCH_FILE_NAME + "-" if prev_version: archive_file_name += prev_version + "-" archive_file_name += current_version + PATCH_FILE_EXT orig_file = patch_file else: archive_file_name = C_FULL_ARCHIVE_FILE orig_file = archive_file compressed_archive_file_path = os.path.join(output_dir, archive_file_name) cmd = '%s a -t7z "%s" "%s" -mx9' % (lzma_exec, compressed_archive_file_path, orig_file) if os.path.exists(compressed_archive_file_path): os.remove(compressed_archive_file_path) RunSystemCommand(cmd) return archive_file_name def CompressSetupExec(output_dir): """Compresses setup.exe to reduce size.""" cmd = 'makecab.exe /V1 /L "%s" "%s"' % (output_dir, os.path.join(output_dir, SETUP_EXEC)) RunSystemCommand(cmd) def GetFileMD5Hash(file): f = open(file, 'rb') hash = md5.new(f.read()).hexdigest() f.close() return hash def CreateResourceInputFile(output_dir, prev_version_dir, archive_file_name): """Creates resource input file (packed_files.txt) for mini_installer project. This method checks if we are generating a patch instead of full installer. In case of patch it also checks if setup.exe has changed by comparing its MD5 hash with the MD5 hash of previous setup.exe. If hash values are same setup.exe is not included in packed_files.txt. In case of patch we include patch.7z and in case of full installer we include chrome.7z in packed_files.txt. """ setup_exe_needed = 1 if (prev_version_dir): current_hash = GetFileMD5Hash(os.path.join(output_dir, SETUP_EXEC)) prev_hash = GetFileMD5Hash(os.path.join(prev_version_dir, SETUP_EXEC)) if (current_hash == prev_hash): setup_exe_needed = 0 if (setup_exe_needed): CompressSetupExec(output_dir) c_setup_file = SETUP_EXEC[:len(SETUP_EXEC) - 1] + "_" setup_file_entry = "%s\t\tBL\n\"%s\"" % (c_setup_file, os.path.join(output_dir, c_setup_file).replace("\\","/")) archive_file_entry = "\n%s\t\tB7\n\"%s\"" % (archive_file_name, os.path.join(output_dir, archive_file_name).replace("\\","/")) output_file = os.path.join(output_dir, MINI_INSTALLER_INPUT_FILE) f = open(output_file, 'w') try: f.write(PACKED_FILE_COMMENTS) if (setup_exe_needed): f.write(setup_file_entry) f.write(archive_file_entry) finally: f.close() def main(options): """Main method that reads input file, creates archive file and write resource input file. """ current_version = BuildVersion(options.output_dir) config = Readconfig(options.output_dir, options.input_file, current_version) staging_dir = MakeStagingDirectory(options.output_dir) CopyFilesToStagingDir(config, staging_dir, options.output_dir) # Name of the archive file built (for example - chrome.lz or # patch--.lz or patch-.lz archive_file_name = CreateArchiveFile(options.output_dir, staging_dir, current_version, options.last_chrome_installer, options.last_chrome_version, options.rebuild_archive) CreateResourceInputFile(options.output_dir, options.last_chrome_installer, archive_file_name) if '__main__' == __name__: option_parser = optparse.OptionParser() option_parser.add_option('-o', '--output_dir', help='Output directory') option_parser.add_option('-i', '--input_file', help='Input file') option_parser.add_option('-r', '--rebuild_archive', action='store_true', default=False, help='Rebuild Chrome.7z archive, even if it exists.') option_parser.add_option('-l', '--last_chrome_installer', help='Generate differential installer. The value of this parameter ' + 'specifies the directory that contains base versions of ' + 'setup.exe & chrome.7z.') option_parser.add_option('-v', '--last_chrome_version', help='Version of the previous installer. ' + 'Used only for the purpose of naming archive file. Optional.') options, args = option_parser.parse_args() sys.exit(main(options))