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