Repository: conan-io/conan

Issue:
[bug] Can't call `conan upload --recipe-only` twice with backup sources enabled
### Steps to reproduce

1. Enable backup sources
2. Export a recipe that downloads file
3. Call conan upload only recipe for ref
4. Do it again, it fails due to KeyError


Found while prepping for https://github.com/conan-io/conan-center-index/pull/18082

Discussion:

Patch:
diff --git a/conan/tools/cmake/toolchain/blocks.py b/conan/tools/cmake/toolchain/blocks.py
index 48fabe933..c10b6b03f 100644
--- a/conan/tools/cmake/toolchain/blocks.py
+++ b/conan/tools/cmake/toolchain/blocks.py
@@ -733,8 +733,9 @@ class GenericSystemBlock(Block):
         os_host = self._conanfile.settings.get_safe("os")
         arch_host = self._conanfile.settings.get_safe("arch")
         arch_build = self._conanfile.settings_build.get_safe("arch")
+        os_build = self._conanfile.settings_build.get_safe("os")
         return os_host in ('iOS', 'watchOS', 'tvOS') or (
-                os_host == 'Macos' and arch_host != arch_build)
+                os_host == 'Macos' and (arch_host != arch_build or os_build != os_host))
 
     def _get_cross_build(self):
         user_toolchain = self._conanfile.conf.get("tools.cmake.cmaketoolchain:user_toolchain")
@@ -746,6 +747,7 @@ class GenericSystemBlock(Block):
         system_processor = self._conanfile.conf.get("tools.cmake.cmaketoolchain:system_processor")
 
         assert hasattr(self._conanfile, "settings_build")
+        # TODO: Remove unused if
         if hasattr(self._conanfile, "settings_build"):
             os_host = self._conanfile.settings.get_safe("os")
             arch_host = self._conanfile.settings.get_safe("arch")
diff --git a/conans/client/downloaders/download_cache.py b/conans/client/downloaders/download_cache.py
index b086f52b4..c9ea0df70 100644
--- a/conans/client/downloaders/download_cache.py
+++ b/conans/client/downloaders/download_cache.py
@@ -60,9 +60,12 @@ class DownloadCache:
         if excluded_urls is None:
             excluded_urls = []
 
-        all_refs = {str(k) for k, ref in package_list.refs()
-                    if ref.get("upload") or any(should_upload_sources(p)
-                                                for p in ref["packages"].values())}
+        all_refs = set()
+        for k, ref in package_list.refs():
+            packages = ref.get("packages", {}).values()
+            if ref.get("upload") or any(should_upload_sources(p) for p in packages):
+                all_refs.add(str(k))
+
         for f in os.listdir(path_backups):
             if f.endswith(".json"):
                 f = os.path.join(path_backups, f)


Test patch:
diff --git a/conans/test/integration/cache/backup_sources_test.py b/conans/test/integration/cache/backup_sources_test.py
index 433055b6b..f8a99b3af 100644
--- a/conans/test/integration/cache/backup_sources_test.py
+++ b/conans/test/integration/cache/backup_sources_test.py
@@ -687,6 +687,7 @@ class TestDownloadCacheBackupSources:
     def test_export_then_upload_workflow(self):
         client = TestClient(default_server_user=True)
         download_cache_folder = temp_folder()
+        mkdir(os.path.join(download_cache_folder, "s"))
         http_server = StoppableThreadBottle()
 
         http_server_base_folder_internet = temp_folder()
@@ -735,3 +736,56 @@ class TestDownloadCacheBackupSources:
         client.run("create .")
         client.run("upload * -c -r=default")
         assert sha256 in os.listdir(http_server_base_folder_backup1)
+
+    def test_export_then_upload_recipe_only_workflow(self):
+        client = TestClient(default_server_user=True)
+        download_cache_folder = temp_folder()
+        mkdir(os.path.join(download_cache_folder, "s"))
+        http_server = StoppableThreadBottle()
+
+        http_server_base_folder_internet = temp_folder()
+        http_server_base_folder_backup1 = temp_folder()
+
+        sha256 = "315f5bdb76d078c43b8ac0064e4a0164612b1fce77c869345bfc94c75894edd3"
+        save(os.path.join(http_server_base_folder_internet, "myfile.txt"), "Hello, world!")
+
+        @http_server.server.get("/internet/<file>")
+        def get_internet_file(file):
+            return static_file(file, http_server_base_folder_internet)
+
+        @http_server.server.get("/downloader1/<file>")
+        def get_file(file):
+            return static_file(file, http_server_base_folder_backup1)
+
+        @http_server.server.put("/uploader/<file>")
+        def put_file(file):
+            dest = os.path.join(http_server_base_folder_backup1, file)
+            with open(dest, 'wb') as f:
+                f.write(request.body.read())
+
+        http_server.run_server()
+
+        conanfile = textwrap.dedent(f"""
+           from conan import ConanFile
+           from conan.tools.files import download
+           class Pkg2(ConanFile):
+               name = "pkg"
+               version = "1.0"
+               def source(self):
+                   download(self, "http://localhost:{http_server.port}/internet/myfile.txt", "myfile.txt",
+                            sha256="{sha256}")
+           """)
+
+        client.save({"global.conf": f"core.sources:download_cache={download_cache_folder}\n"
+                                    f"core.sources:download_urls=['http://localhost:{http_server.port}/downloader1/', 'origin']\n"
+                                    f"core.sources:upload_url=http://localhost:{http_server.port}/uploader/"},
+                    path=client.cache.cache_folder)
+
+        client.save({"conanfile.py": conanfile})
+        client.run("export .")
+        exported_rev = client.exported_recipe_revision()
+        client.run("upload * --only-recipe -c -r=default")
+        # This second run used to crash because we thought there would be some packages always
+        client.run("upload * --only-recipe -c -r=default")
+        # Ensure we are testing for an already uploaded recipe
+        assert f"Recipe 'pkg/1.0#{exported_rev}' already in server, skipping upload" in client.out
diff --git a/conans/test/integration/toolchains/cmake/test_cmaketoolchain.py b/conans/test/integration/toolchains/cmake/test_cmaketoolchain.py
index 792f69d98..5800bf03e 100644
--- a/conans/test/integration/toolchains/cmake/test_cmaketoolchain.py
+++ b/conans/test/integration/toolchains/cmake/test_cmaketoolchain.py
@@ -42,6 +42,36 @@ def test_cross_build():
     assert "set(CMAKE_SYSTEM_PROCESSOR armv8)" in toolchain
 
 
+def test_cross_build_linux_to_macos():
+    linux_profile = textwrap.dedent("""
+        [settings]
+        os=Linux
+        arch=x86_64
+        """)
+    macos_profile = textwrap.dedent("""
+        [settings]
+        os=Macos
+        os.sdk_version=13.1
+        arch=x86_64
+        compiler=apple-clang
+        compiler.version=13
+        compiler.libcxx=libc++
+        build_type=Release
+        """)
+
+    client = TestClient(path_with_spaces=False)
+
+    client.save({"conanfile.txt": "[generators]\nCMakeToolchain",
+                 "linux": linux_profile,
+                 "macos": macos_profile})
+    client.run("install . --profile:build=linux --profile:host=macos")
+    toolchain = client.load("conan_toolchain.cmake")
+
+    assert "set(CMAKE_SYSTEM_NAME Darwin)" in toolchain
+    assert "set(CMAKE_SYSTEM_VERSION 13.1)" in toolchain
+    assert "set(CMAKE_SYSTEM_PROCESSOR x86_64)" in toolchain
+
+
 def test_cross_build_user_toolchain():
     # When a user_toolchain is defined in [conf], CMakeToolchain will not generate anything
     # for cross-build