Created
June 27, 2025 15:19
-
-
Save MathiasSven/2e65bdb4a27a2be89d0ff502facaf613 to your computer and use it in GitHub Desktop.
A script to search for past hydra builds
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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