Repository: conan-io/conan
Pull request #16102 (2024-04-18 09:09:24)

extend `conan version` to report current python and system for troubleshooting
Changelog: Feature: Extend `conan version` to report current python and system for troubleshooting
Docs: https://github.com/conan-io/docs/pull/3691

Close: #15974



Patch:
diff --git a/conan/api/subapi/cache.py b/conan/api/subapi/cache.py
index 0875f6fb2..1fcb49219 100644
--- a/conan/api/subapi/cache.py
+++ b/conan/api/subapi/cache.py
@@ -155,6 +155,8 @@ class CacheAPI:
             tgz.close()
 
     def restore(self, path):
+        if not os.path.isfile(path):
+            raise ConanException(f"Restore archive doesn't exist in {path}")
         with open(path, mode='rb') as file_handler:
             the_tar = tarfile.open(fileobj=file_handler)
             fileobj = the_tar.extractfile("pkglist.json")
diff --git a/conan/cli/commands/version.py b/conan/cli/commands/version.py
index 507f0f7d1..b2509afef 100644
--- a/conan/cli/commands/version.py
+++ b/conan/cli/commands/version.py
@@ -12,10 +12,22 @@ def version(conan_api, parser, *args):
     Give information about the Conan client version.
     """
 
-    return {'version': str(conan_version),
+    return {
+            'version': str(conan_version),
+            'conan_path': sys.argv[0],
             'python': {
                 'version': platform.python_version().replace('\n', ''),
                 'sys_version': sys.version.replace('\n', ''),
+                'sys_executable': sys.executable,
+                'is_frozen': getattr(sys, 'frozen', False),
+                'architecture': platform.machine(),
+                },
+            'system': {
+                'version': platform.version(),
+                'platform': platform.platform(),
+                'system': platform.system(),
+                'release': platform.release(),
+                'cpu': platform.processor(),
                 }
             }
 
diff --git a/conan/internal/api/detect_api.py b/conan/internal/api/detect_api.py
index bafffade5..8bdd5843e 100644
--- a/conan/internal/api/detect_api.py
+++ b/conan/internal/api/detect_api.py
@@ -361,39 +361,38 @@ def detect_default_compiler():
         output.info("CC and CXX: %s, %s " % (cc or "None", cxx or "None"))
         command = cc or cxx
         if "clang" in command.lower():
-            return _clang_compiler(command)
+            return detect_clang_compiler(command)
         if "gnu-cc" in command or "gcc" in command or "g++" in command or "c++" in command:
-            gcc, gcc_version, compiler_exe = _gcc_compiler(command)
+            gcc, gcc_version, compiler_exe = detect_gcc_compiler(command)
             if platform.system() == "Darwin" and gcc is None:
                 output.error("%s detected as a frontend using apple-clang. "
                              "Compiler not supported" % command)
             return gcc, gcc_version, compiler_exe
         if "icpx" in command or "icx" in command:
-            intel, intel_version, compiler_exe = _intel_compiler(command)
+            intel, intel_version, compiler_exe = detect_intel_compiler(command)
             return intel, intel_version, compiler_exe
         if platform.system() == "SunOS" and command.lower() == "cc":
-            return _sun_cc_compiler(command)
+            return detect_suncc_compiler(command)
         if (platform.system() == "Windows" and command.rstrip('"').endswith(("cl", "cl.exe"))
                 and "clang" not in command):
-            return _msvc_cl_compiler(command)
+            return detect_cl_compiler(command)
 
         # I am not able to find its version
         output.error("Not able to automatically detect '%s' version" % command)
         return None, None, None
 
     if platform.system() == "Windows":
-        version = _detect_vs_ide_version()
-        version = {"17": "193", "16": "192", "15": "191"}.get(str(version))  # Map to compiler
-        if version:
-            return 'msvc', Version(version), None
+        compiler, version, compiler_exe = detect_msvc_compiler()
+        if compiler:
+            return compiler, version, compiler_exe
 
     if platform.system() == "SunOS":
-        sun_cc, sun_cc_version, compiler_exe = _sun_cc_compiler()
+        sun_cc, sun_cc_version, compiler_exe = detect_suncc_compiler()
         if sun_cc:
             return sun_cc, sun_cc_version, compiler_exe
 
     if platform.system() in ["Darwin", "FreeBSD"]:
-        clang, clang_version, compiler_exe = _clang_compiler()  # prioritize clang
+        clang, clang_version, compiler_exe = detect_clang_compiler()  # prioritize clang
         if clang:
             return clang, clang_version, compiler_exe
         return None, None, None
@@ -401,10 +400,10 @@ def detect_default_compiler():
         compiler, compiler_version, compiler_exe = _cc_compiler()
         if compiler:
             return compiler, compiler_version, compiler_exe
-        gcc, gcc_version, compiler_exe = _gcc_compiler()
+        gcc, gcc_version, compiler_exe = detect_gcc_compiler()
         if gcc:
             return gcc, gcc_version, compiler_exe
-        return _clang_compiler()
+        return detect_clang_compiler()
 
 
 def default_msvc_ide_version(version):
@@ -441,7 +440,7 @@ def _cc_compiler():
         return None, None, None
 
 
-def _gcc_compiler(compiler_exe="gcc"):
+def detect_gcc_compiler(compiler_exe="gcc"):
     try:
         if platform.system() == "Darwin":
             # In Mac OS X check if gcc is a fronted using apple-clang
@@ -471,7 +470,7 @@ def detect_compiler():
     return compiler, version
 
 
-def _intel_compiler(compiler_exe="icx"):
+def detect_intel_compiler(compiler_exe="icx"):
     try:
         ret, out = detect_runner("%s --version" % compiler_exe)
         if ret != 0:
@@ -485,7 +484,7 @@ def _intel_compiler(compiler_exe="icx"):
         return None, None, None
 
 
-def _sun_cc_compiler(compiler_exe="cc"):
+def detect_suncc_compiler(compiler_exe="cc"):
     try:
         _, out = detect_runner('%s -V' % compiler_exe)
         compiler = "sun-cc"
@@ -501,7 +500,7 @@ def _sun_cc_compiler(compiler_exe="cc"):
         return None, None, None
 
 
-def _clang_compiler(compiler_exe="clang"):
+def detect_clang_compiler(compiler_exe="clang"):
     try:
         ret, out = detect_runner('%s --version' % compiler_exe)
         if ret != 0:
@@ -520,7 +519,15 @@ def _clang_compiler(compiler_exe="clang"):
         return None, None, None
 
 
-def _msvc_cl_compiler(compiler_exe="cl"):
+def detect_msvc_compiler():
+    version = _detect_vs_ide_version()
+    version = {"17": "193", "16": "192", "15": "191"}.get(str(version))  # Map to compiler
+    if version:
+        return 'msvc', Version(version), None
+    return None, None, None
+
+
+def detect_cl_compiler(compiler_exe="cl"):
     """ only if CC/CXX env-vars are defined pointing to cl.exe, and the VS environment must
     be active to have them in the path
     """
diff --git a/conans/client/cmd/uploader.py b/conans/client/cmd/uploader.py
index 65afb5150..424e100ff 100644
--- a/conans/client/cmd/uploader.py
+++ b/conans/client/cmd/uploader.py
@@ -9,7 +9,8 @@ from conans.errors import ConanException, NotFoundException
 from conans.paths import (CONAN_MANIFEST, CONANFILE, EXPORT_SOURCES_TGZ_NAME,
                           EXPORT_TGZ_NAME, PACKAGE_TGZ_NAME, CONANINFO)
 from conans.util.files import (clean_dirty, is_dirty, gather_files,
-                               gzopen_without_timestamps, set_dirty_context_manager, mkdir)
+                               gzopen_without_timestamps, set_dirty_context_manager, mkdir,
+                               human_size)
 
 UPLOAD_POLICY_FORCE = "force-upload"
 UPLOAD_POLICY_SKIP = "skip-upload"
@@ -220,10 +221,11 @@ class UploadExecutor:
 
     def upload_recipe(self, ref, bundle, remote):
         output = ConanOutput(scope=str(ref))
-        output.info(f"Uploading recipe '{ref.repr_notime()}'")
-        t1 = time.time()
         cache_files = bundle["files"]
 
+        output.info(f"Uploading recipe '{ref.repr_notime()}' ({_total_size(cache_files)})")
+
+        t1 = time.time()
         self._app.remote_manager.upload_recipe(ref, cache_files, remote)
 
         duration = time.time() - t1
@@ -232,11 +234,12 @@ class UploadExecutor:
 
     def upload_package(self, pref, prev_bundle, remote):
         output = ConanOutput(scope=str(pref.ref))
-        output.info(f"Uploading package '{pref.repr_notime()}'")
         cache_files = prev_bundle["files"]
         assert (pref.revision is not None), "Cannot upload a package without PREV"
         assert (pref.ref.revision is not None), "Cannot upload a package without RREV"
 
+        output.info(f"Uploading package '{pref.repr_notime()}' ({_total_size(cache_files)})")
+
         t1 = time.time()
         self._app.remote_manager.upload_package(pref, cache_files, remote)
         duration = time.time() - t1
@@ -259,3 +262,11 @@ def compress_files(files, name, dest_dir, compresslevel=None, ref=None):
     duration = time.time() - t1
     ConanOutput().debug(f"{name} compressed in {duration} time")
     return tgz_path
+
+
+def _total_size(cache_files):
+    total_size = 0
+    for file in cache_files.values():
+        stat = os.stat(file)
+        total_size += stat.st_size
+    return human_size(total_size)
diff --git a/conans/model/settings.py b/conans/model/settings.py
index 8ebe924b4..347ab40a5 100644
--- a/conans/model/settings.py
+++ b/conans/model/settings.py
@@ -205,7 +205,7 @@ class Settings(object):
 
     def get_safe(self, name, default=None):
         """
