diff options
Diffstat (limited to 'third_party/libwebrtc/build/util/lastchange.py')
-rwxr-xr-x | third_party/libwebrtc/build/util/lastchange.py | 332 |
1 files changed, 332 insertions, 0 deletions
diff --git a/third_party/libwebrtc/build/util/lastchange.py b/third_party/libwebrtc/build/util/lastchange.py new file mode 100755 index 0000000000..02a36642b1 --- /dev/null +++ b/third_party/libwebrtc/build/util/lastchange.py @@ -0,0 +1,332 @@ +#!/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. + +""" +lastchange.py -- Chromium revision fetching utility. +""" +from __future__ import print_function + +import argparse +import collections +import datetime +import logging +import os +import subprocess +import sys + +VersionInfo = collections.namedtuple("VersionInfo", + ("revision_id", "revision", "timestamp")) + +class GitError(Exception): + pass + +# This function exists for compatibility with logic outside this +# repository that uses this file as a library. +# TODO(eliribble) remove this function after it has been ported into +# the repositories that depend on it +def RunGitCommand(directory, command): + """ + Launches git subcommand. + + Errors are swallowed. + + Returns: + A process object or None. + """ + command = ['git'] + command + # Force shell usage under cygwin. This is a workaround for + # mysterious loss of cwd while invoking cygwin's git. + # We can't just pass shell=True to Popen, as under win32 this will + # cause CMD to be used, while we explicitly want a cygwin shell. + if sys.platform == 'cygwin': + command = ['sh', '-c', ' '.join(command)] + try: + proc = subprocess.Popen(command, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + cwd=directory, + shell=(sys.platform=='win32')) + return proc + except OSError as e: + logging.error('Command %r failed: %s' % (' '.join(command), e)) + return None + + +def _RunGitCommand(directory, command): + """Launches git subcommand. + + Returns: + The stripped stdout of the git command. + Raises: + GitError on failure, including a nonzero return code. + """ + command = ['git'] + command + # Force shell usage under cygwin. This is a workaround for + # mysterious loss of cwd while invoking cygwin's git. + # We can't just pass shell=True to Popen, as under win32 this will + # cause CMD to be used, while we explicitly want a cygwin shell. + if sys.platform == 'cygwin': + command = ['sh', '-c', ' '.join(command)] + try: + logging.info("Executing '%s' in %s", ' '.join(command), directory) + proc = subprocess.Popen(command, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + cwd=directory, + shell=(sys.platform=='win32')) + stdout, stderr = tuple(x.decode(encoding='utf_8') + for x in proc.communicate()) + stdout = stdout.strip() + logging.debug("returncode: %d", proc.returncode) + logging.debug("stdout: %s", stdout) + logging.debug("stderr: %s", stderr) + if proc.returncode != 0 or not stdout: + raise GitError(( + "Git command '{}' in {} failed: " + "rc={}, stdout='{}' stderr='{}'").format( + " ".join(command), directory, proc.returncode, stdout, stderr)) + return stdout + except OSError as e: + raise GitError("Git command 'git {}' in {} failed: {}".format( + " ".join(command), directory, e)) + + +def GetMergeBase(directory, ref): + """ + Return the merge-base of HEAD and ref. + + Args: + directory: The directory containing the .git directory. + ref: The ref to use to find the merge base. + Returns: + The git commit SHA of the merge-base as a string. + """ + logging.debug("Calculating merge base between HEAD and %s in %s", + ref, directory) + command = ['merge-base', 'HEAD', ref] + return _RunGitCommand(directory, command) + + +def FetchGitRevision(directory, commit_filter, start_commit="HEAD"): + """ + Fetch the Git hash (and Cr-Commit-Position if any) for a given directory. + + Args: + directory: The directory containing the .git directory. + commit_filter: A filter to supply to grep to filter commits + start_commit: A commit identifier. The result of this function + will be limited to only consider commits before the provided + commit. + Returns: + A VersionInfo object. On error all values will be 0. + """ + hash_ = '' + + git_args = ['log', '-1', '--format=%H %ct'] + if commit_filter is not None: + git_args.append('--grep=' + commit_filter) + + git_args.append(start_commit) + + output = _RunGitCommand(directory, git_args) + hash_, commit_timestamp = output.split() + if not hash_: + return VersionInfo('0', '0', 0) + + revision = hash_ + output = _RunGitCommand(directory, ['cat-file', 'commit', hash_]) + for line in reversed(output.splitlines()): + if line.startswith('Cr-Commit-Position:'): + pos = line.rsplit()[-1].strip() + logging.debug("Found Cr-Commit-Position '%s'", pos) + revision = "{}-{}".format(hash_, pos) + break + return VersionInfo(hash_, revision, int(commit_timestamp)) + + +def GetHeaderGuard(path): + """ + Returns the header #define guard for the given file path. + This treats everything after the last instance of "src/" as being a + relevant part of the guard. If there is no "src/", then the entire path + is used. + """ + src_index = path.rfind('src/') + if src_index != -1: + guard = path[src_index + 4:] + else: + guard = path + guard = guard.upper() + return guard.replace('/', '_').replace('.', '_').replace('\\', '_') + '_' + + +def GetHeaderContents(path, define, version): + """ + Returns what the contents of the header file should be that indicate the given + revision. + """ + header_guard = GetHeaderGuard(path) + + header_contents = """/* Generated by lastchange.py, do not edit.*/ + +#ifndef %(header_guard)s +#define %(header_guard)s + +#define %(define)s "%(version)s" + +#endif // %(header_guard)s +""" + header_contents = header_contents % { 'header_guard': header_guard, + 'define': define, + 'version': version } + return header_contents + + +def GetGitTopDirectory(source_dir): + """Get the top git directory - the directory that contains the .git directory. + + Args: + source_dir: The directory to search. + Returns: + The output of "git rev-parse --show-toplevel" as a string + """ + return _RunGitCommand(source_dir, ['rev-parse', '--show-toplevel']) + + +def WriteIfChanged(file_name, contents): + """ + Writes the specified contents to the specified file_name + iff the contents are different than the current contents. + Returns if new data was written. + """ + try: + old_contents = open(file_name, 'r').read() + except EnvironmentError: + pass + else: + if contents == old_contents: + return False + os.unlink(file_name) + open(file_name, 'w').write(contents) + return True + + +def main(argv=None): + if argv is None: + argv = sys.argv + + parser = argparse.ArgumentParser(usage="lastchange.py [options]") + parser.add_argument("-m", "--version-macro", + help=("Name of C #define when using --header. Defaults to " + "LAST_CHANGE.")) + parser.add_argument("-o", "--output", metavar="FILE", + help=("Write last change to FILE. " + "Can be combined with --header to write both files.")) + parser.add_argument("--header", metavar="FILE", + help=("Write last change to FILE as a C/C++ header. " + "Can be combined with --output to write both files.")) + parser.add_argument("--merge-base-ref", + default=None, + help=("Only consider changes since the merge " + "base between HEAD and the provided ref")) + parser.add_argument("--revision-id-only", action='store_true', + help=("Output the revision as a VCS revision ID only (in " + "Git, a 40-character commit hash, excluding the " + "Cr-Commit-Position).")) + parser.add_argument("--revision-id-prefix", + metavar="PREFIX", + help=("Adds a string prefix to the VCS revision ID.")) + parser.add_argument("--print-only", action="store_true", + help=("Just print the revision string. Overrides any " + "file-output-related options.")) + parser.add_argument("-s", "--source-dir", metavar="DIR", + help="Use repository in the given directory.") + parser.add_argument("--filter", metavar="REGEX", + help=("Only use log entries where the commit message " + "matches the supplied filter regex. Defaults to " + "'^Change-Id:' to suppress local commits."), + default='^Change-Id:') + + args, extras = parser.parse_known_args(argv[1:]) + + logging.basicConfig(level=logging.WARNING) + + out_file = args.output + header = args.header + commit_filter=args.filter + + while len(extras) and out_file is None: + if out_file is None: + out_file = extras.pop(0) + if extras: + sys.stderr.write('Unexpected arguments: %r\n\n' % extras) + parser.print_help() + sys.exit(2) + + source_dir = args.source_dir or os.path.dirname(os.path.abspath(__file__)) + try: + git_top_dir = GetGitTopDirectory(source_dir) + except GitError as e: + logging.error("Failed to get git top directory from '%s': %s", + source_dir, e) + return 2 + + if args.merge_base_ref: + try: + merge_base_sha = GetMergeBase(git_top_dir, args.merge_base_ref) + except GitError as e: + logging.error("You requested a --merge-base-ref value of '%s' but no " + "merge base could be found between it and HEAD. Git " + "reports: %s", args.merge_base_ref, e) + return 3 + else: + merge_base_sha = 'HEAD' + + try: + version_info = FetchGitRevision(git_top_dir, commit_filter, merge_base_sha) + except GitError as e: + logging.error("Failed to get version info: %s", e) + logging.info(("Falling back to a version of 0.0.0 to allow script to " + "finish. This is normal if you are bootstrapping a new environment " + "or do not have a git repository for any other reason. If not, this " + "could represent a serious error.")) + version_info = VersionInfo('0', '0', 0) + + revision_string = version_info.revision + if args.revision_id_only: + revision_string = version_info.revision_id + + if args.revision_id_prefix: + revision_string = args.revision_id_prefix + revision_string + + if args.print_only: + print(revision_string) + else: + lastchange_year = datetime.datetime.utcfromtimestamp( + version_info.timestamp).year + contents_lines = [ + "LASTCHANGE=%s" % revision_string, + "LASTCHANGE_YEAR=%s" % lastchange_year, + ] + contents = '\n'.join(contents_lines) + '\n' + if not out_file and not args.header: + sys.stdout.write(contents) + else: + if out_file: + committime_file = out_file + '.committime' + out_changed = WriteIfChanged(out_file, contents) + if out_changed or not os.path.exists(committime_file): + with open(committime_file, 'w') as timefile: + timefile.write(str(version_info.timestamp)) + if header: + WriteIfChanged(header, + GetHeaderContents(header, args.version_macro, + revision_string)) + + return 0 + + +if __name__ == '__main__': + sys.exit(main()) |