Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
21 changes: 21 additions & 0 deletions .github/assets/release-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
## macOS Installation Instructions

**Important:** macOS will prevent this app from running due to quarantine restrictions.
Follow these steps after downloading:

1. Open Terminal (found in Applications → Utilities)
2. Navigate to your Downloads folder:
```bash
cd ~/Downloads
```
3. Remove the quarantine flag from the downloaded zip:
```bash
xattr -d com.apple.quarantine macos-arm64.zip
```
4. Now you can unzip `macos-arm64.zip` in the Finder and move the `Writingway` folder where your want.
5. Start the application with the `Writingway` script in the `Writingway` folder.

**Notes:**
- Removing the quarantine flag is required because the application is not signed.
Only proceed if you trust the site where you downloaded this file.
- Writeingway stores your data (Projects) and setting in the application's folder: `Writingway`.
8 changes: 8 additions & 0 deletions .github/parameters/setup-macports-arm64.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
prefix: '/opt/local'
variants:
select:
# - universal
ports:
- name: portaudio
# select: [ universal ]
# deselect: [ cxx ] # Do not build C++ bindings
149 changes: 149 additions & 0 deletions .github/workflows/build-osx-arm64.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# Build a onedir OSX release using pyinstaller

name: Create OSX arm64 release

on:
workflow_dispatch:
push:
tags:
- '*'

jobs:
build:
runs-on: macos-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

# Install macports to get portaudio
- name: Setup MacPorts (macOS)
id: setup-macports
uses: melusina-org/setup-macports@v1
with:
# Specify packages to install
parameters: '.github/parameters/setup-macports-arm64.yaml'
# Allow the workflow to continue even if this fails
continue-on-error: true

- name: Display MacPorts build log on failure
if: steps.setup-macports.outcome == 'failure'
run: |
echo "=== MacPorts build log ==="
cat $(port logfile portaudio)
exit 1

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version-file: '.python-version'

- name: Install Python dependencies using pip
run: |
# Install requirements using pip
# Add macports includes
export CPPFLAGS="-I/opt/local/include"
export LDFLAGS="-L/opt/local/lib"

# universal2 build of portaudio is broken
# See: https://trac.macports.org/ticket/71481
# See: https://github.com/PortAudio/portaudio/issues/994
# export ARCHFLAGS="-arch arm64 -arch x86_64" # Enable universal builds
export ARCHFLAGS="-arch arm64"

pip install -r requirements.txt
working-directory: ./

- name: Install spaCy English model
run: |
python -m spacy download en_core_web_sm
working-directory: ./

- name: install beautifulsoup4
run: |
python -m pip install beautifulsoup4
working-directory: ./

- name: Install PyInstaller
run: |
pip install pyinstaller
working-directory: ./

- name: Build executable using pyinstaller
run: |
TARGET_ARCH=arm64 pyinstaller pyinstaller/Writingway_osx.spec
working-directory: ./

# Create Artifact associated with this run that can be downloaded later
#- name: Create Artifact
# uses: actions/upload-artifact@v4
# with:
# name: macos-arm64
# path: dist/

# Determine release information
- name: Set release info
id: release_info
run: |
# Release instructions to add the body of the github release
RELEASE_INSTRUCTIONS=$(cat .github/assets/release-instructions.md)

