Repository: lutris/lutris

Issue:
Merge installer task does not work for directory to directory copy on Python < 3.8
### Bug description

The `merge` functionality of the installer does not work for directory to directory copies with Python version < 3.8. This used to work in Lutris about a year ago. Lutris source code alludes to support for versions < 3.8 but this does not appear to be the case for the `merge` function.

FYI - I realize that you removed the issue template about a month ago. I thought it was a good template so I used it for this issue. If you'd like me to not do this in the future, please let me know and I won't. :)

### How to Reproduce

1. Create a script using an `merge` installer task:
```

name: Bug Reproducer
game_slug: bug-reproducer
version: Installer
slug: my-game-installer
runner: linux

script:
  game:
    game_id: test
    path: $GAMEDIR
    subtitles: true
  installer:
  - merge:
      dst: $GAMEDIR/bugreproducer-dst
      src: /tmp/bugreproducer-src
```
2. Create source directory with file inside: `mkdir -p /tmp/bugreproducer-src && touch /tmp/bugreproducer-src/test`
3. Run Lutris with this script: `lutris -d -i script.yml`
4. Select a installation directory.
5. See that Lutris fails to install because "FileExistsError(17, 'File Exists')

### Expected behavior

After selecting a installation directory, Lutris not fail on attempting to copy from `/tmp/bugreproducer-src` to the destination directory.

### Log output

```shell
$ lutris -d -i script.yml 
INFO     2022-06-20 10:56:41,768 [startup.init_lutris:173]:Starting Lutris 0.5.10
INFO     2022-06-20 10:56:41,870 [startup.check_driver:69]:Running Intel Open Source Technology Center Mesa driver 20.0.8 on Mesa DRI Intel(R) HD Graphics 620 (KBL GT2) (0x5916)
INFO     2022-06-20 10:56:41,870 [startup.check_driver:77]:GPU: 8086:5916 1028:07A8 (i915 drivers)
WARNING  2022-06-20 10:56:41,913 [libretro.get_platform:117]:'mupen64plus' not found in Libretro cores
WARNING  2022-06-20 10:56:41,913 [game.set_platform_from_runner:259]:The Libretro runner didn't provide a platform for Mario Kart 64 (libretro)
WARNING  2022-06-20 10:56:41,915 [libretro.get_platform:117]:'mupen64plus' not found in Libretro cores
WARNING  2022-06-20 10:56:41,915 [game.set_platform_from_runner:259]:The Libretro runner didn't provide a platform for Mario Tennis (libretro)
Gtk-Message: 10:56:41.932: GtkDialog mapped without a transient parent. This is discouraged.
INFO     2022-06-20 10:56:41,936 [startup.update_runtime:213]:Startup complete
DEBUG    2022-06-20 10:56:42,113 [lutriswindow.update_store:437]:Showing 41 games
DEBUG    2022-06-20 10:56:42,366 [__init__.read_script:14]:Loading script(s) from /tmp/script.yml
DEBUG    2022-06-20 10:56:42,380 [application.show_window:332]:Showing window InstallerWindowbug-reproducer
DEBUG    2022-06-20 10:56:44,125 [displayconfig.get_current_resolution:624]:Retrieving current resolution
DEBUG    2022-06-20 10:56:44,140 [interpreter.get_extras:159]:Checking if service provide extra files
INFO     2022-06-20 10:56:44,141 [installer.prepare_game_files:143]:No files to prepare
DEBUG    2022-06-20 10:56:44,141 [installerwindow.on_runners_ready:312]:Installer doesn't require files
DEBUG    2022-06-20 10:56:44,143 [interpreter._iter_commands:317]:Installer command: {'merge': {'dst': '$GAMEDIR/bugreproducer-dst', 'src': '/tmp/bugreproducer-src'}}
DEBUG    2022-06-20 10:56:44,148 [commands.merge:275]:Merging /tmp/bugreproducer-src into None/bugreproducer-dst
DEBUG    2022-06-20 10:56:44,169 [system.merge_folders:213]:Merging /tmp/bugreproducer-src into None/bugreproducer-dst
ERROR    2022-06-20 10:56:44,274 [jobs.target:36]:Error while completing task <bound method CommandsMixin.merge of <interpreter.ScriptInterpreter object at 0x7f98a350abd0 (lutris+installer+interpreter+ScriptInterpreter at 0x14b5fa0)>>: <class 'FileExistsError'> [Errno 17] File exists: 'None/bugreproducer-dst'
  File "/usr/lib/python3/dist-packages/lutris/util/jobs.py", line 34, in target
    result = self.function(*args, **kwargs)
  File "/usr/lib/python3/dist-packages/lutris/installer/commands.py", line 291, in merge
    self._killable_process(system.merge_folders, src, dst)
  File "/usr/lib/python3/dist-packages/lutris/installer/commands.py", line 550, in _killable_process
    result = result_obj.get()  # Wait process end & re-raise exceptions
  File "/usr/lib/python3.6/multiprocessing/pool.py", line 644, in get
    raise self._value
ERROR    2022-06-20 10:56:44,279 [interpreter._iter_commands:302]:Last install command failed, show error
Gtk-Message: 10:56:45.835: GtkDialog mapped without a transient parent. This is discouraged.
INFO     2022-06-20 10:57:21,619 [interpreter.revert:374]:Cancelling installation of Bug Reproducer
DEBUG    2022-06-20 10:57:21,620 [system.remove_folder:229]:Removing folder /home/traack/.cache/lutris/installer/bug-reproducer
DEBUG    2022-06-20 10:57:21,621 [application.on_app_window_destroyed:350]:Removed window InstallerWindowbug-reproducer
```

