Skip to content
Merged
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
77 changes: 51 additions & 26 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,16 @@ on:

jobs:
# ── macOS arm64 + x64 (matrix) ──────────────────────────────────────────────
# ── macOS arm64 ─────────────────────────────────────────────────────────────
# We ship arm64 only on macOS. Apple Silicon shipped in 2020; users on Intel
# Macs without Rosetta 2 are vanishingly rare for a hobby project. The
# GitHub-hosted macos-13 (Intel) free runner queue is currently 1h+, which
# blocked PRs for hours; building on macos-14 (arm64) keeps CI predictable.
# If we ever need x64, build it offline from a developer machine and attach
# to the release manually, or revisit the matrix.
# ── macOS arm64 (static, ad-hoc signed, zip + dmg) ──────────────────────────
# SDL2/Freetype/libpng are statically linked into cardputer-emu (see
# EMU_MACOS_STATIC_DEPS in CMakeLists.txt) and SDL2_image is gone, so the
# bundle ships NO third-party dylibs — only our own libAPPLaunch/libUserDemo.
# Everything is ad-hoc codesigned and the quarantine xattr is stripped, so a
# downloaded build opens without the "Apple cannot verify libSDL2_image…"
# Gatekeeper prompt that used to fire 22 times (once per bundled dylib).
#
# arm64 only: Apple Silicon shipped in 2020; building x64 needs the slow,
# frequently-1h+-queued macos-13 Intel runner. Build x64 offline if needed.
macos-arm64:
name: macos-arm64
runs-on: macos-14
Expand All @@ -26,13 +29,13 @@ jobs:
- name: Install dependencies
run: |
brew update >/dev/null
brew install sdl2 sdl2_image freetype pkg-config
- name: Build
brew install sdl2 freetype libpng pkg-config
- name: Build (static deps)
run: |
mkdir build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release
cmake .. -DCMAKE_BUILD_TYPE=Release -DEMU_MACOS_STATIC_DEPS=ON
make -j$(sysctl -n hw.ncpu)
- name: Bundle dylibs
- name: Assemble bundle + ad-hoc sign
run: |
set -euxo pipefail
DIST=dist/M5CardputerZero-Emulator
Expand All @@ -41,23 +44,41 @@ jobs:
cp -r build/apps "$DIST/"
cp -r build/assets "$DIST/"
cp -r build/share "$DIST/"
# Copy non-system dylib deps next to the binary and rewrite rpaths.
if ! command -v dylibbundler >/dev/null 2>&1; then
brew install dylibbundler

# Sanity: the executable must carry no third-party dylib references.
echo "== otool -L cardputer-emu =="
otool -L "$DIST/cardputer-emu"
if otool -L "$DIST/cardputer-emu" | grep -E '/opt/homebrew|/usr/local' ; then
echo "::error::cardputer-emu still references Homebrew dylibs — static link failed"
exit 1
fi
dylibbundler -od -b \
-x "$DIST/cardputer-emu" \
-x "$DIST/apps/libAPPLaunch.dylib" \
-x "$DIST/apps/libUserDemo.dylib" \
-d "$DIST/libs" -p "@executable_path/libs/" || \
echo "[warn] dylibbundler returned non-zero (continuing — deps may resolve already)"
- name: Package

# Ad-hoc codesign every Mach-O so Gatekeeper sees a valid (if
# unidentified) signature instead of "damaged / cannot verify".
for f in "$DIST/cardputer-emu" "$DIST"/apps/*.dylib; do
codesign --force --sign - --timestamp=none "$f"
codesign --verify --verbose "$f"
done
- name: Package — green zip
run: |
set -euxo pipefail
# Strip the quarantine xattr so first launch is prompt-free, then zip.
xattr -cr dist/M5CardputerZero-Emulator
(cd dist && zip -ry ../M5CardputerZero-Emulator-macOS-arm64.zip M5CardputerZero-Emulator)
- name: Package — DMG
run: |
cd dist && tar czf ../M5CardputerZero-Emulator-macOS-arm64.tar.gz M5CardputerZero-Emulator
set -euxo pipefail
# Plain folder-style DMG: drag the whole folder out and run.
hdiutil create -volname "M5CardputerZero Emulator" \
-srcfolder dist/M5CardputerZero-Emulator \
-ov -format UDZO \
M5CardputerZero-Emulator-macOS-arm64.dmg
- uses: actions/upload-artifact@v4
with:
name: emulator-macos-arm64
path: M5CardputerZero-Emulator-macOS-arm64.tar.gz
path: |
M5CardputerZero-Emulator-macOS-arm64.zip
M5CardputerZero-Emulator-macOS-arm64.dmg

# ── Windows x64 (MinGW-w64) ────────────────────────────────────────────────
windows:
Expand Down Expand Up @@ -206,13 +227,17 @@ jobs:
--notes "Automated pre-release from commit ${GITHUB_SHA:0:7}

**Downloads:**
- macOS arm64: \`M5CardputerZero-Emulator-macOS-arm64.tar.gz\`
- macOS arm64 (DMG): \`M5CardputerZero-Emulator-macOS-arm64.dmg\`
- macOS arm64 (green zip): \`M5CardputerZero-Emulator-macOS-arm64.zip\`
- Linux x64: \`M5CardputerZero-Emulator-Linux-x64.tar.gz\`
- Windows x64 (MinGW): \`M5CardputerZero-Emulator-Windows-x64.zip\`
- Web (WASM): \`M5CardputerZero-Emulator-Web.zip\`
- Playground (hosted): \`M5CardputerZero-Playground.zip\`" \
- Playground (hosted): \`M5CardputerZero-Playground.zip\`

_macOS builds are statically linked and ad-hoc signed — they open without Gatekeeper prompts._" \
--prerelease \
artifacts/emulator-macos-arm64/M5CardputerZero-Emulator-macOS-arm64.tar.gz \
artifacts/emulator-macos-arm64/M5CardputerZero-Emulator-macOS-arm64.dmg \
artifacts/emulator-macos-arm64/M5CardputerZero-Emulator-macOS-arm64.zip \
artifacts/emulator-linux-x64/M5CardputerZero-Emulator-Linux-x64.tar.gz \
artifacts/emulator-windows-x64/M5CardputerZero-Emulator-Windows-x64.zip \
artifacts/emulator-web/M5CardputerZero-Emulator-Web.zip \
Expand Down
104 changes: 67 additions & 37 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,8 @@ endif()
if(WIN32 AND NOT EMU_SKIP_APPLAUNCH)
# Windows: statically link APPLaunch (avoids DLL duplicate LVGL globals)
add_executable(cardputer-emu
src/main.cpp src/device_skin.cpp src/emu_launcher_stubs.c
src/main.cpp src/device_skin.cpp
src/emu_launcher_stubs.c src/png_to_sdl_surface.c
${APPLAUNCH_UI_C} ${APPLAUNCH_UI_CPP} ${APPLAUNCH_KBD}
${APPLAUNCH_HAL_SDL_C} ${APPLAUNCH_HAL_SDL_CPP})
target_include_directories(cardputer-emu PRIVATE
Expand All @@ -216,11 +217,13 @@ if(WIN32 AND NOT EMU_SKIP_APPLAUNCH)
target_link_options(cardputer-emu PRIVATE -Wl,--allow-multiple-definition)
elseif(WIN32)
# Fallback: UserDemo only
add_executable(cardputer-emu src/main.cpp src/device_skin.cpp ${USERDEMO_UI_C})
add_executable(cardputer-emu
src/main.cpp src/device_skin.cpp src/png_to_sdl_surface.c
${USERDEMO_UI_C})
target_include_directories(cardputer-emu PRIVATE ${USERDEMO_INCLUDES})
target_compile_definitions(cardputer-emu PRIVATE EMU_STATIC_APP=1)
else()
add_executable(cardputer-emu src/main.cpp src/device_skin.cpp)
add_executable(cardputer-emu src/main.cpp src/device_skin.cpp src/png_to_sdl_surface.c)
endif()
add_dependencies(cardputer-emu lvgl lvgl_thorvg)
# Export all symbols so dlopen'd apps can resolve LVGL functions
Expand Down Expand Up @@ -259,39 +262,67 @@ else()
)
endif()

# SDL2
if(TARGET SDL2::SDL2)
target_link_libraries(cardputer-emu PRIVATE SDL2::SDL2)
elseif(TARGET SDL2::SDL2-static)
target_link_libraries(cardputer-emu PRIVATE SDL2::SDL2-static)
else()
target_include_directories(cardputer-emu PRIVATE ${SDL2_INCLUDE_DIRS})
target_link_libraries(cardputer-emu PRIVATE ${SDL2_LIBRARIES})
endif()
if(TARGET SDL2::SDL2main)
target_link_libraries(cardputer-emu PRIVATE SDL2::SDL2main)
endif()
# Note: we no longer link SDL2_image. PNG loading goes through
# src/png_to_sdl_surface.c, which delegates to LVGL's bundled lodepng. This
# drops 19 transitive dylibs from the macOS bundle (libavif, libjxl, libwebp,
# libtiff, etc.) and the Gatekeeper "unable to verify" prompt that fires on
# every one of them at first launch.

# SDL2_image
if(PkgConfig_FOUND)
pkg_check_modules(SDL2_IMAGE SDL2_image)
endif()
if(SDL2_IMAGE_FOUND)
target_include_directories(cardputer-emu PRIVATE ${SDL2_IMAGE_INCLUDE_DIRS})
target_link_libraries(cardputer-emu PRIVATE ${SDL2_IMAGE_LINK_LIBRARIES})
target_link_directories(cardputer-emu PRIVATE ${SDL2_IMAGE_LIBRARY_DIRS})
else()
target_link_directories(cardputer-emu PRIVATE /opt/homebrew/lib)
target_link_libraries(cardputer-emu PRIVATE SDL2_image)
endif()
# ── SDL2 + Freetype linkage ──────────────────────────────────────────────────
# On macOS we statically link SDL2/Freetype/libpng into the executable so the
# distributed bundle carries NO third-party dylibs at all. Combined with
# dropping SDL2_image above, the only Mach-O files we ship are cardputer-emu
# (static, self-contained) plus our own libAPPLaunch/libUserDemo dylibs — all
# of which get ad-hoc codesigned in CI. Result: zero Gatekeeper quarantine
# prompts. The static .a files live next to the dylibs in Homebrew.
option(EMU_MACOS_STATIC_DEPS "Statically link SDL2/Freetype on macOS" ON)

# Freetype (needed by LVGL)
if(FREETYPE_FOUND AND FREETYPE_LINK_LIBRARIES)
target_link_libraries(cardputer-emu PRIVATE ${FREETYPE_LINK_LIBRARIES})
target_link_directories(cardputer-emu PRIVATE ${FREETYPE_LIBRARY_DIRS})
if(APPLE AND EMU_MACOS_STATIC_DEPS)
# Resolve Homebrew prefixes (arm64 default, Intel fallback).
foreach(_pfx /opt/homebrew /usr/local)
if(EXISTS "${_pfx}/opt/sdl2/lib/libSDL2.a")
set(_brew "${_pfx}")
break()
endif()
endforeach()
if(NOT _brew)
message(FATAL_ERROR "EMU_MACOS_STATIC_DEPS: libSDL2.a not found under Homebrew. "
"Run `brew install sdl2 freetype libpng` or set -DEMU_MACOS_STATIC_DEPS=OFF.")
endif()
set(_sdl2_a ${_brew}/opt/sdl2/lib/libSDL2.a)
set(_ft_a ${_brew}/opt/freetype/lib/libfreetype.a)
set(_png_a ${_brew}/opt/libpng/lib/libpng16.a)
target_link_libraries(cardputer-emu PRIVATE
${_sdl2_a} ${_ft_a} ${_png_a} bz2 z
# Frameworks required by static SDL2 (from `pkg-config --static --libs sdl2`)
"-framework CoreAudio" "-framework AudioToolbox"
"-weak_framework CoreHaptics" "-weak_framework GameController"
"-framework ForceFeedback" objc
"-framework CoreVideo" "-framework Cocoa" "-framework Carbon"
"-framework IOKit" "-weak_framework QuartzCore" "-weak_framework Metal"
"-weak_framework UniformTypeIdentifiers"
)
else()
target_link_directories(cardputer-emu PRIVATE /opt/homebrew/lib)
target_link_libraries(cardputer-emu PRIVATE freetype)
# Dynamic linkage (Linux, Windows, or macOS with -DEMU_MACOS_STATIC_DEPS=OFF)
if(TARGET SDL2::SDL2)
target_link_libraries(cardputer-emu PRIVATE SDL2::SDL2)
elseif(TARGET SDL2::SDL2-static)
target_link_libraries(cardputer-emu PRIVATE SDL2::SDL2-static)
else()
target_include_directories(cardputer-emu PRIVATE ${SDL2_INCLUDE_DIRS})
target_link_libraries(cardputer-emu PRIVATE ${SDL2_LIBRARIES})
endif()
if(TARGET SDL2::SDL2main)
target_link_libraries(cardputer-emu PRIVATE SDL2::SDL2main)
endif()
# Freetype (needed by LVGL)
if(FREETYPE_FOUND AND FREETYPE_LINK_LIBRARIES)
target_link_libraries(cardputer-emu PRIVATE ${FREETYPE_LINK_LIBRARIES})
target_link_directories(cardputer-emu PRIVATE ${FREETYPE_LIBRARY_DIRS})
else()
target_link_directories(cardputer-emu PRIVATE /opt/homebrew/lib)
target_link_libraries(cardputer-emu PRIVATE freetype)
endif()
endif()

# dl (for dlopen) + threads
Expand Down Expand Up @@ -338,6 +369,7 @@ if(EMSCRIPTEN)
src/main_web.cpp
src/device_skin.cpp
src/emu_launcher_stubs.c
src/png_to_sdl_surface.c
${APPLAUNCH_UI_C}
${APPLAUNCH_UI_CPP}
${APPLAUNCH_KBD}
Expand All @@ -364,13 +396,11 @@ if(EMSCRIPTEN)

target_link_libraries(cardputer-emu-web PRIVATE lvgl)

# Emscripten SDL2 + SDL2_image ports
target_compile_options(cardputer-emu-web PRIVATE -sUSE_SDL=2 -sUSE_SDL_IMAGE=2 -pthread)
# Emscripten SDL2 (PNG decoding goes through lodepng — see png_to_sdl_surface.c)
target_compile_options(cardputer-emu-web PRIVATE -sUSE_SDL=2 -pthread)
target_link_options(cardputer-emu-web PRIVATE
-sUSE_SDL=2
-sUSE_SDL_IMAGE=2
-pthread -sPROXY_TO_PTHREAD=0
-sSDL2_IMAGE_FORMATS=["png"]
-sALLOW_MEMORY_GROWTH=1
-sINITIAL_MEMORY=67108864
"--preload-file=${CMAKE_CURRENT_SOURCE_DIR}/assets/device_skin.png@/assets/device_skin.png"
Expand Down
8 changes: 3 additions & 5 deletions src/main.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#include "lvgl/lvgl.h"
#include <SDL2/SDL.h>
#include <SDL2/SDL_image.h>
#include "png_to_sdl_surface.h"
#include <cstdio>
#include <cstdlib>
#include <cstring>
Expand Down Expand Up @@ -302,7 +302,6 @@ int main(int argc, char *argv[])
printf("========================================\n");

SDL_Init(SDL_INIT_VIDEO);
IMG_Init(IMG_INIT_PNG);

int win_w = (int)(SKIN_W * SCALE), win_h = (int)(SKIN_H * SCALE);
SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "best");
Expand All @@ -319,8 +318,8 @@ int main(int argc, char *argv[])
printf("[EMU] Window: %dx%d Renderer: %dx%d DPI scale: %.1f\n",
win_w, win_h, render_w, render_h, g_dpi_scale);

SDL_Surface *surf = IMG_Load("assets/device_skin.png");
if (!surf) { fprintf(stderr, "skin: %s\n", IMG_GetError()); return 1; }
SDL_Surface *surf = load_png_as_sdl_surface("assets/device_skin.png");
if (!surf) { fprintf(stderr, "skin: failed to load assets/device_skin.png\n"); return 1; }
g_skin_tex = SDL_CreateTextureFromSurface(g_ren, surf);
SDL_SetTextureBlendMode(g_skin_tex, SDL_BLENDMODE_BLEND);
SDL_FreeSurface(surf);
Expand Down Expand Up @@ -442,7 +441,6 @@ int main(int argc, char *argv[])
SDL_DestroyTexture(g_skin_tex);
SDL_DestroyRenderer(g_ren);
SDL_DestroyWindow(g_win);
IMG_Quit();
SDL_Quit();
return 0;
}
7 changes: 3 additions & 4 deletions src/main_web.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

#include "lvgl/lvgl.h"
#include <SDL2/SDL.h>
#include <SDL2/SDL_image.h>
#include "png_to_sdl_surface.h"
#include <cstdio>
#include <cstdlib>
#include <cstring>
Expand Down Expand Up @@ -207,7 +207,6 @@ int main(int, char*[]) {
printf("M5CardputerZero Emulator (Web)\n");

SDL_Init(SDL_INIT_VIDEO);
IMG_Init(IMG_INIT_PNG);

// Use skin native resolution for crisp rendering
g_win=SDL_CreateWindow("M5CardputerZero",
Expand All @@ -216,8 +215,8 @@ int main(int, char*[]) {
g_ren=SDL_CreateRenderer(g_win,-1,SDL_RENDERER_ACCELERATED);
SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "best");

SDL_Surface *surf=IMG_Load("assets/device_skin.png");
if(!surf){printf("skin load failed: %s\n",IMG_GetError());return 1;}
SDL_Surface *surf=load_png_as_sdl_surface("assets/device_skin.png");
if(!surf){printf("skin load failed\n");return 1;}
g_skin_tex=SDL_CreateTextureFromSurface(g_ren,surf);
SDL_SetTextureBlendMode(g_skin_tex,SDL_BLENDMODE_BLEND);
SDL_FreeSurface(surf);
Expand Down
45 changes: 45 additions & 0 deletions src/png_to_sdl_surface.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* png_to_sdl_surface.c — see header. Uses LVGL's bundled lodepng directly
* (the same one used by LV_USE_LODEPNG=1), so no extra dependency is added.
*/
#include "png_to_sdl_surface.h"

#include <stdio.h>
#include <stdlib.h>

#include "lvgl/src/libs/lodepng/lodepng.h"

SDL_Surface *load_png_as_sdl_surface(const char *path)
{
unsigned char *rgba = NULL;
unsigned w = 0, h = 0;
unsigned err = lodepng_decode32_file(&rgba, &w, &h, path);
if (err) {
fprintf(stderr, "[png] lodepng_decode32_file(%s): %u %s\n",
path, err, lodepng_error_text(err));
return NULL;
}

/* lodepng outputs RGBA in memory order R,G,B,A — matches SDL_PIXELFORMAT_RGBA32. */
SDL_Surface *surf = SDL_CreateRGBSurfaceWithFormat(
0, (int)w, (int)h, 32, SDL_PIXELFORMAT_RGBA32);
if (!surf) {
fprintf(stderr, "[png] SDL_CreateRGBSurfaceWithFormat: %s\n", SDL_GetError());
free(rgba);
return NULL;
}

/* Copy row-by-row in case SDL added pitch padding. */
if (surf->pitch == (int)(w * 4)) {
SDL_memcpy(surf->pixels, rgba, (size_t)w * h * 4u);
} else {
for (unsigned y = 0; y < h; ++y) {
SDL_memcpy((unsigned char *)surf->pixels + (size_t)y * surf->pitch,
rgba + (size_t)y * w * 4u,
(size_t)w * 4u);
}
}

free(rgba);
return surf;
}
23 changes: 23 additions & 0 deletions src/png_to_sdl_surface.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* png_to_sdl_surface.h — load a PNG file into an SDL_Surface using LVGL's
* bundled lodepng decoder. Replaces SDL_image, which dragged in 19 transitive
* dylibs (libavif/libjxl/libwebp/etc.) and triggered macOS Gatekeeper
* quarantine prompts on first launch for every single one.
*
* Returns NULL on failure. Caller owns the surface and must SDL_FreeSurface().
* The surface is RGBA32, suitable for SDL_CreateTextureFromSurface() with
* BLEND mode.
*/
#pragma once

#include <SDL2/SDL.h>

#ifdef __cplusplus
extern "C" {
#endif

SDL_Surface *load_png_as_sdl_surface(const char *path);

#ifdef __cplusplus
}
#endif
Loading