if [[ "${GITHUB_REF}" == refs/tags/* ]]; then
# Tag-triggered release
TAG_NAME=${GITHUB_REF#refs/tags/}
echo "release_name=${TAG_NAME}" >> $GITHUB_OUTPUT
echo "tag_name=${TAG_NAME}" >> $GITHUB_OUTPUT
echo "prerelease=false" >> $GITHUB_OUTPUT

# Use multiline format for body output
{
echo "body<<EOF"
echo "Release ${TAG_NAME}"
echo ""
cat .github/assets/release-instructions.md
echo "EOF"
} >> $GITHUB_OUTPUT

else
# Manual/snapshot release
BRANCH_NAME=${GITHUB_REF#refs/heads/}
DATE=$(date +'%Y%m%d-%H%M%S')
SHORT_SHA=${GITHUB_SHA:0:7}
SNAPSHOT_TAG="snapshot-${BRANCH_NAME}-${DATE}-${SHORT_SHA}"

echo "release_name=Snapshot ${BRANCH_NAME} (${DATE})" >> $GITHUB_OUTPUT
echo "tag_name=${SNAPSHOT_TAG}" >> $GITHUB_OUTPUT
echo "prerelease=true" >> $GITHUB_OUTPUT

# Use multiline format for body output
{
echo "body<<EOF"
echo "Snapshot build from branch \`${BRANCH_NAME}\` at commit ${GITHUB_SHA}"
echo ""
cat .github/assets/release-instructions.md
echo "EOF"
} >> $GITHUB_OUTPUT
fi

# Create ZIP archive of the build
- name: Create release archive
run: |
cd dist
zip -r ../macos-arm64.zip .
cd ..

- name: Create Release
uses: softprops/action-gh-release@v1
with:
tag_name: ${{ steps.release_info.outputs.tag_name }}
name: ${{ steps.release_info.outputs.release_name }}
body: ${{ steps.release_info.outputs.body }}
prerelease: ${{ steps.release_info.outputs.prerelease }}
files: |
macos-arm64.zip
generate_release_notes: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: List files in dist folder
run: ls -R ./dist/
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,7 @@ Projects/
.DS_Store
Thumbs.db
*.bak"

# Ignore pyinstaller directories
build
dist
1 change: 1 addition & 0 deletions .python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.11.x
21 changes: 21 additions & 0 deletions pyinstaller/Writingway
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/bin/bash

# Get the directory of this script
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

# Set additional required environment variables
export KMP_DUPLICATE_LIB_OK=TRUE

echo "Starting Writingway from $SCRIPT_DIR. This can take a while..."
echo ""
echo "Notes:"
echo "- If you close this window Writingway will exit immediately"
echo "- Your projects are saved in $SCRIPT_DIR/Projects"
echo ""

# Set the working directory so assets and configuration can be found.
cd "$SCRIPT_DIR" || exit 1

# Start the Writingway application
./main

122 changes: 122 additions & 0 deletions pyinstaller/Writingway_osx.spec
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# .spec file for use with pyinstaller.
# This creates a standalone distribution of Writingway with python and all dependencies included
#
# Use:
# 1. Install Writingway first (e.g. run setup_writingway.sh)
# 2. Activate the venv
# 3. Install pyinstaller: pip install pyinstaller
# 4. Run pyinstaller: pyinstaller Writingway.spec
# 5. The output is in the dist folder.
import shutil

from PyInstaller.building.api import COLLECT
from PyInstaller.building.datastruct import Tree
from pathlib import Path
from PyInstaller.utils.hooks import collect_all
import os

ApplicationName = 'Writingway'

# Additional files to add to the _internal directory
datas = [
]

binaries = []
hiddenimports = ['tiktoken_ext.openai_public', 'tiktoken_ext', 'pyaudio']
tmp_ret = collect_all('cmudict')
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]


a = Analysis(
['../main.py'],
pathex=[],
binaries=binaries,
datas=datas,
hiddenimports=hiddenimports,
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
noarchive=False,
optimize=0,
)
pyz = PYZ(a.pure)

# Get the target_arch from the environment. For OSX valid options are "arm64", "x86_64" and "universal2"
target_arch=os.environ.get('TARGET_ARCH')
if target_arch is not None:
print(f"TARGET_ARCH={target_arch}")

exe = EXE(
pyz,
a.scripts,
[],
exclude_binaries=True,
name='main', # Name of the executable
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
console=True,
disable_windowed_traceback=False,
argv_emulation=False,
codesign_identity=None,
entitlements_file=None,
# If the TARGET_ARCH environment variable is set, add a target_arch to this call to EXE()

target_arch=target_arch
# contents_directory='_internal'
)

coll = COLLECT(
exe,
a.binaries,
a.datas,
strip=False,
upx=True,
upx_exclude=[],
name=ApplicationName,
)

# Files to add to the root (i.e. where the executable is) instead of the _internal directory
root_files = [
# Project
'MyFirstProject_structure.json',
'project_settings.json',
'prompts_MyFirstProject.json',

# App version
'version.json',

# Documentation
'README.md',
'Writingway_Introduction.docx',
'Writingway_TLDR.docx',
]

# Create a root_tree array of the files to add to the collection: [ (dest, source, DATA), ... ]
# Add the Writingway assets directory
root_tree = Tree('assets', 'assets')
# Add the startup script
root_tree += [('Writingway', 'pyinstaller/Writingway', 'DATA')]
# Add files from the Writingway root that we want to ship. We're in a subdirectory, so we need to add '../'
for f in root_files:
root_tree += [(f, f, 'DATA')]

# I Can't find a way to let pyinstaller handle the copying, so copy the files manually.
dist_path = Path(DISTPATH) / ApplicationName

for dest_name, source_path, _ in root_tree:
src_path = Path(source_path)
dst_path = dist_path / dest_name

if src_path.exists():
# Create destination directory if needed
dst_path.parent.mkdir(parents=True, exist_ok=True)

if src_path.is_file():
shutil.copy2(src_path, dst_path)

print(f"Copied {src_path} to {dst_path}")
else:
print(f"Warning: File not found: {src_path}")