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