diff --git a/CLAUDE.md b/CLAUDE.md index 7405ab7..0a9d0b0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -23,10 +23,10 @@ to `master` — that stays in sync with upstream via `git rebase upstream/master ## Current phase -**Phase 2 — runtime integration** (active). **Phase 1 #1–#5 complete** (2026-05-22). +**Phase 3 — .app bundle & distribution** (active). Phase 1 + Phase 2 complete (2026-05-25). Check [open macOS issues](https://github.com/braggpd/PathOfBuilding-SimpleGraphic/issues?q=label%3Amacos-port+is%3Aopen) -and **`MACOS_PORT.md` → “Next session — close #8”** for the handoff plan. +and **`MACOS_PORT.md` §Phase 3** for current status. ### Status snapshot (2026-05-25) diff --git a/CMakeLists.txt b/CMakeLists.txt index bcb027f..853aae5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -263,22 +263,34 @@ target_link_libraries(SimpleGraphic if (APPLE) set(PoB_HOST_TARGET pob-host) + # Bundle dir name is driven by OUTPUT_NAME, not BUNDLE_NAME. Keep in sync. + set(MACOS_APP_NAME "Path of Building-PoE2") + set(MACOS_FW_DEST "${MACOS_APP_NAME}.app/Contents/Frameworks") add_executable(${PoB_HOST_TARGET} mac/host.cpp) set_target_properties(${PoB_HOST_TARGET} PROPERTIES OUTPUT_NAME "Path of Building-PoE2" + MACOSX_BUNDLE TRUE + MACOSX_BUNDLE_BUNDLE_NAME "${MACOS_APP_NAME}" + MACOSX_BUNDLE_GUI_IDENTIFIER "com.braggpd.PathOfBuilding2" + MACOSX_BUNDLE_BUNDLE_VERSION "0.15.0" + MACOSX_BUNDLE_SHORT_VERSION_STRING "0.15.0" + MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_SOURCE_DIR}/mac/Info.plist.in" + BUILD_RPATH "@executable_path;@loader_path" + INSTALL_RPATH "@executable_path/../Frameworks" + MACOSX_RPATH ON ) target_link_libraries(${PoB_HOST_TARGET} PRIVATE SimpleGraphic) - set_target_properties(SimpleGraphic ${PoB_HOST_TARGET} + set_target_properties(SimpleGraphic PROPERTIES BUILD_RPATH "@executable_path;@loader_path" - INSTALL_RPATH "@executable_path;@loader_path" + INSTALL_RPATH "@loader_path" MACOSX_RPATH ON ) endif () if (APPLE) - install(TARGETS SimpleGraphic LIBRARY DESTINATION ".") - install(TARGETS ${PoB_HOST_TARGET} RUNTIME DESTINATION ".") + install(TARGETS ${PoB_HOST_TARGET} BUNDLE DESTINATION ".") + install(TARGETS SimpleGraphic LIBRARY DESTINATION "${MACOS_FW_DEST}") else () install(TARGETS SimpleGraphic RUNTIME DESTINATION ".") endif () @@ -336,7 +348,7 @@ if (APPLE) set_target_properties(lcurl PROPERTIES PREFIX "" SUFFIX ".so") endif () if (APPLE) - install(TARGETS lcurl LIBRARY DESTINATION ".") + install(TARGETS lcurl LIBRARY DESTINATION "${MACOS_FW_DEST}") else () install(TARGETS lcurl RUNTIME DESTINATION ".") endif () @@ -363,7 +375,7 @@ if (APPLE) set_target_properties(lua-utf8 PROPERTIES PREFIX "" SUFFIX ".so") endif () if (APPLE) - install(TARGETS lua-utf8 LIBRARY DESTINATION ".") + install(TARGETS lua-utf8 LIBRARY DESTINATION "${MACOS_FW_DEST}") else () install(TARGETS lua-utf8 RUNTIME DESTINATION ".") endif () @@ -411,7 +423,7 @@ if (APPLE) set_target_properties(luasocket PROPERTIES PREFIX "" SUFFIX ".so") endif () if (APPLE) - install(TARGETS luasocket LIBRARY DESTINATION ".") + install(TARGETS luasocket LIBRARY DESTINATION "${MACOS_FW_DEST}") else () install(TARGETS luasocket RUNTIME DESTINATION ".") endif () @@ -435,17 +447,18 @@ if (APPLE) set_target_properties(lzip PROPERTIES PREFIX "" SUFFIX ".so") endif () if (APPLE) - install(TARGETS lzip LIBRARY DESTINATION ".") + install(TARGETS lzip LIBRARY DESTINATION "${MACOS_FW_DEST}") else () install(TARGETS lzip RUNTIME DESTINATION ".") endif () install(FILES $ DESTINATION ".") if (APPLE) + # Lua modules sit in Contents/Frameworks/ next to SimpleGraphic; @loader_path finds siblings. set_target_properties(lcurl luasocket lzip lua-utf8 PROPERTIES BUILD_RPATH "@executable_path;@loader_path" - INSTALL_RPATH "@executable_path;@loader_path" + INSTALL_RPATH "@loader_path" MACOSX_RPATH ON ) @@ -456,6 +469,8 @@ if (APPLE) set(MACOS_BUNDLE_SOCKET "${CMAKE_CURRENT_BINARY_DIR}/socket.so") set(MACOS_BUNDLE_LZIP "${CMAKE_CURRENT_BINARY_DIR}/lzip.so") set(MACOS_BUNDLE_UTF8 "${CMAKE_CURRENT_BINARY_DIR}/lua-utf8.so") + # MACOS_FW_DEST (the relative path) is substituted into the script at configure time; + # CMAKE_INSTALL_PREFIX is resolved at install time when the script runs. set(_macos_bundle_script "${CMAKE_CURRENT_BINARY_DIR}/macos_bundle_runtime.cmake") configure_file( "${CMAKE_CURRENT_SOURCE_DIR}/cmake/macos_bundle_runtime.cmake" @@ -463,4 +478,18 @@ if (APPLE) @ONLY ) install(SCRIPT "${_macos_bundle_script}") + + # Ad-hoc sign the bundle so Gatekeeper allows it (#11). + # Users launching for the first time may need to right-click → Open. + # \${CMAKE_INSTALL_PREFIX} is escaped so it resolves at install time, not configure time. + install(CODE " + execute_process( + COMMAND codesign --deep --force --sign - + \"\${CMAKE_INSTALL_PREFIX}/${MACOS_APP_NAME}.app\" + RESULT_VARIABLE _codesign_result + ) + if(NOT _codesign_result EQUAL 0) + message(WARNING \"codesign returned \${_codesign_result} — bundle may not be trusted\") + endif() + ") endif () diff --git a/MACOS_PORT.md b/MACOS_PORT.md index 6e199eb..ce24d8f 100644 --- a/MACOS_PORT.md +++ b/MACOS_PORT.md @@ -340,7 +340,7 @@ First full Main load may take **1–3+ minutes** — do not kill early. ### Tasks -- [ ] **3.1** Add CPack/CMake `.app` bundle config in SimpleGraphic's `CMakeLists.txt` · [#10](https://github.com/braggpd/PathOfBuilding-SimpleGraphic/issues/10) +- [x] **3.1** Add CPack/CMake `.app` bundle config in SimpleGraphic's `CMakeLists.txt` · [#10](https://github.com/braggpd/PathOfBuilding-SimpleGraphic/issues/10) ```cmake if (APPLE) @@ -354,12 +354,13 @@ First full Main load may take **1–3+ minutes** — do not kill early. endif() ``` -- [ ] **3.2** Code signing strategy · [#11](https://github.com/braggpd/PathOfBuilding-SimpleGraphic/issues/11) +- [x] **3.2** Code signing strategy · [#11](https://github.com/braggpd/PathOfBuilding-SimpleGraphic/issues/11) - Initial release: ad-hoc signing (`codesign --deep --force --sign -`) Users see a first-launch warning; right-click → Open bypasses it. - Long-term: shared Apple Developer ID ($99/yr) funded by donations. Required for full notarization and no Gatekeeper warnings. + - Implemented as a CMake `install(CODE ...)` step after the bundle is assembled. - [ ] **3.3** DMG packaging · [#12](https://github.com/braggpd/PathOfBuilding-SimpleGraphic/issues/12) @@ -579,3 +580,9 @@ Target command: `brew install --cask path-of-building-2` (5) Subscript `package.path` — added `runtime/lua/` so background threads can `require("xml")`. Also: manifest.xml version parsing from C; `launch._isMacOS` platform flag; update check disabled. **#8 success criteria met:** UI renders, passive tree loads, mouse tracks, text displays. +- **2026-05-25** — **Phase 3.1 + 3.2: `.app` bundle + ad-hoc signing.** `MACOSX_BUNDLE TRUE` on + `pob-host`; `mac/Info.plist.in` added; all dylibs install to `Contents/Frameworks/`; RPATHs + updated (`@executable_path/../Frameworks` for binary, `@loader_path` for dylibs); + `cmake/macos_bundle_runtime.cmake` copies vcpkg deps to `Frameworks/`; `codesign --deep + --force --sign -` added as final install step. `mac/host.cpp` auto-discovers `Launch.lua` + by walking up from the executable (handles both `.app` layout and flat dev layout). diff --git a/cmake/macos_bundle_runtime.cmake b/cmake/macos_bundle_runtime.cmake index f90ea03..8b0b6f6 100644 --- a/cmake/macos_bundle_runtime.cmake +++ b/cmake/macos_bundle_runtime.cmake @@ -1,8 +1,11 @@ -# Installed by CMake install(SCRIPT) on APPLE — copies vcpkg runtime dylibs next to PoB binaries. -# CMAKE_INSTALL_PREFIX and paths below are substituted at configure time. +# Installed by CMake install(SCRIPT) on APPLE — copies vcpkg runtime dylibs into +# the .app bundle's Contents/Frameworks/. @...@ tokens are substituted at configure +# time; ${CMAKE_INSTALL_PREFIX} is resolved at install time. set(_sg_lib_dir "@MACOS_BUNDLE_LIB_DIR@") -set(_install_prefix "${CMAKE_INSTALL_PREFIX}") +# @MACOS_FW_DEST@ = relative bundle path (configure-time constant). +# CMAKE_INSTALL_PREFIX = actual install root (install-time). +set(_fw_dir "${CMAKE_INSTALL_PREFIX}/@MACOS_FW_DEST@") set(_built_libs "@MACOS_BUNDLE_SG_DYLIB@" "@MACOS_BUNDLE_LCURL@" @@ -30,9 +33,9 @@ endif () foreach (_dep IN LISTS _resolved) get_filename_component(_dep_name "${_dep}" NAME) - set(_dest "${_install_prefix}/${_dep_name}") + set(_dest "${_fw_dir}/${_dep_name}") if (NOT EXISTS "${_dest}") - file(INSTALL "${_dep}" DESTINATION "${_install_prefix}" FOLLOW_SYMLINK_CHAIN) + file(INSTALL "${_dep}" DESTINATION "${_fw_dir}" FOLLOW_SYMLINK_CHAIN) message(STATUS "Bundled runtime: ${_dep_name}") endif () endforeach () @@ -44,8 +47,8 @@ foreach (_pair IN ITEMS ) list(GET _pair 0 _egl_link) list(GET _pair 1 _egl_target) - set(_egl_target_path "${_install_prefix}/${_egl_target}") - set(_egl_link_path "${_install_prefix}/${_egl_link}") + set(_egl_target_path "${_fw_dir}/${_egl_target}") + set(_egl_link_path "${_fw_dir}/${_egl_link}") if (EXISTS "${_egl_target_path}" AND NOT EXISTS "${_egl_link_path}") file(CREATE_LINK "${_egl_target}" "${_egl_link_path}" SYMBOLIC) message(STATUS "EGL alias: ${_egl_link} -> ${_egl_target}") diff --git a/mac/Info.plist.in b/mac/Info.plist.in new file mode 100644 index 0000000..cbc753f --- /dev/null +++ b/mac/Info.plist.in @@ -0,0 +1,25 @@ + + + + + CFBundleExecutable + Path of Building-PoE2 + CFBundleIdentifier + com.braggpd.PathOfBuilding2 + CFBundleName + ${MACOSX_BUNDLE_BUNDLE_NAME} + CFBundlePackageType + APPL + CFBundleShortVersionString + ${MACOSX_BUNDLE_SHORT_VERSION_STRING} + CFBundleVersion + ${MACOSX_BUNDLE_BUNDLE_VERSION} + LSMinimumSystemVersion + 13.0 + NSHighResolutionCapable + + NSPrincipalClass + NSApplication + + diff --git a/mac/host.cpp b/mac/host.cpp index 1183fe9..ff96dc4 100644 --- a/mac/host.cpp +++ b/mac/host.cpp @@ -1,10 +1,47 @@ -// Path of Building 2 — macOS host launcher (dev / runtime-macos) +// Path of Building 2 — macOS host launcher // Loads libSimpleGraphic.dylib by linking at build time; same role as // Path of Building-PoE2.exe + SimpleGraphic.dll on Windows. +#include +#include +#include +#include + extern "C" int RunLuaFileAsWin(int argc, char** argv); +// Walk up from the executable looking for src/Launch.lua. +// Works for both .app bundle layout (exe in Contents/MacOS/) and +// flat runtime-macos/ dev layout (exe alongside src/). +static std::string findDefaultScript() +{ + char raw[PATH_MAX]; + uint32_t size = sizeof(raw); + if (_NSGetExecutablePath(raw, &size) != 0) return {}; + char resolved[PATH_MAX]; + if (!realpath(raw, resolved)) return {}; + + std::string dir(resolved); + for (int i = 0; i < 6; ++i) { + auto slash = dir.rfind('/'); + if (slash == std::string::npos) break; + dir = dir.substr(0, slash); + std::string candidate = dir + "/src/Launch.lua"; + if (FILE* f = fopen(candidate.c_str(), "r")) { + fclose(f); + return candidate; + } + } + return {}; +} + int main(int argc, char** argv) { + if (argc < 2) { + std::string script = findDefaultScript(); + if (!script.empty()) { + char* newArgv[] = { argv[0], script.data(), nullptr }; + return RunLuaFileAsWin(2, newArgv); + } + } return RunLuaFileAsWin(argc, argv); } diff --git a/ui_api.cpp b/ui_api.cpp index 4c3335f..078d57b 100644 --- a/ui_api.cpp +++ b/ui_api.cpp @@ -2601,7 +2601,11 @@ int ui_main_c::InitAPI(lua_State* L) old_path += ";lua/?.lua;lua/?/init.lua"; #if __APPLE__ && __MACH__ // Dev layout: host in runtime-macos/, shared scripts in ../runtime/lua/ (see PoB #8). + // .app bundle layout: executable is in Contents/MacOS/; go 4 levels up to reach the + // directory containing the .app, then find runtime/lua/. auto runtime_lua_dir = (ui->sys->basePath / ".." / "runtime" / "lua").lexically_normal(); + if (!std::filesystem::exists(runtime_lua_dir)) + runtime_lua_dir = (ui->sys->basePath / ".." / ".." / ".." / ".." / "runtime" / "lua").lexically_normal(); old_path += ";" + (runtime_lua_dir / "?.lua").generic_string(); old_path += ";" + (runtime_lua_dir / "?" / "init.lua").generic_string(); #endif diff --git a/ui_main.cpp b/ui_main.cpp index 176c44f..b09670d 100644 --- a/ui_main.cpp +++ b/ui_main.cpp @@ -1604,6 +1604,9 @@ void ui_main_c::ScriptInit() lua_getfield(L, -1, "preload"); for (const auto& ext : kExts) { auto soPath = (sys->basePath / ext.file).lexically_normal(); + // .app bundle layout: .so files are in Contents/Frameworks/, not Contents/MacOS/. + if (!std::filesystem::exists(soPath)) + soPath = (sys->basePath / ".." / "Frameworks" / ext.file).lexically_normal(); void* handle = dlopen(soPath.generic_u8string().c_str(), RTLD_NOW | RTLD_LOCAL); if (handle) { auto fn = reinterpret_cast(dlsym(handle, ext.sym)); @@ -1631,8 +1634,11 @@ void ui_main_c::ScriptInit() // sha1.common has no require(); safe before LIGHTFUNC install. (#8) { - auto const commonLua = + auto commonLua = (sys->basePath / ".." / "runtime" / "lua" / "sha1" / "common.lua").lexically_normal(); + // .app bundle layout fallback: executable is in Contents/MacOS/, so go 4 levels up. + if (!std::filesystem::exists(commonLua)) + commonLua = (sys->basePath / ".." / ".." / ".." / ".." / "runtime" / "lua" / "sha1" / "common.lua").lexically_normal(); if (!mac_preload_lua_file(L, sys, "sha1.common", commonLua)) { sys->con->Printf("Warning: macOS sha1.common preload failed.\n"); }