From f5903b084e5d9ac6b271313b010f3cf66db6d3d0 Mon Sep 17 00:00:00 2001 From: Jan Staněk Date: Jun 07 2019 12:41:25 +0000 Subject: Refactor/rewrite nodejs.prov Adjust the nodejs.prov script to be more pythonic and easier to modify downstream (i.e. SCLs). - Remove global state - Better function separation (metadata formatting/metadata retrieval) - Replace ad-hoc directory traversal with os.walk() - Extract bits usually modified downstream into module-level "constants" Signed-off-by: Jan Staněk --- diff --git a/nodejs.prov b/nodejs.prov index f0ced15..bfea318 100755 --- a/nodejs.prov +++ b/nodejs.prov @@ -1,12 +1,8 @@ #!/usr/bin/python3 - -""" -Automatic provides generator for Node.js libraries. - -Taken from package.json. See `man npm-json` for details. -""" +# -*- coding: utf-8 -*- # Copyright 2012 T.C. Hollingsworth # Copyright 2017 Tomas Tomecek +# Copyright 2019 Jan Staněk # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to @@ -26,73 +22,100 @@ Taken from package.json. See `man npm-json` for details. # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. -from __future__ import print_function +"""Automatic provides generator for Node.js libraries. +Metadata taken from package.json. See `man npm-json` for details. +""" + +from __future__ import print_function, with_statement + +import json import os import sys -import json +from itertools import chain, groupby -provides = set() +DEPENDENCY_TEMPLATE = "npm(%(name)s) = %(version)s" +BUNDLED_TEMPLATE = "bundled(nodejs-%(name)s) = %(version)s" +NODE_MODULES = {"node_modules"} -def handle_package_json(path, bundled=False): - """ - process package.json file available on path, print RPM dependency based on name and version - """ - if not path.endswith('package.json') or not os.path.isfile(path): - return - fh = open(path) - metadata = json.load(fh) - fh.close() - - try: - if metadata['private']: - return - except KeyError: - pass - - try: - name = metadata["name"] - except KeyError: - return - try: - version = metadata["version"] - except KeyError: - return - - if bundled: - value = "bundled(nodejs-%s) = %s" % (name, version) - else: - value = "npm(%s) = %s" % (name, version) - provides.add(value) - - -def handle_module(path, bundled): - """ - process npm module and all its bundled dependencies + +class PrivatePackage(RuntimeError): + """Private package metadata that should not be listed.""" + + +#: Something is wrong with the ``package.json`` file +_INVALID_METADATA_FILE = (IOError, PrivatePackage, KeyError) + + +def format_metadata(metadata, bundled=False): + """Format ``package.json``-like metadata into RPM dependency. + + Arguments: + metadata (dict): Package metadata, presumably read from ``package.json``. + bundled (bool): Should the bundled dependency format be used? + + Returns: + str: RPM dependency (i.e. ``npm(example) = 1.0.0``) + + Raises: + KeyError: Expected key (i.e. ``name``, ``version``) missing in metadata. + PrivatePackage: The metadata indicate private (unlisted) package. """ - handle_package_json(path, bundled=bundled) - if not os.path.isdir(path): - path = os.path.dirname(path) - node_modules_dir_candidate = os.path.join(path, "node_modules") - if os.path.isdir(node_modules_dir_candidate): - for module_path in os.listdir(node_modules_dir_candidate): - module_abs_path = os.path.join(node_modules_dir_candidate, module_path) - # skip modules which are linked against system module - if not os.path.islink(module_abs_path): - p_json_file = os.path.join(module_abs_path, "package.json") - handle_module(p_json_file, bundled=True) - - -def main(): - """ read list of package.json paths from stdin """ - paths = [path.rstrip() for path in sys.stdin.readlines()] - - for path in paths: - handle_module(path, bundled=False) - - for provide in sorted(provides): - print(provide) + # Skip private packages + if metadata.get("private", False): + raise PrivatePackage(metadata) + + template = BUNDLED_TEMPLATE if bundled else DEPENDENCY_TEMPLATE + return template % metadata -if __name__ == '__main__': - main() + +def generate_dependencies(module_path, module_dir_set=NODE_MODULES): + """Generate RPM dependency for a module and all it's dependencies. + + Arguments: + module_path (str): Path to a module directory or it's ``package.json`` + module_dir_set (set): Base names of directories to look into + for bundled dependencies. + + Yields: + str: RPM dependency for the module and each of it's (public) bundled dependencies. + + Raises: + ValueError: module_path is not valid module or ``package.json`` file + """ + + # Determine paths to root module directory and package.json + if os.path.isdir(module_path): + root_dir = module_path + elif os.path.basename(module_path) == "package.json": + root_dir = os.path.dirname(module_path) + else: # Invalid metadata path + raise ValueError("Invalid module path '%s'" % module_path) + + for dir_path, subdir_list, __ in os.walk(root_dir): + # Currently in node_modules (or similar), continue to subdirs + if os.path.basename(dir_path) in module_dir_set: + continue + + # Read and format metadata + metadata_path = os.path.join(dir_path, "package.json") + bundled = dir_path != root_dir + try: + with open(metadata_path, mode="r") as metadata_file: + metadata = json.load(metadata_file) + yield format_metadata(metadata, bundled=bundled) + except _INVALID_METADATA_FILE: + pass # Ignore + + # Only visit subdirectories in module_dir_set + subdir_list[:] = list(module_dir_set & set(subdir_list)) + + +if __name__ == "__main__": + module_paths = (path.strip() for path in sys.stdin) + provides = chain.from_iterable(generate_dependencies(m) for m in module_paths) + + # sort|uniq + for provide, __ in groupby(sorted(provides)): + print(provide)