#! /usr/bin/env python # -*- coding: utf-8 -*- # ############################################################# # # # Copyright @ 2018 - Dashingsoft corp. # # All rights reserved. # # # # pyarmor # # # # Version: 3.4.0 - # # # ############################################################# # # # @File: utils.py # # @Author: Jondy Zhao(jondy.zhao@gmail.com) # # @Create Date: 2018/01/17 # # @Description: # # All the routines of pytransform. # import hashlib import logging import os import re import shutil import struct import sys from base64 import b64encode, b64decode from codecs import BOM_UTF8 from glob import glob from json import dumps as json_dumps, loads as json_loads from subprocess import PIPE, Popen, check_output from time import gmtime, strftime from zipfile import ZipFile try: from urllib.request import urlopen, Request except ImportError: from urllib2 import urlopen, Request import pytransform from config import dll_ext, dll_name, entry_lines, protect_code_template, \ platform_url, platform_config, \ core_version, capsule_filename, platform_old_urls, sppmode_info from sppmode import build as sppbuild, mixin as sppmixin PYARMOR_PATH = os.getenv('PYARMOR_PATH', os.path.dirname(__file__)) PYARMOR_HOME = os.getenv('PYARMOR_HOME', os.path.join('~', '.pyarmor')) PYARMOR_TIMEOUT = float(os.getenv('PYARMOR_TIMEOUT', '6.0')) PLATFORM_PATH = os.path.join(PYARMOR_PATH, pytransform.plat_path) HOME_PATH = os.path.abspath(os.path.expanduser(PYARMOR_HOME)) CROSS_PLATFORM_PATH = os.path.join(HOME_PATH, pytransform.plat_path) DEFAULT_CAPSULE = os.path.join(HOME_PATH, capsule_filename) # From v6.2.0, change the location of default capsule to ~/.pyarmor/ OLD_CAPSULE = os.path.join(HOME_PATH, '..', capsule_filename) FEATURE_ANTI = 1 FEATURE_JIT = 2 FEATURE_ADV = 4 FEATURE_MAPOP = 8 FEATURE_VM = 16 def _format_platid(platid=None): if platid is None: platid = pytransform.format_platform() if os.path.isabs(platid) or os.path.isfile(platid): return os.path.normpath(platid) return platid.replace('\\', '/').replace('/', '.') def _search_downloaded_files(path, platid, libname): libpath = os.path.join(path, platid) if os.path.exists(libpath): for x in os.listdir(libpath): if os.path.exists(os.path.join(libpath, x, libname)): return os.path.join(platid, x) def pytransform_bootstrap(capsule=None, force=False): if pytransform._pytransform is not None and not force: logging.debug('No bootstrap, pytransform has been loaded') return logging.debug('PyArmor installation path: %s', PYARMOR_PATH) logging.debug('PyArmor home path: %s', HOME_PATH) path = PYARMOR_PATH licfile = os.path.join(path, 'license.lic') if not os.path.exists(licfile): if not os.getenv('PYARMOR_HOME', os.getenv('HOME', os.getenv('USERPROFILE'))): logging.info('Create trial license file: %s', licfile) shutil.copy(os.path.join(path, 'license.tri'), licfile) else: licfile = os.path.join(HOME_PATH, 'license.lic') if not os.path.exists(licfile): if not os.path.exists(HOME_PATH): logging.info('Create pyarmor home path: %s', HOME_PATH) os.makedirs(HOME_PATH) old_license = os.path.join(HOME_PATH, '..', 'license.lic') if os.path.exists(old_license): logging.info('Create license file %s from old license %s', licfile, old_license) shutil.move(old_license, licfile) else: logging.info('Create trial license file: %s', licfile) shutil.copy(os.path.join(path, 'license.tri'), licfile) if sys.platform.startswith('cygwin'): pyshieldlic = os.path.join(path, 'pyshield.lic') shutil.copy(pyshieldlic, HOME_PATH) if sys.platform.startswith('cygwin'): os.environ['PYARMOR_CYGHOME'] = check_output( ['cygpath', '-w', HOME_PATH.strip('/')] ) libname = dll_name + dll_ext platid = pytransform.format_platform() logging.debug('Native platform is %s', _format_platid(platid)) if os.getenv('PYARMOR_PLATFORM'): p = os.getenv('PYARMOR_PLATFORM') logging.info('PYARMOR_PLATFORM is %s', p) platid = os.path.normpath(p) if os.path.isabs(p) or os.path.isfile(p) \ else os.path.join(*os.path.normpath(p).split('.')) logging.debug('Build platform is %s', _format_platid(platid)) if os.path.isabs(platid): if not os.path.exists(os.path.join(platid, libname)): raise RuntimeError('No dynamic library found at %s', platid) elif not os.path.isfile(platid): libpath = PLATFORM_PATH logging.debug('Search dynamic library in the path: %s', libpath) if not os.path.exists(os.path.join(libpath, platid, libname)): libpath = CROSS_PLATFORM_PATH logging.debug('Search dynamic library in the path: %s', libpath) if not os.path.exists(os.path.join(libpath, platid, libname)): found = _search_downloaded_files(libpath, platid, libname) if found: logging.debug('Found available dynamic library %s', found) platid = found else: if not os.path.exists(libpath): logging.info('Create cross platform libraries path %s', libpath) os.makedirs(libpath) rid = download_pytransform(platid, libpath, firstonly=1)[0] platid = os.path.join(*rid.split('.')) if libpath == CROSS_PLATFORM_PATH: platid = os.path.abspath(os.path.join(libpath, platid)) pytransform.pyarmor_init(platid=platid) logging.debug('Loaded dynamic library: %s', pytransform._pytransform._name) ver = pytransform.version_info() logging.debug('The version of core library is %s', ver) if ver[0] < 32: raise RuntimeError('PyArmor does not work with this core library ' '(r%d), which reversion < r32, please remove ' '"%s" then run command again' % (ver[0], platid)) if capsule is not None and not os.path.exists(capsule): logging.info('Generating public capsule ...') make_capsule(capsule) def _get_old_remote_file(path, timeout=6.0): for prefix in platform_old_urls: url = '/'.join([prefix, path]) logging.info('Getting remote file: %s', url) try: return _urlopen(url, timeout=timeout) except Exception as e: logging.info('Could not get file from %s: %s', prefix, e) def _get_user_secret(data): secret = [] data = bytearray(data) for i in range(0, len(data), 10): secret.append(sum(data[i:i+10]) & 0xFF) return b64encode(bytearray(secret)).decode() def _get_download_license_info(): licfile = os.path.join(PYARMOR_PATH, 'license.lic') if not os.path.exists(licfile): licfile = os.path.join(HOME_PATH, 'license.lic') logging.debug('Got license data from %s', licfile) with open(licfile, 'rb') as f: licdata = f.read() rcode = decode_license_key(licdata) if not rcode: licfile = os.path.join(PYARMOR_PATH, '.key', 'license.lic') if os.path.exists(licfile): logging.debug('Got seconday license data from %s', licfile) with open(licfile, 'rb') as f: licdata = f.read() rcode = decode_license_key(licdata) if not rcode: raise RuntimeError('This license key may be expired') return rcode, _get_user_secret(licdata) def _get_remote_file(path, timeout=6.0, prefix=None): if is_trial_version(): logging.warning('The trial version could not download ' 'the latest platform library') return _get_old_remote_file(path, timeout=PYARMOR_TIMEOUT) rcode, secret = _get_download_license_info() url = platform_url if prefix is None else prefix url = '/'.join([url.format(version=core_version), path]) logging.info('Getting remote file: %s', url) req = Request(url) auth = b64encode(('%s:%s' % (rcode, secret)).encode()) req.add_header('Authorization', 'Basic ' + auth.decode()) return _urlopen(req, None, timeout) def _get_platform_list(platid=None): filename = os.path.join(CROSS_PLATFORM_PATH, platform_config) logging.debug('Load platform list from %s', filename) cached = os.path.exists(filename) if not cached: res = _get_remote_file(platform_config, timeout=PYARMOR_TIMEOUT) if res is None: raise RuntimeError('No platform list file %s found' % filename) if not os.path.exists(CROSS_PLATFORM_PATH): logging.info('Create platform path: %s' % CROSS_PLATFORM_PATH) os.makedirs(CROSS_PLATFORM_PATH) logging.info('Write cached platform list file %s', filename) with open(filename, 'wb') as f: f.write(res.read()) with open(filename) as f: cfg = json_loads(f.read()) ver = cfg.get('version') if not ver == core_version: if is_trial_version(): logging.warning('The trial version could not download the latest' ' core libraries, tag r41.15a is always used') if _format_platid() in ('windows.x86_64', 'windows.x86', 'linux.x86_64', 'linux.x86', 'darwin.x86_64'): raise RuntimeError('The latest pyarmor could not work with ' 'core library r41.15a, please use ' 'pyarmor < v7.0.0') elif cached: logging.info('Remove cached platform list file %s', filename) os.remove(filename) return _get_platform_list(platid) logging.warning('The core library excepted version is %s, ' 'but got %s from platform list file %s', core_version, ver, filename) return cfg.get('platforms', []) if platid is None \ else [x for x in cfg.get('platforms', []) if (platid is None or (x['id'] == platid) or (x['id'].find(platid + '.') == 0) or (x['path'] == platid))] def get_platform_list(platid=None): return _get_platform_list(platid=platid) def download_pytransform(platid, output=None, url=None, firstonly=False): platid = _format_platid(platid) logging.info('Search library for platform: %s', platid) plist = _get_platform_list(platid=platid) if not plist: logging.error('Unsupport platform %s', platid) raise RuntimeError('No available library for this platform') if firstonly: plist = plist[:1] result = [p['id'] for p in plist] logging.info('Found available libraries: %s', result) if output is None: output = CROSS_PLATFORM_PATH if not os.path.exists(output): logging.info('Create cross platforms path: %s', output) os.makedirs(output) if not os.access(output, os.W_OK): logging.error('Cound not download library file to %s', output) raise RuntimeError('No write permission for target path') for p in plist: libname = p['filename'] path = '/'.join([p['path'], libname]) dest = os.path.join(output, *p['id'].split('.')) logging.info('Target path for %s: %s', p['id'], dest) makedirs(dest, exist_ok=True) logging.info('Downloading library file for %s ...', p['id']) res = _get_remote_file(path, timeout=PYARMOR_TIMEOUT, prefix=url) if res is None: raise RuntimeError('Download library file failed') data = res.read() if hashlib.sha256(data).hexdigest() != p['sha256']: raise RuntimeError('Verify dynamic library failed, try to ' 'reinstall the latest pyarmor and run ' '"pyarmor download -u" to fix it') target = os.path.join(dest, libname) logging.info('Writing target file: %s', target) with open(target, 'wb') as f: f.write(data) logging.info('Download dynamic library %s OK', p['id']) return result def update_pytransform(pattern): platfile = os.path.join(CROSS_PLATFORM_PATH, platform_config) if os.path.exists(platfile): logging.info('Removed cached platform index file %s', platfile) os.remove(platfile) platforms = dict([(p['id'], p) for p in _get_platform_list()]) path = os.path.join(CROSS_PLATFORM_PATH, '*', '*', '*') flist = glob(os.path.join(path, '_pytransform.*')) + \ glob(os.path.join(path, 'py*', 'pytransform.*')) plist = [] n = len(CROSS_PLATFORM_PATH) + 1 for filename in flist: platid = _format_platid(os.path.dirname(filename)[n:]) if not ((pattern == '*') or platid.startswith(pattern)): continue p = platforms.get(platid) if p is None: logging.warning('No %s found in supported platforms', platid) else: with open(filename, 'rb') as f: data = f.read() if hashlib.sha256(data).hexdigest() == p['sha256']: logging.info('The platform %s has been the latest', platid) else: plist.append(p['id']) if not plist: logging.info('Nothing updated') return for platid in plist: download_pytransform(platid) logging.info('Update library successfully') def make_capsule(filename): if os.path.exists(OLD_CAPSULE): logging.info('Move old capsule %s to %s', OLD_CAPSULE, filename) shutil.move(OLD_CAPSULE, filename) return if not is_trial_version(): logging.error('The registered version would use private capsule.' '\n\t Please run `pyarmor register KEYFILE` ' 'to restore your private capsule.') raise RuntimeError('Could not generate private capsule.') public_capsule = os.path.join(PYARMOR_PATH, 'public_capsule.zip') logging.debug('Copy %s to %s', public_capsule, filename) shutil.copy(public_capsule, filename) logging.debug('Generate public capsule %s OK.', filename) def check_capsule(capsule): if os.path.getmtime(capsule) < os.path.getmtime( os.path.join(PYARMOR_PATH, 'license.lic')): logging.info('Capsule %s has been out of date', capsule) suffix = strftime('%Y%m%d%H%M%S', gmtime()) logging.info('Rename it as %s.%s', capsule, suffix) os.rename(capsule, capsule + '.' + suffix) return False return True def _make_entry(filename, rpath=None, relative=None, shell=None, suffix='', advanced=0): pkg = os.path.basename(filename) == '__init__.py' entry_code = entry_lines[0] % ( '.' if (relative is True) or ((relative is None) and pkg) else '', suffix) with open(filename, 'r') as f: lines = f.readlines() # Fix empty file issue n = 0 for n in range(len(lines)): if lines[n].strip() == '' or lines[n].find('__future__') > 0: continue if not lines[n][0] == '#': break for line in lines[n:]: if line.strip() == entry_code.strip(): return with open(filename, 'w') as f: f.write(''.join(lines[:n])) if shell: f.write(shell) f.write(entry_code) paras = [] if rpath is not None: paras.append(repr(rpath)) if suffix: paras.append('suffix=%s' % repr(suffix)) if advanced: paras.append('advanced=1') f.write(entry_lines[1] % ', '.join(paras)) f.write(''.join(lines[n:])) def _get_script_shell(script): with open(script, 'r') as f: try: line = f.read(60) if len(line) > 2 and line[:2] == '#!': i = line.find('\n') + 1 if i > 0: return line[:i] except Exception: pass def make_entry(entris, path, output, rpath=None, relative=None, suffix='', advanced=0): for entry in entris.split(','): entry = entry.strip() filename = build_path(entry, output) src = build_path(entry, path) if os.path.exists(filename): shell = _get_script_shell(src) else: shell = None logging.info('Copy entry script %s to %s', src, relpath(filename)) shutil.copy(src, filename) if shell: logging.info('Insert shell line: %s', shell.strip()) logging.info('Insert bootstrap code to entry script %s', relpath(filename)) _make_entry(filename, rpath, relative=relative, shell=shell, suffix=suffix, advanced=advanced) def obfuscate_scripts(filepairs, mode, capsule, output): makedirs(output, exist_ok=True) prokey = os.path.join(output, 'product.key') if not os.path.exists(prokey): ZipFile(capsule).extract('product.key', path=output) dirs = [] for x in filepairs: dirs.append(os.path.dirname(x[1])) for d in set(dirs): makedirs(d, exist_ok=True) if filepairs: pytransform.encrypt_project_files(prokey, tuple(filepairs), mode) os.remove(prokey) return filepairs def _get_library_filename(platid, checksums=None): if os.path.isabs(platid) or os.path.isfile(platid): if not os.path.exists(platid): raise RuntimeError('No platform library %s found' % platid) return platid xlist = [str(x) for x in platid.split('.')] n = len(xlist) if n < 3: raise RuntimeError('Missing features in platform name %s' % platid) # Always download core libraries # if (xlist[2] == '7') and xlist[1] in ('x86', 'x86_64') and \ # xlist[0] in ('windows', 'darwin', 'linux'): # path = os.path.join(PLATFORM_PATH, *xlist[:2]) # names = [x for x in os.listdir(path) if x.startswith('_pytransform.')] # if names: # return os.path.join(path, names[0]) names = None path = os.path.join(CROSS_PLATFORM_PATH, *xlist) if os.path.exists(path): names = [x for x in os.listdir(path) if x.find('pytransform.') > -1] if len(names) > 1: raise RuntimeError('Invalid platform data, there is more than ' '1 file in the path %s', path) if not names: download_pytransform(platid) return _get_library_filename(platid, checksums) filename = os.path.join(path, names[0]) if checksums is not None and platid in checksums: with open(filename, 'rb') as f: data = f.read() if hashlib.sha256(data).hexdigest() != checksums[platid]: if hasattr(sys, '_debug_pyarmor'): logging.warning('Found library %s for platform %s, but it does' ' not match this pyarmor', filename, platid) return filename logging.info('The platform %s is out of date', platid) download_pytransform(platid) return _get_library_filename(platid, checksums) return filename def _build_platforms(platforms): results = [] checksums = dict([(p['id'], p['sha256']) for p in _get_platform_list()]) n = len(platforms) if not os.path.exists(CROSS_PLATFORM_PATH): logging.info('Create cross platforms path: %s', CROSS_PLATFORM_PATH) os.makedirs(CROSS_PLATFORM_PATH) for platid in platforms: if (n > 1) and (os.path.isabs(platid) or os.path.isfile(platid)): raise RuntimeError('Invalid platform `%s`, for multiple platforms ' 'it must be `platform.machine`' % platid) if (n > 1) and platid.startswith('vs2015.'): raise RuntimeError('The platform `%s` does not work ' 'in multiple platforms target' % platid) filename = _get_library_filename(platid, checksums) results.append(filename) logging.debug('Target dynamic library: %s', results) return results def _build_license_file(capsule, licfile, output=None): if licfile is None: myzip = ZipFile(capsule, 'r') try: if 'default.lic2' in myzip.namelist(): logging.info('Read default license from capsule') lickey = myzip.read('default.lic2') else: logging.info('Generate default license file') lickey = make_license_key(capsule, '*CODE:PyArmor-Project') logging.info('Update capsule to add default license file') with ZipFile(capsule, 'a') as f: f.writestr('default.lic2', lickey) finally: myzip.close() elif licfile == 'no-restrict': logging.info('Generate no restrict mode license file') licode = '*FLAGS:%c*CODE:PyArmor-Project' % chr(1) lickey = make_license_key(capsule, licode) elif licfile in ('no', 'outer'): logging.info('Use outer license file') lickey = b'' else: logging.info('Generate license file from %s', relpath(licfile)) with open(licfile, 'rb') as f: lickey = f.read() if output is not None and lickey: logging.info('Write license file: %s', output) with open(output, 'wb') as f: f.write(lickey) return lickey def make_runtime(capsule, output, licfile=None, platforms=None, package=False, suffix='', supermode=False): if supermode: return _make_super_runtime(capsule, output, platforms, licfile=licfile, suffix=suffix) if package: output = os.path.join(output, 'pytransform' + suffix) makedirs(output, exist_ok=True) logging.info('Generating runtime files to %s', relpath(output)) checklist = [] keylist = _build_keylist(capsule, licfile) def copy3(src, dst, onlycopy=False): x = os.path.basename(src) if suffix: x = x.replace('.', ''.join([suffix, '.'])) logging.info('Rename it to %s', x) target = os.path.join(dst, x) shutil.copy2(src, target) if onlycopy: return logging.info('Patch library %s', target) data = _patch_extension(target, keylist, suffix, supermode=False) checklist.append(sum(bytearray(data))) if not platforms: libfile = pytransform._pytransform._name if not os.path.exists(libfile): libname = dll_name + dll_ext libfile = os.path.join(PYARMOR_PATH, libname) if not os.path.exists(libfile): pname = pytransform.format_platform() libpath = os.path.join(PYARMOR_PATH, 'platforms') libfile = os.path.join(libpath, pname, libname) logging.info('Copying %s', libfile) copy3(libfile, output) elif len(platforms) == 1: filename = _build_platforms(platforms)[0] logging.info('Copying %s', filename) copy3(filename, output) elif osx_is_universal_platforms(platforms): filelist = _build_platforms(platforms) targets = [os.path.join(output, a + '.' + os.path.basename(b)) for a, b in zip(platforms, filelist)] for target, filename in zip(targets, filelist): shutil.copy2(filename, target) logging.info('Patch extension %s', target) data = _patch_extension(target, keylist, suffix) checklist.append(sum(bytearray(data))) name = _format_extension_name(filelist[0]) if suffix: name = name.replace('.', ''.join([suffix, '.'])) dest = os.path.join(output, name) logging.info('Generate universal binary %s', dest) osx_merge_binary(dest, *targets) [os.remove(x) for x in targets] else: libpath = os.path.join(output, pytransform.plat_path) logging.info('Create library path to support multiple platforms: %s', libpath) if not os.path.exists(libpath): os.mkdir(libpath) filenames = _build_platforms(platforms) for platid, filename in list(zip(platforms, filenames)): logging.info('Copying %s', filename) path = os.path.join(libpath, *platid.split('.')[:2]) logging.info('To %s', path) makedirs(path, exist_ok=True) copy3(filename, path) filename = os.path.join(PYARMOR_PATH, 'pytransform.py') if package: logging.info('Copying %s', filename) logging.info('Rename it to %s/__init__.py', os.path.basename(output)) shutil.copy2(filename, os.path.join(output, '__init__.py')) else: logging.info('Copying %s', filename) copy3(filename, output, onlycopy=True) logging.info('Generate runtime files OK') return checklist def copy_runtime(path, output, licfile=None, dryrun=False): logging.info('Copying runtime files from %s', path) logging.info('To %s', output) makedirs(output, exist_ok=True) def copy3(src, dst): if dryrun: return if os.path.isdir(src): if os.path.exists(dst): logging.info('Remove old path %s', dst) shutil.rmtree(dst) logging.info('Copying directory %s', os.path.basename(src)) shutil.copytree(src, dst) else: logging.info('Copying file %s', os.path.basename(src)) shutil.copy2(src, dst) name = None tlist = [] for x in os.listdir(path): root, ext = os.path.splitext(x) if root in ('pytransform_protection', 'pytransform_bootstrap'): continue src = os.path.join(path, x) dst = os.path.join(output, x) if x.startswith('pytransform'): copy3(src, dst) name = x tlist.append(ext) elif x.startswith('_pytransform') or x == 'platforms': copy3(src, dst) if name is None: raise RuntimeError('No module "pytransform" found in runtime package') if (('' in tlist or '.py' in tlist) and len(tlist) > 1): raise RuntimeError('Multiple runtime modules found') if licfile and not dryrun: if not os.path.exists(licfile): raise RuntimeError('No found license file "%s"' % licfile) logging.info('Copying outer license %s', licfile) dst = os.path.join(output, '' if name.find('.') > 0 else name) logging.info('To %s/license.lic', dst) shutil.copy2(licfile, os.path.join(dst, 'license.lic')) def make_license_key(capsule, code, output=None, key=None, legency=0): prikey = ZipFile(capsule, 'r').read('private.key') \ if key is None else key size = len(prikey) if not legency else -len(prikey) lickey = pytransform.generate_license_key(prikey, size, code) if output is None: return lickey elif output in ('stdout', 'stderr'): getattr(sys, output).write( lickey.decode() if hasattr(lickey, 'decode') else lickey) else: with open(output, 'wb') as f: f.write(lickey) def show_hd_info(name=None): if name is None: pytransform.show_hd_info() else: t, sep = (0, ':') if name.startswith('/') else (1, '/') info = pytransform.get_hd_info(t, name) print('Query hardware information: "%s%s%s"' % (name, sep, info)) def build_path(path, start): return path if os.path.isabs(path) else os.path.join(start, path) def make_project_command(platform, python, pyarmor, output): script = os.path.abspath(pyarmor) if platform.startswith('win'): filename = os.path.join(output, 'pyarmor.bat') with open(filename, 'w') as f: f.write('%s %s %%*' % (python, script)) else: filename = os.path.join(output, 'pyarmor') with open(filename, 'w') as f: f.write('%s %s "$@"' % (python, script)) os.chmod(filename, 0o755) return filename def is_trial_version(): licfile = os.path.join(HOME_PATH, 'license.lic') if not os.path.exists(licfile): return True with open(licfile, 'rb') as f: return len(f.read()) == 256 def decode_license_key(data): if len(data) == 256: return data = b64decode(data) i = data.find(b'pyarmor-vax-') if i > -1: return data[i:i+18].decode() def get_registration_code(): try: code = pytransform.get_license_info()['CODE'] except Exception: code = None return code def search_plugins(plugins): if plugins: result = [] for name in plugins: if name == 'on': logging.info('Enable inline plugin') result.append(['', '', 0]) continue i = 1 if name[0] == '@' else 0 filename = name[i:] + ('' if name.endswith('.py') else '.py') key = os.path.basename(name[i:]) if not os.path.exists(filename): if os.path.isabs(filename): raise RuntimeError('No script found for plugin %s' % name) for path in [os.path.join(x, 'plugins') for x in (HOME_PATH, PYARMOR_PATH)]: testname = build_path(filename, path) if os.path.exists(testname): filename = testname break else: raise RuntimeError('No script found for plugin %s' % name) logging.info('Found plugin %s at: %s', key, filename) result.append([key, filename, not i]) return result def _patch_plugins(plugins): result = [] for key, filename, x in plugins: if x: logging.info('Apply plugin %s', key) lines = _readlines(filename) result.append(''.join(lines)) return ['\n'.join(result)] def _filter_call_marker(plugins, name): for plugin in plugins: if plugin[0] == name: plugin[-1] = True return True def _build_source_keylist(source, code, closure): result = [] flist = ('dllmethod', 'init_pytransform', 'init_runtime', '_load_library', 'get_registration_code', 'get_expired_days', 'get_hd_info', 'get_license_info', 'get_license_code', 'format_platform', 'pyarmor_init', 'pyarmor_runtime', 'assert_armored') def _make_value(co): return len(co.co_names), len(co.co_consts), len(co.co_code) def _make_code_key(co): v1 = _make_value(co) v2 = _make_value(co.co_consts[1]) if co.co_name == 'dllmethod' else None co_closure = getattr(co, closure, None) v3 = _make_value(getattr(co_closure[0].cell_contents, code)) \ if co_closure else None return v1, v2, v3 mod_co = compile(source, 'pytransform', 'exec') result.append((-1, _make_code_key(mod_co))) mod_consts = mod_co.co_consts for i in range(len(mod_consts)): co_const = mod_consts[i] co = getattr(co_const, code, None) if co and co.co_name in flist: result.append((i, _make_code_key(co))) return result def _build_pytransform_keylist(mod, code, closure): result = [] flist = ('dllmethod', 'init_pytransform', 'init_runtime', '_load_library', 'get_registration_code', 'get_expired_days', 'get_hd_info', 'get_license_info', 'get_license_code', 'format_platform', 'pyarmor_init', 'pyarmor_runtime', '_match_features') def _make_value(co): return len(co.co_names), len(co.co_consts), len(co.co_code) def _make_code_key(co): v1 = _make_value(co) v2 = _make_value(co.co_consts[1]) if co.co_name == 'dllmethod'else None co_closure = getattr(co, closure, None) v3 = _make_value(getattr(co_closure[0].cell_contents, code)) \ if co_closure else None return v1, v2, v3 for name in flist: co = getattr(getattr(mod, name), code) result.append((name, _make_code_key(co))) return result def _get_checksum(filename): size = os.path.getsize(filename) & 0xFFFFFFF0 n = size >> 2 with open(filename, 'rb') as f: buf = f.read(size) fmt = 'I' * n return sum(struct.unpack(fmt, buf)) & 0xFFFFFFFF def _make_protection_code(relative, checksums, suffix='', multiple=False): template = os.path.join(PYARMOR_PATH, protect_code_template % '') with open(template) as f: buf = f.read() code = '__code__' if sys.version_info[0] == 3 else 'func_code' closure = '__closure__' if sys.version_info[0] == 3 else 'func_closure' keylist = _build_pytransform_keylist(pytransform, code, closure) rpath = '{0}.os.path.dirname({0}.__file__)'.format('pytransform') spath = '{0}.os.path.join({0}.plat_path, {0}.format_platform())'.format( 'pytransform') if multiple else repr('') return buf.format(code=code, closure=closure, rpath=rpath, spath=spath, checksum=str(checksums), keylist=keylist, suffix=suffix, relative='from . ' if relative else '') def _frozen_modname(filename, filename2): names = os.path.normpath(filename).split(os.sep) names2 = os.path.normpath(filename2).split(os.sep) k = -1 while True: try: if names[k] != names2[k]: break except IndexError: break k -= 1 if names[-1] == '__init__.py': dotnames = names[k if k == -2 else k + 1:-1] else: names[-1] = names[-1][:-3] dotnames = names[k+1:] return "" % '.'.join(dotnames) def _guess_encoding(filename): with open(filename, 'rb') as f: line = f.read(80) if line and line[:3] == BOM_UTF8: return 'utf-8' if line and line[0] == 35: n = line.find(b'\n') m = re.search(r'coding[=:]\s*([-\w.]+)', line[:n].decode()) if m: return m.group(1) if n > -1 and len(line) > (n+1) and line[n+1] == 35: k = n + 1 n = line.find(b'\n', k) m = re.search(r'coding[=:]\s*([-\w.]+)', line[k:n].decode()) return m and m.group(1) def _readlines(filename): if sys.version_info[0] == 2: with open(filename, 'r') as f: lines = f.readlines() else: encoding = _guess_encoding(filename) try: with open(filename, 'r', encoding=encoding) as f: lines = f.readlines() except UnicodeDecodeError: encoding = 'utf-8' with open(filename, 'r', encoding=encoding) as f: lines = f.readlines() # Try to remove any UTF BOM bytes if encoding == 'utf-8' and lines: i = 0 for c in lines[0]: if ord(c) < 128: break i += 1 if i: lines[0] = lines[0][i:] return lines def encrypt_script(pubkey, filename, destname, wrap_mode=1, obf_code=1, obf_mod=1, adv_mode=0, rest_mode=1, entry=0, protection=0, platforms=None, plugins=None, rpath=None, suffix='', sppmode=False): lines = _readlines(filename) if plugins: n = 0 k = -1 plist = [] stub_marker = '# {PyArmor Plugins}' inline_marker = '# PyArmor Plugin: ' call_markers = '# pyarmor_', '# @pyarmor_' for line in lines: if line.startswith(stub_marker): k = n + 1 else: i = line.find(inline_marker) if i > -1: plist.append((n if k == -1 else n+1, i, inline_marker)) else: for marker in call_markers: i = line.find(marker) if i == -1: continue name = line[i+len(marker):line.find('(')].strip() if _filter_call_marker(plugins, name): plist.append((n if k == -1 else n+1, i, marker)) n += 1 if k > -1: logging.info('Patch this script with plugins') lines[k:k] = _patch_plugins(plugins) for n, i, m in plist: c = '@' if m[2] == '@' else '' lines[n] = lines[n][:i] + c + lines[n][i+len(m):] if protection: n = 0 for line in lines: if line.startswith('# No PyArmor Protection Code') or \ line.startswith('# {No PyArmor Protection Code}'): break elif (line.startswith('# {PyArmor Protection Code}') or line.startswith("if __name__ == '__main__':") or line.startswith('if __name__ == "__main__":')): logging.info('Patch this entry script with protection code') if os.path.exists(protection): logging.info('Use template: %s', protection) with open(protection) as f: lines[n:n] = [f.read()] else: lines[n:n] = [protection] break n += 1 if hasattr(sys, '_debug_pyarmor') and (protection or plugins): patched_script = filename + '.pyarmor-patched' logging.info('Write patched script for debugging: %s', patched_script) with open(patched_script, 'w') as f: f.write(''.join(lines)) modname = _frozen_modname(filename, destname) if sppmode: if sys.version_info[0] * 100 + sys.version_info[1] < 307: raise RuntimeError('This Python version is not supported by spp ' 'mode, only Python 3.7+ works') co = sppbuild(''.join(lines), modname, destname) if not co: logging.info('Ignore this module because of %s', 'sppmode inline option' if co is False else 'no any function available for sppmode') sppmode = False co = compile(''.join(lines), modname, 'exec') else: co = compile(''.join(lines), modname, 'exec') if (adv_mode & 0x7) > 1 and sys.version_info[0] > 2 and not sppmode: co = _check_code_object_for_super_mode(co, lines, modname) if rest_mode > 100: if sum(sys.version_info[:2]) < 10: raise RuntimeError('This Python version is not supported by ' 'restrict mode %s, it only works ' 'for Python 3.7 and later' % rest_mode) rest_mode -= 100 rest_mod_dict_flag = 2 else: rest_mod_dict_flag = 0 flags = obf_code | obf_mod << 8 | (wrap_mode | (adv_mode << 4)) << 16 | \ ((0xB4 if rest_mode == 5 else 0xB0 if rest_mode == 4 else 0xF0 if rest_mode == 3 else 0x70 if rest_mode == 2 else 0x10 if rest_mode else 0) | (8 if entry else 0) | rest_mod_dict_flag) << 24 s = pytransform.encrypt_code_object(pubkey, co, flags, suffix=suffix) with open(destname, 'w') as f: f.write(sppmixin(s.decode()) if sppmode else s.decode()) def get_product_key(capsule): return ZipFile(capsule).read('product.key') def upgrade_capsule(capsule): myzip = ZipFile(capsule, 'r') try: if 'pytransform.key' in myzip.namelist(): logging.info('The capsule is latest, nothing to do') return logging.info('Read product key from old capsule') pubkey = myzip.read('product.key') finally: myzip.close() myzip = ZipFile(capsule, 'a') try: logging.info('Generate new key') licfile = os.path.join(PYARMOR_PATH, 'license.lic') _, newkey = pytransform._generate_pytransform_key(licfile, pubkey) logging.info('Write new key pytransform.key to the capsule') myzip.writestr('pytransform.key', newkey) finally: myzip.close() logging.info('Upgrade capsule OK.') def load_config(filename): if os.path.exists(filename): with open(filename, 'r') as f: cfg = json_loads(f.read()) else: cfg = {} return cfg def save_config(cfg, filename=None): s = json_dumps(cfg, indent=2) with open(filename, 'w') as f: f.write(s) def relpath(path, start=os.curdir): try: r = os.path.relpath(path, start) return path if r.count('..') > 1 else r except Exception: return path def _reboot_pytransform(platid): os.putenv('PYARMOR_PLATFORM', platid) if sys.platform == 'win32' and sys.argv[0].endswith('pyarmor'): p = Popen(sys.argv) else: p = Popen([sys.executable] + sys.argv) p.wait() return p.returncode def _get_preferred_platid(platname, features=None): if os.path.isabs(platname) or os.path.isfile(platname): return platname nlist = platname.split('.') name = '.'.join(nlist[:2]) if name in ('linux.arm', 'linux.ppc64', 'linux.mips64', 'linux.mips64el', 'musl.arm', 'musl.mips32', 'freebsd.x86_64', 'android.aarch64', 'android.x86', 'android.x86_64', 'poky.x86', 'vs2015.x86_64', 'vs2015.x86'): if features and '0' not in features: raise RuntimeError('No feature %s for platform %s', features, name) features = ['0'] elif len(nlist) > 2: if features and nlist[2] not in features: raise RuntimeError('Feature conflicts for platname %s', name) features = nlist[2:3] elif features is None: features = ['7', '3'] if pytransform.version_info()[-1] else ['0'] pyver = None if '8' in features or '11' in features or '25' in features: pyver = 'py%d%d' % sys.version_info[:2] plist = [x['id'] for x in _get_platform_list() if x['name'] == name] for platid in plist: ns = [str(x) for x in platid.split('.')] if (features is None or str(ns[2]) in features) \ and (pyver is None or pyver in ns[3:]): return platid def check_cross_platform(platforms, supermode=False, vmode=False): if not platforms: platforms = [] fn1 = pytransform.version_info()[2] features = None if vmode: features = ['25' if supermode else '21'] if sys.platform not in ('win32',): raise RuntimeError('VM Protect mode only works for Windows') for platid in platforms: if not platid.startswith('windows'): raise RuntimeError('VM Protect mode only works for Windows') nlist = platid.split('.') if len(nlist) > 2 and features[0] not in nlist: raise RuntimeError('Invalid platform name "%s" for VM mode' % platid) if not len(platforms): platforms = [_format_platid()] elif supermode: features = ['11' if (fn1 & FEATURE_JIT) else '8'] if not len(platforms): v = 'py%d%d' % sys.version_info[:2] platforms = ['.'.join([_format_platid(), features[0], v])] result = [] for name in platforms: platid = _get_preferred_platid(name, features=features) if platid is None: msg = 'default' if features is None else features raise RuntimeError('No available dynamic library for %s ' 'with features %s' % (name, msg)) result.append(platid) reboot = None if result and not (os.path.isabs(result[0]) or os.path.isfile(result[0])): platid = result[0] nlist = platid.split('.') fn2 = int(nlist[2]) if fn2 in (21, 25): n = 21 elif fn2 in (0, 8): n = 0 else: n = 7 if (n != fn1) and not (n & fn1 & 0x12): if n == 7 and _format_platid().split('.')[1] in ( 'armv6', 'armv7', 'aarch32', 'aarch64'): n = 3 reboot = '.'.join([_format_platid(), str(n)]) os.environ['PYARMOR_PLATFORM'] = reboot logging.info('Update target platforms to: %s', result) for p in result[1:]: fn3 = int(p.split('.')[2]) if (n != fn3) and not (n & fn3): raise RuntimeError('Multi platforms conflict, platform %s' ' could not mixed with %s' % (p, platid)) if reboot: logging.info('====================================================') logging.info('Reload PyArmor with platform: %s', reboot) logging.info('====================================================') pytransform_bootstrap(force=True) # _reboot_pytransform(reboot) # return False return result def compatible_platform_names(platforms): '''Only for compatibility, it may be removed in next major version.''' if not platforms: return platforms old_forms = { 'armv5': 'linux.arm', 'ppc64le': 'linux.ppc64', 'ios.arm64': 'ios.aarch64', 'darwin.arm64': 'darwin.aarch64', 'freebsd': 'freebsd.x86_64', 'alpine': 'musl.x86_64', 'alpine.arm': 'musl.arm', 'alpine.x86_64': 'musl.x86_64', 'poky-i586': 'poky.x86', } result = [] for names in platforms: for name in names.split(','): name = name.strip() if name in old_forms: logging.warning( 'This platform name `%s` has been deprecated, ' 'use `%s` instead. Display all standard platform ' 'names by `pyarmor download --help-platform`', name, old_forms[name]) result.append(old_forms[name]) else: result.append(name) return result def make_bootstrap_script(output, capsule=None, relative=None, suffix=''): filename = os.path.basename(output) co = compile('', filename, 'exec') flags = 0x18000000 prokey = get_product_key(capsule) buf = pytransform.encrypt_code_object(prokey, co, flags, suffix=suffix) with open(output, 'w') as f: f.write(buf.decode()) _make_entry(output, relative=relative, suffix=suffix) def get_name_suffix(): rcode = get_registration_code() if not rcode: return '' m, n = rcode.replace('-sn-1.txt', '').split('-')[-2:] d = { 'vax': 'vax', 'clickbank': 'vac', 'shareit': 'vas', 'regnow': 'var', 'Pyarmor': 'vad', } if len(n) > 6: n = n[-6:] pad = '0' * (6 - len(n)) return '_'.join(['', d.get(m, 'unk'), pad + n]) def get_bind_key(filename): if not os.path.exists(filename): raise RuntimeError('Bind file %s not found' % filename) with open(filename, 'rb') as f: buf = f.read() size = len(buf) >> 2 fmt = 'I' * size return sum(struct.unpack(fmt, buf[:size*4])) def make_super_bootstrap(source, filename, output, relative=None, suffix=''): pkg = os.path.basename(filename) == '__init__.py' level = '' if (relative is True) or ((relative is None) and pkg): n = len(filename[len(output)+1:].replace('\\', '/').split('/')) level = '.' * n bootstrap = 'from %spytransform%s import pyarmor\n' % (level, suffix) with open(filename, 'r') as f: lines = f.readlines() for line in lines: if line.startswith(bootstrap): return lines.insert(0, bootstrap) shell = _get_script_shell(source) if shell: lines.insert(0, shell) with open(filename, 'w') as f: f.write(''.join(lines)) def _patch_extension(filename, keylist, suffix='', supermode=True): logging.debug('Patching %s', relpath(filename)) patkey = b'\x60\x70\x00\x0f' patlen = len(patkey) sizelist = [len(x) for x in keylist] big_endian = False def write_integer(data, offset, value): if big_endian: offset += 3 step = -1 else: step = 1 for i in range(4): data[offset] = value & 0xFF offset += step value >>= 8 with open(filename, 'rb') as f: data = bytearray(f.read()) n = len(data) for i in range(n): if data[i:i+patlen] == patkey: fmt = 'I' * 8 header = struct.unpack(fmt, bytes(data[i:i+32])) if sum(header[2:]) not in (912, 1452): continue logging.debug('Found pattern at %x', i) max_size = header[1] if sum(sizelist) > max_size: raise RuntimeError('Too much license data') break else: # Maybe big endian patkey = b'\x0f\x00\x70\x60' for i in range(n): if data[i:i+patlen] == patkey: fmt = 'I' * 8 header = struct.unpack('>' + fmt, bytes(data[i:i+32])) if sum(header[2:]) not in (912, 1452): continue logging.debug('Found pattern at %x', i) max_size = header[1] if sum(sizelist) > max_size: raise RuntimeError('Too much license data') big_endian = True break else: raise RuntimeError('Invalid extension, no data found') write_integer(data, i + 12, sizelist[0]) write_integer(data, i + 16, sizelist[0]) write_integer(data, i + 20, sizelist[1]) write_integer(data, i + 24, sizelist[0] + sizelist[1]) write_integer(data, i + 28, sizelist[2]) offset = i + 32 for j in range(3): size = sizelist[j] if size: logging.debug('Patch %d bytes from %x', size, offset) data[offset:offset+size] = keylist[j] offset += size if suffix: marker = bytes(b'_vax_000000') k = len(marker) for i in range(n): if data[i:i+k] == marker: logging.debug('Found marker at %x', i) data[i:i+k] = bytes(suffix.encode()) if supermode and data[0] == 0x7f and data[1:4] == b'ELF': if not _fix_up_gnu_hash(data, suffix): raise RuntimeError('Failed to add symbol suffix for library %s' % filename) with open(filename, 'wb') as f: f.write(data) sign_binary(filename) logging.info('Patch library file OK') return data def _build_keylist(capsule, licfile): myzip = ZipFile(capsule, 'r') if 'pytransform.key' not in myzip.namelist(): raise RuntimeError('No pytransform.key found in capsule') logging.info('Extract pytransform.key') keydata = myzip.read('pytransform.key') myzip.close() lickey = _build_license_file(capsule, licfile) if sys.version_info[0] == 2: size1 = ord(keydata[0]) + ord(keydata[1]) * 256 size2 = ord(keydata[2]) + ord(keydata[3]) * 256 else: size1 = keydata[0] + keydata[1] * 256 size2 = keydata[2] + keydata[3] * 256 k1 = 16 k2 = k1 + size1 return keydata[k1:k2], keydata[k2:k2+size2], lickey def _format_extension_name(filename): plist = os.path.basename(filename).split('.') return '%s.%s' % (plist[0], plist[-1]) def _make_super_runtime(capsule, output, platforms, licfile=None, suffix=''): logging.info('Generating super runtime library to "%s"', relpath(output)) makedirs(output, exist_ok=True) if not platforms: raise RuntimeError('No platform specified in Super mode') elif len(platforms) == 1: filelist = _build_platforms(platforms)[:1] else: filelist = _build_platforms(platforms) keylist = _build_keylist(capsule, licfile) namelist = [] for filename in filelist: name = _format_extension_name(filename) if name in namelist: return _package_super_runtime(output, platforms, filelist, keylist, suffix) namelist.append(name) checklist = [] for filename in filelist: logging.info('Copying %s', filename) name = _format_extension_name(filename) if suffix: k = name.rfind('pytransform') + len('pytransform') name = name[:k] + suffix + name[k:] logging.info('Rename extension to %s', name) target = os.path.join(output, name) shutil.copy2(filename, target) logging.info('Patch extension %s', target) data = _patch_extension(target, keylist, suffix) checklist.append(sum(bytearray(data))) logging.info('Generate runtime files OK') return checklist def _package_super_runtime(output, platforms, filelist, keylist, suffix): if osx_is_universal_platforms(platforms): checklist = [] targets = [os.path.join(output, a + '.' + os.path.basename(b)) for a, b in zip(platforms, filelist)] for target, filename in zip(targets, filelist): shutil.copy2(filename, target) logging.info('Patch extension %s', target) data = _patch_extension(target, keylist, suffix) checklist.append(sum(bytearray(data))) name = _format_extension_name(filelist[0]) if suffix: name = name.replace('.', ''.join([suffix, '.'])) dest = os.path.join(output, name) logging.info('Generate universal binary %s', dest) osx_merge_binary(dest, *targets) [os.remove(x) for x in targets] logging.info('Generate super runtime package OK') return checklist output = os.path.join(output, 'pytransform' + suffix) logging.info('Make package path %s', os.path.basename(output)) makedirs(output, exist_ok=True) src = os.path.join(PYARMOR_PATH, 'helper', 'superuntime.py') dst = os.path.join(output, '__init__.py') logging.info('Copying %s', src) logging.info('To %s', dst) shutil.copy2(src, dst) checklist = [] for platname, filename in zip(platforms, filelist): logging.info('Copying %s', filename) if os.path.isfile(platname): raise RuntimeError('Unknown standard platform "%s"' % platname) path = '_'.join(platname.split('.')[:2]) name = _format_extension_name(filename) target = os.path.join(output, path, name) makedirs(os.path.dirname(target), exist_ok=True) shutil.copy2(filename, target) logging.info('Patch extension %s', target) data = _patch_extension(target, keylist, suffix) checklist.append(sum(bytearray(data))) logging.info('Generate super runtime package OK') return checklist def _make_protection_code2(relative, checklist, suffix=''): template = os.path.join(PYARMOR_PATH, protect_code_template % '2') logging.info('Use protection template: %s', relpath(template)) with open(template) as f: buf = f.read() return buf.format(relative='from . ' if relative else '', checklist=checklist, suffix=suffix) def make_protection_code(args, multiple=False, supermode=False): return _make_protection_code2(*args) if supermode \ else _make_protection_code(*args, multiple=multiple) def _check_code_object_for_super_mode(co, lines, name): from dis import hasjabs, hasjrel, get_instructions HEADER_SIZE = 8 hasjins = hasjabs + hasjrel def is_special_code_object(co): has_special_jabs = False has_header_label = True if co.co_code[6:7] == b'\x90' else False for ins in get_instructions(co): if ins.opcode in hasjabs and \ (ins.arg & ~0xF) in (0xF0, 0xFFF0, 0xFFFFF0): has_special_jabs = True if has_header_label: if has_special_jabs: return True continue if ins.offset < HEADER_SIZE: if ins.is_jump_target or ins.opcode in hasjins: has_header_label = True elif not has_header_label: break def check_code_object(co): co_list = [co] if is_special_code_object(co) else [] for obj in [x for x in co.co_consts if hasattr(x, 'co_code')]: co_list.extend(check_code_object(obj)) return co_list co_list = check_code_object(co) if co_list: pat = re.compile(r'^\s*') for c in co_list: # In some cases, co_lnotab[1] is not the first statement i = c.co_firstlineno - 1 k = i + c.co_lnotab[1] while i < k: s = lines[i].strip() j = s.find('#') if j > 0 and s[j:].find('"') == -1 and s[j:].find("'") == -1: s = s[:j].strip() if s.endswith('):') or (s.endswith(':') and s.find('->') > -1): break i += 1 else: logging.error('Function does not end with "):"') raise RuntimeError('Patch function "%s" failed' % c.co_name) i += 1 docs = c.co_consts[0] n_docs = len(docs.splitlines()) if isinstance(docs, str) else 0 while i < k: if lines[i].strip(): if n_docs: i += n_docs n_docs = 0 continue break i += 1 logging.info('\tPatch function "%s" at line %s', c.co_name, i + 1) s = lines[i] indent = pat.match(s).group(0) lines[i] = '%s[None, None]\n%s' % (indent, s) co = compile(''.join(lines), name, 'exec') return co def _urlopen(*args, **kwargs): try: return urlopen(*args, **kwargs) except Exception: from ssl import _create_unverified_context kwargs['context'] = _create_unverified_context() return urlopen(*args, **kwargs) def makedirs(path, exist_ok=False): if not (exist_ok and os.path.exists(path)): os.makedirs(path) def _fix_up_gnu_hash(data, suffix): n = 0x200 fmt = 'I' * n arr = struct.unpack(fmt, bytes(data[:n*4])) # ix, kx, key, prefix = (0, 2, 0xb4270e0b, 'PyInit_') \ ix, kx, key, prefix = (0, 0, 0xb4239787, 'PyInit_') \ if sys.version_info[0] == 3 else (2, 0, 0xe746a6ab, 'init') symhash = 5381 for c in ''.join([prefix, 'pytransform', suffix]): symhash = symhash * 33 + ord(c) symhash &= 0xffffffff nx = symhash % 3 i = 0 def write_integer(buf, offset, value): for j in range(offset, offset + 4): buf[j] = value & 0xFF value >>= 8 while True: try: i = arr.index(key, i) except Exception: return k = i + kx if (arr[k-12] == 3 and arr[k-10] == 1 and arr[k-9] == 6) \ or (arr[k-11] == 3 and arr[k-9] == 1 and arr[k-8] == 5): logging.debug('Fix suffix symbol hash at %s', k) write_integer(data, (k if ix else (k-3))*4, symhash) write_integer(data, (k-6+nx)*4, arr[k-6+ix]) write_integer(data, (k-7)*4, 0xffffffff) if arr[k-9] == 6: write_integer(data, (k-8)*4, 0xffffffff) return True i += 1 def is_pyscript(filename): return os.path.splitext(filename)[-1].lower() in ('.py', '.pyw') def exclude_functions(names=''): if pytransform._pytransform.set_option(7, names.encode()) == -1: raise RuntimeError('Excluding functions is not supported by this ' 'version, please upgrade pyarmor to the latest') def get_sppmode_files(timeout=None): licfile = os.path.join(HOME_PATH, 'license.lic') sppver = sppmode_info['version'] spplatforms = sppmode_info['platforms'] platpath = pytransform.format_platform().replace('\\', '/') platid = platpath.replace('/', '.') if platid not in spplatforms.keys(): raise RuntimeError('sppmode does NOT work in platform "%s"' % platid) ext = '.dll' if platid.startswith('win') else '.so' libname = os.path.join(HOME_PATH, 'sppmode' + ext) vername = os.path.join(HOME_PATH, '.sppver') if os.path.exists(vername) and os.path.exists(libname): with open(vername) as f: libver = f.readline().strip() else: libver = '' if libver != sppver: if is_trial_version(): raise RuntimeError('sppmode is not available in the trial version') rcode, secret = _get_download_license_info() url = platform_url.format(version=sppver) url = '/'.join([url, 'spp', platid, os.path.basename(libname)]) logging.info('Getting remote file: %s', url) timeout = PYARMOR_TIMEOUT if timeout is None else timeout req = Request(url) auth = b64encode(('%s:%s' % (rcode, secret)).encode()) req.add_header('Authorization', 'Basic ' + auth.decode()) res = _urlopen(req, None, timeout) logging.info('Downloading sppmode library for "%s" ...', platid) if res is None: raise RuntimeError('Download sppmode library failed') data = res.read() if hashlib.sha256(data).hexdigest() != spplatforms[platid]: raise RuntimeError('Incomplete sppmode library is downloaded') logging.info('Writing target file: %s', libname) with open(libname, 'wb') as f: f.write(data) logging.info('Writing version file: %s', vername) with open(vername, 'w') as f: f.write(sppver) logging.info('Download sppmode library "%s" OK', platid) return libname, licfile def sign_binary(filename): if not sys.platform.startswith('darwin'): return # Maybe cross platform output = check_output(['file', filename]) if output.find(b' Mach-O ') == -1: return logging.info("Signing file %s", filename) identity = '-' cmdlist = ['codesign', '-s', identity, '--force', '--all-architectures', '--timestamp', filename] p = Popen(cmdlist, stdout=PIPE, stderr=PIPE) stdout, stderr = p.communicate() if p.returncode != 0: logging.warning("codesign command (%r) failed with error code %d!\n" "stdout: %r\n" "stderr: %r", cmdlist, p.returncode, stdout, stderr) raise SystemError("codesign failure!") def osx_is_universal_platforms(platforms): platforms = ['.'.join(name.split('.')[:2]) for name in platforms] return set(platforms) == set(['darwin.x86_64', 'darwin.aarch64']) def osx_merge_binary(target, *filelist): cmdlist = ['lipo', '-create', '-output', target] for filename in filelist: arch = os.path.basename(filename).split('.')[1] if arch == 'aarch64': arch = 'arm64' cmdlist.extend(['-arch', arch, filename]) logging.debug('Call lipo: %s' % ' '.join(cmdlist)) p = Popen(cmdlist, stdout=PIPE, stderr=PIPE) stdout, stderr = p.communicate() if p.returncode != 0: logging.warning("lipo command (%r) failed with error code %d!\n" "stdout: %r\n" "stderr: %r", cmdlist, p.returncode, stdout, stderr) raise SystemError("merge binary failure!")