Skip to content

Commit

Permalink
allow version to be a path (#95)
Browse files Browse the repository at this point in the history
* resolves #94
  • Loading branch information
2bndy5 authored Jun 4, 2024
1 parent b93b1da commit 8d5f32b
Show file tree
Hide file tree
Showing 7 changed files with 252 additions and 52 deletions.
45 changes: 22 additions & 23 deletions clang_tools/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,38 +4,35 @@
The module that performs the installation of clang-tools.
"""

import os
from pathlib import Path, PurePath
import re
import shutil
import subprocess
import sys
from typing import Optional
from . import release_tag
from typing import Optional, cast

from . import install_os, RESET_COLOR, suffix, YELLOW
from .util import download_file, verify_sha512, get_sha_checksum
from . import release_tag, install_os, RESET_COLOR, suffix, YELLOW
from .util import download_file, verify_sha512, get_sha_checksum, Version


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


def is_installed(tool_name: str, version: str) -> Optional[Path]:
def is_installed(tool_name: str, version: Version) -> Optional[Path]:
"""Detect if the specified tool is installed.
:param tool_name: The name of the specified tool.
:param version: The specific version to expect.
:param version: The specific major version to expect.
:returns: The path to the detected tool (if found), otherwise `None`.
"""
version_tuple = version.split(".")
ver_major = version_tuple[0]
if len(version_tuple) < 3:
# append minor and patch version numbers if not specified
version_tuple += ("0",) * (3 - len(version_tuple))
exe_name = (
f"{tool_name}" + (f"-{ver_major}" if install_os != "windows" else "") + suffix
f"{tool_name}"
+ (f"-{version.info[0]}" if install_os != "windows" else "")
+ suffix
)
try:
result = subprocess.run(
Expand All @@ -47,19 +44,21 @@ def is_installed(tool_name: str, version: str) -> Optional[Path]:
except (FileNotFoundError, subprocess.CalledProcessError):
return None # tool is not installed
ver_num = RE_PARSE_VERSION.search(result.stdout)
assert ver_num is not None, "Failed to parse version from tool output"
ver_match = cast(bytes, ver_num.groups(0)[0]).decode(encoding="utf-8")
print(
f"Found a installed version of {tool_name}:",
ver_num.groups(0)[0].decode(encoding="utf-8"),
ver_match,
end=" ",
)
path = shutil.which(exe_name) # find the installed binary
if path is None:
exe_path = shutil.which(exe_name) # find the installed binary
if exe_path is None:
print() # print end-of-line
return None # failed to locate the binary
path = Path(path).resolve()
path = Path(exe_path).resolve()
print("at", str(path))
ver_num = ver_num.groups(0)[0].decode(encoding="utf-8").split(".")
if ver_num is None or ver_num[0] != ver_major:
ver_tuple = ver_match.split(".")
if ver_tuple is None or ver_tuple[0] != str(version.info[0]):
return None # version is unknown or not the desired major release
return path

Expand Down Expand Up @@ -160,7 +159,7 @@ def create_sym_link(
version: str,
install_dir: str,
overwrite: bool = False,
target: Path = None,
target: Optional[Path] = None,
) -> bool:
"""Create a symlink to the installed binary that
doesn't have the version number appended.
Expand Down Expand Up @@ -249,7 +248,7 @@ def uninstall_clang_tools(version: str, directory: str):


def install_clang_tools(
version: str, tools: str, directory: str, overwrite: bool, no_progress_bar: bool
version: Version, tools: str, directory: str, overwrite: bool, no_progress_bar: bool
) -> None:
"""Wraps functions used to individually install tools.
Expand All @@ -261,7 +260,7 @@ def install_clang_tools(
:param no_progress_bar: A flag used to disable the downloads' progress bar.
"""
install_dir = install_dir_name(directory)
if install_dir.rstrip(os.sep) not in os.environ.get("PATH"):
if install_dir.rstrip(os.sep) not in os.environ.get("PATH", ""):
print(
f"{YELLOW}{install_dir}",
f"directory is not in your environment variable PATH.{RESET_COLOR}",
Expand All @@ -270,7 +269,7 @@ def install_clang_tools(
native_bin = is_installed(tool_name, version)
if native_bin is None: # (not already installed)
# `install_tool()` guarantees that the binary exists now
install_tool(tool_name, version, install_dir, no_progress_bar)
install_tool(tool_name, version.string, install_dir, no_progress_bar)
create_sym_link( # pragma: no cover
tool_name, version, install_dir, overwrite, native_bin
tool_name, version.string, install_dir, overwrite, native_bin
)
27 changes: 19 additions & 8 deletions clang_tools/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
The module containing main entrypoint function.
"""

import argparse

from .install import install_clang_tools, uninstall_clang_tools
from . import RESET_COLOR, YELLOW
from .util import Version


def get_parser() -> argparse.ArgumentParser:
Expand All @@ -18,7 +20,9 @@ def get_parser() -> argparse.ArgumentParser:
"-i",
"--install",
metavar="VERSION",
help="Install clang-tools about a specific version.",
help="Install clang-tools about a specific version. This can be in the form of"
" a semantic version specification (``x.y.z``, ``x.y``, ``x``). NOTE: A "
"malformed version specification will cause a silent failure.",
)
parser.add_argument(
"-t",
Expand Down Expand Up @@ -66,13 +70,20 @@ def main():
if args.uninstall:
uninstall_clang_tools(args.uninstall, args.directory)
elif args.install:
install_clang_tools(
args.install,
args.tool,
args.directory,
args.overwrite,
args.no_progress_bar,
)
version = Version(args.install)
if version.info != (0, 0, 0):
install_clang_tools(
version,
args.tool,
args.directory,
args.overwrite,
args.no_progress_bar,
)
else:
print(
f"{YELLOW}The version specified is not a semantic",
f"specification{RESET_COLOR}",
)
else:
print(
f"{YELLOW}Nothing to do because `--install` and `--uninstall`",
Expand Down
28 changes: 26 additions & 2 deletions clang_tools/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@
A module containing utility functions.
"""

import platform
import hashlib
from pathlib import Path
import urllib.request
from typing import Optional
from typing import Optional, Tuple
from urllib.error import HTTPError
from http.client import HTTPResponse

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


Expand All @@ -99,3 +99,27 @@ def verify_sha512(checksum: str, exe: bytes) -> bool:
# released checksum's include the corresponding filename (which we don't need)
checksum = checksum.split(" ", 1)[0]
return checksum == hashlib.sha512(exe).hexdigest()


class Version:
"""Parse the given version string into a semantic specification.
:param user_input: The version specification as a string.
"""

def __init__(self, user_input: str):
#: The version input in string form
self.string = user_input
version_tuple = user_input.split(".")
self.info: Tuple[int, int, int]
"""
A tuple of integers that describes the major, minor, and patch versions.
If the version `string` is a path, then this tuple is just 3 zeros.
"""
if len(version_tuple) < 3:
# append minor and patch version numbers if not specified
version_tuple += ["0"] * (3 - len(version_tuple))
try:
self.info = tuple([int(x) for x in version_tuple]) # type: ignore[assignment]
except ValueError:
self.info = (0, 0, 0)
43 changes: 43 additions & 0 deletions docs/_static/extra_css.css
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,46 @@ thead {
background-color: var(--md-accent-bg-color--light);
color: var(--md-default-bg-color);
}

.md-typeset .mdx-badge {
font-size: .85em
}

.md-typeset .mdx-badge--right {
float: right;
margin-left: .35em
}

.md-typeset .mdx-badge__icon {
background: var(--md-accent-fg-color--transparent);
padding: .2rem;
}

.md-typeset .mdx-badge__icon:last-child {
border-radius: .1rem;
}

[dir=ltr] .md-typeset .mdx-badge__icon {
border-top-left-radius: .1rem;
border-bottom-left-radius: .1rem;
}

[dir=rtl] .md-typeset .mdx-badge__icon {
border-top-right-radius: .1rem;
border-bottom-right-radius: .1rem;
}

.md-typeset .mdx-badge__text {
box-shadow: 0 0 0 1px inset var(--md-accent-fg-color--transparent);
padding: .2rem .3rem;
}

[dir=ltr] .md-typeset .mdx-badge__text {
border-top-right-radius: .1rem;
border-bottom-right-radius: .1rem;
}

[dir=rtl] .md-typeset .mdx-badge__text {
border-top-left-radius: .1rem;
border-bottom-left-radius: .1rem;
}
Loading

0 comments on commit 8d5f32b

Please sign in to comment.