From be1a753f8ba802f9f4fff39e65c42a494fda759a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abril=20Rinc=C3=B3n=20Blanco?= Date: Mon, 22 Sep 2025 12:18:45 +0200 Subject: [PATCH 1/9] Initial sketch for conan run command --- conan/cli/commands/run.py | 37 ++++++++++++++++++++++++++++ test/integration/command/test_run.py | 33 +++++++++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 conan/cli/commands/run.py create mode 100644 test/integration/command/test_run.py diff --git a/conan/cli/commands/run.py b/conan/cli/commands/run.py new file mode 100644 index 00000000000..8e8bee3137c --- /dev/null +++ b/conan/cli/commands/run.py @@ -0,0 +1,37 @@ +import os + +from conan.cli.command import conan_command + + +@conan_command(group="Creator") +def run(conan_api, parser, *args): + """ + Run a command in the environment defined by a previous call to 'conan install'. + """ + # Initially we're not adding a way to specify the requirement from here, + # or a conanfile to use, because that would add 300 args, + # and the idea is to start small. If we see a need for this, it might be + # discussed later, but the recommended way is to do 'conan install' first + # if your use-case is more complex than the current one, and then + # just source the envs directly + parser.add_argument("command", help="Command to run", + nargs='+') + parser.add_argument("--context", help="Context to use, host or build", + choices=["host", "build"], default="host") + args = parser.parse_args(*args) + command = " ".join(args.command) + cwd = os.getcwd() + # TODO: most of this will need to go into something like + #conan_api.local.run(args.command, cwd, ...) + import subprocess + + # TODO: We'll also need a way to find the env files, + # harcoded for now, but things to take into account are: + # - tools.env.virtualenv:powershell conf when using Windows + # - then different shells/prefixes will be necessary + # - what context? How do we choose it? Is the current parameter a good idea? + # - error handling + # - Should we capture the output? Does not look like a good idea for interactive commands + prefix = "conanbuild.sh" if args.context == "build" else "conanrun.sh" + composed_command = f". {prefix} && {command}" + subprocess.run(composed_command, shell=True, cwd=cwd, check=True) diff --git a/test/integration/command/test_run.py b/test/integration/command/test_run.py new file mode 100644 index 00000000000..a33b7a6a658 --- /dev/null +++ b/test/integration/command/test_run.py @@ -0,0 +1,33 @@ +import textwrap +import pytest + +from conan.test.utils.tools import TestClient + + +@pytest.mark.parametrize("context", ["host", "build"]) +def test_run_basic(context): + tc = TestClient() + conanfile = textwrap.dedent(""" + from conan import ConanFile + from conan.tools.files import save + import os + + class Pkg(ConanFile): + name = "pkg" + version = "0.1" + # So that the requirement is run=True even for --requires + package_type = "application" + + def package(self): + save(self, os.path.join(self.package_folder, "bin", "myapp.sh"), "echo Hello World!") + # Make it executable + os.chmod(os.path.join(self.package_folder, "bin", "myapp.sh"), 0o755) + """) + + tc.save({"pkg/conanfile.py": conanfile}) + tc.run("create pkg") + requires = "requires" if context == "host" else "tool-requires" + tc.run(f"install --{requires}=pkg/0.1") + tc.run(f"run myapp.sh --context={context}") + # Commented, find a way to test the output, right now we are not capturing it + # assert "Hello World!" in tc.out From 709d1f5b8eb59ab35cb4030b6374e2b772adac3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abril=20Rinc=C3=B3n=20Blanco?= Date: Mon, 22 Sep 2025 13:20:56 +0200 Subject: [PATCH 2/9] Sketch install built-in in run command --------- Co-authored-by: PerseoGI --- conan/cli/commands/install.py | 23 ++++++++++++++-------- conan/cli/commands/run.py | 37 +++++++++++++++++++++++++++-------- 2 files changed, 44 insertions(+), 16 deletions(-) diff --git a/conan/cli/commands/install.py b/conan/cli/commands/install.py index faff8ed2a96..df8b4a8581d 100644 --- a/conan/cli/commands/install.py +++ b/conan/cli/commands/install.py @@ -43,8 +43,21 @@ def install(conan_api, parser, *args): help="Generation strategy for virtual environment files for the root") args = parser.parse_args(*args) validate_common_graph_args(args) - # basic paths cwd = os.getcwd() + + deps_graph, lockfile = run_install_command(conan_api, args, cwd) + + # Update lockfile if necessary + lockfile = conan_api.lockfile.update_lockfile(lockfile, deps_graph, args.lockfile_packages, + clean=args.lockfile_clean) + conan_api.lockfile.save_lockfile(lockfile, args.lockfile_out, cwd) + return {"graph": deps_graph, + "conan_api": conan_api} + + +def run_install_command(conan_api, args, cwd): + # basic paths + path = conan_api.local.get_conanfile_path(args.path, cwd, py=None) if args.path else None source_folder = os.path.dirname(path) if args.path else cwd output_folder = make_abs_path(args.output_folder, cwd) if args.output_folder else None @@ -79,10 +92,4 @@ def install(conan_api, parser, *args): deploy_folder=args.deployer_folder, envs_generation=args.envs_generation) ConanOutput().success("Install finished successfully") - - # Update lockfile if necessary - lockfile = conan_api.lockfile.update_lockfile(lockfile, deps_graph, args.lockfile_packages, - clean=args.lockfile_clean) - conan_api.lockfile.save_lockfile(lockfile, args.lockfile_out, cwd) - return {"graph": deps_graph, - "conan_api": conan_api} + return deps_graph, lockfile diff --git a/conan/cli/commands/run.py b/conan/cli/commands/run.py index 8e8bee3137c..e0457bbd367 100644 --- a/conan/cli/commands/run.py +++ b/conan/cli/commands/run.py @@ -1,26 +1,45 @@ import os +from conan.cli.args import common_graph_args, validate_common_graph_args from conan.cli.command import conan_command +from conan.cli.commands.install import run_install_command -@conan_command(group="Creator") +@conan_command(group="Consumer") def run(conan_api, parser, *args): """ Run a command in the environment defined by a previous call to 'conan install'. """ - # Initially we're not adding a way to specify the requirement from here, - # or a conanfile to use, because that would add 300 args, - # and the idea is to start small. If we see a need for this, it might be - # discussed later, but the recommended way is to do 'conan install' first - # if your use-case is more complex than the current one, and then - # just source the envs directly + common_graph_args(parser) parser.add_argument("command", help="Command to run", nargs='+') parser.add_argument("--context", help="Context to use, host or build", choices=["host", "build"], default="host") + # Install commands + + # This would need to be cleaned up a lot + parser.add_argument("-g", "--generator", action="append", help='Generators to use') + parser.add_argument("-of", "--output-folder", + help='The root output folder for generated and build files') + parser.add_argument("-d", "--deployer", action="append", + help="Deploy using the provided deployer to the output folder. " + "Built-in deployers: 'full_deploy', 'direct_deploy', 'runtime_deploy'") + parser.add_argument("--deployer-folder", + help="Deployer output folder, base build folder by default if not set") + parser.add_argument("--deployer-package", action="append", + help="Execute the deploy() method of the packages matching " + "the provided patterns") + parser.add_argument("--build-require", action='store_true', default=False, + help='Whether the provided path is a build-require') + parser.add_argument("--envs-generation", default=None, choices=["false"], + help="Generation strategy for virtual environment files for the root") args = parser.parse_args(*args) + validate_common_graph_args(args) command = " ".join(args.command) cwd = os.getcwd() + + deps_graph, lockfile = run_install_command(conan_api, args, cwd) + # TODO: most of this will need to go into something like #conan_api.local.run(args.command, cwd, ...) import subprocess @@ -32,6 +51,8 @@ def run(conan_api, parser, *args): # - what context? How do we choose it? Is the current parameter a good idea? # - error handling # - Should we capture the output? Does not look like a good idea for interactive commands + + generators_folder = deps_graph.root.conanfile.folders.generators_folder prefix = "conanbuild.sh" if args.context == "build" else "conanrun.sh" - composed_command = f". {prefix} && {command}" + composed_command = f". {os.path.join(generators_folder, prefix)} && {command}" subprocess.run(composed_command, shell=True, cwd=cwd, check=True) From a2b04d13f4be8898715384623d7809ca9c70e506 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abril=20Rinc=C3=B3n=20Blanco?= Date: Mon, 22 Sep 2025 13:31:47 +0200 Subject: [PATCH 3/9] Fix test --- conan/cli/commands/run.py | 2 +- test/integration/command/test_run.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/conan/cli/commands/run.py b/conan/cli/commands/run.py index e0457bbd367..b8a0012c7cd 100644 --- a/conan/cli/commands/run.py +++ b/conan/cli/commands/run.py @@ -54,5 +54,5 @@ def run(conan_api, parser, *args): generators_folder = deps_graph.root.conanfile.folders.generators_folder prefix = "conanbuild.sh" if args.context == "build" else "conanrun.sh" - composed_command = f". {os.path.join(generators_folder, prefix)} && {command}" + composed_command = f'. "{os.path.join(generators_folder, prefix)}" && {command}' subprocess.run(composed_command, shell=True, cwd=cwd, check=True) diff --git a/test/integration/command/test_run.py b/test/integration/command/test_run.py index a33b7a6a658..5660bf5c2b0 100644 --- a/test/integration/command/test_run.py +++ b/test/integration/command/test_run.py @@ -27,7 +27,6 @@ def package(self): tc.save({"pkg/conanfile.py": conanfile}) tc.run("create pkg") requires = "requires" if context == "host" else "tool-requires" - tc.run(f"install --{requires}=pkg/0.1") - tc.run(f"run myapp.sh --context={context}") + tc.run(f"run myapp.sh --{requires}=pkg/0.1 --context={context}") # Commented, find a way to test the output, right now we are not capturing it # assert "Hello World!" in tc.out From 6967d6b00018680d34ae18ccaa1ce2f03336bda8 Mon Sep 17 00:00:00 2001 From: PerseoGI Date: Tue, 23 Sep 2025 11:59:55 +0200 Subject: [PATCH 4/9] Use conanfile run method which handles context and subprocess --- conan/cli/commands/install.py | 7 ++++--- conan/cli/commands/run.py | 35 +++++++---------------------------- conan/internal/loader.py | 1 + 3 files changed, 12 insertions(+), 31 deletions(-) diff --git a/conan/cli/commands/install.py b/conan/cli/commands/install.py index df8b4a8581d..a41185faf6b 100644 --- a/conan/cli/commands/install.py +++ b/conan/cli/commands/install.py @@ -88,8 +88,9 @@ def run_install_command(conan_api, args, cwd): conan_api.install.install_binaries(deps_graph=deps_graph, remotes=remotes) ConanOutput().title("Finalizing install (deploy, generators)") conan_api.install.install_consumer(deps_graph, args.generator, source_folder, output_folder, - deploy=args.deployer, deploy_package=args.deployer_package, - deploy_folder=args.deployer_folder, - envs_generation=args.envs_generation) + deploy=getattr(args, "deployer", None), + deploy_package=getattr(args, "deployer_package", None), + deploy_folder=getattr(args,"deployer_folder", None), + envs_generation=getattr(args, "envs_generation", None)) ConanOutput().success("Install finished successfully") return deps_graph, lockfile diff --git a/conan/cli/commands/run.py b/conan/cli/commands/run.py index b8a0012c7cd..d15b14bd3e4 100644 --- a/conan/cli/commands/run.py +++ b/conan/cli/commands/run.py @@ -16,23 +16,11 @@ def run(conan_api, parser, *args): parser.add_argument("--context", help="Context to use, host or build", choices=["host", "build"], default="host") # Install commands - - # This would need to be cleaned up a lot parser.add_argument("-g", "--generator", action="append", help='Generators to use') parser.add_argument("-of", "--output-folder", help='The root output folder for generated and build files') - parser.add_argument("-d", "--deployer", action="append", - help="Deploy using the provided deployer to the output folder. " - "Built-in deployers: 'full_deploy', 'direct_deploy', 'runtime_deploy'") - parser.add_argument("--deployer-folder", - help="Deployer output folder, base build folder by default if not set") - parser.add_argument("--deployer-package", action="append", - help="Execute the deploy() method of the packages matching " - "the provided patterns") parser.add_argument("--build-require", action='store_true', default=False, help='Whether the provided path is a build-require') - parser.add_argument("--envs-generation", default=None, choices=["false"], - help="Generation strategy for virtual environment files for the root") args = parser.parse_args(*args) validate_common_graph_args(args) command = " ".join(args.command) @@ -40,19 +28,10 @@ def run(conan_api, parser, *args): deps_graph, lockfile = run_install_command(conan_api, args, cwd) - # TODO: most of this will need to go into something like - #conan_api.local.run(args.command, cwd, ...) - import subprocess - - # TODO: We'll also need a way to find the env files, - # harcoded for now, but things to take into account are: - # - tools.env.virtualenv:powershell conf when using Windows - # - then different shells/prefixes will be necessary - # - what context? How do we choose it? Is the current parameter a good idea? - # - error handling - # - Should we capture the output? Does not look like a good idea for interactive commands - - generators_folder = deps_graph.root.conanfile.folders.generators_folder - prefix = "conanbuild.sh" if args.context == "build" else "conanrun.sh" - composed_command = f'. "{os.path.join(generators_folder, prefix)}" && {command}' - subprocess.run(composed_command, shell=True, cwd=cwd, check=True) + # TODO: + # - Context: could be both host and build? + # - Output folder? /tmp, $CWD/.conanrun, ~/.conan + # - Conan internal loader: is it good idea to initialize conan_helpers in a virtual node? + # - Tests + scope = "run" if args.context == "host" else "build" + deps_graph.root.conanfile.run(command, cwd=cwd, scope=scope) diff --git a/conan/internal/loader.py b/conan/internal/loader.py index f3634ee9a6f..b92972d3039 100644 --- a/conan/internal/loader.py +++ b/conan/internal/loader.py @@ -254,6 +254,7 @@ def load_virtual(self, requires=None, tool_requires=None, python_requires=None, # If user don't specify namespace in options, assume that it is # for the reference (keep compatibility) conanfile = ConanFile(display_name="cli") + conanfile._conan_helpers = self._conanfile_helpers if tool_requires: for reference in tool_requires: From 49785fd4dae08d812bd3f96c89afbd2ea021195b Mon Sep 17 00:00:00 2001 From: PerseoGI Date: Tue, 23 Sep 2025 13:00:06 +0200 Subject: [PATCH 5/9] Add test assert --- test/integration/command/test_run.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/command/test_run.py b/test/integration/command/test_run.py index 5660bf5c2b0..80671aa3eb1 100644 --- a/test/integration/command/test_run.py +++ b/test/integration/command/test_run.py @@ -29,4 +29,4 @@ def package(self): requires = "requires" if context == "host" else "tool-requires" tc.run(f"run myapp.sh --{requires}=pkg/0.1 --context={context}") # Commented, find a way to test the output, right now we are not capturing it - # assert "Hello World!" in tc.out + assert "Hello World!" in tc.out From 8253f027af6662e45c4deab580e324d176dad202 Mon Sep 17 00:00:00 2001 From: PerseoGI Date: Wed, 24 Sep 2025 10:32:27 +0200 Subject: [PATCH 6/9] Added test case for conan run with conanfile --- conan/cli/commands/run.py | 4 +--- test/integration/command/test_run.py | 29 ++++++++++++++++++++++++---- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/conan/cli/commands/run.py b/conan/cli/commands/run.py index d15b14bd3e4..88b3c06e865 100644 --- a/conan/cli/commands/run.py +++ b/conan/cli/commands/run.py @@ -15,8 +15,8 @@ def run(conan_api, parser, *args): nargs='+') parser.add_argument("--context", help="Context to use, host or build", choices=["host", "build"], default="host") - # Install commands parser.add_argument("-g", "--generator", action="append", help='Generators to use') + # TODO: Output folder? /tmp, $CWD/.conanrun, ~/.conan parser.add_argument("-of", "--output-folder", help='The root output folder for generated and build files') parser.add_argument("--build-require", action='store_true', default=False, @@ -30,8 +30,6 @@ def run(conan_api, parser, *args): # TODO: # - Context: could be both host and build? - # - Output folder? /tmp, $CWD/.conanrun, ~/.conan - # - Conan internal loader: is it good idea to initialize conan_helpers in a virtual node? # - Tests scope = "run" if args.context == "host" else "build" deps_graph.root.conanfile.run(command, cwd=cwd, scope=scope) diff --git a/test/integration/command/test_run.py b/test/integration/command/test_run.py index 80671aa3eb1..f039d669169 100644 --- a/test/integration/command/test_run.py +++ b/test/integration/command/test_run.py @@ -1,11 +1,14 @@ import textwrap import pytest +import platform +from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient @pytest.mark.parametrize("context", ["host", "build"]) -def test_run_basic(context): +@pytest.mark.parametrize("use_conanfile", [True, False]) +def test_run(context, use_conanfile): tc = TestClient() conanfile = textwrap.dedent(""" from conan import ConanFile @@ -20,13 +23,31 @@ class Pkg(ConanFile): def package(self): save(self, os.path.join(self.package_folder, "bin", "myapp.sh"), "echo Hello World!") + save(self, os.path.join(self.package_folder, "bin", "myapp.bat"), "echo Hello World!") # Make it executable os.chmod(os.path.join(self.package_folder, "bin", "myapp.sh"), 0o755) + os.chmod(os.path.join(self.package_folder, "bin", "myapp.bat"), 0o755) """) - tc.save({"pkg/conanfile.py": conanfile}) + conanfile_consumer = GenConanfile("consumer", "1.0") + if context == "host": + conanfile_consumer.with_requires("pkg/0.1") + else: + conanfile_consumer.with_tool_requires("pkg/0.1") + + tc.save({"pkg/conanfile.py": conanfile, "conanfile.py": conanfile_consumer }) tc.run("create pkg") requires = "requires" if context == "host" else "tool-requires" - tc.run(f"run myapp.sh --{requires}=pkg/0.1 --context={context}") - # Commented, find a way to test the output, right now we are not capturing it + + if use_conanfile: + if platform.system() == "Windows": + tc.run(f"run . myapp.bat --context={context}") + else: + tc.run(f"run . myapp.sh --context={context}") + else: + if platform.system() == "Windows": + tc.run(f"run myapp.bat --{requires}=pkg/0.1 --context={context}") + else: + tc.run(f"run myapp.sh --{requires}=pkg/0.1 --context={context}") assert "Hello World!" in tc.out + From 0110713bf8463038030923b2fd4fea816a01536f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abril=20Rinc=C3=B3n=20Blanco?= Date: Wed, 24 Sep 2025 13:01:49 +0200 Subject: [PATCH 7/9] Add missing os setting for tests --- test/integration/command/test_run.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/command/test_run.py b/test/integration/command/test_run.py index f039d669169..e84245acd56 100644 --- a/test/integration/command/test_run.py +++ b/test/integration/command/test_run.py @@ -29,7 +29,7 @@ def package(self): os.chmod(os.path.join(self.package_folder, "bin", "myapp.bat"), 0o755) """) - conanfile_consumer = GenConanfile("consumer", "1.0") + conanfile_consumer = GenConanfile("consumer", "1.0").with_settings("os") if context == "host": conanfile_consumer.with_requires("pkg/0.1") else: From fd72c3d2f47a408387790bd962edf32b176fe943 Mon Sep 17 00:00:00 2001 From: PerseoGI Date: Tue, 30 Sep 2025 19:18:55 +0200 Subject: [PATCH 8/9] Updated default options according to Look Intos meet --- conan/cli/commands/run.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/conan/cli/commands/run.py b/conan/cli/commands/run.py index 88b3c06e865..d0fd28c6a73 100644 --- a/conan/cli/commands/run.py +++ b/conan/cli/commands/run.py @@ -8,15 +8,13 @@ @conan_command(group="Consumer") def run(conan_api, parser, *args): """ - Run a command in the environment defined by a previous call to 'conan install'. + Run a command given a set of requirements from a recipe or from command line. """ common_graph_args(parser) - parser.add_argument("command", help="Command to run", - nargs='+') + parser.add_argument("command", help="Command to run", nargs='+') parser.add_argument("--context", help="Context to use, host or build", - choices=["host", "build"], default="host") + choices=["host", "build"], default="build") parser.add_argument("-g", "--generator", action="append", help='Generators to use') - # TODO: Output folder? /tmp, $CWD/.conanrun, ~/.conan parser.add_argument("-of", "--output-folder", help='The root output folder for generated and build files') parser.add_argument("--build-require", action='store_true', default=False, @@ -26,10 +24,14 @@ def run(conan_api, parser, *args): command = " ".join(args.command) cwd = os.getcwd() + # If conanfile is provided, delegate output_folder definition to the conanfile layout + # Otherwise, use a hidden folder to avoid cluttering the workspace + if not args.path: + args.output_folder = ".conanrun" + deps_graph, lockfile = run_install_command(conan_api, args, cwd) # TODO: - # - Context: could be both host and build? # - Tests scope = "run" if args.context == "host" else "build" deps_graph.root.conanfile.run(command, cwd=cwd, scope=scope) From 9d78a1b1829e07acf69e2f9ba14c2037a24b8c8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abril=20Rinc=C3=B3n=20Blanco?= Date: Wed, 1 Oct 2025 11:40:10 +0200 Subject: [PATCH 9/9] Sketch output handling, might be reverted later --- conan/cli/commands/run.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/conan/cli/commands/run.py b/conan/cli/commands/run.py index d0fd28c6a73..48f1e5d8aca 100644 --- a/conan/cli/commands/run.py +++ b/conan/cli/commands/run.py @@ -1,5 +1,6 @@ import os +from conan.api.output import ConanOutput, LEVEL_WARNING, LEVEL_VERBOSE, LEVEL_STATUS from conan.cli.args import common_graph_args, validate_common_graph_args from conan.cli.command import conan_command from conan.cli.commands.install import run_install_command @@ -29,7 +30,11 @@ def run(conan_api, parser, *args): if not args.path: args.output_folder = ".conanrun" + previous_log_level = ConanOutput._conan_output_level + if previous_log_level == LEVEL_STATUS: + ConanOutput._conan_output_level = LEVEL_WARNING deps_graph, lockfile = run_install_command(conan_api, args, cwd) + ConanOutput._conan_output_level = previous_log_level # TODO: # - Tests