#!/usr/bin/python3 import gi import git import os import sys import logging from logging import error, info from gi.repository import GLib gi.require_version("Modulemd", "2.0") from gi.repository import Modulemd # noqa logging.getLogger().setLevel(logging.INFO) def do_validate(filename, obsoletes=False): # Valid filenames must end in ".yaml" to be properly included by Pungi if not filename.endswith(".yaml"): error( "{} does not end with .yaml. It will not be included by " "Pungi. If this file does not contain defaults, it should " "be added to the tests/exclusions.txt file.".format(filename) ) return False, None # The files must parse correctly idx = Modulemd.ModuleIndex() try: (objects, failures) = idx.update_from_file(filename, True) except GLib.Error as e: error("{}".format(e.message)) return False, None if failures: for failure in failures: error( "Failed subdocument ({}): \n{}\n".format( failure.get_gerror().message, failure.get_yaml() ) ) return False, None # There must be exactly one object per file names = idx.get_module_names() if len(names) != 1: error("There must be exactly one module represented by this file") return False, None # The files must have exactly one Modulemd object module = idx.get_module(names[0]) if obsoletes: mmds = module.get_obsoletes() if not len(mmds) == 1: error( "There must be exactly one module obsoletes document for {}".format( module.props.module_name ) ) return False, None mmd = mmds[0] else: mmd = module.get_defaults() if mmd is None: error( "No defaults document provided for {}".format( module.props.module_name ) ) return False, None # The files must not contain any streams if len(module.get_stream_names()) != 0: error("File included a stream document.") return False, None # Filenames must match their contents expected_name = os.path.basename(filename).rsplit(".", maxsplit=1)[0] if obsoletes: if ":" not in expected_name: error( "Module obsoletes filename must be in the form of " ":.yaml" ) return False, None expected_module_name, expected_stream = expected_name.rsplit(":", maxsplit=1) if (expected_module_name != mmd.props.module_name or expected_stream != mmd.props.module_stream): error( 'Module name/stream "{}:{}" doesn\'t match filename "{}.yaml"'.format( mmd.props.module_name, mmd.props.module_stream, expected_name ) ) return False, None elif expected_name != mmd.props.module_name: error( 'Module name "{}" doesn\'t match filename "{}.yaml"'.format( mmd.props.module_name, expected_name ) ) return False, None if obsoletes: is_valid = mmd.validate() if is_valid: info("{} obsoletes are valid".format(filename)) else: error("{} obsoletes are NOT valid".format(filename)) return (is_valid, mmd) default_stream = mmd.get_default_stream() if default_stream: # Default streams must also appear in the profiles list if mmd.get_default_profiles_for_stream(default_stream) is None: error( "Stream '{}' is missing from the profiles for '{}'".format( default_stream, mmd.get_module_name() ) ) return False, None # Modules in Fedora must not specify "Intents" # TODO: This needs a new interface exposed in libmodulemd v2 info("{} defaults are valid".format(filename)) return (True, idx) def main(): result = os.EX_OK if len(sys.argv) > 1 and sys.argv[1]: script_dir = os.path.abspath(sys.argv[1]) else: script_dir = os.path.dirname(os.path.realpath(__file__)) defaults_dir = os.path.abspath(os.path.join(script_dir, '..')) overrides_dir = os.path.join(defaults_dir, 'overrides') obsoletes_dir = os.path.abspath(os.path.join(script_dir, '..', 'obsoletes')) # Get the repo we're running in repo = git.Repo(defaults_dir, search_parent_directories=True) # Get the list of excluded files exclusions = [] with open(os.path.join(script_dir, "exclusions.txt")) as f: for line in f: stripped_line = line.strip() if not stripped_line or stripped_line.startswith("#"): # Ignore comments and empty lines continue exclusions.append(line.strip()) # Get the list of files in this repository while ignoring excluded ones. defaults_files = list() obsoletes_files = list() obsoletes_test_files = list() for (x, y) in repo.index.entries.keys(): for excl in exclusions: if x.startswith(excl): break else: if x.startswith('obsoletes/'): obsoletes_files.append(x) elif x.startswith('tests/obsoletes/'): obsoletes_test_files.append(x) else: defaults_files.append(x) print("\nValidation of modulemd documents:\n" "=================================") if defaults_files: print("\nModule defaults:\n" "----------------") # Validate module defaults files for file in defaults_files: (valid, _) = do_validate(file) if not valid: error("{} failed to validate".format(file)) result = os.EX_DATAERR if obsoletes_test_files: print("\nModule obsoletes (testing):\n" "---------------------------") # Validate module obsoletes test files for file in obsoletes_test_files: (valid, _) = do_validate(file, obsoletes=True) if not valid: error("{} failed to validate".format(file)) result = os.EX_DATAERR if result == os.EX_DATAERR: return result print("\nDefault streams (Runtime):\n" "==========================") # For sanity's sake, also do a merge of all the defaults to make sure no # conflicts arise that weren't detected by the above tests. This should be # impossible. try: idx_runtime = Modulemd.ModuleIndex() idx_runtime.update_from_defaults_directory(path=defaults_dir, strict=True) except GLib.Error as e: error("Could not merge all defaults: {}".format(e.message)) result = os.EX_DATAERR if result == os.EX_OK: info("Merging all of the documents encountered no errors.") for m, s in idx_runtime.get_default_streams().items(): print("{}:{}".format(m, s)) print("\nDefault streams (Buildroot):\n" "============================") try: idx_buildroot = Modulemd.ModuleIndex() idx_buildroot.update_from_defaults_directory(path=defaults_dir, overrides_path=overrides_dir, strict=True) except GLib.Error as e: error("Could not merge all defaults: {}".format(e.message)) result = os.EX_DATAERR if result == os.EX_OK: info("Merging all of the documents encountered no errors.") for m, s in idx_buildroot.get_default_streams().items(): print("{}:{}".format(m, s)) print("\nObsoletes:\n" "==========") try: idx_obsoletes = Modulemd.ModuleIndex() idx_obsoletes.update_from_defaults_directory(path=obsoletes_dir, strict=True) # Try merging module obsoletes into the module obsoletes index. for file in obsoletes_files: (_, mmd) = do_validate(file, obsoletes=True) if not mmd: result = os.EX_DATAERR break idx_obsoletes.add_obsoletes(mmd) except GLib.Error as e: error("Could not merge all obsoletes: {}".format(e.message)) result = os.EX_DATAERR if result == os.EX_OK: info("Merging all of the documents encountered no errors.") for module_name in idx_obsoletes.get_module_names(): module = idx_obsoletes.get_module(module_name) obsoletes = module.get_obsoletes() if not obsoletes: error("Module {} has no obsoletes.".format(module_name)) result = os.EX_DATAERR break return result if __name__ == "__main__": sys.exit(main())