summaryrefslogtreecommitdiffstats
path: root/remoting/host/installer/build-installer-archive.py
blob: 4585d0a231ab8e0c11e2a7de0e8744f5d90bab19 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
#!/usr/bin/env python
# Copyright (c) 2012 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.

"""Creates a zip archive for the Chrome Remote Desktop Host installer.

This script builds a zip file that contains all the files needed to build an
installer for Chrome Remote Desktop Host.

This zip archive is then used by the signing bots to:
(1) Sign the binaries
(2) Build the final installer

TODO(garykac) We should consider merging this with build-webapp.py.
"""

import os
import shutil
import subprocess
import sys
import zipfile


def cleanDir(dir):
  """Deletes and recreates the dir to make sure it is clean.

  Args:
    dir: The directory to clean.
  """
  try:
    shutil.rmtree(dir)
  except OSError:
    if os.path.exists(dir):
      raise
    else:
      pass
  os.makedirs(dir, 0775)


def buildDefDictionary(definitions):
  """Builds the definition dictionary from the VARIABLE=value array.

  Args:
    defs: Array of variable definitions: 'VARIABLE=value'.

    Returns:
      Dictionary with the definitions.
  """
  defs = {}
  for d in definitions:
    (key, val) = d.split('=')
    defs[key] = val
  return defs


def createZip(zip_path, directory):
  """Creates a zipfile at zip_path for the given directory.

  Args:
    zip_path: Path to zip file to create.
    directory: Directory with contents to archive.
  """
  zipfile_base = os.path.splitext(os.path.basename(zip_path))[0]
  zip = zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED)
  for (root, dirs, files) in os.walk(directory):
    for f in files:
      full_path = os.path.join(root, f)
      rel_path = os.path.relpath(full_path, directory)
      zip.write(full_path, os.path.join(zipfile_base, rel_path))
  zip.close()


def remapSrcFile(dst_root, src_roots, src_file):
  """Calculates destination file path and creates directory.

  Any matching |src_roots| prefix is stripped from |src_file| before
  appending to |dst_root|.

  For example, given:
    dst_root = '/output'
    src_roots = ['host/installer/mac']
    src_file = 'host/installer/mac/Scripts/keystone_install.sh'
  The final calculated path is:
    '/output/Scripts/keystone_install.sh'

  The |src_file| must match one of the |src_roots| prefixes. If there are no
  matches, then an error is reported.

  If multiple |src_roots| match, then only the first match is applied. Because
  of this, if you have roots that share a common prefix, the longest string
  should be first in this array.

  Args:
    dst_root: Target directory where files are copied.
    src_roots: Array of path prefixes which will be stripped of |src_file|
               (if they match) before appending it to the |dst_root|.
    src_file: Source file to be copied.
  Returns:
    Full path to destination file in |dst_root|.
  """
  # Strip of directory prefix.
  found_root = False
  for root in src_roots:
    root = os.path.normpath(root)
    src_file = os.path.normpath(src_file)
    if os.path.commonprefix([root, src_file]) == root:
      src_file = os.path.relpath(src_file, root)
      found_root = True
      break

  if not found_root:
    error('Unable to match prefix for %s' % src_file)

  dst_file = os.path.join(dst_root, src_file)
  # Make sure target directory exists.
  dst_dir = os.path.dirname(dst_file)
  if not os.path.exists(dst_dir):
    os.makedirs(dst_dir, 0775)
  return dst_file


def copyFileWithDefs(src_file, dst_file, defs):
  """Copies from src_file to dst_file, performing variable substitution.

  Any @@VARIABLE@@ in the source is replaced with the value of VARIABLE
  in the |defs| dictionary when written to the destination file.

  Args:
    src_file: Full or relative path to source file to copy.
    dst_file: Relative path (and filename) where src_file should be copied.
    defs: Dictionary of variable definitions.
  """
  data = open(src_file, 'r').read()
  for key, val in defs.iteritems():
    try:
      data = data.replace('@@' + key + '@@', val)
    except TypeError:
      print repr(key), repr(val)
  open(dst_file, 'w').write(data)
  shutil.copystat(src_file, dst_file)


