OLD | NEW |
1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. | 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 | 3 # Use of this source code is governed by a BSD-style license that can be |
4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
5 | 5 |
6 """Creates a zip archive for the Chrome Remote Desktop Host installer. | 6 """Creates a zip archive for the Chrome Remote Desktop Host installer. |
7 | 7 |
8 This script builds a zip file that contains all the files needed to build an | 8 This script builds a zip file that contains all the files needed to build an |
9 installer for Chrome Remote Desktop Host. | 9 installer for Chrome Remote Desktop Host. |
10 | 10 |
11 This zip archive is then used by the signing bots to: | 11 This zip archive is then used by the signing bots to: |
12 (1) Sign the binaries | 12 (1) Sign the binaries |
13 (2) Build the final installer | 13 (2) Build the final installer |
14 | 14 |
15 TODO(garykac) We should consider merging this with build-webapp.py. | 15 TODO(garykac) We should consider merging this with build-webapp.py. |
16 """ | 16 """ |
17 | 17 |
18 import os | 18 import os |
19 import shutil | 19 import shutil |
| 20 import subprocess |
20 import sys | 21 import sys |
21 import zipfile | 22 import zipfile |
22 | 23 |
23 | 24 |
24 def cleanDir(dir): | 25 def cleanDir(dir): |
25 """Deletes and recreates the dir to make sure it is clean. | 26 """Deletes and recreates the dir to make sure it is clean. |
26 | 27 |
27 Args: | 28 Args: |
28 dir: The directory to clean. | 29 dir: The directory to clean. |
29 """ | 30 """ |
30 try: | 31 try: |
31 shutil.rmtree(dir) | 32 shutil.rmtree(dir) |
32 except OSError: | 33 except OSError: |
33 if os.path.exists(dir): | 34 if os.path.exists(dir): |
34 raise | 35 raise |
35 else: | 36 else: |
36 pass | 37 pass |
37 os.makedirs(dir, 0775) | 38 os.makedirs(dir, 0775) |
38 | 39 |
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 |
40 def createZip(zip_path, directory): | 57 def createZip(zip_path, directory): |
41 """Creates a zipfile at zip_path for the given directory. | 58 """Creates a zipfile at zip_path for the given directory. |
42 | 59 |
43 Args: | 60 Args: |
44 zip_path: Path to zip file to create. | 61 zip_path: Path to zip file to create. |
45 directory: Directory with contents to archive. | 62 directory: Directory with contents to archive. |
46 """ | 63 """ |
47 zipfile_base = os.path.splitext(os.path.basename(zip_path))[0] | 64 zipfile_base = os.path.splitext(os.path.basename(zip_path))[0] |
48 zip = zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) | 65 zip = zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) |
49 for (root, dirs, files) in os.walk(directory): | 66 for (root, dirs, files) in os.walk(directory): |
50 for f in files: | 67 for f in files: |
51 full_path = os.path.join(root, f) | 68 full_path = os.path.join(root, f) |
52 rel_path = os.path.relpath(full_path, directory) | 69 rel_path = os.path.relpath(full_path, directory) |
53 zip.write(full_path, os.path.join(zipfile_base, rel_path)) | 70 zip.write(full_path, os.path.join(zipfile_base, rel_path)) |
54 zip.close() | 71 zip.close() |
55 | 72 |
56 | 73 |
57 def copyFileIntoArchive(src_file, out_dir, files_root, dst_file): | 74 def copyFileIntoArchive(src_file, out_dir, files_root, dst_file, defs): |
58 """Copies the src_file into the out_dir, preserving the directory structure. | 75 """Copies the src_file into the out_dir, preserving the directory structure. |
59 | 76 |
60 Args: | 77 Args: |
61 src_file: Full or relative path to source file to copy. | 78 src_file: Full or relative path to source file to copy. |
62 out_dir: Target directory where files are copied. | 79 out_dir: Target directory where files are copied. |
63 files_root: Path prefix which is stripped of dst_file before appending | 80 files_root: Path prefix which is stripped of dst_file before appending |
64 it to the out_dir. | 81 it to the out_dir. |
65 dst_file: Relative path (and filename) where src_file should be copied. | 82 dst_file: Relative path (and filename) where src_file should be copied. |
| 83 defs: Dictionary of variable definitions. |
66 """ | 84 """ |
67 root_len = len(files_root) | 85 root_len = len(files_root) |
68 local_file_path = dst_file[root_len:] | 86 local_file_path = dst_file[root_len:] |
69 full_dst_file = os.path.join(out_dir, local_file_path) | 87 full_dst_file = os.path.join(out_dir, local_file_path) |
70 dst_dir = os.path.dirname(full_dst_file) | 88 dst_dir = os.path.dirname(full_dst_file) |
71 if not os.path.exists(dst_dir): | 89 if not os.path.exists(dst_dir): |
72 os.makedirs(dst_dir, 0775) | 90 os.makedirs(dst_dir, 0775) |
73 shutil.copy2(src_file, full_dst_file) | 91 |
| 92 (base, ext) = os.path.splitext(src_file) |
| 93 if ext == '.app': |
| 94 shutil.copytree(src_file, full_dst_file) |
| 95 elif ext in ['.sh', '.packproj', '.plist']: |
| 96 copyFileWithDefs(src_file, full_dst_file, defs) |
| 97 else: |
| 98 shutil.copy2(src_file, full_dst_file) |
| 99 |
| 100 |
| 101 def copyFileWithDefs(src_file, dst_file, defs): |
| 102 """Copies from src_file to dst_file, performing variable substitution. |
| 103 |
| 104 Any @@variables@@ in the source are replaced with |
| 105 file with the out_dir, preserving the directory structure. |
| 106 |
| 107 Args: |
| 108 src_file: Full or relative path to source file to copy. |
| 109 dst_file: Relative path (and filename) where src_file should be copied. |
| 110 defs: Dictionary of variable definitions. |
| 111 """ |
| 112 data = open(src_file, 'r').read() |
| 113 for key, val in defs.iteritems(): |
| 114 try: |
| 115 data = data.replace('@@' + key + '@@', val) |
| 116 except TypeError: |
| 117 print repr(key), repr(val) |
| 118 open(dst_file, 'w').write(data) |
| 119 shutil.copystat(src_file, dst_file) |
74 | 120 |
75 | 121 |
76 def copyZipIntoArchive(out_dir, files_root, zip_file): | 122 def copyZipIntoArchive(out_dir, files_root, zip_file): |
77 """Expands the zip_file into the out_dir, preserving the directory structure. | 123 """Expands the zip_file into the out_dir, preserving the directory structure. |
78 | 124 |
79 Args: | 125 Args: |
80 out_dir: Target directory where unzipped files are copied. | 126 out_dir: Target directory where unzipped files are copied. |
81 files_root: Path prefix which is stripped of zip_file before appending | 127 files_root: Path prefix which is stripped of zip_file before appending |
82 it to the out_dir. | 128 it to the out_dir. |
83 zip_file: Relative path (and filename) to the zip file. | 129 zip_file: Relative path (and filename) to the zip file. |
84 """ | 130 """ |
| 131 base_zip_name = os.path.basename(zip_file) |
| 132 |
| 133 # We don't use the 'zipfile' module here because it doesn't restore all the |
| 134 # file permissions correctly. We use the 'unzip' command manually. |
| 135 old_dir = os.getcwd(); |
| 136 os.chdir(os.path.dirname(zip_file)) |
| 137 subprocess.call(['unzip', '-qq', '-o', base_zip_name]) |
| 138 os.chdir(old_dir) |
| 139 |
85 # Unzip into correct dir in out_dir. | 140 # Unzip into correct dir in out_dir. |
86 zip_archive = zipfile.ZipFile(zip_file, 'r') | |
87 root_len = len(files_root) | 141 root_len = len(files_root) |
88 relative_zip_path = zip_file[root_len:] | 142 relative_zip_path = zip_file[root_len:] |
89 out_zip_path = os.path.join(out_dir, relative_zip_path) | 143 out_zip_path = os.path.join(out_dir, relative_zip_path) |
90 out_zip_dir = os.path.dirname(out_zip_path) | 144 out_zip_dir = os.path.dirname(out_zip_path) |
91 if not os.path.exists(out_zip_dir): | |
92 os.makedirs(out_zip_dir, 0775) | |
93 | 145 |
94 # Ideally, we'd simply do: | 146 (src_dir, ignore1) = os.path.splitext(zip_file) |
95 # zip_archive.extractall(out_zip_dir) | 147 (base_dir_name, ignore2) = os.path.splitext(base_zip_name) |
96 # but http://bugs.python.org/issue4710 bites us because the some 'bots (like | 148 shutil.copytree(src_dir, os.path.join(out_zip_dir, base_dir_name)) |
97 # mac_rel) are still running Python 2.6. | |
98 # See http://stackoverflow.com/questions/639962/unzipping-directory-structure-
with-python | |
99 # for workarounds. | |
100 # TODO(garykac): Remove this once all bots are > 2.6. | |
101 for f in zip_archive.namelist(): | |
102 if f.endswith('/'): | |
103 if not os.path.exists(f): | |
104 os.makedirs(os.path.join(out_zip_dir, f)) | |
105 else: | |
106 zip_archive.extract(f, out_zip_dir) | |
107 | 149 |
108 | 150 |
109 def buildHostArchive(temp_dir, zip_path, source_files_root, source_files, | 151 def buildHostArchive(temp_dir, zip_path, source_files_root, source_files, |
110 gen_files, gen_files_dst): | 152 gen_files, gen_files_dst, defs): |
111 """Builds a zip archive with the files needed to build the installer. | 153 """Builds a zip archive with the files needed to build the installer. |
112 | 154 |
113 Args: | 155 Args: |
114 temp_dir: Temporary dir used to build up the contents for the archive. | 156 temp_dir: Temporary dir used to build up the contents for the archive. |
115 zip_path: Full path to the zip file to create. | 157 zip_path: Full path to the zip file to create. |
116 source_files_root: Path prefix to strip off |files| when adding to archive. | 158 source_files_root: Path prefix to strip off |files| when adding to archive. |
117 source_files: The array of files to add to archive. The path structure is | 159 source_files: The array of files to add to archive. The path structure is |
118 preserved (except for the |files_root| prefix). | 160 preserved (except for the |files_root| prefix). |
119 gen_files: Full path to binaries to add to archive. | 161 gen_files: Full path to binaries to add to archive. |
120 gen_files_dst: Relative path of where to add binary files in archive. | 162 gen_files_dst: Relative path of where to add binary files in archive. |
121 This array needs to parallel |binaries_src|. | 163 This array needs to parallel |binaries_src|. |
| 164 defs: Dictionary of variable definitions. |
122 """ | 165 """ |
123 cleanDir(temp_dir) | 166 cleanDir(temp_dir) |
124 | 167 |
125 for file in source_files: | 168 for file in source_files: |
126 base_file = os.path.basename(file) | 169 base_file = os.path.basename(file) |
127 (base, ext) = os.path.splitext(file) | 170 (base, ext) = os.path.splitext(file) |
128 if ext == '.zip': | 171 if ext == '.zip': |
129 copyZipIntoArchive(temp_dir, source_files_root, file) | 172 copyZipIntoArchive(temp_dir, source_files_root, file) |
130 else: | 173 else: |
131 copyFileIntoArchive(file, temp_dir, source_files_root, file) | 174 copyFileIntoArchive(file, temp_dir, source_files_root, file, defs) |
132 | 175 |
133 for bs, bd in zip(gen_files, gen_files_dst): | 176 for bs, bd in zip(gen_files, gen_files_dst): |
134 copyFileIntoArchive(bs, temp_dir, '', bd) | 177 copyFileIntoArchive(bs, temp_dir, '', bd, {}) |
135 | 178 |
136 createZip(zip_path, temp_dir) | 179 createZip(zip_path, temp_dir) |
137 | 180 |
138 | 181 |
| 182 def error(msg): |
| 183 sys.stderr.write('ERROR: %s' % msg) |
| 184 sys.exit(1) |
| 185 |
139 def usage(): | 186 def usage(): |
140 """Display basic usage information.""" | 187 """Display basic usage information.""" |
141 print ('Usage: %s\n' | 188 print ('Usage: %s\n' |
142 ' <temp-dir> <zip-path> <files-root-dir>\n' | 189 ' <temp-dir> <zip-path> <files-root-dir>\n' |
143 ' --source-files <list of source files...>\n' | 190 ' --source-files <list of source files...>\n' |
144 ' --generated-files <list of generated target files...>\n' | 191 ' --generated-files <list of generated target files...>\n' |
145 ' --generated-files-dst <dst for each generated file...>' | 192 ' --generated-files-dst <dst for each generated file...>\n' |
| 193 ' --defs <list of VARIABLE=value definitions...>' |
146 ) % sys.argv[0] | 194 ) % sys.argv[0] |
147 | 195 |
148 | 196 |
149 def main(): | 197 def main(): |
150 if len(sys.argv) < 3: | 198 if len(sys.argv) < 3: |
151 usage() | 199 usage() |
152 return 1 | 200 error('Too few arguments') |
153 | 201 |
154 temp_dir = sys.argv[1] | 202 temp_dir = sys.argv[1] |
155 zip_path = sys.argv[2] | 203 zip_path = sys.argv[2] |
156 source_files_root = sys.argv[3] | 204 source_files_root = sys.argv[3] |
157 | 205 |
158 arg_mode = '' | 206 arg_mode = '' |
159 source_files = [] | 207 source_files = [] |
160 generated_files = [] | 208 generated_files = [] |
161 generated_files_dst = [] | 209 generated_files_dst = [] |
| 210 definitions = [] |
162 for arg in sys.argv[4:]: | 211 for arg in sys.argv[4:]: |
163 if arg == '--source-files': | 212 if arg == '--source-files': |
164 arg_mode = 'files' | 213 arg_mode = 'files' |
165 elif arg == '--generated-files': | 214 elif arg == '--generated-files': |
166 arg_mode = 'gen-src' | 215 arg_mode = 'gen-src' |
167 elif arg == '--generated-files-dst': | 216 elif arg == '--generated-files-dst': |
168 arg_mode = 'gen-dst' | 217 arg_mode = 'gen-dst' |
| 218 elif arg == '--defs': |
| 219 arg_mode = 'defs' |
169 | 220 |
170 elif arg_mode == 'files': | 221 elif arg_mode == 'files': |
171 source_files.append(arg) | 222 source_files.append(arg) |
172 elif arg_mode == 'gen-src': | 223 elif arg_mode == 'gen-src': |
173 generated_files.append(arg) | 224 generated_files.append(arg) |
174 elif arg_mode == 'gen-dst': | 225 elif arg_mode == 'gen-dst': |
175 generated_files_dst.append(arg) | 226 generated_files_dst.append(arg) |
| 227 elif arg_mode == 'defs': |
| 228 definitions.append(arg) |
176 else: | 229 else: |
177 print "ERROR: Expected --source-files" | |
178 usage() | 230 usage() |
179 return 1 | 231 error('Expected --source-files') |
180 | 232 |
181 # Make sure at least one file was specified. | 233 # Make sure at least one file was specified. |
182 if len(source_files) == 0 and len(generated_files) == 0: | 234 if len(source_files) == 0 and len(generated_files) == 0: |
183 print "ERROR: At least one input file must be specified." | 235 error('At least one input file must be specified.') |
184 return 1 | |
185 | 236 |
186 # Ensure that source_files_root ends with a directory separator. | 237 # Ensure that source_files_root ends with a directory separator. |
187 if source_files_root[-1:] != os.sep: | 238 if source_files_root[-1:] != os.sep: |
188 source_files_root += os.sep | 239 source_files_root += os.sep |
189 | 240 |
190 # Verify that the 2 generated_files arrays have the same number of elements. | 241 # Verify that the 2 generated_files arrays have the same number of elements. |
191 if len(generated_files) < len(generated_files_dst): | 242 if len(generated_files) != len(generated_files_dst): |
192 print "ERROR: len(--generated-files) != len(--generated-files-dst)" | 243 error('len(--generated-files) != len(--generated-files-dst)') |
193 return 1 | 244 |
194 while len(generated_files) > len(generated_files_dst): | 245 defs = buildDefDictionary(definitions) |
195 generated_files_dst.append('') | |
196 | 246 |
197 result = buildHostArchive(temp_dir, zip_path, source_files_root, | 247 result = buildHostArchive(temp_dir, zip_path, source_files_root, |
198 source_files, generated_files, generated_files_dst) | 248 source_files, generated_files, generated_files_dst, |
| 249 defs) |
199 | 250 |
200 return 0 | 251 return 0 |
201 | 252 |
202 if __name__ == '__main__': | 253 if __name__ == '__main__': |
203 sys.exit(main()) | 254 sys.exit(main()) |
OLD | NEW |