OLD | NEW |
1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
2 # | 2 # |
3 # Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file | 3 # Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file |
4 # for details. All rights reserved. Use of this source code is governed by a | 4 # for details. All rights reserved. Use of this source code is governed by a |
5 # BSD-style license that can be found in the LICENSE file. | 5 # BSD-style license that can be found in the LICENSE file. |
6 | 6 |
7 # Dart Editor promote and google storage cleanup tools. | 7 # Dart Editor promote tools. |
8 | 8 |
9 import gsutil | |
10 import imp | 9 import imp |
11 import optparse | 10 import optparse |
12 import os | 11 import os |
13 import subprocess | 12 import subprocess |
14 import sys | 13 import sys |
15 import urllib | 14 import urllib |
16 | 15 |
17 from os.path import join | 16 from os.path import join |
18 | 17 |
19 CONTINUOUS = 'gs://dart-editor-archive-continuous' | |
20 TRUNK = 'gs://dart-editor-archive-trunk' | |
21 TESTING = 'gs://dart-editor-archive-testing' | |
22 INTEGRATION = 'gs://dart-editor-archive-integration' | |
23 RELEASE = 'gs://dart-editor-archive-release' | |
24 INTERNAL = 'gs://dart-editor-archive-internal' | |
25 | |
26 DART_PATH = os.path.abspath(os.path.join(__file__, '..', '..', '..')) | 18 DART_PATH = os.path.abspath(os.path.join(__file__, '..', '..', '..')) |
27 BOT_UTILS = os.path.abspath(os.path.join( | 19 BOT_UTILS = os.path.abspath(os.path.join( |
28 DART_PATH, 'tools', 'bots', 'bot_utils.py')) | 20 DART_PATH, 'tools', 'bots', 'bot_utils.py')) |
29 | 21 |
30 bot_utils = imp.load_source('bot_utils', BOT_UTILS) | 22 bot_utils = imp.load_source('bot_utils', BOT_UTILS) |
31 | 23 |
32 def _BuildOptions(): | 24 def BuildOptions(): |
33 """Setup the argument processing for this program. | 25 usage = """usage: %prog promote [options] |
| 26 where: |
| 27 promote - Will promote builds from raw/signed locations to release |
| 28 locations. |
34 | 29 |
35 Returns: | 30 Example: Promote revision r29962 on dev channel: |
36 the OptionParser to process the CLI | 31 python editor/build/promote.py promote --channel=dev --revision=29962 |
37 """ | 32 """ |
38 usage = """usage: %prog [options] cleanup|promote | |
39 where: | |
40 cleanup will cleanup the Google storage continuous bucket | |
41 promote will promote code between different stages | |
42 | |
43 Examples: | |
44 cleanup saving the last 150 revisions | |
45 python gsTool.py cleanup --keepcount=150 | |
46 | |
47 promote revision 567 from trunk to integration | |
48 python gsTool.py promote --trunk --revision=567""" | |
49 | |
50 | 33 |
51 result = optparse.OptionParser(usage=usage) | 34 result = optparse.OptionParser(usage=usage) |
52 result.set_default('gsbucketuri', 'gs://dart-editor-archive-continuous') | 35 |
53 result.set_default('keepcount', 1000) | 36 group = optparse.OptionGroup( |
54 result.set_default('dryrun', False) | 37 result, 'Promote', 'options used to promote code') |
55 result.set_default('continuous', False) | 38 group.add_option( |
56 result.set_default('trunk', False) | 39 '--revision', help='The svn revision to promote', action='store') |
57 result.set_default('integration', False) | 40 group.add_option( |
58 result.set_default('testing', False) | 41 '--channel', type='string', help='Channel to promote.', default=None) |
59 group = optparse.OptionGroup(result, 'Cleanup', | |
60 'options used to cleanup Google Storage') | |
61 group.add_option('--keepcount', | |
62 type='int', | |
63 help='Numer of Builds to keep.', | |
64 action='store') | |
65 result.add_option_group(group) | 42 result.add_option_group(group) |
66 | 43 |
67 group = optparse.OptionGroup(result, 'Promote', | |
68 'options used to promote code') | |
69 group.add_option('--revision', | |
70 help='The svn revision to promote', | |
71 action='store') | |
72 group.add_option('--continuous', | |
73 help='Promote from continuous', | |
74 action='store_true') | |
75 group.add_option('--trunk', | |
76 help='Promote from trunk', | |
77 action='store_true') | |
78 group.add_option('--internal', | |
79 help='Promote from trunk to internal', | |
80 action='store_true') | |
81 group.add_option('--integration', | |
82 help='Promote from integration', | |
83 action='store_true') | |
84 group.add_option('--channel', type='string', | |
85 help='Promote from this channel', | |
86 default=None) | |
87 result.add_option_group(group) | |
88 | |
89 result.add_option('--gsbucketuri', | |
90 help='Dart Continuous Google Storage bucket URI.', | |
91 action='store') | |
92 result.add_option('--gsutilloc', | |
93 help='location of gsutil the program', | |
94 action='store') | |
95 result.add_option('--dryrun', help='don\'t do anything that would change' | |
96 ' Google Storage', | |
97 action='store_true') | |
98 result.add_option('--testing', help='user test bucket in ' | |
99 ' Google Storage', | |
100 action='store_true') | |
101 | |
102 return result | 44 return result |
103 | 45 |
104 | 46 |
105 def main(): | 47 def main(): |
106 """Main entry point for Google Storage Tools.""" | 48 parser = BuildOptions() |
107 | |
108 parser = _BuildOptions() | |
109 (options, args) = parser.parse_args() | 49 (options, args) = parser.parse_args() |
110 | 50 |
111 if not args: | 51 def die(msg): |
112 print 'At least one command must be specified' | 52 print msg |
113 parser.print_help() | 53 parser.print_help() |
114 sys.exit(1) | 54 sys.exit(1) |
115 | 55 |
| 56 if not args: |
| 57 die('At least one command must be specified') |
| 58 |
116 if args[0] == 'promote': | 59 if args[0] == 'promote': |
117 command = 'promote' | 60 command = 'promote' |
118 if options.revision is None: | 61 if options.revision is None: |
119 print 'You must specify a --revision to specify which revision to promote' | 62 die('You must specify a --revision to specify which revision to promote') |
120 parser.print_help() | |
121 sys.exit(3) | |
122 | 63 |
123 # Make sure revision is a valid integer | 64 # Make sure revision is a valid integer |
124 try: | 65 try: |
125 _ = int(options.revision) | 66 _ = int(options.revision) |
126 except: | 67 except: |
127 print 'You must supply a valid integer argument to --revision to promote' | 68 die('You must supply a valid integer argument to --revision to promote') |
128 parser.print_help() | |
129 sys.exit(3) | |
130 | 69 |
131 # Make sure options.channel is a valid channel if given | 70 # Make sure options.channel is a valid |
132 if options.channel: | 71 if not options.channel: |
133 if options.channel not in bot_utils.Channel.ALL_CHANNELS: | 72 die('Specify --channel=be/dev/stable') |
134 print 'You must supply a valid channel to --channel to promote' | 73 if options.channel not in bot_utils.Channel.ALL_CHANNELS: |
135 parser.print_help() | 74 die('You must supply a valid channel to --channel to promote') |
136 sys.exit(3) | 75 else: |
| 76 die('Invalid command specified: {0}. See help below'.format(args[0])) |
137 | 77 |
138 if not (options.continuous or options.integration or | 78 if command == 'promote': |
139 options.testing or options.trunk or options.internal or | 79 _PromoteDartArchiveBuild(options.channel, options.revision) |
140 options.channel): | |
141 print ('Specify --continuous, --integration, --testing, --trunk or ' | |
142 '--channel=be/dev/stable') | |
143 parser.print_help() | |
144 sys.exit(4) | |
145 if options.continuous and options.integration: | |
146 print 'continuous and integration can not be specified at the same time' | |
147 parser.print_help() | |
148 sys.exit(5) | |
149 if (options.continuous or options.integration) and options.testing: | |
150 print """Warning --continuous or --integration and --testing are | |
151 specified. The --testing flag will take precedence and all data will | |
152 go to the testing bucket {0}""".format(TESTING) | |
153 elif args[0] == 'cleanup': | |
154 command = 'cleanup' | |
155 if options.keepcount is None: | |
156 print 'You must specify --keepcount' | |
157 parser.print_help() | |
158 sys.exit(6) | |
159 else: | |
160 print 'Invalid command specified: {0}. See help below'.format(args[0]) | |
161 parser.print_help() | |
162 sys.exit(2) | |
163 | |
164 gsu = gsutil.GsUtil(options.dryrun, options.gsutilloc) | |
165 if options.testing: | |
166 bucket_from = CONTINUOUS | |
167 bucket_to = TESTING | |
168 print """The --testing attribute is specified. All data will go to the | |
169 testing bucket {0}. Press enter to continue""".format(TESTING) | |
170 raw_input('Press Enter to continue') | |
171 elif options.continuous: | |
172 bucket_from = CONTINUOUS | |
173 bucket_to = INTEGRATION | |
174 elif options.trunk: | |
175 bucket_from = TRUNK | |
176 bucket_to = INTEGRATION | |
177 elif options.integration: | |
178 bucket_from = INTEGRATION | |
179 bucket_to = RELEASE | |
180 elif options.internal: | |
181 bucket_from = TRUNK | |
182 bucket_to = INTERNAL | |
183 | |
184 if command == 'cleanup': | |
185 #if the testing flag is set remove the date from the testing bucket | |
186 if options.testing: | |
187 bucket = TESTING | |
188 #otherwise use the value passed as --gsbucketuri | |
189 else: | |
190 bucket = options.gsbucketuri | |
191 version_dirs = _ReadBucket(gsu, '{0}/{1}'.format(bucket, '[0-9]*')) | |
192 _RemoveElements(gsu, bucket, version_dirs, options.keepcount) | |
193 elif command == 'promote': | |
194 if options.channel: | |
195 _PromoteDartArchiveBuild(options.channel, options.revision) | |
196 else: | |
197 _PromoteBuild(options.revision, bucket_from, bucket_to) | |
198 _UpdateDocs() | |
199 | 80 |
200 | 81 |
201 def _UpdateDocs(): | 82 def UpdateDocs(): |
202 try: | 83 try: |
203 print 'Updating docs' | 84 print 'Updating docs' |
204 url = "http://api.dartlang.org/docs/releases/latest/?force_reload=true" | 85 url = "http://api.dartlang.org/docs/releases/latest/?force_reload=true" |
205 f = urllib.urlopen(url) | 86 f = urllib.urlopen(url) |
206 f.read() | 87 f.read() |
207 print 'Successfully updated api docs' | 88 print 'Successfully updated api docs' |
208 except Exception as e: | 89 except Exception as e: |
209 print 'Could not update api docs, please manually update them' | 90 print 'Could not update api docs, please manually update them' |
210 print 'Failed with: %s' % e | 91 print 'Failed with: %s' % e |
211 | 92 |
212 def _ReadBucket(gsu, bucket): | |
213 """Read the contents of a Google Storage Bucket. | |
214 | |
215 Args: | |
216 gsu: the location of the gsutil program | |
217 bucket: the bucket to read the contents of | |
218 | |
219 Returns: | |
220 a list of bucket entries excluding all entries starting with "latest" | |
221 """ | |
222 _PrintSeparator('_ReadBucket({0}, {1})'.format(gsu, bucket)) | |
223 elements = [] | |
224 items = gsu.ReadBucket(bucket) | |
225 for item in items: | |
226 dirpaths = item.split('/') | |
227 if len(dirpaths) >= 3: | |
228 dirname = dirpaths[3] | |
229 if dirname != 'latest': | |
230 try: | |
231 dirnum = int(dirname) | |
232 if not dirnum in elements: | |
233 elements.append(dirnum) | |
234 except ValueError: | |
235 pass | |
236 | |
237 return elements | |
238 | |
239 | |
240 def _RemoveElements(gsu, bucket, version_dirs, keepcount): | |
241 """Remove the selected elements from Google Storage. | |
242 | |
243 Args: | |
244 gsu: the gsutil program to run | |
245 bucket: the bucket to remove the dirs from | |
246 version_dirs: the dictionary of elements to remove keyed by | |
247 svn version number | |
248 keepcount: the number of elements to keep | |
249 """ | |
250 _PrintSeparator('_RemoveElements({0}, version_dirs,' | |
251 ' {1}'.format(gsu, keepcount)) | |
252 version_dirs_size = len(version_dirs) | |
253 delete_count = version_dirs_size - keepcount | |
254 if delete_count > 0: | |
255 count = 0 | |
256 version_dirs.sort() | |
257 for gs_dir in version_dirs: | |
258 if count < delete_count: | |
259 gsu.RemoveAll('{0}/{1}/*'.format(bucket, gs_dir)) | |
260 else: | |
261 print 'version {0}/{1} will be saved'.format(bucket, gs_dir) | |
262 count += 1 | |
263 else: | |
264 print ('nothing to delete because that are only {0} elemens in the list' | |
265 ' and the keep count is set to {0}').format(len(version_dirs), | |
266 keepcount) | |
267 | |
268 | |
269 def _PromoteBuild(revision, from_bucket, to_bucket): | |
270 """Promote a build from one bucket to another. | |
271 | |
272 Args: | |
273 revision: the revision to promote | |
274 from_bucket: the bucket to promote from | |
275 to_bucket: the bucket to promote to | |
276 """ | |
277 | |
278 # print the gsutil version | |
279 _Gsutil(['version']) | |
280 | |
281 src = '%s/%s/' % (from_bucket, revision) | |
282 srcVersion = src + 'VERSION' | |
283 | |
284 # copy from continuous/1234 to trunk/1234 | |
285 dest = '%s/%s/' % (to_bucket, revision) | |
286 destUpdate = dest + 'eclipse-update/' | |
287 print 'copying: %s -> %s' % (src, dest) | |
288 _Gsutil(['cp', '-a', 'public-read', srcVersion, destUpdate + 'features/']) | |
289 _Gsutil(['cp', '-a', 'public-read', srcVersion, destUpdate + 'plugins/']) | |
290 _Gsutil(['cp', '-r', '-a', 'public-read', src + '*', dest]) | |
291 | |
292 # copy from continuous/1234 to trunk/latest | |
293 dest = '%s/%s/' % (to_bucket, 'latest') | |
294 destUpdate = dest + 'eclipse-update/' | |
295 print 'copying: %s -> %s' % (src, dest) | |
296 _Gsutil(['cp', '-a', 'public-read', srcVersion, destUpdate + 'features/']) | |
297 _Gsutil(['cp', '-a', 'public-read', srcVersion, destUpdate + 'plugins/']) | |
298 _Gsutil(['cp', '-r', '-a', 'public-read', src + '*', dest]) | |
299 | 93 |
300 def _PromoteDartArchiveBuild(channel, revision): | 94 def _PromoteDartArchiveBuild(channel, revision): |
301 # These namer objects will be used to create GCS object URIs. For the | 95 # These namer objects will be used to create GCS object URIs. For the |
302 # structure we use, please see tools/bots/bot_utils.py:GCSNamer | 96 # structure we use, please see tools/bots/bot_utils.py:GCSNamer |
303 raw_namer = bot_utils.GCSNamer(channel, bot_utils.ReleaseType.RAW) | 97 raw_namer = bot_utils.GCSNamer(channel, bot_utils.ReleaseType.RAW) |
304 signed_namer = bot_utils.GCSNamer(channel, bot_utils.ReleaseType.SIGNED) | 98 signed_namer = bot_utils.GCSNamer(channel, bot_utils.ReleaseType.SIGNED) |
305 release_namer = bot_utils.GCSNamer(channel, bot_utils.ReleaseType.RELEASE) | 99 release_namer = bot_utils.GCSNamer(channel, bot_utils.ReleaseType.RELEASE) |
306 | 100 |
307 def promote(to_revision): | 101 def promote(to_revision): |
308 def safety_check_on_gs_path(gs_path, revision, channel): | 102 def safety_check_on_gs_path(gs_path, revision, channel): |
309 if not ((revision == 'latest' or int(revision) > 0) | 103 if not ((revision == 'latest' or int(revision) > 0) |
310 and len(channel) > 0 | 104 and len(channel) > 0 |
311 and ('%s' % revision) in gs_path | 105 and ('%s' % revision) in gs_path |
312 and channel in gs_path): | 106 and channel in gs_path): |
313 raise Exception( | 107 raise Exception( |
314 "InternalError: Sanity check failed on GS URI: %s" % gs_path) | 108 "InternalError: Sanity check failed on GS URI: %s" % gs_path) |
315 | 109 |
316 def remove_gs_directory(gs_path): | 110 def remove_gs_directory(gs_path): |
317 safety_check_on_gs_path(gs_path, to_revision, channel) | 111 safety_check_on_gs_path(gs_path, to_revision, channel) |
318 _Gsutil(['-m', 'rm', '-R', '-f', gs_path]) | 112 Gsutil(['-m', 'rm', '-R', '-f', gs_path]) |
319 | 113 |
320 # Copy VERSION file. | 114 # Copy VERSION file. |
321 from_loc = raw_namer.version_filepath(revision) | 115 from_loc = raw_namer.version_filepath(revision) |
322 to_loc = release_namer.version_filepath(to_revision) | 116 to_loc = release_namer.version_filepath(to_revision) |
323 _Gsutil(['cp', '-a', 'public-read', from_loc, to_loc]) | 117 Gsutil(['cp', '-a', 'public-read', from_loc, to_loc]) |
324 | 118 |
325 # Copy sdk directory. | 119 # Copy sdk directory. |
326 from_loc = raw_namer.sdk_directory(revision) | 120 from_loc = raw_namer.sdk_directory(revision) |
327 to_loc = release_namer.sdk_directory(to_revision) | 121 to_loc = release_namer.sdk_directory(to_revision) |
328 remove_gs_directory(to_loc) | 122 remove_gs_directory(to_loc) |
329 _Gsutil(['-m', 'cp', '-a', 'public-read', '-R', from_loc, to_loc]) | 123 Gsutil(['-m', 'cp', '-a', 'public-read', '-R', from_loc, to_loc]) |
330 | 124 |
331 # Copy eclipse update directory. | 125 # Copy eclipse update directory. |
332 from_loc = raw_namer.editor_eclipse_update_directory(revision) | 126 from_loc = raw_namer.editor_eclipse_update_directory(revision) |
333 to_loc = release_namer.editor_eclipse_update_directory(to_revision) | 127 to_loc = release_namer.editor_eclipse_update_directory(to_revision) |
334 remove_gs_directory(to_loc) | 128 remove_gs_directory(to_loc) |
335 _Gsutil(['-m', 'cp', '-a', 'public-read', '-R', from_loc, to_loc]) | 129 Gsutil(['-m', 'cp', '-a', 'public-read', '-R', from_loc, to_loc]) |
336 | 130 |
337 # Copy api-docs zipfile. | 131 # Copy api-docs zipfile. |
338 from_loc = raw_namer.apidocs_zipfilepath(revision) | 132 from_loc = raw_namer.apidocs_zipfilepath(revision) |
339 to_loc = release_namer.apidocs_zipfilepath(to_revision) | 133 to_loc = release_namer.apidocs_zipfilepath(to_revision) |
340 _Gsutil(['-m', 'cp', '-a', 'public-read', from_loc, to_loc]) | 134 Gsutil(['-m', 'cp', '-a', 'public-read', from_loc, to_loc]) |
341 | 135 |
342 # Copy dartium directory. | 136 # Copy dartium directory. |
343 from_loc = raw_namer.dartium_directory(revision) | 137 from_loc = raw_namer.dartium_directory(revision) |
344 to_loc = release_namer.dartium_directory(to_revision) | 138 to_loc = release_namer.dartium_directory(to_revision) |
345 remove_gs_directory(to_loc) | 139 remove_gs_directory(to_loc) |
346 _Gsutil(['-m', 'cp', '-a', 'public-read', '-R', from_loc, to_loc]) | 140 Gsutil(['-m', 'cp', '-a', 'public-read', '-R', from_loc, to_loc]) |
347 | 141 |
348 # Copy editor zip files. | 142 # Copy editor zip files. |
349 target_editor_dir = release_namer.editor_directory(to_revision) | 143 target_editor_dir = release_namer.editor_directory(to_revision) |
350 remove_gs_directory(target_editor_dir) | 144 remove_gs_directory(target_editor_dir) |
351 for system in ['windows', 'macos', 'linux']: | 145 for system in ['windows', 'macos', 'linux']: |
352 for arch in ['ia32', 'x64']: | 146 for arch in ['ia32', 'x64']: |
353 from_namer = raw_namer | 147 from_namer = raw_namer |
354 # We have signed versions of the editor for windows and macos. | 148 # We have signed versions of the editor for windows and macos. |
355 if system == 'windows' or system == 'macos': | 149 if system == 'windows' or system == 'macos': |
356 from_namer = signed_namer | 150 from_namer = signed_namer |
357 from_loc = from_namer.editor_zipfilepath(revision, system, arch) | 151 from_loc = from_namer.editor_zipfilepath(revision, system, arch) |
358 to_loc = release_namer.editor_zipfilepath(to_revision, system, arch) | 152 to_loc = release_namer.editor_zipfilepath(to_revision, system, arch) |
359 _Gsutil(['cp', '-a', 'public-read', from_loc, to_loc]) | 153 Gsutil(['cp', '-a', 'public-read', from_loc, to_loc]) |
360 _Gsutil(['cp', '-a', 'public-read', from_loc + '.md5sum', | 154 Gsutil(['cp', '-a', 'public-read', from_loc + '.md5sum', |
361 to_loc + '.md5sum']) | 155 to_loc + '.md5sum']) |
362 | 156 |
| 157 # Copy signed editor installers for macos/windows. |
| 158 for system, extension in [('windows', 'msi'), ('macos', 'dmg')]: |
| 159 for arch in ['ia32', 'x64']: |
| 160 from_loc = signed_namer.editor_installer_filepath( |
| 161 revision, system, arch, extension) |
| 162 to_loc = release_namer.editor_installer_filepath( |
| 163 to_revision, system, arch, extension) |
| 164 Gsutil(['cp', '-a', 'public-read', from_loc, to_loc]) |
| 165 |
363 promote(revision) | 166 promote(revision) |
364 promote('latest') | 167 promote('latest') |
365 | 168 |
366 def _PrintSeparator(text): | 169 def Gsutil(cmd): |
367 print '================================' | |
368 print '== %s' % text | |
369 | |
370 | |
371 def _PrintFailure(text): | |
372 print '*****************************' | |
373 print '** %s' % text | |
374 print '*****************************' | |
375 | |
376 | |
377 def _Gsutil(cmd): | |
378 gsutilTool = join(DART_PATH, 'third_party', 'gsutil', 'gsutil') | 170 gsutilTool = join(DART_PATH, 'third_party', 'gsutil', 'gsutil') |
379 return _ExecuteCommand([sys.executable, gsutilTool] + cmd) | 171 bot_utils.run([sys.executable, gsutilTool] + cmd) |
380 | |
381 | |
382 def _ExecuteCommand(cmd, directory=None): | |
383 """Execute the given command.""" | |
384 if directory is not None: | |
385 cwd = os.getcwd() | |
386 os.chdir(directory) | |
387 subprocess.call(cmd, env=os.environ) | |
388 if directory is not None: | |
389 os.chdir(cwd) | |
390 | 172 |
391 | 173 |
392 if __name__ == '__main__': | 174 if __name__ == '__main__': |
393 sys.exit(main()) | 175 sys.exit(main()) |
OLD | NEW |