Repository: conan-io/conan
Pull request #18166 (2025-04-23 13:49:08)

Add URL Preview for Package Uploads
Changelog: Feature: Add URL Preview for Package Uploads json output format
Docs: https://github.com/conan-io/docs/pull/4088
Close: https://github.com/conan-io/conan/issues/4935


Patch:
diff --git a/conan/api/subapi/upload.py b/conan/api/subapi/upload.py
index f336d89ee..727ae63c5 100644
--- a/conan/api/subapi/upload.py
+++ b/conan/api/subapi/upload.py
@@ -3,6 +3,7 @@ import time
 from multiprocessing.pool import ThreadPool
 
 from conan.api.output import ConanOutput
+from conan.internal.api.upload import add_urls
 from conan.internal.conan_app import ConanApp
 from conan.internal.api.uploader import PackagePreparator, UploadExecutor, UploadUpstreamChecker, \
     gather_metadata
@@ -99,6 +100,7 @@ class UploadAPI:
             thread_pool.join()
         elapsed = time.time() - t
         ConanOutput().success(f"Upload completed in {int(elapsed)}s\n")
+        add_urls(package_list, remote)
 
     def upload_backup_sources(self, files):
         config = self.conan_api.config.global_conf
diff --git a/conan/internal/api/upload.py b/conan/internal/api/upload.py
new file mode 100644
index 000000000..0093c083a
--- /dev/null
+++ b/conan/internal/api/upload.py
@@ -0,0 +1,16 @@
+from conan.internal.rest.client_routes import ClientV2Router
+from conan.internal.util.files import sha1sum
+
+
+def add_urls(package_list, remote):
+    router = ClientV2Router(remote.url.rstrip("/"))
+    for ref, bundle in package_list.refs().items():
+        for f, fp in bundle.get("files", {}).items():
+            bundle.setdefault("upload-urls", {})[f] = {
+                'url': router.recipe_file(ref, f), 'checksum': sha1sum(fp)
+            }
+        for pref, prev_bundle in package_list.prefs(ref, bundle).items():
+            for f, fp in prev_bundle.get("files", {}).items():
+                prev_bundle.setdefault("upload-urls", {})[f] = {
+                    'url': router.package_file(pref, f), 'checksum': sha1sum(fp)
+                }
diff --git a/conan/tools/cmake/cmakedeps2/cmakedeps.py b/conan/tools/cmake/cmakedeps2/cmakedeps.py
index d6b33b8cc..78358b574 100644
--- a/conan/tools/cmake/cmakedeps2/cmakedeps.py
+++ b/conan/tools/cmake/cmakedeps2/cmakedeps.py
@@ -4,7 +4,7 @@ import textwrap
 
 from jinja2 import Template
 
-from conan.api.output import Color, ConanOutput
+from conan.api.output import Color
 from conan.errors import ConanException
 from conan.internal import check_duplicated_generator
 from conan.internal.api.install.generators import relativize_path
@@ -59,12 +59,6 @@ class CMakeDeps2:
             cmake_find_mode = cmake_find_mode.lower()
             if cmake_find_mode == FIND_MODE_NONE:
                 continue
-            if cmake_find_mode == FIND_MODE_MODULE:
-                ConanOutput(self._conanfile.ref).warning("CMakeConfigDeps does not support "
-                                                         f"module find mode in {dep}.\n"
-                                                         f"Config mode will be used regardless.",
-                                                         # Should this be risk?
-                                                         warn_tag="deprecated")
 
             if require.direct:
                 direct_deps.append((require, dep))
@@ -250,8 +244,7 @@ class _PathGenerator:
                     f = self._cmakedeps.get_cmake_filename(dep)
                     for filename in (f"{f}-config.cmake", f"{f}Config.cmake"):
                         if os.path.isfile(os.path.join(pkg_folder, filename)):
-                            pkg_paths[pkg_name] = relativize_path(pkg_folder, self._conanfile,
-                                                                  "${CMAKE_CURRENT_LIST_DIR}")
+                            pkg_paths[pkg_name] = pkg_folder
                 continue
 
             # If CMakeDeps generated, the folder is this one
@@ -299,7 +292,6 @@ class _PathGenerator:
             runtime_dirs = aggregated_cppinfo.bindirs if is_win else aggregated_cppinfo.libdirs
             for d in runtime_dirs:
                 d = d.replace("\\", "/")
