Skip to content

Instantly share code, notes, and snippets.

@themasch
Created October 25, 2025 11:28
Show Gist options
  • Select an option

  • Save themasch/cce26c1220480e355f988b1ab8f43acc to your computer and use it in GitHub Desktop.

Select an option

Save themasch/cce26c1220480e355f988b1ab8f43acc to your computer and use it in GitHub Desktop.
script to install tools from github releases if they are not properly packages for your distribution
#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.13"
# dependencies = ["PyGithub", "requests", "xdg", "tomlkit"]
# ///
#
# example config: ~/.config/install-from-github/packages.toml
# ```toml
# github_api_token="github_<GET_YOUR_OWNY>"
#
# [picotool]
# repo = "raspberrypi/pico-sdk-tools"
# artifact_pattern = "picotool-*-x86_64-lin.tar.gz"
# binary_path = "picotool/picotool"
# ```
from dataclasses import dataclass
from github import Github, GitReleaseAsset, Auth
import fnmatch
import hashlib
import os
from xdg import BaseDirectory
@dataclass
class GlobalConfig:
cache_base: str
state_base: str
config_base: str
@dataclass
class ToolConfig:
name: str
repo: str
artifact_pattern: str
binary_path: str
class Storage:
cfg: GlobalConfig
def __init__(self, cfg: GlobalConfig):
self.cfg = cfg
def tool_download_dir(self, tool_name: str) -> str:
tool_cache_dir = os.path.join(self.cfg.cache_base, tool_name)
os.makedirs(tool_cache_dir, exist_ok=True)
return tool_cache_dir
def tool_download_artifact_path(self, tool_name: str, artifact_name: str) -> str:
base_dir = self.tool_download_dir(tool_name)
return os.path.join(base_dir, artifact_name)
def tool_unpack_directory_path(self, tool_name: str, version: str) -> str:
return os.path.join(
self.cfg.state_base, "packages", tool_name, "versions", version
)
def tool_unpack_directory(self, tool_name: str, version: str) -> str:
dir = self.tool_unpack_directory_path(tool_name, version)
os.makedirs(dir, exist_ok=True)
return dir
def bin_directory(self) -> str:
dir = os.path.join(self.cfg.state_base, "bin")
os.makedirs(dir, exist_ok=True)
return dir
def tool_binary_path(self, tool_name: str) -> str:
bindir = self.bin_directory()
return os.path.join(bindir, tool_name)
def package_config_path(self) -> str:
return os.path.join(self.cfg.config_base, "packages.toml")
def unpack_archive(archive_path: str, destination_dir: str) -> bool:
"""Unpack a tar archive (or other archive type) into a directory.
Args:
archive_path: Path to the archive file (tar, tar.gz, tar.bz2, etc.)
destination_dir: Directory to extract files into
Returns:
True if extraction was successful, False otherwise
written by claude, modified by me
"""
import subprocess
import shutil
# Try unar first if available (supports many archive formats)
if shutil.which("unar"):
try:
subprocess.run(
[
"unar",
"-no-directory",
"-force-skip",
"-output-directory",
destination_dir,
archive_path,
],
check=True,
capture_output=True,
text=True,
)
return True
except subprocess.CalledProcessError as e:
print(f"unar failed: {e.stderr}")
return False
# Fall back to tar for tar-based archives
import tarfile
try:
with tarfile.open(archive_path, "r:*") as tar:
tar.extractall(path=destination_dir)
return True
except Exception as e:
print(f"Failed to extract archive: {e}")
return False
def unpack_artifact(
asset: GitReleaseAsset, tool: ToolConfig, storage: Storage
) -> bool | str:
file_path = storage.tool_download_artifact_path(tool.name, asset.name)
if not os.path.exists(file_path):
print(f"[ERROR] File {file_path} does not exist")
return False
unpack_dir = storage.tool_unpack_directory(tool.name, asset.name)
if not unpack_archive(file_path, unpack_dir):
print("[ERROR] Failed to extract archive")
return False
return unpack_dir
def download_artifact(asset: GitReleaseAsset, tool: ToolConfig, storage: Storage):
file_path = storage.tool_download_artifact_path(tool.name, asset.name)
if os.path.exists(file_path):
print(f"[DEBUG] File {file_path} already exists")
return file_path
status, _, stream = asset.download_asset(chunk_size=1024)
if status != 200:
print(f"Failed to download {asset.name}")
return None
with open(file_path, "wb") as f:
hasher = hashlib.sha256()
for chunk in stream:
if chunk:
hasher.update(chunk)
f.write(chunk)
actual_digest = hasher.hexdigest()
if actual_digest.lower() != asset.digest.replace("sha256:", "").lower():
print(f"SHA256 hash mismatch for {asset.name}")
os.remove(file_path)
return None
print(f"[DEBUG] downloaded {asset.name} to {file_path}")
return file_path
def symlink_tool(unpacked_path: str, tool: ToolConfig, cfg: Storage) -> bool:
source = os.path.join(unpacked_path, tool.binary_path)
if not os.path.exists(source):
print(f"Source binary {source} does not exist")
return False
target = cfg.tool_binary_path(tool.name)
if os.path.islink(target):
os.unlink(target)
if os.path.exists(target):
print(f"Target binary {target} already exists")
return False
os.symlink(source, target)
return True
def install_tool(gh: Github, tool: ToolConfig, storage: Storage):
release = gh.get_repo(tool.repo).get_latest_release()
print(f"latest release for {tool.name} is {release.name}")
for assets in release.get_assets():
if fnmatch.fnmatch(assets.name, tool.artifact_pattern):
fp = download_artifact(assets, tool, storage)
if fp is None:
print(f"Failed to download {assets.name}")
return
dp = unpack_artifact(assets, tool, storage)
if not dp:
print(f"failed unpacking {assets.name}")
return
if symlink_tool(dp, tool, storage):
print(f"{tool.name} version {release.name} installed successfully!")
else:
print(f"{tool.name} version {release.tag_} installation failed!")
return
def main() -> None:
global_config = GlobalConfig(
cache_base=BaseDirectory.save_cache_path("install-from-github"),
state_base=BaseDirectory.save_data_path("install-from-github"),
config_base=BaseDirectory.save_config_path("install-from-github"),
)
storage = Storage(global_config)
import tomlkit
packages = []
with open(storage.package_config_path(), "r") as f:
config = tomlkit.load(f)
for key in config:
if isinstance(config[key], tomlkit.items.Table):
cfg = ToolConfig(
name=key,
repo=config[key]["repo"],
artifact_pattern=config[key]["artifact_pattern"],
binary_path=config[key]["binary_path"],
)
packages.append(cfg)
if "github_api_token" in config:
token_auth = Auth.Token(config["github_api_token"])
gh = Github(auth=token_auth)
else:
gh = Github()
for package in packages:
install_tool(gh, package, storage)
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment