Repository: dbt-labs/dbt-core

Issue:
[CT-2876] [Feature] add possibility to copy dbt local packages instead of make it symlink
### Is this your first time submitting a feature request?

- [X] I have read the [expectations for open source contributors](https://docs.getdbt.com/docs/contributing/oss-expectations)
- [X] I have searched the existing issues, and I could not find an existing issue for this feature
- [X] I am requesting a straightforward extension of existing dbt functionality, rather than a Big Idea better suited to a discussion

### Describe the feature

Hello team,

This is a feature request that comes from the following issues/discussions:
- https://github.com/dbt-labs/dbt-core/issues/5271
- https://github.com/dbt-labs/dbt-core/issues/4538

In this piece of code https://github.com/dbt-labs/dbt-core/blob/main/core/dbt/deps/local.py#L62 

```python
 if can_create_symlink: 
     fire_event(DepsCreatingLocalSymlink())
     system.make_symlink(src_path, dest_path)
 else: 
     fire_event(DepsSymlinkNotAvailable())
     shutil.copytree(src_path, dest_path)
```

we are evaluating whether symlink can be setuped. In case the OS does not support symlink (i.e. Windows) we do a deep copy.

We would like to be able to do a dbt local packages deep copy on Linux machines, for instance. 

The reasons for this aim are:
- `gsutils rsync` [does not support](https://cloud.google.com/storage/docs/gsutil/commands/rsync#be-careful-when-synchronizing-over-os-specific-file-types-symlinks,-devices,-etc.) symlinks as well as `rsync` does not support symlinks
- `Google Cloud Storage` [does not support](https://stackoverflow.com/questions/57777777/symlink-or-an-alternative-solution-on-gcs) symlinks

### Describe alternatives you've considered

As a workaround, I had to write custom Python code, that iterates thru all the packages we have, find the symlink, and make a deep copy of all the content.

### Who will this benefit?

In my humble opinion, at least, GCP users would benefit a lot

### Are you interested in contributing this feature?

There is a chance, with all the context and pitfalls to be provided

### Anything else?

Probably, no. However, if I miss something please feel free to ask all the questions and raise all the concerns you might be having


Discussion:

Resolving patch:
diff --git a/core/dbt/deps/local.py b/core/dbt/deps/local.py
--- a/core/dbt/deps/local.py
+++ b/core/dbt/deps/local.py
@@ -51,19 +51,15 @@ def install(self, project, renderer):
         src_path = self.resolve_path(project)
         dest_path = self.get_installation_path(project, renderer)
 
-        can_create_symlink = system.supports_symlinks()
-
         if system.path_exists(dest_path):
             if not system.path_is_symlink(dest_path):
                 system.rmdir(dest_path)
             else:
                 system.remove_file(dest_path)
-
-        if can_create_symlink:
+        try:
             fire_event(DepsCreatingLocalSymlink())
             system.make_symlink(src_path, dest_path)
-
-        else:
+        except OSError:
             fire_event(DepsSymlinkNotAvailable())
             shutil.copytree(src_path, dest_path)
 


Test patch:
diff --git a/tests/unit/test_deps.py b/tests/unit/test_deps.py
--- a/tests/unit/test_deps.py
+++ b/tests/unit/test_deps.py
@@ -6,7 +6,7 @@
 import dbt.deps
 import dbt.exceptions
 from dbt.deps.git import GitUnpinnedPackage
-from dbt.deps.local import LocalUnpinnedPackage
+from dbt.deps.local import LocalUnpinnedPackage, LocalPinnedPackage
 from dbt.deps.tarball import TarballUnpinnedPackage
 from dbt.deps.registry import RegistryUnpinnedPackage
 from dbt.clients.registry import is_compatible_version
@@ -92,6 +92,21 @@ def test_init(self):
         self.assertEqual(a_pinned.source_type(), "git")
         self.assertIs(a_pinned.warn_unpinned, True)
 
+    @mock.patch("shutil.copytree")
+    @mock.patch("dbt.deps.local.system.make_symlink")
+    @mock.patch("dbt.deps.local.LocalPinnedPackage.get_installation_path")
+    @mock.patch("dbt.deps.local.LocalPinnedPackage.resolve_path")
+    def test_deps_install(
+        self, mock_resolve_path, mock_get_installation_path, mock_symlink, mock_shutil
+    ):
+        mock_resolve_path.return_value = "/tmp/source"
+        mock_get_installation_path.return_value = "/tmp/dest"
+        mock_symlink.side_effect = OSError("Install deps symlink error")
+
+        LocalPinnedPackage("local").install("dummy", "dummy")
+        self.assertEqual(mock_shutil.call_count, 1)
+        mock_shutil.assert_called_once_with("/tmp/source", "/tmp/dest")
+
     def test_invalid(self):
         with self.assertRaises(ValidationError):
             GitPackage.validate(