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 os 13import shutil 14import subprocess 15import sys 16import tempfile 17from contextlib import contextmanager 18 19import git_llvm_rev 20from subprocess_helpers import CheckCommand 21from subprocess_helpers import check_output 22 23_LLVM_GIT_URL = ('https://chromium.googlesource.com/external/github.com/llvm' 24 '/llvm-project') 25 26KNOWN_HASH_SOURCES = {'google3', 'google3-unstable', 'tot'} 27 28 29def GetVersionFrom(src_dir, git_hash): 30 """Obtain an SVN-style version number based on the LLVM git hash passed in. 31 32 Args: 33 src_dir: LLVM's source directory. 34 git_hash: The git hash. 35 36 Returns: 37 An SVN-style version number associated with the git hash. 38 """ 39 40 version = git_llvm_rev.translate_sha_to_rev( 41 git_llvm_rev.LLVMConfig(remote='origin', dir=src_dir), git_hash) 42 # Note: branches aren't supported 43 assert version.branch == git_llvm_rev.MAIN_BRANCH, version.branch 44 return version.number 45 46 47def GetGitHashFrom(src_dir, version): 48 """Finds the commit hash(es) of the LLVM version in the git log history. 49 50 Args: 51 src_dir: The LLVM source tree. 52 version: The version number. 53 54 Returns: 55 A git hash string corresponding to the version number. 56 57 Raises: 58 subprocess.CalledProcessError: Failed to find a git hash. 59 """ 60 61 return git_llvm_rev.translate_rev_to_sha( 62 git_llvm_rev.LLVMConfig(remote='origin', dir=src_dir), 63 git_llvm_rev.Rev(branch=git_llvm_rev.MAIN_BRANCH, number=version)) 64 65 66@contextmanager 67def CreateTempLLVMRepo(temp_dir): 68 """Adds a LLVM worktree to 'temp_dir'. 69 70 Creating a worktree because the LLVM source tree in 71 '../toolchain-utils/llvm_tools/llvm-project-copy' should not be modified. 72 73 This is useful for applying patches to a source tree but do not want to modify 74 the actual LLVM source tree in 'llvm-project-copy'. 75 76 Args: 77 temp_dir: An absolute path to the temporary directory to put the worktree in 78 (obtained via 'tempfile.mkdtemp()'). 79 80 Returns: 81 The absolute path to 'temp_dir'. 82 83 Raises: 84 subprocess.CalledProcessError: Failed to remove the worktree. 85 ValueError: Failed to add a worktree. 86 """ 87 88 abs_path_to_llvm_project_dir = GetAndUpdateLLVMProjectInLLVMTools() 89 CheckCommand([ 90 'git', '-C', abs_path_to_llvm_project_dir, 'worktree', 'add', '--detach', 91 temp_dir, git_llvm_rev.MAIN_BRANCH 92 ]) 93 94 try: 95 yield temp_dir 96 finally: 97 if os.path.isdir(temp_dir): 98 check_output([ 99 'git', '-C', abs_path_to_llvm_project_dir, 'worktree', 'remove', '-f', 100 temp_dir 101 ]) 102 103 104def GetAndUpdateLLVMProjectInLLVMTools(): 105 """Gets the absolute path to 'llvm-project-copy' directory in 'llvm_tools'. 106 107 The intent of this function is to avoid cloning the LLVM repo and then 108 discarding the contents of the repo. The function will create a directory 109 in '../toolchain-utils/llvm_tools' called 'llvm-project-copy' if this 110 directory does not exist yet. If it does not exist, then it will use the 111 LLVMHash() class to clone the LLVM repo into 'llvm-project-copy'. Otherwise, 112 it will clean the contents of that directory and then fetch from the chromium 113 LLVM mirror. In either case, this function will return the absolute path to 114 'llvm-project-copy' directory. 115 116 Raises: 117 ValueError: LLVM repo (in 'llvm-project-copy' dir.) has changes or failed to 118 checkout to main or failed to fetch from chromium mirror of LLVM. 119 """ 120 121 abs_path_to_llvm_tools_dir = os.path.dirname(os.path.abspath(__file__)) 122 123 abs_path_to_llvm_project_dir = os.path.join(abs_path_to_llvm_tools_dir, 124 'llvm-project-copy') 125 126 if not os.path.isdir(abs_path_to_llvm_project_dir): 127 print( 128 'Checking out LLVM from scratch. This could take a while...\n' 129 '(This should only need to be done once, though.)', 130 file=sys.stderr) 131 os.mkdir(abs_path_to_llvm_project_dir) 132 133 LLVMHash().CloneLLVMRepo(abs_path_to_llvm_project_dir) 134 else: 135 # `git status` has a '-s'/'--short' option that shortens the output. 136 # With the '-s' option, if no changes were made to the LLVM repo, then the 137 # output (assigned to 'repo_status') would be empty. 138 repo_status = check_output( 139 ['git', '-C', abs_path_to_llvm_project_dir, 'status', '-s']) 140 141 if repo_status.rstrip(): 142 raise ValueError('LLVM repo in %s has changes, please remove.' % 143 abs_path_to_llvm_project_dir) 144 145 CheckCommand([ 146 'git', '-C', abs_path_to_llvm_project_dir, 'checkout', 147 git_llvm_rev.MAIN_BRANCH 148 ]) 149 CheckCommand(['git', '-C', abs_path_to_llvm_project_dir, 'pull']) 150 151 return abs_path_to_llvm_project_dir 152 153 154def GetGoogle3LLVMVersion(stable): 155 """Gets the latest google3 LLVM version. 156 157 Returns: 158 The latest LLVM SVN version as an integer. 159 160 Raises: 161 subprocess.CalledProcessError: An invalid path has been provided to the 162 `cat` command. 163 """ 164 165 subdir = 'stable' if stable else 'llvm_unstable' 166 167 # Cmd to get latest google3 LLVM version. 168 cmd = [ 169 'cat', 170 os.path.join('/google/src/head/depot/google3/third_party/crosstool/v18', 171 subdir, 'installs/llvm/git_origin_rev_id') 172 ] 173 174 # Get latest version. 175 git_hash = check_output(cmd) 176 177 # Change type to an integer 178 return GetVersionFrom(GetAndUpdateLLVMProjectInLLVMTools(), git_hash.rstrip()) 179 180 181def is_svn_option(svn_option): 182 """Validates whether the argument (string) is a git hash option. 183 184 The argument is used to find the git hash of LLVM. 185 186 Args: 187 svn_option: The option passed in as a command line argument. 188 189 Raises: 190 ValueError: Invalid svn option provided. 191 """ 192 193 if svn_option.lower() in KNOWN_HASH_SOURCES: 194 return svn_option.lower() 195 196 try: 197 svn_version = int(svn_option) 198 199 return svn_version 200 201 # Unable to convert argument to an int, so the option is invalid. 202 # 203 # Ex: 'one'. 204 except ValueError: 205 pass 206 207 raise ValueError('Invalid LLVM git hash option provided: %s' % svn_option) 208 209 210def GetLLVMHashAndVersionFromSVNOption(svn_option): 211 """Gets the LLVM hash and LLVM version based off of the svn option. 212 213 Args: 214 svn_option: A valid svn option obtained from the command line. 215 Ex: 'google3', 'tot', or <svn_version> such as 365123. 216 217 Returns: 218 A tuple that is the LLVM git hash and LLVM version. 219 """ 220 221 new_llvm_hash = LLVMHash() 222 223 # Determine which LLVM git hash to retrieve. 224 if svn_option == 'tot': 225 git_hash = new_llvm_hash.GetTopOfTrunkGitHash() 226 version = GetVersionFrom(GetAndUpdateLLVMProjectInLLVMTools(), git_hash) 227 elif isinstance(svn_option, int): 228 version = svn_option 229 git_hash = GetGitHashFrom(GetAndUpdateLLVMProjectInLLVMTools(), version) 230 else: 231 assert svn_option in ('google3', 'google3-unstable') 232 version = GetGoogle3LLVMVersion(stable=svn_option == 'google3') 233 234 git_hash = GetGitHashFrom(GetAndUpdateLLVMProjectInLLVMTools(), version) 235 236 return git_hash, version 237 238 239class LLVMHash(object): 240 """Provides methods to retrieve a LLVM hash.""" 241 242 @staticmethod 243 @contextmanager 244 def CreateTempDirectory(): 245 temp_dir = tempfile.mkdtemp() 246 247 try: 248 yield temp_dir 249 finally: 250 if os.path.isdir(temp_dir): 251 shutil.rmtree(temp_dir, ignore_errors=True) 252 253 def CloneLLVMRepo(self, temp_dir): 254 """Clones the LLVM repo. 255 256 Args: 257 temp_dir: The temporary directory to clone the repo to. 258 259 Raises: 260 ValueError: Failed to clone the LLVM repo. 261 """ 262 263 clone_cmd = ['git', 'clone', _LLVM_GIT_URL, temp_dir] 264 265 clone_cmd_obj = subprocess.Popen(clone_cmd, stderr=subprocess.PIPE) 266 _, stderr = clone_cmd_obj.communicate() 267 268 if clone_cmd_obj.returncode: 269 raise ValueError('Failed to clone the LLVM repo: %s' % stderr) 270 271 def GetLLVMHash(self, version): 272 """Retrieves the LLVM hash corresponding to the LLVM version passed in. 273 274 Args: 275 version: The LLVM version to use as a delimiter. 276 277 Returns: 278 The hash as a string that corresponds to the LLVM version. 279 """ 280 281 hash_value = GetGitHashFrom(GetAndUpdateLLVMProjectInLLVMTools(), version) 282 return hash_value 283 284 def GetGoogle3LLVMHash(self): 285 """Retrieves the google3 LLVM hash.""" 286 287 return self.GetLLVMHash(GetGoogle3LLVMVersion(stable=True)) 288 289 def GetGoogle3UnstableLLVMHash(self): 290 """Retrieves the LLVM hash of google3's unstable compiler.""" 291 return self.GetLLVMHash(GetGoogle3LLVMVersion(stable=False)) 292 293 def GetTopOfTrunkGitHash(self): 294 """Gets the latest git hash from top of trunk of LLVM.""" 295 296 path_to_main_branch = 'refs/heads/main' 297 llvm_tot_git_hash = check_output( 298 ['git', 'ls-remote', _LLVM_GIT_URL, path_to_main_branch]) 299 return llvm_tot_git_hash.rstrip().split()[0] 300 301 302def main(): 303 """Prints the git hash of LLVM. 304 305 Parses the command line for the optional command line 306 arguments. 307 """ 308 309 # Create parser and add optional command-line arguments. 310 parser = argparse.ArgumentParser(description='Finds the LLVM hash.') 311 parser.add_argument( 312 '--llvm_version', 313 type=is_svn_option, 314 required=True, 315 help='which git hash of LLVM to find. Either a svn revision, or one ' 316 'of %s' % sorted(KNOWN_HASH_SOURCES)) 317 318 # Parse command-line arguments. 319 args_output = parser.parse_args() 320 321 cur_llvm_version = args_output.llvm_version 322 323 new_llvm_hash = LLVMHash() 324 325 if isinstance(cur_llvm_version, int): 326 # Find the git hash of the specific LLVM version. 327 print(new_llvm_hash.GetLLVMHash(cur_llvm_version)) 328 elif cur_llvm_version == 'google3': 329 print(new_llvm_hash.GetGoogle3LLVMHash()) 330 elif cur_llvm_version == 'google3-unstable': 331 print(new_llvm_hash.GetGoogle3UnstableLLVMHash()) 332 else: 333 assert cur_llvm_version == 'tot' 334 print(new_llvm_hash.GetTopOfTrunkGitHash()) 335 336 337if __name__ == '__main__': 338 main() 339