1# -*- coding: utf-8 -*- 2"""The Apple Framework builds require their own customization""" 3import logging 4import os 5import struct 6import subprocess 7from abc import ABCMeta, abstractmethod 8from textwrap import dedent 9 10from six import add_metaclass 11 12from virtualenv.create.via_global_ref.builtin.ref import ExePathRefToDest, PathRefToDest 13from virtualenv.util.path import Path 14from virtualenv.util.six import ensure_text 15 16from .common import CPython, CPythonPosix, is_mac_os_framework 17from .cpython2 import CPython2PosixBase 18from .cpython3 import CPython3 19 20 21@add_metaclass(ABCMeta) 22class CPythonmacOsFramework(CPython): 23 @classmethod 24 def can_describe(cls, interpreter): 25 return is_mac_os_framework(interpreter) and super(CPythonmacOsFramework, cls).can_describe(interpreter) 26 27 @classmethod 28 def sources(cls, interpreter): 29 for src in super(CPythonmacOsFramework, cls).sources(interpreter): 30 yield src 31 # add a symlink to the host python image 32 ref = PathRefToDest(cls.image_ref(interpreter), dest=lambda self, _: self.dest / ".Python", must_symlink=True) 33 yield ref 34 35 def create(self): 36 super(CPythonmacOsFramework, self).create() 37 38 # change the install_name of the copied python executables 39 target = "@executable_path/../.Python" 40 current = self.current_mach_o_image_path() 41 for src in self._sources: 42 if isinstance(src, ExePathRefToDest): 43 if src.must_copy or not self.symlinks: 44 exes = [self.bin_dir / src.base] 45 if not self.symlinks: 46 exes.extend(self.bin_dir / a for a in src.aliases) 47 for exe in exes: 48 fix_mach_o(str(exe), current, target, self.interpreter.max_size) 49 50 @classmethod 51 def _executables(cls, interpreter): 52 for _, targets in super(CPythonmacOsFramework, cls)._executables(interpreter): 53 # Make sure we use the embedded interpreter inside the framework, even if sys.executable points to the 54 # stub executable in ${sys.prefix}/bin. 55 # See http://groups.google.com/group/python-virtualenv/browse_thread/thread/17cab2f85da75951 56 fixed_host_exe = Path(interpreter.prefix) / "Resources" / "Python.app" / "Contents" / "MacOS" / "Python" 57 yield fixed_host_exe, targets 58 59 @abstractmethod 60 def current_mach_o_image_path(self): 61 raise NotImplementedError 62 63 @classmethod 64 def image_ref(cls, interpreter): 65 raise NotImplementedError 66 67 68class CPython2macOsFramework(CPythonmacOsFramework, CPython2PosixBase): 69 @classmethod 70 def image_ref(cls, interpreter): 71 return Path(interpreter.prefix) / "Python" 72 73 def current_mach_o_image_path(self): 74 return os.path.join(self.interpreter.prefix, "Python") 75 76 @classmethod 77 def sources(cls, interpreter): 78 for src in super(CPython2macOsFramework, cls).sources(interpreter): 79 yield src 80 # landmark for exec_prefix 81 exec_marker_file, to_path, _ = cls.from_stdlib(cls.mappings(interpreter), "lib-dynload") 82 yield PathRefToDest(exec_marker_file, dest=to_path) 83 84 @property 85 def reload_code(self): 86 result = super(CPython2macOsFramework, self).reload_code 87 result = dedent( 88 """ 89 # the bundled site.py always adds the global site package if we're on python framework build, escape this 90 import sysconfig 91 config = sysconfig.get_config_vars() 92 before = config["PYTHONFRAMEWORK"] 93 try: 94 config["PYTHONFRAMEWORK"] = "" 95 {} 96 finally: 97 config["PYTHONFRAMEWORK"] = before 98 """.format( 99 result, 100 ), 101 ) 102 return result 103 104 105class CPython3macOsFramework(CPythonmacOsFramework, CPython3, CPythonPosix): 106 @classmethod 107 def image_ref(cls, interpreter): 108 return Path(interpreter.prefix) / "Python3" 109 110 def current_mach_o_image_path(self): 111 return "@executable_path/../../../../Python3" 112 113 @property 114 def reload_code(self): 115 result = super(CPython3macOsFramework, self).reload_code 116 result = dedent( 117 """ 118 # the bundled site.py always adds the global site package if we're on python framework build, escape this 119 import sys 120 before = sys._framework 121 try: 122 sys._framework = None 123 {} 124 finally: 125 sys._framework = before 126 """.format( 127 result, 128 ), 129 ) 130 return result 131 132 133def fix_mach_o(exe, current, new, max_size): 134 """ 135 https://en.wikipedia.org/wiki/Mach-O 136 137 Mach-O, short for Mach object file format, is a file format for executables, object code, shared libraries, 138 dynamically-loaded code, and core dumps. A replacement for the a.out format, Mach-O offers more extensibility and 139 faster access to information in the symbol table. 140 141 Each Mach-O file is made up of one Mach-O header, followed by a series of load commands, followed by one or more 142 segments, each of which contains between 0 and 255 sections. Mach-O uses the REL relocation format to handle 143 references to symbols. When looking up symbols Mach-O uses a two-level namespace that encodes each symbol into an 144 'object/symbol name' pair that is then linearly searched for by first the object and then the symbol name. 145 146 The basic structure—a list of variable-length "load commands" that reference pages of data elsewhere in the file—was 147 also used in the executable file format for Accent. The Accent file format was in turn, based on an idea from Spice 148 Lisp. 149 150 With the introduction of Mac OS X 10.6 platform the Mach-O file underwent a significant modification that causes 151 binaries compiled on a computer running 10.6 or later to be (by default) executable only on computers running Mac 152 OS X 10.6 or later. The difference stems from load commands that the dynamic linker, in previous Mac OS X versions, 153 does not understand. Another significant change to the Mach-O format is the change in how the Link Edit tables 154 (found in the __LINKEDIT section) function. In 10.6 these new Link Edit tables are compressed by removing unused and 155 unneeded bits of information, however Mac OS X 10.5 and earlier cannot read this new Link Edit table format. 156 """ 157 try: 158 logging.debug(u"change Mach-O for %s from %s to %s", ensure_text(exe), current, ensure_text(new)) 159 _builtin_change_mach_o(max_size)(exe, current, new) 160 except Exception as e: 161 logging.warning("Could not call _builtin_change_mac_o: %s. " "Trying to call install_name_tool instead.", e) 162 try: 163 cmd = ["install_name_tool", "-change", current, new, exe] 164 subprocess.check_call(cmd) 165 except Exception: 166 logging.fatal("Could not call install_name_tool -- you must " "have Apple's development tools installed") 167 raise 168 169 170def _builtin_change_mach_o(maxint): 171 MH_MAGIC = 0xFEEDFACE 172 MH_CIGAM = 0xCEFAEDFE 173 MH_MAGIC_64 = 0xFEEDFACF 174 MH_CIGAM_64 = 0xCFFAEDFE 175 FAT_MAGIC = 0xCAFEBABE 176 BIG_ENDIAN = ">" 177 LITTLE_ENDIAN = "<" 178 LC_LOAD_DYLIB = 0xC 179 180 class FileView(object): 181 """A proxy for file-like objects that exposes a given view of a file. Modified from macholib.""" 182 183 def __init__(self, file_obj, start=0, size=maxint): 184 if isinstance(file_obj, FileView): 185 self._file_obj = file_obj._file_obj 186 else: 187 self._file_obj = file_obj 188 self._start = start 189 self._end = start + size 190 self._pos = 0 191 192 def __repr__(self): 193 return "<fileview [{:d}, {:d}] {!r}>".format(self._start, self._end, self._file_obj) 194 195 def tell(self): 196 return self._pos 197 198 def _checkwindow(self, seek_to, op): 199 if not (self._start <= seek_to <= self._end): 200 msg = "{} to offset {:d} is outside window [{:d}, {:d}]".format(op, seek_to, self._start, self._end) 201 raise IOError(msg) 202 203 def seek(self, offset, whence=0): 204 seek_to = offset 205 if whence == os.SEEK_SET: 206 seek_to += self._start 207 elif whence == os.SEEK_CUR: 208 seek_to += self._start + self._pos 209 elif whence == os.SEEK_END: 210 seek_to += self._end 211 else: 212 raise IOError("Invalid whence argument to seek: {!r}".format(whence)) 213 self._checkwindow(seek_to, "seek") 214 self._file_obj.seek(seek_to) 215 self._pos = seek_to - self._start 216 217 def write(self, content): 218 here = self._start + self._pos 219 self._checkwindow(here, "write") 220 self._checkwindow(here + len(content), "write") 221 self._file_obj.seek(here, os.SEEK_SET) 222 self._file_obj.write(content) 223 self._pos += len(content) 224 225 def read(self, size=maxint): 226 assert size >= 0 227 here = self._start + self._pos 228 self._checkwindow(here, "read") 229 size = min(size, self._end - here) 230 self._file_obj.seek(here, os.SEEK_SET) 231 read_bytes = self._file_obj.read(size) 232 self._pos += len(read_bytes) 233 return read_bytes 234 235 def read_data(file, endian, num=1): 236 """Read a given number of 32-bits unsigned integers from the given file with the given endianness.""" 237 res = struct.unpack(endian + "L" * num, file.read(num * 4)) 238 if len(res) == 1: 239 return res[0] 240 return res 241 242 def mach_o_change(at_path, what, value): 243 """Replace a given name (what) in any LC_LOAD_DYLIB command found in the given binary with a new name (value), 244 provided it's shorter.""" 245 246 def do_macho(file, bits, endian): 247 # Read Mach-O header (the magic number is assumed read by the caller) 248 cpu_type, cpu_sub_type, file_type, n_commands, size_of_commands, flags = read_data(file, endian, 6) 249 # 64-bits header has one more field. 250 if bits == 64: 251 read_data(file, endian) 252 # The header is followed by n commands 253 for _ in range(n_commands): 254 where = file.tell() 255 # Read command header 256 cmd, cmd_size = read_data(file, endian, 2) 257 if cmd == LC_LOAD_DYLIB: 258 # The first data field in LC_LOAD_DYLIB commands is the offset of the name, starting from the 259 # beginning of the command. 260 name_offset = read_data(file, endian) 261 file.seek(where + name_offset, os.SEEK_SET) 262 # Read the NUL terminated string 263 load = file.read(cmd_size - name_offset).decode() 264 load = load[: load.index("\0")] 265 # If the string is what is being replaced, overwrite it. 266 if load == what: 267 file.seek(where + name_offset, os.SEEK_SET) 268 file.write(value.encode() + b"\0") 269 # Seek to the next command 270 file.seek(where + cmd_size, os.SEEK_SET) 271 272 def do_file(file, offset=0, size=maxint): 273 file = FileView(file, offset, size) 274 # Read magic number 275 magic = read_data(file, BIG_ENDIAN) 276 if magic == FAT_MAGIC: 277 # Fat binaries contain nfat_arch Mach-O binaries 278 n_fat_arch = read_data(file, BIG_ENDIAN) 279 for _ in range(n_fat_arch): 280 # Read arch header 281 cpu_type, cpu_sub_type, offset, size, align = read_data(file, BIG_ENDIAN, 5) 282 do_file(file, offset, size) 283 elif magic == MH_MAGIC: 284 do_macho(file, 32, BIG_ENDIAN) 285 elif magic == MH_CIGAM: 286 do_macho(file, 32, LITTLE_ENDIAN) 287 elif magic == MH_MAGIC_64: 288 do_macho(file, 64, BIG_ENDIAN) 289 elif magic == MH_CIGAM_64: 290 do_macho(file, 64, LITTLE_ENDIAN) 291 292 assert len(what) >= len(value) 293 294 with open(at_path, "r+b") as f: 295 do_file(f) 296 297 return mach_o_change 298