#!/usr/bin/python # Copyright (c) 2006-2008 The Chromium Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. """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 CopyAllFilesToStagingDir(config, distribution, staging_dir, output_dir): """Copies the files required for installer archive. Copies all common files required for various distributions of Chromium and also files for the specific Chromium build specified by distribution. """ CopySectionFilesToStagingDir(config, 'GENERAL', staging_dir, output_dir) if distribution: if len(distribution) > 1 and distribution[0] == '_': distribution = distribution[1:] CopySectionFilesToStagingDir(config, distribution.upper(), staging_dir, output_dir) def CopySectionFilesToStagingDir(config, section, staging_dir, output_dir): """Copies installer archive files specified in section to staging dir. This method copies reads section from config file and copies all the files specified to staging dir. """ for option in config.options(section): if option.endswith('dir'): continue dst = os.path.join(staging_dir, config.get(section, 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, skip_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 skip_rebuild_archive != "true": 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 + 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) CopyAllFilesToStagingDir(config, options.distribution, staging_dir, options.output_dir) # Name of the archive file built (for example - chrome.7z or # patch--.7z or patch-.7z archive_file_name = CreateArchiveFile(options.output_dir, staging_dir, current_version, options.last_chrome_installer, options.last_chrome_version, options.skip_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('-d', '--distribution', help='Name of Chromium Distribution. Optional.') option_parser.add_option('-s', '--skip_rebuild_archive', default="False", help='Skip re-building Chrome.7z archive 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))