From 822135e2ee39d24d088c9b89453a1807ec5a81ee Mon Sep 17 00:00:00 2001 From: relikd Date: Mon, 1 Sep 2025 14:05:18 +0200 Subject: [PATCH] feat: pin + unpin --- brew.py | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 55 insertions(+), 7 deletions(-) diff --git a/brew.py b/brew.py index e50108d..6ae1510 100755 --- a/brew.py +++ b/brew.py @@ -189,6 +189,8 @@ def cli_list(args: ArgParams) -> None: infos = Cellar.infoAll(args.packages) if args.multiple: infos = [x for x in infos if len(x.verAll) > 1] + if args.pinned: + infos = [x for x in infos if x.pinned] if not infos: Log.main('no package found.') return @@ -407,6 +409,24 @@ def cli_switch(args: ArgParams) -> None: Log.warn('no binary links found. Skipped for new version as well.') +# https://docs.brew.sh/Manpage#pin-installed_formula- +def cli_pin(args: ArgParams) -> None: + ''' Prevent specified packages from being upgraded. ''' + Cellar.assertAllInstalled(args.packages) + for pkg in args.packages: + if Cellar.pinPackage(pkg): + Log.info('pinned', pkg) + + +# https://docs.brew.sh/Manpage#unpin-installed_formula- +def cli_unpin(args: ArgParams) -> None: + ''' Allow specified packages to be upgraded. ''' + Cellar.assertAllInstalled(args.packages) + for pkg in args.packages: + if Cellar.pinPackage(pkg, False): + Log.info('unpinned', pkg) + + # https://docs.brew.sh/Manpage#cleanup-options-formulacask- def cli_cleanup(args: ArgParams) -> None: ''' @@ -509,6 +529,8 @@ def parseArgs() -> ArgParams: This is the default when output is not to a terminal.''') cmd.arg_bool('--multiple', help=''' Only show packages with multiple versions installed''') + cmd.arg_bool('--pinned', help=''' + List only pinned packages. See also pin, unpin.''') # deps cmd = cli.subcommand('deps', cli_deps) @@ -600,6 +622,14 @@ def parseArgs() -> ArgParams: cmd.arg('package', help='Brew package name') cmd.arg('version', nargs='?', help='Package version') # convenience omit + # pin + cmd = cli.subcommand('pin', cli_pin) + cmd.arg('packages', nargs='+', help='Brew package name') + + # unpin + cmd = cli.subcommand('unpin', cli_unpin) + cmd.arg('packages', nargs='+', help='Brew package name') + # cleanup cmd = cli.subcommand('cleanup', cli_cleanup) cmd.arg('packages', nargs='*', help='Brew package name') @@ -1038,6 +1068,8 @@ class Cellar: if File.isOutdated(file.path, maxage): os.remove(file.path) + # Paths + @staticmethod def downloadPath(pkg: str, version: str) -> str: ''' Returns `@/download/-.tar.gz` ''' @@ -1067,6 +1099,7 @@ class Cellar: class PackageInfo(NamedTuple): package: str installed: bool + pinned: bool verActive: Optional[str] verInactive: list[str] verAll: list[str] @@ -1079,6 +1112,7 @@ class Cellar: inactive = [] available = [] pkgPath = Cellar.installPath(pkg) + isPinned = os.path.exists(os.path.join(pkgPath, '.pinned')) if os.path.isdir(pkgPath): for ver in sorted(os.listdir(pkgPath)): if os.path.isdir(os.path.join(pkgPath, ver, '.brew')): @@ -1086,7 +1120,7 @@ class Cellar: if ver != active: inactive.append(ver) return Cellar.PackageInfo( - pkg, len(available) > 0, active, inactive, available) + pkg, len(available) > 0, isPinned, active, inactive, available) @staticmethod def infoAll(filterPkg: list[str] = []) -> list[PackageInfo]: @@ -1094,6 +1128,26 @@ class Cellar: pkgs = filterPkg if filterPkg else sorted(os.listdir(Cellar.CELLAR)) return [info for pkg in pkgs if (info := Cellar.info(pkg)).installed] + @staticmethod + def pinPackage(pkg: str, pin: bool = True) -> bool: + pkgPath = Cellar.installPath(pkg) + assert os.path.isdir(pkgPath), 'Package must be installed to (un-)pin' + pin_file = os.path.join(pkgPath, '.pinned') + changed = pin ^ os.path.exists(pin_file) + if pin: + File.touch(pin_file) + elif os.path.exists(pin_file): + os.remove(pin_file) + return changed + + @staticmethod + def assertAllInstalled(pkgs: list[str], msg: str = 'unknown package:') \ + -> None: + ''' Print any non-installed package and exit with status code 1 ''' + if unknownPkgs := [x for x in pkgs if not Cellar.info(x).installed]: + Log.error(msg, ', '.join(unknownPkgs)) + exit(1) + @staticmethod def getDependencyTree() -> DependencyTree: ''' Returns dict object for dependency traversal ''' @@ -2409,12 +2463,6 @@ if __name__ == '__main__': # Show formulae with an updated version available # https://docs.brew.sh/Manpage#outdated-options-formulacask- -# Prevent the specified formulae from being upgraded -# https://docs.brew.sh/Manpage#pin-installed_formula- ???? - -# Allow the specified formulae to be upgraded. -# https://docs.brew.sh/Manpage#unpin-installed_formula- ???? - # https://docs.brew.sh/Manpage#reinstall-options-formulacask- # List all the current tapped repositories (taps)