Skip to content

allow version to be a path #95

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Jun 4, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 33 additions & 31 deletions clang_tools/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,38 +4,33 @@

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, parse_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: int) -> 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}" if install_os != "windows" else "") + suffix
)
try:
result = subprocess.run(
Expand All @@ -47,28 +42,30 @@ 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):
return None # version is unknown or not the desired major release
return path


def clang_tools_binary_url(tool: str, version: str, tag: str = release_tag) -> str:
def clang_tools_binary_url(tool: str, version: int, tag: str = release_tag) -> str:
"""Assemble the URL to the binary.

:param tool: The name of the tool to download.
:param version: The version of the tool to download.
:param version: The major version of the tool to download.
:param tag: The release tag used in the base URL.

:returns: The URL used to download the specified tool.
Expand All @@ -82,12 +79,12 @@ def clang_tools_binary_url(tool: str, version: str, tag: str = release_tag) -> s


def install_tool(
tool_name: str, version: str, directory: str, no_progress_bar: bool
tool_name: str, version: int, directory: str, no_progress_bar: bool
) -> bool:
"""An abstract function that can install either clang-tidy or clang-format.

:param tool_name: The name of the clang-tool to install.
:param version: The version of the tools to install.
:param version: The major version of the tools to install.
:param directory: The installation directory.
:param no_progress_bar: A flag used to disable the downloads' progress bar.

Expand Down Expand Up @@ -157,16 +154,16 @@ def move_and_chmod_bin(old_bin_name: str, new_bin_name: str, install_dir: str) -

def create_sym_link(
tool_name: str,
version: str,
version: int,
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.

:param tool_name: The name of the clang-tool to symlink.
:param version: The version of the clang-tool to symlink.
:param version: The major version of the clang-tool to symlink.
:param install_dir: The installation directory to create the symlink in.
:param overwrite: A flag to indicate if an existing symlink should be overwritten.
:param target: The target executable's path and name for which to create a symlink
Expand Down Expand Up @@ -215,11 +212,11 @@ def create_sym_link(
return False


def uninstall_tool(tool_name: str, version: str, directory: str):
def uninstall_tool(tool_name: str, version: int, directory: str):
"""Remove a specified tool of a given version.

:param tool_name: The name of the clang tool to uninstall.
:param version: The version of the clang-tools to remove.
:param version: The major version of the clang-tools to remove.
:param directory: The directory from which to remove the
installed clang-tools.
"""
Expand All @@ -244,24 +241,29 @@ def uninstall_clang_tools(version: str, directory: str):
"""
install_dir = install_dir_name(directory)
print(f"Uninstalling version {version} from {str(install_dir)}")
version_tuple = parse_version(version)
for tool in ("clang-format", "clang-tidy"):
uninstall_tool(tool, version, install_dir)
uninstall_tool(tool, version_tuple[0], install_dir)


def install_clang_tools(
version: str, tools: str, directory: str, overwrite: bool, no_progress_bar: bool
version: int,
tools: str,
directory: str,
overwrite: bool,
no_progress_bar: bool,
) -> None:
"""Wraps functions used to individually install tools.

:param version: The version of the tools to install.
:param version: The major version of the tools to install.
:param tools: The specify tool(s) to install.
:param directory: The installation directory.
:param overwrite: A flag to indicate if the creation of a symlink has
permission to overwrite an existing symlink.
: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 Down
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 parse_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``) or a path that "
"points to the directory where the binaries already exist.",
)
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 = parse_version(args.install)
if version != (0, 0, 0):
install_clang_tools(
version[0],
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
23 changes: 21 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,22 @@ 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()


def parse_version(version: str) -> Tuple[int, int, int]:
"""Parse the given version string into a semantic specification.

:param version: The version specification as a string.

:returns: A tuple of ints that describes the major, minor, and patch versions.
If the version is a path, then the tuple is just 3 zeros.
"""
version_tuple = version.split(".")
if len(version_tuple) < 3:
# append minor and patch version numbers if not specified
version_tuple += ["0"] * (3 - len(version_tuple))
try:
return tuple([int(x) for x in version_tuple]) # type: ignore[return-value]
except ValueError:
assert Path(version).exists(), "specified version is not a semantic or a path"
return (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
}

[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__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__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;
}

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