Skip to content

Commit

Permalink
fix: add --remove_from_release to install binary script
Browse files Browse the repository at this point in the history
  • Loading branch information
natelandau committed Sep 25, 2024
1 parent f4f5ab7 commit b600bbe
Show file tree
Hide file tree
Showing 3 changed files with 127 additions and 59 deletions.
16 changes: 16 additions & 0 deletions dotfiles/.chezmoidata/binaries.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,59 +7,75 @@
# version_regex (optional): A custom regex to extract the version from `binary --version`.
# required_architecture (optional): The architecture required for the binary. If the current architecture does not match, the binary will not be installed. Must match .chezmoi.arch.
# install_filter (optional): Filters to use when installing the package. Must match the name of a filter in .chezmoi.data - ie "dev_computer", "personal_computer", "homelab_member".
# remove_from_release (optional): A regex used to filter out releases. Useful when more than one release is available for your platform.
# executable_name (optional): Name of executable if different from name of package.

[binaries]
[binaries.bottom]
executable_name = "btm"
install_filter = ""
name = "bottom"
remove_from_release = ""
repository = "ClementTsang/bottom"
required_architecture = ""
systems = ["linux"]
version_regex = ""

[binaries.doggo]
executable_name = ""
install_filter = ""
name = "doggo"
remove_from_release = "_web_"
repository = "mr-karan/doggo"
required_architecture = ""
systems = ["linux"]
version_regex = ""

[binaries.git-delta]
executable_name = ""
install_filter = ""
name = "delta"
remove_from_release = ""
repository = "dandavison/delta"
required_architecture = ""
systems = ["linux"]
version_regex = ""

[binaries.lazygit]
executable_name = ""
install_filter = "dev_computer"
name = "lazygit"
remove_from_release = ""
repository = "jesseduffield/lazygit"
required_architecture = ""
systems = ["linux"]
version_regex = "version=(.*?),"

[binaries.otree]
executable_name = ""
install_filter = ""
name = "otree"
remove_from_release = ""
repository = "fioncat/otree"
required_architecture = "amd64"
systems = ["linux"]
version_regex = ""

[binaries.rip2]
executable_name = ""
install_filter = ""
name = "rip"
remove_from_release = ""
repository = "MilesCranmer/rip2"
required_architecture = ""
systems = ["darwin", "linux"]
version_regex = ""

