| 1 | """pyutil.py: Code that's only needed in Python.
 | 
| 2 | 
 | 
| 3 | C++ will use other mechanisms.
 | 
| 4 | """
 | 
| 5 | from __future__ import print_function
 | 
| 6 | 
 | 
| 7 | import sys
 | 
| 8 | import zipimport  # NOT the zipfile module.
 | 
| 9 | 
 | 
| 10 | from mycpp import mylib
 | 
| 11 | from pgen2 import grammar
 | 
| 12 | from pylib import os_path
 | 
| 13 | 
 | 
| 14 | import posix_ as posix
 | 
| 15 | 
 | 
| 16 | from typing import List, Union
 | 
| 17 | 
 | 
| 18 | # Copied from 'string' module
 | 
| 19 | _PUNCT = """!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~"""
 | 
| 20 | 
 | 
| 21 | 
 | 
| 22 | def IsValidCharEscape(ch):
 | 
| 23 |     # type: (str) -> bool
 | 
| 24 |     """Is this a valid character escape when unquoted?"""
 | 
| 25 |     # These punctuation chars never needs to be escaped.  (Note that _ is a
 | 
| 26 |     # keyword sometimes.)
 | 
| 27 |     if ch == '/' or ch == '.' or ch == '-':
 | 
| 28 |         return False
 | 
| 29 | 
 | 
| 30 |     if ch == ' ':  # foo\ bar is idiomatic
 | 
| 31 |         return True
 | 
| 32 | 
 | 
| 33 |     # Borderline: ^ and %.  But ^ is used for history?
 | 
| 34 |     # ! is an operator.  And used for history.
 | 
| 35 | 
 | 
| 36 |     # What about ^(...) or %(...) or /(...) .(1+2), etc.?
 | 
| 37 | 
 | 
| 38 |     return ch in _PUNCT  # like ispunct() in C
 | 
| 39 | 
 | 
| 40 | 
 | 
| 41 | def ChArrayToString(ch_array):
 | 
| 42 |     # type: (List[int]) -> str
 | 
| 43 |     """We avoid allocating 1 byte string objects in the C++ implementation.
 | 
| 44 | 
 | 
| 45 |     'ch' is short for an integer that represents a character.
 | 
| 46 |     """
 | 
| 47 |     return ''.join(chr(ch) for ch in ch_array)
 | 
| 48 | 
 | 
| 49 | 
 | 
| 50 | def BackslashEscape(s, meta_chars):
 | 
| 51 |     # type: (str, str) -> str
 | 
| 52 |     """Escaped certain characters with backslashes.
 | 
| 53 | 
 | 
| 54 |     Used for shell syntax (i.e. quoting completed filenames), globs, and
 | 
| 55 |     EREs.
 | 
| 56 |     """
 | 
| 57 |     escaped = []
 | 
| 58 |     for c in s:
 | 
| 59 |         if c in meta_chars:
 | 
| 60 |             escaped.append('\\')
 | 
| 61 |         escaped.append(c)
 | 
| 62 |     return ''.join(escaped)
 | 
| 63 | 
 | 
| 64 | 
 | 
| 65 | def strerror(e):
 | 
| 66 |     # type: (Union[IOError, OSError]) -> str
 | 
| 67 |     return posix.strerror(e.errno)
 | 
| 68 | 
 | 
| 69 | 
 | 
| 70 | def LoadYshGrammar(loader):
 | 
| 71 |     # type: (_ResourceLoader) -> grammar.Grammar
 | 
| 72 |     g = grammar.Grammar()
 | 
| 73 |     contents = loader.Get('_devbuild/gen/grammar.marshal')
 | 
| 74 |     g.loads(contents)
 | 
| 75 |     return g
 | 
| 76 | 
 | 
| 77 | 
 | 
| 78 | class _ResourceLoader(object):
 | 
| 79 | 
 | 
| 80 |     def Get(self, rel_path):
 | 
| 81 |         # type: (str) -> str
 | 
| 82 |         raise NotImplementedError()
 | 
| 83 | 
 | 
| 84 | 
 | 
| 85 | class _FileResourceLoader(_ResourceLoader):
 | 
| 86 |     """Open resources relative to argv[0]."""
 | 
| 87 | 
 | 
| 88 |     def __init__(self, root_dir):
 | 
| 89 |         # type: (str) -> None
 | 
| 90 |         self.root_dir = root_dir
 | 
| 91 | 
 | 
| 92 |     def Get(self, rel_path):
 | 
| 93 |         # type: (str) -> str
 | 
| 94 |         with open(os_path.join(self.root_dir, rel_path)) as f:
 | 
| 95 |             contents = f.read()
 | 
| 96 |         return contents
 | 
| 97 | 
 | 
| 98 | 
 | 
| 99 | class _ZipResourceLoader(_ResourceLoader):
 | 
| 100 |     """Open resources INSIDE argv[0] as a zip file."""
 | 
| 101 | 
 | 
| 102 |     def __init__(self, argv0):
 | 
| 103 |         # type: (str) -> None
 | 
| 104 |         self.z = zipimport.zipimporter(argv0)
 | 
| 105 | 
 | 
| 106 |     def Get(self, rel_path):
 | 
| 107 |         # type: (str) -> str
 | 
| 108 |         return self.z.get_data(rel_path)
 | 
| 109 | 
 | 
| 110 | 
 | 
| 111 | def IsAppBundle():
 | 
| 112 |     # type: () -> bool
 | 
| 113 |     """Are we running inside Oil's patched version of CPython?
 | 
| 114 | 
 | 
| 115 |     As opposed to a "stock" Python interpreter.
 | 
| 116 |     """
 | 
