Remove PySide dependency and deprecate desktop builds (#475)

* Remove PySide, gui option from code
* Remove pyside 6 dependency from code
* Remove workflows which build desktop applications
* Update unit tests and update line in documentation
* Remove additional references to pyinstaller, gui
* Add uninstall steps to normal uninstall instructions
This commit is contained in:
sabaimran 2023-09-07 11:36:27 -07:00 committed by GitHub
parent 76562f4250
commit dccfae3853
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 13 additions and 568 deletions

View file

@ -1,113 +0,0 @@
name: desktop_dev_build
on:
push:
branches:
- master
paths:
- src/khoj/**
- pyproject.toml
- Khoj.spec
- .github/workflows/build_desktop.yml
workflow_dispatch:
jobs:
publish_desktop_apps:
name: 🖥️ Publish Desktop Apps
strategy:
matrix:
include:
- os: ubuntu-20.04
extension: deb
- os: macos-latest
extension: dmg
- os: windows-latest
extension: exe
runs-on: ${{ matrix.os }}
permissions:
contents: write
steps:
- uses: actions/checkout@v3
- name: Set up Python 3.9
uses: actions/setup-python@v4
with:
python-version: '3.9'
- name: ⏬️ Install Dependencies
shell: bash
run: |
if [ "$RUNNER_OS" == "Linux" ]; then
sudo apt update && sudo apt install libegl1 libxcb-xinerama0 python3-tk -y
fi
python -m pip install --upgrade pip
pip install pyinstaller
- name: ⬇️ Install Khoj App
run: |
pip install --upgrade .
- name: 📦 Package Khoj App
shell: bash
run: |
# Setup Environment for Reproducible Builds
export PYTHONHASHSEED=42
export SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)
pyinstaller --noconfirm Khoj.spec
if [ "$RUNNER_OS" == "Windows" ]; then
mv dist/Khoj.exe dist/khoj_dev_amd64.exe
fi
- name: 💻 Create Mac App DMG
if: matrix.os == 'macos-latest'
run: |
# Install Mac DMG Creator
brew install create-dmg
# Copy app to separate dmg folder
mkdir -p dist/dmg && cp -r dist/Khoj.app dist/dmg
# Create disk image with the app
create-dmg \
--volname "Khoj" \
--volicon "src/khoj/interface/web/assets/icons/favicon.icns" \
--window-pos 200 120 \
--window-size 600 300 \
--icon-size 100 \
--icon "Khoj.app" 175 120 \
--hide-extension "Khoj.app" \
--app-drop-link 425 120 \
"dist/khoj_dev_amd64.dmg" \
"dist/dmg/"
- uses: ruby/setup-ruby@v1
if: matrix.os == 'ubuntu-20.04'
with:
ruby-version: '3.0'
- name: 🐧 Create Debian Package
if: matrix.os == 'ubuntu-20.04'
shell: bash
run: |
# Install Debian Packager
gem install fpm
# Copy app files into expected output directory structure
mkdir -p package/opt package/usr/share/applications package/usr/share/icons/hicolor/128x128/apps
cp -r dist/Khoj package/opt/Khoj
cp src/khoj/interface/web/assets/icons/favicon-128x128.png package/usr/share/icons/hicolor/128x128/apps/Khoj.png
cp Khoj.desktop package/usr/share/applications
# Fix permissions to be usable by non-root users
find package/usr/share -type f -exec chmod 644 -- {} +
chmod 755 package/opt/Khoj
# Package the app
fpm -C package -s dir -t deb -n Khoj -p dist/khoj_dev_amd64.deb
- uses: actions/upload-artifact@v3
with:
name: khoj_dev_amd64.${{matrix.extension}}
path: dist/khoj_dev_amd64.${{matrix.extension}}
retention-days: 3

View file

@ -64,111 +64,3 @@ jobs:
src/interface/obsidian/main.js
src/interface/obsidian/manifest.json
src/interface/obsidian/styles.css
publish_desktop_apps:
name: 🖥️ Publish Desktop Apps
strategy:
matrix:
include:
- os: ubuntu-20.04
extension: deb
- os: macos-latest
extension: dmg
- os: windows-latest
extension: exe
runs-on: ${{ matrix.os }}
permissions:
contents: write
steps:
- uses: actions/checkout@v3
- name: Set up Python 3.9
uses: actions/setup-python@v4
with:
python-version: '3.9'
- name: ⏬️ Install Dependencies
shell: bash
run: |
if [ "$RUNNER_OS" == "Linux" ]; then
sudo apt update && sudo apt install libegl1 libxcb-xinerama0 python3-tk -y
fi
python -m pip install --upgrade pip
pip install pyinstaller
- name: ⬇️ Install Khoj App
run: |
pip install --upgrade .
- name: 📦 Package Khoj App
shell: bash
run: |
# Setup Environment for Reproducible Builds
export PYTHONHASHSEED=42
export SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)
pyinstaller --noconfirm Khoj.spec
if [ "$RUNNER_OS" == "Windows" ]; then
mv dist/Khoj.exe dist/khoj_"$GITHUB_REF_NAME"_amd64.exe
fi
- name: 💻 Create Mac App DMG
if: matrix.os == 'macos-latest'
run: |
# Install Mac DMG Creator
brew install create-dmg
# Copy app to separate dmg folder
mkdir -p dist/dmg && cp -r dist/Khoj.app dist/dmg
# Create disk image with the app
create-dmg \
--volname "Khoj" \
--volicon "src/khoj/interface/web/assets/icons/favicon.icns" \
--window-pos 200 120 \
--window-size 600 300 \
--icon-size 100 \
--icon "Khoj.app" 175 120 \
--hide-extension "Khoj.app" \
--app-drop-link 425 120 \
"dist/khoj_"$GITHUB_REF_NAME"_amd64.dmg" \
"dist/dmg/"
- uses: ruby/setup-ruby@v1
if: matrix.os == 'ubuntu-20.04'
with:
ruby-version: '3.0'
- name: 🐧 Create Debian Package
if: matrix.os == 'ubuntu-20.04'
shell: bash
env:
DEBIAN_PACKAGE_VERSION: ${{ inputs.version }}
run: |
# Install Debian Packager
gem install fpm
# Copy app files into expected output directory structure
mkdir -p package/opt package/usr/share/applications package/usr/share/icons/hicolor/128x128/apps
cp -r dist/Khoj package/opt/Khoj
cp src/khoj/interface/web/assets/icons/favicon-128x128.png package/usr/share/icons/hicolor/128x128/apps/Khoj.png
cp Khoj.desktop package/usr/share/applications
# Fix permissions to be usable by non-root users
find package/usr/share -type f -exec chmod 644 -- {} +
chmod 755 package/opt/Khoj
# Package the app
if [ -z "$DEBIAN_PACKAGE_VERSION" ]; then
DEBIAN_PACKAGE_VERSION=$(echo $GITHUB_REF_NAME | sed -E 's/v(.*)/\1/g')
fi
fpm -C package -s dir -t deb -n Khoj --version $DEBIAN_PACKAGE_VERSION -p dist/khoj_"$GITHUB_REF_NAME"_amd64.deb
- uses: actions/upload-artifact@v3
with:
name: khoj_${{github.ref_name}}_amd64.${{matrix.extension}}
path: dist/khoj_${{github.ref_name}}_amd64.${{matrix.extension}}
- name: 🌈 Release
uses: softprops/action-gh-release@v1
if: startsWith(github.ref, 'refs/tags/')
with:
generate_release_notes: true
files: dist/khoj_${{github.ref_name}}_amd64.${{matrix.extension}}

View file

@ -1,7 +0,0 @@
[Desktop Entry]
Type=Application
Name=Khoj
Comment=An AI personal assistant for your Digital Brain
Path=/opt
Exec=/opt/Khoj
Icon=Khoj

123
Khoj.spec
View file

@ -1,123 +0,0 @@
# -*- mode: python ; coding: utf-8 -*-
from os.path import join
from platform import system
from PyInstaller.utils.hooks import copy_metadata
import sysconfig
datas = [
('src/khoj/interface/web', 'khoj/interface/web'),
(f'{sysconfig.get_paths()["purelib"]}/transformers', 'transformers'),
(f'{sysconfig.get_paths()["purelib"]}/langchain', 'langchain'),
(f'{sysconfig.get_paths()["purelib"]}/PIL', 'PIL'),
(f'{sysconfig.get_paths()["purelib"]}/gpt4all', 'gpt4all'),
]
datas += copy_metadata('torch')
datas += copy_metadata('tqdm')
datas += copy_metadata('regex')
datas += copy_metadata('requests')
datas += copy_metadata('packaging')
datas += copy_metadata('filelock')
datas += copy_metadata('numpy')
datas += copy_metadata('tokenizers')
datas += copy_metadata('pillow')
datas += copy_metadata('huggingface_hub')
datas += copy_metadata('safetensors')
datas += copy_metadata('pyyaml')
block_cipher = None
a = Analysis(
['src/khoj/main.py'],
pathex=[],
binaries=[],
datas=datas,
hiddenimports=['huggingface_hub.repository', 'PIL', 'PIL._tkinter_finder', 'tiktoken_ext', 'tiktoken_ext.openai_public'],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False,
)
# Filter out unused and/or duplicate shared libs
torch_lib_paths = {
join('torch', 'lib', 'libtorch_cuda.so'),
join('torch', 'lib', 'libtorch_cpu.so'),
}
a.datas = [entry for entry in a.datas if not entry[0] in torch_lib_paths]
os_path_separator = '\\' if system() == 'Windows' else '/'
a.datas = [entry for entry in a.datas if not f'torch{os_path_separator}_C.cp' in entry[0]]
a.datas = [entry for entry in a.datas if not f'torch{os_path_separator}_dl.cp' in entry[0]]
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
if system() != 'Darwin':
# Add Splash screen to show on app launch
splash = Splash(
'src/khoj/interface/web/assets/icons/favicon-128x128.png',
binaries=a.binaries,
datas=a.datas,
text_pos=(10, 160),
text_size=12,
text_color='black',
minify_script=True,
always_on_top=True
)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
splash,
splash.binaries,
[],
name='Khoj',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=False,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch='x86_64',
codesign_identity=None,
entitlements_file=None,
icon='src/khoj/interface/web/assets/icons/favicon-128x128.ico',
)
else:
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
[],
name='Khoj',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=False,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch='x86_64',
codesign_identity=None,
entitlements_file=None,
icon='src/khoj/interface/web/assets/icons/favicon.icns',
)
app = BUNDLE(
exe,
name='Khoj.app',
icon='src/khoj/interface/web/assets/icons/favicon.icns',
bundle_identifier=None,
)

View file

@ -27,7 +27,7 @@ For more detailed Windows installation and troubleshooting, see [Windows Install
Run the following command from your terminal to start the Khoj backend and open Khoj in your browser.
```shell
khoj --gui
khoj
```
Note: To start Khoj automatically in the background use [Task scheduler](https://www.windowscentral.com/how-create-automated-task-using-task-scheduler-windows-10) on Windows or [Cron](https://en.wikipedia.org/wiki/Cron) on Mac, Linux (e.g with `@reboot khoj`)
@ -73,6 +73,9 @@ pip install --upgrade --pre khoj-assistant
## Uninstall
1. (Optional) Hit `Ctrl-C` in the terminal running the khoj server to stop it
2. Delete the khoj directory in your home folder (i.e `~/.khoj` on Linux, Mac or `C:\Users\<your-username>\.khoj` on Windows)
5. You might want to `rm -rf` the following directories:
- `~/.khoj`
- `~/.cache/gpt4all`
3. Uninstall the khoj server with `pip uninstall khoj-assistant`
4. (Optional) Uninstall khoj.el or the khoj obsidian plugin in the standard way on Emacs, Obsidian

View file

@ -4,7 +4,7 @@
"version": "0.11.4",
"minAppVersion": "0.15.0",
"description": "An AI Personal Assistant for your Digital Brain",
"author": "Debanjum Singh Solanky",
"authorUrl": "https://github.com/debanjum",
"author": "Khoj",
"authorUrl": "https://github.com/khoj-ai/",
"isDesktopOnly": true
}

View file

@ -46,7 +46,6 @@ dependencies = [
"tenacity >= 8.2.2",
"pillow == 9.3.0",
"pydantic >= 1.10.10",
"pyside6 >= 6.5.1",
"pyyaml == 6.0",
"rich >= 13.3.1",
"schedule == 1.1.0",

View file

@ -1,75 +0,0 @@
# Standard Packages
import webbrowser
import os
import signal
# External Packages
from PySide6 import QtGui, QtWidgets
from PySide6.QtCore import Qt
# Internal Packages
from khoj.utils import constants
from PySide6.QtCore import QThread
class ServerThread(QThread):
def __init__(self, start_server_func, parent=None):
super(ServerThread, self).__init__(parent)
self.start_server_func = start_server_func
def __del__(self):
self.wait()
def run(self):
self.start_server_func()
def exit(self):
os.kill(os.getpid(), signal.SIGTERM)
super(ServerThread, self).exit()
class MainWindow(QtWidgets.QMainWindow):
"""Create Window to Navigate users to the web UI"""
def __init__(self, host: str, port: int):
super(MainWindow, self).__init__()
# Initialize Configure Window
self.setWindowTitle("Khoj")
# Set Window Icon
icon_path = constants.web_directory / "assets/icons/favicon-128x128.png"
self.setWindowIcon(QtGui.QIcon(f"{icon_path.absolute()}"))
# Initialize Configure Window Layout
self.wlayout = QtWidgets.QVBoxLayout()
# Add a Label that says "Khoj Configuration" to the Window
self.wlayout.addWidget(QtWidgets.QLabel("Welcome to Khoj"))
# Add a Button to open the Web UI at http://host:port/config
self.open_web_ui_button = QtWidgets.QPushButton("Open Web UI")
self.open_web_ui_button.clicked.connect(lambda: webbrowser.open(f"http://{host}:{port}/config"))
self.wlayout.addWidget(self.open_web_ui_button)
# Set the central widget of the Window. Widget will expand
# to take up all the space in the window by default.
self.config_window = QtWidgets.QWidget()
self.config_window.setLayout(self.wlayout)
self.setCentralWidget(self.config_window)
self.position_window()
def position_window(self):
"Position the window at center of X axis and near top on Y axis"
window_rectangle = self.geometry()
screen_center = self.screen().availableGeometry().center()
window_rectangle.moveCenter(screen_center)
self.move(window_rectangle.topLeft().x(), 25)
def show_on_top(self):
"Bring Window on Top"
self.show()
self.setWindowState(Qt.WindowState.WindowActive)
self.activateWindow() # For Bringing to Top on Windows
self.raise_() # For Bringing to Top from Minimized State on OSX

View file

@ -1,43 +0,0 @@
# Standard Packages
import webbrowser
# External Packages
from PySide6 import QtGui, QtWidgets
# Internal Packages
from khoj.utils import constants, state
from khoj.interface.desktop.main_window import MainWindow
def create_system_tray(gui: QtWidgets.QApplication, main_window: MainWindow):
"""Create System Tray with Menu. Menu contain options to
1. Open Search Page on the Web Interface
2. Open App Configuration Screen
3. Quit Application
"""
# Create the system tray with icon
icon_path = constants.web_directory / "assets/icons/favicon-128x128.png"
icon = QtGui.QIcon(f"{icon_path.absolute()}")
tray = QtWidgets.QSystemTrayIcon(icon)
tray.setVisible(True)
# Create the menu and menu actions
menu = QtWidgets.QMenu()
menu_actions = [
("Search", lambda: webbrowser.open(f"http://{state.host}:{state.port}/")),
("Configure", lambda: webbrowser.open(f"http://{state.host}:{state.port}/config")),
("App", main_window.show),
("Quit", gui.quit),
]
# Add the menu actions to the menu
for action_text, action_function in menu_actions:
menu_action = QtGui.QAction(action_text, menu)
menu_action.triggered.connect(action_function) # type: ignore[attr-defined]
menu.addAction(menu_action)
# Add the menu to the system tray
tray.setContextMenu(menu)
return tray

View file

@ -1,6 +1,5 @@
# Standard Packages
import os
import signal
import sys
import locale
@ -12,8 +11,6 @@ if sys.stderr is None:
import logging
import threading
import warnings
from platform import system
import webbrowser
from importlib.metadata import version
# Ignore non-actionable warnings
@ -70,81 +67,13 @@ def run():
logger.info("🌘 Starting Khoj")
if not args.gui:
# Setup task scheduler
poll_task_scheduler()
# Setup task scheduler
poll_task_scheduler()
# Start Server
configure_routes(app)
initialize_server(args.config, required=False)
start_server(app, host=args.host, port=args.port, socket=args.socket)
else:
from PySide6 import QtWidgets
from PySide6.QtCore import QTimer
from khoj.interface.desktop.main_window import MainWindow, ServerThread
from khoj.interface.desktop.system_tray import create_system_tray
# Setup GUI
gui = QtWidgets.QApplication([])
main_window = MainWindow(args.host, args.port)
# System tray is only available on Windows, MacOS.
# On Linux (Gnome) the System tray is not supported.
# Since only the Main Window is available
# Quitting it should quit the application
if system() in ["Windows", "Darwin"]:
gui.setQuitOnLastWindowClosed(False)
tray = create_system_tray(gui, main_window)
tray.show()
# Setup Server
initialize_server(args.config, required=False)
configure_routes(app)
server = ServerThread(start_server_func=lambda: start_server(app, host=args.host, port=args.port), parent=gui)
url = f"http://{args.host}:{args.port}"
logger.info(f"🌗 Khoj is running at {url}")
try:
startup_url = url if args.config else f"{url}/config"
webbrowser.open(startup_url)
except:
logger.warning(f"🚧 Unable to open browser. Please open {url} manually to configure or use Khoj.")
# Show Main Window on First Run Experience or if on Linux
if args.config is None or system() not in ["Windows", "Darwin"]:
main_window.show()
# Setup Signal Handlers
signal.signal(signal.SIGINT, sigint_handler)
# Invoke Python interpreter every 500ms to handle signals, run scheduled tasks
timer = QTimer()
timer.start(500)
timer.timeout.connect(schedule.run_pending)
# Start Application
server.start()
gui.aboutToQuit.connect(server.exit)
# Close Splash Screen if still open
if system() != "Darwin":
try:
import pyi_splash
# Update the text on the splash screen
pyi_splash.update_text("Khoj setup complete")
# Close Splash Screen
pyi_splash.close()
except:
pass
gui.exec()
def sigint_handler(*args):
from PySide6 import QtWidgets
QtWidgets.QApplication.quit()
# Start Server
configure_routes(app)
initialize_server(args.config, required=False)
start_server(app, host=args.host, port=args.port, socket=args.socket)
def set_state(args):
@ -171,12 +100,3 @@ def poll_task_scheduler():
timer_thread.daemon = True
timer_thread.start()
schedule.run_pending()
def run_gui():
sys.argv += ["--gui"]
run()
if __name__ == "__main__":
run_gui()

View file

@ -17,7 +17,6 @@ def cli(args=None):
parser.add_argument(
"--config-file", "-c", default="~/.khoj/khoj.yml", type=pathlib.Path, help="YAML file to configure Khoj"
)
parser.add_argument("--gui", action="store_true", default=False, help="Show native desktop GUI. Default: false")
parser.add_argument(
"--regenerate",
action="store_true",

View file

@ -106,11 +106,6 @@ def load_model(
return model
def is_pyinstaller_app():
"Returns true if the app is running from Native GUI created by PyInstaller"
return getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS")
def get_class_by_name(name: str) -> object:
"Returns the class object from name string"
module_name, class_name = name.rsplit(".", 1)

View file

@ -16,7 +16,6 @@ def test_cli_minimal_default():
# Assert
assert actual_args.config_file == resolve_absolute_path(Path("~/.khoj/khoj.yml"))
assert actual_args.regenerate == False
assert actual_args.gui == False
assert actual_args.verbose == 0
@ -36,11 +35,10 @@ def test_cli_invalid_config_file_path():
# ----------------------------------------------------------------------------------------------------
def test_cli_config_from_file():
# Act
actual_args = cli(["-c=tests/data/config.yml", "--regenerate", "--gui", "-vvv"])
actual_args = cli(["-c=tests/data/config.yml", "--regenerate", "-vvv"])
# Assert
assert actual_args.config_file == resolve_absolute_path(Path("tests/data/config.yml"))
assert actual_args.gui == True
assert actual_args.regenerate == True
assert actual_args.config is not None
assert actual_args.verbose == 3