[binaries.zoxide]
executable_name = ""
install_filter = ""
name = "zoxide"
remove_from_release = ""
repository = "ajeetdsouza/zoxide"
required_architecture = ""
systems = ["linux"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ if [[ ! -e ${INSTALL_SCRIPT} ]] then
fi


uv run -q "${INSTALL_SCRIPT}" --binary-name="{{ $binary.name }}" --repository="{{ $binary.repository }}" --version-regex="{{ $binary.version_regex }}"
uv run -q "${INSTALL_SCRIPT}" --binary-name="{{ $binary.name }}" --repository="{{ $binary.repository }}" --version-regex="{{ $binary.version_regex }}" --remove-from-release="{{ $binary.remove_from_release }}" --executable-name="{{ $binary.executable_name }}"


{{ end }}
Expand Down
168 changes: 110 additions & 58 deletions dotfiles/bin/executable_install-binary.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import re
import shutil
import tarfile
from collections.abc import Callable
from enum import Enum
from pathlib import Path
from tempfile import TemporaryDirectory
Expand Down Expand Up @@ -160,13 +161,22 @@ def instantiate_logger(
class BinaryUpdater:
"""Class to check for updates to a binary and install the update if available."""

def __init__(self, binary_name: str, repository: str, version_regex: str = ""):
def __init__(
self,
binary_name: str,
repository: str,
version_regex: str = "",
remove_from_release: str = "",
executable_name: str = "",
):
"""Initialize the BinaryUpdater class.
Args:
binary_name (str): Name of the binary available in the PATH
repository (str): GitHub repository in the format 'owner/repo'
version_regex (str): Custom regex to identify the version in the binary --version output
remove_from_release (str): Regex used to filter out releases. Useful when more than one release is available.
executable_name (str): Name of executable, if different from binary_name
"""
# Initialize instance attributes
self._release_info: dict = {}
Expand All @@ -181,9 +191,11 @@ def __init__(self, binary_name: str, repository: str, version_regex: str = ""):
self.binary_name = binary_name
self.repository = repository
self.version_regex = version_regex
self.remove_from_release = remove_from_release
self.executable_name = executable_name if executable_name else binary_name

# Get the latest release information
self.latest_version: str = self.release_info["name"].replace("v", "").strip()
self.latest_version: str = re.search(r"(\d+\.\d+\.\d+)", self.release_info["name"]).group(1)
self.is_draft: bool = self.release_info["draft"]
self.is_prerelease: bool = self.release_info["prerelease"]
self.assets: list = self.release_info["assets"]
Expand All @@ -203,7 +215,7 @@ def have_local_binary(self) -> bool:
return self._have_local_binary

try:
self._cmd = sh.Command(self.binary_name)
self._cmd = sh.Command(self.executable_name)
except sh.CommandNotFound:
self._cmd = None

Expand Down Expand Up @@ -256,9 +268,7 @@ def local_version(self) -> str:
self.version_regex, local_version, re.IGNORECASE
).group(1)
else:
self._local_version = re.sub(
rf"{self.binary_name} ?v?", "", local_version, flags=re.IGNORECASE
)
self._local_version = re.search(r"(\d+\.\d+\.\d+)", local_version).group(1)

return self._local_version

Expand Down Expand Up @@ -289,14 +299,15 @@ def need_install(self) -> bool:
console.print(f"{self.latest_version=}")
raise typer.Exit(1) from e

def _find_deb_release(self, architecture: str) -> list[dict]:
def _find_deb_release(self, architecture: str, remove_from_release: str) -> list[dict]:
"""Find a .deb release for the host system based on the specified architecture.
This method searches through the available assets to find .deb packages that match
the given architecture. It prioritizes non-MUSL builds over MUSL builds if both are available.
Args:
architecture (str): The architecture of the host system (e.g., 'x86_64', 'arm64').
remove_from_release (str): A regex pattern to filter out releases.
Returns:
list[dict]: A list of dictionaries representing the .deb assets that match the specified architecture.
Expand All @@ -312,6 +323,9 @@ def _find_deb_release(self, architecture: str) -> list[dict]:
if not a["name"].endswith(".deb"):
continue

if remove_from_release and re.search(remove_from_release, a["name"].lower()):
continue

try:
if not re.search(Arches[architecture.upper()].value, a["name"].lower()):
continue
Expand All @@ -335,7 +349,9 @@ def _find_deb_release(self, architecture: str) -> list[dict]:

return possible_assets

def _find_packaged_release(self, operating_system: str, architecture: str) -> list[dict]:
def _find_packaged_release(
self, operating_system: str, architecture: str, remove_from_release: str
) -> list[dict]:
"""Find a packaged release for the host system based on the operating system and architecture.
This method filters the available assets to find those that match the specified operating system
Expand All @@ -344,6 +360,7 @@ def _find_packaged_release(self, operating_system: str, architecture: str) -> li
Args:
operating_system (str): The operating system of the host (e.g., 'linux', 'windows').
architecture (str): The architecture of the host (e.g., 'x86_64', 'arm').
remove_from_release (str): A regex pattern to filter out releases.
Returns:
list[dict]: A list of dictionaries representing the possible assets that match the criteria.
Expand All @@ -358,6 +375,9 @@ def _find_packaged_release(self, operating_system: str, architecture: str) -> li
if not a["name"].endswith(".tar.gz"):
continue

if remove_from_release and re.search(remove_from_release, a["name"].lower()):
continue

if not re.search(operating_system.lower(), a["name"].lower()):
continue

Expand Down Expand Up @@ -413,11 +433,13 @@ def download_url(self) -> str:

possible_releases = []
if host_platform.system.lower() == "linux":
possible_releases = self._find_deb_release(host_platform.machine)
possible_releases = self._find_deb_release(
host_platform.machine, self.remove_from_release
)

if not possible_releases:
possible_releases = self._find_packaged_release(
host_platform.system, host_platform.machine
host_platform.system, host_platform.machine, self.remove_from_release
)

if not possible_releases:
Expand Down Expand Up @@ -455,64 +477,79 @@ def download_asset_name(self) -> str:
return self._download_asset_name


def install_from_tarball(binary: BinaryUpdater, dry_run: bool) -> None:
def install_from_tarball(binary: BinaryUpdater, dry_run: bool) -> None: # noqa: C901
"""Download and install a binary from a tarball.
This function handles the process of downloading a tarball, extracting its contents,
and moving the binary to the appropriate directory. It supports dry-run mode to
simulate the actions without making any changes.
This function handles downloading a tarball, extracting its contents,
and moving the binary to the appropriate directory. Supports dry-run mode.
Args:
binary (BinaryUpdater): An instance of the BinaryUpdater class containing information
about the binary to be installed.
dry_run (bool): If True, the function will only log the actions that would be taken
without actually performing them.
binary (BinaryUpdater): Contains binary info for installation.
dry_run (bool): If True, logs actions without performing them.
Raises:
typer.Exit: If any step in the process fails, the function will log an error and exit.
typer.Exit: If any step fails.
"""
bin_dir = Path.home() / ".local" / "bin"
bin_dir.mkdir(parents=True, exist_ok=True)
tempdir = TemporaryDirectory(ignore_cleanup_errors=True)
work_dir = Path(tempdir.name)
download_path = work_dir / binary.download_asset_name
unarchive_path = work_dir / binary.binary_name

# Download the asset
msg = f"Download: {binary.download_url}"
if dry_run:
logger.log("DRYRUN", msg)
else:
logger.debug(msg)
with httpx.stream("GET", binary.download_url, follow_redirects=True) as r:
r.raise_for_status()
with download_path.open("wb") as f:
for chunk in r.iter_bytes():
f.write(chunk)
def find_unarchived_binary(possible_dirs: list[Path], binary_name: str) -> Path | None:
"""Find the binary in one of the possible unarchived directories."""
for directory in possible_dirs:
if directory.exists() and directory.is_dir():
unarchive_path = directory / binary_name
if unarchive_path.exists() and unarchive_path.is_file():
return unarchive_path
return None

# Unarchive the asset
msg = f"tar xf {download_path}"
if dry_run:
logger.log("DRYRUN", msg)
else:
logger.debug(msg)
with tarfile.open(download_path, "r:gz") as f:
f.extractall(path=work_dir, filter="data")
bin_dir = Path.home() / ".local" / "bin"
bin_dir.mkdir(parents=True, exist_ok=True)

# Move the binary to the bin directory
msg = f"mv {unarchive_path} {bin_dir / binary.binary_name}"
if dry_run:
logger.log("DRYRUN", msg)
else:
logger.debug(msg)
if unarchive_path.exists():
logger.debug(f"Moving {unarchive_path} to {bin_dir / binary.binary_name}")
shutil.move(unarchive_path, bin_dir / binary.binary_name)
with TemporaryDirectory() as tempdir:
work_dir = Path(tempdir)
download_path = work_dir / binary.download_asset_name
possible_unarchive_dirs = [
work_dir / binary.binary_name,
work_dir / download_path.name,
work_dir / download_path.stem,
work_dir / download_path.name.replace(".tar.gz", ""),
]

# Download the asset
download_msg = f"Download: {binary.download_url}"
if dry_run:
logger.log("DRYRUN", download_msg)
else:
logger.error(f"Failed to find {binary.binary_name} in archive")
raise typer.Exit(1)

tempdir.cleanup()
logger.debug(download_msg)
with httpx.stream("GET", binary.download_url, follow_redirects=True) as r:
r.raise_for_status()
with download_path.open("wb") as f:
for chunk in r.iter_bytes():
f.write(chunk)

# Unarchive the asset
unarchive_msg = f"tar xf {download_path}"
if dry_run:
logger.log("DRYRUN", unarchive_msg)
else:
logger.debug(unarchive_msg)
with tarfile.open(download_path, "r:gz") as tar:
tar.extractall(path=work_dir, filter="data")

# Move the binary to the bin directory
unarchive_path = find_unarchived_binary(possible_unarchive_dirs, binary.binary_name)
if not unarchive_path:
error_msg = f"Failed to find {binary.binary_name} in archive"
if dry_run:
logger.log("DRYRUN", error_msg)
else:
logger.error(error_msg)
raise typer.Exit(1)
else:
move_msg = f"mv {unarchive_path} {bin_dir / binary.binary_name}"
if dry_run:
logger.log("DRYRUN", move_msg)
else:
logger.debug(move_msg)
shutil.move(unarchive_path, bin_dir / binary.binary_name)


def install_deb_package(binary: BinaryUpdater, dry_run: bool) -> None:
Expand Down Expand Up @@ -605,6 +642,10 @@ def main(
repository: Annotated[
str, typer.Option(help="GitHub repository in the format 'owner/repo'", show_default=False)
],
executable_name: Annotated[
str,
typer.Option(help="Name of executable if different from binary_name", show_default=False),
] = "",
install_script: Annotated[
str, typer.Option(help="URL for an install script to be piped to sh", show_default=False)
] = "",
Expand All @@ -615,6 +656,13 @@ def main(
show_default=False,
),
] = "",
remove_from_release: Annotated[
str,
typer.Option(
help="Regex used to filter out releases. Useful when more than one release is available.",
show_default=False,
),
] = "",
log_file: Annotated[
Path,
typer.Option(
Expand Down Expand Up @@ -663,7 +711,11 @@ def main(
)

binary = BinaryUpdater(
binary_name=binary_name, repository=repository, version_regex=version_regex
binary_name=binary_name,
repository=repository,
version_regex=version_regex,
remove_from_release=remove_from_release,
executable_name=executable_name,
)

logger.log("SECONDARY", f"Repository: https://github.com/{binary.repository}")
Expand Down

0 comments on commit b600bbe

Please sign in to comment.