You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

202 lines
7.3 KiB

# This file is part of Korman.
#
# Korman is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Korman is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Korman. If not, see <http://www.gnu.org/licenses/>.
from __future__ import generators # Python 2.2
import marshal
import os.path
import sys
_python_executables = {}
class PythonNotAvailableError(Exception):
pass
def compyle(file_name, py_code, py_version, report=None):
# NOTE: Should never run under Python 2.x
my_version = sys.version_info[:2]
assert my_version == (2, 7) or my_version[0] > 2
# Remember: Python 2.2 file, so no single line if statements...
idx = file_name.find('.')
if idx == -1:
module_name = file_name
else:
module_name = file_name[:idx]
if report is not None:
report.msg("Compyling {}", file_name)
if my_version != py_version:
import subprocess
py_executable = _find_python(py_version)
args = (py_executable, __file__, module_name)
try:
py_code = py_code.encode("utf-8")
except UnicodeError:
if report is not None:
report.error("Could not encode '{}'", file_name, indent=report.indent_level+1)
return (False, "Could not encode file")
result = subprocess.run(args, input=py_code, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
if result.returncode != 0:
try:
error = result.stdout.decode("utf-8").replace('\r\n', '\n')
except UnicodeError:
error = result.stdout
if report is not None:
report.error("Compylation Error in '{}'\n{}", file_name, error, indent=report.indent_level+1)
return (result.returncode == 0, result.stdout)
else:
raise NotImplementedError()
def _compyle(module_name, py_code):
# Old python versions have major issues with Windows style newlines.
# Also, bad things happen if there is no newline at the end.
py_code += '\n' # sigh, this is slow on old Python...
py_code = py_code.replace('\r\n', '\n')
py_code = py_code.replace('\r', '\n')
code_object = compile(py_code, module_name, "exec")
# The difference between us and the py_compile module is twofold:
# 1) py_compile compyles to a file. We might be exporting to memory, so that's
# not what we want.
# 2) py_compile saves a *pyc format file containing information such as compyle
# time and marshal format version. These items are not included in Cyan's
# Python.pak format.
# Therefore, we simply return the marshalled data as a string.
return marshal.dumps(code_object)
def _find_python(py_version):
def find_executable(py_version):
# First, try to use Blender to find the Python executable
try:
import bpy
except ImportError:
pass
else:
userprefs = bpy.context.user_preferences.addons["korman"].preferences
py_executable = getattr(userprefs, "python{}{}_executable".format(*py_version), None)
if verify_python(py_version, py_executable):
return py_executable
# Second, try looking Python up in the registry.
try:
import winreg
except ImportError:
pass
else:
py_executable = _find_python_reg(winreg.HKEY_CURRENT_USER, py_version)
if verify_python(py_version, py_executable):
return py_executable
py_executable = _find_python_reg(winreg.HKEY_LOCAL_MACHINE, py_version)
if verify_python(py_version, py_executable):
return py_executable
# I give up, you win.
return None
py_executable = _python_executables.setdefault(py_version, find_executable(py_version))
if py_executable:
return py_executable
else:
raise PythonNotAvailableError("{}.{}".format(*py_version))
def _find_python_reg(reg_key, py_version):
import winreg
subkey_name = "Software\\Python\\PythonCore\\{}.{}\\InstallPath".format(*py_version)
try:
python_dir = winreg.QueryValue(reg_key, subkey_name)
except FileNotFoundError:
return None
else:
return os.path.join(python_dir, "python.exe")
def package_python(stream, pyc_objects):
# Python.pak format:
# uint32_t numFiles
# - safeStr filename
# - uint32_t offset
# ~~~~~
# uint32_t filesz
# uint8_t data[filesz]
if not pyc_objects:
stream.writeInt(0)
return
# `stream` might be a plEncryptedStream, which doesn't seek very well at all.
# Therefore, we will go ahead and calculate the size of the index block so
# there is no need to seek around to write offset values
base_offset = 4 # uint32_t numFiles
data_offset = 0
pyc_info = [] # sad, but makes life easier...
for module_name, compyled_code in pyc_objects:
pyc_info.append((module_name, data_offset, compyled_code))
# index offset overall
base_offset += 2 # writeSafeStr length
# NOTE: This assumes that libHSPlasma's hsStream::writeSafeStr converts
# the Python unicode/string object to UTF-8. Currently, this is true.
base_offset += len(module_name.encode("utf-8")) # writeSafeStr
base_offset += 4
# current file data offset
data_offset += 4 # uint32_t filesz
data_offset += len(compyled_code)
stream.writeInt(len(pyc_info))
for module_name, data_offset, compyled_code in pyc_info:
stream.writeSafeStr(module_name)
# offset of data == index size (base_offset) + offset to data blob (data_offset)
stream.writeInt(base_offset + data_offset)
for module_name, data_offset, compyled_code in pyc_info:
stream.writeInt(len(compyled_code))
stream.write(compyled_code)
def verify_python(py_version, py_exe):
if not py_exe:
return False
import subprocess
try:
args = (py_exe, "-V")
result = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=5)
except OSError:
return False
else:
output = result.stdout.decode()
try:
py_str, py_check = output[:6], output[7:10]
except IndexError:
return False
else:
if py_str != "Python":
return False
return "{}.{}".format(*py_version) == py_check
if __name__ == "__main__":
# Python tries to be "helpful" on Windows by converting \n to \r\n.
# Therefore we must change the mode of stdout.
if sys.platform == "win32":
import os, msvcrt
msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
try:
module_name = sys.argv[1]
except IndexError:
module_name = "<string>"
py_code_source = sys.stdin.read()
py_code_object = _compyle(module_name, py_code_source)
sys.stdout.write(py_code_object)