Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions conan/internal/api/export.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,21 @@ def cmd_export(app, hook_manager, global_conf, conanfile_path, name, version, us
conanfile.display_name = str(ref)
conanfile.output.scope = conanfile.display_name
scoped_output = conanfile.output
# Even though the package_id_non_embed_mode is minor_mode by default,
# recipes with buggy versions that do not define the attribute will have
# the same problem regardless
if not isinstance(ref.version.major.value, int) and ref.version.minor is not None:
for mode in ("package_id_embed_mode", "package_id_non_embed_mode", "package_id_unknown_mode"):
if getattr(conanfile, mode, None) in ("patch_mode", "minor_mode"):
scoped_output.warning(f"{mode} is set to '{getattr(conanfile, mode)}', but the version '{ref.version}' contains "
f"an alphanumeric major '{ref.version.major}'.\n"
f"This is highly discouraged due to unexpected package ID calculation risks. "
f"Either a different version scheme should be used (e.g., semantic versioning), "
f"or the package_id modes should be changed (e.g 'full_mode').\n"
f"Refer to the documentation for more details: "
f"TODO",
warn_tag="risk")
break

recipe_layout = cache.create_export_recipe_layout(ref)

Expand Down
8 changes: 8 additions & 0 deletions conan/internal/model/info.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,15 @@ def stable(self):
return self.major()

def major(self):
# This check is to avoid breaking non-integer major versions
# for legacy reasons. Users are warned against using them
if not isinstance(self._version.major.value, int):
return str(self._version.major)
return ".".join([str(self._version.major), 'Y', 'Z'])

def minor(self):
# This check is to avoid breaking non-integer major versions
# for legacy reasons. Users are warned against using them
if not isinstance(self._version.major.value, int):
return str(self._version.major)

Expand All @@ -33,6 +37,8 @@ def minor(self):
return ".".join([v0, v1, 'Z'])

def patch(self):
# This check is to avoid breaking non-integer major versions
# for legacy reasons. Users are warned against using them
if not isinstance(self._version.major.value, int):
return str(self._version.major)

Expand All @@ -42,6 +48,8 @@ def patch(self):
return ".".join([v0, v1, v2])

def pre(self):
# This check is to avoid breaking non-integer major versions
# for legacy reasons. Users are warned against using them
if not isinstance(self._version.major.value, int):
return str(self._version.major)

Expand Down
15 changes: 15 additions & 0 deletions test/integration/command/export/export_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -438,3 +438,18 @@ def test_export_json():
info = json.loads(c.stdout)
assert info["reference"] == "foo/0.1#4d670581ccb765839f2239cc8dff8fbd"
assert len(info) == 1 # Only "reference" key yet


@pytest.mark.parametrize("mode", ["patch_mode", "minor_mode", "major_mode", None])
@pytest.mark.parametrize("mode_attr", ["package_id_embed_mode", "package_id_non_embed_mode", "package_id_unknown_mode"])
def test_export_alphanumeric_major(mode, mode_attr):
c = TestClient(light=True)
conanfile = GenConanfile("pkg")
if mode:
conanfile = conanfile.with_class_attribute(f'{mode_attr} = "{mode}"')
c.save({"conanfile.py": conanfile})
c.run("export . --version=v1.3")
if mode in ("patch_mode", "minor_mode"):
assert f"{mode_attr} is set to '{mode}'" in c.out
c.run("export . --version=v1")
assert f"This is not a recommended practice" not in c.out
Loading