Skip to content

Instantly share code, notes, and snippets.

@OscarL
Created May 5, 2023 12:56
Show Gist options
  • Save OscarL/92d79e4c82ae5690a655bceba214a9d3 to your computer and use it in GitHub Desktop.
Save OscarL/92d79e4c82ae5690a655bceba214a9d3 to your computer and use it in GitHub Desktop.
Proof of Concept for `pkgman search --not-required`
#!python3
"""
Proof of concept for `pkgman search --not-required`.
The idea is *very* simple.
- Get the list of installed packages.
- From that, builds a list of all unique requirements.
- Print the list of packages that do not provide any of those requirements.
This PoC uses assumes a few things, and takes a couple of shortcuts:
- Assumes installed packages are the ones directly under /system/packages/ and ~/config/packages
(no "activated_packages" or state_* dirs get considered).
- Depends on `ls *.hpkg`, and `package list -i <package_name>`.
- Version info in requirements/provides is ignored.
"""
from dataclasses import dataclass
from pprint import pprint
import hashlib
import os
import pickle
import subprocess
import sys
HELP_TEXT = """%s {-h,-v}
Prints a list of packages that are not dependencies of any other installed package.
-h Prints this helps and exits.
-v Be more verbose.
Note: The first run, or ones after you (un)install packages, will be slow (it takes aprox
40 second to process 354 packages on my PC).
Subsequent runs take half a second.
""" % os.path.basename(__file__)
@dataclass
class Package:
name: str
filename: str
provides: list
requires: list
def get_installed_package_files():
sys = subprocess.check_output('ls -1 /boot/system/packages/*.hpkg', shell=True, text=True).splitlines()
home = subprocess.check_output('ls -1 /boot/home/config/packages/*.hpkg', shell=True, text=True).splitlines()
return sys + home
def get_package_info(filename):
def drop_version(str):
for c in ['>', '<', '=']:
if c in str:
str = str.split(c)[0]
return str
lines = subprocess.check_output(['package', 'list', '-i', filename], text=True).splitlines()
lines = [l.strip() for l in lines]
provides = []
requires = []
name = None
for l in lines:
if l.startswith('name: '):
columns = [c.strip() for c in l.split(' ') if c.strip() != '']
name = columns[1]
elif l.startswith('provides: ') or l.startswith('requires: '):
columns = [c.strip() for c in l.split(' ') if c.strip() != '']
if l.startswith('provides: '):
provides.append(drop_version(columns[1]))
elif l.startswith('requires: '):
requires.append(drop_version(columns[1]))
return Package(name, filename, provides, requires)
def update_packages_info(package_files):
packages_info = dict()
for p in package_files:
package_info = get_package_info(p)
packages_info[package_info.name] = package_info
return packages_info
def get_reqs(packages):
requirements = set()
for p in packages:
for req in packages[p].requires:
requirements.add(req)
# print('Debug: len(requirements) = %s' % len(requirements))
# pprint(requirements)
required = set()
for p in packages:
for prov in packages[p].provides:
if prov in requirements:
required.add(p)
break
return required, set(packages) - required
def main(verbose):
package_files = get_installed_package_files()
packages_info = dict()
hash = hashlib.sha256()
hash.update(''.join(package_files).encode())
info_hash = hash.digest()
PACKAGE_INFO_CACHE_FILE="pinfo.pickle"
refresh_cache = False
try:
with open(PACKAGE_INFO_CACHE_FILE, 'rb') as f:
info_hash_cached, packages_info = pickle.load(f)
if info_hash_cached != info_hash:
print('Outdated cache... Will refresh info (slow).', file=sys.stderr)
refresh_cache = True
except:
print('Problems reading the cache file. Will refresh info (slow).', file=sys.stderr)
refresh_cache = True
if refresh_cache:
packages_info = update_packages_info(package_files)
with open(PACKAGE_INFO_CACHE_FILE, 'wb') as f:
pickle.dump([info_hash, packages_info], f, pickle.HIGHEST_PROTOCOL)
# print(packages_info['vim'])
pi = dict(packages_info)
required, not_required = get_reqs(pi)
num_nr = len(not_required)
if num_nr:
if verbose:
print('Packages not required by other installed packages:')
for n in sorted(not_required):
print(n)
if verbose:
print('Number of independent packages: %d (of a total of %d).' % (num_nr, len(required) + num_nr))
def help(exit):
print(HELP_TEXT)
if exit:
sys.exit(0)
if __name__ == '__main__':
if sys.argv[-1].startswith('-h'):
help(exit=True)
verbose=False
if sys.argv[-1].startswith('-v'):
verbose=True
main(verbose)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment