blob: 002f38f309d873fcfd84ead84595c0fdbead15e8 [file] [log] [blame]
#!/usr/bin/env python3
"""Usage: release-info.py <repo> <revision>
Examples:
Print info about an emscripten-releases revision:
release-info.py emscripten-releases 30825b84
The repo also defaults to emscripten-releases, e.g.:
release-info.py 30825b8
This will print the full hash, timestamp, and tool revisions for the
specified revision
Print info about an llvm-project, binaryen, or emscripten revision:
release-info.py llvm-project <hash>
This will print the first emscripten-releases revision (and tag revision)
that contains the specified tool revision
As a special case, you can print info about a tag version
Examples: release-info.py tag 2.0.16
Or when you use a tag version you can leave out the mode:
release-info.py 2.0.16
"""
# TODO: Support another way to find the checkout directories (i.e. remove the
# current requirement that they be in subdirectories of the cwd)
# TODO: Support automatically fetching the git remotes of the subdirs
import argparse
import json
import os
import subprocess
import sys
import urllib.request
TAG_INFO_URL = 'https://raw.githubusercontent.com/emscripten-core/emsdk/main/emscripten-releases-tags.json'
EMR_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
MAIN_BRANCH = 'origin/main'
def Git(*args, **kwargs):
# print('Running: git ' + ' '.join(args))
return subprocess.check_output(['git'] + list(args), **kwargs).decode()
def RevisionDate(rev, cwd):
if os.path.isdir(cwd):
return Git('log', '-n1', '--pretty=format:%cd', rev, cwd=cwd)
return '(none)'
def IsAncestor(rev1, rev2, cwd):
"""Return True if rev1 is an ancestor of rev2"""
# In other words, the history from rev2 includes rev1
try:
Git('merge-base', '--is-ancestor', rev1, rev2, cwd=cwd)
return True
except subprocess.CalledProcessError as e:
# Exit status is 1 if rev1 is not an ancestor, something else otherwise
if e.returncode == 1:
return False
raise
def TagEmrInfo():
"""Return tag revision info from EMSDK
Map tag version to emscripten-releases revision.
Example: { '2.0.16': <hash>, '2.0.15': <hash> }
"""
info_stream = urllib.request.urlopen(TAG_INFO_URL)
info = json.load(info_stream)
# EMSDK's data format includes the latest release, which we don't care
# about here, e.g.: { 'latest': '2.0.16', 'releases': {...} }
return info['releases']
def ParseDeps(deps_str):
"""Parse a DEPS file and return the relevant revisions.
Structure is { 'emscripten': hash, 'binaryen': hash, 'llvm-project': hash }
"""
# DEPS files are basically python, with a bit of extra environment required
# that we can fake.
def Var(x):
return ''
Globals = {'Var': Var}
exec(deps_str, Globals)
Vars = Globals['vars']
revisions = {
'emscripten': Vars['emscripten_revision'],
'binaryen': Vars['binaryen_revision'],
'llvm-project': Vars['llvm_project_revision']
}
return revisions
def GetDeps(emr_rev):
"""Return the relevant DEPS info for an emscripten-releases revision"""
deps_str = Git('show', emr_rev + ':DEPS', cwd=EMR_DIR)
return ParseDeps(deps_str)
def PrintEmrToolInfo(emr_rev):
"""Print the date and deps info for an escripten-releases revision"""
deps = GetDeps(emr_rev)
emr_rev = Git('rev-parse', emr_rev).strip()
emr_date = RevisionDate(emr_rev, EMR_DIR)
print(f'emscripten-releases revision: {emr_rev} ({emr_date})\n')
for tool, rev in deps.items():
date = RevisionDate(rev, os.path.join(EMR_DIR, tool))
print(f'{tool:19} revision: {rev} ({date})')
return emr_date, deps
def PrintTagFullInfo(tag):
taginfo = TagEmrInfo()
emr_rev = taginfo[tag]
print(f'Revision info for {tag}:')
return emr_rev, PrintEmrToolInfo(emr_rev)
def TagSortKey(tag):
# Strip suffix like "-asserts"
suffix = tag.find('-')
if suffix != -1:
tag = tag[:suffix]
c = tag.split('.')
return int(c[0]) * 10000 + int(c[1]) * 100 + int(c[2])
def IsTagVersion(revision):
try:
TagSortKey(revision)
return True
except Exception:
return False
def EmrTagInfo(emr_rev):
"""Find earliest tag that includes emr_rev"""
taginfo = TagEmrInfo()
first_tag = None
first_emr_rev = None
# Invoking git merge-base for every tag ever is slow, and most of the
# revisions we care about are recent. So iterate from the newest first.
for r in sorted(taginfo.keys(), key=TagSortKey, reverse=True):
tag_emr_rev = taginfo[r]
if not IsAncestor(emr_rev, tag_emr_rev, cwd=None):
# r is the newest tag that does not contain emr_rev
break
first_tag = r
first_emr_rev = tag_emr_rev
if first_tag is None:
print(f'No tag version contains {emr_rev}')
else:
print(f'{first_tag} is the first tag that contains {emr_rev}')
return first_tag, first_emr_rev
def ToolTagInfo(tool, tool_rev):
"""Find the earliest tag that contains tool_rev"""
tool_dir = os.path.join(EMR_DIR, tool)
tool_rev = Git('rev-parse', tool_rev, cwd=tool_dir).strip()
taginfo = TagEmrInfo()
first_tag = None
first_emr_rev = None
first_tool_rev = None
for r in sorted(taginfo.keys(), key=TagSortKey, reverse=True):
tag_emr_rev = taginfo[r]
deps = GetDeps(tag_emr_rev)
tag_tool_rev = deps[tool]
if not IsAncestor(tool_rev, tag_tool_rev, cwd=tool_dir):
# r is the newest tag that does not contain tool_rev
break
first_tag = r
first_emr_rev = tag_emr_rev
first_tool_rev = tag_tool_rev
if first_tag is None:
print(f'No tag version contains {tool} rev {tool_rev}')
else:
print(f'{first_tag} (emscripten-releases rev {first_emr_rev}) is the '
f'first tag that contains {tool} rev {tool_rev}')
print(f'(It includes {tool} rev up to {first_tool_rev})')
return first_tag, first_emr_rev, first_tool_rev
def FindEmrRevContaining(tool, tool_rev):
"""Find the earliest emscripten-releases rev that contains tool_rev"""
tool_dir = os.path.join(EMR_DIR, tool)
tool_rev = Git('rev-parse', tool_rev, cwd=tool_dir).strip()
first_emr_rev = None
first_tool_rev = None
dep_name = tool.replace('-', '_') + '_revision'
emr_commits = Git('log', MAIN_BRANCH, '--pretty=format:%H', '-G', dep_name
).strip().split('\n')
for r in emr_commits:
emr_rev = r.split()[0]
deps = GetDeps(emr_rev)
emr_tool_rev = deps[tool]
if not IsAncestor(tool_rev, emr_tool_rev, cwd=tool_dir):
# emr_rev is the newest rev that does not contain tool_rev
break
first_emr_rev = emr_rev
first_tool_rev = emr_tool_rev
return first_emr_rev, first_tool_rev
def FindEmrRevExact(tool, tool_rev):
"""Find an emscripten-releases rev that updates tool to tool_rev, if any"""
# This method is much faster but only finds commits that update DEPS to
# exactly the desired rev. So it only works if every commit is rolled by
# itself (i.e. not LLVM, and sometimes not other tools) .
tool_dir = os.path.join(EMR_DIR, tool)
tool_rev = Git('rev-parse', tool_rev, cwd=tool_dir).strip()
# Rather than manually searching for the rev that rolled our revision, let
# git do it for us. We expect to find 0-2 revs
deps_revs = Git('log', MAIN_BRANCH, '--format=oneline', '-G', tool_rev
).strip().split('\n')
assert len(deps_revs) <= 2
if len(deps_revs) == 0:
return None, None
assert 'to ' + tool_rev[:12] in deps_revs[-1]
emr_rev = deps_revs[-1].split()[0]
return emr_rev, emr_rev
def PrintEmrRevContaining(tool, tool_rev):
# Try the fast way first
emr_rev, first_tool_rev = FindEmrRevExact(tool, tool_rev)
if emr_rev is None:
emr_rev, first_tool_rev = FindEmrRevContaining(tool, tool_rev)
if emr_rev is None:
print(f'No emscripten-releases rev contains {tool} rev {tool_rev}')
else:
print(f'emscripten-releases rev {emr_rev} is the first that contains '
f'{tool} rev {tool_rev}')
print(f'(It includes {tool} rev up to {first_tool_rev})')
def test():
# This is a bad unit tests because it works on real data instead of mock
# data. So it can't ensure we capture cases like searching for a revision
# that is not yet in a release.
date, deps = PrintEmrToolInfo('30825b84')
assert date == 'Wed Mar 31 15:19:52 2021'
assert deps['emscripten'] == '0aef720ece71c9950bd388f226c21a2f9be75d04'
emr_rev, (date, deps) = PrintTagFullInfo('2.0.15')
assert emr_rev == '89202930a98fe7f9ed59b574469a9471b0bda7dd'
assert deps['llvm-project'] == '1c5f08312874717caf5d94729d825c32845773ec'
# This is ugly but if we choose a single hash it will eventually end up in a
# revision and this test will fail. It will also fail if HEAD just happens
# to be a tagged revision
tag, emr_rev = EmrTagInfo('HEAD')
assert tag is None and emr_rev is None
tag, emr_rev = EmrTagInfo('a05ef8f')
assert tag == '2.0.16'
assert emr_rev == '80d9674f2fafa6b9346d735c42d5c52b8cc8aa8e'
tag, emr_rev = EmrTagInfo('d1451d3')
assert tag == '2.0.14'
assert emr_rev == 'fc5562126762ab26c4757147a3b4c24e85a7289e'
tag, emr_rev, tool_rev = ToolTagInfo('llvm-project', 'HEAD')
assert tag == emr_rev == tool_rev is None
tag, emr_rev, tool_rev = ToolTagInfo('llvm-project', 'ad8010e5')
assert tag == '2.0.16'
assert emr_rev == '80d9674f2fafa6b9346d735c42d5c52b8cc8aa8e'
assert tool_rev == 'ad8010e598d9aa3747c34ce28aa2ba6de1650bd4'
tag, emr_rev, tool_rev = ToolTagInfo('binaryen', '4423bcce31d')
assert tag == '2.0.11'
assert emr_rev == '4764c5c323a474f7ba28ae991b0c9024fccca43c'
assert tool_rev == 'a8ded16f56afd880a9a6459fe5ce55a8667d9b3e'
emr_rev, _ = FindEmrRevExact('binaryen', '4423bcce31')
assert emr_rev == '13afe1e659088eb091b2a4d22eba6a1968e1bd9c'
emr_rev, tool_rev = FindEmrRevContaining('llvm-project', 'ae7b1e8823a5')
assert emr_rev == 'b1db25970643e56641a27fd45c282d405cee355b'
assert tool_rev == '4ced958dc205de0de935e6d2f27767ebcec6c29f'
emr_rev, tool_rev = FindEmrRevContaining('binaryen', '4423bcce31')
assert emr_rev == '13afe1e659088eb091b2a4d22eba6a1968e1bd9c'
if __name__ == '__main__':
parser = argparse.ArgumentParser(
description='Info about emscripten-releases and tool revisions')
parser.add_argument('mode', nargs='?', choices=[
'tag', 'emscripten-releases', 'llvm-project', 'binaryen',
'emscripten', 'test'], help='Type of revision to print info about')
parser.add_argument('revision', help='Tag version or git revision')
args = parser.parse_args()
if args.revision == 'test':
sys.exit(test())
if IsTagVersion(args.revision):
if args.mode and args.mode != 'tag':
print("Tag revisions such as 2.0.16 can't be used as %s revisions "
% args.mode)
sys.exit(1)
PrintTagFullInfo(args.revision)
elif not args.mode or args.mode == 'emscripten-releases':
PrintEmrToolInfo(args.revision)
elif args.mode in ('llvm-project', 'binaryen', 'emscripten'):
tag, emr_rev, tool_rev = ToolTagInfo(args.mode, args.revision)
if not tag:
PrintEmrRevContaining(args.mode, args.revision)
if tag:
PrintTagFullInfo(tag)
elif emr_rev:
PrintEmrToolInfo(emr_rev)