Repository: conan-io/conan
Pull request #18427 (2025-06-09 06:53:01)

Create folders if they don't exist when using `--out-file`
Changelog: Fix: Create folders if they don't exist when using `--out-file`
Docs: Omit

Closes https://github.com/conan-io/docs/issues/4118


Patch:
diff --git a/conan/cli/command.py b/conan/cli/command.py
index 4a7e80971..b4e0ba610 100644
--- a/conan/cli/command.py
+++ b/conan/cli/command.py
@@ -1,4 +1,5 @@
 import argparse
+import os
 import textwrap
 from contextlib import redirect_stdout
 
@@ -96,6 +97,8 @@ class BaseConanCommand:
                 formatarg, ", ".join(self._help_formatters)))
 
         if out_file:
+            if os.path.dirname(out_file):
+                os.makedirs(os.path.dirname(out_file), exist_ok=True)
             with open(out_file, 'w') as f:
                 with redirect_stdout(f):
                     formatter(info)
diff --git a/conan/internal/rest/remote_manager.py b/conan/internal/rest/remote_manager.py
index d87f3d849..b5bfb0e2d 100644
--- a/conan/internal/rest/remote_manager.py
+++ b/conan/internal/rest/remote_manager.py
@@ -38,11 +38,13 @@ class RemoteManager:
     def upload_recipe(self, ref, files_to_upload, remote):
         assert isinstance(ref, RecipeReference)
         assert ref.revision, "upload_recipe requires RREV"
+        remote.invalidate_cache()
         self._call_remote(remote, "upload_recipe", ref, files_to_upload)
 
     def upload_package(self, pref, files_to_upload, remote):
         assert pref.ref.revision, "upload_package requires RREV"
         assert pref.revision, "upload_package requires PREV"
+        remote.invalidate_cache()
         self._call_remote(remote, "upload_package", pref, files_to_upload)
 
     def get_recipe(self, ref, remote, metadata=None):
@@ -204,12 +206,15 @@ class RemoteManager:
         return packages
 
     def remove_recipe(self, ref, remote):
+        remote.invalidate_cache()
         return self._call_remote(remote, "remove_recipe", ref)
 
     def remove_packages(self, prefs, remote):
+        remote.invalidate_cache()
         return self._call_remote(remote, "remove_packages", prefs)
 
     def remove_all_packages(self, ref, remote):
+        remote.invalidate_cache()
         return self._call_remote(remote, "remove_all_packages", ref)
 
     def authenticate(self, remote, name, password):
@@ -243,10 +248,19 @@ class RemoteManager:
 
         cached_method = remote._caching.setdefault("get_latest_package_reference", {})
         try:
-            return cached_method[pref]
+            result = cached_method[pref]
         except KeyError:
-            result = self._call_remote(remote, "get_latest_package_reference", pref, headers=headers)
-            cached_method[pref] = result
+            try:
+                result = self._call_remote(remote, "get_latest_package_reference", pref,
+                                           headers=headers)
+                cached_method[pref] = result
+                return result
+            except Exception as e:
+                cached_method[pref] = e
+                raise e
+        else:
+            if isinstance(result, Exception):
+                raise result
             return result
 
     def get_recipe_revision_reference(self, ref, remote) -> bool:


Test patch:
diff --git a/test/integration/command/test_output.py b/test/integration/command/test_output.py
index 52a6b9603..bd383905f 100644
--- a/test/integration/command/test_output.py
+++ b/test/integration/command/test_output.py
@@ -1,6 +1,8 @@
 import json
+import os
 
 from conan.test.assets.genconanfile import GenConanfile
+from conan.test.utils.test_files import temp_folder
 from conan.test.utils.tools import TestClient
 from conan.test.utils.env import environment_update
 
@@ -291,3 +293,18 @@ def test_formatter_redirection_to_file():
     graph = json.loads(c.load("graph.json"))
     assert len(graph["graph"]["nodes"]) == 1
     assert not "nodes" in c.stdout
+
+
+def test_redirect_to_file_create_dir():
+    c = TestClient(light=True)
+    c.save({"conanfile.py": GenConanfile("pkg", "0.1")})
+    c.run("config home --out-file=subdir/cmd_out.txt")
+    cmd_out = c.load("subdir/cmd_out.txt")
+    assert f"{c.cache_folder}" in cmd_out
+
+    base = temp_folder()
+    c = TestClient(light=True, current_folder=os.path.join(base, "sub1"))
+    c.save({"conanfile.py": GenConanfile("pkg", "0.1")})
+    c.run("config home --out-file=../subdir/cmd_out.txt")
+    cmd_out = c.load("../subdir/cmd_out.txt")
+    assert f"{c.cache_folder}" in cmd_out
diff --git a/test/performance/.gitignore b/test/performance/.gitignore
new file mode 100644
index 000000000..e934adfd1
--- /dev/null
+++ b/test/performance/.gitignore
@@ -0,0 +1,1 @@
+cache/
diff --git a/test/performance/test_large_graph.py b/test/performance/test_large_graph.py
index 8ccc0fed9..866bdbd6a 100644
--- a/test/performance/test_large_graph.py
+++ b/test/performance/test_large_graph.py
@@ -1,5 +1,5 @@
 import cProfile
-import json
+import os
 import pstats
 import time
 from pstats import SortKey
@@ -11,7 +11,7 @@ from conan.test.utils.tools import TestClient
 
 @pytest.mark.skip(reason="This is a performance test, skip for normal runs")
 def test_large_graph():
-    c = TestClient(cache_folder="T:/mycache")
+    c = TestClient(cache_folder=os.path.join(os.path.dirname(__file__), "cache"))
     num_test = 40
     num_pkgs = 40
 
@@ -47,7 +47,7 @@ def test_large_graph():
 
 @pytest.mark.skip(reason="This is a performance test, skip for normal runs")
 def test_large_graph2():
-    c = TestClient(cache_folder="T:/mycache")
+    c = TestClient(cache_folder=os.path.join(os.path.dirname(__file__), "cache"))
     num_test = 20
     num_pkgs = 20
     branches = ["a", "b", "c"]