138 lines
5.5 KiB
Python
138 lines
5.5 KiB
Python
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")
|