@@ -1635,6 +1635,85 @@ def _is_plausible_python_version(version: str) -> bool:
16351635
16361636
16371637
1638+ def _ensure_all_version_shims (bin_dir : Path , debug_mode : bool = False ) -> None :
1639+ """
1640+ Eagerly create versioned shims for Python 3.7–3.15 in bin_dir.
1641+
1642+ On Unix: symlinks (8pkg39 -> 8pkg, omnipkg311 -> omnipkg, etc.)
1643+ On Windows: .bat wrappers with injected --python X.Y, because .bat files
1644+ cannot be launched by subprocess without cmd /c — they need
1645+ actual file content, not symlinks.
1646+
1647+ This runs at every dispatcher startup so shims exist for ALL versions
1648+ including ones not yet adopted (e.g. 3.13 before its interpreter is
1649+ downloaded). The dispatcher's --python routing + healing takes over
1650+ when the interpreter is actually missing.
1651+ """
1652+ # Case-variant aliases for case-sensitive filesystems (Linux)
1653+ if sys .platform != "win32" :
1654+ main_shim = bin_dir / "8pkg"
1655+ omni_shim = bin_dir / "omnipkg"
1656+ for alias , target in [("8PKG" , main_shim ), ("OMNIPKG" , omni_shim )]:
1657+ p = bin_dir / alias
1658+ if (not p .exists () and not p .is_symlink ()) and target .exists ():
1659+ try :
1660+ p .symlink_to (target .name )
1661+ except Exception :
1662+ pass
1663+
1664+ for minor in range (7 , 16 ):
1665+ ver = f"3.{ minor } "
1666+ flat = f"3{ minor } "
1667+ for base_name in ("8pkg" , "omnipkg" ):
1668+ if sys .platform == "win32" :
1669+ # Find the real executable to delegate to.
1670+ # IMPORTANT: only match the bare entry point (8pkg.exe / omnipkg.exe),
1671+ # never a versioned shim like 8pkg310.bat — those are outputs, not sources.
1672+ src_exe = None
1673+ for ext in (".exe" , ".bat" , ".cmd" , "" ):
1674+ candidate = bin_dir / f"{ base_name } { ext } "
1675+ # Exclude versioned shims (names contain a digit after the base)
1676+ name_stem = candidate .stem # e.g. "8pkg310" or "8pkg"
1677+ if name_stem != base_name :
1678+ continue
1679+ if candidate .exists ():
1680+ src_exe = candidate
1681+ break
1682+ if src_exe is None :
1683+ if debug_mode :
1684+ print (f"[DEBUG-DISPATCH] ⚠️ No src_exe for { base_name } in { bin_dir } — skipping { base_name } { flat } .bat" , file = sys .stderr )
1685+ continue
1686+ bat_path = bin_dir / f"{ base_name } { flat } .bat"
1687+ bat_content = f'@echo off\r \n "{ src_exe } " --python { ver } %*\r \n '
1688+ # Idempotency: skip only if existing content is already correct.
1689+ if bat_path .exists ():
1690+ try :
1691+ existing = bat_path .read_text (encoding = "ascii" )
1692+ if existing == bat_content :
1693+ continue # already correct — truly idempotent
1694+ # Content is stale (e.g. src_exe path changed) — rewrite below
1695+ if debug_mode :
1696+ print (f"[DEBUG-DISPATCH] 🔄 Stale .bat shim { bat_path .name } — rewriting" , file = sys .stderr )
1697+ except Exception :
1698+ pass # unreadable — fall through to overwrite
1699+ try :
1700+ bat_path .write_text (bat_content , encoding = "ascii" )
1701+ if debug_mode :
1702+ print (f"[DEBUG-DISPATCH] ✅ Eager .bat shim: { bat_path .name } " , file = sys .stderr )
1703+ except Exception as e :
1704+ # Always surface write failures — silent skip here is the primary
1705+ # reason shims appear to be created but are actually missing.
1706+ print (f"[WARNING-DISPATCH] Could not write { bat_path .name } : { e } " , file = sys .stderr )
1707+ else :
1708+ target = bin_dir / base_name
1709+ versioned = bin_dir / f"{ base_name } { flat } "
1710+ if (not versioned .exists () and not versioned .is_symlink ()) and target .exists ():
1711+ try :
1712+ versioned .symlink_to (target .name )
1713+ except Exception :
1714+ pass
1715+
1716+
16381717def _ensure_native_shims () -> None :
16391718 """
16401719 Self-healing shim check for the native/primary Python.
@@ -1670,16 +1749,7 @@ def _ensure_native_shims() -> None:
16701749
16711750 if shim_path .exists ():
16721751 # Still proactively create all versioned shims 3.7-3.15 if any are missing
1673- main_shim = bin_dir / "8pkg"
1674- omni_shim = bin_dir / "omnipkg"
1675- for minor in range (7 , 16 ):
1676- for prefix , target in [("8pkg" , main_shim ), ("omnipkg" , omni_shim )]:
1677- versioned = bin_dir / f"{ prefix } 3{ minor } "
1678- if (not versioned .exists () and not versioned .is_symlink ()) and target .exists ():
1679- try :
1680- versioned .symlink_to (target .name )
1681- except Exception :
1682- pass
1752+ _ensure_all_version_shims (bin_dir , debug_mode )
16831753 return # already installed, nothing to do
16841754
16851755 # Shim is missing — this is the native Python that adoption skipped.
@@ -1698,24 +1768,7 @@ def _ensure_native_shims() -> None:
16981768 # Also immediately create the full range of versioned shims so 8pkg310 etc exist.
16991769 # We do this here because the shim_path.exists() quick-exit branch won't run
17001770 # on this first call since we just created the shim above.
1701- main_shim = bin_dir / "8pkg"
1702- omni_shim = bin_dir / "omnipkg"
1703- # Case-variant aliases for case-sensitive filesystems (Linux)
1704- for alias , target in [("8PKG" , main_shim ), ("OMNIPKG" , omni_shim )]:
1705- p = bin_dir / alias
1706- if (not p .exists () and not p .is_symlink ()) and target .exists ():
1707- try :
1708- p .symlink_to (target .name )
1709- except Exception :
1710- pass
1711- for minor in range (7 , 16 ):
1712- for prefix , target in [("8pkg" , main_shim ), ("omnipkg" , omni_shim )]:
1713- versioned = bin_dir / f"{ prefix } 3{ minor } "
1714- if (not versioned .exists () and not versioned .is_symlink ()) and target .exists ():
1715- try :
1716- versioned .symlink_to (target .name )
1717- except Exception :
1718- pass
1771+ _ensure_all_version_shims (bin_dir , debug_mode )
17191772
17201773 # Call the canonical shim installer — handles Unix symlinks and Windows .bat
17211774 interpreter_path = Path (sys .executable )
@@ -1769,6 +1822,9 @@ def install_versioned_entrypoints(
17691822 src_bat = None
17701823 for ext in (".exe" , ".bat" , ".cmd" , "" ):
17711824 candidate = bin_dir / f"{ base_name } { ext } "
1825+ # Exclude versioned shims — only match the bare entry point.
1826+ if candidate .stem != base_name :
1827+ continue
17721828 if candidate .exists ():
17731829 src_bat = candidate
17741830 break
@@ -1781,16 +1837,23 @@ def install_versioned_entrypoints(
17811837 _maj = flat [0 ]
17821838 _min = flat [1 :]
17831839 _ver = f"{ _maj } .{ _min } "
1840+ bat_content = f'@echo off\r \n "{ src_bat } " --python { _ver } %*\r \n '
1841+ # Idempotency: skip only if existing content is already correct.
1842+ if link_bat .exists ():
1843+ try :
1844+ existing = link_bat .read_text (encoding = "ascii" )
1845+ if existing == bat_content :
1846+ continue # already correct
1847+ if debug_mode :
1848+ print (f"[DEBUG-DISPATCH] 🔄 Stale .bat shim { link_bat .name } — rewriting" , file = sys .stderr )
1849+ except Exception :
1850+ pass
17841851 try :
1785- # Inject --python X.Y as first arg so dispatcher version detection
1786- # works even though sys.argv[0] will be "8pkg" not "8pkg310"
1787- bat_content = f'@echo off\r \n "{ src_bat } " --python { _ver } %*\r \n '
17881852 link_bat .write_text (bat_content , encoding = "ascii" )
17891853 if debug_mode :
17901854 print (f"[DEBUG-DISPATCH] ✅ Windows .bat shim: { link_bat } -> { src_bat .name } --python { _ver } " , file = sys .stderr )
17911855 except Exception as e :
1792- if debug_mode :
1793- print (f"[DEBUG-DISPATCH] ⚠️ Could not create Windows shim { link_bat } : { e } " , file = sys .stderr )
1856+ print (f"[WARNING-DISPATCH] Could not create Windows shim { link_bat .name } : { e } " , file = sys .stderr )
17941857 else :
17951858 # Unix: relative symlink within same directory
17961859 src = bin_dir / base_name
0 commit comments