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