Created
October 7, 2025 06:57
-
-
Save hsupu/feacdda135332d847bd5e3ccaa3ee351 to your computer and use it in GitHub Desktop.
Script to fix python venv exe after moved.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 251004 | |
| # see https://stackoverflow.com/questions/35412392/how-can-i-use-setuptools-to-create-an-exe-launcher | |
| from __future__ import annotations | |
| from pip._vendor.distlib.scripts import ScriptMaker | |
| import io | |
| import logging | |
| import os | |
| import re | |
| import sys | |
| import zipfile | |
| def main(args = None): | |
| logger = logging.getLogger(__name__) | |
| # Windows venv 不在 bin/ 子目录 | |
| bindir = os.path.join(sys.exec_prefix, "Scripts") | |
| # logger.info(bindir) | |
| regex_shabang = re.compile(r'^#!\s*(.+\\python(?:w)?\.exe)\s*$', re.MULTILINE) | |
| regex_entrypoint = re.compile(r'^\s*from (\S+) import (\S+)\s*$', re.MULTILINE) | |
| sm = ScriptMaker( | |
| source_dir=None, # None to using entry spec instead | |
| target_dir=bindir, # folder to put | |
| add_launchers=True, # True to create .exe, False to create .py | |
| ) | |
| pyexe = sys.executable | |
| # while os.path.islink(pyexe): | |
| # pyexe = os.path.realpath(pyexe) | |
| # pyexe = os.path.abspath(pyexe) | |
| # 对 Windows 而言 python pythonw 不同,后者没有下挂控制台窗口 | |
| sm.executable = pyexe | |
| if sm.executable.endswith("pythonw.exe") > -1: | |
| sm.executable = sm.executable.replace("pythonw", "python") | |
| # create only the main variant (not the one with X.Y suffix) | |
| sm.variants = [""] | |
| for filename in os.listdir(bindir): | |
| if not filename.endswith('.exe'): | |
| continue | |
| if filename in ('python.exe', 'pythonw.exe', 'python_d.exe', 'pythonw_d.exe'): | |
| continue | |
| fullname = os.path.join(bindir, filename) | |
| logger.info(f'Open {fullname}') | |
| with open(fullname, 'rb') as f: | |
| # 解析 PE format,确定 overlay 位置 | |
| # | |
| # 0:2 MZ | |
| f.seek(0) | |
| if f.read(2) != b'MZ': | |
| logger.error(f'Not a MZ file: {filename}') | |
| continue | |
| # 0x3C:4 e_lfanew, offset to PE header | |
| f.seek(0x3C) | |
| pe_offset = int.from_bytes(f.read(4), 'little') | |
| # 0:4 PE\0\0 | |
| f.seek(pe_offset) | |
| if f.read(4) != b'PE\0\0': | |
| logger.error(f'Not a PE file: {filename}') | |
| continue | |
| pe_offset += 4 | |
| # 2:2 NumberOfSections | |
| f.seek(pe_offset + 2) | |
| NumberOfSections = int.from_bytes(f.read(2), 'little', signed=False) | |
| # 16:2 SizeOfOptionalHeader | |
| f.seek(pe_offset + 16) | |
| SizeOfOptionalHeader = int.from_bytes(f.read(2), 'little', signed=False) | |
| # skip OptionalHeader | |
| pe_offset += 20 + SizeOfOptionalHeader | |
| # now at SectionTable | |
| # each section 40 bytes | |
| max_end = 0 | |
| for i in range(NumberOfSections): | |
| # 16:4 SizeOfRawData | |
| f.seek(pe_offset + i * 40 + 16) | |
| SizeOfRawData = int.from_bytes(f.read(4), 'little', signed=False) | |
| # 20:4 PointerToRawData | |
| f.seek(pe_offset + i * 40 + 20) | |
| PointerToRawData = int.from_bytes(f.read(4), 'little', signed=False) | |
| end = PointerToRawData + SizeOfRawData | |
| # logger.debug(f'Section {i}: {PointerToRawData:x} + {SizeOfRawData:x} = {end:x}') | |
| if end > max_end: | |
| max_end = end | |
| logger.debug(f'Overlay offset: {max_end:x}') | |
| f.seek(max_end) | |
| # 目前有两种情况得到支持 | |
| header = f.read(2) | |
| if header == b'PK': | |
| def parse_250806(): | |
| """ | |
| 整体是 zip(__main__.py) | |
| 需要手动解压缩得到 __main__.py,该文件头部有 shabang python.exe | |
| """ | |
| f.seek(max_end) | |
| zip_data = f.read() | |
| with zipfile.ZipFile(io.BytesIO(zip_data)) as zf: | |
| with zf.open('__main__.py') as main_py: | |
| content = main_py.read().decode() | |
| match = regex_shabang.search(content) | |
| if not match: | |
| logger.error(f'shabang not matched our regex pattern, skipping: {filename} {content}') | |
| return None | |
| if match.group(1).casefold() == pyexe.casefold(): | |
| logger.debug(f'no need to update: {filename}') | |
| return None | |
| content = parse_250806() | |
| elif header == b'#!': | |
| def parse_220806(): | |
| """ | |
| 结构是 shabang python.exe + \n + zip(__main__.py) | |
| 运行 python 来解压缩得到 __main__.py | |
| """ | |
| shabang = '#!' + f.readline(1024).decode().rstrip() | |
| match = regex_shabang.search(shabang) | |
| if not match: | |
| logger.error(f'shabang not matched our regex pattern, skipping: {filename} {shabang}') | |
| return None | |
| if match.group(1).casefold() == pyexe.casefold(): | |
| logger.debug(f'no need to update: {filename}') | |
| return None | |
| zip_data = f.read() | |
| with zipfile.ZipFile(io.BytesIO(zip_data)) as zf: | |
| with zf.open('__main__.py') as main_py: | |
| content = main_py.read().decode() | |
| return content | |
| content = parse_220806() | |
| else: | |
| logger.error(f'Unknown header, fix_entrypoints.py is outdated?: {filename} {header}') | |
| continue | |
| if content is None: | |
| continue | |
| logger.debug(content) | |
| match = regex_entrypoint.search(content) | |
| if not match: | |
| logger.error(f'Entrypoint not matched. fix_entrypoints.py is outdated?: {filename}\n{content}') | |
| return | |
| basename, extname = os.path.basename(fullname).rsplit('.', maxsplit=1) | |
| modulepath, varname = match.group(1), match.group(2) | |
| spec = f"{basename} = {modulepath}:{varname}" | |
| logger.warning(spec) | |
| # return | |
| # provide an entry specification string here, just like in pyproject.toml | |
| sm.make(spec) | |
| # return | |
| if __name__ == '__main__': | |
| import argparse | |
| parser = argparse.ArgumentParser() | |
| parser.add_argument('-v', '--verbose', action='count', default=0) | |
| args = parser.parse_args() | |
| if args.verbose > 1: | |
| logging.basicConfig(level=logging.DEBUG) | |
| elif args.verbose > 0: | |
| logging.basicConfig(level=logging.INFO) | |
| else: | |
| logging.basicConfig(level=logging.WARNING) | |
| main(args) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment