Home | History | Annotate | Download | only in installer
      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