feat: config.ini

This commit is contained in:
relikd
2025-09-02 20:22:01 +02:00
parent 5c20a65345
commit 810f8efd72

130
brew.py
View File

@@ -21,6 +21,7 @@ from io import StringIO # Log summary
from tarfile import TarInfo, open as openTarfile from tarfile import TarInfo, open as openTarfile
from urllib import request as Req # build_opener, install_opener, urlretrieve from urllib import request as Req # build_opener, install_opener, urlretrieve
from urllib.error import HTTPError from urllib.error import HTTPError
from configparser import ConfigParser as IniFile
from webbrowser import open as launchBrowser from webbrowser import open as launchBrowser
from functools import cached_property from functools import cached_property
from argparse import ( from argparse import (
@@ -36,11 +37,7 @@ from typing import (
class Env: class Env:
IS_TTY = sys.stdout.isatty() IS_TTY = sys.stdout.isatty()
MAX_AGE_CACHE = 5 * 24 * 60 * 60 # 5 days
MAX_AGE_DOWNLOAD = int(os.environ.get('BREW_PY_CLEANUP_MAX_AGE_DAYS', 21))
CELLAR_PATH = os.environ.get('BREW_PY_CELLAR', '').rstrip('/') CELLAR_PATH = os.environ.get('BREW_PY_CELLAR', '').rstrip('/')
LINK_BINARIES = os.environ.get('BREW_PY_LINK_BINARIES', '1').lower() in (
'true', '1', 'yes', 'y', 'on')
def main() -> None: def main() -> None:
@@ -506,8 +503,7 @@ def cli_cleanup(args: ArgParams) -> None:
''' '''
Remove old versions of installed packages. Remove old versions of installed packages.
If arguments are specified, only do this for the given packages. If arguments are specified, only do this for the given packages.
Removes all downloads more than 21 days old. Removes all downloads older than 21 days (see config.ini).
This can be adjusted with $BREW_PY_CLEANUP_MAX_AGE_DAYS.
''' '''
total_savings = 0 total_savings = 0
packages = Cellar.infoAll(args.packages, assertInstalled=True) packages = Cellar.infoAll(args.packages, assertInstalled=True)
@@ -517,10 +513,7 @@ def cli_cleanup(args: ArgParams) -> None:
if not args.packages: if not args.packages:
Log.info('==> Removing cached downloads') Log.info('==> Removing cached downloads')
maxage = Env.MAX_AGE_DOWNLOAD if args.prune is None else args.prune total_savings += Cellar.cleanup(args.prune, dryRun=args.dry)
for file in os.scandir(Cellar.DOWNLOAD):
if File.isOutdated(file.path, maxage * 24 * 60 * 60):
total_savings += File.remove(file.path, dryRun=args.dry)
# remove all non-active versions # remove all non-active versions
Log.info('==> Removing old versions') Log.info('==> Removing old versions')
@@ -668,8 +661,8 @@ def parseArgs() -> ArgParams:
cmd.arg_bool('--no-dependencies', help='Do not install dependencies') cmd.arg_bool('--no-dependencies', help='Do not install dependencies')
cmd.arg_bool('--skip-link', help='Install but skip linking to opt') cmd.arg_bool('--skip-link', help='Install but skip linking to opt')
cmd.arg('--binaries', action=BooleanOptionalAction, help=''' cmd.arg('--binaries', action=BooleanOptionalAction, help='''
Enable/disable linking of helper executables (default: enabled). Enable/disable linking of helper executables (default: enabled,
Can be set with $BREW_PY_LINK_BINARIES.''') see config.ini)''')
cmd.arg('-arch', help='''Manually set platform architecture cmd.arg('-arch', help='''Manually set platform architecture
(e.g. 'arm64_sequoia' (brew), 'arm64|darwin|macOS 15' (ghcr))''') (e.g. 'arm64_sequoia' (brew), 'arm64|darwin|macOS 15' (ghcr))''')
@@ -726,7 +719,7 @@ def parseArgs() -> ArgParams:
cmd = cli.subcommand('cleanup', cli_cleanup, aliases=['clean']) cmd = cli.subcommand('cleanup', cli_cleanup, aliases=['clean'])
cmd.arg('packages', nargs='*', help='Brew package name') cmd.arg('packages', nargs='*', help='Brew package name')
cmd.arg('--prune', type=int, help=''' cmd.arg('--prune', type=int, help='''
Remove all cache files older than specified days''') Remove all cache files and downloads older than specified days''')
cmd.arg_bool('-n', '--dry-run', dest='dry', help=''' cmd.arg_bool('-n', '--dry-run', dest='dry', help='''
Show what would be removed, but do not actually remove anything''') Show what would be removed, but do not actually remove anything''')
@@ -851,6 +844,63 @@ class Arch:
return currentVer >= [int(x) for x in version.split('.')] return currentVer >= [int(x) for x in version.split('.')]
# -----------------------------------
# Config
# -----------------------------------
class Config:
class Install(NamedTuple):
LINK_BIN_PRIM: bool
LINK_BIN_DEPS: bool
class Cleanup(NamedTuple):
DOWNLOAD: int
CACHE: int
AUTH: int
INSTALL: Install
CLEANUP: Cleanup
@staticmethod
def load(fname: str) -> None:
if not os.path.exists(fname):
with open(fname, 'w') as fp:
fp.write('''
[install]
; whether install should link binaries of main package (user-installed)
link_bin_primary = yes ; default: yes
; whether install should link binaries of dependencies
link_bin_dependency = yes ; default: yes
[cleanup]
; unit: s|m|h|d (secs, mins, hours, days)
download = 21d ; default: 21d
cache = 5d ; default: 5d
auth = 365d ; default: 365d
''')
ini = IniFile(inline_comment_prefixes=(';', '#'))
ini.read(fname)
def timed(value: str) -> int:
unit = value[-1].lower()
mul = {'s': 1, 'm': 60, 'h': 60 * 60, 'd': 24 * 60 * 60}.get(unit)
if not mul:
raise AttributeError(f'Unkown time unit "{value}" in config')
return int(value[:-1]) * mul
sec = ini['install']
Config.INSTALL = Config.Install(
LINK_BIN_PRIM=sec.getboolean('link_bin_primary', fallback=True),
LINK_BIN_DEPS=sec.getboolean('link_bin_dependency', fallback=True),
)
sec = ini['cleanup']
Config.CLEANUP = Config.Cleanup(
DOWNLOAD=timed(sec.get('download', '21d')),
CACHE=timed(sec.get('cache', '5d')),
AUTH=timed(sec.get('auth', 'never')),
)
# ----------------------------------- # -----------------------------------
# TreeDict # TreeDict
# ----------------------------------- # -----------------------------------
@@ -1434,17 +1484,38 @@ class Cellar:
for x in (Cellar.BIN, Cellar.CACHE, Cellar.CELLAR, Cellar.DOWNLOAD, for x in (Cellar.BIN, Cellar.CACHE, Cellar.CELLAR, Cellar.DOWNLOAD,
Cellar.OPT): Cellar.OPT):
os.makedirs(x, exist_ok=True) os.makedirs(x, exist_ok=True)
Cellar.cleanup(Env.MAX_AGE_CACHE)
Config.load(os.path.join(Cellar.ROOT, 'config.ini')) # after makedirs
Cellar.cleanup(quiet=True) # after Config.load()
@staticmethod @staticmethod
def cleanup(maxage: int) -> None: def cleanup(
''' Check all files in cache and delete outdated. ''' maxAgeDays: 'int|None' = None, *,
dryRun: bool = False, quiet: bool = False,
) -> int:
''' Delete outdated files in cache and download. '''
savings = 0
if maxAgeDays is not None:
maxAgeDays *= 24 * 60 * 60 # days
if maxAgeDays == 0:
maxAgeDays = 1
for file in os.scandir(Cellar.CACHE): for file in os.scandir(Cellar.CACHE):
# TODO: different maxage for auth-token?
if file.name == '_auth-token.json': if file.name == '_auth-token.json':
continue # TODO: test how long the token is valid # TODO: test how long the token is valid
maxage = Config.CLEANUP.AUTH
else:
maxage = maxAgeDays or Config.CLEANUP.CACHE
if File.isOutdated(file.path, maxage): if File.isOutdated(file.path, maxage):
os.remove(file.path) savings += File.remove(file.path, dryRun=dryRun, quiet=quiet)
maxage = maxAgeDays or Config.CLEANUP.DOWNLOAD
for file in os.scandir(Cellar.DOWNLOAD):
if File.isOutdated(file.path, maxage):
savings += File.remove(file.path, dryRun=dryRun, quiet=quiet)
return savings
# Paths # Paths
@@ -1690,14 +1761,27 @@ class InstallQueue:
total = len(self.installQueue) total = len(self.installQueue)
Log.beginCounter(total) Log.beginCounter(total)
Log.beginErrorSummary() Log.beginErrorSummary()
# flags
linkPrim = Config.INSTALL.LINK_BIN_PRIM if linkExe is None else linkExe
linkDeps = Config.INSTALL.LINK_BIN_DEPS if linkExe is None else linkExe
# reverse to install main package last (allow re-install until success) # reverse to install main package last (allow re-install until success)
for i, tar in enumerate(reversed(self.installQueue), 1): for i, tar in enumerate(reversed(self.installQueue), 1):
bundle = TarPackage(tar).extract(dryRun=self.dryRun) bundle = TarPackage(tar).extract(dryRun=self.dryRun)
if bundle and not self.dryRun: if bundle:
isPrimary = i == total
linkBin = linkPrim if isPrimary else linkDeps
if self.dryRun:
if not linkBin:
Log.info('will NOT link binaries')
continue
# post-install stuff # post-install stuff
pkg = LocalPackage(bundle.package) pkg = LocalPackage(bundle.package)
if not isUpgrade: if not isUpgrade:
pkg.setPrimary(i == total) pkg.setPrimary(isPrimary)
vpkg = pkg.version(bundle.version) vpkg = pkg.version(bundle.version)
vpkg.setDigest(File.sha256(tar)) vpkg.setDigest(File.sha256(tar))
@@ -1713,9 +1797,9 @@ class InstallQueue:
pkg.name, vpkg.version), summary=True) pkg.name, vpkg.version), summary=True)
continue continue
withBin = Env.LINK_BINARIES if linkExe is None else linkExe
pkg.unlink() pkg.unlink()
vpkg.link(linkBin=withBin) vpkg.link(linkBin=linkBin)
Log.endCounter() Log.endCounter()
Log.dumpErrorSummary() Log.dumpErrorSummary()