1 #!/usr/bin/env python 2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. 3 # Use of this source code is governed by a BSD-style license that can be 4 # found in the LICENSE file. 5 6 """Creates a zip archive for the Chrome Remote Desktop Host installer. 7 8 This script builds a zip file that contains all the files needed to build an 9 installer for Chrome Remote Desktop Host. 10 11 This zip archive is then used by the signing bots to: 12 (1) Sign the binaries 13 (2) Build the final installer 14 15 TODO(garykac) We should consider merging this with build-webapp.py. 16 """ 17 18 import os 19 import shutil 20 import subprocess 21 import sys 22 import zipfile 23 24 25 def cleanDir(dir): 26 """Deletes and recreates the dir to make sure it is clean. 27 28 Args: 29 dir: The directory to clean. 30 """ 31 try: 32 shutil.rmtree(dir) 33 except OSError: 34 if os.path.exists(dir): 35 raise 36 else: 37 pass 38 os.makedirs(dir, 0775) 39 40 41 def buildDefDictionary(definitions): 42 """Builds the definition dictionary from the VARIABLE=value array. 43 44 Args: 45 defs: Array of variable definitions: 'VARIABLE=value'. 46 47 Returns: 48 Dictionary with the definitions. 49 """ 50 defs = {} 51 for d in definitions: 52 (key, val) = d.split('=') 53 defs[key] = val 54 return defs 55 56 57 def createZip(zip_path, directory): 58 """Creates a zipfile at zip_path for the given directory. 59 60 Args: 61 zip_path: Path to zip file to create. 62 directory: Directory with contents to archive. 63 """ 64 zipfile_base = os.path.splitext(os.path.basename(zip_path))[0] 65 zip = zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) 66 for (root, dirs, files) in os.walk(directory): 67 for f in files: 68 full_path = os.path.join(root, f) 69 rel_path = os.path.relpath(full_path, directory) 70 zip.write(full_path, os.path.join(zipfile_base, rel_path)) 71 zip.close() 72 73 74 def remapSrcFile(dst_root, src_roots, src_file): 75 """Calculates destination file path and creates directory. 76 77 Any matching |src_roots| prefix is stripped from |src_file| before 78 appending to |dst_root|. 79 80 For example, given: 81 dst_root = '/output' 82 src_roots = ['host/installer/mac'] 83 src_file = 'host/installer/mac/Scripts/keystone_install.sh' 84 The final calculated path is: 85 '/output/Scripts/keystone_install.sh' 86 87 The |src_file| must match one of the |src_roots| prefixes. If there are no 88 matches, then an error is reported. 89 90 If multiple |src_roots| match, then only the first match is applied. Because 91 of this, if you have roots that share a common prefix, the longest string 92 should be first in this array. 93 94 Args: 95 dst_root: Target directory where files are copied. 96 src_roots: Array of path prefixes which will be stripped of |src_file| 97 (if they match) before appending it to the |dst_root|. 98 src_file: Source file to be copied. 99 Returns: 100 Full path to destination file in |dst_root|. 101 """ 102 # Strip of directory prefix. 103 found_root = False 104 for root in src_roots: 105 root = os.path.normpath(root) 106 src_file = os.path.normpath(src_file) 107 if os.path.commonprefix([root, src_file]) == root: 108 src_file = os.path.relpath(src_file, root) 109 found_root = True 110 break 111 112 if not found_root: 113 error('Unable to match prefix for %s' % src_file) 114 115 dst_file = os.path.join(dst_root, src_file) 116 # Make sure target directory exists. 117 dst_dir = os.path.dirname(dst_file) 118 if not os.path.exists(dst_dir): 119 os.makedirs(dst_dir, 0775) 120 return dst_file 121 122 123 def copyFileWithDefs(src_file, dst_file, defs): 124 """Copies from src_file to dst_file, performing variable substitution. 125 126 Any @@VARIABLE@@ in the source is replaced with the value of VARIABLE 127 in the |defs| dictionary when written to the destination file. 128 129 Args: 130 src_file: Full or relative path to source file to copy. 131 dst_file: Relative path (and filename) where src_file should be copied. 132 defs: Dictionary of variable definitions. 133 """ 134 data = open(src_file, 'r').read() 135 for key, val in defs.iteritems(): 136 try: 137 data = data.replace('@@' + key + '@@', val) 138 except TypeError: 139 print repr(key), repr(val) 140 open(dst_file, 'w').write(data) 141 shutil.copystat(src_file, dst_file) 142 143 144 def copyZipIntoArchive(out_dir, files_root, zip_file): 145 """Expands the zip_file into the out_dir, preserving the directory structure. 146 147 Args: 148 out_dir: Target directory where unzipped files are copied. 149 files_root: Path prefix which is stripped of zip_file before appending 150 it to the out_dir. 151 zip_file: Relative path (and filename) to the zip file. 152 """ 153 base_zip_name = os.path.basename(zip_file) 154 155 # We don't use the 'zipfile' module here because it doesn't restore all the 156 # file permissions correctly. We use the 'unzip' command manually. 157 old_dir = os.getcwd(); 158 os.chdir(os.path.dirname(zip_file)) 159 subprocess.call(['unzip', '-qq', '-o', base_zip_name]) 160 os.chdir(old_dir) 161 162 # Unzip into correct dir in out_dir. 163 out_zip_path = remapSrcFile(out_dir, files_root, zip_file) 164 out_zip_dir = os.path.dirname(out_zip_path) 165 166 (src_dir, ignore1) = os.path.splitext(zip_file) 167 (base_dir_name, ignore2) = os.path.splitext(base_zip_name) 168 shutil.copytree(src_dir, os.path.join(out_zip_dir, base_dir_name)) 169 170 171 def buildHostArchive(temp_dir, zip_path, source_file_roots, source_files, 172 gen_files, gen_files_dst, defs): 173 """Builds a zip archive with the files needed to build the installer. 174 175 Args: 176 temp_dir: Temporary dir used to build up the contents for the archive. 177 zip_path: Full path to the zip file to create. 178 source_file_roots: Array of path prefixes to strip off |files| when adding 179 to the archive. 180 source_files: The array of files to add to archive. The path structure is 181 preserved (except for the |files_root| prefix). 182 gen_files: Full path to binaries to add to archive. 183 gen_files_dst: Relative path of where to add binary files in archive. 184 This array needs to parallel |binaries_src|. 185 defs: Dictionary of variable definitions. 186 """ 187 cleanDir(temp_dir) 188 189 for f in source_files: 190 dst_file = remapSrcFile(temp_dir, source_file_roots, f) 191 base_file = os.path.basename(f) 192 (base, ext) = os.path.splitext(f) 193 if ext == '.zip': 194 copyZipIntoArchive(temp_dir, source_file_roots, f) 195 elif ext in ['.packproj', '.pkgproj', '.plist', '.props', '.sh', '.json']: 196 copyFileWithDefs(f, dst_file, defs) 197 else: 198 shutil.copy2(f, dst_file) 199 200 for bs, bd in zip(gen_files, gen_files_dst): 201 dst_file = os.path.join(temp_dir, bd) 202 if not os.path.exists(os.path.dirname(dst_file)): 203 os.makedirs(os.path.dirname(dst_file)) 204 if os.path.isdir(bs): 205 shutil.copytree(bs, dst_file) 206 else: 207 shutil.copy2(bs, dst_file) 208 209 createZip(zip_path, temp_dir) 210 211 212 def error(msg): 213 sys.stderr.write('ERROR: %s\n' % msg) 214 sys.exit(1) 215 216 217 def usage(): 218 """Display basic usage information.""" 219 print ('Usage: %s\n' 220 ' <temp-dir> <zip-path>\n' 221 ' --source-file-roots <list of roots to strip off source files...>\n' 222 ' --source-files <list of source files...>\n' 223 ' --generated-files <list of generated target files...>\n' 224 ' --generated-files-dst <dst for each generated file...>\n' 225 ' --defs <list of VARIABLE=value definitions...>' 226 ) % sys.argv[0] 227 228 229 def main(): 230 if len(sys.argv) < 2: 231 usage() 232 error('Too few arguments') 233 234 temp_dir = sys.argv[1] 235 zip_path = sys.argv[2] 236 237 arg_mode = '' 238 source_file_roots = [] 239 source_files = [] 240 generated_files = [] 241 generated_files_dst = [] 242 definitions = [] 243 for arg in sys.argv[3:]: 244 if arg == '--source-file-roots': 245 arg_mode = 'src-roots' 246 elif arg == '--source-files': 247 arg_mode = 'files' 248 elif arg == '--generated-files': 249 arg_mode = 'gen-src' 250 elif arg == '--generated-files-dst': 251 arg_mode = 'gen-dst' 252 elif arg == '--defs': 253 arg_mode = 'defs' 254 255 elif arg_mode == 'src-roots': 256 source_file_roots.append(arg) 257 elif arg_mode == 'files': 258 source_files.append(arg) 259 elif arg_mode == 'gen-src': 260 generated_files.append(arg) 261 elif arg_mode == 'gen-dst': 262 generated_files_dst.append(arg) 263 elif arg_mode == 'defs': 264 definitions.append(arg) 265 else: 266 usage() 267 error('Expected --source-files') 268 269 # Make sure at least one file was specified. 270 if len(source_files) == 0 and len(generated_files) == 0: 271 error('At least one input file must be specified.') 272 273 # Sort roots to ensure the longest one is first. See comment in remapSrcFile 274 # for why this is necessary. 275 source_file_roots = map(os.path.normpath, source_file_roots) 276 source_file_roots.sort(key=len, reverse=True) 277 278 # Verify that the 2 generated_files arrays have the same number of elements. 279 if len(generated_files) != len(generated_files_dst): 280 error('len(--generated-files) != len(--generated-files-dst)') 281 282 defs = buildDefDictionary(definitions) 283 284 result = buildHostArchive(temp_dir, zip_path, source_file_roots, 285 source_files, generated_files, generated_files_dst, 286 defs) 287 288 return 0 289 290 291 if __name__ == '__main__': 292 sys.exit(main()) 293