Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
21e3b66
Add AGENTS.md with cloud-specific development instructions
cursoragent May 4, 2026
e1b64f4
docs: fix SQLite spelling in TodoApp moduledoc
cursoragent May 4, 2026
218c22e
AGENTS.md: clarify assets.deploy vs dev watchers, lint env, Git LFS
cursoragent May 4, 2026
9e337d4
Clarify AGENTS.md: DB path, dev run row, Android note
cursoragent May 4, 2026
b637d2e
Fix CI: skip desktop installer for mobile releases; install NSIS for …
cursoragent May 4, 2026
4abd6f1
AGENTS.md: note CI uses different Elixir/OTP than .tool-versions
cursoragent May 4, 2026
6407399
Android run_mix: mkdir assets dir before writing app.zip
cursoragent May 4, 2026
4eb7f1a
docs(AGENTS): clarify prod asset build and npm install step
cursoragent May 4, 2026
8575b39
CI (Windows): use WSL Ubuntu 22.04 for NSIS 3; extend job timeout
cursoragent May 4, 2026
a928006
CI (Windows): use Chocolatey NSIS + WSL wrapper for makensis.exe
cursoragent May 4, 2026
2fb7a1b
CI (Windows): retry apt-get in WSL when mirrors are unreachable
cursoragent May 4, 2026
af21421
CI (Windows): xcopy OpenSSL to C:\OpenSSL-Win64 (avoid move access de…
cursoragent May 4, 2026
d868ac0
CI (Windows): raise binary job timeout to 300 minutes
cursoragent May 4, 2026
b7279b8
CI (macOS): use macos-15 runner instead of deprecated macos-13
cursoragent May 5, 2026
8814182
CI (macOS): use Homebrew libpng for wxWidgets (fix fp.h on macOS 15)
cursoragent May 5, 2026
a1fbcb7
CI (Windows): avoid Git/LFS checkout stalls; shallow clones; 20m job cap
cursoragent May 5, 2026
3c86b88
CI (macOS): point wx configure at Homebrew libpng (CPPFLAGS/LDFLAGS)
cursoragent May 5, 2026
9347681
CI (macOS): Homebrew libtiff + resilient shallow wxWidgets clone
cursoragent May 5, 2026
4b77da7
CI (macOS): install asdf via Homebrew (v0.19 has no asdf.sh)
cursoragent May 5, 2026
458475e
CI (macOS): fix KERL CXX flags for asdf-erlang on Apple Clang
cursoragent May 5, 2026
02ac245
CI (macOS): do not pass MACOS_PEM into Build Release on CI
cursoragent May 5, 2026
0246702
CI (Windows): raise job timeout to 90 minutes
cursoragent May 5, 2026
2735ec1
CI (Windows): extend job timeout to 180 minutes
cursoragent May 5, 2026
42ffd11
CI (Windows): use maximum job timeout (360 minutes)
cursoragent May 5, 2026
90e8ee9
CI (Windows): cache deps + Hex/Mix homes; faster Hex downloads
cursoragent May 6, 2026
5d60996
fix(ci): speed Windows mix deps with shallow git clones and prod-only…
cursoragent May 6, 2026
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
185 changes: 149 additions & 36 deletions .github/workflows/binaries.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,42 +12,88 @@ env:
jobs:
windows:
runs-on: windows-latest
timeout-minutes: 90
# GitHub-hosted jobs allow up to 360 minutes; Windows mix/npm/installer can exceed 180 on slow runs.
timeout-minutes: 360
defaults:
run:
shell: wsl-bash {0}
env:
# Repo uses Git LFS for Android *.a etc.; smudge on NTFS/WSL often stalls after fetch.
GIT_LFS_SKIP_SMUDGE: "1"
name: Build Erlang/OTP (Windows)
steps:
- name: Restore Windows Cache
uses: actions/cache/restore@v3
id: win32-cache
with:
path: "c:\\opt\\otp.exe"
key: win32-wxwidgets-${{ env.WXWIDGETS_VERSION }}-otp-${{ env.OTP_VERSION }}
key: win32-wxwidgets-${{ env.WXWIDGETS_VERSION }}-otp-${{ env.OTP_VERSION }}-v4

- uses: Vampire/setup-wsl@v2
if: steps.win32-cache.outputs.cache-hit != 'true'
with:
distribution: Ubuntu-18.04
# Ubuntu 22.04 for up-to-date userland; OTP's installer_win32 invokes makensis.exe with
# Windows-style flags, so we install NSIS for Windows and expose it via /usr/local/bin/makensis.exe.
distribution: Ubuntu-22.04

- name: Install WSL dependencies
if: steps.win32-cache.outputs.cache-hit != 'true'
run: apt update && apt install -y g++-mingw-w64 gcc-mingw-w64 make autoconf unzip
run: |
set -euo pipefail
export DEBIAN_FRONTEND=noninteractive
for attempt in 1 2 3 4 5; do
if apt-get update && apt-get install -y g++-mingw-w64 gcc-mingw-w64 make autoconf unzip; then
break
fi
echo "apt-get failed (attempt $attempt), retrying in 45s..." >&2
sleep 45
if [ "$attempt" -eq 5 ]; then
echo "apt-get install failed after 5 attempts" >&2
exit 1
fi
done

- name: Install openssl
- name: Install openssl and NSIS (Windows)
if: steps.win32-cache.outputs.cache-hit != 'true'
shell: cmd
run: |
choco install openssl --version=1.1.1.2100
IF EXIST "c:\\Program Files\\OpenSSL-Win64" (move "c:\\Program Files\\OpenSSL-Win64" "c:\\OpenSSL-Win64") ELSE (move "c:\\Program Files\\OpenSSL" "c:\\OpenSSL-Win64")
choco install openssl --version=1.1.1.2100 -y
choco install nsis -y
rem Copy instead of move — Program Files paths are often locked on runners (move fails with Access denied).
if exist "C:\\Program Files\\OpenSSL-Win64" (
xcopy "C:\\Program Files\\OpenSSL-Win64" "C:\\OpenSSL-Win64\\" /E /I /H /Y /Q
) else if exist "C:\\Program Files\\OpenSSL" (
xcopy "C:\\Program Files\\OpenSSL" "C:\\OpenSSL-Win64\\" /E /I /H /Y /Q
)

- name: Wire Windows NSIS into WSL PATH
if: steps.win32-cache.outputs.cache-hit != 'true'
run: |
NSIS="/mnt/c/Program Files (x86)/NSIS/makensis.exe"
if [ ! -f "$NSIS" ]; then
NSIS="/mnt/c/Program Files/NSIS/makensis.exe"
fi
if [ ! -f "$NSIS" ]; then
echo "Windows NSIS makensis.exe not found after choco install" >&2
exit 1
fi
install -d /usr/local/bin
# otp_build runs makensis.exe /V2 ... — Windows NSIS accepts those flags; Linux makensis does not.
printf '%s\n' '#!/bin/sh' "exec \"$NSIS\" \"\$@\"" > /usr/local/bin/makensis.exe
chmod +x /usr/local/bin/makensis.exe

- name: Download wxWidgets
if: steps.win32-cache.outputs.cache-hit != 'true'
run: |
git clone ${{ env.WXWIDGETS_REPO }}
set -euo pipefail
cd "${GITHUB_WORKSPACE}"
# Shallow clone + stall limits avoid hangs on slow/stuck HTTP after "Compressing objects".
git config --global http.lowSpeedLimit 500
git config --global http.lowSpeedTime 120
git config --global http.postBuffer 524288000
git clone --depth 1 --branch ${{ env.WXWIDGETS_VERSION }} --single-branch ${{ env.WXWIDGETS_REPO }} wxWidgets
cd wxWidgets
git checkout ${{ env.WXWIDGETS_VERSION }}
git submodule update --init
git submodule update --init --depth 1
sed -i -r -e 's/wxUSE_POSTSCRIPT +0/wxUSE_POSTSCRIPT 1/' include/wx/msw/setup.h
sed -i -r -e 's/wxUSE_WEBVIEW_EDGE +0/wxUSE_WEBVIEW_EDGE 1/' include/wx/msw/setup.h
sed -i -r -e 's/WXWIN_COMPATIBILITY_3_0 +0/WXWIN_COMPATIBILITY_3_0 1/' include/wx/msw/setup.h
Expand Down Expand Up @@ -77,9 +123,10 @@ jobs:
- name: Compile Erlang
if: steps.win32-cache.outputs.cache-hit != 'true'
run: |
git clone ${{ env.OTP_GITHUB_URL }}
set -euo pipefail
cd "${GITHUB_WORKSPACE}"
git clone --depth 1 --branch ${{ env.OTP_VERSION }} --single-branch ${{ env.OTP_GITHUB_URL }} otp
cd otp
git checkout ${{ env.OTP_VERSION }}
export ERL_TOP=`pwd`
export MAKEFLAGS=-j$(($(nproc) + 2))
export ERLC_USE_SERVER=true
Expand All @@ -96,7 +143,7 @@ jobs:
uses: actions/cache/save@v3
with:
path: "c:\\opt\\otp.exe"
key: win32-wxwidgets-${{ env.WXWIDGETS_VERSION }}-otp-${{ env.OTP_VERSION }}
key: win32-wxwidgets-${{ env.WXWIDGETS_VERSION }}-otp-${{ env.OTP_VERSION }}-v4

- name: Run Erlang installer
shell: cmd
Expand Down Expand Up @@ -124,20 +171,52 @@ jobs:
shell: msys2 {0}
run: |
cd $HOME
git clone https://github.com/elixir-lang/elixir.git
git clone --depth 1 --branch v${{ env.ELIXIR_VERSION }} --single-branch https://github.com/elixir-lang/elixir.git
cd elixir
git checkout v${{ env.ELIXIR_VERSION }}
make
echo export PATH=\"\$PATH:$HOME/elixir/bin\" >> $HOME/.bashrc

- uses: actions/checkout@v1
# Do not run Git LFS smudge: rel/android *.a are huge and stall on Windows I/O after fetch.
- uses: actions/checkout@v4
with:
lfs: false
fetch-depth: 1

- name: Restore Mix / Hex cache
id: mix-cache
uses: actions/cache/restore@v4
with:
path: |
deps
.ci_mix_home
.ci_hex_home
key: windows-mix-${{ hashFiles('mix.lock') }}-elixir-${{ env.ELIXIR_VERSION }}

- name: "Get dependencies"
shell: msys2 {0}
run: |
set -euo pipefail
export MIX_HOME="$GITHUB_WORKSPACE/.ci_mix_home"
export HEX_HOME="$GITHUB_WORKSPACE/.ci_hex_home"
export HEX_HTTP_CONCURRENCY=8
export GIT_TERMINAL_PROMPT=0
git config --global http.lowSpeedLimit 500
git config --global http.lowSpeedTime 120
mkdir -p "$MIX_HOME" "$HEX_HOME"
mix local.hex --force
mix local.rebar --force
mix deps.get
# Installer/release uses prod; skip dev/test-only deps to shrink Hex + git work.
MIX_ENV=prod mix deps.get --only prod

- name: Save Mix / Hex cache
if: steps.mix-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@v4
with:
path: |
deps
.ci_mix_home
.ci_hex_home
key: windows-mix-${{ hashFiles('mix.lock') }}-elixir-${{ env.ELIXIR_VERSION }}

- name: "npm install"
shell: msys2 {0}
Expand All @@ -152,6 +231,10 @@ jobs:
WIN32_KEY_PASS: ${{ secrets.WIN32_KEY_PASS }}
shell: msys2 {0}
run: |
set -euo pipefail
export MIX_HOME="$GITHUB_WORKSPACE/.ci_mix_home"
export HEX_HOME="$GITHUB_WORKSPACE/.ci_hex_home"
export MIX_ENV=prod
mix assets.deploy
mix desktop.installer

Expand All @@ -163,49 +246,78 @@ jobs:
_build/prod/*.exe

macos:
runs-on: macos-13
runs-on: macos-15
steps:
# asdf v0.19+ is a Go binary from Homebrew; the old git clone + ~/.asdf/asdf.sh flow is invalid.
- name: Install asdf (Homebrew)
run: |
brew install asdf
echo "$(brew --prefix asdf)/bin" >> "$GITHUB_PATH"

- name: macOS Cache
uses: actions/cache@v3
id: macos-cache
with:
path: /Users/runner/.asdf
key: macos-wxwidgets-${{ env.WXWIDGETS_VERSION }}-otp-${{ env.OTP_VERSION }}
key: macos15-wxwidgets-${{ env.WXWIDGETS_VERSION }}-otp-${{ env.OTP_VERSION }}-png-tiff-sys-asdf-brew-kerl-cxx

- name: "Install brew deps"
if: steps.macos-cache.outputs.cache-hit != 'true'
run: |
brew install binutils coreutils wget automake autoconf libtool
brew install binutils coreutils wget automake autoconf libtool libpng libtiff

- name: "Installing wxWidgets"
if: steps.macos-cache.outputs.cache-hit != 'true'
run: |
mkdir ~/projects && cd ~/projects
git clone ${{ env.WXWIDGETS_REPO }}
cd wxWidgets;
git checkout ${{ env.WXWIDGETS_VERSION }}
git submodule update --init
./configure --prefix=/usr/local/wxWidgets --enable-webview --enable-compat30 --disable-shared
set -euo pipefail
mkdir -p ~/projects && cd ~/projects
git config --global http.lowSpeedLimit 500
git config --global http.lowSpeedTime 120
git config --global http.postBuffer 524288000
for attempt in 1 2 3 4 5; do
rm -rf wxWidgets
if git clone --depth 1 --branch ${{ env.WXWIDGETS_VERSION }} ${{ env.WXWIDGETS_REPO }} wxWidgets; then
break
fi
echo "git clone wxWidgets failed (attempt $attempt), retrying..." >&2
if [ "$attempt" -eq 5 ]; then
echo "git clone wxWidgets failed after 5 attempts" >&2
exit 1
fi
sleep $((attempt * 25))
done
cd wxWidgets
git submodule update --init --depth 1
LIBPNG_PREFIX="$(brew --prefix libpng)"
LIBTIFF_PREFIX="$(brew --prefix libtiff)"
export CPPFLAGS="-I${LIBPNG_PREFIX}/include -I${LIBTIFF_PREFIX}/include ${CPPFLAGS:-}"
export LDFLAGS="-L${LIBPNG_PREFIX}/lib -L${LIBTIFF_PREFIX}/lib ${LDFLAGS:-}"
export PKG_CONFIG_PATH="${LIBPNG_PREFIX}/lib/pkgconfig:${LIBTIFF_PREFIX}/lib/pkgconfig:${PKG_CONFIG_PATH:-}"
# Bundled libpng can hit fp.h issues on recent Xcode; use Homebrew libpng/libtiff (keg-only paths above).
./configure --prefix=/usr/local/wxWidgets --enable-webview --enable-compat30 --disable-shared \
--with-libpng=sys --with-libtiff=sys
make -j8

- name: "Installing Erlang"
if: steps.macos-cache.outputs.cache-hit != 'true'
run: |
git clone https://github.com/asdf-vm/asdf.git ~/.asdf
. $HOME/.asdf/asdf.sh
set -euo pipefail
export PATH="${ASDF_DATA_DIR:-$HOME/.asdf}/shims:$PATH"
asdf plugin add erlang
asdf plugin add elixir
asdf plugin add nodejs
echo "erlang ref:${{ env.OTP_VERSION }}" >> .tool-versions
echo "elixir ${{ env.ELIXIR_VERSION }}${{ env.ELIXIR_VARIANT }}" >> .tool-versions
echo "nodejs v18.7.0" >> .tool-versions
export KERL_CONFIGURE_OPTIONS="--enable-parallel-configure --with-wxdir=`echo ~/projects/wxWidgets` --disable-jit --without-javac --disable-debug CXX='gcc -std=c++11'"
echo "erlang ref:${{ env.OTP_VERSION }}" >> ~/.tool-versions
echo "elixir ${{ env.ELIXIR_VERSION }}${{ env.ELIXIR_VARIANT }}" >> ~/.tool-versions
echo "nodejs v18.7.0" >> ~/.tool-versions
# Do not use CXX='gcc -std=c++11' — kerl splits that into CXX=gcc plus a stray -std=c++11 configure arg.
export KERL_CONFIGURE_OPTIONS="--enable-parallel-configure --with-wxdir=$HOME/projects/wxWidgets --disable-jit --without-javac --disable-debug CXX=clang++"
asdf install

- uses: actions/checkout@v1
- name: "Compile and Lint"
run: |
. $HOME/.asdf/asdf.sh
set -euo pipefail
export PATH="${ASDF_DATA_DIR:-$HOME/.asdf}/shims:$PATH"
echo "erlang ref:${{ env.OTP_VERSION }}" > .tool-versions
echo "elixir ${{ env.ELIXIR_VERSION }}${{ env.ELIXIR_VARIANT }}" >> .tool-versions
echo "nodejs v18.7.0" >> .tool-versions
Expand All @@ -216,10 +328,11 @@ jobs:
cd assets && npm install

- name: "Build Release"
env:
MACOS_PEM: ${{ secrets.MACOS_PEM }}
run: |
. $HOME/.asdf/asdf.sh
set -euo pipefail
export PATH="${ASDF_DATA_DIR:-$HOME/.asdf}/shims:$PATH"
# Do not set MACOS_PEM here: if the secret is set but not a valid Developer ID PEM,
# create_keychain maybe still runs the full path and fails import (CI cannot code-sign).
mix desktop.create_keychain maybe
export MACOS_KEYCHAIN="$HOME/Library/Keychains/macos-build.keychain"
export LD_LIBRARY_PATH="$HOME/projects/wxWidgets/lib/"
Expand Down
40 changes: 40 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# AGENTS.md

## Cursor Cloud specific instructions

### Overview

This is a desktop TodoApp built with Elixir, Phoenix LiveView, and the `elixir-desktop` library. It renders UI via a native wxWidgets webview backed by a local Phoenix server on `127.0.0.1:30979`. Data is stored in SQLite under the app config directory (`~/.config/todo/database.sq3` on Unix-like systems; see `TodoApp.config_dir/0` in `lib/todo_app.ex`).

### Running the app

```bash
. "$HOME/.asdf/asdf.sh" && cd /workspace && DISPLAY=:1 iex -S mix
```

The web UI is accessible at http://127.0.0.1:30979/ in a browser (useful for testing without needing the native window).

### Key commands

| Task | Command |
|------|---------|
| Install deps | `mix deps.get` |
| Compile | `mix compile` |
| Lint | `mix lint` |
| Tests | `mix test` |
| Asset build | `MIX_ENV=prod mix assets.deploy` |
| Run app (dev) | Same as [Running the app](#running-the-app): load asdf, set `DISPLAY`, then `iex -S mix` |

`MIX_ENV=prod mix assets.deploy` minifies and digests static assets for production releases. In development, esbuild and dart_sass run as Mix watchers when you start the app with `iex -S mix` (see `mix.exs` `asset_apps/1`).

### Non-obvious notes

- **No external services required.** This is a fully self-contained desktop app — no Postgres, Redis, Docker containers, or external APIs.
- **DBus/EGL warnings are expected** in headless/cloud environments. The app logs errors about DBus (desktop notifications) and EGL (GPU acceleration) but these do not affect functionality.
- **wxWidgets is required** for the desktop window. In headless environments, ensure `DISPLAY=:1` is set and an X server (Xvfb) is running.
- **Version management uses asdf** with `.tool-versions` pinning Erlang 28.4.3, Elixir 1.19.5, and Node.js 22.19.0. GitHub Actions (for example `.github/workflows/ci.yml`) still pins older Elixir/OTP for the default CI job; treat that file as the source of truth for CI, not `.tool-versions`.
- **The `mix lint` alias** runs `compile --warnings-as-errors`, `format --check-formatted`, and `credo --ignore design`. Run it with the default Mix environment (`dev`); Credo is only a dev/test dependency, so `MIX_ENV=prod mix lint` will fail on the Credo step.
- **Git LFS:** `*.a` files under `rel/android/` are tracked with Git LFS. After cloning, if those libraries look like small text files instead of archives, run `git lfs pull` before Android-related builds.
- **No test files exist** in the project currently — `mix test` compiles in the test environment but reports "There are no tests to run".
- **Assets** are built via esbuild (JS) and dart_sass (SCSS), both downloaded automatically by Mix on first run. Run `npm install` in `assets/` before `mix assets.deploy` if you have not yet (see the root `README.md`).
- **Android sample** lives under `rel/android/` (separate Gradle project). It uses a pinned Erlang/Elixir pair for the embedded runtime that can differ from the desktop `.tool-versions`; read `rel/android/README.md` before building an APK.
2 changes: 1 addition & 1 deletion lib/todo_app.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ defmodule TodoApp do
@moduledoc """
TodoApp Application. This module takes care of the the boot.
Because the TodoApp is a standalone desktop application there is
initial Database initialization needed when the SQlite database is
initial Database initialization needed when the SQLite database is
not yet existing. This is done during start() by
calling `TodoApp.Repo.initialize()`.

Expand Down
Loading
Loading