Skip to content

Commit

Permalink
Make the name of onedir mode's _internal directory configurable.
Browse files Browse the repository at this point in the history
Add a --internals-prefix CLI option and corresponding internals_prefix
spec file option to change the otherwise hard-coded name of the
directory in which all a onedir's non EXE contents are hidden.

Note that this, whilst this spec file option would make the most sense
as a parameter to BUNDLE(), it instead has to be an option to EXE()
which BUNDLE() then fishes out of the EXE()'s configuration due to EXE()
being the only place where OPTION TOC entries can go.
  • Loading branch information
bwoodsend committed Jul 9, 2023
1 parent d50e778 commit da84b85
Show file tree
Hide file tree
Showing 7 changed files with 85 additions and 13 deletions.
21 changes: 20 additions & 1 deletion PyInstaller/building/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,8 @@ def __init__(self, *args, **kwargs):
entitlements_file
macOS only. Optional path to entitlements file to use with code signing of collected binaries
(--entitlements option to codesign utility).
internals_prefix
Onedir mode only. Specifies the name of the directory where all files par the executable will be placed.
"""
from PyInstaller.config import CONF

Expand All @@ -407,6 +409,7 @@ def __init__(self, *args, **kwargs):
self.strip = kwargs.get('strip', False)
self.upx_exclude = kwargs.get("upx_exclude", [])
self.runtime_tmpdir = kwargs.get('runtime_tmpdir', None)
self.meipass_prefix = kwargs.get("internals_prefix", "_internal")
# If ``append_pkg`` is false, the archive will not be appended to the exe, but copied beside it.
self.append_pkg = kwargs.get('append_pkg', True)

Expand Down Expand Up @@ -456,6 +459,13 @@ def __init__(self, *args, **kwargs):
logger.warning('Ignoring icon; supported only on Windows and macOS!')
self.icon = None

if self.meipass_prefix in ("", ".", "..") \
or "/" in self.meipass_prefix or "\\" in self.meipass_prefix:
raise SystemExit(
f'Invalid value "{self.meipass_prefix}" passed to `--internals-prefix` or `internals_prefix`. '
'Exactly one directory level is required.'
)

# Old .spec format included in 'name' the path where to put created app. New format includes only exename.
#
# Ignore fullpath in the 'name' and prepend DISTPATH or WORKPATH.
Expand Down Expand Up @@ -512,6 +522,8 @@ def __init__(self, *args, **kwargs):
# no value; presence means "true"
self.toc.append(("pyi-macos-argv-emulation", "", "OPTION"))

self.toc.append(("pyi-meipass-prefix " + self.meipass_prefix, "", "OPTION"))

# If the icon path is relative, make it relative to the .spec file.
def makeabs(path):
if os.path.isabs(path):
Expand Down Expand Up @@ -898,6 +910,13 @@ def __init__(self, *args, **kwargs):
# DISTPATH). Old .spec formats included parent path, so strip it away.
self.name = os.path.join(CONF['distpath'], os.path.basename(kwargs.get('name')))

for arg in args:
if isinstance(arg, EXE):
self.meipass_prefix = arg.meipass_prefix
break
else:
raise ValueError("No EXE() instance was passed to COLLECT()")

self.toc = []
for arg in args:
# Valid arguments: EXE object and TOC-like iterables
Expand Down Expand Up @@ -963,7 +982,7 @@ def assemble(self):
if typecode in ("EXECUTABLE", "PKG"):
dest_path = os.path.join(self.name, dest_name)
else:
dest_path = os.path.join(self.name, "_internal", dest_name) # Absolute destination path
dest_path = os.path.join(self.name, self.meipass_prefix, dest_name)
dest_dir = os.path.dirname(dest_path)
try:
os.makedirs(dest_dir, exist_ok=True)
Expand Down
7 changes: 7 additions & 0 deletions PyInstaller/building/makespec.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,11 @@ def __add_options(parser):
"--name",
help="Name to assign to the bundled app and spec file (default: first script's basename)",
)
g.add_argument(
"--internals-prefix",
help="For onedir builds only, specify the name of the directory in which all supporting files (i.e. everything "
"except the executable itself) will be placed in.",
)

g = parser.add_argument_group('What to bundle, where to search')
g.add_argument(
Expand Down Expand Up @@ -615,6 +620,7 @@ def main(
noupx=False,
upx_exclude=None,
runtime_tmpdir=None,
internals_prefix=None,
pathex=[],
version_file=None,
specpath=None,
Expand Down Expand Up @@ -695,6 +701,7 @@ def main(
# On Mac OS, the default icon has to be copied into the .app bundle.
# The the text value 'None' means - use default icon.
icon_file = 'None'
exe_options += "\n internals_prefix='%s'," % (internals_prefix or "_internal")

if bundle_identifier:
# We need to encapsulate it into apostrofes.
Expand Down
24 changes: 15 additions & 9 deletions bootloader/src/pyi_archive.c
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,16 @@ pyi_arch_setup(ARCHIVE_STATUS *status, char const * archive_path, char const * e
if (snprintf(status->executablename, PATH_MAX, "%s", executable_path) >= PATH_MAX) {
return false;
}

/* Open the archive */
if (pyi_arch_open(status)) {
/* If this is not an archive, we MUST close the file, */
/* otherwise the open file-handle will be reused when */
/* testing the next file. */
pyi_arch_close_fp(status);
return false;
}

/* Set homepath (a.k.a. sys._MEIPASS) */
char executable_dir[PATH_MAX];
pyi_path_dirname(executable_dir, executable_path);
Expand All @@ -528,9 +538,13 @@ pyi_arch_setup(ARCHIVE_STATUS *status, char const * archive_path, char const * e
pyi_path_dirname(contents_dir, executable_dir);
pyi_path_join(status->homepath, contents_dir, "Frameworks");
} else {
char * meipass_prefix = pyi_arch_get_option(status, "pyi-meipass-prefix");
if (!meipass_prefix) {
FATALERROR("pyi-meipass-prefix option not found in onedir bundle archive!");
}
char root_path[PATH_MAX];
pyi_path_dirname(root_path, archive_path);
pyi_path_join(status->homepath, root_path, "_internal");
pyi_path_join(status->homepath, root_path, meipass_prefix);
}

/*
Expand All @@ -540,14 +554,6 @@ pyi_arch_setup(ARCHIVE_STATUS *status, char const * archive_path, char const * e
status->has_temp_directory = false;
strcpy(status->mainpath, status->homepath);

/* Open the archive */
if (pyi_arch_open(status)) {
/* If this is not an archive, we MUST close the file, */
/* otherwise the open file-handle will be reused when */
/* testing the next file. */
pyi_arch_close_fp(status);
return false;
}
return true;
}

Expand Down
8 changes: 5 additions & 3 deletions bootloader/src/pyi_launch.c
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ _format_and_check_path(char *buf, const char *fmt, ...)
};
va_end(args);

VS("Checking for file %s\n", buf);
return stat(buf, &tmp);
}

Expand Down Expand Up @@ -206,6 +207,7 @@ _extract_dependency(ARCHIVE_STATUS *archive_pool[], const char *item)

pyi_path_dirname(dirname, path);
pyi_path_dirname(homepath_parent, archive_status->homepath);
char * meipass_prefix = pyi_arch_get_option(archive_pool[0], "pyi-meipass-prefix");

/* We need to identify and handle three situations:
* 1) dependencies are in a onedir archive next to the current onefile archive,
Expand All @@ -214,7 +216,7 @@ _extract_dependency(ARCHIVE_STATUS *archive_pool[], const char *item)
*/
VS("LOADER: homepath is %s\n", archive_status->homepath);
/* TODO implement pyi_path_join to accept variable length of arguments for this case. */
if (_format_and_check_path(srcpath, "%s%c%s%c%s%c%s", homepath_parent, PYI_SEP, dirname, PYI_SEP, "_internal", PYI_SEP, filename) == 0) {
if (_format_and_check_path(srcpath, "%s%c%s%c%s%c%s", homepath_parent, PYI_SEP, dirname, PYI_SEP, meipass_prefix, PYI_SEP, filename) == 0) {
VS("LOADER: File %s found, assuming onedir reference\n", srcpath);

if (_copy_dependency_from_dir(archive_status, srcpath, filename) == -1) {
Expand All @@ -223,7 +225,7 @@ _extract_dependency(ARCHIVE_STATUS *archive_pool[], const char *item)
}
/* TODO implement pyi_path_join to accept variable length of arguments for this case. */
}
else if (_format_and_check_path(srcpath, "%s%c%s%c%s%c%s", homepath_parent, PYI_SEP, dirname, PYI_SEP, "_internal", PYI_SEP, filename) == 0) {
else if (_format_and_check_path(srcpath, "%s%c%s%c%s%c%s", homepath_parent, PYI_SEP, dirname, PYI_SEP, meipass_prefix, PYI_SEP, filename) == 0) {
VS("LOADER: File %s found, assuming onedir reference\n", srcpath);

if (_copy_dependency_from_dir(archive_status, srcpath, filename) == -1) {
Expand All @@ -235,7 +237,7 @@ _extract_dependency(ARCHIVE_STATUS *archive_pool[], const char *item)
VS("LOADER: File %s not found, assuming onefile reference.\n", srcpath);

/* TODO implement pyi_path_join to accept variable length of arguments for this case. */
if ((_format_and_check_path(archive_path, "%s%c%s%c%s.pkg", homepath_parent, PYI_SEP, "_internal", PYI_SEP, path) != 0) &&
if ((_format_and_check_path(archive_path, "%s%c%s%c%s.pkg", homepath_parent, PYI_SEP, meipass_prefix, PYI_SEP, path) != 0) &&
(_format_and_check_path(archive_path, "%s%c%s.exe", homepath_parent, PYI_SEP, path) != 0) &&
(_format_and_check_path(archive_path, "%s%c%s", homepath_parent, PYI_SEP, path) != 0)) {
FATALERROR("Archive not found: %s\n", archive_path);
Expand Down
13 changes: 13 additions & 0 deletions news/7713.breaking.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
All of onedir build's contents except for the executable are now moved into a
sub-directory (called ``_internal`` by default). ``sys._MEIPASS`` is adjusted to
point to this ``_internal`` directory. The breaking implications for this are:

* Assumptions that ``os.path.dirname(sys.executable) == sys._MEIPASS`` will
break. Code locating application resources using
``os.path.dirname(sys.executable)`` should be adjusted to use ``__file__``
or ``sys._MEIPASS`` and any code locating the original executable using
``sys._MEIPASS`` should use :data:`sys.executable` directly.

* Any custom post processing steps (either in the ``.spec`` file or
externally) which modify the bundle will likely need adjusting to
accommodate the new directory.
5 changes: 5 additions & 0 deletions news/7713.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Restructure onedir mode builds so that everything except the executable (and
``.pkg`` if you're using external PYZ archive mode) are hidden inside a
sub-directory. This sub-directory's name defaults to ``_internal`` but may be
configured with the new :option:`--internals-prefix` option. Onefile
applications and macOS ``.app`` bundles are unaffected.
20 changes: 20 additions & 0 deletions tests/functional/test_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -771,3 +771,23 @@ def test_package_entry_point_name_collision(pyi_builder):
exe, = pyi_builder._find_executables("matching_name")
p = subprocess.run([exe], stdout=subprocess.PIPE, universal_newlines=True)
assert re.findall("Running (.*) as (.*)", p.stdout) == expected


def test_internals_prefix(pyi_builder):
"""
Test the --internals-prefix option, including changing it without --clean.
"""
if pyi_builder._mode != 'onedir':
pytest.skip('--internals-prefix does not affect onefile builds.')

pyi_builder.test_source("", pyi_args=["--internals-prefix=foo"])
exe, = pyi_builder._find_executables("test_source")
bundle = Path(exe).parent
assert (bundle / "foo").is_dir()

pyi_builder.test_source("", pyi_args=["--internals-prefix=é³þ³źć🚀", "--noconfirm"])
assert not (bundle / "foo").exists()
assert (bundle / "é³þ³źć🚀").is_dir()

with pytest.raises(SystemExit, match='Invalid value "\\." passed'):
pyi_builder.test_source("", pyi_args=["--internals-prefix=.", "--noconfirm"])

0 comments on commit da84b85

Please sign in to comment.