@@ -184,6 +184,19 @@ def test_getframemodulename():
184184 with tempfile .TemporaryDirectory () as directory :
185185 root = Path (directory ).resolve ()
186186 interpreter = Path (sys .executable ).resolve ()
187+ # A framework build can also run its embedded app directly. Match the
188+ # installed launcher's file identity before applying launcher semantics.
189+ framework_base = (
190+ Path (sys .base_prefix )
191+ / "bin"
192+ / f"python{ sys .version_info .major } .{ sys .version_info .minor } "
193+ )
194+ framework_launcher = (
195+ sys .platform == "darwin"
196+ and bool (getattr (sys , "_framework" , "" ))
197+ and framework_base .is_file ()
198+ and interpreter .samefile (framework_base )
199+ )
187200 home = root / "home"
188201 home .mkdir ()
189202 venv_env = dict (os .environ )
@@ -227,46 +240,50 @@ def executable_paths(executable, *, cwd=None, env=venv_env):
227240 invocation = f"./bin/{ kind } " if kind == "relative" else executable
228241 assert executable_paths (invocation , cwd = bin_dir .parent ) == [
229242 str (executable ),
230- str (interpreter ),
243+ str (framework_base if framework_launcher else interpreter ),
231244 str (prefix ),
232245 ]
233246
234- # Without a venv, preserve the invoked symlink for both executable fields .
247+ # Framework launchers preset their base; ordinary binaries keep argv[0] .
235248 plain = root / "plain" / "python"
236249 plain .parent .mkdir ()
237250 plain .symlink_to (interpreter )
238- assert executable_paths (plain )[:2 ] == [str (plain ), str (plain )]
251+ assert executable_paths (plain )[:2 ] == [
252+ str (plain ),
253+ str (framework_base if framework_launcher else plain ),
254+ ]
239255
240- # A copied executable still falls back to home/<invoked name>.
256+ # Without a launcher preset, copies fall back to home/<invoked name>.
241257 copied = root / "copied" / "python"
242258 copied .parent .mkdir ()
243259 shutil .copy2 (interpreter , copied )
244260 (home / copied .name ).touch ()
245261 (copied .parent / "pyvenv.cfg" ).write_text (f"home = { home } \n " )
246262 assert executable_paths (copied ) == [
247263 str (copied ),
248- str (home / copied .name ),
264+ str (framework_base if framework_launcher else home / copied .name ),
249265 str (copied .parent ),
250266 ]
251267
252268 # A directory alias does not turn a copied executable into a symlink.
253269 directory_alias = root / "directory-alias"
254270 directory_alias .symlink_to (copied .parent , target_is_directory = True )
255271 aliased_copy = directory_alias / copied .name
272+ # The framework launcher canonicalizes its directory before dispatch.
256273 assert executable_paths (aliased_copy ) == [
257- str (aliased_copy ),
258- str (home / copied .name ),
259- str (directory_alias ),
274+ str (copied if framework_launcher else aliased_copy ),
275+ str (framework_base if framework_launcher else home / copied .name ),
276+ str (copied . parent if framework_launcher else directory_alias ),
260277 ]
261278
262- # Resolving a final-component link must preserve aliases in its target.
279+ # Without a launcher preset, final-link resolution preserves target aliases .
263280 target_link = root / "linked" / "python"
264281 target_link .parent .mkdir ()
265282 (target_link .parent / "pyvenv.cfg" ).write_text (f"home = { home } \n " )
266283 target_link .symlink_to ("../directory-alias/python" )
267284 assert executable_paths (target_link ) == [
268285 str (target_link ),
269- str (aliased_copy ),
286+ str (framework_base if framework_launcher else aliased_copy ),
270287 str (target_link .parent ),
271288 ]
272289
0 commit comments