@@ -96,9 +96,11 @@ def generate(self):
96
96
url = f"https://github.com/python/cpython/archive/v{ self .pyversion } .tar.gz"
97
97
files .get (self , url , strip_root = True )
98
98
99
- tc = AutotoolsToolchain (self , prefix = pathlib .Path (self .package_folder , "embedded_python" ))
99
+ prefix = pathlib .Path (self .package_folder , "embedded_python" )
100
+ tc = AutotoolsToolchain (self , prefix = prefix )
100
101
openssl_path = self .dependencies ["openssl" ].package_folder
101
102
tc .configure_args += [
103
+ f"--bindir={ prefix } " , # see `_isolate()` for the reason why we override this path
102
104
"--enable-shared" ,
103
105
"--without-static-libpython" ,
104
106
"--disable-test-modules" ,
@@ -114,7 +116,7 @@ def generate(self):
114
116
# package. Unlike RUNPATH, RPATH takes precedence over LD_LIBRARY_PATH.
115
117
if self .settings .os == "Linux" :
116
118
deps .environment .append (
117
- "LDFLAGS" , [r"-Wl,-rpath='\$\$ORIGIN/../ lib'" , "-Wl,--disable-new-dtags" ]
119
+ "LDFLAGS" , [r"-Wl,-rpath='\$\$ORIGIN/lib'" , "-Wl,--disable-new-dtags" ]
118
120
)
119
121
120
122
# Statically linking CPython with OpenSSL requires a bit of extra care. See the discussion
@@ -151,14 +153,14 @@ def _patch_libpython_path(self, dst):
151
153
if self .settings .os != "Macos" :
152
154
return
153
155
154
- exe = dst / f"bin/ python{ self .short_pyversion } "
156
+ exe = dst / f"python{ self .short_pyversion } "
155
157
buffer = io .StringIO ()
156
158
self .run (f"otool -L { exe } " , output = buffer )
157
159
lines = buffer .getvalue ().strip ().split ("\n " )[1 :]
158
160
libraries = [line .split ()[0 ] for line in lines ]
159
161
hardcoded_libraries = [lib for lib in libraries if lib .startswith (str (dst ))]
160
162
for lib in hardcoded_libraries :
161
- relocatable_library = lib .replace (str (dst ), "@executable_path/.. " )
163
+ relocatable_library = lib .replace (str (dst ), "@executable_path" )
162
164
self .output .info (f"Patching { exe } , replace { lib } with { relocatable_library } " )
163
165
self .run (f"install_name_tool -change { lib } { relocatable_library } { exe } " )
164
166
@@ -230,6 +232,57 @@ def is_landmark(filepath):
230
232
elif path .is_dir () and path .name not in keep_lib_dirs :
231
233
shutil .rmtree (path )
232
234
235
+ def _isolate (self , prefix ):
236
+ """Isolate this embedded environment from any other Python installations
237
+
238
+ Creating a `._pth` file puts Python into isolated mode: it will ignore any `PYTHON*`
239
+ env variables or additional packages installed in the users home directory. Only
240
+ the paths listed in the `._pth` file will be in `sys.path` on startup.
241
+
242
+ There's an extra quirk that complicates things on non-Windows systems. The `._pth` file
243
+ must be in the same directory as the real (non-symlink) executable, but it also must be
244
+ in the home/prefix directory. Usually, the executable is in `prefix/bin`. This forces us
245
+ to move the executable to `prefix` (this is done in `generate()`). To avoid issues with
246
+ established Unix Python conventions, we put symlinks back into `prefix/bin`. This is not
247
+ an issue on Windows since it already has `bin == prefix` by default.
248
+
249
+ Note that `._pth == isolated_mode` is only the case when running Python via the `python(3)`
250
+ executable. When embedding into an application executable, the `._pth` file is not relevant.
251
+ Isolated mode is set via the C API: https://docs.python.org/3/c-api/init_config.html While
252
+ embedding in the app is the primary use case, running the `python(3)` exe is also useful
253
+ for various build and runtime tasks. It's important to maintain isolated mode in all cases
254
+ to avoid obscure, hard-to-debug issues.
255
+
256
+ Finally, both `-core` and regular variants of this recipe will have the `._pth` file in the
257
+ package. All installed `pip` packages work correctly at runtime in isolated mode. However,
258
+ some older packages cannot be installed in isolated mode (they are using outdated `setup.py`
259
+ conventions). For this reason, we temporarily delete the `._pth` file and fall back to
260
+ partial isolation while installing `pip` packages. See `_build_bootstrap()` for details.
261
+ """
262
+ if self .settings .os == "Windows" :
263
+ paths = [
264
+ f"python{ self .int_pyversion } .zip" ,
265
+ "." ,
266
+ "Lib/site-packages" ,
267
+ ]
268
+ # `.pth` file must be next to the main `.dll` and use the same name.
269
+ with open (prefix / f"python{ self .int_pyversion } ._pth" , "w" ) as f :
270
+ f .write ("\n " .join (paths ))
271
+ else :
272
+ paths = [
273
+ f"lib/python{ self .int_pyversion } .zip" ,
274
+ f"lib/python{ self .short_pyversion } " ,
275
+ f"lib/python{ self .short_pyversion } /lib-dynload" ,
276
+ f"lib/python{ self .short_pyversion } /site-packages" ,
277
+ ]
278
+ # `.pth` file must be next to real (non-symlink) executable and use the same name.
279
+ with open (prefix / f"python{ self .short_pyversion } ._pth" , "w" ) as f :
280
+ f .write ("\n " .join (paths ))
281
+
282
+ py_exe = f"python{ self .short_pyversion } "
283
+ os .symlink (f"../{ py_exe } " , prefix / f"bin/{ py_exe } " )
284
+ os .symlink (f"../{ py_exe } " , prefix / f"bin/python3" )
285
+
233
286
def package (self ):
234
287
src = self .build_folder
235
288
dst = pathlib .Path (self .package_folder , "embedded_python" )
@@ -249,13 +302,15 @@ def package(self):
249
302
files .rmdir (self , "tmp" )
250
303
files .rm (self , "dev.msi" , dst )
251
304
305
+ self ._isolate (dst )
252
306
files .copy (self , "LICENSE.txt" , src = dst , dst = license_folder )
253
307
else :
254
308
from conan .tools .gnu import Autotools
255
309
256
310
autotools = Autotools (self )
257
311
autotools .install (args = ["DESTDIR=''" ]) # already handled by AutotoolsToolchain prefix
258
312
self ._patch_libpython_path (dst )
313
+ self ._isolate (dst )
259
314
260
315
# Give write permissions, otherwise end-user projects won't be able to re-import
261
316
# the shared libraries (re-import happens on subsequent `conan install` runs).
0 commit comments