關於Pyinstaller打包eel和pygame需要註意的坑第1/3頁
這幾天我們團隊用python做瞭一個遊戲,引擎用的pygame,UI界面用的eel(一個python庫,用於利用HTML開發桌面應用程序,主要是因為QT機制太過復雜,而博主Deadpool不願做費力不討好的事,import一個eel,便可通過HTML、CSS、JS開發桌面應用程序,這簡直不要太爽,另外,關於eel的使用也許我會在後續文章中寫到)
1、打開cmd / Visual Studio等等可以裝包的工具
下載pyinstaller
- 如果是python環境,那麼:pip install pyinstaller
- 如果是conda環境,那麼:conda install pyinstaller
2、找到你的python_main程序
假設項目根目錄為F:\xx\xx\xx,該目錄的文件管理如下:
- Web(包括HTML、CSS、JS) - 遊戲資源文件夾 (包括圖片巴拉巴拉的) - Class1.py (Class1中導入Class3) - Class2.py - Class3.py - Python_Main.py (Python_Main中導入Class1,Class2和eel)
3、查看eel庫
eel庫一般在site-pakages裡
打開__main__.py文件
#eel的__main__.py文件 from __future__ import print_function import sys import pkg_resources as pkg import PyInstaller.__main__ as pyi import os args = sys.argv[1:] main_script = args.pop(0) web_folder = args.pop(0) print("Building executable with main script '%s' and web folder '%s'...\n" % (main_script, web_folder)) eel_js_file = pkg.resource_filename('eel', 'eel.js') js_file_arg = '%s%seel' % (eel_js_file, os.pathsep) web_folder_arg = '%s%s%s' % (web_folder, os.pathsep, web_folder) needed_args = ['--hidden-import', 'bottle_websocket', '--add-data', js_file_arg, '--add-data', web_folder_arg] full_args = [main_script] + needed_args + args print('Running:\npyinstaller', ' '.join(full_args), '\n') pyi.run(full_args)
註意到print(“Building executable with main script ‘%s’ and web folder ‘%s’…\n” %
(main_script, web_folder))和print(‘Running:\npyinstaller’, ’ '.join(full_args), ‘\n’)可以知道eel的打包原理還是利用pyinstaller
4、打開cmd開始將含有eel的python_main打包起來
python -m eel F:\xx\xx\xx\Python_Main.py F:\xx\xx\xx\Web
這裡暫時不加-w -f -i參數。(因為無論如何必然會出錯)
報錯,提示找不到eel.js文件。呼呼,我就奇怪瞭,eel.js文件不就在那兒擺著嗎。
打開eel的__init__.py文件查看
from __future__ import print_function # Python 2 compatibility stuff from builtins import range from io import open import gevent as gvt import json as jsn import bottle as btl import bottle.ext.websocket as wbs import re as rgx import os import eel.browsers as brw import random as rnd import sys import pkg_resources as pkg import socket _eel_js_file = pkg.resource_filename('eel', 'eel.js') _eel_js = open(_eel_js_file, encoding='utf-8').read() _websockets = [] _call_return_values = {} _call_return_callbacks = {} _call_number = 0 _exposed_functions = {} _js_functions = [] _mock_queue = [] _mock_queue_done = set() # All start() options must provide a default value and explanation here _start_args = { 'mode': 'chrome', # What browser is used 'host': 'localhost', # Hostname use for Bottle server 'port': 8000, # Port used for Bottle server (use 0 for auto) 'block': True, # Whether start() blocks calling thread 'jinja_templates': None, # Folder for jinja2 templates 'cmdline_args': ['--disable-http-cache'], # Extra cmdline flags to pass to browser start 'size': None, # (width, height) of main window 'position': None, # (left, top) of main window 'geometry': {}, # Dictionary of size/position for all windows 'close_callback': None, # Callback for when all windows have closed 'app_mode': True, # (Chrome specific option) 'all_interfaces': False, # Allow bottle server to listen for connections on all interfaces 'disable_cache': True, # Sets the no-store response header when serving assets 'app': btl.default_app(), # Allows passing in a custom Bottle instance, e.g. with middleware } # == Temporary (suppressable) error message to inform users of breaking API change for v1.0.0 === _start_args['suppress_error'] = False api_error_message = ''' ---------------------------------------------------------------------------------- 'options' argument deprecated in v1.0.0, see https://github.com/ChrisKnott/Eel To suppress this error, add 'suppress_error=True' to start() call. This option will be removed in future versions ---------------------------------------------------------------------------------- ''' # =============================================================================================== # Public functions def expose(name_or_function=None): # Deal with '@eel.expose()' - treat as '@eel.expose' if name_or_function is None: return expose if type(name_or_function) == str: # Called as '@eel.expose("my_name")' name = name_or_function def decorator(function): _expose(name, function) return function return decorator else: function = name_or_function _expose(function.__name__, function) return function def init(path, allowed_extensions=['.js', '.html', '.txt', '.htm', '.xhtml', '.vue']): global root_path, _js_functions root_path = _get_real_path(path) js_functions = set() for root, _, files in os.walk(root_path): for name in files: if not any(name.endswith(ext) for ext in allowed_extensions): continue try: with open(os.path.join(root, name), encoding='utf-8') as file: contents = file.read() expose_calls = set() finder = rgx.findall(r'eel\.expose\(([^\)]+)\)', contents) for expose_call in finder: # If name specified in 2nd argument, strip quotes and store as function name if ',' in expose_call: expose_call = rgx.sub(r'["\']', '', expose_call.split(',')[1]) expose_call = expose_call.strip() # Verify that function name is valid msg = "eel.expose() call contains '(' or '='" assert rgx.findall(r'[\(=]', expose_call) == [], msg expose_calls.add(expose_call) js_functions.update(expose_calls) except UnicodeDecodeError: pass # Malformed file probably _js_functions = list(js_functions) for js_function in _js_functions: _mock_js_function(js_function) def start(*start_urls, **kwargs): _start_args.update(kwargs) if 'options' in kwargs: if _start_args['suppress_error']: _start_args.update(kwargs['options']) else: raise RuntimeError(api_error_message) if _start_args['port'] == 0: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.bind(('localhost', 0)) _start_args['port'] = sock.getsockname()[1] sock.close() if _start_args['jinja_templates'] != None: from jinja2 import Environment, FileSystemLoader, select_autoescape templates_path = os.path.join(root_path, _start_args['jinja_templates']) _start_args['jinja_env'] = Environment(loader=FileSystemLoader(templates_path), autoescape=select_autoescape(['html', 'xml'])) # Launch the browser to the starting URLs show(*start_urls) def run_lambda(): if _start_args['all_interfaces'] == True: HOST = '0.0.0.0' else: HOST = _start_args['host'] app = _start_args['app'] # type: btl.Bottle for route_path, route_params in BOTTLE_ROUTES.items(): route_func, route_kwargs = route_params app.route(path=route_path, callback=route_func, **route_kwargs) return btl.run( host=HOST, port=_start_args['port'], server=wbs.GeventWebSocketServer, quiet=True, app=app) # Start the webserver if _start_args['block']: run_lambda() else: spawn(run_lambda) def show(*start_urls): brw.open(start_urls, _start_args) def sleep(seconds): gvt.sleep(seconds) def spawn(function, *args, **kwargs): gvt.spawn(function, *args, **kwargs) # Bottle Routes def _eel(): start_geometry = {'default': {'size': _start_args['size'], 'position': _start_args['position']}, 'pages': _start_args['geometry']} page = _eel_js.replace('/** _py_functions **/', '_py_functions: %s,' % list(_exposed_functions.keys())) page = page.replace('/** _start_geometry **/', '_start_geometry: %s,' % _safe_json(start_geometry)) btl.response.content_type = 'application/javascript' _set_response_headers(btl.response) return page def _static(path): response = None if 'jinja_env' in _start_args and 'jinja_templates' in _start_args: template_prefix = _start_args['jinja_templates'] + '/' if path.startswith(template_prefix): n = len(template_prefix) template = _start_args['jinja_env'].get_template(path[n:]) response = btl.HTTPResponse(template.render()) if response is None: response = btl.static_file(path, root=root_path) _set_response_headers(response) return response def _websocket(ws): global _websockets for js_function in _js_functions: _import_js_function(js_function) page = btl.request.query.page if page not in _mock_queue_done: for call in _mock_queue: _repeated_send(ws, _safe_json(call)) _mock_queue_done.add(page) _websockets += [(page, ws)] while True: msg = ws.receive() if msg is not None: message = jsn.loads(msg) spawn(_process_message, message, ws) else: _websockets.remove((page, ws)) break _websocket_close(page) BOTTLE_ROUTES = { "/eel.js": (_eel, dict()), "/<path:path>": (_static, dict()), "/eel": (_websocket, dict(apply=[wbs.websocket])) } # Private functions def _safe_json(obj): return jsn.dumps(obj, default=lambda o: None) def _repeated_send(ws, msg): for attempt in range(100): try: ws.send(msg) break except Exception: sleep(0.001) def _process_message(message, ws): if 'call' in message: return_val = _exposed_functions[message['name']](*message['args']) _repeated_send(ws, _safe_json({ 'return': message['call'], 'value': return_val })) elif 'return' in message: call_id = message['return'] if call_id in _call_return_callbacks: callback = _call_return_callbacks.pop(call_id) callback(message['value']) else: _call_return_values[call_id] = message['value'] else: print('Invalid message received: ', message) def _get_real_path(path): if getattr(sys, 'frozen', False): return os.path.join(sys._MEIPASS, path) else: return os.path.abspath(path) def _mock_js_function(f): exec('%s = lambda *args: _mock_call("%s", args)' % (f, f), globals()) def _import_js_function(f): exec('%s = lambda *args: _js_call("%s", args)' % (f, f), globals()) def _call_object(name, args): global _call_number _call_number += 1 call_id = _call_number + rnd.random() return {'call': call_id, 'name': name, 'args': args} def _mock_call(name, args): call_object = _call_object(name, args) global _mock_queue _mock_queue += [call_object] return _call_return(call_object) def _js_call(name, args): call_object = _call_object(name, args) for _, ws in _websockets: _repeated_send(ws, _safe_json(call_object)) return _call_return(call_object) def _call_return(call): call_id = call['call'] def return_func(callback=None): if callback is not None: _call_return_callbacks[call_id] = callback else: for w in range(10000): if call_id in _call_return_values: return _call_return_values.pop(call_id) sleep(0.001) return return_func def _expose(name, function): msg = 'Already exposed function with name "%s"' % name assert name not in _exposed_functions, msg _exposed_functions[name] = function def _websocket_close(page): close_callback = _start_args.get('close_callback') if close_callback is not None: sockets = [p for _, p in _websockets] close_callback(page, sockets) else: # Default behaviour - wait 1s, then quit if all sockets are closed sleep(1.0) if len(_websockets) == 0: sys.exit() def _set_response_headers(response): if _start_args['disable_cache']: # https://stackoverflow.com/a/24748094/280852 response.set_header('Cache-Control', 'no-store')
註意到瞭嗎?代碼中有這樣兩句:
_eel_js_file = pkg.resource_filename('eel', 'eel.js') _eel_js = open(_eel_js_file, encoding='utf-8').read()
一不做,二不休,把eel.js的內容復制下來,然後,奧裡給。將代碼做如下修改(即貼即用)
from __future__ import print_function # Python 2 compatibility stuff
from builtins import range
from io import open
import gevent as gvt
import json as jsn
import bottle as btl
import bottle.ext.websocket as wbs
import re as rgx
import os
import eel.browsers as brw
import random as rnd
import sys
import pkg_resources as pkg
import socket
eelJS = '''
eel = {
_host: window.location.origin,
set_host: function (hostname) {
eel._host = hostname
},
expose: function(f, name) {
if(name === undefined){
name = f.toString();
let i = 'function '.length, j = name.indexOf('(');
name = name.substring(i, j).trim();
}
eel._exposed_functions[name] = f;
},
guid: function() {
return eel._guid;
},
// These get dynamically added by library when file is served
/** _py_functions **/
/** _start_geometry **/
_guid: ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c =>
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
),
_exposed_functions: {},
_mock_queue: [],
_mock_py_functions: function() {
for(let i = 0; i < eel._py_functions.length; i++) {
let name = eel._py_functions[i];
eel[name] = function() {
let call_object = eel._call_object(name, arguments);
eel._mock_queue.push(call_object);
return eel._call_return(call_object);
}
}
},
_import_py_function: function(name) {
let func_name = name;
eel[name] = function() {
let call_object = eel._call_object(func_name, arguments);
eel._websocket.send(eel._toJSON(call_object));
return eel._call_return(call_object);
}
},
_call_number: 0,
_call_return_callbacks: {},
_call_object: function(name, args) {
let arg_array = [];
for(let i = 0; i < args.length; i++){
arg_array.push(args[i]);
}
let call_id = (eel._call_number += 1) + Math.random();
return {'call': call_id, 'name': name, 'args': arg_array};
},
_sleep: function(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
},
_toJSON: function(obj) {
return JSON.stringify(obj, (k, v) => v === undefined ? null : v);
},
_call_return: function(call) {
return function(callback = null) {
if(callback != null) {
eel._call_return_callbacks[call.call] = callback;
} else {
return new Promise(function(resolve) {
eel._call_return_callbacks[call.call] = resolve;
});
}
}
},
_position_window: function(page) {
let size = eel._start_geometry['default'].size;
let position = eel._start_geometry['default'].position;
if(page in eel._start_geometry.pages) {
size = eel._start_geometry.pages123下一頁閱讀全文推薦閱讀:
- python 實現百度網盤非會員上傳超過500個文件的方法
- python 批量壓縮圖片的腳本
- Python實現如何根據文件後綴進行分類
- 如何利用飾器實現 Python 函數重載
- vue項目中如何使用mock你知道嗎