nixpkgs-tools/treewidePrePostPhase/fix_hooks.py

170 lines
5.8 KiB
Python
Executable File

#! /usr/bin/env nix-shell
#! nix-shell -i python3 -p python3 nix git
import itertools
import json
import os
import regex
from collections import defaultdict
from pathlib import Path
from textwrap import dedent
phase_hooks = [
('buildPhase', 'preBuild', 'postBuild'),
('checkInstallPhase', 'preCheckInstall', 'postCheckInstall'),
('checkPhase', 'preCheck', 'postCheck'),
('configurePhase', 'preConfigure', 'postConfigure'),
('distPhase', 'preDist', 'postDist'),
('installPhase', 'preInstall', 'postInstall'),
('patchPhase', 'prePatch', 'postPatch'),
('unpackPhase', 'preUnpack', 'postUnpack'),
]
def provide_paths(rootdir):
for parent_dir, dirs, files in os.walk(rootdir):
for basename in files:
if not basename.endswith('.nix'):
continue
path = Path(parent_dir, basename)
try:
with open(path) as file:
pass
yield path
except FileNotFoundError as e:
pass
class AbortEditException(Exception):
pass
def try_to_fix_file_runhooks(path, dryrun) -> list[str]:
with open(path) as file:
raw_content = file.read()
edited_hooks = []
for phase_hook in phase_hooks:
if (edit := insert_phase_if_not_exists_multiline(path, raw_content, *phase_hook)) is not None:
edited_hooks.append(phase_hook[0])
if not dryrun:
with open(path, 'w') as file:
file.write(edit)
raw_content = edit
if (edit := insert_phase_if_not_exists_singleline(path, raw_content, *phase_hook)) is not None:
edited_hooks.append(phase_hook[0])
if not dryrun:
with open(path, 'w') as file:
file.write(edit)
raw_content = edit
return edited_hooks
def insert_phase_if_not_exists_multiline(path, text, phase, pre_phase, post_phase) -> str | None:
pattern = regex.compile(f"{phase} ?= ?''(.|\n)+?'';", regex.MULTILINE)
match = regex.search(pattern, text)
if match is None or 'runHook' in match.group():
return None
inner_lines = match.group().split('\n')
indent = ''.join(
itertools.takewhile(
lambda x: x == ' ',
next(line for line in inner_lines[1:-1] if line.strip() != '')
)
)
replacement ='\n'.join(
inner_lines[0:1]
+ [indent + f'runHook {pre_phase}', '']
+ inner_lines[1:-1]
+ ['', indent + f'runHook {post_phase}']
+ inner_lines[-1:]
)
result = text[0:match.start()] + replacement + text[match.end():]
# Sanity check for string concatenations with optional string concats,
# and (TODO) contained string interpolations which contain "'';"
if regex.search(r"''(?!\$)", '\n'.join(inner_lines[1:-1])) is not None:
colorizedResult = text[0:match.start()] + '\u001b[32m' + replacement + '\u001b[0m' + text[match.end():]
print('-----\n' + colorizedResult + '\n------')
if input(f"Accept {path} ({phase})? [N/y] ") not in 'Yy':
print(f"Rejecting {path} ({phase})")
raise AbortEditException
print()
return result
def insert_phase_if_not_exists_singleline(path, text, phase, pre_phase, post_phase) -> str | None:
pattern = regex.compile(f"{phase} ?= ?\"(.+?)\";")
match = regex.search(pattern, text)
if match is None:
return None
base_indent_num = ''.join(reversed(text[:match.start()])).find('\n')
phase_content = [
f"{(base_indent_num + 2) * ' '}runHook {pre_phase}",
"",
f"{(base_indent_num + 2) * ' '}{match.group(1)}",
"",
f"{(base_indent_num + 2) * ' '}runHook {post_phase}",
]
replacement = '\n'.join(
[ f"{phase} = ''" ]
+ phase_content
+ [ f"{base_indent_num * ' '}'';" ]
)
result = text[0:match.start()] + replacement + text[match.end():]
return result
def build_packages_path_dict(path_to_nixpkgs_root) -> dict[str, list[str]]:
print("Generating package index...")
os.system(f'nix-env -f "{path_to_nixpkgs_root}" -I nixpkgs="{path_to_nixpkgs_root}" -qa --meta --json --show-trace --arg config "import {path_to_nixpkgs_root}/pkgs/top-level/packages-config.nix" >/tmp/packages.json')
with open("/tmp/packages.json") as file:
packages_json = json.load(file)
result = defaultdict(list)
for key, package in packages_json.items():
if 'position' in package['meta']:
path = Path(package['meta']['position'].split(':')[0]).resolve()
result[path].append(key)
return dict(result)
def stage_and_commit_changes(package_path_dict, path, edited_hooks, dryrun):
package_names = package_path_dict.get(path.resolve(), [])
if len(package_names) == 0:
while input(dedent(f"""
Could not find package name for {path}.
Please commit manually and press y to continue:
""")) not in 'Yy':
pass
elif len(package_names) == 1:
package_name = package_names[0]
print(f'[GIT COMMIT] {package_name}: added missing `runHooks` for {", ".join(edited_hooks)}')
else:
# TODO: cat the file and let user choose a number
# if package count below threshold (including abort option)
# else ask user to commit manually
input(f'{path} contains many packages: {", ".join(package_names)}, press anything to continue...')
if __name__ == '__main__':
dryrun = True
package_path_dict = build_packages_path_dict("../..")
for path in provide_paths('.'):
try:
edited_hooks = try_to_fix_file_runhooks(path, dryrun)
if len(edited_hooks) > 0:
stage_and_commit_changes(package_path_dict, path, edited_hooks, dryrun)
except AbortEditException:
pass