Skip to content

Commit 8d5f32b

Browse files
authored
allow version to be a path (#95)
* resolves #94
1 parent b93b1da commit 8d5f32b

File tree

7 files changed

+252
-52
lines changed

7 files changed

+252
-52
lines changed

clang_tools/install.py

+22-23
Original file line numberDiff line numberDiff line change
@@ -4,38 +4,35 @@
44
55
The module that performs the installation of clang-tools.
66
"""
7+
78
import os
89
from pathlib import Path, PurePath
910
import re
1011
import shutil
1112
import subprocess
1213
import sys
13-
from typing import Optional
14-
from . import release_tag
14+
from typing import Optional, cast
1515

16-
from . import install_os, RESET_COLOR, suffix, YELLOW
17-
from .util import download_file, verify_sha512, get_sha_checksum
16+
from . import release_tag, install_os, RESET_COLOR, suffix, YELLOW
17+
from .util import download_file, verify_sha512, get_sha_checksum, Version
1818

1919

2020
#: This pattern is designed to match only the major version number.
2121
RE_PARSE_VERSION = re.compile(rb"version\s([\d\.]+)", re.MULTILINE)
2222

2323

24-
def is_installed(tool_name: str, version: str) -> Optional[Path]:
24+
def is_installed(tool_name: str, version: Version) -> Optional[Path]:
2525
"""Detect if the specified tool is installed.
2626
2727
:param tool_name: The name of the specified tool.
28-
:param version: The specific version to expect.
28+
:param version: The specific major version to expect.
2929
3030
:returns: The path to the detected tool (if found), otherwise `None`.
3131
"""
32-
version_tuple = version.split(".")
33-
ver_major = version_tuple[0]
34-
if len(version_tuple) < 3:
35-
# append minor and patch version numbers if not specified
36-
version_tuple += ("0",) * (3 - len(version_tuple))
3732
exe_name = (
38-
f"{tool_name}" + (f"-{ver_major}" if install_os != "windows" else "") + suffix
33+
f"{tool_name}"
34+
+ (f"-{version.info[0]}" if install_os != "windows" else "")
35+
+ suffix
3936
)
4037
try:
4138
result = subprocess.run(
@@ -47,19 +44,21 @@ def is_installed(tool_name: str, version: str) -> Optional[Path]:
4744
except (FileNotFoundError, subprocess.CalledProcessError):
4845
return None # tool is not installed
4946
ver_num = RE_PARSE_VERSION.search(result.stdout)
47+
assert ver_num is not None, "Failed to parse version from tool output"
48+
ver_match = cast(bytes, ver_num.groups(0)[0]).decode(encoding="utf-8")
5049
print(
5150
f"Found a installed version of {tool_name}:",
52-
ver_num.groups(0)[0].decode(encoding="utf-8"),
51+
ver_match,
5352
end=" ",
5453
)
55-
path = shutil.which(exe_name) # find the installed binary
56-
if path is None:
54+
exe_path = shutil.which(exe_name) # find the installed binary
55+
if exe_path is None:
5756
print() # print end-of-line
5857
return None # failed to locate the binary
59-
path = Path(path).resolve()
58+
path = Path(exe_path).resolve()
6059
print("at", str(path))
61-
ver_num = ver_num.groups(0)[0].decode(encoding="utf-8").split(".")
62-
if ver_num is None or ver_num[0] != ver_major:
60+
ver_tuple = ver_match.split(".")
61+
if ver_tuple is None or ver_tuple[0] != str(version.info[0]):
6362
return None # version is unknown or not the desired major release
6463
return path
6564

@@ -160,7 +159,7 @@ def create_sym_link(
160159
version: str,
161160
install_dir: str,
162161
overwrite: bool = False,
163-
target: Path = None,
162+
target: Optional[Path] = None,
164163
) -> bool:
165164
"""Create a symlink to the installed binary that
166165
doesn't have the version number appended.
@@ -249,7 +248,7 @@ def uninstall_clang_tools(version: str, directory: str):
249248

250249

251250
def install_clang_tools(
252-
version: str, tools: str, directory: str, overwrite: bool, no_progress_bar: bool
251+
version: Version, tools: str, directory: str, overwrite: bool, no_progress_bar: bool
253252
) -> None:
254253
"""Wraps functions used to individually install tools.
255254
@@ -261,7 +260,7 @@ def install_clang_tools(
261260
:param no_progress_bar: A flag used to disable the downloads' progress bar.
262261
"""
263262
install_dir = install_dir_name(directory)
264-
if install_dir.rstrip(os.sep) not in os.environ.get("PATH"):
263+
if install_dir.rstrip(os.sep) not in os.environ.get("PATH", ""):
265264
print(
266265
f"{YELLOW}{install_dir}",
267266
f"directory is not in your environment variable PATH.{RESET_COLOR}",
@@ -270,7 +269,7 @@ def install_clang_tools(
270269
native_bin = is_installed(tool_name, version)
271270
if native_bin is None: # (not already installed)
272271
# `install_tool()` guarantees that the binary exists now
273-
install_tool(tool_name, version, install_dir, no_progress_bar)
272+
install_tool(tool_name, version.string, install_dir, no_progress_bar)
274273
create_sym_link( # pragma: no cover
275-
tool_name, version, install_dir, overwrite, native_bin
274+
tool_name, version.string, install_dir, overwrite, native_bin
276275
)

clang_tools/main.py

+19-8
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@
44
55
The module containing main entrypoint function.
66
"""
7+
78
import argparse
89

910
from .install import install_clang_tools, uninstall_clang_tools
1011
from . import RESET_COLOR, YELLOW
12+
from .util import Version
1113

1214

1315
def get_parser() -> argparse.ArgumentParser:
@@ -18,7 +20,9 @@ def get_parser() -> argparse.ArgumentParser:
1820
"-i",
1921
"--install",
2022
metavar="VERSION",
21-
help="Install clang-tools about a specific version.",
23+
help="Install clang-tools about a specific version. This can be in the form of"
24+
" a semantic version specification (``x.y.z``, ``x.y``, ``x``). NOTE: A "
25+
"malformed version specification will cause a silent failure.",
2226
)
2327
parser.add_argument(
2428
"-t",
@@ -66,13 +70,20 @@ def main():
6670
if args.uninstall:
6771
uninstall_clang_tools(args.uninstall, args.directory)
6872
elif args.install:
69-
install_clang_tools(
70-
args.install,
71-
args.tool,
72-
args.directory,
73-
args.overwrite,
74-
args.no_progress_bar,
75-
)
73+
version = Version(args.install)
74+
if version.info != (0, 0, 0):
75+
install_clang_tools(
76+
version,
77+
args.tool,
78+
args.directory,
79+
args.overwrite,
80+
args.no_progress_bar,
81+
)
82+
else:
83+
print(
84+
f"{YELLOW}The version specified is not a semantic",
85+
f"specification{RESET_COLOR}",
86+
)
7687
else:
7788
print(
7889
f"{YELLOW}Nothing to do because `--install` and `--uninstall`",

clang_tools/util.py

+26-2
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@
44
55
A module containing utility functions.
66
"""
7+
78
import platform
89
import hashlib
910
from pathlib import Path
1011
import urllib.request
11-
from typing import Optional
12+
from typing import Optional, Tuple
1213
from urllib.error import HTTPError
1314
from http.client import HTTPResponse
1415

@@ -82,7 +83,6 @@ def get_sha_checksum(binary_url: str) -> str:
8283
with urllib.request.urlopen(
8384
binary_url.replace(".exe", "") + ".sha512sum"
8485
) as response:
85-
response: HTTPResponse
8686
return response.read(response.length).decode(encoding="utf-8")
8787

8888

@@ -99,3 +99,27 @@ def verify_sha512(checksum: str, exe: bytes) -> bool:
9999
# released checksum's include the corresponding filename (which we don't need)
100100
checksum = checksum.split(" ", 1)[0]
101101
return checksum == hashlib.sha512(exe).hexdigest()
102+
103+
104+
class Version:
105+
"""Parse the given version string into a semantic specification.
106+
107+
:param user_input: The version specification as a string.
108+
"""
109+
110+
def __init__(self, user_input: str):
111+
#: The version input in string form
112+
self.string = user_input
113+
version_tuple = user_input.split(".")
114+
self.info: Tuple[int, int, int]
115+
"""
116+
A tuple of integers that describes the major, minor, and patch versions.
117+
If the version `string` is a path, then this tuple is just 3 zeros.
118+
"""
119+
if len(version_tuple) < 3:
120+
# append minor and patch version numbers if not specified
121+
version_tuple += ["0"] * (3 - len(version_tuple))
122+
try:
123+
self.info = tuple([int(x) for x in version_tuple]) # type: ignore[assignment]
124+
except ValueError:
125+
self.info = (0, 0, 0)

docs/_static/extra_css.css

+43
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,46 @@ thead {
33
background-color: var(--md-accent-bg-color--light);
44
color: var(--md-default-bg-color);
55
}
6+
7+
.md-typeset .mdx-badge {
8+
font-size: .85em
9+
}
10+
11+
.md-typeset .mdx-badge--right {
12+
float: right;
13+
margin-left: .35em
14+
}
15+
16+
.md-typeset .mdx-badge__icon {
17+
background: var(--md-accent-fg-color--transparent);
18+
padding: .2rem;
19+
}
20+
21+
.md-typeset .mdx-badge__icon:last-child {
22+
border-radius: .1rem;
23+
}
24+
25+
[dir=ltr] .md-typeset .mdx-badge__icon {
26+
border-top-left-radius: .1rem;
27+
border-bottom-left-radius: .1rem;
28+
}
29+
30+
[dir=rtl] .md-typeset .mdx-badge__icon {
31+
border-top-right-radius: .1rem;
32+
border-bottom-right-radius: .1rem;
33+
}
34+
35+
.md-typeset .mdx-badge__text {
36+
box-shadow: 0 0 0 1px inset var(--md-accent-fg-color--transparent);
37+
padding: .2rem .3rem;
38+
}
39+
40+
[dir=ltr] .md-typeset .mdx-badge__text {
41+
border-top-right-radius: .1rem;
42+
border-bottom-right-radius: .1rem;
43+
}
44+
45+
[dir=rtl] .md-typeset .mdx-badge__text {
46+
border-top-left-radius: .1rem;
47+
border-bottom-left-radius: .1rem;
48+
}

0 commit comments

Comments
 (0)