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
8This script builds a zip file that contains all the files needed to build an
9installer for Chrome Remote Desktop Host.
10
11This zip archive is then used by the signing bots to:
12(1) Sign the binaries
13(2) Build the final installer
14
15TODO(garykac) We should consider merging this with build-webapp.py.
16"""
17
18import os
19import shutil
20import subprocess
21import sys
22import zipfile
23
24
25def 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
41def 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
57def 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
74def 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
123def 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
144def 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
171def 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
212def error(msg):
213  sys.stderr.write('ERROR: %s\n' % msg)
214  sys.exit(1)
215
216
217def 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
229def 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
291if __name__ == '__main__':
292  sys.exit(main())
293