| 117 |     # Ovm_Main in main.c sets this.
 | 
| 118 |     return posix.environ.get('_OVM_IS_BUNDLE') == '1'
 | 
| 119 | 
 | 
| 120 | 
 | 
| 121 | _loader = None  # type: _ResourceLoader
 | 
| 122 | 
 | 
| 123 | 
 | 
| 124 | def GetResourceLoader():
 | 
| 125 |     # type: () -> _ResourceLoader
 | 
| 126 |     global _loader
 | 
| 127 |     if _loader:
 | 
| 128 |         return _loader
 | 
| 129 | 
 | 
| 130 |     if IsAppBundle():
 | 
| 131 |         ovm_path = posix.environ.get('_OVM_PATH')
 | 
| 132 |         _loader = _ZipResourceLoader(ovm_path)
 | 
| 133 | 
 | 
| 134 |         # Now clear them so we don't pollute the environment.  In Python, this
 | 
| 135 |         # calls unsetenv().
 | 
| 136 |         del posix.environ['_OVM_IS_BUNDLE']
 | 
| 137 |         del posix.environ['_OVM_PATH']
 | 
| 138 | 
 | 
| 139 |     elif posix.environ.get('_OVM_RESOURCE_ROOT'):  # Unit tests set this
 | 
| 140 |         root_dir = posix.environ.get('_OVM_RESOURCE_ROOT')
 | 
| 141 |         _loader = _FileResourceLoader(root_dir)
 | 
| 142 | 
 | 
| 143 |     else:
 | 
| 144 |         # Find resources relative to the binary, e.g.
 | 
| 145 |         # ~/git/oilshell/oil/bin/oil.py.  But it also assumes that all unit tests
 | 
| 146 |         # that use resources are are one directory deep, e.g. core/util_test.py.
 | 
| 147 |         bin_dir = os_path.dirname(os_path.abspath(sys.argv[0]))
 | 
| 148 |         root_dir = os_path.join(bin_dir, '..')  # ~/git/oilshell/oil
 | 
| 149 |         _loader = _FileResourceLoader(root_dir)
 | 
| 150 | 
 | 
| 151 |     return _loader
 | 
| 152 | 
 | 
| 153 | 
 | 
| 154 | def GetVersion(loader):
 | 
| 155 |     # type: (_ResourceLoader) -> str
 | 
| 156 |     contents = loader.Get('oil-version.txt')
 | 
| 157 |     version_str, _ = mylib.split_once(contents, '\n')
 | 
| 158 |     return version_str
 | 
| 159 | 
 | 
| 160 | 
 | 
| 161 | def PrintVersionDetails(loader):
 | 
| 162 |     # type: (_ResourceLoader) -> None
 | 
| 163 |     """Show version and platform information."""
 | 
| 164 |     try:
 | 
| 165 |         contents = loader.Get('release-date.txt')
 | 
| 166 |         release_date, _ = mylib.split_once(contents, '\n')
 | 
| 167 |     except IOError:
 | 
| 168 |         release_date = '-'  # in dev tree
 | 
| 169 | 
 | 
| 170 |     try:
 | 
| 171 |         contents = loader.Get('pyc-version.txt')
 | 
| 172 |         pyc_version, _ = mylib.split_once(contents, '\n')
 | 
| 173 |     except IOError:
 | 
| 174 |         pyc_version = '-'  # in dev tree
 | 
| 175 | 
 | 
| 176 |     # node is like 'hostname'
 | 
| 177 |     # release is the kernel version
 | 
| 178 |     system, unused_node, unused_release, platform_version, machine = posix.uname(
 | 
| 179 |     )
 | 
| 180 | 
 | 
| 181 |     # The platform.py module has a big regex that parses sys.version, but we
 | 
| 182 |     # don't want to depend on regular expressions.  So we will do our own parsing
 | 
| 183 |     # here.
 | 
| 184 |     version_line, py_compiler = sys.version.splitlines()
 | 
| 185 | 
 | 
| 186 |     # Pick off the first part of '2.7.12 (default, ...)'
 | 
| 187 |     py_version = version_line.split()[0]
 | 
| 188 | 
 | 
| 189 |     assert py_compiler.startswith('['), py_compiler
 | 
| 190 |     assert py_compiler.endswith(']'), py_compiler
 | 
| 191 |     py_compiler = py_compiler[1:-1]
 | 
| 192 | 
 | 
| 193 |     # We removed sys.executable from sysmodule.c.
 | 
| 194 |     py_impl = 'CPython' if hasattr(sys, 'executable') else 'OVM'
 | 
| 195 | 
 | 
| 196 |     # Call it OSH because "Oil" is deprecated
 | 
| 197 |     print('Release Date: %s' % release_date)
 | 
| 198 |     print('Arch: %s' % machine)
 | 
| 199 |     print('OS: %s' % system)
 | 
| 200 |     print('Platform: %s' % platform_version)
 | 
| 201 |     print('Compiler: %s' % py_compiler)
 | 
| 202 |     print('Interpreter: %s' % py_impl)
 | 
| 203 |     print('Interpreter version: %s' % py_version)
 | 
| 204 |     print('Bytecode: %s' % pyc_version)
 | 
| 205 | 
 | 
| 206 |     # TODO: advertise oils-for-unix when it's ready
 | 
| 207 | 
 | 
| 208 | 
 | 
| 209 | # This was useful for debugging.
 | 
| 210 | def ShowFdState():
 | 
| 211 |     # type: () -> None
 | 
| 212 |     import subprocess
 | 
| 213 |     import posix_ as posix
 | 
| 214 |     subprocess.call(['ls', '-l', '/proc/%d/fd' % posix.getpid()])
 |