Repository: pyinstaller/pyinstaller

Issue:
PermissionError in assemble / copy2 / copystat (only in directory build mode)
## Description of the issue

Trying to build in directory mode fails due to a permissions error.

### Context information (for bug reports)

* Output of `pyinstaller --version`: ```5.13.2```
* Version of Python: 3.11.2 (same with 3.11.5)
* Platform: FreeBSD 13
* How you installed Python: pyenv
* Did you also try this on another platform? Worked on linux.
* try the latest development version: **same issue**
* follow *all* the instructions in our "If Things Go Wrong" Guide: **Read through it, but doesn't seem to help here.**

### A minimal example program which shows the error

Not minimal, but booting the borgbackup master branch freebsd vagrant VM reproduces the issue.

### Stacktrace / full error message

```
43290 INFO: checking COLLECT
43290 INFO: Building COLLECT because COLLECT-00.toc is non existent
WARNING: The output directory "/vagrant/borg/borg-dir" and ALL ITS CONTENTS will be REMOVED! Continue? (y/N)y
On your own risk, you can use the option `--noconfirm` to get rid of this question.
45418 INFO: Removing dir /vagrant/borg/borg-dir
45425 INFO: Building COLLECT COLLECT-00.toc
Traceback (most recent call last):
  File "/home/vagrant/.pyenv/versions/3.11.2/envs/borg-env/bin/pyinstaller", line 8, in <module>
    sys.exit(_console_script_run())
             ^^^^^^^^^^^^^^^^^^^^^
  File "/home/vagrant/.pyenv/versions/3.11.2/envs/borg-env/lib/python3.11/site-packages/PyInstaller/__main__.py", line 194, in _console_script_run
    run()
  File "/home/vagrant/.pyenv/versions/3.11.2/envs/borg-env/lib/python3.11/site-packages/PyInstaller/__main__.py", line 180, in run
    run_build(pyi_config, spec_file, **vars(args))
  File "/home/vagrant/.pyenv/versions/3.11.2/envs/borg-env/lib/python3.11/site-packages/PyInstaller/__main__.py", line 61, in run_build
    PyInstaller.building.build_main.main(pyi_config, spec_file, **kwargs)
  File "/home/vagrant/.pyenv/versions/3.11.2/envs/borg-env/lib/python3.11/site-packages/PyInstaller/building/build_main.py", line 1019, in main
    build(specfile, distpath, workpath, clean_build)
  File "/home/vagrant/.pyenv/versions/3.11.2/envs/borg-env/lib/python3.11/site-packages/PyInstaller/building/build_main.py", line 944, in build
    exec(code, spec_namespace)
  File "scripts/borg.exe.spec", line 68, in <module>
    coll = COLLECT(slim_exe,
           ^^^^^^^^^^^^^^^^^
  File "/home/vagrant/.pyenv/versions/3.11.2/envs/borg-env/lib/python3.11/site-packages/PyInstaller/building/api.py", line 931, in __init__
    self.__postinit__()
  File "/home/vagrant/.pyenv/versions/3.11.2/envs/borg-env/lib/python3.11/site-packages/PyInstaller/building/datastruct.py", line 184, in __postinit__
    self.assemble()
  File "/home/vagrant/.pyenv/versions/3.11.2/envs/borg-env/lib/python3.11/site-packages/PyInstaller/building/api.py", line 994, in assemble
    shutil.copy2(src_name, dest_path)  # Use copy2 to (attempt to) preserve metadata
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/vagrant/.pyenv/versions/3.11.2/lib/python3.11/shutil.py", line 437, in copy2
    copystat(src, dst, follow_symlinks=follow_symlinks)
  File "/home/vagrant/.pyenv/versions/3.11.2/lib/python3.11/shutil.py", line 397, in copystat
    lookup("chflags")(dst, st.st_flags, follow_symlinks=follow)
PermissionError: [Errno 1] Operation not permitted: '/vagrant/borg/borg-dir/libthr.so.3'

(borg-env) [vagrant@freebsd /vagrant/borg/borg]$ ls -l /vagrant/borg/borg-dir/libthr.so.3
-r--r--r--  1 vagrant  wheel  128224 May 12  2022 /vagrant/borg/borg-dir/libthr.so.3
```



Discussion:
You can essentially land on the same issue on Linux just by `shutil.copy2()`-ing a read only file twice.
```python
> echo hello > foo
> chmod 444 foo 
> ls -l foo 
-r--r--r-- 1 brenainn brenainn 6 Sep 14 18:02 foo
> python
>>> import shutil
>>> shutil.copy2("foo", "bar")
'bar'
>>> shutil.copy2("foo", "bar")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/brenainn/.pyenv/versions/3.10.9/lib/python3.10/shutil.py", line 434, in copy2
    copyfile(src, dst, follow_symlinks=follow_symlinks)
  File "/home/brenainn/.pyenv/versions/3.10.9/lib/python3.10/shutil.py", line 256, in copyfile
    with open(dst, 'wb') as fdst:
PermissionError: [Errno 13] Permission denied: 'bar'
```
Makes me wonder why we bother trying to preserve file metadata...
> Makes me wonder why we bother trying to preserve file metadata...

It might be a good idea to replace all those `copy2` with plain `copy` (I think that uses default 664 mode?). Especially since for binaries, we explicitly change the mode to 755 after the copy.
With just `copy()`, the read only permissions still propagate to the destination file for me.
`shutil.copyfile`, perhaps?
Yeah, that gives me `0644`.

Resolving patch:
diff --git a/PyInstaller/building/api.py b/PyInstaller/building/api.py
--- a/PyInstaller/building/api.py
+++ b/PyInstaller/building/api.py
@@ -1140,8 +1140,12 @@ def assemble(self):
                     raise ValueError(
                         f"Attempting to collect a duplicated file into COLLECT: {dest_name} (type: {typecode})"
                     )
-                shutil.copy2(src_name, dest_path)  # Use copy2 to (attempt to) preserve metadata
-            if typecode in ('EXTENSION', 'BINARY'):
+                # Use `shutil.copyfile` to copy file with default permissions. We do not attempt to preserve original
+                # permissions nor metadata, as they might be too restrictive and cause issues either during subsequent
+                # re-build attempts or when trying to move the application bundle. For binaries, we manually set the
+                # executable bits after copying the file.
+                shutil.copyfile(src_name, dest_path)
+            if typecode in ('EXTENSION', 'BINARY', 'EXECUTABLE'):
                 os.chmod(dest_path, 0o755)
         logger.info("Building COLLECT %s completed successfully.", self.tocbasename)
 
diff --git a/PyInstaller/building/osx.py b/PyInstaller/building/osx.py
--- a/PyInstaller/building/osx.py
+++ b/PyInstaller/building/osx.py
@@ -555,7 +555,7 @@ def assemble(self):
         self.icon = os.path.abspath(self.icon)
 
         # Copy icns icon to Resources directory.
-        shutil.copy(self.icon, os.path.join(self.name, 'Contents', 'Resources'))
+        shutil.copyfile(self.icon, os.path.join(self.name, 'Contents', 'Resources', os.path.basename(self.icon)))
 
         # Key/values for a minimal Info.plist file
         info_plist_dict = {
@@ -644,7 +644,11 @@ def assemble(self):
                     raise ValueError(
                         f"Attempting to collect a duplicated file into BUNDLE: {dest_name} (type: {typecode})"
                     )
-                shutil.copy2(src_name, dest_path)  # Use copy2 to (attempt to) preserve metadata
+                # Use `shutil.copyfile` to copy file with default permissions. We do not attempt to preserve original
+                # permissions nor metadata, as they might be too restrictive and cause issues either during subsequent
+                # re-build attempts or when trying to move the application bundle. For binaries, we manually set the
+                # executable bits after copying the file.
+                shutil.copyfile(src_name, dest_path)
             if typecode in ('EXTENSION', 'BINARY', 'EXECUTABLE'):
                 os.chmod(dest_path, 0o755)
 
diff --git a/PyInstaller/building/utils.py b/PyInstaller/building/utils.py
--- a/PyInstaller/building/utils.py
+++ b/PyInstaller/building/utils.py
@@ -249,17 +249,10 @@ def process_collected_binary(
     # Ensure parent path exists
     os.makedirs(os.path.dirname(cached_name), exist_ok=True)
 
-    # There are known some issues with 'shutil.copy2' on Mac OS 10.11 with copying st_flags. Issue #1650.
-    # 'shutil.copy' copies also permission bits and it should be sufficient for PyInstaller's purposes.
-    shutil.copy(src_name, cached_name)
-    # TODO: find out if this is still necessary when no longer using shutil.copy2()
-    if hasattr(os, 'chflags'):
-        # Some libraries on FreeBSD have immunable flag (libthr.so.3, for example). If this flag is preserved,
-        # os.chmod() fails with: OSError: [Errno 1] Operation not permitted.
-        try:
-            os.chflags(cached_name, 0)
-        except OSError:
-            pass
+    # Use `shutil.copyfile` to copy the file with default permissions bits, then manually set executable
+    # bits. This way, we avoid copying permission bits and metadata from the original file, which might be too
+    # restrictive for further processing (read-only permissions, immutable flag on FreeBSD, and so on).
+    shutil.copyfile(src_name, cached_name)
     os.chmod(cached_name, 0o755)
 
     if cmd:


Test patch: