• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# Copyright 2019 The Chromium OS Authors. All rights reserved.
4# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6
7"""Returns the latest LLVM version's hash."""
8
9from __future__ import print_function
10
11import argparse
12import contextlib
13import functools
14import os
15import re
16import shutil
17import subprocess
18import sys
19import tempfile
20
21import git_llvm_rev
22from subprocess_helpers import check_output
23from subprocess_helpers import CheckCommand
24
25_LLVM_GIT_URL = ('https://chromium.googlesource.com/external/github.com/llvm'
26                 '/llvm-project')
27
28KNOWN_HASH_SOURCES = {'google3', 'google3-unstable', 'tot'}
29
30
31def GetVersionFrom(src_dir, git_hash):
32  """Obtain an SVN-style version number based on the LLVM git hash passed in.
33
34  Args:
35    src_dir: LLVM's source directory.
36    git_hash: The git hash.
37
38  Returns:
39    An SVN-style version number associated with the git hash.
40  """
41
42  version = git_llvm_rev.translate_sha_to_rev(
43      git_llvm_rev.LLVMConfig(remote='origin', dir=src_dir), git_hash)
44  # Note: branches aren't supported
45  assert version.branch == git_llvm_rev.MAIN_BRANCH, version.branch
46  return version.number
47
48
49def GetGitHashFrom(src_dir, version):
50  """Finds the commit hash(es) of the LLVM version in the git log history.
51
52  Args:
53    src_dir: The LLVM source tree.
54    version: The version number.
55
56  Returns:
57    A git hash string corresponding to the version number.
58
59  Raises:
60    subprocess.CalledProcessError: Failed to find a git hash.
61  """
62
63  return git_llvm_rev.translate_rev_to_sha(
64      git_llvm_rev.LLVMConfig(remote='origin', dir=src_dir),
65      git_llvm_rev.Rev(branch=git_llvm_rev.MAIN_BRANCH, number=version))
66
67
68def CheckoutBranch(src_dir, branch):
69  """Checks out and pulls from a branch in a git repo.
70
71  Args:
72    src_dir: The LLVM source tree.
73    branch: The git branch to checkout in src_dir.
74
75  Raises:
76    ValueError: Failed to checkout or pull branch version
77  """
78  CheckCommand(['git', '-C', src_dir, 'checkout', branch])
79  CheckCommand(['git', '-C', src_dir, 'pull'])
80
81
82def ParseLLVMMajorVersion(cmakelist):
83  """Reads CMakeList.txt file contents for LLVMMajor Version.
84
85  Args:
86    cmakelist: contents of CMakeList.txt
87
88  Returns:
89    The major version number as a string
90
91  Raises:
92    ValueError: The major version cannot be parsed from cmakelist
93  """
94  match = re.search(r'\n\s+set\(LLVM_VERSION_MAJOR (?P<major>\d+)\)', cmakelist)
95  if not match:
96    raise ValueError('Failed to parse CMakeList for llvm major version')
97  return match.group('major')
98
99
100@functools.lru_cache(maxsize=1)
101def GetLLVMMajorVersion(git_hash=None):
102  """Reads llvm/CMakeList.txt file contents for LLVMMajor Version.
103
104  Args:
105    git_hash: git hash of llvm version as string or None for top of trunk
106
107  Returns:
108    The major version number as a string
109
110  Raises:
111    ValueError: The major version cannot be parsed from cmakelist or
112      there was a failure to checkout git_hash version
113    FileExistsError: The src directory doe not contain CMakeList.txt
114  """
115  src_dir = GetAndUpdateLLVMProjectInLLVMTools()
116  cmakelists_path = os.path.join(src_dir, 'llvm', 'CMakeLists.txt')
117  if git_hash:
118    CheckCommand(['git', '-C', src_dir, 'checkout', git_hash])
119  try:
120    with open(cmakelists_path) as cmakelists_file:
121      return ParseLLVMMajorVersion(cmakelists_file.read())
122  finally:
123    if git_hash:
124      CheckoutBranch(src_dir, git_llvm_rev.MAIN_BRANCH)
125
126
127@contextlib.contextmanager
128def CreateTempLLVMRepo(temp_dir):
129  """Adds a LLVM worktree to 'temp_dir'.
130
131  Creating a worktree because the LLVM source tree in
132  '../toolchain-utils/llvm_tools/llvm-project-copy' should not be modified.
133
134  This is useful for applying patches to a source tree but do not want to modify
135  the actual LLVM source tree in 'llvm-project-copy'.
136
137  Args:
138    temp_dir: An absolute path to the temporary directory to put the worktree in
139    (obtained via 'tempfile.mkdtemp()').
140
141  Yields:
142    The absolute path to 'temp_dir'.
143
144  Raises:
145    subprocess.CalledProcessError: Failed to remove the worktree.
146    ValueError: Failed to add a worktree.
147  """
148
149  abs_path_to_llvm_project_dir = GetAndUpdateLLVMProjectInLLVMTools()
150  CheckCommand([
151      'git', '-C', abs_path_to_llvm_project_dir, 'worktree', 'add', '--detach',
152      temp_dir,
153      'origin/%s' % git_llvm_rev.MAIN_BRANCH
154  ])
155
156  try:
157    yield temp_dir
158  finally:
159    if os.path.isdir(temp_dir):
160      check_output([
161          'git', '-C', abs_path_to_llvm_project_dir, 'worktree', 'remove', '-f',
162          temp_dir
163      ])
164
165
166def GetAndUpdateLLVMProjectInLLVMTools():
167  """Gets the absolute path to 'llvm-project-copy' directory in 'llvm_tools'.
168
169  The intent of this function is to avoid cloning the LLVM repo and then
170  discarding the contents of the repo. The function will create a directory
171  in '../toolchain-utils/llvm_tools' called 'llvm-project-copy' if this
172  directory does not exist yet. If it does not exist, then it will use the
173  LLVMHash() class to clone the LLVM repo into 'llvm-project-copy'. Otherwise,
174  it will clean the contents of that directory and then fetch from the chromium
175  LLVM mirror. In either case, this function will return the absolute path to
176  'llvm-project-copy' directory.
177
178  Returns:
179    Absolute path to 'llvm-project-copy' directory in 'llvm_tools'
180
181  Raises:
182    ValueError: LLVM repo (in 'llvm-project-copy' dir.) has changes or failed to
183    checkout to main or failed to fetch from chromium mirror of LLVM.
184  """
185
186  abs_path_to_llvm_tools_dir = os.path.dirname(os.path.abspath(__file__))
187
188  abs_path_to_llvm_project_dir = os.path.join(abs_path_to_llvm_tools_dir,
189                                              'llvm-project-copy')
190
191  if not os.path.isdir(abs_path_to_llvm_project_dir):
192    print(
193        (f'Checking out LLVM to {abs_path_to_llvm_project_dir}\n'
194         'so that we can map between commit hashes and revision numbers.\n'
195         'This may take a while, but only has to be done once.'),
196        file=sys.stderr)
197    os.mkdir(abs_path_to_llvm_project_dir)
198
199    LLVMHash().CloneLLVMRepo(abs_path_to_llvm_project_dir)
200  else:
201    # `git status` has a '-s'/'--short' option that shortens the output.
202    # With the '-s' option, if no changes were made to the LLVM repo, then the
203    # output (assigned to 'repo_status') would be empty.
204    repo_status = check_output(
205        ['git', '-C', abs_path_to_llvm_project_dir, 'status', '-s'])
206
207    if repo_status.rstrip():
208      raise ValueError('LLVM repo in %s has changes, please remove.' %
209                       abs_path_to_llvm_project_dir)
210
211    CheckoutBranch(abs_path_to_llvm_project_dir, git_llvm_rev.MAIN_BRANCH)
212
213  return abs_path_to_llvm_project_dir
214
215
216def GetGoogle3LLVMVersion(stable):
217  """Gets the latest google3 LLVM version.
218
219  Args:
220    stable: boolean, use the stable version or the unstable version
221
222  Returns:
223    The latest LLVM SVN version as an integer.
224
225  Raises:
226    subprocess.CalledProcessError: An invalid path has been provided to the
227    `cat` command.
228  """
229
230  subdir = 'stable' if stable else 'llvm_unstable'
231
232  # Cmd to get latest google3 LLVM version.
233  cmd = [
234      'cat',
235      os.path.join('/google/src/head/depot/google3/third_party/crosstool/v18',
236                   subdir, 'installs/llvm/git_origin_rev_id')
237  ]
238
239  # Get latest version.
240  git_hash = check_output(cmd)
241
242  # Change type to an integer
243  return GetVersionFrom(GetAndUpdateLLVMProjectInLLVMTools(), git_hash.rstrip())
244
245
246def IsSvnOption(svn_option):
247  """Validates whether the argument (string) is a git hash option.
248
249  The argument is used to find the git hash of LLVM.
250
251  Args:
252    svn_option: The option passed in as a command line argument.
253
254  Returns:
255    lowercase svn_option if it is a known hash source, otherwise the svn_option
256    as an int
257
258  Raises:
259    ValueError: Invalid svn option provided.
260  """
261
262  if svn_option.lower() in KNOWN_HASH_SOURCES:
263    return svn_option.lower()
264
265  try:
266    svn_version = int(svn_option)
267
268    return svn_version
269
270  # Unable to convert argument to an int, so the option is invalid.
271  #
272  # Ex: 'one'.
273  except ValueError:
274    pass
275
276  raise ValueError('Invalid LLVM git hash option provided: %s' % svn_option)
277
278
279def GetLLVMHashAndVersionFromSVNOption(svn_option):
280  """Gets the LLVM hash and LLVM version based off of the svn option.
281
282  Args:
283    svn_option: A valid svn option obtained from the command line.
284      Ex. 'google3', 'tot', or <svn_version> such as 365123.
285
286  Returns:
287    A tuple that is the LLVM git hash and LLVM version.
288  """
289
290  new_llvm_hash = LLVMHash()
291
292  # Determine which LLVM git hash to retrieve.
293  if svn_option == 'tot':
294    git_hash = new_llvm_hash.GetTopOfTrunkGitHash()
295    version = GetVersionFrom(GetAndUpdateLLVMProjectInLLVMTools(), git_hash)
296  elif isinstance(svn_option, int):
297    version = svn_option
298    git_hash = GetGitHashFrom(GetAndUpdateLLVMProjectInLLVMTools(), version)
299  else:
300    assert svn_option in ('google3', 'google3-unstable')
301    version = GetGoogle3LLVMVersion(stable=svn_option == 'google3')
302
303    git_hash = GetGitHashFrom(GetAndUpdateLLVMProjectInLLVMTools(), version)
304
305  return git_hash, version
306
307
308class LLVMHash(object):
309  """Provides methods to retrieve a LLVM hash."""
310
311  @staticmethod
312  @contextlib.contextmanager
313  def CreateTempDirectory():
314    temp_dir = tempfile.mkdtemp()
315
316    try:
317      yield temp_dir
318    finally:
319      if os.path.isdir(temp_dir):
320        shutil.rmtree(temp_dir, ignore_errors=True)
321
322  def CloneLLVMRepo(self, temp_dir):
323    """Clones the LLVM repo.
324
325    Args:
326      temp_dir: The temporary directory to clone the repo to.
327
328    Raises:
329      ValueError: Failed to clone the LLVM repo.
330    """
331
332    clone_cmd = ['git', 'clone', _LLVM_GIT_URL, temp_dir]
333
334    clone_cmd_obj = subprocess.Popen(clone_cmd, stderr=subprocess.PIPE)
335    _, stderr = clone_cmd_obj.communicate()
336
337    if clone_cmd_obj.returncode:
338      raise ValueError('Failed to clone the LLVM repo: %s' % stderr)
339
340  def GetLLVMHash(self, version):
341    """Retrieves the LLVM hash corresponding to the LLVM version passed in.
342
343    Args:
344      version: The LLVM version to use as a delimiter.
345
346    Returns:
347      The hash as a string that corresponds to the LLVM version.
348    """
349
350    hash_value = GetGitHashFrom(GetAndUpdateLLVMProjectInLLVMTools(), version)
351    return hash_value
352
353  def GetGoogle3LLVMHash(self):
354    """Retrieves the google3 LLVM hash."""
355
356    return self.GetLLVMHash(GetGoogle3LLVMVersion(stable=True))
357
358  def GetGoogle3UnstableLLVMHash(self):
359    """Retrieves the LLVM hash of google3's unstable compiler."""
360    return self.GetLLVMHash(GetGoogle3LLVMVersion(stable=False))
361
362  def GetTopOfTrunkGitHash(self):
363    """Gets the latest git hash from top of trunk of LLVM."""
364
365    path_to_main_branch = 'refs/heads/main'
366    llvm_tot_git_hash = check_output(
367        ['git', 'ls-remote', _LLVM_GIT_URL, path_to_main_branch])
368    return llvm_tot_git_hash.rstrip().split()[0]
369
370
371def main():
372  """Prints the git hash of LLVM.
373
374  Parses the command line for the optional command line
375  arguments.
376  """
377
378  # Create parser and add optional command-line arguments.
379  parser = argparse.ArgumentParser(description='Finds the LLVM hash.')
380  parser.add_argument(
381      '--llvm_version',
382      type=IsSvnOption,
383      required=True,
384      help='which git hash of LLVM to find. Either a svn revision, or one '
385      'of %s' % sorted(KNOWN_HASH_SOURCES))
386
387  # Parse command-line arguments.
388  args_output = parser.parse_args()
389
390  cur_llvm_version = args_output.llvm_version
391
392  new_llvm_hash = LLVMHash()
393
394  if isinstance(cur_llvm_version, int):
395    # Find the git hash of the specific LLVM version.
396    print(new_llvm_hash.GetLLVMHash(cur_llvm_version))
397  elif cur_llvm_version == 'google3':
398    print(new_llvm_hash.GetGoogle3LLVMHash())
399  elif cur_llvm_version == 'google3-unstable':
400    print(new_llvm_hash.GetGoogle3UnstableLLVMHash())
401  else:
402    assert cur_llvm_version == 'tot'
403    print(new_llvm_hash.GetTopOfTrunkGitHash())
404
405
406if __name__ == '__main__':
407  main()
408