Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: DMG for MacOS ARM64 #2296

Closed
wants to merge 34 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
c92cbf0
works as POC with embedded pg+pgvector
norton120 Nov 20, 2024
7aeff19
this was working: icon appears in toolbar, app runs, app.letta.com op…
norton120 Nov 20, 2024
3090153
qt-based log viewer
norton120 Nov 22, 2024
bf66399
temporarily using a log terminal, moving to a viewer shortly
norton120 Nov 25, 2024
1dd6627
using a thin web viewer that is easy to style. This is the way here IMO.
norton120 Nov 26, 2024
6476b6b
title
norton120 Nov 26, 2024
dd88f95
added debug route for log
norton120 Nov 26, 2024
e250b29
logging and organization for startup before packaging
norton120 Nov 26, 2024
f9b2a85
setup started for py2app
norton120 Nov 26, 2024
8fcb77a
recursion issue in reqs. dmg builds but is faulty
norton120 Nov 26, 2024
752a241
ignore
norton120 Dec 2, 2024
5de253c
pivoting to pyinstaller to see if that has better luck with the deps …
norton120 Dec 3, 2024
47b43cb
builds but fails to include pg binaries, need to add them
norton120 Dec 3, 2024
3905304
noticed spec was missing from git :( fixed that. builds to sytray now
norton120 Dec 3, 2024
9e8ec64
correct asset path
norton120 Dec 3, 2024
3eec14c
builds and runs, logs run. Need to add letta assets for web gui
norton120 Dec 3, 2024
d75e8c0
distro unix works on mac but app bundle fails silently, no logs in Co…
norton120 Dec 3, 2024
f8f8af6
try with new version
norton120 Dec 4, 2024
c355498
mac bundle app works once copied to /Applications folder, but on olde…
norton120 Dec 6, 2024
281337b
macos installer builds usable dmg
norton120 Dec 6, 2024
e8ce928
added arch and version to image name
norton120 Dec 6, 2024
3e17a7e
organization improvements
norton120 Dec 9, 2024
71a57fe
added GH workflow to build and publish for apple silicon and intel
norton120 Dec 9, 2024
6cd291d
remembered homebrew install
norton120 Dec 9, 2024
2701ceb
pathing corrected
norton120 Dec 11, 2024
72a0cc2
builds with 0.6.3 now, but sqlite is starting? so something changed h…
norton120 Dec 12, 2024
833e4f4
Merge remote-tracking branch 'norton120/mac-installable' into mindy/l…
Dec 19, 2024
b8093f2
new poetry envs for desktop_app
Dec 20, 2024
c63134a
update letta to 0.6.5
Dec 20, 2024
b46b29e
Add missing packages, imports to macos.spec
Dec 20, 2024
e3d419c
correct import paths in desktop_application/
Dec 20, 2024
f3e20a9
remove existing DMG before creating a new one
Dec 20, 2024
393b701
fixed template import
Dec 20, 2024
39feeb1
add poetry instructions into github actions dmg build
Dec 20, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions .github/workflows/release_dmg.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
name: Create DMG on Release

on:
release:
types: [published]

jobs:
build-dmg:
runs-on: ${{ matrix.os }}
strategy:
# see this weird matrix to determine who gets what architecture
# https://github.com/actions/runner-images?tab=readme-ov-file#available-images
# supporting only current OS. to extend that, update the dmg_packager.sh script as well
matrix:
os:
- macos-15-large # intel
- macos-15 # arm64
steps:
- name: setup python
uses: actions/setup-python@v2
with:
python-version: '3.12'

- name: Checkout repository
uses: actions/checkout@v2

- name: install create-dmg from homebrew
run: |
brew install create-dmg


- name: Create and activate virtual environment
run: |
cd installable_apps
python -m venv venv
source venv/bin/activate

- name: Install requirements
run: |
cd installable_apps
source venv/bin/activate
pip install -r desktop_app/requirements.txt

- name: Create poetry env
run: |
cd installable_apps
poetry install

- name: Run DMG creator script
run: |
cd installable_apps
source venv/bin/activate
bash dmg_packager.sh

- name: Upload created dmg to GCP bucket
run: |
gsutil cp installable_apps/*.dmg gs://some-bucket-where-dmgs-live/
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -388,14 +388,16 @@ __pycache__/
develop-eggs/
downloads/
eggs#letta/letta-server:0.3.7
**/**/.eggs/
MANIFEST
**/**/*.dmg

# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

!installable_apps/*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
Expand Down
Empty file added installable_apps/__init__.py
Empty file.
Binary file added installable_apps/assets/dark_icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added installable_apps/assets/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added installable_apps/assets/installer_background.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added installable_apps/assets/letta.icns
Binary file not shown.
Empty file.
16 changes: 16 additions & 0 deletions installable_apps/desktop_application/installable_image.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from pathlib import Path
import darkdetect

from desktop_application.installable_logger import get_logger

logger = get_logger(__name__)


class InstallableImage:

@classmethod
def get_icon_path(cls) -> Path:
logger.debug("Determining icon path from system settings...")
image_name = ("dark_" if darkdetect.isDark() else "") + "icon.png"
logger.debug(f"Icon path determined to be {image_name} based on system settings.")
return (Path(__file__).parent.parent / "assets") / image_name
6 changes: 6 additions & 0 deletions installable_apps/desktop_application/installable_logger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from logging import getLogger

def get_logger(name: str):
logger = getLogger("letta_installable_apps")
logger.setLevel("DEBUG")
return logger.getChild(name)
58 changes: 58 additions & 0 deletions installable_apps/desktop_application/logserver.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
from pathlib import Path
from fastapi import FastAPI, WebSocket, Request
from fastapi.responses import JSONResponse
from fastapi.templating import Jinja2Templates
import asyncio

from letta.settings import settings
from desktop_application.installable_image import InstallableImage
from desktop_application.installable_logger import get_logger

logger = get_logger(__name__)

target_file = settings.letta_dir / "logs" / "letta.log"

app = FastAPI()


async def log_reader(n=5):
log_lines = []
with open(target_file, "r") as file:
for line in file.readlines()[-n:]:
if line is None:
continue
if line.__contains__("ERROR"):
log_line = {"content": line, "color": "red"}
elif line.__contains__("WARNING"):
log_line = {"content": line, "color": "yellow"}
else:
log_line = {"content": line, "color": "green"}
log_lines.append(log_line)
return log_lines

@app.get("/log")
async def rest_endpoint_log():
"""used to debug log_reader on the fly"""
logs = await log_reader(30)
return JSONResponse(logs)

@app.websocket("/ws/log")
async def websocket_endpoint_log(websocket: WebSocket):
await websocket.accept()

try:
while True:
await asyncio.sleep(1)
logs = await log_reader(30)
await websocket.send_json(logs)
except Exception as e:
logger.error(f"Error in log websocket: {e}")

finally:
await websocket.close()

@app.get("/")
async def get(request: Request):
context = {"log_file": target_file, "icon": InstallableImage().get_icon_path()}
templates = Jinja2Templates(directory=str((Path(__file__).parent / "templates").absolute()))
return templates.TemplateResponse("index.html", {"request": request, "context": context})
Loading
Loading