Repository: pyinstaller/pyinstaller

Issue:
PyInstaller 5.0 can't find icon when running spec file out of cwd
## Description of the issue
```
root/
├── file.py
├── icon
│   └── icons_library.ico
├── pyinstaller_build
│   └── file.spec
```
With the above file structure, PyInstaller now tries to resolve the icon both relative to the spec file's location, and the cwd, making the icon to be impossible to find. This worked fine in previous version.


Running from the `root` directory,
with the icon set to `../icon/icons_library.ico` an exception is raised from https://github.com/pyinstaller/pyinstaller/blob/68a79e1c089aece4bfe93eae58b3edaa0ff223ec/PyInstaller/building/icon.py#L32-L34 and printing the path reveals it's relative to the cwd (goes above the root).

When changed to `./icon/icons_library.ico`, PyInstaller ends with `Unable to open icon file ./icon/icons_library.ico` https://github.com/pyinstaller/pyinstaller/blob/68a79e1c089aece4bfe93eae58b3edaa0ff223ec/PyInstaller/utils/win32/icon.py#L100-L106

### Context information (for bug reports)

* Output of `pyinstaller --version`: `5.0`
* Version of Python: 3.10.4
* Platform: Windows
* How you installed Python: python.org/downloads
* Did you also try this on another platform? Does it work there? Tried windows only

### A minimal example program which shows the error
https://github.com/Numerlor/pyinstaller_icon
set up project (install pyinstaller/`poetry install`) and run `pyinstaller .\pyinstaller_build\file.spec` from the project root



Discussion:
> With the above file structure, PyInstaller now tries to resolve the icon both relative to the spec file's location, and the cwd, making the icon to be impossible to find. This worked fine in previous version.

I suppose that we need to choose which behaviour we actually want. I vote for `./foo` being equivalent to `$PWD/foo` only and we ditch this *relative to the .spec file* behaviour everywhere it has been inconsistently applied. But whichever we go for - it must be one and not both. If we do choose to anchor paths relative to the spec file then we should apply that to `datas` and `binaries` too and implement it the same way that Sphinx does with its `conf.py` - by `cd`-ing to that file's parent directory before running it.
> I suppose that we need to choose which behaviour we actually want. I vote for `./foo` being equivalent to `$PWD/foo` only and we ditch this _relative to the .spec file_ behaviour everywhere it has been inconsistently applied.  But whichever we go for - it must be one and not both. If we do choose to anchor paths relative to the spec file then we should apply that to `datas` and `binaries` too ...

Being relative to the spec file is how the paths act now as far as I can tell (at least for datas) as I used paths relative to the spec file in the project where I encountered this bug https://github.com/Numerlor/Auto_Neutron/blob/f17fae47e6b5b92b89f01a274e367b9275c58400/pyinstaller_build/spec_helper.py#L7-L11 

Being relative to the working directory feels like what I'd expect initially, though if it's a file I think there's some merit to the paths inside of it being relative to the file's location (except for pathex, maybe hooks too though I didn't check those) - which is the behaviour now (or up to now).
I'd be vary of changing it all to be relative to the cwd as it is a breaking change, though I'm not sure in how many cases the file's location and working directory differ.


FWIW I was hit by this as well, as I have the `.spec` file in a `misc/` subdirectory, but run it from the project root. I assume that's something a few larger projects are doing, but I might be wrong.

As a workaround, an absolute icon path can be constructed in the `.spec` file. Unfortunately, `__file__` doesn't seem to be set, but at least for my case, it was fine to assume that the cwd is the repository root: https://github.com/qutebrowser/qutebrowser/commit/07ce0349837fffc221184857be56517a2d208bd8
There's a `SPECPATH` global which points to the parent directory of the spec file for anchoring files relative to it (although I personally wish we just used `__file__`).
I guess since `datas` works relative to the spec file then the icon should too. I do still think that if we go down this path then PyInstaller should just `cd` to the spec's parent directory so that `datas = [("foo/*.txt", ".")]` and `datas = [(i, ".") for i in glob.glob("foo/*.txt")]` aren't doing two totally different things but that will have to wait until the next major release.
> I do still think that if we go down this path then PyInstaller should just `cd` to the spec's parent directory

Note that would also change where e.g. `build/` and `dist/` end up. I think if I start PyInstaller in the repo root, I would expect the output to be there (or if I want to e.g. test something, I might want to cd to a different output directory). In contrast to that, it makes sense to resolve *input* files relative to the spec file (rather than cwd) so that the spec file works no matter where PyInstaller was invoked from. It would be weird if a `mkdir pyinstaller-test; cd pyinstaller-test; pyinstaller ../myapp.spec` required changes to the spec file, and it wouild also be weird if the output landed in `..`.
> It would be weird if a mkdir pyinstaller-test; cd pyinstaller-test; pyinstaller ../myapp.spec required changes to the spec file, and it wouild also be weird if the output landed in ...

Personally I'd like `dist` and `build` to be controlled by the spec and therefore would expect the output to land in `..` so that the `cwd` is effectively irrelevant. But yeah, it doesn't make much sense no matter what we do so just not changing things is probably best. If the spec file was a declarative config/toml/yaml file  instead of half a Python script then the _relative paths are relative to the spec_ pattern would feel less of an inconsistency to me but that's never likely to happen either.

Resolving patch:
diff --git a/PyInstaller/building/api.py b/PyInstaller/building/api.py
--- a/PyInstaller/building/api.py
+++ b/PyInstaller/building/api.py
@@ -468,6 +468,10 @@ def __init__(self, *args, **kwargs):
             # no value; presence means "true"
             self.toc.append(("pyi-macos-argv-emulation", "", "OPTION"))
 
+        # If the icon path is relative, make it relative to the .spec file.
+        if self.icon and self.icon != "NONE" and not os.path.isabs(self.icon):
+            self.icon = os.path.join(CONF['specpath'], self.icon)
+
         if is_win:
             if not self.exclude_binaries:
                 # onefile mode forces embed_manifest=True
diff --git a/PyInstaller/utils/win32/icon.py b/PyInstaller/utils/win32/icon.py
--- a/PyInstaller/utils/win32/icon.py
+++ b/PyInstaller/utils/win32/icon.py
@@ -214,10 +214,7 @@ def splitter(s):
     if srcext.lower() == '.ico':
         return CopyIcons_FromIco(dstpath, [srcpath])
 
-    # Single source is not .ico, presumably it is .exe (and if not, some error will occur). If relative, make it
-    # relative to the .spec file.
-    if not os.path.isabs(srcpath):
-        srcpath = os.path.join(config.CONF['specpath'], srcpath)
+    # Single source is not .ico, presumably it is .exe (and if not, some error will occur).
     if index is not None:
         logger.info("Copying icon from %s, %d", srcpath, index)
     else:


Test patch:
diff --git a/tests/functional/test_basic.py b/tests/functional/test_basic.py
--- a/tests/functional/test_basic.py
+++ b/tests/functional/test_basic.py
@@ -387,6 +387,7 @@ def test_set_icon(pyi_builder, data_dir):
         args = ['--windowed', '--icon', os.path.join(data_dir.strpath, 'pyi_icon.icns')]
     else:
         pytest.skip('option --icon works only on Windows and Mac OS X')
+    os.chdir(os.path.expanduser("~"))
     pyi_builder.test_source("print('Hello Python!')", pyi_args=args)