-                d = relativize_path(d, self._conanfile, "${CMAKE_CURRENT_LIST_DIR}")
                 existing = host_runtime_dirs.setdefault(config, [])
                 if d not in existing:
                     existing.append(d)
diff --git a/conan/tools/cmake/cmakedeps2/config.py b/conan/tools/cmake/cmakedeps2/config.py
index fe7be4a00..f31f3e67d 100644
--- a/conan/tools/cmake/cmakedeps2/config.py
+++ b/conan/tools/cmake/cmakedeps2/config.py
@@ -3,8 +3,6 @@ import textwrap
 import jinja2
 from jinja2 import Template
 
-from conan.internal.api.install.generators import relativize_path
-
 
 class ConfigTemplate2:
     """
@@ -35,9 +33,6 @@ class ConfigTemplate2:
         # FIXME: Proper escaping of paths for CMake and relativization
         # FIXME: build_module_paths coming from last config only
         build_modules_paths = [f.replace("\\", "/") for f in build_modules_paths]
-        build_modules_paths = [relativize_path(p, self._cmakedeps._conanfile,
-                                               "${CMAKE_CURRENT_LIST_DIR}")
-                               for p in build_modules_paths]
         components = self._cmakedeps.get_property("cmake_components", self._conanfile,
                                                   check_type=list)
         if components is None:  # Lets compute the default components names
diff --git a/conan/tools/cmake/cmakedeps2/target_configuration.py b/conan/tools/cmake/cmakedeps2/target_configuration.py
index b377d5121..6eead0d31 100644
--- a/conan/tools/cmake/cmakedeps2/target_configuration.py
+++ b/conan/tools/cmake/cmakedeps2/target_configuration.py
@@ -5,7 +5,6 @@ import jinja2
 from jinja2 import Template
 
 from conan.errors import ConanException
-from conan.internal.api.install.generators import relativize_path
 from conan.internal.model.pkg_type import PackageType
 from conan.internal.graph.graph import CONTEXT_BUILD, CONTEXT_HOST
 
@@ -121,17 +120,13 @@ class TargetConfigurationTemplate2:
         include_dirs = definitions = libraries = None
         if not self._require.build:  # To add global variables for try_compile and legacy
             aggregated_cppinfo = cpp_info.aggregated_components()
-            # FIXME: Proper escaping of paths for CMake
-            incdirs = [i.replace("\\", "/") for i in aggregated_cppinfo.includedirs]
-            incdirs = [relativize_path(i, self._cmakedeps._conanfile, "${CMAKE_CURRENT_LIST_DIR}")
-                       for i in incdirs]
-            include_dirs = ";".join(incdirs)
+            # FIXME: Proper escaping of paths for CMake and relativization
+            include_dirs = ";".join(i.replace("\\", "/") for i in aggregated_cppinfo.includedirs)
             definitions = ""
             root_target_name = self._cmakedeps.get_property("cmake_target_name", self._conanfile)
             libraries = root_target_name or f"{pkg_name}::{pkg_name}"
 
-        pkg_folder = relativize_path(pkg_folder, self._cmakedeps._conanfile,
-                                     "${CMAKE_CURRENT_LIST_DIR}")
+        # TODO: Missing find_modes
         dependencies = self._get_dependencies()
         return {"dependencies": dependencies,
                 "pkg_folder": pkg_folder,
diff --git a/conan/tools/microsoft/subsystems.py b/conan/tools/microsoft/subsystems.py
index 0a44ae00f..b8a57a714 100644
--- a/conan/tools/microsoft/subsystems.py
+++ b/conan/tools/microsoft/subsystems.py
@@ -10,5 +10,5 @@ def unix_path_package_info_legacy(conanfile, path, path_flavor=None):
     message = "The use of 'unix_path_legacy_compat' is deprecated in Conan 2.0 and does not " \
               "perform path conversions. This is retained for compatibility with Conan 1.x " \
               "and will be removed in a future version."
-    conanfile.output.warning(message, warn_tag="deprecated")
+    conanfile.output.warning(message)
     return path


Test patch:
diff --git a/test/integration/command/upload/upload_test.py b/test/integration/command/upload/upload_test.py
index a80610f4c..7b0bbaa82 100644
--- a/test/integration/command/upload/upload_test.py
+++ b/test/integration/command/upload/upload_test.py
@@ -16,7 +16,7 @@ from conan.api.model import RecipeReference
 from conan.internal.paths import EXPORT_SOURCES_TGZ_NAME, PACKAGE_TGZ_NAME
 from conan.test.utils.tools import NO_SETTINGS_PACKAGE_ID, TestClient, TestServer, \
     GenConanfile, TestRequester, TestingResponse
-from conan.internal.util.files import gzopen_without_timestamps, is_dirty, save, set_dirty
+from conan.internal.util.files import gzopen_without_timestamps, is_dirty, save, set_dirty, sha1sum
 
 conanfile = """from conan import ConanFile
 from conan.tools.files import copy
@@ -542,4 +542,66 @@ def test_upload_json_output():
     list_pkgs = json.loads(c.stdout)
     revs = list_pkgs["default"]["liba/0.1"]["revisions"]["a565bd5defd3a99e157698fcc6e23b25"]
     pkg = revs["packages"]["9e0f8140f0fe6b967392f8d5da9881e232e05ff8"]
+    prev = pkg["revisions"]["f50f552c6e04b1f241e5f7864bc3957f"]
     assert pkg["info"] == {"settings": {"os": "Linux"}, "options": {"shared": "False"}}
+    assert revs["upload-urls"] == {
+        "conanfile.py": {
+            "url": f"{c.servers['default']}/v2/conans/liba/0.1/_/_/revisions/a565bd5defd3a99e157698fcc6e23b25/files/conanfile.py",
+            "checksum": sha1sum(revs["files"]["conanfile.py"])
+        },
+        "conanmanifest.txt": {
+            "url": f"{c.servers['default']}/v2/conans/liba/0.1/_/_/revisions/a565bd5defd3a99e157698fcc6e23b25/files/conanmanifest.txt",
+            "checksum": sha1sum(revs["files"]["conanmanifest.txt"])
+        }
+    }
+    assert prev["upload-urls"] == {
+        "conan_package.tgz": {
+            "url": f"{c.servers['default']}/v2/conans/liba/0.1/_/_/revisions/a565bd5defd3a99e157698fcc6e23b25/packages/9e0f8140f0fe6b967392f8d5da9881e232e05ff8/revisions/f50f552c6e04b1f241e5f7864bc3957f/files/conan_package.tgz",
+            "checksum": sha1sum(prev["files"]["conan_package.tgz"])
+        },
+        "conaninfo.txt": {
+            "url": f"{c.servers['default']}/v2/conans/liba/0.1/_/_/revisions/a565bd5defd3a99e157698fcc6e23b25/packages/9e0f8140f0fe6b967392f8d5da9881e232e05ff8/revisions/f50f552c6e04b1f241e5f7864bc3957f/files/conaninfo.txt",
+            "checksum": sha1sum(prev["files"]["conaninfo.txt"])
+        },
+        "conanmanifest.txt": {
+            "url": f"{c.servers['default']}/v2/conans/liba/0.1/_/_/revisions/a565bd5defd3a99e157698fcc6e23b25/packages/9e0f8140f0fe6b967392f8d5da9881e232e05ff8/revisions/f50f552c6e04b1f241e5f7864bc3957f/files/conanmanifest.txt",
+            "checksum": sha1sum(prev["files"]["conanmanifest.txt"])
+        }
+    }
+
+
+def test_upload_dry_run_json_output():
+    c = TestClient(default_server_user=True)
+    c.save({"conanfile.py": GenConanfile("liba", "0.1").with_settings("os")
+                                                       .with_shared_option(False)})
+    c.run("create . -s os=Linux")
+    c.run("upload * -r=default -c --dry-run --format=json")
+    list_pkgs = json.loads(c.stdout)
+    revs = list_pkgs["default"]["liba/0.1"]["revisions"]["a565bd5defd3a99e157698fcc6e23b25"]
+    pkg = revs["packages"]["9e0f8140f0fe6b967392f8d5da9881e232e05ff8"]
+    prev = pkg["revisions"]["f50f552c6e04b1f241e5f7864bc3957f"]
+    assert pkg["info"] == {"settings": {"os": "Linux"}, "options": {"shared": "False"}}
+    assert revs["upload-urls"] == {
+        "conanfile.py": {
+            "url": f"{c.servers['default']}/v2/conans/liba/0.1/_/_/revisions/a565bd5defd3a99e157698fcc6e23b25/files/conanfile.py",
+            "checksum": sha1sum(revs["files"]["conanfile.py"])
+        },
+        "conanmanifest.txt": {
+            "url": f"{c.servers['default']}/v2/conans/liba/0.1/_/_/revisions/a565bd5defd3a99e157698fcc6e23b25/files/conanmanifest.txt",
+            "checksum": sha1sum(revs["files"]["conanmanifest.txt"])
+        }
+    }
+    assert prev["upload-urls"] == {
+        "conan_package.tgz": {
+            "url": f"{c.servers['default']}/v2/conans/liba/0.1/_/_/revisions/a565bd5defd3a99e157698fcc6e23b25/packages/9e0f8140f0fe6b967392f8d5da9881e232e05ff8/revisions/f50f552c6e04b1f241e5f7864bc3957f/files/conan_package.tgz",
+            "checksum": sha1sum(prev["files"]["conan_package.tgz"])
+        },
+        "conaninfo.txt": {
+            "url": f"{c.servers['default']}/v2/conans/liba/0.1/_/_/revisions/a565bd5defd3a99e157698fcc6e23b25/packages/9e0f8140f0fe6b967392f8d5da9881e232e05ff8/revisions/f50f552c6e04b1f241e5f7864bc3957f/files/conaninfo.txt",
+            "checksum": sha1sum(prev["files"]["conaninfo.txt"])
+        },
+        "conanmanifest.txt": {
+            "url": f"{c.servers['default']}/v2/conans/liba/0.1/_/_/revisions/a565bd5defd3a99e157698fcc6e23b25/packages/9e0f8140f0fe6b967392f8d5da9881e232e05ff8/revisions/f50f552c6e04b1f241e5f7864bc3957f/files/conanmanifest.txt",
+            "checksum": sha1sum(prev["files"]["conanmanifest.txt"])
+        }
+    }
diff --git a/test/integration/toolchains/cmake/cmakedeps2/test_cmakedeps.py b/test/integration/toolchains/cmake/cmakedeps2/test_cmakedeps.py
index 30228b704..7c9583d87 100644
--- a/test/integration/toolchains/cmake/cmakedeps2/test_cmakedeps.py
+++ b/test/integration/toolchains/cmake/cmakedeps2/test_cmakedeps.py
@@ -1,8 +1,6 @@
 import re
 import textwrap
 
-import pytest
-
 from conan.test.assets.genconanfile import GenConanfile
 from conan.test.utils.tools import TestClient
 
@@ -32,6 +30,9 @@ def test_cmakedeps_direct_deps_paths():
             requires = "lib/1.0"
             settings = "os", "arch", "compiler", "build_type"
             generators = "CMakeDeps"
+            def build(self):
+                cmake = CMake(self)
+                cmake.configure()
     """)
     c.save({"conanfile.py": conanfile}, clean_first=True)
     c.run(f"install . -c tools.cmake.cmakedeps:new={new_value}")
@@ -75,81 +76,25 @@ def test_cmakedeps_transitive_paths():
     c.run("create .")
     conanfile = textwrap.dedent(f"""
         from conan import ConanFile
+        from conan.tools.cmake import CMake
         class PkgConan(ConanFile):
             requires = "libb/1.0"
             settings = "os", "arch", "compiler", "build_type"
             generators = "CMakeDeps"
+            def build(self):
+                cmake = CMake(self)
+                cmake.configure()
     """)
     c.save({"conanfile.py": conanfile}, clean_first=True)
     c.run(f"install . -c tools.cmake.cmakedeps:new={new_value}")
     cmake_paths = c.load("conan_cmakedeps_paths.cmake")
+    cmake_paths.replace("\\", "/")
     assert re.search(r"list\(PREPEND CMAKE_PROGRAM_PATH \".*/libb.*/p/binb\"\)", cmake_paths)
     assert not re.search(r"list\(PREPEND CMAKE_PROGRAM_PATH /bina\"", cmake_paths)
     assert re.search(r"list\(PREPEND CMAKE_LIBRARY_PATH \".*/libb.*/p/libb\" \".*/liba.*/p/liba\"\)", cmake_paths)
     assert re.search(r"list\(PREPEND CMAKE_INCLUDE_PATH \".*/libb.*/p/includeb\" \".*/liba.*/p/includea\"\)", cmake_paths)
 
 
-def test_cmakedeps_deployer_relative_paths():
-    c = TestClient()
-    conanfile = textwrap.dedent("""
-        import os
-        from conan.tools.files import copy
-        from conan import ConanFile
-        class TestConan(ConanFile):
-            name = "liba"
-            version = "1.0"
-
-            def package_info(self):
-                self.cpp_info.includedirs = ["includea"]
-                self.cpp_info.libdirs = ["bina"]
-                self.cpp_info.bindirs = ["bina"]
-                crypto_module = os.path.join("share", "cmake", "crypto.cmake")
-                self.cpp_info.set_property("cmake_build_modules", [crypto_module])
-    """)
-    c.save({"conanfile.py": conanfile})
-    c.run("create .")
-
-    conanfile_cmake = textwrap.dedent("""
-        import os
-        from conan.tools.files import save
-        from conan import ConanFile
-        class TestConan(ConanFile):
-            name = "libb"
-            version = "1.0"
-
-            def package(self):
-                save(self, os.path.join(self.package_folder, "libb-config.cmake"), "")
-            def package_info(self):
-                self.cpp_info.set_property("cmake_find_mode", "none")
-        """)
-
-    c.save({"conanfile.py": conanfile_cmake})
-    c.run("create .")
-    conanfile = textwrap.dedent(f"""
-        from conan import ConanFile
-        from conan.tools.cmake import CMake
-        class PkgConan(ConanFile):
-            requires = "liba/1.0", "libb/1.0"
-            settings = "os", "arch", "compiler", "build_type"
-            generators = "CMakeDeps"
-    """)
-    c.save({"conanfile.py": conanfile}, clean_first=True)
-
-    # Now with a deployment
-    c.run(f"install . -c tools.cmake.cmakedeps:new={new_value} --deployer=full_deploy")
-    cmake_paths = c.load("conan_cmakedeps_paths.cmake")
-    assert 'set(libb_DIR "${CMAKE_CURRENT_LIST_DIR}/full_deploy/host/libb/1.0")' in cmake_paths
-    assert ('set(CONAN_RUNTIME_LIB_DIRS "$<$<CONFIG:Release>:${CMAKE_CURRENT_LIST_DIR}'
-            '/full_deploy/host/liba/1.0/bina>"') in cmake_paths
-    liba_config = c.load("liba-config.cmake")
-    assert ('include("${CMAKE_CURRENT_LIST_DIR}/full_deploy/'
-            'host/liba/1.0/share/cmake/crypto.cmake")') in liba_config
-    liba_targets = c.load("liba-Targets-release.cmake")
-    assert ('set(liba_PACKAGE_FOLDER_RELEASE "${CMAKE_CURRENT_LIST_DIR}/full_deploy/'
-            'host/liba/1.0")') in liba_targets
-    assert ('set(liba_INCLUDE_DIRS "${CMAKE_CURRENT_LIST_DIR}/full_deploy/'
-            'host/liba/1.0/includea" )') in liba_targets
-
 def test_cmakeconfigdeps_recipe():
     c = TestClient()
     conanfile = textwrap.dedent("""
@@ -340,22 +285,3 @@ def test_consuming_cpp_info_transitively_by_requiring_root_component_in_another_
             "main/test_package/conanfile.py":test_package})
     c.run("create ./dependent/ --name=dependent --version=0.1 -c tools.cmake.cmakedeps:new=will_break_next")
     c.run("create ./main/ --name=pkg --version=0.1 -c tools.cmake.cmakedeps:new=will_break_next")
-
-
-def test_cmake_find_mode_deprecated():
-    tc = TestClient()
-    dep = textwrap.dedent("""
-        from conan import ConanFile
-        class Pkg(ConanFile):
-            name = "dep"
-            version = "0.1"
-            def package_info(self):
-                # Having both is ok as the user expects that config would
-                # be generated nonetheless
-                self.cpp_info.set_property("cmake_find_mode", "module")
-        """)
-    tc.save({"conanfile.py": dep})
-    tc.run("create .")
-    args = f"-g CMakeDeps -c tools.cmake.cmakedeps:new={new_value}"
-    tc.run(f"install --requires=dep/0.1 {args}")
-    assert "CMakeConfigDeps does not support module find mode"