Repository: pyinstaller/pyinstaller

Issue:
multipackage (MERGE) boken in PyInstaller 3.0
In PyInstaller 3.0 no much attention was paid to 'multipackage' feature and it is broken:
- MERGE should be fixed
- MERGE tests should be migrated to pytest



Discussion:
Maybe a chance to rework this. PyInstaller supports loading modules from a external file, see `old_suite/basic/test_pyz_as_external_file.spec` and `bootloader/main`. We could change `MERGE` into using this and dramatically simplify the code.

Edit: This test-case has now been moved to `test_pyz_as_external_file` in functional/test_basic.py: `py.test -k test_pyz_as_external_file`.

I'm interested in this feature, and I might be able to help with fixing it. I did a very simple test (just an import and a print), and it seems not to be duplicating the libs, so I don't see exactly where it could be broken.

```
samsara 戝 ~/tmp/pyinst-MERGE/dist 
10712 ◯ : tree                                                                                                                                                                      
.
├── bar
│   └── bar
├── baz
│   └── baz
└── foo
    ├── bar
    ├── baz
    ├── bz2.x86_64-linux-gnu.so
    ├── _codecs_cn.x86_64-linux-gnu.so
    ├── _codecs_hk.x86_64-linux-gnu.so
    ├── _codecs_iso2022.x86_64-linux-gnu.so
    ├── _codecs_jp.x86_64-linux-gnu.so
    ├── _codecs_kr.x86_64-linux-gnu.so
    ├── _codecs_tw.x86_64-linux-gnu.so
    ├── _ctypes.x86_64-linux-gnu.so
    ├── foo
    ├── _hashlib.x86_64-linux-gnu.so
    ├── _json.x86_64-linux-gnu.so
    ├── libbz2.so.1.0
    ├── libcrypto.so.1.0.2
    ├── libexpat.so.1
    ├── libffi.so.6
    ├── libpython2.7.so.1.0
    ├── libreadline.so.6
    ├── libssl.so.1.0.2
    ├── libtinfo.so.5
    ├── libz.so.1
    ├── _multibytecodec.x86_64-linux-gnu.so
    ├── pyexpat.x86_64-linux-gnu.so
    ├── readline.x86_64-linux-gnu.so
    ├── resource.x86_64-linux-gnu.so
    ├── _ssl.x86_64-linux-gnu.so
    ├── termios.x86_64-linux-gnu.so
    ├── twisted.python._sendmsg.x86_64-linux-gnu.so
    └── zope.interface._zope_interface_coptimizations.x86_64-linux-gnu.so

3 directories, 32 files
(env2) 
```

(using d9be7da due to #1863 affecting me at least on my local enviroment)

@htgoebel could you please elaborate a bit more on the needed rewrite when you have some time?

@kalikaneko Well, the tests (which are still in the old test-suite only) all fail, Try:

```
cd tests/old_suite/
./runtests.py multipackage/test_*.py
```

My idea for rewriting it is: Instead of appending the PKG to one of the executables, create an external file pkg which is used.  The bootloader already contains code for this. But now as I'm checking the code, I'm no longer convinced that this will be a notable improvement.

Selected PyInstaller based on this feature. Disappointed to see it not working. What to do?

Not working still?
Now my app consists of two simple executables that build with pyinstaller so horrible large.
Please add this to your todo list. 
I just wanted to say that this works for me, given a small hack.

For some time I was trying to fork+exec my own application, to launch some workers due to inherent issues with multiprocessing on OS X (not pyinstaller related). To prevent unpacking overhead, I would do something like this::

```python
def pyinstaller_env():
    """
    Returns None if not in pyinstaller. (None means: Keep current environment,
    at least to Popen)

    Otherwise returns the current environment, plus the _MEIPASS2 environment
    variable, provided to ensure Pyinstaller doesn't unpack itself again when
    we exec() it.

    For multipackage bundles, this is required for the multipackage bundles to
    function at all.
    """
    if hasattr(sys, 'frozen'):
        env = os.environ
        # If Pyinstaller encounters _MEIPASS2 in the env, it will not unpack
        # and instead use that directory for the bootloader. By using this we
        # avoid unpacking again for every fork+exec.
        env['_MEIPASS2'] = sys._MEIPASS

        return env

    return


def fork_self(args):
    """ Fork self. Possibly using a different entry point (args)
    """
    print('fork_self:', args)
    env = pyinstaller_env()

    main = subprocess.Popen([sys.argv[0]] + args, env=env)

    return main
```

However, the second (smaller) application does not start, with an error like this:
Error loading Python lib '/var/folders/sy/ky6wtvhs3yx_b1856h3g8w6r0000gn/T/_MEIk5dZOT/.Python': dlopen(/var/folders/sy/ky6wtvhs3yx_b1856h3g8w6r0000gn/T/_MEIk5dZOT/.Python, 10): image not found

But, using my pyinstaller_env() function when spawning the second application does work.

Maybe this is useful for others - if you fork+exec to start one of your other MERGE'd + BUNDLE'd application, just pass _MEIPASS2 (with value set to the current _MEIPASS)

(Background: my fork+exec worker combo worked fine, except that it would spawn lots of dock icons, due to the main application being a windowed application, and pyinstaller setting up every time. That's why I created a second binary, which is otherwise identical, except that it has console=True, instead of console=False.)
Would this be suitable as a Wiki [recipe](https://github.com/pyinstaller/pyinstaller/wiki/Recipes)?

I don't get any hits on a search in the repository for `_MEIPASS2` so I wonder where in the bootloader this is tested?
_MEIPASS2 is mentioned here - https://github.com/pyinstaller/pyinstaller/wiki/Recipe-Multiprocessing

I didn't find it in the documentation either, but that recipe made me try the same on OS X/Linux, and it also seems to work there.

_MEIPASS2 is found here: bootloader/src/pyi_main.c

I don't know if this is the right 'solution', so I don't think I am qualified to comment on whether this counts as a recipe or not.
Yeah, it's confusing. I'm confused because github search still doesn't turn up that pyi_main.c hit in the 41 results for '_MEIPASS2'. But you have fingered the place it matters, [here](https://github.com/pyinstaller/pyinstaller/blob/develop/bootloader/src/pyi_main.c#L81). The bootloader starts, fetches that envar, and at [line 122](https://github.com/pyinstaller/pyinstaller/blob/develop/bootloader/src/pyi_main.c#L122) makes a critical decision.

If _MEIPASS2 was *not* defined, this is the original bootloader thread and it starts the process of creating the temp folder and unpacking into it. When that completes it will fork itself, set _MEIPASS2 and we come back to line 122 now in a second thread. Because the envar is set, it knows this is the second instance, unpacking is complete, and it can proceed to kick off the unpacked user program.

So by pre-setting the envar and then launching *the same executable* in a subprocess, you are preventing the bootloader at the head of that executable from going through the unpacking-and-forking business. It just proceeds into the user code in its subprocess thread.

If you made a subprocess out of a *different executable* it would be a mistake to set that envar because that different executable would not unpack itself, would try to import modules from the parent program's temp folder and would likely fail with an import error.

The multiprocessing recipe you point to seems to be doing this same thing. (The same code appears in [this functional test](https://github.com/pyinstaller/pyinstaller/blob/b78bfe530cdc2904f65ce098bdf2de08c9037abb/tests/functional/scripts/pyi_multiprocess_pool.py) and [this one](https://github.com/pyinstaller/pyinstaller/blob/b78bfe530cdc2904f65ce098bdf2de08c9037abb/tests/functional/scripts/pyi_multiprocess_forking.py).

However it looks odd to me for a couple of reasons, one is this:

````
if sys.platform.startswith('win'):
...
            finally:
...
                    # On some platforms (e.g. AIX) 'os.unsetenv()' is not
                    # available. 
 ````

Since the code is restricted to Windows, why is it nattering about AIX?
Yes, that is indeed what _MEIPASS2 does. However - your note on 'different executable' is not entirely correct. In this case (MERGE target), only one of the binaries (first one passed to MERGE) contains all of the dependencies. If that one unpacks first, and then the others are started with that _MEIPASS2, it just seems to work.

The multiprocessing recipe mentions Windows specifically because multiprocessing on Windows, using python2, can't do fork+exec (because it is Windows) and you need the hacks that they mention in the recipe. Why they mention AIX is beyond me. It may just be general coding practice / a habbit regarding os.unsetenv. Multiprocessing on python3 will likely require similar hacks/recipes for all platforms.
@MerlijnWajer Some tests fail unexpectedly at the moment because the loader doesn't setup the environment correctly for the parent thread sometimes. You can see a test failure [here][1], but they have also occurred on linux. Can you guess what might be causing this?

[1]: https://ci.appveyor.com/project/matysek/pyinstaller/build/1737/job/c2sw5awh2lj5dox4#L1114
Are you sure that this is related to the MERGE target? I haven't looked at pyinstaller tests before, so I don't know offhand what is going wrong there. It's also Windows, which I don't usually test pyinstaller on.

@htgoebel - any comments on my previous notes on this? Could this be related to the broken tests? I guess I should perhaps dive into the bootloader code and see if this can be fixed/changed, but I don't know what the intended behaviour is.
@MerlijnWajer See #2371.
@xoviat: Just to check, I don't see how this relates to the MERGE issue?
I don't think it does now. Originally I thought that it was related to the bootloader, but @htgoebel has come up with a more plausible explanation. 
I may look at this after #2341 is merged.
I've made some progress at [github.com/xoviat/mergemodule][1]. The main roadblock now is that Using the single package approach, the bootloader is unable to determine the correct script to start for both executables. 

[1]: https://github.com/xoviat/pyinstaller/tree/mergemodule
Unfortunately, It appears that I will have to backtrack because append_pkg cannot be used to create a package that an executable can distinguish the specific scripts to load.
Cannot you please restore the feature? It is really useful for us.

We develop a Python package and have about 10 demo Python scripts that show its capabilities.
We want to distribute the demo scripts with PyInstaller but we want to put all 10 generated exe files into same folder.
This would be a really useful feature; it is the only thing that stops me moving from py2exe to pyinstaller.
I recently discovered PyInstaller; it works very well, thanks! It seems that my program also needs this feature. I'm not sure that I would understand the codebase well enough to contribute patches, but I would be happy to help test out any solutions on which you may be working.

(I noticed the mention of [subzero](https://github.com/pyinstaller/pyinstaller/issues/2547#issuecomment-292412734) in another issue, and will check it out, but it would be great to see this feature restored to solve the problem fully.)
I don't know `pyinstaller` well enough to know if this is a bad idea, but if I just copy the second executable into the directory of the first they both work with the same shared library. I'm using `Python 3.5.1`, `Pyinstaller 3.2.1` and `MacOS 10.12.5`. It at least works for this environment.

For executables with non-conflicting dependencies this might be an option.
This requires larger changes to the bootloader and will not make it into release 3.3.
Just discovered this feature in the docs. I'm building a multi-program bundle using PyInstaller by having one spec file for each program and building them individually. When done I simply copy all the files into the same directory (seems to be what @Terrabits is doing as well). The project is [ActivityWatch](https://github.com/ActivityWatch/activitywatch).

Haven't had any issues with it, so I'm wondering what the practical differences are.
The idea behind MERGE mostly applies to `onefile` executables.

With `onedir` exes, all of the Python bytecode is archived into the main executable, while the rest of the app dependencies (including data files and shared libraries) are stored in the executable's folder. This is why it often works to just merge two `onedir` exes file-wise.

With `onefile` exes, every single file is archived into the main exe (and extracted to a temp folder at runtime). Putting two of these exes in the same folder means that there will be many files that are archived twice, with one copy in each exe. What MERGE allows is to have the first exe contain all of the dependencies, and the second exe just have references to the first one instead of second copies of, for example, `python.dll`.
Please fix it, it can be really important
If one needs this, please provide a pull request or sponsor a project grant.
@htgoebel Could you comment on the larger changes required to the bootloader?
Are you a C programmer? Look at the PRs I submitted and fix them.
Yeah, C is no problem. Can you link me to the relevant PRs? You have rather a lot. :) 
See gh-2416. Note that you'll need to extract the relevant changes manually (ugh!) and then clean them up so that just the bootloader is modified. In addition, you should try to avoid moving functions around (as I did to try to make up for my poor C knowledge at the time I submitted the PR).
Hi, just wondering if there's any progress that was made on this issue?  It seems that #2416 was abandoned and the user was deleted.
#2416 is *huge*, and nobody started a new approach.
@htgoebel I don't understand the specifics of the bootloader change, but I could implement some of your comments in #2416.  Would you consider merging separate PRs for bootloader changes / the rest of the changes (tests, etc)?  I can handle pytest changes well.

Resolving patch:
diff --git a/PyInstaller/building/api.py b/PyInstaller/building/api.py
--- a/PyInstaller/building/api.py
+++ b/PyInstaller/building/api.py
@@ -232,7 +232,7 @@ def assemble(self):
                 # file is contained within python egg, it is added with the egg
                 continue
             if typ in ('BINARY', 'EXTENSION', 'DEPENDENCY'):
-                if not self.exclude_binaries:
+                if not self.exclude_binaries or typ == 'DEPENDENCY':
                     if typ == 'BINARY':
                         # Avoid importing the same binary extension twice. This might
                         # happen if they come from different sources (eg. once from
@@ -790,7 +790,8 @@ def _merge_dependencies(self, args):
         Filter shared dependencies to be only in first executable.
         """
         for analysis, _, _ in args:
-            path = os.path.abspath(analysis.scripts[-1][1]).replace(self._common_prefix, "", 1)
+            path = os.path.normcase(os.path.abspath(analysis.scripts[-1][1]))
+            path = path.replace(self._common_prefix, "", 1)
             path = os.path.splitext(path)[0]
             if os.path.normcase(path) in self._id_to_path:
                 path = self._id_to_path[os.path.normcase(path)]
@@ -808,7 +809,10 @@ def _set_dependencies(self, analysis, path):
                 else:
                     dep_path = self._get_relative_path(path, self._dependencies[tpl[1]])
                     logger.debug("Referencing %s to be a dependecy for %s, located in %s" % (tpl[1], path, dep_path))
-                    analysis.dependencies.append((":".join((dep_path, tpl[0])), tpl[1], "DEPENDENCY"))
+                    analysis.dependencies.append(
+                        (":".join((dep_path, os.path.basename(tpl[1]))),
+                         tpl[1],
+                         "DEPENDENCY"))
                     toc[i] = (None, None, None)
             # Clean the list
             toc[:] = [tpl for tpl in toc if tpl != (None, None, None)]


Test patch:
diff --git a/tests/functional/conftest.py b/tests/functional/conftest.py
--- a/tests/functional/conftest.py
+++ b/tests/functional/conftest.py
@@ -259,7 +259,8 @@ def _test_executables(self, name, args, runtime, run_from_path):
         assert exes != [], 'No executable file was found.'
         for exe in exes:
             # Try to find .toc log file. .toc log file has the same basename as exe file.
-            toc_log = os.path.join(_LOGS_DIR, os.path.basename(exe) + '.toc')
+            toc_log = os.path.join(
+                _LOGS_DIR, os.path.splitext(os.path.basename(exe))[0] + '.toc')
             if os.path.exists(toc_log):
                 if not self._examine_executable(exe, toc_log):
                     pytest.fail('Matching .toc of %s failed.' % exe)
diff --git a/tests/old_suite/multipackage/multipackage1_B.toc b/tests/functional/logs/multipackage1_B.toc
similarity index 100%
rename from tests/old_suite/multipackage/multipackage1_B.toc
rename to tests/functional/logs/multipackage1_B.toc
diff --git a/tests/old_suite/multipackage/multipackage2_B.toc b/tests/functional/logs/multipackage2_B.toc
similarity index 100%
rename from tests/old_suite/multipackage/multipackage2_B.toc
rename to tests/functional/logs/multipackage2_B.toc
diff --git a/tests/old_suite/multipackage/multipackage3_B.toc b/tests/functional/logs/multipackage3_B.toc
similarity index 100%
rename from tests/old_suite/multipackage/multipackage3_B.toc
rename to tests/functional/logs/multipackage3_B.toc
diff --git a/tests/old_suite/multipackage/multipackage4_B.toc b/tests/functional/logs/multipackage4_B.toc
similarity index 100%
rename from tests/old_suite/multipackage/multipackage4_B.toc
rename to tests/functional/logs/multipackage4_B.toc
diff --git a/tests/old_suite/multipackage/multipackage5_B.toc b/tests/functional/logs/multipackage5_B.toc
similarity index 100%
rename from tests/old_suite/multipackage/multipackage5_B.toc
rename to tests/functional/logs/multipackage5_B.toc
diff --git a/tests/functional/logs/multipackage5_C.toc b/tests/functional/logs/multipackage5_C.toc
new file mode 100644
--- /dev/null
+++ b/tests/functional/logs/multipackage5_C.toc
@@ -0,0 +1,6 @@
+[
+    'getopt',
+    'multipackage5_C',
+    '.*multipackage5_B.multipackage5_B:.*ssl.*',
+    '(?i).*multipackage5_B.multipackage5_B:.*python.*',
+]
diff --git a/tests/functional/logs/test_multipackage1.toc b/tests/functional/logs/test_multipackage1.toc
new file mode 100644
--- /dev/null
+++ b/tests/functional/logs/test_multipackage1.toc
@@ -0,0 +1,6 @@
+[
+    'getopt',
+    'test_multipackage1',
+    '.*multipackage1_B:.*ssl.*',
+    '(?i).*multipackage1_B:.*python.*',
+]
diff --git a/tests/functional/logs/test_multipackage2.toc b/tests/functional/logs/test_multipackage2.toc
new file mode 100644
--- /dev/null
+++ b/tests/functional/logs/test_multipackage2.toc
@@ -0,0 +1,6 @@
+[
+    'getopt',
+    'test_multipackage2',
+    '.*multipackage2_B.multipackage2_B:.*ssl.*',
+    '(?i).*multipackage2_B.multipackage2_B:.*python.*',
+]
diff --git a/tests/functional/logs/test_multipackage3.toc b/tests/functional/logs/test_multipackage3.toc
new file mode 100644
--- /dev/null
+++ b/tests/functional/logs/test_multipackage3.toc
@@ -0,0 +1,6 @@
+[
+    'getopt',
+    'test_multipackage3',
+    '.*multipackage3_B:.*ssl.*',
+    '(?i).*multipackage3_B:.*python.*',
+]
diff --git a/tests/functional/logs/test_multipackage4.toc b/tests/functional/logs/test_multipackage4.toc
new file mode 100644
--- /dev/null
+++ b/tests/functional/logs/test_multipackage4.toc
@@ -0,0 +1,6 @@
+[
+    'getopt',
+    'test_multipackage4',
+    '.*multipackage4_B.multipackage4_B:.*ssl.*',
+    '(?i).*multipackage4_B.multipackage4_B:.*python.*',
+]
diff --git a/tests/functional/logs/test_multipackage5.toc b/tests/functional/logs/test_multipackage5.toc
new file mode 100644
--- /dev/null
+++ b/tests/functional/logs/test_multipackage5.toc
@@ -0,0 +1,6 @@
+[
+    'getopt',
+    'test_multipackage5',
+    '.*multipackage5_B.multipackage5_B:.*ssl.*',
+    '(?i).*multipackage5_B.multipackage5_B:.*python.*',
+]
diff --git a/tests/old_suite/multipackage/multipackage1_B.py b/tests/functional/specs/multipackage-scripts/multipackage1_B.py
similarity index 100%
rename from tests/old_suite/multipackage/multipackage1_B.py
rename to tests/functional/specs/multipackage-scripts/multipackage1_B.py
diff --git a/tests/old_suite/multipackage/multipackage2_B.py b/tests/functional/specs/multipackage-scripts/multipackage2_B.py
similarity index 100%
rename from tests/old_suite/multipackage/multipackage2_B.py
rename to tests/functional/specs/multipackage-scripts/multipackage2_B.py
diff --git a/tests/old_suite/multipackage/multipackage3_B.py b/tests/functional/specs/multipackage-scripts/multipackage3_B.py
similarity index 100%
rename from tests/old_suite/multipackage/multipackage3_B.py
rename to tests/functional/specs/multipackage-scripts/multipackage3_B.py
diff --git a/tests/old_suite/multipackage/multipackage4_B.py b/tests/functional/specs/multipackage-scripts/multipackage4_B.py
similarity index 100%
rename from tests/old_suite/multipackage/multipackage4_B.py
rename to tests/functional/specs/multipackage-scripts/multipackage4_B.py
diff --git a/tests/old_suite/multipackage/multipackage5_B.py b/tests/functional/specs/multipackage-scripts/multipackage5_B.py
similarity index 100%
rename from tests/old_suite/multipackage/multipackage5_B.py
rename to tests/functional/specs/multipackage-scripts/multipackage5_B.py
diff --git a/tests/old_suite/multipackage/multipackage5_C.py b/tests/functional/specs/multipackage-scripts/multipackage5_C.py
similarity index 100%
rename from tests/old_suite/multipackage/multipackage5_C.py
rename to tests/functional/specs/multipackage-scripts/multipackage5_C.py
diff --git a/tests/old_suite/multipackage/test_multipackage1.py b/tests/functional/specs/multipackage-scripts/test_multipackage1.py
similarity index 100%
rename from tests/old_suite/multipackage/test_multipackage1.py
rename to tests/functional/specs/multipackage-scripts/test_multipackage1.py
diff --git a/tests/old_suite/multipackage/test_multipackage2.py b/tests/functional/specs/multipackage-scripts/test_multipackage2.py
similarity index 100%
rename from tests/old_suite/multipackage/test_multipackage2.py
rename to tests/functional/specs/multipackage-scripts/test_multipackage2.py
diff --git a/tests/old_suite/multipackage/test_multipackage3.py b/tests/functional/specs/multipackage-scripts/test_multipackage3.py
similarity index 100%
rename from tests/old_suite/multipackage/test_multipackage3.py
rename to tests/functional/specs/multipackage-scripts/test_multipackage3.py
diff --git a/tests/old_suite/multipackage/test_multipackage4.py b/tests/functional/specs/multipackage-scripts/test_multipackage4.py
similarity index 100%
rename from tests/old_suite/multipackage/test_multipackage4.py
rename to tests/functional/specs/multipackage-scripts/test_multipackage4.py
diff --git a/tests/old_suite/multipackage/test_multipackage5.py b/tests/functional/specs/multipackage-scripts/test_multipackage5.py
similarity index 100%
rename from tests/old_suite/multipackage/test_multipackage5.py
rename to tests/functional/specs/multipackage-scripts/test_multipackage5.py
diff --git a/tests/old_suite/multipackage/test_multipackage1.spec b/tests/functional/specs/test_multipackage1.spec
similarity index 78%
rename from tests/old_suite/multipackage/test_multipackage1.spec
rename to tests/functional/specs/test_multipackage1.spec
--- a/tests/old_suite/multipackage/test_multipackage1.spec
+++ b/tests/functional/specs/test_multipackage1.spec
@@ -12,17 +12,18 @@
 
 
 # MULTIPROCESS FEATURE: file A (onefile pack) depends on file B (onefile pack).
+import os
 
-
+SCRIPT_DIR = 'multipackage-scripts'
 __testname__ = 'test_multipackage1'
 __testdep__ = 'multipackage1_B'
 
-a = Analysis([__testname__ + '.py'],
+a = Analysis([os.path.join(SCRIPT_DIR, __testname__ + '.py')],
              pathex=['.'])
-b = Analysis([__testdep__ + '.py'],
+b = Analysis([os.path.join(SCRIPT_DIR, __testdep__ + '.py')],
              pathex=['.'])
 
-MERGE((b, __testdep__, __testdep__ + '.exe'), (a, __testname__, __testname__ + '.exe'))
+MERGE((b, __testdep__, __testdep__), (a, __testname__, __testname__))
 
 pyz = PYZ(a.pure)
 exe = EXE(pyz,
@@ -31,7 +32,7 @@ exe = EXE(pyz,
           a.zipfiles,
           a.datas,
           a.dependencies,
-          name=os.path.join('dist', __testname__ + '.exe'),
+          name=os.path.join('dist', __testname__),
           debug=True,
           strip=False,
           upx=False,
@@ -44,7 +45,7 @@ exeB = EXE(pyzB,
           b.zipfiles,
           b.datas,
           b.dependencies,
-          name=os.path.join('dist', __testdep__ + '.exe'),
+          name=os.path.join('dist', __testdep__),
           debug=True,
           strip=False,
           upx=False,
diff --git a/tests/old_suite/multipackage/test_multipackage2.spec b/tests/functional/specs/test_multipackage2.spec
similarity index 78%
rename from tests/old_suite/multipackage/test_multipackage2.spec
rename to tests/functional/specs/test_multipackage2.spec
--- a/tests/old_suite/multipackage/test_multipackage2.spec
+++ b/tests/functional/specs/test_multipackage2.spec
@@ -12,18 +12,20 @@
 
 
 # MULTIPROCESS FEATURE: file A (onefile pack) depends on file B (onedir pack)
+import os
+import sys
 
-
+SCRIPT_DIR = 'multipackage-scripts'
 __testname__ = 'test_multipackage2'
 __testdep__ = 'multipackage2_B'
 
-a = Analysis([__testname__ + '.py'],
+a = Analysis([os.path.join(SCRIPT_DIR, __testname__ + '.py')],
              pathex=['.'])
-b = Analysis([__testdep__ + '.py'],
+b = Analysis([os.path.join(SCRIPT_DIR, __testdep__ + '.py')],
              pathex=['.'])
 
-MERGE((b, __testdep__, os.path.join(__testdep__, __testdep__ + '.exe')),
-      (a, __testname__, __testname__ + '.exe'))
+MERGE((b, __testdep__, os.path.join(__testdep__, __testdep__)),
+      (a, __testname__, __testname__))
 
 pyz = PYZ(a.pure)
 exe = EXE(pyz,
@@ -32,7 +34,7 @@ exe = EXE(pyz,
           a.zipfiles,
           a.datas,
           a.dependencies,
-          name=os.path.join('dist', __testname__ + '.exe'),
+          name=os.path.join('dist', __testname__),
           debug=True,
           strip=False,
           upx=True,
@@ -44,7 +46,7 @@ exeB = EXE(pyzB,
           b.dependencies,
           exclude_binaries=1,
           name=os.path.join('build', 'pyi.'+sys.platform, __testdep__,
-                            __testdep__ + '.exe'),
+                            __testdep__),
           debug=True,
           strip=False,
           upx=True,
diff --git a/tests/old_suite/multipackage/test_multipackage3.spec b/tests/functional/specs/test_multipackage3.spec
similarity index 78%
rename from tests/old_suite/multipackage/test_multipackage3.spec
rename to tests/functional/specs/test_multipackage3.spec
--- a/tests/old_suite/multipackage/test_multipackage3.spec
+++ b/tests/functional/specs/test_multipackage3.spec
@@ -12,18 +12,20 @@
 
 
 # TESTING MULTIPROCESS FEATURE: file A (onedir pack) depends on file B (onefile pack).
+import os
+import sys
 
-
+SCRIPT_DIR = 'multipackage-scripts'
 __testname__ = 'test_multipackage3'
 __testdep__ = 'multipackage3_B'
 
-a = Analysis([__testname__ + '.py'],
+a = Analysis([os.path.join(SCRIPT_DIR, __testname__ + '.py')],
              pathex=['.'])
-b = Analysis([__testdep__ + '.py'],
+b = Analysis([os.path.join(SCRIPT_DIR, __testdep__ + '.py')],
              pathex=['.'])
 
-MERGE((b, __testdep__, os.path.join(__testdep__ + '.exe')),
-      (a, __testname__, os.path.join(__testname__, __testname__ + '.exe')))
+MERGE((b, __testdep__, os.path.join(__testdep__)),
+      (a, __testname__, os.path.join(__testname__, __testname__)))
 
 pyz = PYZ(a.pure)
 exe = EXE(pyz,
@@ -31,7 +33,7 @@ exe = EXE(pyz,
           a.dependencies,
           exclude_binaries=1,
           name=os.path.join('build', 'pyi.'+sys.platform, __testname__,
-                            __testname__ + '.exe'),
+                            __testname__),
           debug=True,
           strip=False,
           upx=True,
@@ -52,7 +54,7 @@ exeB = EXE(pyzB,
           b.zipfiles,
           b.datas,
           b.dependencies,
-          name=os.path.join('dist', __testdep__ + '.exe'),
+          name=os.path.join('dist', __testdep__),
           debug=True,
           strip=False,
           upx=True,
diff --git a/tests/old_suite/multipackage/test_multipackage4.spec b/tests/functional/specs/test_multipackage4.spec
similarity index 80%
rename from tests/old_suite/multipackage/test_multipackage4.spec
rename to tests/functional/specs/test_multipackage4.spec
--- a/tests/old_suite/multipackage/test_multipackage4.spec
+++ b/tests/functional/specs/test_multipackage4.spec
@@ -12,18 +12,20 @@
 
 
 # TESTING MULTIPROCESS FEATURE: file A (onedir pack) depends on file B (onedir pack).
+import os
+import sys
 
-
+SCRIPT_DIR = 'multipackage-scripts'
 __testname__ = 'test_multipackage4'
 __testdep__ = 'multipackage4_B'
 
-a = Analysis([__testname__ + '.py'],
+a = Analysis([os.path.join(SCRIPT_DIR, __testname__ + '.py')],
              pathex=['.'])
-b = Analysis([__testdep__ + '.py'],
+b = Analysis([os.path.join(SCRIPT_DIR, __testdep__ + '.py')],
              pathex=['.'])
 
-MERGE((b, __testdep__, os.path.join(__testdep__, __testdep__ + '.exe')),
-      (a, __testname__, os.path.join(__testname__, __testname__ + '.exe')))
+MERGE((b, __testdep__, os.path.join(__testdep__, __testdep__)),
+      (a, __testname__, os.path.join(__testname__, __testname__)))
 
 pyz = PYZ(a.pure)
 exe = EXE(pyz,
@@ -31,7 +33,7 @@ exe = EXE(pyz,
           a.dependencies,
           exclude_binaries=1,
           name=os.path.join('build', 'pyi.'+sys.platform, __testname__,
-                            __testname__ + '.exe'),
+                            __testname__),
           debug=True,
           strip=False,
           upx=True,
@@ -51,7 +53,7 @@ exeB = EXE(pyzB,
           b.dependencies,
           exclude_binaries=1,
           name=os.path.join('build', 'pyi.'+sys.platform, __testdep__,
-                            __testdep__ + '.exe'),
+                            __testdep__),
           debug=True,
           strip=False,
           upx=True,
diff --git a/tests/old_suite/multipackage/test_multipackage5.spec b/tests/functional/specs/test_multipackage5.spec
similarity index 77%
rename from tests/old_suite/multipackage/test_multipackage5.spec
rename to tests/functional/specs/test_multipackage5.spec
--- a/tests/old_suite/multipackage/test_multipackage5.spec
+++ b/tests/functional/specs/test_multipackage5.spec
@@ -13,23 +13,25 @@
 
 # TESTING MULTIPROCESS FEATURE: file A (onedir pack) depends on file B (onedir pack)
 # and file C (onefile pack)
+import os
+import sys
 
-
+SCRIPT_DIR = 'multipackage-scripts'
 __testname__ = 'test_multipackage5'
 __testdep__ = 'multipackage5_B'
 __testdep2__ = 'multipackage5_C'
 
-a = Analysis([__testname__ + '.py'],
+a = Analysis([os.path.join(SCRIPT_DIR, __testname__ + '.py')],
              pathex=['.'])
-b = Analysis([__testdep__ + '.py'],
+b = Analysis([os.path.join(SCRIPT_DIR, __testdep__ + '.py')],
              pathex=['.'])
-c = Analysis([__testdep2__ + '.py'],
+c = Analysis([os.path.join(SCRIPT_DIR, __testdep2__ + '.py')],
              pathex=['.'])
 
 
-MERGE((b, __testdep__, os.path.join(__testdep__, __testdep__ + '.exe')),
-      (c, __testdep2__, os.path.join(__testdep2__ + '.exe')),
-      (a, __testname__, os.path.join(__testname__, __testname__ + '.exe')))
+MERGE((b, __testdep__, os.path.join(__testdep__, __testdep__)),
+      (c, __testdep2__, os.path.join(__testdep2__)),
+      (a, __testname__, os.path.join(__testname__, __testname__)))
 
 pyz = PYZ(a.pure)
 exe = EXE(pyz,
@@ -37,7 +39,7 @@ exe = EXE(pyz,
           a.dependencies,
           exclude_binaries=1,
           name=os.path.join('build', 'pyi.'+sys.platform, __testname__,
-                            __testname__ + '.exe'),
+                            __testname__),
           debug=True,
           strip=False,
           upx=True,
@@ -57,7 +59,7 @@ exeB = EXE(pyzB,
           b.dependencies,
           exclude_binaries=1,
           name=os.path.join('build', 'pyi.'+sys.platform, __testdep__,
-                            __testdep__ + '.exe'),
+                            __testdep__),
           debug=True,
           strip=False,
           upx=True,
@@ -78,7 +80,7 @@ exeC = EXE(pyzC,
           c.zipfiles,
           c.datas,
           c.dependencies,
-          name=os.path.join('dist', __testdep2__ + '.exe'),
+          name=os.path.join('dist', __testdep2__),
           debug=True,
           strip=False,
           upx=True,
diff --git a/tests/functional/test_multipackage.py b/tests/functional/test_multipackage.py
new file mode 100644
--- /dev/null
+++ b/tests/functional/test_multipackage.py
@@ -0,0 +1,22 @@
+import pytest
+
+
+@pytest.mark.parametrize(
+    "spec_file",
+    (
+        "test_multipackage1.spec",
+        "test_multipackage2.spec",
+        "test_multipackage3.spec",
+        "test_multipackage4.spec",
+        "test_multipackage5.spec",
+    ),
+    ids=(
+        "onefile_depends_on_onefile",
+        "onedir_depends_on_onefile",
+        "onefile_depends_on_onedir",
+        "onedir_depends_on_onedir",
+        "onedir_and_onefile_depends_on_onedir",
+    )
+)
+def test_spec_with_multipackage(pyi_builder_spec, spec_file):
+    pyi_builder_spec.test_spec(spec_file)
diff --git a/tests/old_suite/multipackage/multipackage5_C.toc b/tests/old_suite/multipackage/multipackage5_C.toc
deleted file mode 100644
--- a/tests/old_suite/multipackage/multipackage5_C.toc
+++ /dev/null
@@ -1,6 +0,0 @@
-[
-    'getopt',
-    'multipackage5_C',
-    '.*multipackage5_B.multipackage5_B\.exe:.*ssl.*',
-    '(?i).*multipackage5_B.multipackage5_B\.exe:.*python.*',
-]
diff --git a/tests/old_suite/multipackage/test_multipackage1.toc b/tests/old_suite/multipackage/test_multipackage1.toc
deleted file mode 100644
--- a/tests/old_suite/multipackage/test_multipackage1.toc
+++ /dev/null
@@ -1,6 +0,0 @@
-[
-    'getopt',
-    'test_multipackage1',
-    '.*multipackage1_B\.exe:.*ssl.*',
-    '(?i).*multipackage1_B\.exe:.*python.*',
-]
diff --git a/tests/old_suite/multipackage/test_multipackage2.toc b/tests/old_suite/multipackage/test_multipackage2.toc
deleted file mode 100644
--- a/tests/old_suite/multipackage/test_multipackage2.toc
+++ /dev/null
@@ -1,6 +0,0 @@
-[
-    'getopt',
-    'test_multipackage2',
-    '.*multipackage2_B.multipackage2_B\.exe:.*ssl.*',
-    '(?i).*multipackage2_B.multipackage2_B\.exe:.*python.*',
-]
diff --git a/tests/old_suite/multipackage/test_multipackage3.toc b/tests/old_suite/multipackage/test_multipackage3.toc
deleted file mode 100644
--- a/tests/old_suite/multipackage/test_multipackage3.toc
+++ /dev/null
@@ -1,6 +0,0 @@
-[
-    'getopt',
-    'test_multipackage3',
-    '.*multipackage3_B\.exe:.*ssl.*',
-    '(?i).*multipackage3_B\.exe:.*python.*',
-]
diff --git a/tests/old_suite/multipackage/test_multipackage4.toc b/tests/old_suite/multipackage/test_multipackage4.toc
deleted file mode 100644
--- a/tests/old_suite/multipackage/test_multipackage4.toc
+++ /dev/null
@@ -1,6 +0,0 @@
-[
-    'getopt',
-    'test_multipackage4',
-    '.*multipackage4_B.multipackage4_B\.exe:.*ssl.*',
-    '(?i).*multipackage4_B.multipackage4_B\.exe:.*python.*',
-]
diff --git a/tests/old_suite/multipackage/test_multipackage5.toc b/tests/old_suite/multipackage/test_multipackage5.toc
deleted file mode 100644
--- a/tests/old_suite/multipackage/test_multipackage5.toc
+++ /dev/null
@@ -1,6 +0,0 @@
-[
-    'getopt',
-    'test_multipackage5',
-    '.*multipackage5_B.multipackage5_B\.exe:.*ssl.*',
-    '(?i).*multipackage5_B.multipackage5_B\.exe:.*python.*',
-]