diff --git a/CHANGES/209.bugfix b/CHANGES/209.bugfix new file mode 100644 index 00000000..18b10f37 --- /dev/null +++ b/CHANGES/209.bugfix @@ -0,0 +1 @@ +Fixed a bug where legacy gems would cause an exception in pulpcore-content. diff --git a/pulp_gem/specs.py b/pulp_gem/specs.py index e6f753e4..d0d9cd7e 100644 --- a/pulp_gem/specs.py +++ b/pulp_gem/specs.py @@ -1,4 +1,5 @@ from collections import namedtuple +from logging import getLogger import aiofiles import datetime @@ -14,6 +15,8 @@ import rubymarshal.reader +log = getLogger(__name__) + NAME_REGEX = re.compile(r"[\w\.-]+") VERSION_REGEX = re.compile(r"\d+(?:\.\d+)*") PRERELEASE_VERSION_REGEX = NAME_REGEX @@ -274,6 +277,41 @@ def yaml_constructor(cls, loader, node): result.attributes = {f"@{key}": value for key, value in values.items()} result.attributes["@type"] = rubymarshal.classes.Symbol(result.attributes["@type"][1:]) + @property + def requirement(self): + # From: + # File lib/rubygems/dependency.rb, line 116 + + # @version_requirements and @version_requirement are legacy ivar + # names, and supported here because older gems need to keep + # working and Dependency doesn't implement marshal_dump and + # marshal_load. In a happier world, this would be an + # attr_accessor. The horrifying instance_variable_get you see + # below is also the legacy of some old restructurings. + # + # Note also that because of backwards compatibility (loading new + # gems in an old RubyGems installation), we can't add explicit + # marshaling to this class until we want to make a big + # break. Maybe 2.0. + # + # Children, define explicit marshal and unmarshal behavior for + # public classes. Marshal formats are part of your public API. + + if result := self.attributes.get("@requirement"): + return result + if version_requirement := self.attributes.pop("@version_requirement", None): + log.warn( + "You are attempting to process a really old gem. " + "If this codepath fails, please report an issue with this gem " + "so we actually have something to experiment on." + ) + version = GemVersion() + version.marshal_load([version_requirement.attributes["@version"]]) + requirement = GemRequirement() + requirement._private_data[0] = [[">=", version]] + self.attributes["@version_requirements"] = requirement + return self.attributes.get("@version_requirements") + class RubyTime(rubymarshal.classes.UserDef): ruby_class_name = "Time" @@ -352,7 +390,7 @@ def analyse_gem(file_obj): gem_info[key] = requirement.to_s() if (dependencies := data._private_data.get("dependencies")) is not None: gem_info["dependencies"] = { - dep.attributes["@name"]: dep.attributes["@requirement"].to_s() for dep in dependencies + dep.attributes["@name"]: dep.requirement.to_s() for dep in dependencies } zdata = zlib.compress(rubymarshal.writer.writes(data)) return gem_info, zdata diff --git a/pulp_gem/tests/functional/api/test_pull_through.py b/pulp_gem/tests/functional/api/test_pull_through.py index 541894b3..4fc1cdd0 100644 --- a/pulp_gem/tests/functional/api/test_pull_through.py +++ b/pulp_gem/tests/functional/api/test_pull_through.py @@ -60,18 +60,27 @@ def test_pull_through_install( remote2 = gem_remote_factory() distribution2 = gem_distribution_factory(remote=remote2.pulp_href) - for dis, gem in ((distribution, "a"), (distribution2, "beryl")): - cmd = ["gem", "i", "--remote", "--clear-sources", "-s", dis.base_url, gem, "-v", "0.1.0"] + for dis, gem, version in ( + (distribution, "a", "0.1.0"), + (distribution2, "beryl", "0.1.0"), + (distribution, "ruby-hmac", "0.4.0"), + ): + cmd = ["gem", "i", "--remote", "--clear-sources", "-s", dis.base_url, gem, "-v", version] out = subprocess.run(cmd, stdout=subprocess.PIPE) - assert f"Successfully installed {gem}-0.1.0" in out.stdout.decode("utf-8") + assert f"Successfully installed {gem}-{version}" in out.stdout.decode("utf-8") - r = gem_content_api_client.list(name=gem, version="0.1.0") + r = gem_content_api_client.list(name=gem, version=version) assert r.count == 1 assert r.results[0].name == gem - assert r.results[0].version == "0.1.0" + assert r.results[0].version == version - subprocess.run(("gem", "uninstall", gem, "-v", "0.1.0")) + subprocess.run(("gem", "uninstall", gem, "-v", version)) content_after = gem_content_api_client.list().count - assert content_before + 2 == content_after + assert content_before + 3 == content_after + + hmac_content = gem_content_api_client.list(name="ruby-hmac", version="0.4.0").results[0] + assert hmac_content.dependencies["hoe"] == ">= 2.5.0" + assert hmac_content.dependencies["gemcutter"] == ">= 0.2.1" + assert hmac_content.dependencies["rubyforge"] == ">= 2.0.3"