From 8f055fceadd439f81cb678ae6e23b09e99d50940 Mon Sep 17 00:00:00 2001 From: Ian Norden Date: Thu, 3 Oct 2019 16:31:47 -0500 Subject: [PATCH] script to resolve conflicts between core go.mod deps and plugin go.mod deps --- scripts/gomoderator.py | 137 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 scripts/gomoderator.py diff --git a/scripts/gomoderator.py b/scripts/gomoderator.py new file mode 100644 index 00000000..7cdde61d --- /dev/null +++ b/scripts/gomoderator.py @@ -0,0 +1,137 @@ +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")