55The module that performs the installation of clang-tools.
66"""
77import os
8+ from pathlib import Path , PurePath
9+ import re
810import shutil
11+ import subprocess
912import sys
10- from pathlib import Path , PurePath
13+ from typing import Optional , Union
1114
1215from . import install_os , RESET_COLOR , suffix , YELLOW
1316from .util import download_file , verify_sha512 , get_sha_checksum
1417
1518
19+ #: This pattern is designed to match only the major version number.
20+ RE_PARSE_VERSION = re .compile (rb"version\s([\d\.]+)" , re .MULTILINE )
21+
22+
23+ def is_installed (tool_name : str , version : str ) -> Optional [Path ]:
24+ """Detect if the specified tool is installed.
25+
26+ :param tool_name: The name of the specified tool.
27+ :param version: The specific version to expect.
28+
29+ :returns: The path to the detected tool (if found), otherwise `None`.
30+ """
31+ version_tuple = version .split ("." )
32+ ver_major = version_tuple [0 ]
33+ if len (version_tuple ) < 3 :
34+ # append minor and patch version numbers if not specified
35+ version_tuple += (0 ,) * (3 - len (version_tuple ))
36+ exe_name = (
37+ f"{ tool_name } " + (f"-{ ver_major } " if install_os != "windows" else "" ) + suffix
38+ )
39+ try :
40+ result = subprocess .run (
41+ [exe_name , "--version" ], capture_output = True , check = True
42+ )
43+ except (FileNotFoundError , subprocess .CalledProcessError ):
44+ return None # tool is not installed
45+ ver_num = RE_PARSE_VERSION .search (result .stdout )
46+ print (
47+ f"Found a installed version of { tool_name } :" ,
48+ ver_num .groups (0 )[0 ].decode (encoding = "utf-8" ),
49+ end = " "
50+ )
51+ path = shutil .which (exe_name ) # find the installed binary
52+ if path is None :
53+ print () # print end-of-line
54+ return None # failed to locate the binary
55+ path = Path (path ).resolve ()
56+ print ("at" , str (path ))
57+ if (
58+ ver_num is None
59+ or ver_num .groups (0 )[0 ].decode (encoding = "utf-8" ).split ("." ) != version_tuple
60+ ):
61+ return None # version is unknown or not the desired major release
62+ return path
63+
64+
1665def clang_tools_binary_url (
1766 tool : str , version : str , release_tag : str = "master-208096c1"
1867) -> str :
@@ -107,7 +156,11 @@ def move_and_chmod_bin(old_bin_name: str, new_bin_name: str, install_dir: str) -
107156
108157
109158def create_sym_link (
110- tool_name : str , version : str , install_dir : str , overwrite : bool = False
159+ tool_name : str ,
160+ version : str ,
161+ install_dir : str ,
162+ overwrite : bool = False ,
163+ target : Path = None ,
111164) -> bool :
112165 """Create a symlink to the installed binary that
113166 doesn't have the version number appended.
@@ -116,11 +169,19 @@ def create_sym_link(
116169 :param version: The version of the clang-tool to symlink.
117170 :param install_dir: The installation directory to create the symlink in.
118171 :param overwrite: A flag to indicate if an existing symlink should be overwritten.
172+ :param target: The target executable's path and name for which to create a symlink
173+ to. If this argument is not specified (or is `None`), then the target's path and
174+ name is constructed from the ``tool_name``, ``version``, and ``install_dir``
175+ parameters.
119176
120177 :returns: A `bool` describing if the symlink was created.
121178 """
122- link = Path (install_dir ) / (tool_name + suffix )
123- target = Path (install_dir ) / f"{ tool_name } -{ version } { suffix } "
179+ link_root_path = Path (install_dir )
180+ if not link_root_path .exists ():
181+ link_root_path .mkdir (parents = True )
182+ link = link_root_path / (tool_name + suffix )
183+ if target is None :
184+ target = link_root_path / f"{ tool_name } -{ version } { suffix } "
124185 if link .exists ():
125186 if not link .is_symlink ():
126187 print (
@@ -146,7 +207,7 @@ def create_sym_link(
146207 except OSError as exc : # pragma: no cover
147208 print (
148209 "Encountered an error when trying to create the symbolic link:" ,
149- exc .strerror ,
210+ "; " . join ([ x for x in exc .args if isinstance ( x , str )]) ,
150211 sep = "\n " ,
151212 )
152213 if install_os == "windows" :
@@ -205,6 +266,10 @@ def install_clang_tools(
205266 f"directory is not in your environment variable PATH.{ RESET_COLOR } " ,
206267 )
207268 for tool_name in ("clang-format" , "clang-tidy" ):
208- install_tool (tool_name , version , install_dir , no_progress_bar )
209- # `install_tool()` guarantees that the binary exists now
210- create_sym_link (tool_name , version , install_dir , overwrite ) # pragma: no cover
269+ native_bin = is_installed (tool_name , version )
270+ if native_bin is None : # (not already installed)
271+ # `install_tool()` guarantees that the binary exists now
272+ install_tool (tool_name , version , install_dir , no_progress_bar )
273+ create_sym_link ( # pragma: no cover
274+ tool_name , version , install_dir , overwrite , native_bin
275+ )
0 commit comments