diff --git a/.gitignore b/.gitignore index 34ff768..39d321f 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,6 @@ +# build files +build/ +dist/ + +# local storage storage/ diff --git a/README.md b/README.md index ad1301c..6a9cfda 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,14 @@ python web.py --identity-base64 "GCN6mMhVemdNIK/fw97C1zvU17qjQPFTXRBotVckeGmoOwQ > NOTE: this is a randomly generated identity for example purposes. Do not use it, it has been leaked! +# Build from Source + +You can build a standalone Windows Installer `.msi` with the following command; + +``` +python setup.py bdist_msi +``` + ## TODO - [ ] conversations/contacts list ui with unread indicators diff --git a/logo/icon.ico b/logo/icon.ico new file mode 100644 index 0000000..f00e77e Binary files /dev/null and b/logo/icon.ico differ diff --git a/requirements.txt b/requirements.txt index 2b91f81..f31f233 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ aiohttp>=3.9.5 +cx_freeze>=7.0.0 lxmf>=0.4.3 peewee>=3.17.3 rns>=0.7.5 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..01ca47f --- /dev/null +++ b/setup.py @@ -0,0 +1,44 @@ +from cx_Freeze import setup, Executable + +setup( + name='ReticulumWebChat', + version='1.0.0', + description='A simple open-source web based LXMF client for Reticulum.', + executables=[ + Executable( + script='web.py', # this script to run + base=None, # we are running a console application, not a gui + target_name='ReticulumWebChat', # creates ReticulumWebChat.exe + shortcut_name='ReticulumWebChat', # name shown in shortcut + shortcut_dir='ProgramMenuFolder', # put the shortcut in windows start menu + icon='logo/icon.ico', # set the icon for the exe + ), + ], + options={ + 'build_exe': { + # libs that are required + 'packages': [ + # required for dynamic import fix + # https://github.com/marcelotduarte/cx_Freeze/discussions/2039 + # https://github.com/marcelotduarte/cx_Freeze/issues/2041 + 'RNS', + ], + # files that are required + 'include_files': [ + 'public/', # static files served by web server + ], + # slim down the build by excluding these unused libs + 'excludes': [ + 'PIL', # saves ~200MB + ], + # this has the same effect as the -O command line option when executing CPython directly. + # it also prevents assert statements from executing, removes docstrings and sets __debug__ to False. + # https://stackoverflow.com/a/57948104 + "optimize": 2, + }, + 'build_msi': { + # use a static upgrade code to allow installer to remove existing files on upgrade + 'upgrade_code': '{6c69616d-ae73-460c-88e8-399b3134134e}', + }, + }, +) diff --git a/web.py b/web.py index dcbc34f..23fb349 100644 --- a/web.py +++ b/web.py @@ -3,6 +3,7 @@ import argparse import json import os +import sys import time from datetime import datetime, timezone from typing import Callable, List @@ -21,6 +22,17 @@ from lxmf_message_fields import LxmfImageField, LxmfFileAttachmentsField, LxmfFi from src.audio_call_manager import AudioCall, AudioCallManager +# NOTE: this is required to be able to pack our app with cxfreeze as an exe, otherwise it can't access bundled assets +# this returns a file path based on if we are running web.py directly, or if we have packed it as an exe with cxfreeze +# https://cx-freeze.readthedocs.io/en/latest/faq.html#using-data-files +def get_file_path(filename): + if getattr(sys, "frozen", False): + datadir = os.path.dirname(sys.executable) + else: + datadir = os.path.dirname(__file__) + return os.path.join(datadir, filename) + + class ReticulumWebChat: def __init__(self, identity: RNS.Identity, storage_dir, reticulum_config_dir): @@ -116,7 +128,7 @@ class ReticulumWebChat: # serve index.html @routes.get("/") async def index(request): - return web.FileResponse(path="public/index.html") + return web.FileResponse(path=get_file_path("public/index.html")) # handle websocket clients @routes.get("/ws") @@ -562,7 +574,7 @@ class ReticulumWebChat: # create and run web app app = web.Application() app.add_routes(routes) - app.add_routes([web.static('/', "public")]) # serve anything in public folder + app.add_routes([web.static('/', get_file_path("public/"))]) # serve anything in public folder app.on_shutdown.append(self.shutdown) # need to force close websockets and stop reticulum now app.on_startup.append(on_startup) web.run_app(app, host=host, port=port)