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