### System Information

```shell
[System]
OS:              Ubuntu 18.04 Bionic Beaver
Arch:            x86_64
Kernel:          4.15.0-180-generic
Desktop:         ubuntu:GNOME
Display Server:  x11

[CPU]
Vendor:          GenuineIntel
Model:           Intel(R) Core(TM) i5-7300U CPU @ 2.60GHz
Physical cores:  2
Logical cores:   4

[Memory]
RAM:             15.5 GB
Swap:            10.0 GB

[Graphics]
Vendor:          Intel Open Source Technology Center
OpenGL Renderer: Mesa DRI Intel(R) HD Graphics 620 (KBL GT2)
OpenGL Version:  3.0 Mesa 20.0.8
OpenGL Core:     4.6 (Core Profile) Mesa 20.0.8
OpenGL ES:       OpenGL ES 3.2 Mesa 20.0.8
Vulkan:          Supported
```

### Media (optional)

_No response_

### Checklist:

- [X] I'm not asking for support with a game or the wine runner.
- [X] I have followed the above mentioned guides and have all the graphics and wine dependencies installed.
- [X] I have checked for existing issues that describe my problem prior to opening this one.
- [X] I understand that improperly formatted bug reports may be closed without explanation.


Discussion:

Resolving patch:
diff --git a/lutris/util/system.py b/lutris/util/system.py
--- a/lutris/util/system.py
+++ b/lutris/util/system.py
@@ -216,7 +216,22 @@ def merge_folders(source, destination):
     if "dirs_exist_ok" in sig.parameters:
         shutil.copytree(source, destination, symlinks=False, ignore_dangling_symlinks=True, dirs_exist_ok=True)
     else:
-        shutil.copytree(source, destination, symlinks=False, ignore_dangling_symlinks=True)
+        source = os.path.abspath(source)
+        for (dirpath, dirnames, filenames) in os.walk(source):
+            source_relpath = dirpath[len(source):].strip("/")
+            dst_abspath = os.path.join(destination, source_relpath)
+            for dirname in dirnames:
+                new_dir = os.path.join(dst_abspath, dirname)
+                logger.debug("creating dir: %s", new_dir)
+                try:
+                    os.mkdir(new_dir)
+                except OSError:
+                    pass
+            for filename in filenames:
+                # logger.debug("Copying %s", filename)
+                if not os.path.exists(dst_abspath):
+                    os.makedirs(dst_abspath)
+                shutil.copy(os.path.join(dirpath, filename), os.path.join(dst_abspath, filename), follow_symlinks=False)
 
 
 def remove_folder(path):


Test patch: