Skip to content

Instantly share code, notes, and snippets.

@MathiasSven
Created June 27, 2025 15:19
Show Gist options
  • Save MathiasSven/2e65bdb4a27a2be89d0ff502facaf613 to your computer and use it in GitHub Desktop.
Save MathiasSven/2e65bdb4a27a2be89d0ff502facaf613 to your computer and use it in GitHub Desktop.
A script to search for past hydra builds
#!/usr/bin/env nix-shell
#!nix-shell -i python3 -p python3Packages.requests python3Packages.beautifulsoup4 smenu
import argparse
from collections.abc import Sequence
from dataclasses import dataclass
from functools import partial
from subprocess import PIPE, CalledProcessError, run
from sys import exit
from typing import cast
import requests
from bs4 import BeautifulSoup
from bs4.element import Tag
@dataclass
class BuildList:
page: int
results: Sequence[tuple[str, str, str, str]]
showing: range
max_build: int
def get_pkg_builds(pkg_path: str, page: int = 1) -> BuildList:
URL = f"https://hydra.nixos.org/job/nixos/trunk-combined/nixpkgs.{pkg_path}.x86_64-linux/all"
res = requests.get(URL, params={"page": page})
if res.status_code != 200:
print("Is Hydra down? Request was unsuccessful")
exit(1)
soup = BeautifulSoup(res.content, "html.parser")
table = cast(Tag, soup.find("table"))
showing_l, showing_h, n_builds = tuple(
int(n) for n in table.fetchPrevious("p")[0].text.split() if n.isdecimal()
)
data = [
tuple(col.text for col in cols[1:5])
for row in table.findChildren("tr")[1:]
if (cols := row.findChildren("td"))[0].img.get("title") == "Succeeded"
]
return BuildList(page, data, range(showing_l, showing_h + 1), n_builds)
def smenu(bl: BuildList) -> str:
title = (
"Select Build:"
"\n\n"
"Showing succefull builds from"
f" {bl.showing.start}-{bl.showing.stop - 1} out of {bl.max_build}"
)
# fmt: off
command = [
"smenu",
"-M",
"-n", "25",
"-1", "^[[:alpha:]]", "3/0,bu",
"-d",
"-a", "m:0/6",
"-2", "^.*",
"-W", "\n",
"-l",
"-m", title
]
# fmt: on
controls = []
if bl.max_build > 20:
controls.append("Goto")
if 1 not in bl.showing:
controls.append("Previous")
if bl.max_build not in bl.showing:
controls.append("Next")
input_string = "\n".join(
[f"{build} {date} {pname}" for build, date, pname, system in bl.results]
+ controls
)
try:
command_result = run(
command, input=input_string.encode(), check=True, stdout=PIPE
)
return command_result.stdout.decode().strip().split()[0]
except (CalledProcessError, IndexError):
exit(1)
def goto_page_prompt(bl: BuildList) -> int:
pages = -(-bl.max_build // 20)
while (selected := input(f"Enter page number [1-{pages}]: ")) is not None:
print("\033[A\033[K\033[A") # Move cursor back
if selected.isdecimal() and int(selected) in range(1, pages + 1):
return int(selected)
assert False, "unreachable"
def main():
parser = argparse.ArgumentParser(
description="Search Hydra for built versions of a package"
)
parser.add_argument(
"pkgPath", metavar="pkgPath", type=str, help="Nixpkgs path for the derivation"
)
args = parser.parse_args()
get_builds = partial(get_pkg_builds, args.pkgPath)
build_list = get_builds(page=1)
if not build_list.results:
print(f'Could not find "{args.pkgPath}"')
exit(1)
while True:
match smenu(build_list):
case "Goto":
build_list = get_builds(goto_page_prompt(build_list))
case "Previous":
build_list = get_builds(build_list.page - 1)
case "Next":
build_list = get_builds(build_list.page + 1)
case build_id:
res = requests.get(
f"https://hydra.nixos.org/build/{build_id}#tabs-buildinputs"
)
soup = BeautifulSoup(res.content, "html.parser")
checkout, rev = (
arg.text.strip()
for arg in soup.select("#tabs-buildinputs table tbody td")[2:4]
)
print()
print(f"Git Checkout: {checkout}")
print(f"Rev: {rev}")
print(f"Archive: {checkout[:-4]}/archive/{rev}.tar.gz")
print()
print(f"Shell: nix-shell -p {args.pkgPath} -I nixpkgs={checkout[:-4]}/archive/{rev}.tar.gz")
break
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
print("\b\b\033[K")
exit(1)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment