diff --git a/conan/cli/commands/install.py b/conan/cli/commands/install.py index faff8ed2a96..a41185faf6b 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 @@ -75,14 +88,9 @@ def install(conan_api, parser, *args): 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") - - # 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 new file mode 100644 index 00000000000..945a3a6efd7 --- /dev/null +++ b/conan/cli/commands/run.py @@ -0,0 +1,46 @@ +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 +from conan.internal.util.files import rmdir + + +@conan_command(group="Consumer") +def run(conan_api, parser, *args): + """ + 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("--context", help="Context to use, host or build", + choices=["host", "build"], default="build") + # TODO: is this needed? + # parser.add_argument("--build-require", action='store_true', default=False, + # help='Whether the provided path is a build-require') + args = parser.parse_args(*args) + validate_common_graph_args(args) + command = " ".join(args.command) + cwd = os.getcwd() + + # Default values for install + setattr(args, "build_require", False) + setattr(args, "output_folder", ".conanrun") + setattr(args, "generator", []) + + 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 + + scope = "run" if args.context == "host" else "build" + try: + deps_graph.root.conanfile.run(command, cwd=cwd, scope=scope) + except: + raise + finally: + # Remove previous output folder to ensure a clean install + if os.path.exists(args.output_folder): + rmdir(args.output_folder) 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: diff --git a/test/integration/command/test_run.py b/test/integration/command/test_run.py new file mode 100644 index 00000000000..5df4b26ce26 --- /dev/null +++ b/test/integration/command/test_run.py @@ -0,0 +1,53 @@ +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"]) +@pytest.mark.parametrize("use_conanfile", [True, False]) +def test_run(context, use_conanfile): + 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!") + 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) + """) + + conanfile_consumer = GenConanfile("consumer", "1.0").with_settings("os") + 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" + + 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 +