Repository: conan-io/conan

Issue:
[question] deploy() in conanfile.py
### What is your question?

Hi!

The question is how can I get dependencies from cache in my current project with deploy() method from conanfile.py?

When I try to run conan install command with `--deployer-package=*` argument, I get this error:
line 272, in deploy
  File "<frozen ntpath>", line 108, in join
TypeError: expected str, bytes or os.PathLike object, not NoneType

ERROR: expected str, bytes or os.PathLike object, not NoneType

I tried to use the syntax from documentation:
```
def deploy(self):
        copy(self, "*", src=self.package_folder, dst=self.deploy_folder)
```
`self.package_folder` - what represents? Is it the root for conan dependencies?
What I understood, self.package_folder will take binary artifacts from artifactory server, not from my dependencies.
But I would like to get dependencies from my cache.
        
Thank you!



Discussion:
Hi @Vasile-Creuco 

Thanks for your question

> What I understood, self.package_folder will take binary artifacts from artifactory server, not from my dependencies.

No, the ``self.package_folder`` is always a local folder to the cache, to a package that has already been downloaded and installed in the cache. 

The syntax is intended to be used as ``conan install --requires=mypkg/1.0 --deployer-package="*"``, is this the use case? Maybe you can share a ``conanfile`` and the exact command to reproduce this? (at the very least, it is a UX bug, that kind of trace shouldn't happen, so we might want to improve the error message)
This is the deploy() method from my `conanfile`:
```
def deploy(self):
    _build_type = self.build_type
    if 'package_destinations' in self.conan_data:
        pkgDestinations = self.conan_data['package_destinations']
        if _build_type in pkgDestinations:
            deployData = pkgDestinations[_build_type]
        else:
            deployData = pkgDestinations['default']
            
    # Iterate through the imports
    for dImport in deployData:
      # Import / Copy files from conan cache to workspace
      folderA = os.path.join(self.package_folder, dImport['source'])
      print(f"src folder: {folderA}")
      folderB = os.path.join('.', dImport['target'])
      print(f"dst folder: {folderB}\n")
      # def copy(conanfile, pattern, src, dst, keep_path=True, excludes=None, ignore_case=True):
      copy(self, pattern=dImport['pattern'], src=folderA, dst=folderB, keep_path=True, excludes=("conan*.txt", "conan*.bat", "conan*.sh"))
```
I'm also using `conadata.yml` to get requirements:

```

requires: []
# package dependency requirements for tools
tool_requires: []
# package dependency requirements only for package testing
test_requires: []
# package dependency requirements only to build
build_requires: [name/1.x.x@user/channel]

package_destinations:
  default:
  - pattern: '*.*'
    source: .
    target: ../../rootfolder
  development:
  - pattern: '*.*'
    source: .
    target: ../../rootfolder
```
Thanks for the feedback.
I am afraid that I still don't have enough info to know what is happening. A full ``conanfile`` and the exact ``conan ...`` command would be very appreciated.

Some minor comments:
- ``if 'package_destinations' in self.conan_data:`` is unnecessary. If ``package_destinations`` is not in the data, the code will crash anyway, because ``deployData`` will not be defined
- Having different destinations, specially having a ``development`` one with such a relative ``target = ../..`` path reads a bit confusing. This reads more like an external ``deployer`` than a in-recipe ``deploy()`` method, which is not intended for further development, but for final deploy (well, that is the case of deployers in general). Surely I don't know enough about the use case and the setup, but that variability, coming also from a ``self.build_type`` that is so associated to ``Release\Debug`` build-type, reads complicated to understand for me.
- Check about ``build_requires: [name/1.x.x@user/channel]`` in https://docs.conan.io/2/reference/conanfile/attributes.html#build-requires
  > build_requires are used in Conan 2 to provide compatibility with the Conan 1.X syntax, but their use is discouraged in Conan 2 and will be deprecated in future 2.X releases. Please use tool_requires instead of build_requires in your Conan 2 recipes.

  Not sure how you are using it in your recipe, but probably you shouldn't use it.
Many thanks, that you gave me some good improvements which I can add it in my code.

This is the conan command: 
```
conan install --name=nameProject --version=1.x.x --user=username --channel=release --build=nameProject -pr:h=profileDevelopment -pr:b=profileDevelopment -r remoteRepo --deployer-package=*  'path/where/is/my/conanfile'

```
I created my own profile which I added in my `%USERNAME%/.conan2/profiles`:

profileDevelopment

```
[settings]
build_type=Development
compiler=compiler
compiler.version=versionCompiler
os=Windows
arch=x86_64
```
Now, I thing you understand where it comes from _build_type.

Regarding the target part, why I'm using ../../models, it is because my folder structure it looks like that and if I run `conan install` command in the rootfolder where is `conanfile` (work/conanrecipes), it shoud go two steps back to add dependencies in 'models' folder, because I need them to be there:

```bash
     
    ├── ...
    ├── work         
    │   ├── conanrecipes 
    │           ├── conanfile.py
    │           ├── conandata.yml
    │   └── models           
    └── ...
    
```

I hope I made myself clear now and maybe you have an overall an idea what I'm doing.


> build_type=Development

Yeah, overriding ``build_type`` with other types rather than Release/Debug/etc, you need to make sure the build system integrations are doing what you want to do, as by default they will only manage the built-ins ``Release``, ``Debug`` etc. So ``CMakeToolchain``, ``CMakeDeps`` and all other build-systems integration might rely on the built-ins values for many of the automated configuration.

Thanks for the details, I have managed to reproduce.
Indeed the issue is when a conanfile installed by ``<path>`` contains a ``deploy()`` method. 
The feature is intended to deploy packages, not the current one, so only for ``conan install --requires=xxx`` not for ``conan isntall <path>``. This means most likely the current ``<path>`` conanfile must not be deployed, because there is no package yet to deploy.


Regarding the build_type, I'm sure it works fine. I also added in `settings.yml` file from my `%username/.conan2` in build_type section 'Development', and what I want and need.

So, you mean that deploy() method will work for a specific require when I just run command like `conan install --requires=xxx`, not for command which I showed up?

Thank you!
I am submitting a fix in https://github.com/conan-io/conan/pull/15737 for next release.

I am going to leave the possibility of calling the local consumer recipe ``deploy()`` method, and fix the logic of ``--deployer-package`` allowing exclusions, same as other places in Conan.

> So, you mean that deploy() method will work for a specific require when I just run command like conan install --requires=xxx, not for command which I showed up?

It will work for both, there will most likely be users that want their local ``deploy()`` method to be executed too.



Patch:
diff --git a/conan/internal/deploy.py b/conan/internal/deploy.py
index 36ca53ef6a6..9631645ffdd 100644
--- a/conan/internal/deploy.py
+++ b/conan/internal/deploy.py
@@ -4,8 +4,7 @@
 from conan.internal.cache.home_paths import HomePaths
 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.errors import ConanException, conanfile_exception_formatter
 from conans.util.files import rmdir, mkdir
 
 
@@ -42,15 +41,23 @@ def do_deploys(conan_api, graph, deploy, deploy_package, deploy_folder):
     mkdir(deploy_folder)
     # handle the recipe deploy()
     if deploy_package:
+        # Similar processing as BuildMode class
+        excluded = [p[1:] for p in deploy_package if p[0] in ["!", "~"]]
+        included = [p for p in deploy_package if p[0] not in ["!", "~"]]
         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):