def copyZipIntoArchive(out_dir, files_root, zip_file):
  """Expands the zip_file into the out_dir, preserving the directory structure.

  Args:
    out_dir: Target directory where unzipped files are copied.
    files_root: Path prefix which is stripped of zip_file before appending
                it to the out_dir.
    zip_file: Relative path (and filename) to the zip file.
  """
  base_zip_name = os.path.basename(zip_file)

  # We don't use the 'zipfile' module here because it doesn't restore all the
  # file permissions correctly. We use the 'unzip' command manually.
  old_dir = os.getcwd();
  os.chdir(os.path.dirname(zip_file))
  subprocess.call(['unzip', '-qq', '-o', base_zip_name])
  os.chdir(old_dir)

  # Unzip into correct dir in out_dir.
  out_zip_path = remapSrcFile(out_dir, files_root, zip_file)
  out_zip_dir = os.path.dirname(out_zip_path)

  (src_dir, ignore1) = os.path.splitext(zip_file)
  (base_dir_name, ignore2) = os.path.splitext(base_zip_name)
  shutil.copytree(src_dir, os.path.join(out_zip_dir, base_dir_name))


def buildHostArchive(temp_dir, zip_path, source_file_roots, source_files,
                     gen_files, gen_files_dst, defs):
  """Builds a zip archive with the files needed to build the installer.

  Args:
    temp_dir: Temporary dir used to build up the contents for the archive.
    zip_path: Full path to the zip file to create.
    source_file_roots: Array of path prefixes to strip off |files| when adding
                       to the archive.
    source_files: The array of files to add to archive. The path structure is
                  preserved (except for the |files_root| prefix).
    gen_files: Full path to binaries to add to archive.
    gen_files_dst: Relative path of where to add binary files in archive.
                   This array needs to parallel |binaries_src|.
    defs: Dictionary of variable definitions.
  """
  cleanDir(temp_dir)

  for f in source_files:
    dst_file = remapSrcFile(temp_dir, source_file_roots, f)
    base_file = os.path.basename(f)
    (base, ext) = os.path.splitext(f)
    if ext == '.zip':
      copyZipIntoArchive(temp_dir, source_file_roots, f)
    elif ext in ['.packproj', '.pkgproj', '.plist', '.props', '.sh', '.json']:
      copyFileWithDefs(f, dst_file, defs)
    else:
      shutil.copy2(f, dst_file)

  for bs, bd in zip(gen_files, gen_files_dst):
    dst_file = os.path.join(temp_dir, bd)
    if not os.path.exists(os.path.dirname(dst_file)):
      os.makedirs(os.path.dirname(dst_file))
    if os.path.isdir(bs):
      shutil.copytree(bs, dst_file)
    else:
      shutil.copy2(bs, dst_file)

  createZip(zip_path, temp_dir)


def error(msg):
  sys.stderr.write('ERROR: %s\n' % msg)
  sys.exit(1)


def usage():
  """Display basic usage information."""
  print ('Usage: %s\n'
         '  <temp-dir> <zip-path>\n'
         '  --source-file-roots <list of roots to strip off source files...>\n'
         '  --source-files <list of source files...>\n'
         '  --generated-files <list of generated target files...>\n'
         '  --generated-files-dst <dst for each generated file...>\n'
         '  --defs <list of VARIABLE=value definitions...>'
         ) % sys.argv[0]


def main():
  if len(sys.argv) < 2:
    usage()
    error('Too few arguments')

  temp_dir = sys.argv[1]
  zip_path = sys.argv[2]

  arg_mode = ''
  source_file_roots = []
  source_files = []
  generated_files = []
  generated_files_dst = []
  definitions = []
  for arg in sys.argv[3:]:
    if arg == '--source-file-roots':
      arg_mode = 'src-roots'
    elif arg == '--source-files':
      arg_mode = 'files'
    elif arg == '--generated-files':
      arg_mode = 'gen-src'
    elif arg == '--generated-files-dst':
      arg_mode = 'gen-dst'
    elif arg == '--defs':
      arg_mode = 'defs'

    elif arg_mode == 'src-roots':
      source_file_roots.append(arg)
    elif arg_mode == 'files':
      source_files.append(arg)
    elif arg_mode == 'gen-src':
      generated_files.append(arg)
    elif arg_mode == 'gen-dst':
      generated_files_dst.append(arg)
    elif arg_mode == 'defs':
      definitions.append(arg)
    else:
      usage()
      error('Expected --source-files')

  # Make sure at least one file was specified.
  if len(source_files) == 0 and len(generated_files) == 0:
    error('At least one input file must be specified.')

  # Sort roots to ensure the longest one is first. See comment in remapSrcFile
  # for why this is necessary.
  source_file_roots = map(os.path.normpath, source_file_roots)
  source_file_roots.sort(key=len, reverse=True)

  # Verify that the 2 generated_files arrays have the same number of elements.
  if len(generated_files) != len(generated_files_dst):
    error('len(--generated-files) != len(--generated-files-dst)')

  defs = buildDefDictionary(definitions)

  result = buildHostArchive(temp_dir, zip_path, source_file_roots,
                            source_files, generated_files, generated_files_dst,
                            defs)

  return 0


if __name__ == '__main__':
  sys.exit(main())