import re import os min_version_format_rx = re.compile(r"^>=\s*(?P[A-Za-z0-9\.\-]+)(\+[A-Za-z0-9\.\-\+]*)?\s*$") semver_rx = re.compile(r"^(?P[A-Za-z0-9\.\-]+)(\+[A-Za-z0-9\.\-\+]*)?$") def use_nuget_versioning(enable_debug_log: bool = False): if NugetStyleRangeResolver.using_nuget_versioning: return NugetStyleRangeResolver.using_nuget_versioning = True NugetStyleRangeResolver.enable_debug_log = enable_debug_log NugetStyleRangeResolver.override_conan1() class NugetStyleRangeResolver: replaced = False using_nuget_versioning = False enable_debug_log = False @staticmethod def debug_log(msg): if NugetStyleRangeResolver.enable_debug_log: print(f"[nuget versioning] {msg}") @staticmethod def info_log(msg): print(f"[nuget versioning] {msg}") @staticmethod def override_conan1(): # https://github.com/conan-io/conan/blob/1.66.0/conans/model/requires.py # https://github.com/conan-io/conan/tree/1.66.0/conans/client/graph # https://github.com/conan-io/conan/blob/1.66.0/conans/client/graph/graph_builder.py # https://github.com/conan-io/conan/blob/1.66.0/conans/client/graph/range_resolver.py if NugetStyleRangeResolver.replaced: return NugetStyleRangeResolver.replaced = True from conans.errors import ConanException from conans.client.graph.range_resolver import satisfying, RangeResolver, _parse_versionexpr from conans.search.search import search_recipes from semver import make_range, semver as make_semver, InvalidTypeIncluded, Range def min_satisfying(versions, range_, loose=False, include_prerelease=False): try: range_ob = make_range(range_, loose=loose) except InvalidTypeIncluded: raise except ValueError as e: return None min_ = None min_sv = None for v in versions: if range_ob.test(v, include_prerelease=include_prerelease): # satisfies(v, range_, loose=loose) sv = make_semver(v, loose=loose) if min_ is None or sv.compare(min_sv) == -1: # compare(max, v, true) min_ = v min_sv = sv return min_ def satisfying(list_versions, versionexpr:str, result): from semver import SemVer, Range, max_satisfying import re original_versionexpr = versionexpr select_max = False if versionexpr.startswith("+"): versionexpr = versionexpr[1:] select_max = True else: # minimum version selection doesn't care about upper bounds, so force it to true include_prerelease = True # fix upper bounds including prereleases versionexpr = re.sub(r"(\<)(\d(\.\d)*)(\s|,|$)", r"\1\2-\4", versionexpr) version_range, loose, include_prerelease = _parse_versionexpr(versionexpr, result) NugetStyleRangeResolver.debug_log(f"{original_versionexpr} => {versionexpr}") # Check version range expression try: act_range = Range(version_range, loose) except ValueError: raise ConanException("version range expression '%s' is not valid" % version_range) # Validate all versions candidates = {} for v in list_versions: try: NugetStyleRangeResolver.debug_log(f"\t{v}") ver = SemVer(v, loose=loose) candidates[ver] = v except (ValueError, AttributeError): result.append("WARN: Version '%s' is not semver, cannot be compared with a range" % str(v)) # Search best matching version in range result = None if select_max: result = max_satisfying(candidates, act_range, loose=loose, include_prerelease=include_prerelease) else: result = min_satisfying(candidates, act_range, loose=loose, include_prerelease=True) NugetStyleRangeResolver.debug_log(f"={result}") return candidates.get(result) def _resolve_local(self, search_ref, version_range): local_found = search_recipes(self._cache, search_ref) local_found = \ [ref for ref in local_found if ref.user == search_ref.user and ref.channel == search_ref.channel] if local_found: ret = self._resolve_version(version_range, local_found) range_match = min_version_format_rx.search(version_range) if ret is not None and range_match is not None: min_version = range_match.group("min_version") version_match = semver_rx.search(ret.version) ret_version = version_match.group("version") is_online = os.getenv("INSEINC_OFFLINE") != "1" if is_online and min_version != ret_version: NugetStyleRangeResolver.info_log(f"{ret}: Expected to find version {min_version} via {version_range}, forcing update") return None return ret og_conflicting_references = None @staticmethod def _conflicting_references(previous, new_ref, consumer_ref=None): try: if previous.ref.copy_clear_rev() != new_ref.copy_clear_rev(): if consumer_ref: new_ref_semver = make_semver(new_ref.version, loose=True) previous_semver = make_semver(previous.ref.version, loose=True) latest_version = None if new_ref_semver.compare(previous_semver) == -1: latest_version = previous.ref else: latest_version = new_ref return ("Conflict in %s:\n" " %s\n required from %s\n" " %s\n required from %s\n" "To resolve the conflict, upgrade the dependency with:\n" " self.requires(\"%s\", override=True)" % (consumer_ref, new_ref, consumer_ref, previous.ref, next(iter(previous.dependants)).src, latest_version)) return "Unresolvable conflict between {} and {}".format(previous.ref, new_ref) except: return og_conflicting_references(previous, new_ref, consumer_ref) # now replace the functions import importlib graph_builder = importlib.import_module("conans.client.graph.graph_builder") og_conflicting_references = graph_builder.DepsGraphBuilder._conflicting_references graph_builder.DepsGraphBuilder._conflicting_references = _conflicting_references range_resolver = importlib.import_module("conans.client.graph.range_resolver") range_resolver.satisfying = satisfying range_resolver.RangeResolver._resolve_local = _resolve_local importlib.reload(importlib.import_module("conans.model.graph_lock")) # TODO: this? importlib.reload(importlib.import_module("conans.client.graph.graph_builder")) # TODO: this? @staticmethod def override_conan2(): from conans.client.graph.range_resolver import RangeResolver @staticmethod def _resolve_version(version_range, refs_found, resolve_prereleases): print(f"replaced function: {version_range}") for ref in sorted(refs_found): if version_range.contains(ref.version, resolve_prereleases): return ref RangeResolver._resolve_version = _resolve_version