import os import sys import subprocess import errno from typing import List, Dict """ Resolves dependency conflicts between a plugin repository's and the core repository's go.mods Usage: python3 gomoderator.py {path_to_core_repository} {path_to_plugin_repository} """ ERROR_INVALID_NAME = 123 def is_pathname_valid(pathname: str) -> bool: """ `True` if the passed pathname is a valid pathname for the current OS; `False` otherwise. """ try: if not isinstance(pathname, str) or not pathname: return False _, pathname = os.path.splitdrive(pathname) root_dirname = os.environ.get('HOMEDRIVE', 'C:') \ if sys.platform == 'win32' else os.path.sep assert os.path.isdir(root_dirname) # ...Murphy and her ironclad Law root_dirname = root_dirname.rstrip(os.path.sep) + os.path.sep for pathname_part in pathname.split(os.path.sep): try: os.lstat(root_dirname + pathname_part) except OSError as exc: if hasattr(exc, 'winerror'): if exc.winerror == ERROR_INVALID_NAME: return False elif exc.errno in {errno.ENAMETOOLONG, errno.ERANGE}: return False except TypeError as exc: return False else: return True def map_deps_to_version(deps_arr: List[str]) -> Dict[str, str]: mapping = {} for d in deps_arr: if d.find(' => ') != -1: ds = d.split(' => ') d = ds[1] d = d.replace(" v", "[>v") # might be able to just split on the empty space not _v and skip this :: insertion d_and_v = d.split("[>") mapping[d_and_v[0]] = d_and_v[1] return mapping # argument checks assert len(sys.argv) == 3, "need core repository and plugin repository path arguments" core_repository_path = sys.argv[1] plugin_repository_path = sys.argv[2] assert is_pathname_valid(core_repository_path), "core repository path argument is not valid" assert is_pathname_valid(plugin_repository_path), "plugin repository path argument is not valid" # collect `go list -m all` output from both repositories; remain in the plugin repository os.chdir(core_repository_path) core_deps_b = subprocess.check_output(["go", "list", "-m", "all"]) os.chdir(plugin_repository_path) plugin_deps_b = subprocess.check_output(["go", "list", "-m", "all"]) core_deps = core_deps_b.decode("utf-8") core_deps_arr = core_deps.splitlines() del core_deps_arr[0] # first line is the project repo itself plugin_deps = plugin_deps_b.decode("utf-8") plugin_deps_arr = plugin_deps.splitlines() del plugin_deps_arr[0] core_deps_mapping = map_deps_to_version(core_deps_arr) plugin_deps_mapping = map_deps_to_version(plugin_deps_arr) # iterate over dependency maps for both repos and find version conflicts # attempt to resolve conflicts by adding adding a `require` for the core version to the plugin's go.mod file none = True for dep, core_version in core_deps_mapping.items(): if dep in plugin_deps_mapping.keys(): plugin_version = plugin_deps_mapping[dep] if core_version != plugin_version: print(f'{dep} has a conflict: core is using version {core_version} ' f'but the plugin is using version {plugin_version}') fixed_dep = f'{dep}@{core_version}' print(f'attempting fix by `go mod edit -require={fixed_dep}') subprocess.check_call(["go", "mod", "edit", f'-require={fixed_dep}']) none = False if none: print("no conflicts to resolve") quit() # the above process does not work for all dep conflicts e.g. golang.org/x/text v0.3.0 will not stick this way # so we will try the `go get {dep}` route for any remaining conflicts updated_plugin_deps_b = subprocess.check_output(["go", "list", "-m", "all"]) updated_plugin_deps = updated_plugin_deps_b.decode("utf-8") updated_plugin_deps_arr = updated_plugin_deps.splitlines() del updated_plugin_deps_arr[0] updated_plugin_deps_mapping = map_deps_to_version(updated_plugin_deps_arr) none = True for dep, core_version in core_deps_mapping.items(): if dep in updated_plugin_deps_mapping.keys(): updated_plugin_version = updated_plugin_deps_mapping[dep] if core_version != updated_plugin_version: print(f'{dep} still has a conflict: core is using version {core_version} ' f'but the plugin is using version {updated_plugin_version}') fixed_dep = f'{dep}@{core_version}' print(f'attempting fix by `go get {fixed_dep}') subprocess.check_call(["go", "get", fixed_dep]) none = False if none: print("all conflicts have been resolved") quit() # iterate over plugins `go list -m all` output one more time and inform whether or not the above has worked final_plugin_deps_b = subprocess.check_output(["go", "list", "-m", "all"]) final_plugin_deps = final_plugin_deps_b.decode("utf-8") final_plugin_deps_arr = final_plugin_deps.splitlines() del final_plugin_deps_arr[0] final_plugin_deps_mapping = map_deps_to_version(final_plugin_deps_arr) none = True for dep, core_version in core_deps_mapping.items(): if dep in final_plugin_deps_mapping.keys(): final_plugin_version = final_plugin_deps_mapping[dep] if core_version != final_plugin_version: print(f'{dep} STILL has a conflict: core is using version {core_version} ' f'but the plugin is using version {final_plugin_version}') none = False if none: print("all conflicts have been resolved") quit() print("failed to resolve all conflicts")