Repository: napari/napari

Issue:
Issue with using wrong pyrcc
## 🐛 Bug

<!-- A clear and concise description of what the bug is. -->
I updated `napari` to post- #1229 and `napari` started failing when trying to build icons on Windows after importing napari:
https://github.com/napari/napari/blob/96e9722419a5b0adddbc14fe12c69a7d769f8758/napari/resources/build_icons.py#L178

## Cause

This may be a non-issue for others, but became an issue for me. Basically, each of these binaries were called:
`'pyrcc5.bat', 'pyrcc5', 'pyside2-rcc'`

and my vanilla python virtual environment didn't have `pyrcc5.bat`, but my base anaconda install was on path (bottom of list), and was the only source of `pyrcc5.bat`. This then failed, because it was found, but didn't have `pyqt5-sip` I believe. Rearranging so that `pyrcc5` was tried first fixed the issue for me.

This is a weird bug because I happened to have an Anaconda install on my path that happened to have `pyrcc5.bat`, and is kind of my fault. I don't know if it makes sense for you to rearrange the binaries to try, or ensure it's coming from the same environment. At least this is an explanation for anyone else getting a similar error:

```
subprocess.CalledProcessError: Command '['pyrcc5.bat', '-o', 'XXX\\lib\\site-packages\\napari\\resources\\_qt_resources_0.3.1_pyqt5_5.14.0.py', 'XXX\\lib\\site-packages\\napari\\resources\\res.qrc']' returned non-zero exit status 1.
```

@tlambert03 


Discussion:
wow!   \*shakes fist\*
thank you for your _very_ helpful debugging!

so wait... you _do_ have a binary called `pyrcc5` on windows?  Can you tell exactly where that one was coming from?  (i.e. was it from pyqt5 installed in your environment?  with conda?  with pip?). I don't _think_ it would be a problem to just try pyrcc5 first... but need to check some things.  I think it should also be possible to check in the environment first...
I was losing my mind as I tried to figure it out. I thought I just had some incompatible `pyqt5` version.

From within my virtual environment:
```
(venv) λ where pyrcc5
XXX\venv\Scripts\pyrcc5.exe
XXX\AppData\Local\Programs\Python\Python38\Scripts\pyrcc5.exe
XXX\AppData\Local\Programs\Python\Python37\Scripts\pyrcc5.exe
XXX\Anaconda3\Library\bin\pyrcc5.bat
```

I believe `pyrcc5.exe` is coming from `pyqt5` or `pyqt5-sip` installed in those environments
yeah I feel your pain!  😂 thanks again.  And just to double check, the pyqt5 that is installed in `venv\Scripts` and `Python<38|37>\Scripts` came from pip right?
Correct. Completely pip installs
running into the same issue. Clean conda environments, `pip install napari` and end up with:

```
(cp4rc1) λ napari
Traceback (most recent call last):
  File "C:\Users\Volker\Anaconda3\envs\napari\lib\runpy.py", line 193, in _run_module_as_main
    "__main__", mod_spec)
  File "C:\Users\Volker\Anaconda3\envs\napari\lib\runpy.py", line 85, in _run_code
    exec(code, run_globals)
  File "C:\Users\Volker\Anaconda3\envs\napari\lib\site-packages\PyQt5\pyrcc_main.py", line 21, in <module>
    from PyQt5.QtCore import PYQT_VERSION_STR, QDir, QFile
ImportError: DLL load failed: The specified procedure could not be found.
Traceback (most recent call last):
  File "c:\users\volker\anaconda3\envs\cp4rc1\lib\runpy.py", line 193, in _run_module_as_main
    return _run_code(code, main_globals, None,
  File "c:\users\volker\anaconda3\envs\cp4rc1\lib\runpy.py", line 86, in _run_code
    exec(code, run_globals)
  File "C:\Users\Volker\Anaconda3\envs\cp4rc1\Scripts\napari.exe\__main__.py", line 4, in <module>
  File "c:\users\volker\anaconda3\envs\cp4rc1\lib\site-packages\napari\__init__.py", line 50, in <module>
    from .viewer import Viewer
  File "c:\users\volker\anaconda3\envs\cp4rc1\lib\site-packages\napari\viewer.py", line 6, in <module>
    from ._qt.qt_update_ui import QtUpdateUI
  File "c:\users\volker\anaconda3\envs\cp4rc1\lib\site-packages\napari\_qt\__init__.py", line 5, in <module>
    import_resources()
  File "c:\users\volker\anaconda3\envs\cp4rc1\lib\site-packages\napari\resources\__init__.py", line 119, in import_resources
    respath = build_pyqt_resources(fspath(target_file), overwrite=overwrite)
  File "c:\users\volker\anaconda3\envs\cp4rc1\lib\site-packages\napari\resources\build_icons.py", line 178, in build_pyqt_resources
    check_call([binary, '-o', out_path, qrc_path])
  File "c:\users\volker\anaconda3\envs\cp4rc1\lib\subprocess.py", line 364, in check_call
    raise CalledProcessError(retcode, cmd)
subprocess.CalledProcessError: Command '['pyrcc5.bat', '-o', 'c:\\users\\volker\\anaconda3\\envs\\cp4rc1\\lib\\site-packages\\napari\\resources\\_qt_resources_0.3.1_pyside2_5.14.2.py', 'c:\\users\\volker\\anaconda3\\envs\\cp4rc1\\lib\\site-packages\\napari\\resources\\res.qrc']' returned non-zero exit status 1.
```
@tlambert03 I guess one way to prevent this is to only look for executables sitting next to `sys.executable`?
@hectormz @VolkerH , can you guys try deleting your `_qt_resources.py` file and replace the bit in `napari/resources/build_icons.py` [at line 176](https://github.com/napari/napari/blob/f0ab04af8bf3854325af1e44b5214c4710cab980/napari/resources/build_icons.py#L176-L187) with the following:
```python
    for bin_name in ('pyrcc5', 'pyside2-rcc'):
        binary = shutil.which(bin_name)
        if binary:
            check_call([binary, '-o', out_path, qrc_path])
            break
    else:
        raise FileNotFoundError(
            "Unable to find an executable to build Qt resources (icons).\n"
            "Tried: 'pyrcc5.bat', 'pyrcc5', 'pyside2-rcc'.\n"
            "Please open issue at https://github.com/napari/napari/issues/."
        )
```

in my test on windows `shutil.which` was able to find both `pyrcc5.bat` and `pyrcc5.exe` and prefers things higher up on the PATH.


Resolving patch:
diff --git a/napari/resources/build_icons.py b/napari/resources/build_icons.py
--- a/napari/resources/build_icons.py
+++ b/napari/resources/build_icons.py
@@ -6,6 +6,7 @@
 import os
 import re
 import shutil
+import sys
 from subprocess import check_call
 from typing import Dict, List, Tuple
 
@@ -132,6 +133,46 @@ def build_resources_qrc(
     return qrc_path, theme_dir
 
 
+def _find_rcc_or_raise() -> str:
+    """Locate the Qt rcc binary to generate resource files
+
+    1. we always want to use pyrcc5 if it's available, regardless of API
+    2. it will sometimes, (if not always) be named pyrcc5.bat on windows...
+       but shutil.which() will find that too
+    3. We also want to prefer binaries higher up on the path, and we add
+       sys.executable to the front of the path (and \\Scripts on windows)
+    4. after pyrcc5 we try pyside2-rcc
+
+    see https://github.com/napari/napari/issues/1221
+    and https://github.com/napari/napari/issues/1254
+
+    Returns
+    -------
+    path : str
+        Path to the located rcc binary, or None if not found
+
+    Raises
+    ------
+    FileNotFoundError
+        If no executable can be found.
+    """
+    python_dir = os.path.dirname(sys.executable)
+    paths = [python_dir, os.environ.get("PATH", '')]
+    if os.name == 'nt':
+        paths.insert(0, os.path.join(python_dir, 'Scripts'))
+    path = os.pathsep.join(paths)
+
+    for bin_name in ('pyrcc5', 'pyside2-rcc'):
+        rcc_binary = shutil.which(bin_name, path=path)
+        if rcc_binary:
+            return rcc_binary
+    raise FileNotFoundError(
+        "Unable to find an executable to build Qt resources (icons).\n"
+        "Tried: 'pyrcc5.bat', 'pyrcc5', 'pyside2-rcc'.\n"
+        "Please open issue at https://github.com/napari/napari/issues/."
+    )
+
+
 def build_pyqt_resources(out_path: str, overwrite: bool = False) -> str:
     """Build a res.qrc file from icons and convert for python usage.
 
@@ -167,24 +208,7 @@ def build_pyqt_resources(out_path: str, overwrite: bool = False) -> str:
     )
 
     # then convert it to a python file
-    # this looks a little funny, but rather than pick the exact binary to try
-    # once, we use this try/catch loop because
-    # 1. we always want to use pyrcc5 if it's available, regardless of API
-    # 2. it will sometimes, (if not always) be named pyrcc5.bat on windows...
-    # 3. only then do we try pyside2-rcc
-    # see https://github.com/napari/napari/issues/1221 for background
-    for binary in ('pyrcc5.bat', 'pyrcc5', 'pyside2-rcc'):
-        try:
-            check_call([binary, '-o', out_path, qrc_path])
-        except FileNotFoundError:
-            continue
-        break
-    else:
-        raise FileNotFoundError(
-            "Unable to find an executable to build Qt resources (icons).\n"
-            "Tried: 'pyrcc5.bat', 'pyrcc5', 'pyside2-rcc'.\n"
-            "Please open issue at https://github.com/napari/napari/issues/."
-        )
+    check_call([_find_rcc_or_raise(), '-o', out_path, qrc_path])
 
     # make sure we import from qtpy
     with open(out_path, "rt") as fin:


Test patch: