mirror of
https://github.com/Nexoniarz/Fry3DX.git
synced 2026-11-03 04:24:26 +00:00
macOS (Apple silicon and Intel): Fry3DX.app with its content in Resources and MoltenVK in Frameworks, OpenGL 4.1 when there's no Vulkan; the program's own path, memory, OS and CPU names on a Mac; files dropped on the window (and opened from the Finder) open; presets macos-arm64/macos-x64, a CI job, tools/package.py macos makes one universal app
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
5c28e3da4e
commit
e22cd2e3e6
15 changed files with 273 additions and 40 deletions
30
.github/workflows/build.yml
vendored
30
.github/workflows/build.yml
vendored
|
|
@ -1,4 +1,5 @@
|
||||||
# Builds Fry3DX for Linux (a portable .tar.gz and an AppImage), Windows (.zip) and Android (.apk).
|
# Builds Fry3DX for Linux (a portable .tar.gz and an AppImage), Windows (.zip), macOS (.zip with one
|
||||||
|
# app for Apple silicon and Intel) and Android (.apk).
|
||||||
# A tag v* (git tag v0.4.0 && git push --tags) also makes a GitHub release with them;
|
# A tag v* (git tag v0.4.0 && git push --tags) also makes a GitHub release with them;
|
||||||
# otherwise they're the run's artifacts (Actions tab).
|
# otherwise they're the run's artifacts (Actions tab).
|
||||||
# Android signing: secrets FRY_KEYSTORE_B64 (the keystore, base64) and FRY_KEYSTORE_PASS. Keep
|
# Android signing: secrets FRY_KEYSTORE_B64 (the keystore, base64) and FRY_KEYSTORE_PASS. Keep
|
||||||
|
|
@ -53,6 +54,31 @@ jobs:
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
with: { name: windows, path: "dist/*" }
|
with: { name: windows, path: "dist/*" }
|
||||||
|
|
||||||
|
macos:
|
||||||
|
# Built twice (Apple silicon, and Intel cross-built on the same Mac), then joined into one app.
|
||||||
|
runs-on: macos-15
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Tools
|
||||||
|
run: |
|
||||||
|
brew install ninja shaderc
|
||||||
|
# MoltenVK (Vulkan on Metal), for both kinds of Mac, carried inside the app.
|
||||||
|
curl -sfL https://github.com/KhronosGroup/MoltenVK/releases/download/v1.3.0/MoltenVK-macos.tar | tar -x -C "$RUNNER_TEMP"
|
||||||
|
MVK=$(find "$RUNNER_TEMP" -name libMoltenVK.dylib -path '*macOS*' | head -1)
|
||||||
|
lipo -info "$MVK"
|
||||||
|
echo "MVK=$MVK" >> "$GITHUB_ENV"
|
||||||
|
- name: Build
|
||||||
|
run: |
|
||||||
|
for arch in arm64 x64; do
|
||||||
|
cmake --preset macos-$arch -DFRY_MOLTENVK="$MVK"
|
||||||
|
cmake --build build/macos-$arch
|
||||||
|
done
|
||||||
|
cmake --build build/macos-arm64 --target check
|
||||||
|
- name: Package
|
||||||
|
run: python3 tools/package.py macos
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
with: { name: macos, path: "dist/*" }
|
||||||
|
|
||||||
android:
|
android:
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
|
|
@ -77,7 +103,7 @@ jobs:
|
||||||
|
|
||||||
release:
|
release:
|
||||||
if: startsWith(github.ref, 'refs/tags/v')
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
needs: [linux, windows, android]
|
needs: [linux, windows, macos, android]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions: { contents: write }
|
permissions: { contents: write }
|
||||||
steps:
|
steps:
|
||||||
|
|
|
||||||
|
|
@ -30,18 +30,26 @@ set(CMAKE_CXX_EXTENSIONS OFF)
|
||||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||||
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
|
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
|
||||||
|
|
||||||
|
# Where the content goes: next to the program; on a Mac, inside the app (Fry3DX.app/Contents/
|
||||||
|
# Resources, where SDL_GetBasePath points).
|
||||||
|
if(APPLE)
|
||||||
|
set(FRY_CONTENT_DIR ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/Fry3DX.app/Contents/Resources)
|
||||||
|
else()
|
||||||
|
set(FRY_CONTENT_DIR ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
|
||||||
|
endif()
|
||||||
|
|
||||||
include(cmake/Dependencies.cmake)
|
include(cmake/Dependencies.cmake)
|
||||||
include(cmake/Shaders.cmake)
|
include(cmake/Shaders.cmake)
|
||||||
|
|
||||||
add_subdirectory(engine)
|
add_subdirectory(engine)
|
||||||
add_subdirectory(src)
|
add_subdirectory(src)
|
||||||
|
|
||||||
# Content next to the program: bin/engine/ (the engine's: fonts, sky, config); bin/Fry3DX/ (the app's own, none yet).
|
# Content next to the program: engine/ (the engine's: fonts, sky, config); Fry3DX/ (the app's own: scripts, branding).
|
||||||
add_custom_target(content ALL
|
add_custom_target(content ALL
|
||||||
COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_SOURCE_DIR}/content ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}
|
COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_SOURCE_DIR}/content ${FRY_CONTENT_DIR}
|
||||||
COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/engine/fonts ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/Fry3DX
|
COMMAND ${CMAKE_COMMAND} -E make_directory ${FRY_CONTENT_DIR}/engine/fonts ${FRY_CONTENT_DIR}/Fry3DX
|
||||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different ${dejavu_SOURCE_DIR}/ttf/DejaVuSans.ttf ${dejavu_SOURCE_DIR}/ttf/DejaVuSans-Bold.ttf
|
COMMAND ${CMAKE_COMMAND} -E copy_if_different ${dejavu_SOURCE_DIR}/ttf/DejaVuSans.ttf ${dejavu_SOURCE_DIR}/ttf/DejaVuSans-Bold.ttf
|
||||||
${dejavu_SOURCE_DIR}/LICENSE ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/engine/fonts/
|
${dejavu_SOURCE_DIR}/LICENSE ${FRY_CONTENT_DIR}/engine/fonts/
|
||||||
COMMENT "Copying content" VERBATIM)
|
COMMENT "Copying content" VERBATIM)
|
||||||
|
|
||||||
enable_testing()
|
enable_testing()
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,10 @@
|
||||||
{ "name": "portable", "inherits": "base", "cacheVariables": { "CMAKE_BUILD_TYPE": "Release", "FRY_PORTABLE": "ON" } },
|
{ "name": "portable", "inherits": "base", "cacheVariables": { "CMAKE_BUILD_TYPE": "Release", "FRY_PORTABLE": "ON" } },
|
||||||
{ "name": "windows", "inherits": "base", "toolchainFile": "${sourceDir}/cmake/toolchains/windows-msvc.cmake",
|
{ "name": "windows", "inherits": "base", "toolchainFile": "${sourceDir}/cmake/toolchains/windows-msvc.cmake",
|
||||||
"cacheVariables": { "CMAKE_BUILD_TYPE": "Release", "CMAKE_MSVC_RUNTIME_LIBRARY": "MultiThreaded" } },
|
"cacheVariables": { "CMAKE_BUILD_TYPE": "Release", "CMAKE_MSVC_RUNTIME_LIBRARY": "MultiThreaded" } },
|
||||||
|
{ "name": "macos-arm64", "inherits": "base",
|
||||||
|
"cacheVariables": { "CMAKE_BUILD_TYPE": "Release", "FRY_PORTABLE": "ON", "CMAKE_OSX_ARCHITECTURES": "arm64", "CMAKE_OSX_DEPLOYMENT_TARGET": "13.3" } },
|
||||||
|
{ "name": "macos-x64", "inherits": "base",
|
||||||
|
"cacheVariables": { "CMAKE_BUILD_TYPE": "Release", "FRY_PORTABLE": "ON", "CMAKE_OSX_ARCHITECTURES": "x86_64", "CMAKE_OSX_DEPLOYMENT_TARGET": "13.3" } },
|
||||||
{ "name": "android", "inherits": "base", "toolchainFile": "$env{ANDROID_NDK_ROOT}/build/cmake/android.toolchain.cmake",
|
{ "name": "android", "inherits": "base", "toolchainFile": "$env{ANDROID_NDK_ROOT}/build/cmake/android.toolchain.cmake",
|
||||||
"cacheVariables": { "CMAKE_BUILD_TYPE": "Release", "ANDROID_ABI": "arm64-v8a", "ANDROID_PLATFORM": "android-28", "ANDROID_STL": "c++_static" } }
|
"cacheVariables": { "CMAKE_BUILD_TYPE": "Release", "ANDROID_ABI": "arm64-v8a", "ANDROID_PLATFORM": "android-28", "ANDROID_STL": "c++_static" } }
|
||||||
],
|
],
|
||||||
|
|
@ -15,6 +19,8 @@
|
||||||
{ "name": "release", "configurePreset": "release" },
|
{ "name": "release", "configurePreset": "release" },
|
||||||
{ "name": "portable", "configurePreset": "portable" },
|
{ "name": "portable", "configurePreset": "portable" },
|
||||||
{ "name": "windows", "configurePreset": "windows" },
|
{ "name": "windows", "configurePreset": "windows" },
|
||||||
|
{ "name": "macos-arm64", "configurePreset": "macos-arm64" },
|
||||||
|
{ "name": "macos-x64", "configurePreset": "macos-x64" },
|
||||||
{ "name": "android", "configurePreset": "android" }
|
{ "name": "android", "configurePreset": "android" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
0
a.out
Executable file
0
a.out
Executable file
42
cmake/Info.plist.in
Normal file
42
cmake/Info.plist.in
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<!-- Fry3DX.app's Info.plist (filled in by CMake: src/CMakeLists.txt). -->
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleName</key><string>Fry3DX</string>
|
||||||
|
<key>CFBundleDisplayName</key><string>Fry3DX</string>
|
||||||
|
<key>CFBundleExecutable</key><string>Fry3DX</string>
|
||||||
|
<key>CFBundleIdentifier</key><string>io.github.nexoniarz.fry3dx</string>
|
||||||
|
<key>CFBundleIconFile</key><string>fry3dx</string>
|
||||||
|
<key>CFBundlePackageType</key><string>APPL</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key><string>6.0</string>
|
||||||
|
<key>CFBundleShortVersionString</key><string>${PROJECT_VERSION}</string>
|
||||||
|
<key>CFBundleVersion</key><string>${PROJECT_VERSION}</string>
|
||||||
|
<key>NSHumanReadableCopyright</key><string>Copyright 2026 Nexoniarz. Apache License 2.0.</string>
|
||||||
|
<key>LSMinimumSystemVersion</key><string>${CMAKE_OSX_DEPLOYMENT_TARGET}</string>
|
||||||
|
<key>LSApplicationCategoryType</key><string>public.app-category.graphics-design</string>
|
||||||
|
<key>NSHighResolutionCapable</key><true/>
|
||||||
|
<key>NSSupportsAutomaticGraphicsSwitching</key><true/>
|
||||||
|
<!-- .fry files open in it from the Finder (they arrive as a dropped file). -->
|
||||||
|
<key>UTExportedTypeDeclarations</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>UTTypeIdentifier</key><string>io.github.nexoniarz.fry3dx.fry</string>
|
||||||
|
<key>UTTypeDescription</key><string>Fry3DX model</string>
|
||||||
|
<key>UTTypeConformsTo</key><array><string>public.data</string></array>
|
||||||
|
<key>UTTypeIconFile</key><string>fry3dx</string>
|
||||||
|
<key>UTTypeTagSpecification</key>
|
||||||
|
<dict><key>public.filename-extension</key><array><string>fry</string></array></dict>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleDocumentTypes</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeName</key><string>Fry3DX model</string>
|
||||||
|
<key>CFBundleTypeRole</key><string>Editor</string>
|
||||||
|
<key>LSHandlerRank</key><string>Owner</string>
|
||||||
|
<key>LSItemContentTypes</key><array><string>io.github.nexoniarz.fry3dx.fry</string></array>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
#include <filesystem>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
|
|
||||||
|
|
@ -15,6 +16,11 @@
|
||||||
#include <sys/utsname.h>
|
#include <sys/utsname.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#endif
|
#endif
|
||||||
|
#if defined(__APPLE__)
|
||||||
|
#include <mach-o/dyld.h>
|
||||||
|
#include <mach/mach.h>
|
||||||
|
#include <sys/sysctl.h>
|
||||||
|
#endif
|
||||||
#if defined(_MSC_VER)
|
#if defined(_MSC_VER)
|
||||||
#include <intrin.h>
|
#include <intrin.h>
|
||||||
#elif defined(__x86_64__) || defined(__i386__)
|
#elif defined(__x86_64__) || defined(__i386__)
|
||||||
|
|
@ -30,6 +36,14 @@ std::string trim(std::string s) {
|
||||||
return s.substr(i);
|
return s.substr(i);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if defined(__APPLE__)
|
||||||
|
std::string sysctlText(const char* name) {
|
||||||
|
char buf[256] = {};
|
||||||
|
size_t size = sizeof buf - 1;
|
||||||
|
return sysctlbyname(name, buf, &size, nullptr, 0) == 0 ? trim(buf) : std::string();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
// The CPU's own name for itself (x86: cpuid's brand string; elsewhere /proc/cpuinfo).
|
// The CPU's own name for itself (x86: cpuid's brand string; elsewhere /proc/cpuinfo).
|
||||||
std::string cpuName() {
|
std::string cpuName() {
|
||||||
#if defined(_MSC_VER) || defined(__x86_64__) || defined(__i386__)
|
#if defined(_MSC_VER) || defined(__x86_64__) || defined(__i386__)
|
||||||
|
|
@ -46,6 +60,9 @@ std::string cpuName() {
|
||||||
char brand[49] = {};
|
char brand[49] = {};
|
||||||
std::memcpy(brand, regs, 48);
|
std::memcpy(brand, regs, 48);
|
||||||
if (brand[0]) return trim(brand);
|
if (brand[0]) return trim(brand);
|
||||||
|
#endif
|
||||||
|
#if defined(__APPLE__)
|
||||||
|
if (std::string s = sysctlText("machdep.cpu.brand_string"); !s.empty()) return s; // ("Apple M2 Pro" too)
|
||||||
#endif
|
#endif
|
||||||
std::ifstream in("/proc/cpuinfo");
|
std::ifstream in("/proc/cpuinfo");
|
||||||
for (std::string line; std::getline(in, line);)
|
for (std::string line; std::getline(in, line);)
|
||||||
|
|
@ -64,6 +81,11 @@ std::string osName() {
|
||||||
fn(&v);
|
fn(&v);
|
||||||
const char* name = v.dwMajorVersion == 10 ? (v.dwBuildNumber >= 22000 ? "Windows 11" : "Windows 10") : "Windows";
|
const char* name = v.dwMajorVersion == 10 ? (v.dwBuildNumber >= 22000 ? "Windows 11" : "Windows 10") : "Windows";
|
||||||
return std::string(name) + " (build " + std::to_string(v.dwBuildNumber) + ")";
|
return std::string(name) + " (build " + std::to_string(v.dwBuildNumber) + ")";
|
||||||
|
#elif defined(__APPLE__)
|
||||||
|
utsname u{};
|
||||||
|
uname(&u);
|
||||||
|
const std::string v = sysctlText("kern.osproductversion");
|
||||||
|
return "macOS " + (v.empty() ? std::string("?") : v) + " (" + u.machine + ")";
|
||||||
#else
|
#else
|
||||||
utsname u{};
|
utsname u{};
|
||||||
uname(&u);
|
uname(&u);
|
||||||
|
|
@ -95,6 +117,19 @@ Memory memory() {
|
||||||
if (GlobalMemoryStatusEx(&s)) m.total = s.ullTotalPhys, m.used = s.ullTotalPhys - s.ullAvailPhys;
|
if (GlobalMemoryStatusEx(&s)) m.total = s.ullTotalPhys, m.used = s.ullTotalPhys - s.ullAvailPhys;
|
||||||
PROCESS_MEMORY_COUNTERS pmc{sizeof pmc};
|
PROCESS_MEMORY_COUNTERS pmc{sizeof pmc};
|
||||||
if (K32GetProcessMemoryInfo(GetCurrentProcess(), &pmc, sizeof pmc)) m.ours = pmc.WorkingSetSize;
|
if (K32GetProcessMemoryInfo(GetCurrentProcess(), &pmc, sizeof pmc)) m.ours = pmc.WorkingSetSize;
|
||||||
|
#elif defined(__APPLE__)
|
||||||
|
uint64_t total = 0;
|
||||||
|
size_t size = sizeof total;
|
||||||
|
if (sysctlbyname("hw.memsize", &total, &size, nullptr, 0) == 0) m.total = total;
|
||||||
|
vm_statistics64_data_t vm{};
|
||||||
|
mach_msg_type_number_t count = HOST_VM_INFO64_COUNT;
|
||||||
|
if (host_statistics64(mach_host_self(), HOST_VM_INFO64, reinterpret_cast<host_info64_t>(&vm), &count) == KERN_SUCCESS) {
|
||||||
|
const uint64_t page = uint64_t(sysconf(_SC_PAGESIZE)); // (used as Activity Monitor counts it: in use by apps, wired, compressed)
|
||||||
|
m.used = (uint64_t(vm.internal_page_count) - vm.purgeable_count + vm.wire_count + vm.compressor_page_count) * page;
|
||||||
|
}
|
||||||
|
mach_task_basic_info_data_t task{};
|
||||||
|
mach_msg_type_number_t taskCount = MACH_TASK_BASIC_INFO_COUNT;
|
||||||
|
if (task_info(mach_task_self(), MACH_TASK_BASIC_INFO, reinterpret_cast<task_info_t>(&task), &taskCount) == KERN_SUCCESS) m.ours = task.resident_size;
|
||||||
#else
|
#else
|
||||||
uint64_t available = 0;
|
uint64_t available = 0;
|
||||||
std::ifstream in("/proc/meminfo");
|
std::ifstream in("/proc/meminfo");
|
||||||
|
|
@ -114,4 +149,39 @@ Memory memory() {
|
||||||
return m;
|
return m;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string executablePath() {
|
||||||
|
#if defined(_WIN32)
|
||||||
|
char exe[1024] = {};
|
||||||
|
GetModuleFileNameA(nullptr, exe, sizeof exe);
|
||||||
|
return exe;
|
||||||
|
#elif defined(__APPLE__)
|
||||||
|
char exe[4096] = {};
|
||||||
|
uint32_t size = sizeof exe;
|
||||||
|
if (_NSGetExecutablePath(exe, &size) != 0) return {};
|
||||||
|
std::error_code ec;
|
||||||
|
const auto real = std::filesystem::canonical(exe, ec);
|
||||||
|
return ec ? std::string(exe) : real.string();
|
||||||
|
#else
|
||||||
|
std::error_code ec;
|
||||||
|
const auto p = std::filesystem::read_symlink("/proc/self/exe", ec);
|
||||||
|
return ec ? std::string() : p.string();
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
bool debuggerAttached() {
|
||||||
|
#if defined(_WIN32)
|
||||||
|
return IsDebuggerPresent();
|
||||||
|
#elif defined(__APPLE__)
|
||||||
|
kinfo_proc info{};
|
||||||
|
size_t size = sizeof info;
|
||||||
|
int mib[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid()};
|
||||||
|
return sysctl(mib, 4, &info, &size, nullptr, 0) == 0 && (info.kp_proc.p_flag & P_TRACED) != 0;
|
||||||
|
#else
|
||||||
|
std::ifstream f("/proc/self/status");
|
||||||
|
for (std::string line; std::getline(f, line);)
|
||||||
|
if (line.rfind("TracerPid:", 0) == 0) return std::atoi(line.c_str() + 10) != 0;
|
||||||
|
return false;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace keel::sys
|
} // namespace keel::sys
|
||||||
|
|
|
||||||
|
|
@ -18,4 +18,9 @@ struct Memory {
|
||||||
};
|
};
|
||||||
Memory memory();
|
Memory memory();
|
||||||
|
|
||||||
|
// This program's file (for starting it again: crash reports, restarts). Empty if unknown.
|
||||||
|
std::string executablePath();
|
||||||
|
// Is a debugger watching (the freeze watchdog waits then)?
|
||||||
|
bool debuggerAttached();
|
||||||
|
|
||||||
} // namespace keel::sys
|
} // namespace keel::sys
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
#include "crash.h"
|
#include "crash.h"
|
||||||
|
|
||||||
#include <core/log.h>
|
#include <core/log.h>
|
||||||
|
#include <core/sysinfo.h>
|
||||||
|
|
||||||
#include <SDL3/SDL.h>
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
|
@ -122,17 +123,6 @@ void onSignal(int sig) {
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
bool debuggerAttached() {
|
|
||||||
#ifdef _WIN32
|
|
||||||
return IsDebuggerPresent();
|
|
||||||
#else
|
|
||||||
std::ifstream f("/proc/self/status");
|
|
||||||
for (std::string line; std::getline(f, line);)
|
|
||||||
if (line.rfind("TracerPid:", 0) == 0) return std::atoi(line.c_str() + 10) != 0;
|
|
||||||
return false;
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
void watchdog() {
|
void watchdog() {
|
||||||
constexpr double kTimeout = 45.0;
|
constexpr double kTimeout = 45.0;
|
||||||
for (;;) {
|
for (;;) {
|
||||||
|
|
@ -141,7 +131,7 @@ void watchdog() {
|
||||||
g_beat = seconds();
|
g_beat = seconds();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (seconds() - g_beat < kTimeout || debuggerAttached()) continue;
|
if (seconds() - g_beat < kTimeout || sys::debuggerAttached()) continue;
|
||||||
#ifndef _WIN32
|
#ifndef _WIN32
|
||||||
pthread_kill(g_main, SIGUSR2); // its handler reports from the stuck thread (its backtrace)
|
pthread_kill(g_main, SIGUSR2); // its handler reports from the stuck thread (its backtrace)
|
||||||
std::this_thread::sleep_for(std::chrono::seconds(3));
|
std::this_thread::sleep_for(std::chrono::seconds(3));
|
||||||
|
|
@ -153,12 +143,8 @@ void watchdog() {
|
||||||
|
|
||||||
void install(const std::string& prefPath, const std::vector<std::string>& args, bool dog) {
|
void install(const std::string& prefPath, const std::vector<std::string>& args, bool dog) {
|
||||||
g_report = prefPath + "crash.txt";
|
g_report = prefPath + "crash.txt";
|
||||||
#ifdef _WIN32
|
g_exe = sys::executablePath();
|
||||||
char exe[1024] = {};
|
#ifndef _WIN32
|
||||||
GetModuleFileNameA(nullptr, exe, sizeof exe);
|
|
||||||
g_exe = exe;
|
|
||||||
#else
|
|
||||||
g_exe = std::filesystem::read_symlink("/proc/self/exe").string();
|
|
||||||
g_main = pthread_self();
|
g_main = pthread_self();
|
||||||
#endif
|
#endif
|
||||||
g_args = {g_exe, "-crashreport", g_report, "--"};
|
g_args = {g_exe, "-crashreport", g_report, "--"};
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
#include "core/console.h"
|
#include "core/console.h"
|
||||||
#include "core/cvar.h"
|
#include "core/cvar.h"
|
||||||
#include "core/log.h"
|
#include "core/log.h"
|
||||||
|
#include "core/sysinfo.h"
|
||||||
#include "core/time.h"
|
#include "core/time.h"
|
||||||
#include "fs/vfs.h"
|
#include "fs/vfs.h"
|
||||||
#include "platform/input.h"
|
#include "platform/input.h"
|
||||||
|
|
@ -289,6 +290,10 @@ int runClient(Game& game) {
|
||||||
#if defined(__ANDROID__)
|
#if defined(__ANDROID__)
|
||||||
if (first != RenderApi::Vulkan) first = RenderApi::GLES30;
|
if (first != RenderApi::Vulkan) first = RenderApi::GLES30;
|
||||||
const std::initializer_list<RenderApi> all{RenderApi::Vulkan, RenderApi::GLES30};
|
const std::initializer_list<RenderApi> all{RenderApi::Vulkan, RenderApi::GLES30};
|
||||||
|
#elif defined(__APPLE__)
|
||||||
|
// (A Mac: Vulkan through MoltenVK, else OpenGL as far as macOS has it: 4.1.)
|
||||||
|
if (first != RenderApi::Vulkan) first = RenderApi::OpenGL40;
|
||||||
|
const std::initializer_list<RenderApi> all{RenderApi::Vulkan, RenderApi::OpenGL40};
|
||||||
#else
|
#else
|
||||||
const std::initializer_list<RenderApi> all{RenderApi::Vulkan, RenderApi::OpenGL, RenderApi::OpenGL40, RenderApi::OpenGL31, RenderApi::GLES30};
|
const std::initializer_list<RenderApi> all{RenderApi::Vulkan, RenderApi::OpenGL, RenderApi::OpenGL40, RenderApi::OpenGL31, RenderApi::GLES30};
|
||||||
#endif
|
#endif
|
||||||
|
|
@ -356,6 +361,10 @@ int runClient(Game& game) {
|
||||||
else game.onEscape();
|
else game.onEscape();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (ev.type == SDL_EVENT_DROP_FILE && ev.drop.data) {
|
||||||
|
game.onFileDropped(ev.drop.data);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
// Don't let the toggle key's character land in the console input.
|
// Don't let the toggle key's character land in the console input.
|
||||||
if (ev.type == SDL_EVENT_TEXT_INPUT && ev.text.text[0] == '`') continue;
|
if (ev.type == SDL_EVENT_TEXT_INPUT && ev.text.text[0] == '`') continue;
|
||||||
input::handleEvent(ev);
|
input::handleEvent(ev);
|
||||||
|
|
@ -443,20 +452,17 @@ const char* buildVersion() { return KEEL_COMMIT ", " KEEL_COMMIT_DATE; }
|
||||||
// Replaces this process with a fresh one (Windows: starts it, then this one exits).
|
// Replaces this process with a fresh one (Windows: starts it, then this one exits).
|
||||||
void relaunch(const std::vector<std::string>& args) {
|
void relaunch(const std::vector<std::string>& args) {
|
||||||
std::vector<std::string> a = args;
|
std::vector<std::string> a = args;
|
||||||
|
const std::string exe = sys::executablePath();
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
char exe[1024] = {};
|
|
||||||
GetModuleFileNameA(nullptr, exe, sizeof exe);
|
|
||||||
for (std::string& x : a)
|
for (std::string& x : a)
|
||||||
if (x.find(' ') != std::string::npos) x = "\"" + x + "\"";
|
if (x.find(' ') != std::string::npos) x = "\"" + x + "\"";
|
||||||
#else
|
|
||||||
const std::string exe = std::filesystem::read_symlink("/proc/self/exe").string();
|
|
||||||
#endif
|
#endif
|
||||||
std::vector<char*> argv;
|
std::vector<char*> argv;
|
||||||
for (std::string& x : a) argv.push_back(x.data());
|
for (std::string& x : a) argv.push_back(x.data());
|
||||||
argv.push_back(nullptr);
|
argv.push_back(nullptr);
|
||||||
log::info("Restarting");
|
log::info("Restarting");
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
_spawnv(_P_NOWAIT, exe, argv.data());
|
_spawnv(_P_NOWAIT, exe.c_str(), argv.data());
|
||||||
#else
|
#else
|
||||||
execv(exe.c_str(), argv.data());
|
execv(exe.c_str(), argv.data());
|
||||||
log::error("Restart failed");
|
log::error("Restart failed");
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,9 @@ public:
|
||||||
// The window's close button (or the OS asking it to close): false keeps it open (to ask
|
// The window's close button (or the OS asking it to close): false keeps it open (to ask
|
||||||
// about saving first; the game quits later itself). The quit command always quits.
|
// about saving first; the game quits later itself). The quit command always quits.
|
||||||
virtual bool allowQuit() { return true; }
|
virtual bool allowQuit() { return true; }
|
||||||
|
// A file dropped on the window, or opened with the program from the desktop (a Mac's Finder
|
||||||
|
// hands it over this way, not on the command line).
|
||||||
|
virtual void onFileDropped(const std::string& path) { (void)path; }
|
||||||
// Hot reload: called twice a second (hotreload 1) after the engine reloaded what changed
|
// Hot reload: called twice a second (hotreload 1) after the engine reloaded what changed
|
||||||
// (`changed`: textures, models, sounds); the game reloads its own (scripts, the map).
|
// (`changed`: textures, models, sounds); the game reloads its own (scripts, the map).
|
||||||
virtual void hotReload(const std::vector<std::string>& changed) { (void)changed; }
|
virtual void hotReload(const std::vector<std::string>& changed) { (void)changed; }
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,14 @@ namespace {
|
||||||
// one) unless asked. Vulkan picks the discrete GPU itself; this makes OpenGL do the same. Set
|
// one) unless asked. Vulkan picks the discrete GPU itself; this makes OpenGL do the same. Set
|
||||||
// before SDL loads the GL library; anything the user already set wins. KEEL_IGPU=1 opts out.
|
// before SDL loads the GL library; anything the user already set wins. KEEL_IGPU=1 opts out.
|
||||||
void preferDedicatedGpu() {
|
void preferDedicatedGpu() {
|
||||||
#ifndef _WIN32
|
#if defined(__APPLE__)
|
||||||
|
// macOS has no Vulkan of its own: the app carries MoltenVK (Vulkan on Metal) in
|
||||||
|
// Contents/Frameworks. (Unless the user pointed SDL somewhere else.)
|
||||||
|
if (const char* base = SDL_GetBasePath(); base && !SDL_GetHint(SDL_HINT_VULKAN_LIBRARY)) {
|
||||||
|
const std::filesystem::path mvk = std::filesystem::path(base) / ".." / "Frameworks" / "libMoltenVK.dylib";
|
||||||
|
if (std::filesystem::exists(mvk)) SDL_SetHint(SDL_HINT_VULKAN_LIBRARY, mvk.lexically_normal().string().c_str());
|
||||||
|
}
|
||||||
|
#elif !defined(_WIN32)
|
||||||
if (std::getenv("KEEL_IGPU")) return;
|
if (std::getenv("KEEL_IGPU")) return;
|
||||||
if (std::filesystem::exists("/proc/driver/nvidia")) { // NVIDIA's own driver: PRIME render offload
|
if (std::filesystem::exists("/proc/driver/nvidia")) { // NVIDIA's own driver: PRIME render offload
|
||||||
setenv("__NV_PRIME_RENDER_OFFLOAD", "1", 0);
|
setenv("__NV_PRIME_RENDER_OFFLOAD", "1", 0);
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,11 @@ static VKAPI_ATTR VkBool32 VKAPI_CALL debugCallback(VkDebugUtilsMessageSeverityF
|
||||||
|
|
||||||
bool Device::init(const Window& window, bool validation) {
|
bool Device::init(const Window& window, bool validation) {
|
||||||
window_ = &window;
|
window_ = &window;
|
||||||
|
#if defined(__APPLE__)
|
||||||
|
// The library SDL loaded for the window (the app's MoltenVK, or a Vulkan SDK's loader).
|
||||||
|
if (auto gipa = reinterpret_cast<PFN_vkGetInstanceProcAddr>(SDL_Vulkan_GetVkGetInstanceProcAddr())) volkInitializeCustom(gipa);
|
||||||
|
else
|
||||||
|
#endif
|
||||||
if (volkInitialize() != VK_SUCCESS) {
|
if (volkInitialize() != VK_SUCCESS) {
|
||||||
log::error("No Vulkan loader found - is a Vulkan driver installed?");
|
log::error("No Vulkan loader found - is a Vulkan driver installed?");
|
||||||
return false;
|
return false;
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,22 @@ target_link_libraries(fry_script PUBLIC fry_core PRIVATE lua)
|
||||||
if(ANDROID) # SDL's Java side loads the app as libmain.so and calls its (SDL_)main
|
if(ANDROID) # SDL's Java side loads the app as libmain.so and calls its (SDL_)main
|
||||||
add_library(fry3dx SHARED app.cpp icons.cpp)
|
add_library(fry3dx SHARED app.cpp icons.cpp)
|
||||||
set_target_properties(fry3dx PROPERTIES OUTPUT_NAME main LIBRARY_OUTPUT_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
|
set_target_properties(fry3dx PROPERTIES OUTPUT_NAME main LIBRARY_OUTPUT_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
|
||||||
|
elseif(APPLE) # a Mac app: Fry3DX.app, content in Contents/Resources, MoltenVK in Contents/Frameworks
|
||||||
|
set(icns ${PROJECT_SOURCE_DIR}/branding/fry3dx.icns)
|
||||||
|
add_executable(fry3dx MACOSX_BUNDLE app.cpp icons.cpp ${icns})
|
||||||
|
set_source_files_properties(${icns} PROPERTIES MACOSX_PACKAGE_LOCATION Resources)
|
||||||
|
set_target_properties(fry3dx PROPERTIES OUTPUT_NAME Fry3DX MACOSX_BUNDLE_INFO_PLIST ${PROJECT_SOURCE_DIR}/cmake/Info.plist.in)
|
||||||
|
# Vulkan on a Mac is MoltenVK (Vulkan on Metal): FRY_MOLTENVK=/path/to/libMoltenVK.dylib, or
|
||||||
|
# found (Homebrew's molten-vk, a Vulkan SDK). Without it, the app falls back to OpenGL 4.1.
|
||||||
|
find_library(FRY_MOLTENVK NAMES libMoltenVK.dylib MoltenVK HINTS $ENV{VULKAN_SDK}/lib /opt/homebrew/lib /usr/local/lib NO_CMAKE_FIND_ROOT_PATH)
|
||||||
|
if(FRY_MOLTENVK)
|
||||||
|
add_custom_command(TARGET fry3dx POST_BUILD
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E make_directory $<TARGET_BUNDLE_CONTENT_DIR:fry3dx>/Frameworks
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy_if_different ${FRY_MOLTENVK} $<TARGET_BUNDLE_CONTENT_DIR:fry3dx>/Frameworks/libMoltenVK.dylib
|
||||||
|
VERBATIM)
|
||||||
|
else()
|
||||||
|
message(WARNING "No MoltenVK found (set FRY_MOLTENVK): Fry3DX.app will only have OpenGL 4.1")
|
||||||
|
endif()
|
||||||
else()
|
else()
|
||||||
add_executable(fry3dx app.cpp icons.cpp)
|
add_executable(fry3dx app.cpp icons.cpp)
|
||||||
endif()
|
endif()
|
||||||
|
|
|
||||||
17
src/app.cpp
17
src/app.cpp
|
|
@ -790,7 +790,8 @@ private:
|
||||||
glm::vec3 bedFor_{0.0f};
|
glm::vec3 bedFor_{0.0f};
|
||||||
// Closing, New or Open with changes not saved: asked first (askSave_), then done; afterSave_:
|
// Closing, New or Open with changes not saved: asked first (askSave_), then done; afterSave_:
|
||||||
// what to do once the Save it chose has finished (0 nothing, 1 quit, 2 new, 3 open).
|
// what to do once the Save it chose has finished (0 nothing, 1 quit, 2 new, 3 open).
|
||||||
int askSave_ = 0, afterSave_ = 0;
|
int askSave_ = 0, afterSave_ = 0; // (what to do then: 1 quit, 2 new, 3 open..., 4 open dropped_)
|
||||||
|
std::string dropped_;
|
||||||
bool dialogCancelled_ = false;
|
bool dialogCancelled_ = false;
|
||||||
// Autosave: a copy packed small in the background every fry_autosave minutes while there are
|
// Autosave: a copy packed small in the background every fry_autosave minutes while there are
|
||||||
// changes, kept until saved or closed properly; offered back after a crash.
|
// changes, kept until saved or closed properly; offered back after a crash.
|
||||||
|
|
@ -1138,6 +1139,18 @@ private:
|
||||||
if (action == 1) dropAutosave(), keel::execute("quit");
|
if (action == 1) dropAutosave(), keel::execute("quit");
|
||||||
else if (action == 2) newScene();
|
else if (action == 2) newScene();
|
||||||
else if (action == 3) dialog(1);
|
else if (action == 3) dialog(1);
|
||||||
|
else if (action == 4) openFile(std::exchange(dropped_, std::string()));
|
||||||
|
}
|
||||||
|
// A file dropped on the window (or opened with Fry3DX from the desktop): a .fry opens (after
|
||||||
|
// asking about changes), a script goes in the Script panel, a picture goes on as a reference
|
||||||
|
// (or, in the Paint tab, as a sticker on the model).
|
||||||
|
void onFileDropped(const std::string& path) override {
|
||||||
|
std::string ext = stdfs::path(path).extension().string();
|
||||||
|
std::transform(ext.begin(), ext.end(), ext.begin(), [](unsigned char c) { return char(std::tolower(c)); });
|
||||||
|
if (ext == ".fry") return (void)(dropped_ = path, whenSaved(4));
|
||||||
|
if (ext == ".lua") return openScript(path, fileName(path));
|
||||||
|
if (ext == ".png" || ext == ".jpg" || ext == ".jpeg" || ext == ".bmp" || ext == ".tga") return tab_ == Tab::Paint ? floatFromFile(path) : addPicture(path);
|
||||||
|
say("Fry3DX opens .fry files, scripts (.lua) and pictures");
|
||||||
}
|
}
|
||||||
void saveQuestion() {
|
void saveQuestion() {
|
||||||
if (!askSave_) return;
|
if (!askSave_) return;
|
||||||
|
|
@ -6493,6 +6506,8 @@ private:
|
||||||
rows("Painting", {{"B / E / G / I", "Brush / eraser / fill / pick a colour"}, {"Alt+click", "Pick a colour"}, {"[ / ]", "Smaller / bigger brush"},
|
rows("Painting", {{"B / E / G / I", "Brush / eraser / fill / pick a colour"}, {"Alt+click", "Pick a colour"}, {"[ / ]", "Smaller / bigger brush"},
|
||||||
{"Ctrl+V", "Paste a picture onto it"}, {"Enter / Esc", "Place the picture / cancel"}, {"Shift + corner drag", "Resize keeping its shape"},
|
{"Ctrl+V", "Paste a picture onto it"}, {"Enter / Esc", "Place the picture / cancel"}, {"Shift + corner drag", "Resize keeping its shape"},
|
||||||
{"Wheel, middle or right drag", "Zoom and move the Picture panel"}});
|
{"Wheel, middle or right drag", "Zoom and move the Picture panel"}});
|
||||||
|
rows("Sculpting", {{"Tab (Paint tab)", "Colour or Sculpt"}, {"1 - 6", "Raise, puff out, smooth, flatten, pinch, grab"}, {"Ctrl + brush", "The other way (dig, shrink, push apart)"},
|
||||||
|
{"Shift + brush", "Smooth"}, {"X", "The same on both sides"}, {"[ / ]", "Smaller / bigger brush"}});
|
||||||
rows("Selecting", {{"Click", "Select"}, {"Drag", "Select with a box, brush or lasso (Tool options)"}, {"Shift + click", "Objects: add to the selection. Shape: a whole loop"},
|
rows("Selecting", {{"Click", "Select"}, {"Drag", "Select with a box, brush or lasso (Tool options)"}, {"Shift + click", "Objects: add to the selection. Shape: a whole loop"},
|
||||||
{"Ctrl + click", "Objects: take away. Shape: add or take one"}, {"Shift+Ctrl + click an edge", "The ring across it (shape)"}, {"Alt+click / double-click", "A whole loop (shape editing)"}, {"Ctrl+A / Alt+A", "All / none"},
|
{"Ctrl + click", "Objects: take away. Shape: add or take one"}, {"Shift+Ctrl + click an edge", "The ring across it (shape)"}, {"Alt+click / double-click", "A whole loop (shape editing)"}, {"Ctrl+A / Alt+A", "All / none"},
|
||||||
{"Ctrl+I", "Invert"}, {"Ctrl + / Ctrl -", "More / less"}, {"L", "Everything joined to it"}, {"Esc", "Let go / cancel / leave shape editing"}});
|
{"Ctrl+I", "Invert"}, {"Ctrl + / Ctrl -", "More / less"}, {"L", "Everything joined to it"}, {"Esc", "Let go / cancel / leave shape editing"}});
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,13 @@
|
||||||
Fry3DX-<version>-linux-x64.tar.gz from build/portable (cmake --preset portable)
|
Fry3DX-<version>-linux-x64.tar.gz from build/portable (cmake --preset portable)
|
||||||
Fry3DX-<version>-linux-x64.AppImage the same, as one file (needs appimagetool: $APPIMAGETOOL or on PATH)
|
Fry3DX-<version>-linux-x64.AppImage the same, as one file (needs appimagetool: $APPIMAGETOOL or on PATH)
|
||||||
Fry3DX-<version>-windows-x64.zip from build/windows, or build/<dir> given with --windows-bin
|
Fry3DX-<version>-windows-x64.zip from build/windows, or build/<dir> given with --windows-bin
|
||||||
|
Fry3DX-<version>-macos.zip from build/macos-arm64 and build/macos-x64 (on a Mac): one
|
||||||
|
Fry3DX.app for Apple silicon and Intel
|
||||||
|
|
||||||
python3 tools/package.py linux|appimage|windows [--bin DIR]
|
python3 tools/package.py linux|appimage|windows|macos [--bin DIR]
|
||||||
|
|
||||||
Each unpacks to one Fry3DX/ folder: the program, its content (engine/, Fry3DX/), LICENSE and a
|
Each unpacks to one Fry3DX/ folder: the program, its content (engine/, Fry3DX/; on a Mac inside
|
||||||
README.txt. (The Android APK: android/build_apk.py.)
|
the app), LICENSE and a README.txt. (The Android APK: android/build_apk.py.)
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
|
|
@ -26,7 +28,7 @@ A 3D modeller that does what you'd expect: pick a tool, click, drag.
|
||||||
Made by Nexoniarz - https://nexoniarz.github.io/Fry3DX/
|
Made by Nexoniarz - https://nexoniarz.github.io/Fry3DX/
|
||||||
|
|
||||||
Start: {start}
|
Start: {start}
|
||||||
Needs a graphics card with Vulkan (or OpenGL 4.6). F1 in the app lists every key.
|
Needs {needs}. F1 in the app lists every key.
|
||||||
Your settings and autosaves: {prefs}
|
Your settings and autosaves: {prefs}
|
||||||
|
|
||||||
Open source under the Apache License 2.0 (LICENSE). Sky pictures: Poly Haven, CC0.
|
Open source under the Apache License 2.0 (LICENSE). Sky pictures: Poly Haven, CC0.
|
||||||
|
|
@ -41,6 +43,15 @@ def version():
|
||||||
return "0"
|
return "0"
|
||||||
|
|
||||||
|
|
||||||
|
PC_NEEDS = "a graphics card with Vulkan (or OpenGL 4.6)"
|
||||||
|
|
||||||
|
|
||||||
|
def readme(folder, start, prefs, needs=PC_NEEDS, crlf=False):
|
||||||
|
shutil.copy2(os.path.join(ROOT, "LICENSE"), folder)
|
||||||
|
with open(os.path.join(folder, "README.txt"), "w", newline="\r\n" if crlf else "\n") as f:
|
||||||
|
f.write(README.format(version=version(), start=start, prefs=prefs, needs=needs))
|
||||||
|
|
||||||
|
|
||||||
def stage(bin_dir, exe, extra, start, prefs):
|
def stage(bin_dir, exe, extra, start, prefs):
|
||||||
out = os.path.join(DIST, "stage", "Fry3DX")
|
out = os.path.join(DIST, "stage", "Fry3DX")
|
||||||
shutil.rmtree(os.path.dirname(out), ignore_errors=True)
|
shutil.rmtree(os.path.dirname(out), ignore_errors=True)
|
||||||
|
|
@ -49,9 +60,7 @@ def stage(bin_dir, exe, extra, start, prefs):
|
||||||
shutil.copy2(os.path.join(bin_dir, name), out)
|
shutil.copy2(os.path.join(bin_dir, name), out)
|
||||||
for d in ("engine", "Fry3DX"):
|
for d in ("engine", "Fry3DX"):
|
||||||
shutil.copytree(os.path.join(bin_dir, d), os.path.join(out, d))
|
shutil.copytree(os.path.join(bin_dir, d), os.path.join(out, d))
|
||||||
shutil.copy2(os.path.join(ROOT, "LICENSE"), out)
|
readme(out, start, prefs, crlf=exe.endswith(".exe"))
|
||||||
with open(os.path.join(out, "README.txt"), "w", newline="\r\n" if exe.endswith(".exe") else "\n") as f:
|
|
||||||
f.write(README.format(version=version(), start=start, prefs=prefs))
|
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -94,6 +103,35 @@ def windows(bin_dir):
|
||||||
return path
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def macos(bin_dir):
|
||||||
|
"""One Fry3DX.app for both kinds of Mac: the Apple silicon build's app, its program joined
|
||||||
|
(lipo) with the Intel build's; signed ad hoc (no Apple account: enough to run, with a right-click
|
||||||
|
Open the first time)."""
|
||||||
|
arm = os.path.join(bin_dir, "Fry3DX.app")
|
||||||
|
intel = os.path.join(ROOT, "build", "macos-x64", "bin", "Fry3DX.app")
|
||||||
|
out = os.path.join(DIST, "stage", "Fry3DX")
|
||||||
|
shutil.rmtree(os.path.dirname(out), ignore_errors=True)
|
||||||
|
os.makedirs(out)
|
||||||
|
app = os.path.join(out, "Fry3DX.app")
|
||||||
|
shutil.copytree(arm, app, symlinks=True)
|
||||||
|
exe = os.path.join("Contents", "MacOS", "Fry3DX")
|
||||||
|
if os.path.exists(os.path.join(intel, exe)):
|
||||||
|
subprocess.run(["lipo", "-create", os.path.join(arm, exe), os.path.join(intel, exe), "-output", os.path.join(app, exe)], check=True)
|
||||||
|
kind = "macos"
|
||||||
|
else:
|
||||||
|
print("(no build/macos-x64: Apple silicon only)")
|
||||||
|
kind = "macos-arm64"
|
||||||
|
subprocess.run(["strip", "-x", os.path.join(app, exe)], check=True)
|
||||||
|
subprocess.run(["codesign", "--force", "--deep", "--sign", "-", app], check=True)
|
||||||
|
readme(out, "Fry3DX.app (move it to Applications if you like). The first time: right-click it, Open,\n"
|
||||||
|
" as it isn't from the App Store", "~/Library/Application Support/Nexoniarz/Fry3DX",
|
||||||
|
"macOS 13.3 or newer; Vulkan through MoltenVK (inside the app), else OpenGL 4.1")
|
||||||
|
path = os.path.join(DIST, f"Fry3DX-{version()}-{kind}.zip")
|
||||||
|
# (ditto keeps what a Mac app needs: links, permissions, the signature)
|
||||||
|
subprocess.run(["ditto", "-c", "-k", "--sequesterRsrc", "--keepParent", out, path], check=True)
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
args = sys.argv[1:]
|
args = sys.argv[1:]
|
||||||
bin_dir = None
|
bin_dir = None
|
||||||
|
|
@ -103,8 +141,8 @@ def main():
|
||||||
del args[i:i + 2]
|
del args[i:i + 2]
|
||||||
os.makedirs(DIST, exist_ok=True)
|
os.makedirs(DIST, exist_ok=True)
|
||||||
for what in args:
|
for what in args:
|
||||||
default = {"linux": "build/portable/bin", "appimage": "build/portable/bin", "windows": "build/windows/bin"}[what]
|
default = {"linux": "build/portable/bin", "appimage": "build/portable/bin", "windows": "build/windows/bin", "macos": "build/macos-arm64/bin"}[what]
|
||||||
path = {"linux": linux, "appimage": appimage, "windows": windows}[what](bin_dir or os.path.join(ROOT, default))
|
path = {"linux": linux, "appimage": appimage, "windows": windows, "macos": macos}[what](bin_dir or os.path.join(ROOT, default))
|
||||||
print(f"{os.path.relpath(path, ROOT)}: {os.path.getsize(path) / 1048576:.1f} MB")
|
print(f"{os.path.relpath(path, ROOT)}: {os.path.getsize(path) / 1048576:.1f} MB")
|
||||||
shutil.rmtree(os.path.join(DIST, "stage"), ignore_errors=True)
|
shutil.rmtree(os.path.join(DIST, "stage"), ignore_errors=True)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue