Skip to content

Commit

Permalink
Implement 'python3' command in the app bundle
Browse files Browse the repository at this point in the history
Before this commit the 'python3' command was a copy of the
normal stub, it now supports the same interface as python itself
but runs in isolated mode, does not the user site directory and
cannot be copied outside of the bundle.

SomeApp.app/Contents/MacOS/python3 is basically a python3 command
in the app's context.
  • Loading branch information
ronaldoussoren committed Jul 12, 2024
1 parent 45716a6 commit 267a6e1
Show file tree
Hide file tree
Showing 4 changed files with 129 additions and 90 deletions.
12 changes: 5 additions & 7 deletions src/py2app/_apptemplate/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@


class LauncherType(enum.Enum):
MAIN_PROGRAM = "main"
SECONDARY_PROGRAM = "secondary"
STUB_PROGRAM = "main"
PYTHON_BINARY = "python"


Expand All @@ -22,9 +21,8 @@ class LauncherType(enum.Enum):
}

LAUNCHER_FLAGS = {
LauncherType.MAIN_PROGRAM: "-DLAUNCH_PRIMARY",
LauncherType.SECONDARY_PROGRAM: "-DLAUNCH_SECONDARY",
LauncherType.PYTHON_BINARY: "-DLAUNCH_PYTHON",
LauncherType.STUB_PROGRAM: [],
LauncherType.PYTHON_BINARY: ["-DLAUNCH_PYTHON"],
}


Expand Down Expand Up @@ -62,7 +60,7 @@ def copy_app_launcher(
path: pathlib.Path,
*,
arch: BuildArch,
program_type: LauncherType = LauncherType.MAIN_PROGRAM,
program_type: LauncherType = LauncherType.STUB_PROGRAM,
deployment_target: str,
debug_macho_usage: bool = False,
) -> None:
Expand Down Expand Up @@ -103,8 +101,8 @@ def copy_app_launcher(
"-framework",
"Foundation",
f"-mmacosx-version-min={deployment_target}",
LAUNCHER_FLAGS[program_type],
]
+ LAUNCHER_FLAGS[program_type]
+ ARCH_FLAGS[arch]
+ _pyflags()
+ (["-DENABLE_MACHO_DEBUG"] if debug_macho_usage else [])
Expand Down
134 changes: 80 additions & 54 deletions src/py2app/_apptemplate/launcher.m
Original file line number Diff line number Diff line change
Expand Up @@ -10,45 +10,14 @@
*
* XXX: What's needed to support venv from a bundled Python?
*
* XXX: T.B.D. how to implement semi-standalone bundles
* (which use an installed python and look for that
* at run time). Current opinion: drop that feature.
*
* Possibly another axis of variants (see list below for
* primary axis) that performs manual runtime linking
* of libpython.
*
* XXX: The code should be split into several files (probably
* as a header-only library) to allow for code reuse in
* a number of variants and in the bundle stub)
*
* In the end we need:
* - plugin bundle stub
* - plugin bundle stub using subinterpreters
* - app bundle stub
* - python3 command-line tool
* - stub for "extra" scripts
*
* the preprocessor can help here, but is not ideal.
*
* XXX: The current code just prints to stderr on problems,
* but should pop up a GUI instead (see previous implementation)
*
* Should it? Maybe only in semi-standalone mode when it cannot
* find Python; in all other cases launch problems are either
* bugs in py2app or a user script that doesn't work right)
*
* The build machinery uses a preprocessor define to control which
* variant is build:
* - `-DLAUNCH_PRIMARY`: Main executable for an .app bundle
* - `-DLAUNCH_SECONDARY`: Additional "script" in a bundle
* - No defines: Main or secondary executable for an .app bundle
* - `-DLAUNCH_PYTHON`: Equivalent to `python3` outside of a bundle
*
* Note that all types of binaries only work when located inside a
* bundle, the "secondary" and "python" types can more easily be used
* outside of a bundle by creating symbolic links in a convenient place.
*
* XXX: Actually implement the second and third variants.
* bundle, but can be used outside of the bundle by creating symbolic
* links in a convenient place.
*/

#include <unistd.h>
Expand Down Expand Up @@ -122,9 +91,11 @@ static void debug_dyld_usage(void)
NSLog(@"Mach-O image outside of the bundle: %s", image_name);
}
}

#endif /* ENABLE_MACHO_DEBUG */


static int finalize_python = 1;

/* setup_python - Initialize the python interpreter.
*
* Will call exit(3) when setting up the interpreter
Expand All @@ -140,6 +111,11 @@ static void setup_python(NSBundle* mainBundle, int argc, char* const* argv, char
PyPreConfig_InitIsolatedConfig(&preconfig);
preconfig.utf8_mode = 1;

#ifdef LAUNCH_PYTHON
/* 5. Parse argv when used as python3 */
preconfig.parse_argv = 1;
#endif

/*
* 2. Customize interpreter pre-configuration using the `PyConfig`
* key in the `Info.plist` file.
Expand All @@ -165,11 +141,15 @@ static void setup_python(NSBundle* mainBundle, int argc, char* const* argv, char
}
}

status = Py_PreInitialize(&preconfig);
status = Py_PreInitializeFromBytesArgs(&preconfig, argc, (char**)argv);
if (PyStatus_Exception(status)) goto configerror;


PyConfig_InitIsolatedConfig(&config);
#ifdef LAUNCH_PYTHON
/* 5. Parse argv when used as python3 */
config.parse_argv = 1;
#endif

/* 1. Basic configuration */

Expand Down Expand Up @@ -204,6 +184,12 @@ static void setup_python(NSBundle* mainBundle, int argc, char* const* argv, char
config.faulthandler = [value intValue];
}

/* - finalize (bool), default True */
value = pyconfig[@"finalize"];
if (value && [[value class] isSubclassOfClass:[NSNumber class]]) {
finalize_python = [value intValue];
}

path_suffixes = pyconfig[@"sys.path"];
if (path_suffixes != nil && ![[path_suffixes class] isSubclassOfClass:[NSArray class]]) {
path_suffixes = nil;
Expand Down Expand Up @@ -291,14 +277,20 @@ static void setup_python(NSBundle* mainBundle, int argc, char* const* argv, char
status = PyConfig_SetBytesArgv(&config, argc, argv);
if (PyStatus_Exception(status)) goto configerror;

#ifdef LAUNCH_PYTHON
/* 5. Finish configuration */
status = PyConfig_Read(&config);
if (PyStatus_Exception(status)) goto configerror;
#endif


/* 5. Initialize the Python interpreter: */
/* 6. Initialize the Python interpreter: */
status = Py_InitializeFromConfig(&config);
PyConfig_Clear(&config);
if (PyStatus_Exception(status)) goto configerror;


/* 6. Inject `sys.py2app_bundle_resources` */
/* 7. Inject `sys.py2app_bundle_resources` */
const char* resourcePath = mainBundle.resourcePath.UTF8String;
PyObject* value = PyUnicode_DecodeUTF8(resourcePath, strlen(resourcePath), NULL);
if (!value) {
Expand All @@ -313,7 +305,8 @@ static void setup_python(NSBundle* mainBundle, int argc, char* const* argv, char
}
Py_DECREF(value);

/* 7. Inject `sys.py2app_argv0` */
#ifndef LAUNCH_PYTHON
/* 8. Inject `sys.py2app_argv0` */
value = PyUnicode_DecodeUTF8(argv[0], strlen(argv[0]), NULL);
if (!value) {
status = PyStatus_Error("cannot convert argv[0] to python");
Expand All @@ -324,6 +317,8 @@ static void setup_python(NSBundle* mainBundle, int argc, char* const* argv, char
Py_DECREF(value);
goto pyerror;
}
Py_DECREF(value);
#endif

#ifdef ENABLE_MACHO_DEBUG
/* 8. Check if dylib loading should be verified */
Expand All @@ -341,7 +336,6 @@ static void setup_python(NSBundle* mainBundle, int argc, char* const* argv, char
debug_macho_usage = 1;
}
#endif /* ENABLE_MACHO_DEBUG */
Py_DECREF(value);
return;

pyerror:
Expand Down Expand Up @@ -380,12 +374,16 @@ static void clear_bundle_address(void)
}
}


int
main(int argc, char * const *argv, char * const *envp)
{
NSString* mainPy;
FILE* mainFile;
NSString* prebootPy;
FILE* prebootFile;
#ifndef LAUNCH_PYTHON
NSString* bootPy;
FILE* bootFile;
#endif
int rval;

@autoreleasepool {
NSBundle* mainBundle = [NSBundle mainBundle];
Expand All @@ -398,29 +396,57 @@ static void clear_bundle_address(void)
clear_bundle_address();
setup_python(mainBundle, argc, argv, envp);

mainPy = [[[mainBundle resourcePath] stringByAppendingPathComponent:@"__boot__.py"] retain];
prebootPy = [[[mainBundle resourcePath] stringByAppendingPathComponent:@"__preboot__.py"] retain];
#ifndef LAUNCH_PYTHON
bootPy = [[[mainBundle resourcePath] stringByAppendingPathComponent:@"__boot__.py"] retain];
#endif
}


mainFile = fopen([mainPy UTF8String], "r");
if (mainFile == NULL) {
NSLog(@"Cannot open %@, errno=%d", mainPy, errno);
prebootFile = fopen([prebootPy UTF8String], "r");
if (prebootFile == NULL) {
NSLog(@"Cannot open %@, errno=%d", prebootPy, errno);
return 1;
}

#ifndef LAUNCH_PYTHON
bootFile = fopen([bootPy UTF8String], "r");
if (bootFile == NULL) {
NSLog(@"Cannot open %@, errno=%d", bootPy, errno);
fclose(prebootFile);
return 1;
}
#endif

rval = PyRun_SimpleFile(prebootFile, [prebootPy UTF8String]);
if (rval == 0) {
#ifndef LAUNCH_PYTHON
rval = PyRun_SimpleFile(bootFile, [bootPy UTF8String]);
#elif defined(LAUNCH_PYTHON)
rval = Py_RunMain();
#endif
}

fclose(prebootFile);
[prebootPy release];

int rval = PyRun_SimpleFile(mainFile, [mainPy UTF8String]);
fclose(mainFile);
[mainPy release];
#ifndef LAUNCH_PYTHON
fclose(bootFile);
[bootPy release];
#endif


#ifdef ENABLE_MACHO_DEBUG
debug_dyld_usage();
#endif /* ENABLE_MACHO_DEBUG */


/* XXX: Finalizing the interpreter can be problematic, maybe
* turn this into a config option?
*/
Py_Finalize();
if (finalize_python) {
Py_Finalize();
}
#ifndef LAUNCH_PYTHON
return rval == 0?0:2;
#else
return rval;
#endif
}
67 changes: 38 additions & 29 deletions src/py2app/_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -421,7 +421,7 @@ def add_loader(paths: BundlePaths, bundle: BundleOptions, progress: Progress) ->
progress.step_task(task_id)

copy_app_launcher(
paths.main / "python",
paths.main / "python3",
arch=bundle.macho_arch,
deployment_target=bundle.deployment_target,
program_type=LauncherType.PYTHON_BINARY,
Expand All @@ -437,7 +437,6 @@ def add_loader(paths: BundlePaths, bundle: BundleOptions, progress: Progress) ->
paths.main / script.stem,
arch=bundle.macho_arch,
deployment_target=bundle.deployment_target,
program_type=LauncherType.SECONDARY_PROGRAM,
)
progress.step_task(task_id)

Expand Down Expand Up @@ -483,17 +482,15 @@ def add_bootstrap(
#
# - Setting DEFAULT_SCRIPT and SCRIPT_MAP is incorrect/incomplete

bootstrap_path = paths.resources / "__boot__.py"
# XXX: Split into __preboot__.py and __boot__.py, the former used
# for initial setup and the latter for actually launching app
# code. The preboot file is also used for the "python3" command
# emulation.

# XXX:
# - All hardcoded fragments should either access only builtin modules,
# or addition should be moved to an earlier phase using *graph.add_bootstrap*.
# - handle argv_emulator and argv_inject with recipes, but these
# should only be enabled for the main script and not for secondary
# scripts.
# - Likewise for 'emulate_shell_environment'
prebootstrap_path = paths.resources / "__preboot__.py"
bootstrap_path = paths.resources / "__boot__.py"

with open(bootstrap_path, "w") as stream:
with open(prebootstrap_path, "w") as stream:
if bundle.build_type == BuildType.ALIAS:
# The bundle does not include Python source code, and
# code objects could refer to non-existing paths.
Expand All @@ -506,6 +503,36 @@ def add_bootstrap(
)
stream.write("\n")

stream.write(
importlib.resources.files("py2app.bootstrap")
.joinpath("_setup_importlib.py")
.read_text(encoding="utf-8")
)
stream.write("\n")

# XXX: Audit recipes for this, if needed split
# into two sets of bootstrap.
if graph is not None:
for node in graph.iter_graph():
if not isinstance(node, BaseNode):
continue
bootstrap = graph.bootstrap(node)
if bootstrap is None:
continue

stream.write(bootstrap)
stream.write("\n")

# XXX:
# - All hardcoded fragments should either access only builtin modules,
# or addition should be moved to an earlier phase using *graph.add_bootstrap*.
# - handle argv_emulator and argv_inject with recipes, but these
# should only be enabled for the main script and not for secondary
# scripts.
# - Likewise for 'emulate_shell_environment'

with open(bootstrap_path, "w") as stream:

if bundle.chdir:
if bundle.plugin:
progress.warning(f"Ignoring 'chdir' for plugin bundle {bundle.name!r}")
Expand All @@ -527,24 +554,6 @@ def _chdir_resources() -> None:
)
)

stream.write(
importlib.resources.files("py2app.bootstrap")
.joinpath("_setup_importlib.py")
.read_text(encoding="utf-8")
)
stream.write("\n")

if graph is not None:
for node in graph.iter_graph():
if not isinstance(node, BaseNode):
continue
bootstrap = graph.bootstrap(node)
if bootstrap is None:
continue

stream.write(bootstrap)
stream.write("\n")

stream.write(
importlib.resources.files("py2app.bootstrap")
.joinpath(BOOTSTRAP_MOD[(bundle.plugin, bundle.build_type)])
Expand Down
Loading

0 comments on commit 267a6e1

Please sign in to comment.