-        Get the setting value avoiding
+        Get the setting value avoiding throwing if it does not exist or has been removed
         :param name:
         :param default:
         :return:
@@ -232,7 +232,10 @@ class Settings(object):
             except KeyError:
                 pass
         else:
-            self._data.pop(name, None)
+            if name == "*":
+                self.clear()
+            else:
+                self._data.pop(name, None)
 
     def copy(self):
         """ deepcopy, recursive


Test patch:
diff --git a/conans/test/functional/test_profile_detect_api.py b/conans/test/functional/test_profile_detect_api.py
index 8efa5e7a6..820e41599 100644
--- a/conans/test/functional/test_profile_detect_api.py
+++ b/conans/test/functional/test_profile_detect_api.py
@@ -45,12 +45,14 @@ class TestProfileDetectAPI:
 
     @pytest.mark.skipif(platform.system() != "Linux", reason="Only linux")
     def test_profile_detect_libc(self):
-
         client = TestClient()
         tpl1 = textwrap.dedent("""
+            {% set compiler, version, _ = detect_api.detect_gcc_compiler() %}
             {% set libc, libc_version = detect_api.detect_libc() %}
             [settings]
             os=Linux
+            compiler={{compiler}}
+            compiler.version={{version}}
             [conf]
             user.confvar:libc={{libc}}
             user.confvar:libc_version={{libc_version}}
@@ -61,9 +63,12 @@ class TestProfileDetectAPI:
         libc_name, libc_version = detect_api.detect_libc()
         assert libc_name is not None
         assert libc_version is not None
+        _, version, _ = detect_api.detect_gcc_compiler()
         expected = textwrap.dedent(f"""\
             Host profile:
             [settings]
+            compiler=gcc
+            compiler.version={version}
             os=Linux
             [conf]
             user.confvar:libc={libc_name}
diff --git a/conans/test/integration/command/source_test.py b/conans/test/integration/command/source_test.py
index d6e6a8393..57644154e 100644
--- a/conans/test/integration/command/source_test.py
+++ b/conans/test/integration/command/source_test.py
@@ -1,4 +1,5 @@
 import os
+import re
 import textwrap
 import unittest
 from collections import OrderedDict
@@ -156,6 +157,9 @@ class ConanLib(ConanFile):
         client.run("create . --name=hello --version=0.1")
         rrev = client.exported_recipe_revision()
         client.run("upload hello/0.1 -r server0")
+        # Ensure we uploaded it
+        assert re.search(r"Uploading recipe 'hello/0.1#.*' \(.*\)", client.out)
+        assert re.search(r"Uploading package 'hello/0.1#.*' \(.*\)", client.out)
         client.run("remove * -c")
 
         # install from server0 that has the sources, upload to server1 (does not have the package)
diff --git a/conans/test/integration/command_v2/test_cache_save_restore.py b/conans/test/integration/command_v2/test_cache_save_restore.py
index 046e0fb0e..2d5301b60 100644
--- a/conans/test/integration/command_v2/test_cache_save_restore.py
+++ b/conans/test/integration/command_v2/test_cache_save_restore.py
@@ -211,3 +211,9 @@ def test_cache_save_subfolder():
     c.run("export .")
     c.run("cache save * --file=subfolder/cache.tgz")
     assert os.path.exists(os.path.join(c.current_folder, "subfolder", "cache.tgz"))
+
+
+def test_error_restore_not_existing():
+    c = TestClient()
+    c.run("cache restore potato.tgz", assert_error=True)
+    assert "ERROR: Restore archive doesn't exist in " in c.out
diff --git a/conans/test/integration/command_v2/test_version.py b/conans/test/integration/command_v2/test_version.py
index 6ddc7aff1..3f397d105 100644
--- a/conans/test/integration/command_v2/test_version.py
+++ b/conans/test/integration/command_v2/test_version.py
@@ -21,8 +21,17 @@ def test_version_json():
     t.run("version --format=json")
     js = json.loads(t.stdout)
     assert js["version"] == __version__
+    assert js["conan_path"] == sys.argv[0]
     assert js["python"]["version"] == _python_version()
     assert js["python"]["sys_version"] == _sys_version()
+    assert js["python"]["sys_executable"] == sys.executable
+    assert js["python"]["is_frozen"] == getattr(sys, 'frozen', False)
+    assert js["python"]["architecture"] == platform.machine()
+    assert js["system"]["version"] == platform.version()
+    assert js["system"]["platform"] == platform.platform()
+    assert js["system"]["system"] == platform.system()
+    assert js["system"]["release"] == platform.release()
+    assert js["system"]["cpu"] == platform.processor()
 
 
 def test_version_text():
@@ -47,6 +56,16 @@ def test_version_raw():
 def _validate_text_output(output):
     lines = output.splitlines()
     assert f'version: {__version__}' in lines
+    assert f'conan_path: {sys.argv[0]}' in lines
     assert 'python' in lines
     assert f'  version: {_python_version()}' in lines
     assert f'  sys_version: {_sys_version()}' in lines
+    assert f'  sys_executable: {sys.executable}' in lines
+    assert f'  is_frozen: {getattr(sys, "frozen", False)}' in lines
+    assert f'  architecture: {platform.machine()}' in lines
+    assert 'system' in lines
+    assert f'  version: {platform.version()}' in lines
+    assert f'  platform: {platform.platform()}' in lines
+    assert f'  system: {platform.system()}' in lines
+    assert f'  release: {platform.release()}' in lines
+    assert f'  cpu: {platform.processor()}' in lines
diff --git a/conans/test/integration/configuration/test_profile_jinja.py b/conans/test/integration/configuration/test_profile_jinja.py
index b8dd5d331..4972a5233 100644
--- a/conans/test/integration/configuration/test_profile_jinja.py
+++ b/conans/test/integration/configuration/test_profile_jinja.py
@@ -198,3 +198,10 @@ class TestProfileDetectAPI:
             os={the_os}
             """)
         assert expected in client.out
+
+
+def test_profile_jinja_error():
+    c = TestClient(light=True)
+    c.save({"profile1": "{% set kk = other() %}"})
+    c.run("profile show -pr=profile1", assert_error=True)
+    assert "ERROR: Error while rendering the profile template file" in c.out
diff --git a/conans/test/unittests/client/conf/detect/test_gcc_compiler.py b/conans/test/unittests/client/conf/detect/test_gcc_compiler.py
index e88743832..dd8cc5e80 100644
--- a/conans/test/unittests/client/conf/detect/test_gcc_compiler.py
+++ b/conans/test/unittests/client/conf/detect/test_gcc_compiler.py
@@ -4,7 +4,7 @@ import mock
 from parameterized import parameterized
 
 
-from conan.internal.api.detect_api import _gcc_compiler
+from conan.internal.api.detect_api import detect_gcc_compiler
 
 
 class GCCCompilerTestCase(unittest.TestCase):
@@ -13,7 +13,7 @@ class GCCCompilerTestCase(unittest.TestCase):
     def test_detect_gcc_10(self, version):
         with mock.patch("platform.system", return_value="Linux"):
             with mock.patch("conan.internal.api.detect_api.detect_runner", return_value=(0, version)):
-                compiler, installed_version, compiler_exe = _gcc_compiler()
+                compiler, installed_version, compiler_exe = detect_gcc_compiler()
         self.assertEqual(compiler, 'gcc')
         self.assertEqual(installed_version, version)
         self.assertEqual(compiler_exe, 'gcc')
diff --git a/conans/test/unittests/model/settings_test.py b/conans/test/unittests/model/settings_test.py
index 94d4a4467..63960772d 100644
--- a/conans/test/unittests/model/settings_test.py
+++ b/conans/test/unittests/model/settings_test.py
@@ -465,6 +465,17 @@ def test_rm_safe():
     assert "'settings.compiler.libcxx' doesn't exist for 'clang'" in str(e.value)
 
 
+def test_rm_safe_wildcard():
+    settings = Settings.loads(default_settings_yml)
+    settings.compiler = "gcc"
+    settings.compiler.version = "4.8"
+    settings.compiler.libcxx = "libstdc++"
+    settings.rm_safe("compiler.*")
+    assert settings.compiler == "gcc"
+    assert settings.get_safe("compiler.version") is None
+    assert settings.get_safe("compiler.libcxx") is None
+
+
 def test_settings_intel_cppstd_03():
     settings = Settings.loads(default_settings_yml)
     settings.compiler = "intel-cc"
diff --git a/conans/test/unittests/util/detect_test.py b/conans/test/unittests/util/detect_test.py
index c86bff14e..c5272a005 100644
--- a/conans/test/unittests/util/detect_test.py
+++ b/conans/test/unittests/util/detect_test.py
@@ -48,7 +48,7 @@ class DetectTest(unittest.TestCase):
             result = dict(result)
             self.assertEqual(expected_arch, result['arch'])
 
-    @mock.patch("conan.internal.api.detect_api._clang_compiler",
+    @mock.patch("conan.internal.api.detect_api.detect_clang_compiler",
                 return_value=("clang", Version("9"), "clang"))
     def test_detect_clang_gcc_toolchain(self, _):
         output = RedirectedTestOutput()