Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

proposal for deploy() feature #15172

Merged
merged 5 commits into from
Nov 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions conan/api/subapi/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def install_sources(self, graph, remotes):

# TODO: Look for a better name
def install_consumer(self, deps_graph, generators=None, source_folder=None, output_folder=None,
deploy=False, deploy_folder=None):
deploy=False, deploy_package=None, deploy_folder=None):
""" Once a dependency graph has been installed, there are things to be done, like invoking
generators for the root consumer.
This is necessary for example for conanfile.txt/py, or for "conan install <ref> -g
Expand All @@ -61,9 +61,9 @@ def install_consumer(self, deps_graph, generators=None, source_folder=None, outp
conanfile.folders.set_base_folders(source_folder, output_folder)

# The previous .set_base_folders has already decided between the source_folder and output
if deploy:
if deploy or deploy_package:
base_folder = deploy_folder or conanfile.folders.base_build
do_deploys(self.conan_api, deps_graph, deploy, base_folder)
do_deploys(self.conan_api, deps_graph, deploy, deploy_package, base_folder)

conanfile.generators = list(set(conanfile.generators).union(generators or []))
app = ConanApp(self.conan_api.cache_folder, self.conan_api.config.global_conf)
Expand Down
2 changes: 1 addition & 1 deletion conan/cli/commands/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ def graph_info(conan_api, parser, subparser, *args):
conan_api.lockfile.save_lockfile(lockfile, args.lockfile_out, cwd)
if args.deployer:
base_folder = args.deployer_folder or os.getcwd()
do_deploys(conan_api, deps_graph, args.deployer, base_folder)
do_deploys(conan_api, deps_graph, args.deployer, None, base_folder)

return {"graph": deps_graph,
"field_filter": args.filter,
Expand Down
5 changes: 4 additions & 1 deletion conan/cli/commands/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ def install(conan_api, parser, *args):
help='Deploy using the provided deployer to the output folder')
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 deployment of the specific packages")
memsharded marked this conversation as resolved.
Show resolved Hide resolved
parser.add_argument("--build-require", action='store_true', default=False,
help='Whether the provided path is a build-require')
args = parser.parse_args(*args)
Expand Down Expand Up @@ -70,7 +72,8 @@ 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_folder=args.deployer_folder)
deploy=args.deployer, deploy_package=args.deployer_package,
deploy_folder=args.deployer_folder)
ConanOutput().success("Install finished successfully")

# Update lockfile if necessary
Expand Down
14 changes: 13 additions & 1 deletion conan/internal/deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from conan.api.output import ConanOutput
from conans.client.loader import load_python_file
from conans.errors import ConanException
from conans.model.recipe_ref import ref_matches
from conans.util.files import rmdir, mkdir


Expand Down Expand Up @@ -37,8 +38,19 @@ def _load(path):
raise ConanException(f"Cannot find deployer '{d}'")


def do_deploys(conan_api, graph, deploy, deploy_folder):
def do_deploys(conan_api, graph, deploy, deploy_package, deploy_folder):
mkdir(deploy_folder)
# handle the recipe deploy()
if deploy_package:
for node in graph.ordered_iterate():
conanfile = node.conanfile
if not conanfile.ref or not any(ref_matches(conanfile.ref, p, None)
for p in deploy_package):
continue
if hasattr(conanfile, "deploy"):
conanfile.output.info("Executing deploy()")
conanfile.deploy_folder = deploy_folder
conanfile.deploy()
# Handle the deploys
cache = HomePaths(conan_api.cache_folder)
for d in deploy or []:
Expand Down
46 changes: 46 additions & 0 deletions conans/test/integration/conanfile/test_deploy_method.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import textwrap

from conans.test.utils.tools import TestClient


def test_deploy_method():
c = TestClient()
conanfile = textwrap.dedent("""
import os
from conan import ConanFile
from conan.tools.files import copy, save
class Pkg(ConanFile):
name = "{name}"
version = "0.1"
{requires}
def package(self):
save(self, os.path.join(self.package_folder, f"my{name}file.txt"), "HELLO!!!!")
def deploy(self):
copy(self, "*", src=self.package_folder, dst=self.deploy_folder)
""")
c.save({"dep/conanfile.py": conanfile.format(name="dep", requires=""),
"pkg/conanfile.py": conanfile.format(name="pkg", requires="requires='dep/0.1'")})
c.run("create dep")
assert "Executing deploy()" not in c.out
c.run("create pkg")
assert "Executing deploy()" not in c.out

# Doesn't install by default
c.run("install --requires=pkg/0.1")
assert "Executing deploy()" not in c.out

# Doesn't install with other patterns
c.run("install --requires=pkg/0.1 --deployer-package=other")
assert "Executing deploy()" not in c.out

# install can deploy all
c.run("install --requires=pkg/0.1 --deployer-package=* --deployer-folder=mydeploy")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, I think that this is a great interface. You're right, we might want to deploy multiple packages at once. I'm wondering about overloading the --deployer-folder though - is the design intention here that each deployer is responsible for namespacing itself within the deployer-folder? For instance, if I run

$ conan install --requires=foo/0.1.0 --deployer-package=foo/0.1.0 --deployer=licenses --deployer=full_deploy --deployer-folder=/tmp/deploy-test

then each of the deployers (kind of) conflict:

$tree /tmp/deploy-test -L 2
/tmp/deploy-test
├── conaninfo.txt
├── conanmanifest.txt
├── full_deploy
│   └── host
├── include
│   └── foo.h
├── lib
│   └── libfoo.a
└── licenses.zip

full_deploy namespaces itself properly, licenses.py (from the conan-extensions repo) places licenses.zip in the root of the deployer folder, and in this case I've emulated your deploy() method, which copies the package folder to the root. If this is the most desirable design, then a best practice would need to emerge to avoid contaminating other deployments (i.e., by outputing to a subfolder of deploy-folder). Or is there another design that might remove the need for a best practice?

I guess removing the need for a best practice also has issues - this is the most flexible, and gives the user the most room to construct things the way they want, at the cost of a (potential) maintenance overhead in ensuring deployers deploy consistently if there's a possibility of a clash.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I think it should be the responsibility of the users to namespace their deploys if that is what they want/need. If we try to namespace something, then it would be certainly blocking for users that want to deploy all .dlls in a single common folder to all dependencies for example, and they want to do it in their deploy() method.

Once deployers are running from different origins and different packages deploy() now, the space for conflict is always there (they could be deploying directly to /usr/bin etc, and conflicting anyway). The full_deploy implement the namespace in itself, it is not something external, with the intention that users can take it as inspiration and easily customize the namespace/layout of the deployment to fit their needs.

The rule of thumb is that deployers copy to user space, and as such it is the full responsibiliy of the user (via the different deployers implementations) to control that user space. And then, yes, some common practices might happen with some common namespaces patterns, but that should be it, just some common practices, not something mandated or implemented by the tool.

assert "dep/0.1: Executing deploy()" in c.out
assert "pkg/0.1: Executing deploy()" in c.out
assert c.load("mydeploy/mydepfile.txt") == "HELLO!!!!"
assert c.load("mydeploy/mypkgfile.txt") == "HELLO!!!!"

# install can deploy only "pkg"
c.run("install --requires=pkg/0.1 --deployer-package=pkg/* --deployer-folder=mydeploy")
assert "dep/0.1: Executing deploy()" not in c.out
assert "pkg/0.1: Executing deploy()" in c.out