diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5a10670..a150a06 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,6 +1,7 @@ name: Build and Release TLEscope on: + workflow_dispatch: push: branches: [ "main", "master" ] tags: @@ -73,6 +74,31 @@ jobs: path: dist/TLEscope-Linux-arm64-Portable/ retention-days: 14 + build-macos: + name: Build macOS + runs-on: macos-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Install macOS Build Dependencies + run: | + brew install raylib pkg-config + + - name: Compile and Bundle .app + # bundler compiles the icon + ad-hoc signs natively on the runner + run: make app + + - name: Zip .app (preserves bundle structure and exec bits) + run: ditto -c -k --sequesterRsrc --keepParent dist/TLEscope.app TLEscope-macOS.zip + + - name: Upload macOS Artifact + uses: actions/upload-artifact@v4 + with: + name: TLEscope-macOS + path: TLEscope-macOS.zip + retention-days: 14 + build-windows: name: Build Windows runs-on: windows-latest @@ -162,7 +188,7 @@ jobs: release: name: Publish Release - needs: [build-linux, build-linux-arm64, build-windows, build-windows-arm64] + needs: [build-linux, build-linux-arm64, build-macos, build-windows, build-windows-arm64] if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/v')) runs-on: ubuntu-latest permissions: @@ -191,6 +217,8 @@ jobs: # Move installer out of its artifact subdirectory for flat publishing mv TLEscope-Installer/TLEscope-Installer.exe ../ + # macOS artifact is already a ditto-zipped .app, just move it up + mv TLEscope-macOS/TLEscope-macOS.zip ../ cd .. - name: Clean Existing Release Assets @@ -223,6 +251,7 @@ jobs: files: | TLEscope-Linux-Portable.zip TLEscope-Linux-arm64-Portable.zip + TLEscope-macOS.zip TLEscope-Win-Portable.zip TLEscope-Win-arm64-Portable.zip TLEscope-Installer.exe @@ -235,6 +264,7 @@ jobs: files: | TLEscope-Linux-Portable.zip TLEscope-Linux-arm64-Portable.zip + TLEscope-macOS.zip TLEscope-Win-Portable.zip TLEscope-Win-arm64-Portable.zip TLEscope-Installer.exe diff --git a/.gitignore b/.gitignore index 7fc34d1..73ee818 100644 --- a/.gitignore +++ b/.gitignore @@ -13,5 +13,8 @@ dist/ # Compoile Commands CLANGD /compile_commands.json +# mac finder junk +.DS_Store + # csv exports *.csv \ No newline at end of file diff --git a/Makefile b/Makefile index 29320b3..95dcfc3 100644 --- a/Makefile +++ b/Makefile @@ -42,12 +42,16 @@ APP_DIR ?= /usr/share/applications # macOS (Apple Silicon / Intel) CC_MACOS = clang -RAYLIB_CFLAGS = $(shell pkg-config --cflags raylib 2>/dev/null) -RAYLIB_LIBS = $(shell pkg-config --libs raylib 2>/dev/null) +empty := +space := $(empty) $(empty) +MACOS_PC_PATH := $(subst $(space),:,$(strip $(wildcard /opt/homebrew/lib/pkgconfig /usr/local/lib/pkgconfig))) +MACOS_PKG_CONFIG_ENV = PKG_CONFIG_PATH="$(PKG_CONFIG_PATH):$(MACOS_PC_PATH)" +RAYLIB_CFLAGS = $(shell $(MACOS_PKG_CONFIG_ENV) pkg-config --cflags raylib 2>/dev/null) +RAYLIB_LIBS = $(shell $(MACOS_PKG_CONFIG_ENV) pkg-config --libs raylib 2>/dev/null) LDFLAGS_MACOS = $(RAYLIB_LIBS) -lcurl -framework IOKit -framework Cocoa -framework OpenGL DIST_MACOS = dist/TLEscope-macOS-Portable -.PHONY: all linux macos windows windows-arm64 win-installer clean build bin install uninstall raylib raylib-crossbuild +.PHONY: all linux macos app windows windows-arm64 win-installer clean build bin install uninstall raylib raylib-crossbuild all: linux @@ -68,7 +72,11 @@ macos: bin/TLEscope-macos cp logo*.png $(DIST_MACOS)/ 2>/dev/null || true @echo "macOS build bundled in $(DIST_MACOS)/" @echo "Run it with: cd $(DIST_MACOS)/ && ./TLEscope" - @echo "To make a MacOS bundle: ./macos_bundle.sh" + @echo "To make a proper .app bundle: make app" + +# proper .app bundle: compiles the icon, generates Info.plist, codesigns +app: bin/TLEscope-macos + ./scripts/macos-bundle.sh windows: bin/TLEscope.exe @mkdir -p $(DIST_WIN) @@ -103,7 +111,7 @@ bin/TLEscope: $(OBJ) | bin $(CC_LINUX) $(CFLAGS) -o $@ $^ $(LDFLAGS_LIN) bin/TLEscope-macos: $(SRC) | bin - @if ! pkg-config --exists raylib 2>/dev/null; then echo "Error: raylib not found. Install with: brew install raylib"; exit 1; fi + @if ! $(MACOS_PKG_CONFIG_ENV) pkg-config --exists raylib 2>/dev/null; then echo "Error: raylib not found. Install with: brew install raylib"; exit 1; fi $(CC_MACOS) $(CFLAGS) $(RAYLIB_CFLAGS) -o $@ $^ $(LDFLAGS_MACOS) bin/TLEscope.exe: $(SRC) | bin diff --git a/README.md b/README.md index a059cbb..d886514 100644 --- a/README.md +++ b/README.md @@ -94,8 +94,11 @@ brew install raylib pkg-config git clone https://github.com/aweeri/TLEscope cd TLEscope make macos # Results in dist/TLEscope-macOS-Portable/ +make app # Results in dist/TLEscope.app ``` +`make app` additionally bundles everything into a proper `TLEscope.app`: it compiles `AppIcon.icns` from `logo.png`, generates an `Info.plist` with the git version baked in, and ad-hoc codesigns the bundle (required for Apple Silicon to run at all). To sign with a real identity instead, set `CODE_SIGN_IDENTITY="Developer ID Application: ..."` before bundling. The app must be built on macOS itself - the icon and signing steps use native tooling. + **Windows systems** Install [MSYS2](https://www.msys2.org/), then run the following in a UCRT64 or MINGW64 terminal: ``` diff --git a/bundle/Contents/MacOS/TLEscope b/bundle/Contents/MacOS/TLEscope deleted file mode 100644 index 9462a9b..0000000 --- a/bundle/Contents/MacOS/TLEscope +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash -# this workaround reeks probably please change it later -cd "$(dirname "$0")"/../Resources -../MacOS/TLEscope.bin diff --git a/bundle/Contents/Resources/AppIcon.icns b/bundle/Contents/Resources/AppIcon.icns deleted file mode 100644 index 83ce2dc..0000000 Binary files a/bundle/Contents/Resources/AppIcon.icns and /dev/null differ diff --git a/macos_bundle.sh b/macos_bundle.sh deleted file mode 100644 index 4aa49a3..0000000 --- a/macos_bundle.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash -cp -r bundle dist/TLEscope.app -cp -r dist/TLEscope-macOS-Portable/* dist/TLEscope.app/Contents/Resources -mv dist/TLEscope.app/Contents/Resources/TLEscope dist/TLEscope.app/Contents/MacOS/TLEscope.bin -chmod +x dist/TLEscope.app/Contents/MacOS/TLEscope # jus to be safe diff --git a/scripts/macos-bundle.sh b/scripts/macos-bundle.sh new file mode 100755 index 0000000..09134f6 --- /dev/null +++ b/scripts/macos-bundle.sh @@ -0,0 +1,93 @@ +#!/usr/bin/env bash +# bundles a freshly built macOS binary into a proper TLEscope.app +# +# does everything natively on the build machine: +# - compiles AppIcon.icns from logo.png (sips + iconutil, no precompiled icon) +# - generates Info.plist with the real git version baked in +# - ad-hoc codesigns by default (arm64 binaries refuse to run unsigned) +# +# usage: scripts/macos-bundle.sh [binary] default binary: bin/TLEscope-macos +# +# env vars: +# CODE_SIGN_IDENTITY set to e.g. "Developer ID Application: Your Name (TEAMID)" +# for real signing. default is "-" (ad-hoc). +set -euo pipefail + +APP_NAME="TLEscope" +BUNDLE_ID="com.aweeri.tlescope" +BIN="${1:-bin/${APP_NAME}-macos}" +APP="dist/${APP_NAME}.app" + +GIT_VERSION=$(git describe --tags --always --dirty 2>/dev/null || echo "vUnknown") + +for tool in sips iconutil codesign; do + command -v "$tool" >/dev/null || { echo "error: '$tool' not found - this script must run on macOS"; exit 1; } +done +[ -x "$BIN" ] || { echo "error: $BIN not found - run 'make macos' first"; exit 1; } + +rm -rf "$APP" +mkdir -p "$APP/Contents/MacOS" "$APP/Contents/Resources" + +# portable resource layout lives inside Resources (the binary chdirs there) +cp -r themes "$APP/Contents/Resources/themes" +cp logo.png logo_l.png "$APP/Contents/Resources/" +cp settings.json "$APP/Contents/Resources/" 2>/dev/null || true +cp data.tle "$APP/Contents/Resources/" 2>/dev/null || true +cp "$BIN" "$APP/Contents/MacOS/$APP_NAME" + +# compile a multi-resolution .icns from logo.png +ICONSET_DIR="$(mktemp -d)" +ICONSET="$ICONSET_DIR/${APP_NAME}.iconset" +mkdir -p "$ICONSET" +for size in 16 32 128 256 512; do + double=$((size * 2)) + sips -z "$size" "$size" logo.png --out "$ICONSET/icon_${size}x${size}.png" >/dev/null + sips -z "$double" "$double" logo.png --out "$ICONSET/icon_${size}x${size}@2x.png" >/dev/null +done +iconutil -c icns "$ICONSET" -o "$APP/Contents/Resources/AppIcon.icns" +rm -rf "$ICONSET_DIR" + +cat > "$APP/Contents/Info.plist" < + + + + CFBundleExecutable + ${APP_NAME} + CFBundleIconFile + AppIcon + CFBundleIdentifier + ${BUNDLE_ID} + CFBundleName + ${APP_NAME} + CFBundleDisplayName + ${APP_NAME} + CFBundlePackageType + APPL + CFBundleShortVersionString + ${GIT_VERSION} + CFBundleVersion + ${GIT_VERSION} + NSHighResolutionCapable + + LSMinimumSystemVersion + 11.0 + NSHumanReadableCopyright + See LICENSE + + +EOF + +# signing: ad-hoc by default so the app actually launches on Apple Silicon. +# with CODE_SIGN_IDENTITY set we also opt into hardened runtime (needed for notarization). +# note: settings.json lives inside Resources, so editing it in-place after +# signing invalidates the signature - Gatekeeper only cares on first launch tho +if [ "${CODE_SIGN_IDENTITY:--}" = "-" ]; then + echo "ad-hoc signing $APP (set CODE_SIGN_IDENTITY for real signing)" + codesign --force --deep --sign - "$APP" +else + codesign --force --deep --sign "$CODE_SIGN_IDENTITY" --options runtime "$APP" +fi +codesign --verify --strict "$APP" + +echo "built $APP (${GIT_VERSION})" diff --git a/src/main.c b/src/main.c index ae4fbbb..4b3c080 100644 --- a/src/main.c +++ b/src/main.c @@ -6,6 +6,28 @@ #include #include +#if defined(__APPLE__) +#include +#include +#include +/* launchd starts .app bundles with cwd=/ but all our assets (themes, + * settings.json, logo.png, ...) load relative to cwd, so when the binary + * lives inside a bundle hop over to Contents/Resources. no wrapper needed */ +static void BundleChdir(void) +{ + char exe[4096]; + uint32_t size = sizeof(exe); + if (_NSGetExecutablePath(exe, &size) != 0) + return; + char *contents = strstr(exe, "/Contents/MacOS/"); + if (!contents) + return; + strcpy(contents + strlen("/Contents"), "/Resources"); + if (chdir(exe) != 0) + fprintf(stderr, "warning: could not chdir into %s\n", exe); +} +#endif + #include "astro.h" #include "config.h" @@ -530,6 +552,10 @@ static bool GetMouseEarthIntersection(Vector2 mouse, bool is_2d, Camera2D cam2d, int main(void) { +#if defined(__APPLE__) + BundleChdir(); +#endif + LoadAppConfig("settings.json", &cfg); /* window setup and msaa */