OLD | NEW |
1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
2 # Copyright 2014 The Chromium Authors. All rights reserved. | 2 # Copyright 2014 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 # TODO(hinoka): Use logging. | 6 # TODO(hinoka): Use logging. |
7 | 7 |
8 import codecs | 8 import codecs |
9 import copy | 9 import copy |
10 import cStringIO | 10 import cStringIO |
(...skipping 246 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
257 | 257 |
258 | 258 |
259 class GclientSyncFailed(SubprocessFailed): | 259 class GclientSyncFailed(SubprocessFailed): |
260 pass | 260 pass |
261 | 261 |
262 | 262 |
263 class SVNRevisionNotFound(Exception): | 263 class SVNRevisionNotFound(Exception): |
264 pass | 264 pass |
265 | 265 |
266 | 266 |
| 267 class InvalidDiff(Exception): |
| 268 pass |
| 269 |
| 270 |
267 def call(*args, **kwargs): | 271 def call(*args, **kwargs): |
268 """Interactive subprocess call.""" | 272 """Interactive subprocess call.""" |
269 kwargs['stdout'] = subprocess.PIPE | 273 kwargs['stdout'] = subprocess.PIPE |
270 kwargs['stderr'] = subprocess.STDOUT | 274 kwargs['stderr'] = subprocess.STDOUT |
271 stdin_data = kwargs.pop('stdin_data', None) | 275 stdin_data = kwargs.pop('stdin_data', None) |
| 276 tries = kwargs.pop('tries', RETRIES) |
272 if stdin_data: | 277 if stdin_data: |
273 kwargs['stdin'] = subprocess.PIPE | 278 kwargs['stdin'] = subprocess.PIPE |
274 out = cStringIO.StringIO() | 279 out = cStringIO.StringIO() |
275 new_env = kwargs.get('env', {}) | 280 new_env = kwargs.get('env', {}) |
276 env = copy.copy(os.environ) | 281 env = copy.copy(os.environ) |
277 env.update(new_env) | 282 env.update(new_env) |
278 kwargs['env'] = env | 283 kwargs['env'] = env |
279 for attempt in xrange(RETRIES): | 284 for attempt in range(tries): |
280 attempt_msg = ' (retry #%d)' % attempt if attempt else '' | 285 attempt_msg = ' (retry #%d)' % attempt if attempt else '' |
281 if new_env: | 286 if new_env: |
282 print '===Injecting Environment Variables===' | 287 print '===Injecting Environment Variables===' |
283 for k, v in sorted(new_env.items()): | 288 for k, v in sorted(new_env.items()): |
284 print '%s: %s' % (k, v) | 289 print '%s: %s' % (k, v) |
285 print '===Running %s%s===' % (' '.join(args), attempt_msg) | 290 print '===Running %s%s===' % (' '.join(args), attempt_msg) |
286 start_time = time.time() | 291 start_time = time.time() |
287 proc = subprocess.Popen(args, **kwargs) | 292 proc = subprocess.Popen(args, **kwargs) |
288 if stdin_data: | 293 if stdin_data: |
289 proc.stdin.write(stdin_data) | 294 proc.stdin.write(stdin_data) |
(...skipping 17 matching lines...) Expand all Loading... |
307 code = proc.wait() | 312 code = proc.wait() |
308 elapsed_time = ((time.time() - start_time) / 60.0) | 313 elapsed_time = ((time.time() - start_time) / 60.0) |
309 if not code: | 314 if not code: |
310 print '===Succeeded in %.1f mins===' % elapsed_time | 315 print '===Succeeded in %.1f mins===' % elapsed_time |
311 print | 316 print |
312 return out.getvalue() | 317 return out.getvalue() |
313 print '===Failed in %.1f mins===' % elapsed_time | 318 print '===Failed in %.1f mins===' % elapsed_time |
314 print | 319 print |
315 | 320 |
316 raise SubprocessFailed('%s failed with code %d in %s after %d attempts.' % | 321 raise SubprocessFailed('%s failed with code %d in %s after %d attempts.' % |
317 (' '.join(args), code, os.getcwd(), RETRIES), code) | 322 (' '.join(args), code, os.getcwd(), tries), code) |
318 | 323 |
319 | 324 |
320 def git(*args, **kwargs): | 325 def git(*args, **kwargs): |
321 """Wrapper around call specifically for Git commands.""" | 326 """Wrapper around call specifically for Git commands.""" |
322 if args and args[0] == 'cache': | 327 if args and args[0] == 'cache': |
323 # Rewrite "git cache" calls into "python git_cache.py". | 328 # Rewrite "git cache" calls into "python git_cache.py". |
324 cmd = (sys.executable, '-u', GIT_CACHE_PATH) + args[1:] | 329 cmd = (sys.executable, '-u', GIT_CACHE_PATH) + args[1:] |
325 else: | 330 else: |
326 git_executable = 'git' | 331 git_executable = 'git' |
327 # On windows, subprocess doesn't fuzzy-match 'git' to 'git.bat', so we | 332 # On windows, subprocess doesn't fuzzy-match 'git' to 'git.bat', so we |
(...skipping 215 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
543 | 548 |
544 | 549 |
545 def _last_commit_for_file(filename, repo_base): | 550 def _last_commit_for_file(filename, repo_base): |
546 cmd = ['log', '--format=%H', '--max-count=1', '--', filename] | 551 cmd = ['log', '--format=%H', '--max-count=1', '--', filename] |
547 return git(*cmd, cwd=repo_base).strip() | 552 return git(*cmd, cwd=repo_base).strip() |
548 | 553 |
549 | 554 |
550 def need_to_run_deps2git(repo_base, deps_file, deps_git_file): | 555 def need_to_run_deps2git(repo_base, deps_file, deps_git_file): |
551 """Checks to see if we need to run deps2git. | 556 """Checks to see if we need to run deps2git. |
552 | 557 |
553 Returns True if there was a DEPS change after the last .DEPS.git update. | 558 Returns True if there was a DEPS change after the last .DEPS.git update |
| 559 or if DEPS has local modifications. |
554 """ | 560 """ |
555 print 'Checking if %s exists' % deps_git_file | 561 print 'Checking if %s exists' % deps_git_file |
556 if not path.isfile(deps_git_file): | 562 if not path.isfile(deps_git_file): |
557 # .DEPS.git doesn't exist but DEPS does? We probably want to generate one. | 563 # .DEPS.git doesn't exist but DEPS does? We probably want to generate one. |
558 print 'it exists!' | 564 print 'it doesn\'t exist!' |
| 565 return True |
| 566 |
| 567 if git('diff', deps_file, cwd=repo_base).strip(): |
559 return True | 568 return True |
560 | 569 |
561 last_known_deps_ref = _last_commit_for_file(deps_file, repo_base) | 570 last_known_deps_ref = _last_commit_for_file(deps_file, repo_base) |
562 last_known_deps_git_ref = _last_commit_for_file(deps_git_file, repo_base) | 571 last_known_deps_git_ref = _last_commit_for_file(deps_git_file, repo_base) |
563 merge_base_ref = git('merge-base', last_known_deps_ref, | 572 merge_base_ref = git('merge-base', last_known_deps_ref, |
564 last_known_deps_git_ref, cwd=repo_base).strip() | 573 last_known_deps_git_ref, cwd=repo_base).strip() |
565 | 574 |
566 # If the merge base of the last DEPS and last .DEPS.git file is not | 575 # If the merge base of the last DEPS and last .DEPS.git file is not |
567 # equivilent to the hash of the last DEPS file, that means the DEPS file | 576 # equivilent to the hash of the last DEPS file, that means the DEPS file |
568 # was committed after the last .DEPS.git file. | 577 # was committed after the last .DEPS.git file. |
(...skipping 47 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
616 version = m.group(1) | 625 version = m.group(1) |
617 git_buildspec = get_git_buildspec(version) | 626 git_buildspec = get_git_buildspec(version) |
618 with open(deps_git_file, 'wb') as f: | 627 with open(deps_git_file, 'wb') as f: |
619 f.write(git_buildspec) | 628 f.write(git_buildspec) |
620 | 629 |
621 | 630 |
622 def ensure_deps2git(sln_dir, shallow): | 631 def ensure_deps2git(sln_dir, shallow): |
623 repo_base = path.join(os.getcwd(), sln_dir) | 632 repo_base = path.join(os.getcwd(), sln_dir) |
624 deps_file = path.join(repo_base, 'DEPS') | 633 deps_file = path.join(repo_base, 'DEPS') |
625 deps_git_file = path.join(repo_base, '.DEPS.git') | 634 deps_git_file = path.join(repo_base, '.DEPS.git') |
626 print 'Checking if %s is newer than %s' % (deps_file, deps_git_file) | |
627 if not path.isfile(deps_file): | 635 if not path.isfile(deps_file): |
628 return | 636 return |
629 | 637 |
| 638 print 'Checking if %s is newer than %s' % (deps_file, deps_git_file) |
630 if not need_to_run_deps2git(repo_base, deps_file, deps_git_file): | 639 if not need_to_run_deps2git(repo_base, deps_file, deps_git_file): |
631 return | 640 return |
632 | 641 |
633 print '===DEPS file modified, need to run deps2git===' | 642 print '===DEPS file modified, need to run deps2git===' |
634 # Magic to get deps2git to work with internal DEPS. | 643 # Magic to get deps2git to work with internal DEPS. |
635 shutil.copyfile(S2G_INTERNAL_FROM_PATH, S2G_INTERNAL_DEST_PATH) | 644 shutil.copyfile(S2G_INTERNAL_FROM_PATH, S2G_INTERNAL_DEST_PATH) |
636 | 645 |
637 # TODO(hinoka): This might need to be smarter if we need to deal with | 646 # TODO(hinoka): This might need to be smarter if we need to deal with |
638 # DEPS changes that are in an internal repository. | 647 # DEPS changes that are in an internal repository. |
639 repo_type = 'public' | 648 repo_type = 'public' |
(...skipping 139 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
779 def _download(url): | 788 def _download(url): |
780 """Fetch url and return content, with retries for flake.""" | 789 """Fetch url and return content, with retries for flake.""" |
781 for attempt in xrange(RETRIES): | 790 for attempt in xrange(RETRIES): |
782 try: | 791 try: |
783 return urllib2.urlopen(url).read() | 792 return urllib2.urlopen(url).read() |
784 except Exception: | 793 except Exception: |
785 if attempt == RETRIES - 1: | 794 if attempt == RETRIES - 1: |
786 raise | 795 raise |
787 | 796 |
788 | 797 |
789 def apply_issue_svn(root, patch_url): | 798 def parse_diff(diff): |
790 patch_data = call('svn', 'cat', patch_url) | 799 """Takes a unified diff and returns a list of diffed files and their diffs. |
791 call(PATCH_TOOL, '-p0', '--remove-empty-files', '--force', '--forward', | 800 |
792 stdin_data=patch_data, cwd=root) | 801 The return format is a list of pairs of: |
| 802 (<filename>, <diff contents>) |
| 803 <diff contents> is inclusive of the diff line. |
| 804 """ |
| 805 result = [] |
| 806 current_diff = '' |
| 807 current_header = None |
| 808 for line in diff.splitlines(): |
| 809 # "diff" is for git style patches, and "Index: " is for SVN style patches. |
| 810 if line.startswith('diff') or line.startswith('Index: '): |
| 811 if current_header: |
| 812 # If we are in a diff portion, then save the diff. |
| 813 result.append((current_header, '%s\n' % current_diff)) |
| 814 git_header_match = re.match(r'diff (?:--git )?(\S+) (\S+)', line) |
| 815 svn_header_match = re.match(r'Index: (.*)', line) |
| 816 |
| 817 if git_header_match: |
| 818 # First, see if its a git style header. |
| 819 from_file = git_header_match.group(1) |
| 820 to_file = git_header_match.group(2) |
| 821 if from_file != to_file and from_file.startswith('a/'): |
| 822 # Sometimes git prepends 'a/' and 'b/' in front of file paths. |
| 823 from_file = from_file[2:] |
| 824 current_header = from_file |
| 825 |
| 826 elif svn_header_match: |
| 827 # Otherwise, check if its an SVN style header. |
| 828 current_header = svn_header_match.group(1) |
| 829 |
| 830 else: |
| 831 # Otherwise... I'm not really sure what to do with this. |
| 832 raise InvalidDiff('Can\'t process header: %s\nFull diff:\n%s' % |
| 833 (line, diff)) |
| 834 |
| 835 current_diff = '' |
| 836 current_diff += '%s\n' % line |
| 837 if current_header: |
| 838 # We hit EOF, gotta save the last diff. |
| 839 result.append((current_header, current_diff)) |
| 840 return result |
793 | 841 |
794 | 842 |
795 def apply_issue_rietveld(issue, patchset, root, server, rev_map, revision): | 843 def get_svn_patch(patch_url): |
| 844 """Fetch patch from patch_url, return list of (filename, diff)""" |
| 845 patch_data = call('svn', 'cat', patch_url) |
| 846 return parse_diff(patch_data) |
| 847 |
| 848 |
| 849 def apply_svn_patch(patch_root, patches, whitelist=None, blacklist=None): |
| 850 """Expects a list of (filename, diff), applies it on top of patch_root.""" |
| 851 if whitelist: |
| 852 patches = [(name, diff) for name, diff in patches if name in whitelist] |
| 853 elif blacklist: |
| 854 patches = [(name, diff) for name, diff in patches if name not in blacklist] |
| 855 diffs = [diff for _, diff in patches] |
| 856 patch = ''.join(diffs) |
| 857 |
| 858 if patch: |
| 859 print '===Patching files===' |
| 860 for filename, _ in patches: |
| 861 print 'Patching %s' % filename |
| 862 call(PATCH_TOOL, '-p0', '--remove-empty-files', '--force', '--forward', |
| 863 stdin_data=patch, cwd=patch_root, tries=1) |
| 864 |
| 865 |
| 866 def apply_rietveld_issue(issue, patchset, root, server, rev_map, revision, |
| 867 whitelist=None, blacklist=None): |
796 apply_issue_bin = ('apply_issue.bat' if sys.platform.startswith('win') | 868 apply_issue_bin = ('apply_issue.bat' if sys.platform.startswith('win') |
797 else 'apply_issue') | 869 else 'apply_issue') |
798 call(apply_issue_bin, | 870 cmd = [apply_issue_bin, |
799 '--root_dir', root, | 871 # The patch will be applied on top of this directory. |
800 '--issue', issue, | 872 '--root_dir', root, |
801 '--patchset', patchset, | 873 # Tell apply_issue how to fetch the patch. |
802 '--no-auth', | 874 '--issue', issue, |
803 '--server', server, | 875 '--patchset', patchset, |
804 '--base_ref', revision, | 876 '--no-auth', |
805 '--force', | 877 '--server', server, |
806 '--ignore_deps') | 878 # Always run apply_issue.py, otherwise it would see update.flag |
| 879 # and then bail out. |
| 880 '--force', |
| 881 # Don't run gclient sync when it sees a DEPS change. |
| 882 '--ignore_deps', |
| 883 # Don't commit the patch or add it to the index. |
| 884 '--no_commit', |
| 885 ] |
| 886 if whitelist: |
| 887 for item in whitelist: |
| 888 cmd.extend(['--whitelist', item]) |
| 889 elif blacklist: |
| 890 for item in blacklist: |
| 891 cmd.extend(['--blacklist', item]) |
| 892 |
| 893 # Only try once, since subsequent failures hide the real failure. |
| 894 call(*cmd, tries=1) |
807 | 895 |
808 | 896 |
809 def check_flag(flag_file): | 897 def check_flag(flag_file): |
810 """Returns True if the flag file is present.""" | 898 """Returns True if the flag file is present.""" |
811 return os.path.isfile(flag_file) | 899 return os.path.isfile(flag_file) |
812 | 900 |
813 | 901 |
814 def delete_flag(flag_file): | 902 def delete_flag(flag_file): |
815 """Remove bot update flag.""" | 903 """Remove bot update flag.""" |
816 if os.path.isfile(flag_file): | 904 if os.path.isfile(flag_file): |
817 os.remove(flag_file) | 905 os.remove(flag_file) |
818 | 906 |
819 | 907 |
820 def emit_flag(flag_file): | 908 def emit_flag(flag_file): |
821 """Deposit a bot update flag on the system to tell gclient not to run.""" | 909 """Deposit a bot update flag on the system to tell gclient not to run.""" |
822 print 'Emitting flag file at %s' % flag_file | 910 print 'Emitting flag file at %s' % flag_file |
823 with open(flag_file, 'wb') as f: | 911 with open(flag_file, 'wb') as f: |
824 f.write('Success!') | 912 f.write('Success!') |
825 | 913 |
826 | 914 |
827 def get_real_git_hash(git_hash, dir_name): | |
828 """Return git_hash or the parent of git_hash if its a patched hash.""" | |
829 log = git('log', '-1', '--format=%B', cwd=dir_name).strip() | |
830 if 'committed patch' in log.lower(): | |
831 return git('log', '-1', '--format=%H', 'HEAD^', cwd=dir_name).strip() | |
832 return git_hash | |
833 | |
834 | |
835 def parse_got_revision(gclient_output, got_revision_mapping, use_svn_revs): | 915 def parse_got_revision(gclient_output, got_revision_mapping, use_svn_revs): |
836 """Translate git gclient revision mapping to build properties. | 916 """Translate git gclient revision mapping to build properties. |
837 | 917 |
838 If use_svn_revs is True, then translate git hashes in the revision mapping | 918 If use_svn_revs is True, then translate git hashes in the revision mapping |
839 to svn revision numbers. | 919 to svn revision numbers. |
840 """ | 920 """ |
841 properties = {} | 921 properties = {} |
842 solutions_output = gclient_output['solutions'] | 922 solutions_output = gclient_output['solutions'] |
843 for dir_name, property_name in got_revision_mapping.iteritems(): | 923 for dir_name, property_name in got_revision_mapping.iteritems(): |
844 if dir_name not in solutions_output: | 924 if dir_name not in solutions_output: |
(...skipping 45 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
890 | 970 |
891 def ensure_checkout(solutions, revisions, first_sln, target_os, target_os_only, | 971 def ensure_checkout(solutions, revisions, first_sln, target_os, target_os_only, |
892 root, issue, patchset, patch_url, rietveld_server, | 972 root, issue, patchset, patch_url, rietveld_server, |
893 revision_mapping, buildspec_name, gyp_env, shallow): | 973 revision_mapping, buildspec_name, gyp_env, shallow): |
894 # Get a checkout of each solution, without DEPS or hooks. | 974 # Get a checkout of each solution, without DEPS or hooks. |
895 # Calling git directly because there is no way to run Gclient without | 975 # Calling git directly because there is no way to run Gclient without |
896 # invoking DEPS. | 976 # invoking DEPS. |
897 print 'Fetching Git checkout' | 977 print 'Fetching Git checkout' |
898 git_ref = git_checkout(solutions, revisions, shallow) | 978 git_ref = git_checkout(solutions, revisions, shallow) |
899 | 979 |
900 # If either patch_url or issue is passed in, then we need to apply a patch. | 980 patches = None |
901 if patch_url: | 981 if patch_url: |
902 # patch_url takes precidence since its only passed in on gcl try/git try. | 982 patches = get_svn_patch(patch_url) |
903 apply_issue_svn(root, patch_url) | 983 |
904 elif issue: | 984 if root == first_sln: |
905 apply_issue_rietveld(issue, patchset, root, rietveld_server, | 985 # Only top level DEPS patching is supported right now. |
906 revision_mapping, git_ref) | 986 if patches: |
| 987 apply_svn_patch(root, patches, whitelist=['DEPS']) |
| 988 elif issue: |
| 989 apply_rietveld_issue(issue, patchset, root, rietveld_server, |
| 990 revision_mapping, git_ref, whitelist=['DEPS']) |
| 991 |
907 | 992 |
908 if buildspec_name: | 993 if buildspec_name: |
909 buildspecs2git(root, buildspec_name) | 994 buildspecs2git(root, buildspec_name) |
910 elif first_sln == root: | 995 else: |
911 # Run deps2git if there is a DEPS commit after the last .DEPS.git commit. | 996 # Run deps2git if there is a DEPS change after the last .DEPS.git commit. |
912 # We only need to ensure deps2git if the root is not overridden, since | |
913 # if the root is overridden, it means we are working with a sub repository | |
914 # patch, which means its impossible for it to touch DEPS. | |
915 ensure_deps2git(root, shallow) | 997 ensure_deps2git(root, shallow) |
916 | 998 |
917 # Ensure our build/ directory is set up with the correct .gclient file. | 999 # Ensure our build/ directory is set up with the correct .gclient file. |
918 gclient_configure(solutions, target_os, target_os_only) | 1000 gclient_configure(solutions, target_os, target_os_only) |
919 | 1001 |
920 # Let gclient do the DEPS syncing. | 1002 # Let gclient do the DEPS syncing. |
921 gclient_output = gclient_sync(buildspec_name) | 1003 gclient_output = gclient_sync(buildspec_name) |
922 if buildspec_name: | 1004 if buildspec_name: |
923 # Run gclient runhooks if we're on an official builder. | 1005 # Run gclient runhooks if we're on an official builder. |
924 # TODO(hinoka): Remove this when the official builders run their own | 1006 # TODO(hinoka): Remove this when the official builders run their own |
925 # runhooks step. | 1007 # runhooks step. |
926 gclient_runhooks(gyp_env) | 1008 gclient_runhooks(gyp_env) |
927 | 1009 |
928 # Finally, ensure that all DEPS are pinned to the correct revision. | 1010 # Finally, ensure that all DEPS are pinned to the correct revision. |
929 dir_names = [sln['name'] for sln in solutions] | 1011 dir_names = [sln['name'] for sln in solutions] |
930 ensure_deps_revisions(gclient_output.get('solutions', {}), | 1012 ensure_deps_revisions(gclient_output.get('solutions', {}), |
931 dir_names, revisions) | 1013 dir_names, revisions) |
| 1014 # Apply the rest of the patch here (sans DEPS) |
| 1015 if patches: |
| 1016 apply_svn_patch(root, patches, blacklist=['DEPS']) |
| 1017 elif issue: |
| 1018 apply_rietveld_issue(issue, patchset, root, rietveld_server, |
| 1019 revision_mapping, git_ref, blacklist=['DEPS']) |
| 1020 |
932 return gclient_output | 1021 return gclient_output |
933 | 1022 |
934 | 1023 |
935 class UploadTelemetryThread(threading.Thread): | 1024 class UploadTelemetryThread(threading.Thread): |
936 def __init__(self, prefix, master, builder, slave, kwargs): | 1025 def __init__(self, prefix, master, builder, slave, kwargs): |
937 super(UploadTelemetryThread, self).__init__() | 1026 super(UploadTelemetryThread, self).__init__() |
938 self.master = master | 1027 self.master = master |
939 self.builder = builder | 1028 self.builder = builder |
940 self.slave = slave | 1029 self.slave = slave |
941 self.prefix = prefix | 1030 self.prefix = prefix |
(...skipping 282 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
1224 specs=options.specs) | 1313 specs=options.specs) |
1225 all_threads.append(thr) | 1314 all_threads.append(thr) |
1226 | 1315 |
1227 # Sort of wait for all telemetry threads to finish. | 1316 # Sort of wait for all telemetry threads to finish. |
1228 for thr in all_threads: | 1317 for thr in all_threads: |
1229 thr.join(5) | 1318 thr.join(5) |
1230 | 1319 |
1231 | 1320 |
1232 if __name__ == '__main__': | 1321 if __name__ == '__main__': |
1233 sys.exit(main()) | 1322 sys.exit(main()) |
OLD | NEW |