diff --git a/src/repror/cli/check_recipe.py b/src/repror/cli/check_recipe.py deleted file mode 100644 index 9d65e8d..0000000 --- a/src/repror/cli/check_recipe.py +++ /dev/null @@ -1,51 +0,0 @@ -import platform - -from repror.internals.rattler_build import rattler_build_hash -from repror.internals.db import get_latest_build_with_rebuild, Recipe -from rich.table import Table -from repror.internals.print import print - - -def check( - recipes: list[Recipe], -): - """ - Build recipes using rattler-build - """ - platform_name, platform_version = platform.system().lower(), platform.release() - rattler_hash = rattler_build_hash() - - info_table = Table("Platform", "Version", "Rattler Build Hash", title="Build Info") - info_table.add_row(platform_name, platform_version, rattler_hash) - print(info_table) - - status = {} - - recipes_to_find = [(recipe.name, recipe.content_hash) for recipe in recipes] - - latest_build_and_rebuild = get_latest_build_with_rebuild( - recipes_to_find, - rattler_hash, - platform_name, - platform_version, - ) - for recipe in recipes: - latest_build, latest_rebuild = latest_build_and_rebuild.get( - recipe.name, (None, None) - ) - - if not latest_build and not latest_rebuild: - status[recipe.name] = "No (not built yet)" - continue - - rebuild_hash = latest_rebuild.rebuild_hash if latest_rebuild else "" - status[recipe.name] = ( - "Yes" if latest_build and latest_build.build_hash == rebuild_hash else "No" - ) - - title = "Is the Recipe Repro?" if len(recipes) == 1 else "Are Recipes Repro?" - table = Table("Name", "Is Repro?", title=title) - for recipe, is_repro in status.items(): - table.add_row(recipe, is_repro) - - print(table) diff --git a/src/repror/cli/cli.py b/src/repror/cli/cli.py index 3327666..a1df4c8 100644 --- a/src/repror/cli/cli.py +++ b/src/repror/cli/cli.py @@ -11,6 +11,7 @@ from rich.live import Live from repror.internals.config import load_config +from repror.internals.db import get_rebuild_data from repror.internals.print import print from repror.internals import patch_database @@ -21,8 +22,7 @@ from . import generate_html as html from . import setup_rattler_build as setup from . import rebuild_recipe as rebuild -from . import check_recipe -from .utils import pixi_root_cli +from .utils import pixi_root_cli, platform_name, platform_version, reproducible_table from ..internals.options import global_options @@ -103,7 +103,8 @@ def build_recipe( recipes_to_build, Path(tmp_dir), force, patch, actions_url ) print("Verifying if rebuilds are reproducible...") - check_recipe.check(recipes_to_build) + builds = get_rebuild_data(recipe_names, platform_name(), platform_version()) + print(reproducible_table(builds)) @app.command() @@ -171,7 +172,11 @@ def setup_rattler_build(): @app.command() -def check(recipe_names: Annotated[Optional[list[str]], typer.Argument()] = None): - """Check if recipe name[s] is reproducible, by verifying it's build and rebuild hash.""" - recipes_to_check = build.recipes_for_names(recipe_names) - check_recipe.check(recipes_to_check) +def status( + recipe_names: Annotated[Optional[list[str]], typer.Argument()] = None, + platform: Annotated[str, typer.Option()] = platform.system().lower(), +): + """Check if recipe name[s] is reproducible for your platform, by verifying it's build and rebuild hash.""" + recipe_names = [recipe.name for recipe in build.recipes_for_names(recipe_names)] + builds = get_rebuild_data(recipe_names, platform) + print(reproducible_table(recipe_names, builds, platform)) diff --git a/src/repror/cli/generate_html.py b/src/repror/cli/generate_html.py index 73fa22e..319887b 100644 --- a/src/repror/cli/generate_html.py +++ b/src/repror/cli/generate_html.py @@ -10,7 +10,11 @@ from pathlib import Path from jinja2 import Environment, FileSystemLoader -from repror.internals.db import BuildState, get_rebuild_data, get_total_successful_builds_and_rebuilds +from repror.internals.db import ( + BuildState, + get_rebuild_data, + get_total_successful_builds_and_rebuilds, +) from repror.internals.git import github_api from repror.internals.print import print @@ -103,18 +107,22 @@ def rerender_html(root_folder: Path, update_remote: bool = False): timestamps = [end_of_day + timedelta(days=i) for i in range(0, 10)] by_platform = defaultdict(list) for build in builds: - # Do it in the loop so that we do this only once per platform # and we dont have to hardcode the platforms if build.platform_name not in counts_per_platform: - counts = [get_total_successful_builds_and_rebuilds(build.platform_name, before_time=time) for time in timestamps] + counts = [ + get_total_successful_builds_and_rebuilds( + build.platform_name, before_time=time + ) + for time in timestamps + ] counts_per_platform[build.platform_name] = { # Total successful builds "builds": [count.builds for count in counts], # Total reproducible builds "rebuilds": [count.rebuilds for count in counts], # Total builds = builds + failed builds - "total_builds": [count.total_builds for count in counts] + "total_builds": [count.total_builds for count in counts], } if build.state == BuildState.FAIL: diff --git a/src/repror/cli/utils.py b/src/repror/cli/utils.py index 3bc96a5..475859f 100644 --- a/src/repror/cli/utils.py +++ b/src/repror/cli/utils.py @@ -1,3 +1,4 @@ +import platform from rich.table import Table from rich.text import Text from ..internals.db import Build, Rebuild @@ -55,3 +56,55 @@ def rebuild_to_table(rebuild: Rebuild) -> Table: rebuild.actions_url, ) return table + + +def reproducible_table( + recipe_names: list[str], builds: list[Build], platform: str +) -> Table: + """Converts a a list of Build instance to a rich table that shows the reproducibility of the builds""" + cols = [ + "Name", + "Platform", + "Is Repro", + ] + table = Table( + *cols, title=f"Are we repro ? Total recipes in our queue: {len(recipe_names)}" + ) + + build_map = {build.recipe_name: build for build in builds} + + for name in recipe_names: + build_by_name = build_map.get(name) + if not build_by_name: + table.add_row(name, platform, "Not build yet") + continue + + if not build_by_name.rebuilds: + table.add_row( + build_by_name.recipe_name, + build_by_name.platform_name, + "Not rebuild yet", + ) + continue + + rebuild_hash = ( + build_by_name.rebuilds[-1].rebuild_hash if build_by_name.rebuilds else None + ) + is_same = build_by_name.build_hash == rebuild_hash if rebuild_hash else False + table.add_row( + build_by_name.recipe_name, + build_by_name.platform_name, + "Yes" if is_same else "No", + ) + + return table + + +def platform_name() -> str: + """Get the platform name.""" + return platform.system().lower() + + +def platform_version() -> str: + """Get the platform version.""" + return platform.release() diff --git a/src/repror/internals/config.py b/src/repror/internals/config.py index 6e02ebb..359472a 100644 --- a/src/repror/internals/config.py +++ b/src/repror/internals/config.py @@ -36,7 +36,9 @@ class RattlerBuildConfig(BaseModel): class ConfigYaml(BaseModel): repositories: list[RemoteRepository] - rattler_build: Optional[RattlerBuildConfig] = Field(alias="rattler-build", serialization_alias="rattler-build") + rattler_build: Optional[RattlerBuildConfig] = Field( + alias="rattler-build", serialization_alias="rattler-build" + ) local: list[LocalRecipe] diff --git a/src/repror/internals/db.py b/src/repror/internals/db.py index 7eb1e3c..5de606b 100644 --- a/src/repror/internals/db.py +++ b/src/repror/internals/db.py @@ -280,8 +280,11 @@ def save(build: Build | Rebuild | Recipe): session.commit() -# Function to query the database and return rebuild data -def get_rebuild_data() -> Sequence[Build]: +# Function to query the database and return latest rebuild data +def get_rebuild_data( + recipe_names: Optional[list[str]] = None, + platform: Optional[str] = None, +) -> Sequence[Build]: with get_session() as session: # Subquery to get the latest build per platform latest_build_subquery = ( @@ -290,6 +293,15 @@ def get_rebuild_data() -> Sequence[Build]: .group_by(Build.recipe_name) .order_by(col(Build.timestamp).desc()) ) + if platform: + latest_build_subquery = latest_build_subquery.where( + Build.platform_name == platform + ) + + if recipe_names: + latest_build_subquery = latest_build_subquery.where( + or_(col(Build.recipe_name).in_(recipe_names)) + ) # Main query to get the latest builds all_group_builds = session.exec(latest_build_subquery).all() @@ -359,12 +371,16 @@ def get_total_successful_builds_and_rebuilds( Rebuild.state == BuildState.SUCCESS, ) - total_builds_query = select(func.count(col(Build.id))).join(subquery, (col(Build.id) == subquery.c.id)) + total_builds_query = select(func.count(col(Build.id))).join( + subquery, (col(Build.id) == subquery.c.id) + ) # Execute the queries and count the results successful_builds_count = session.exec(successful_builds_query).one() successful_rebuilds_count = session.exec(successful_rebuilds_query).one() total_builds: int = session.exec(total_builds_query).one() return SuccessfulBuildsAndRebuilds( - successful_builds_count, successful_rebuilds_count, total_builds=total_builds + successful_builds_count, + successful_rebuilds_count, + total_builds=total_builds, )