+            if not conanfile.ref:  # virtual or conanfile.txt, can't have deployer
+                continue
+            consumer = conanfile._conan_is_consumer
+            if any(conanfile.ref.matches(p, consumer) for p in excluded):
+                continue
+            if not any(conanfile.ref.matches(p, consumer) for p in included):
                 continue
             if hasattr(conanfile, "deploy"):
                 conanfile.output.info("Executing deploy()")
                 conanfile.deploy_folder = deploy_folder
-                conanfile.deploy()
+                with conanfile_exception_formatter(conanfile, "deploy"):
+                    conanfile.deploy()
     # Handle the deploys
     cache = HomePaths(conan_api.cache_folder)
     for d in deploy or []:
diff --git a/conan/tools/files/copy_pattern.py b/conan/tools/files/copy_pattern.py
index c136a176c96..e960ea13c1f 100644
--- a/conan/tools/files/copy_pattern.py
+++ b/conan/tools/files/copy_pattern.py
@@ -2,6 +2,7 @@
 import os
 import shutil
 
+from conans.errors import ConanException
 from conans.util.files import mkdir
 
 
@@ -26,8 +27,12 @@ def copy(conanfile, pattern, src, dst, keep_path=True, excludes=None,
            ``True``
     :return: list of copied files
     """
-    assert src != dst
-    assert not pattern.startswith("..")
+    if src == dst:
+        raise ConanException("copy() 'src' and 'dst' arguments must have different values")
+    if pattern.startswith(".."):
+        raise ConanException("copy() it is not possible to use relative patterns starting with '..'")
+    if src is None:
+        raise ConanException("copy() received 'src=None' argument")
 
     # This is necessary to add the trailing / so it is not reported as symlink
     src = os.path.join(src, "")


Test patch:
diff --git a/conans/test/integration/conanfile/test_deploy_method.py b/conans/test/integration/conanfile/test_deploy_method.py
index 99af2ebce95..7579ec5c04a 100644
--- a/conans/test/integration/conanfile/test_deploy_method.py
+++ b/conans/test/integration/conanfile/test_deploy_method.py
@@ -44,3 +44,35 @@ def deploy(self):
     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
+
+
+def test_deploy_local():
+    c = TestClient()
+    conanfile = textwrap.dedent("""
+        from conan import ConanFile
+        from conan.tools.files import copy
+        class Pkg(ConanFile):
+            name = "pkg"
+            version = "0.1"
+            def deploy(self):
+                copy(self, "*", src=self.package_folder, dst=self.deploy_folder)
+        """)
+    c.save({"conanfile.py": conanfile})
+    c.run("install . --deployer-package=*", assert_error=True)
+    assert "ERROR: conanfile.py (pkg/0.1): Error in deploy() method, line 8" in c.out
+    assert "copy() received 'src=None' argument" in c.out
+
+    # Without name/version same error
+    conanfile = textwrap.dedent("""
+        from conan import ConanFile
+        class Pkg(ConanFile):
+            def deploy(self):
+                assert self.package_folder  # will fail if None
+        """)
+    c.save({"conanfile.py": conanfile})
+    c.run("install . --deployer-package=*", assert_error=True)
+    assert "ERROR: conanfile.py: Error in deploy() method, line 5" in c.out
+
+    # I can exclude the current consumer, it won't fail
+    c.run("install . --deployer-package=!& --deployer-package=*")
+    assert "Install finished successfully" in c.out
diff --git a/conans/test/unittests/source/merge_directories_test.py b/conans/test/unittests/source/merge_directories_test.py
index 8b1f7517420..b0c99743ee7 100644
--- a/conans/test/unittests/source/merge_directories_test.py
+++ b/conans/test/unittests/source/merge_directories_test.py
@@ -5,6 +5,7 @@
 
 import pytest
 
+from conans.errors import ConanException
 from conans.test.utils.test_files import temp_folder
 from conans.util.files import mkdir, save, merge_directories, load
 
@@ -73,7 +74,7 @@ def test_nested_directories(self):
 
     def test_same_directory(self):
         # Same directory cannot be merged, this should never happen
-        with pytest.raises(AssertionError):
+        with pytest.raises(ConanException):
             merge_directories(self.source, self.source)
 
     def test_parent_directory(self):