emoemu 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. package/.claude/settings.local.json +77 -0
  2. package/.node-version +1 -0
  3. package/CLAUDE.md +435 -0
  4. package/README.md +404 -0
  5. package/TODO.md +655 -0
  6. package/dist/index.cjs +25108 -0
  7. package/dist/index.d.ts +1 -0
  8. package/dist/index.js +25085 -0
  9. package/docs/building-libretro-cores-arm-mac.md +237 -0
  10. package/docs/config-file-format.md +488 -0
  11. package/docs/cores-trd.md +425 -0
  12. package/docs/headless-hardware-rendering-trd.md +676 -0
  13. package/docs/libretro-cores-trd.md +997 -0
  14. package/docs/mupen64-software-rendering-trd.md +751 -0
  15. package/docs/n64-support-trd.md +306 -0
  16. package/docs/native-rendering-trd.md +540 -0
  17. package/docs/native-ui-rendering-trd.md +1195 -0
  18. package/docs/netplay-trd.md +665 -0
  19. package/docs/retroarch-netplay-docs.md +277 -0
  20. package/docs/save-state-format.md +666 -0
  21. package/eslint.config.js +111 -0
  22. package/icon/icon.png +0 -0
  23. package/icon/icon.pxd +0 -0
  24. package/package.json +63 -0
  25. package/pnpm-workspace.yaml +10 -0
  26. package/src/Emulator/consts.ts +14 -0
  27. package/src/Emulator/index.ts +2496 -0
  28. package/src/Emulator/saveState/index.ts +155 -0
  29. package/src/Emulator/screenshot/index.ts +160 -0
  30. package/src/Emulator/terminalDimensions/index.ts +79 -0
  31. package/src/Emulator/types.ts +83 -0
  32. package/src/cli/commands/consts.ts +10 -0
  33. package/src/cli/commands/index.ts +462 -0
  34. package/src/cli/parseArgs/consts.ts +17 -0
  35. package/src/cli/parseArgs/index.ts +457 -0
  36. package/src/cli/parseArgs/types.ts +61 -0
  37. package/src/cli/runEmulator/index.ts +406 -0
  38. package/src/cli/runEmulator/types.ts +7 -0
  39. package/src/consts.ts +19 -0
  40. package/src/core/button/consts.ts +35 -0
  41. package/src/core/button/index.ts +123 -0
  42. package/src/core/core.ts +300 -0
  43. package/src/core/index.ts +19 -0
  44. package/src/cores/libretro/api/index.ts +334 -0
  45. package/src/cores/libretro/api/types.ts +148 -0
  46. package/src/cores/libretro/callbacks/consts.ts +41 -0
  47. package/src/cores/libretro/callbacks/index.ts +456 -0
  48. package/src/cores/libretro/consts.ts +45 -0
  49. package/src/cores/libretro/coreOptions/consts.ts +36 -0
  50. package/src/cores/libretro/coreOptions/index.ts +222 -0
  51. package/src/cores/libretro/environment/consts.ts +118 -0
  52. package/src/cores/libretro/environment/index.ts +1095 -0
  53. package/src/cores/libretro/index.ts +937 -0
  54. package/src/cores/libretro/loader/index.ts +496 -0
  55. package/src/cores/libretro/pixelFormat/consts.ts +43 -0
  56. package/src/cores/libretro/pixelFormat/index.ts +397 -0
  57. package/src/cores/libretro/types.ts +339 -0
  58. package/src/frontend/AudioManager/index.ts +420 -0
  59. package/src/frontend/SettingsManager/index.ts +250 -0
  60. package/src/frontend/config/index.ts +608 -0
  61. package/src/frontend/config/tests.ts +354 -0
  62. package/src/frontend/config/types.ts +36 -0
  63. package/src/frontend/consts.ts +114 -0
  64. package/src/frontend/coreBuilder/index.ts +644 -0
  65. package/src/frontend/coreBuilder/types.ts +15 -0
  66. package/src/frontend/coreDownloader/index.ts +620 -0
  67. package/src/frontend/coreDownloader/types.ts +17 -0
  68. package/src/frontend/corePreferences/index.ts +69 -0
  69. package/src/frontend/coreRegistry/index.ts +276 -0
  70. package/src/frontend/directoryCache/index.ts +75 -0
  71. package/src/frontend/index.ts +79 -0
  72. package/src/frontend/notifications/consts.ts +14 -0
  73. package/src/frontend/notifications/index.ts +250 -0
  74. package/src/frontend/playlist/consts.ts +168 -0
  75. package/src/frontend/playlist/index.ts +899 -0
  76. package/src/frontend/playlist/labelFormatter/consts.ts +15 -0
  77. package/src/frontend/playlist/labelFormatter/index.ts +155 -0
  78. package/src/frontend/playlist/labelFormatter/tests.ts +153 -0
  79. package/src/frontend/playlist/reader/index.ts +559 -0
  80. package/src/frontend/playlist/sync/index.ts +511 -0
  81. package/src/frontend/playlist/systemLookup/index.ts +233 -0
  82. package/src/frontend/playlist/utils/index.ts +50 -0
  83. package/src/frontend/romScanner/consts.ts +348 -0
  84. package/src/frontend/romScanner/index.ts +1957 -0
  85. package/src/frontend/saveServices/consts.ts +2 -0
  86. package/src/frontend/saveServices/index.ts +313 -0
  87. package/src/frontend/serviceProvider/index.ts +108 -0
  88. package/src/frontend/serviceProvider/types.ts +13 -0
  89. package/src/index.ts +428 -0
  90. package/src/input/Controller/consts.ts +50 -0
  91. package/src/input/Controller/index.ts +81 -0
  92. package/src/input/GamepadManager/consts.ts +22 -0
  93. package/src/input/GamepadManager/index.ts +418 -0
  94. package/src/input/InputManager/consts.ts +86 -0
  95. package/src/input/InputManager/index.ts +593 -0
  96. package/src/input/InputMapper/consts.ts +33 -0
  97. package/src/input/InputMapper/index.ts +436 -0
  98. package/src/input/consts.ts +410 -0
  99. package/src/input/gamepadProfiles/index.ts +1100 -0
  100. package/src/input/index.ts +38 -0
  101. package/src/input/inputUtils/index.ts +31 -0
  102. package/src/netplay/FrameBuffer/consts.ts +2 -0
  103. package/src/netplay/FrameBuffer/index.ts +364 -0
  104. package/src/netplay/FrameBuffer/tests.ts +286 -0
  105. package/src/netplay/InputBuffer/consts.ts +7 -0
  106. package/src/netplay/InputBuffer/index.ts +347 -0
  107. package/src/netplay/InputBuffer/tests.ts +166 -0
  108. package/src/netplay/NetplayClient/index.ts +976 -0
  109. package/src/netplay/NetplayConnection/index.ts +536 -0
  110. package/src/netplay/NetplayDiscovery/consts.ts +41 -0
  111. package/src/netplay/NetplayDiscovery/index.ts +525 -0
  112. package/src/netplay/NetplayServer/index.ts +1407 -0
  113. package/src/netplay/SyncManager/index.ts +984 -0
  114. package/src/netplay/SyncManager/tests.ts +419 -0
  115. package/src/netplay/consts.ts +371 -0
  116. package/src/netplay/crc32/consts.ts +14 -0
  117. package/src/netplay/crc32/index.ts +97 -0
  118. package/src/netplay/crc32/tests.ts +40 -0
  119. package/src/netplay/index.ts +41 -0
  120. package/src/netplay/netplayLogger/consts.ts +30 -0
  121. package/src/netplay/netplayLogger/index.ts +345 -0
  122. package/src/netplay/protocol/consts.ts +86 -0
  123. package/src/netplay/protocol/index.ts +1280 -0
  124. package/src/netplay/protocol/tests.ts +606 -0
  125. package/src/netplay/protocol/types.ts +20 -0
  126. package/src/netplay/types.ts +395 -0
  127. package/src/rendering/KittyRenderer/index.ts +616 -0
  128. package/src/rendering/NativeRenderer/index.ts +279 -0
  129. package/src/rendering/NativeRenderer/tests.ts +133 -0
  130. package/src/rendering/TerminalRenderer/index.ts +770 -0
  131. package/src/rendering/consts.ts +401 -0
  132. package/src/rendering/fonts/CozetteVector.ttf +0 -0
  133. package/src/rendering/index.ts +26 -0
  134. package/src/rendering/nativeUi/NativeWindowManager/index.ts +158 -0
  135. package/src/rendering/nativeUi/NativeWindowManager/tests.ts +81 -0
  136. package/src/rendering/nativeUi/consts.ts +6 -0
  137. package/src/rendering/nativeUi/index.ts +20 -0
  138. package/src/rendering/postProcessing/consts.ts +38 -0
  139. package/src/rendering/postProcessing/index.ts +923 -0
  140. package/src/rendering/shared/ansi/consts.ts +12 -0
  141. package/src/rendering/shared/ansi/index.ts +104 -0
  142. package/src/rendering/shared/consts.ts +2 -0
  143. package/src/rendering/shared/fitToTerminal/index.ts +67 -0
  144. package/src/ui/AddRomsPrompt/consts.ts +13 -0
  145. package/src/ui/AddRomsPrompt/index.tsx +781 -0
  146. package/src/ui/App/consts.ts +2 -0
  147. package/src/ui/App/index.tsx +456 -0
  148. package/src/ui/AppCapabilities/index.tsx +67 -0
  149. package/src/ui/ConfigContext/index.tsx +56 -0
  150. package/src/ui/CoreManager/consts.ts +11 -0
  151. package/src/ui/CoreManager/index.tsx +779 -0
  152. package/src/ui/CoreSelector/consts.ts +2 -0
  153. package/src/ui/CoreSelector/index.tsx +251 -0
  154. package/src/ui/DialogContainer/index.tsx +42 -0
  155. package/src/ui/DialogOptionsList/index.tsx +61 -0
  156. package/src/ui/DuplicateCrcPrompt/consts.ts +5 -0
  157. package/src/ui/DuplicateCrcPrompt/index.tsx +146 -0
  158. package/src/ui/GamepadContext/consts.ts +15 -0
  159. package/src/ui/GamepadContext/index.tsx +295 -0
  160. package/src/ui/NativeDialog/index.tsx +120 -0
  161. package/src/ui/NetplayDisconnectedDialog/index.tsx +93 -0
  162. package/src/ui/NetplayPauseMenu/consts.ts +2 -0
  163. package/src/ui/NetplayPauseMenu/index.tsx +97 -0
  164. package/src/ui/RomBrowser/NetplayPanel/consts.ts +24 -0
  165. package/src/ui/RomBrowser/NetplayPanel/index.tsx +520 -0
  166. package/src/ui/RomBrowser/SettingsPanel/index.tsx +478 -0
  167. package/src/ui/RomBrowser/consts.ts +61 -0
  168. package/src/ui/RomBrowser/index.tsx +1164 -0
  169. package/src/ui/RomBrowser/settingsConfig/index.ts +320 -0
  170. package/src/ui/RomBrowser/types.ts +67 -0
  171. package/src/ui/SaveStateDialog/consts.ts +2 -0
  172. package/src/ui/SaveStateDialog/index.tsx +225 -0
  173. package/src/ui/WarningDialog/index.tsx +113 -0
  174. package/src/ui/consts.ts +27 -0
  175. package/src/ui/hooks/useClearTerminal/consts.ts +2 -0
  176. package/src/ui/hooks/useClearTerminal/index.ts +37 -0
  177. package/src/ui/hooks/useDialogNavigation/index.ts +99 -0
  178. package/src/ui/hooks/useGamepad/consts.ts +21 -0
  179. package/src/ui/hooks/useGamepad/index.ts +194 -0
  180. package/src/ui/index.ts +27 -0
  181. package/src/utils/buffer/consts.ts +17 -0
  182. package/src/utils/buffer/index.ts +129 -0
  183. package/src/utils/color/consts.ts +58 -0
  184. package/src/utils/color/index.ts +183 -0
  185. package/src/utils/compression/consts.ts +50 -0
  186. package/src/utils/compression/index.ts +101 -0
  187. package/src/utils/consts.ts +2 -0
  188. package/src/utils/crc32/consts.ts +22 -0
  189. package/src/utils/crc32/index.ts +83 -0
  190. package/src/utils/ensureDirectory/index.ts +10 -0
  191. package/src/utils/format/consts.ts +8 -0
  192. package/src/utils/format/index.ts +53 -0
  193. package/src/utils/getErrorMessage/index.ts +10 -0
  194. package/src/utils/index.ts +113 -0
  195. package/src/utils/ini/index.ts +200 -0
  196. package/src/utils/kitty/consts.ts +13 -0
  197. package/src/utils/kitty/index.ts +181 -0
  198. package/src/utils/logger/consts.ts +35 -0
  199. package/src/utils/logger/index.ts +217 -0
  200. package/src/utils/paths/consts.ts +18 -0
  201. package/src/utils/paths/index.ts +151 -0
  202. package/src/utils/png/consts.ts +34 -0
  203. package/src/utils/png/index.ts +131 -0
  204. package/src/utils/readJsonFile/index.ts +16 -0
  205. package/src/utils/rotateLogFile/index.ts +44 -0
  206. package/src/utils/safeClose/index.ts +10 -0
  207. package/src/utils/terminal/consts.ts +8 -0
  208. package/src/utils/terminal/index.ts +102 -0
  209. package/src/utils/thumbnailRenderer/consts.ts +2 -0
  210. package/src/utils/thumbnailRenderer/index.ts +147 -0
  211. package/src/utils/typedError/index.ts +26 -0
  212. package/tsconfig.json +31 -0
  213. package/vitest.config.ts +13 -0
@@ -0,0 +1,644 @@
1
+ /**
2
+ * Core Builder Service
3
+ *
4
+ * Builds libretro cores from source for platforms where pre-built
5
+ * binaries are not available or require specific build options.
6
+ *
7
+ * Currently supports:
8
+ * - mupen64plus_next on ARM macOS (requires software rendering build)
9
+ */
10
+
11
+ import { execSync, spawn } from "child_process";
12
+ import { existsSync, rmSync, copyFileSync, readFileSync, writeFileSync } from "fs";
13
+ import { ensureDirectory } from '../../utils/ensureDirectory';
14
+ import { join } from "path";
15
+ import { platform, arch, tmpdir } from "os";
16
+ import { getCoresDirectory } from "../config";
17
+ import { logger } from "../../utils/logger";
18
+ import { getErrorMessage } from "../../utils/getErrorMessage";
19
+ import { CoreBuildError } from "./types";
20
+
21
+ /** Default number of CPU cores to use for parallel builds */
22
+ const DEFAULT_CPU_COUNT = 4;
23
+
24
+ /** Number of recent output lines to show on build error */
25
+ const ERROR_OUTPUT_LINES = 10;
26
+
27
+ /** Bytes per kilobyte */
28
+ const BYTES_PER_KB = 1024;
29
+
30
+ /** Bytes per megabyte */
31
+ const BYTES_PER_MB = BYTES_PER_KB * BYTES_PER_KB;
32
+
33
+ /** Buffer size multiplier for make dry run */
34
+ const MAKE_DRY_RUN_BUFFER_MB = 10;
35
+
36
+ /** Buffer size for make dry run (10MB to handle large projects) */
37
+ const MAKE_DRY_RUN_BUFFER_SIZE = MAKE_DRY_RUN_BUFFER_MB * BYTES_PER_MB;
38
+
39
+ /** Length of short git commit hash for display purposes */
40
+ const SHORT_COMMIT_HASH_LENGTH = 8;
41
+
42
+ /** Build progress phases */
43
+ export type BuildPhase = "cloning" | "building" | "installing" | "complete" | "error";
44
+
45
+ /** Progress callback for build operations */
46
+ export interface BuildProgress {
47
+ phase: BuildPhase;
48
+ message: string;
49
+ /** Output lines from the build process */
50
+ output?: string[];
51
+ /** Progress percentage (0-100) if available */
52
+ progressPercent?: number;
53
+ /** Human-readable progress text (e.g., "15 of 238 files") */
54
+ progressText?: string;
55
+ }
56
+
57
+ /** Core build configuration */
58
+ interface CoreBuildConfig {
59
+ /** Git repository URL */
60
+ repo: string;
61
+ /** Specific commit hash to checkout (ensures reproducible builds and stable patches) */
62
+ commit: string;
63
+ /** Build command (uses make by default) */
64
+ buildArgs: string[];
65
+ /** Output filename after build */
66
+ outputFile: string;
67
+ /** Installed filename */
68
+ installedFile: string;
69
+ /** Human-readable description */
70
+ description: string;
71
+ }
72
+
73
+ /** Cores that require building from source on specific platforms */
74
+ const BUILD_CONFIGS: Partial<Record<string, CoreBuildConfig>> = {
75
+ mupen64plus_next: {
76
+ repo: "https://github.com/libretro/mupen64plus-libretro-nx.git",
77
+ // Pin to specific commit for reproducible builds and stable patches
78
+ // Last verified: 2025-01-25 (patches for pngpriv.h and zutil.h tested against this commit)
79
+ commit: "bc43bcedc276861254b48526f56799d63a30723b",
80
+ buildArgs: [
81
+ "platform=osx",
82
+ "HAVE_PARALLEL_RDP=0", // Disable Vulkan-dependent ParaLLEl RDP
83
+ "HAVE_PARALLEL_RSP=1", // Enable fast RSP dynarec (required for Angrylion)
84
+ "HAVE_THR_AL=1", // Enable Angrylion multi-threading
85
+ "LLE=1", // Enable low-level emulation
86
+ // Note: WITH_DYNAREC is intentionally omitted - the ARM64 dynarec assembly
87
+ // uses GNU syntax (.hidden, .type) that's incompatible with macOS's assembler.
88
+ // The Makefile defaults to interpreter mode on macOS, which is slower but
89
+ // compatible. Parallel RSP still uses its own dynarec which works.
90
+ ],
91
+ outputFile: "mupen64plus_next_libretro.dylib",
92
+ installedFile: "mupen64plus_next_libretro.dylib",
93
+ description: "Nintendo 64 (Mupen64Plus-Next with Angrylion software renderer)",
94
+ },
95
+ };
96
+
97
+ /**
98
+ * Stub OpenGL library source code
99
+ *
100
+ * The mupen64plus core links against OpenGL even when using the Angrylion
101
+ * software renderer. On macOS, loading the real OpenGL framework causes
102
+ * hangs during library initialization (likely GPU probing).
103
+ *
104
+ * This stub provides empty implementations of the OpenGL functions needed
105
+ * to satisfy the linker. Since we use Angrylion (software rendering),
106
+ * these functions are never actually called at runtime.
107
+ */
108
+ const GL_STUB_SOURCE = `
109
+ // Stub OpenGL functions to allow linking without the real OpenGL framework
110
+ // These functions will never be called at runtime when using Angrylion
111
+
112
+ typedef unsigned int GLenum;
113
+ typedef unsigned int GLuint;
114
+ typedef int GLint;
115
+ typedef int GLsizei;
116
+ typedef float GLfloat;
117
+ typedef double GLdouble;
118
+ typedef unsigned char GLboolean;
119
+ typedef void GLvoid;
120
+ typedef unsigned char GLubyte;
121
+
122
+ void glBindTexture(GLenum target, GLuint texture) {}
123
+ void glBlendFunc(GLenum sfactor, GLenum dfactor) {}
124
+ void glClear(GLuint mask) {}
125
+ void glClearColor(GLfloat r, GLfloat g, GLfloat b, GLfloat a) {}
126
+ void glClearDepth(GLdouble depth) {}
127
+ void glColorMask(GLboolean r, GLboolean g, GLboolean b, GLboolean a) {}
128
+ void glCullFace(GLenum mode) {}
129
+ void glDeleteTextures(GLsizei n, const GLuint *textures) {}
130
+ void glDepthFunc(GLenum func) {}
131
+ void glDepthMask(GLboolean flag) {}
132
+ void glDepthRange(GLdouble near, GLdouble far) {}
133
+ void glDisable(GLenum cap) {}
134
+ void glDrawArrays(GLenum mode, GLint first, GLsizei count) {}
135
+ void glDrawElements(GLenum mode, GLsizei count, GLenum type, const GLvoid *indices) {}
136
+ void glEnable(GLenum cap) {}
137
+ void glFinish(void) {}
138
+ void glFrontFace(GLenum mode) {}
139
+ void glGenTextures(GLsizei n, GLuint *textures) {}
140
+ GLenum glGetError(void) { return 0; }
141
+ void glGetFloatv(GLenum pname, GLfloat *params) {}
142
+ void glGetIntegerv(GLenum pname, GLint *params) {}
143
+ const GLubyte* glGetString(GLenum name) { return (const GLubyte*)""; }
144
+ void glLineWidth(GLfloat width) {}
145
+ void glPixelStorei(GLenum pname, GLint param) {}
146
+ void glPolygonMode(GLenum face, GLenum mode) {}
147
+ void glPolygonOffset(GLfloat factor, GLfloat units) {}
148
+ void glReadBuffer(GLenum mode) {}
149
+ void glReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, GLvoid *pixels) {}
150
+ void glScissor(GLint x, GLint y, GLsizei width, GLsizei height) {}
151
+ void glStencilFunc(GLenum func, GLint ref, GLuint mask) {}
152
+ void glStencilMask(GLuint mask) {}
153
+ void glStencilOp(GLenum fail, GLenum zfail, GLenum zpass) {}
154
+ void glTexParameteri(GLenum target, GLenum pname, GLint param) {}
155
+ void glTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLenum type, const GLvoid *pixels) {}
156
+ void glViewport(GLint x, GLint y, GLsizei width, GLsizei height) {}
157
+ `;
158
+
159
+ /** Name of the stub OpenGL library */
160
+ const GL_STUB_LIB_NAME = "libGL_stub.dylib";
161
+
162
+ /**
163
+ * Create a stub OpenGL library for macOS
164
+ *
165
+ * This allows mupen64plus to link without the real OpenGL framework,
166
+ * avoiding the library loading hang on macOS.
167
+ *
168
+ * @param outputDir Directory to create the stub library in
169
+ * @returns Path to the created stub library
170
+ */
171
+ const createGLStubLibrary = (outputDir: string): string => {
172
+ const stubSourcePath = join(outputDir, "gl_stubs.c");
173
+ const stubLibPath = join(outputDir, GL_STUB_LIB_NAME);
174
+
175
+ // Write stub source
176
+ writeFileSync(stubSourcePath, GL_STUB_SOURCE, "utf-8");
177
+
178
+ // Compile stub library
179
+ execSync(
180
+ `clang -dynamiclib -o "${stubLibPath}" "${stubSourcePath}" -install_name @rpath/${GL_STUB_LIB_NAME}`,
181
+ { cwd: outputDir }
182
+ );
183
+
184
+ logger.info(`Created stub OpenGL library: ${stubLibPath}`, "CoreBuilder");
185
+
186
+ return stubLibPath;
187
+ };
188
+
189
+ /**
190
+ * Fix library paths in the built core to use the stub library
191
+ *
192
+ * @param corePath Path to the built core dylib
193
+ * @param coresDir Directory where the stub library is installed
194
+ */
195
+ const fixLibraryPaths = (corePath: string, coresDir: string): void => {
196
+ const stubLibPath = join(coresDir, GL_STUB_LIB_NAME);
197
+
198
+ // Change the @rpath reference to an absolute path
199
+ execSync(
200
+ `install_name_tool -change "@rpath/${GL_STUB_LIB_NAME}" "${stubLibPath}" "${corePath}"`,
201
+ { stdio: "pipe" }
202
+ );
203
+
204
+ logger.info(`Fixed library paths in ${corePath}`, "CoreBuilder");
205
+ };
206
+
207
+ /**
208
+ * Apply source patches needed for modern macOS compatibility
209
+ *
210
+ * The bundled libpng and libzlib have outdated macOS compatibility checks
211
+ * that assume Classic Mac OS (pre-OS X) rather than modern macOS.
212
+ *
213
+ * Issues fixed:
214
+ * 1. libpng's pngpriv.h includes <fp.h> which was removed in modern macOS SDKs
215
+ * 2. libzlib's zutil.h defines fdopen() as NULL for TARGET_OS_MAC, but modern
216
+ * macOS has fdopen() available - this breaks when _stdio.h is included
217
+ */
218
+ const applySourcePatches = (repoDir: string): void => {
219
+ // Patch 1: Fix libpng fp.h issue
220
+ const pngPrivPath = join(repoDir, "custom/dependencies/libpng/pngpriv.h");
221
+ if (existsSync(pngPrivPath)) {
222
+ let content = readFileSync(pngPrivPath, "utf-8");
223
+ const fpHPattern = /#\s*include\s*<fp\.h>/g;
224
+ if (fpHPattern.test(content)) {
225
+ content = content.replace(fpHPattern, "# include <math.h>");
226
+ writeFileSync(pngPrivPath, content, "utf-8");
227
+ logger.info("Patched pngpriv.h: replaced <fp.h> with <math.h>", "CoreBuilder");
228
+ }
229
+ }
230
+
231
+ // Patch 2: Fix libzlib fdopen issue
232
+ // The bundled zlib defines fdopen(fd,mode) as NULL for TARGET_OS_MAC,
233
+ // assuming Classic Mac OS. Modern macOS has fdopen(), and the NULL macro
234
+ // breaks when _stdio.h declares the real fdopen function.
235
+ //
236
+ // Fix: Add a check for __APPLE__ to skip the NULL definition on modern macOS
237
+ const zlibUtilPath = join(repoDir, "custom/dependencies/libzlib/zutil.h");
238
+ if (existsSync(zlibUtilPath)) {
239
+ let content = readFileSync(zlibUtilPath, "utf-8");
240
+
241
+ // The problematic code block looks like:
242
+ // #if defined(MACOS) || defined(TARGET_OS_MAC)
243
+ // ...
244
+ // #ifndef fdopen
245
+ // #define fdopen(fd,mode) NULL /* No fdopen() */
246
+ // #endif
247
+ //
248
+ // We need to exclude modern macOS (__APPLE__) from this condition
249
+ const oldCondition = "#if defined(MACOS) || defined(TARGET_OS_MAC)";
250
+ const newCondition = "#if (defined(MACOS) || defined(TARGET_OS_MAC)) && !defined(__APPLE__)";
251
+
252
+ if (content.includes(oldCondition)) {
253
+ content = content.replace(oldCondition, newCondition);
254
+ writeFileSync(zlibUtilPath, content, "utf-8");
255
+ logger.info("Patched zutil.h: excluded modern macOS from fdopen NULL definition", "CoreBuilder");
256
+ }
257
+ }
258
+ };
259
+
260
+ /**
261
+ * Check if a core requires building from source on the current platform
262
+ */
263
+ export const requiresBuildFromSource = (coreName: string): boolean => {
264
+ // Only ARM macOS requires building certain cores
265
+ if (platform() !== "darwin" || arch() !== "arm64") {
266
+ return false;
267
+ }
268
+
269
+ return coreName in BUILD_CONFIGS;
270
+ };
271
+
272
+ /**
273
+ * Get description of why a core needs to be built
274
+ */
275
+ export const getBuildReason = (coreName: string): string | null => {
276
+ if (!requiresBuildFromSource(coreName)) {
277
+ return null;
278
+ }
279
+
280
+ if (coreName === "mupen64plus_next") {
281
+ return "The pre-built N64 core for ARM Mac requires OpenGL. Building from source with Angrylion software renderer enabled.";
282
+ }
283
+
284
+ return "Pre-built binary not available for ARM Mac.";
285
+ };
286
+
287
+ /**
288
+ * Check if required build tools are available
289
+ */
290
+ export const checkBuildPrerequisites = (): { ok: boolean; missing: string[] } => {
291
+ const required = ["git", "make", "clang"];
292
+ const missing: string[] = [];
293
+
294
+ for (const tool of required) {
295
+ try {
296
+ execSync(`which ${tool}`, { stdio: "pipe" });
297
+ } catch {
298
+ missing.push(tool);
299
+ }
300
+ }
301
+
302
+ return { ok: missing.length === 0, missing };
303
+ };
304
+
305
+ /**
306
+ * Get the number of CPU cores for parallel builds
307
+ */
308
+ const getCpuCount = (): number => {
309
+ try {
310
+ const result = execSync("sysctl -n hw.ncpu", { encoding: "utf-8" });
311
+ return parseInt(result.trim(), 10) || DEFAULT_CPU_COUNT;
312
+ } catch {
313
+ return DEFAULT_CPU_COUNT;
314
+ }
315
+ };
316
+
317
+ /**
318
+ * Patterns that indicate a compilation step in make output
319
+ * These are common patterns from C/C++ builds
320
+ */
321
+ const COMPILATION_PATTERNS = [
322
+ /^\s*(CC|CXX|COMPILE|Compiling)\s/i, // GCC/Clang style
323
+ /^\s*\[\s*\d+%\s*\]\s*(Building|Compiling)/i, // CMake style
324
+ /^(cc|c\+\+|gcc|g\+\+|clang|clang\+\+)\s/i, // Direct compiler invocation
325
+ ];
326
+
327
+ /**
328
+ * Check if a line represents a compilation step
329
+ */
330
+ const isCompilationLine = (line: string): boolean => {
331
+ return COMPILATION_PATTERNS.some(pattern => pattern.test(line));
332
+ };
333
+
334
+ /**
335
+ * Count the number of compilation targets in a make dry run output
336
+ */
337
+ const countCompilationTargets = (output: string): number => {
338
+ const lines = output.split("\n");
339
+ let count = 0;
340
+ for (const line of lines) {
341
+ if (isCompilationLine(line)) {
342
+ count++;
343
+ }
344
+ }
345
+ return count;
346
+ };
347
+
348
+ /**
349
+ * Get the total number of compilation steps by doing a make dry run
350
+ */
351
+ const getMakeTargetCount = (cwd: string, makeArgs: string[]): number | null => {
352
+ try {
353
+ // -n does a dry run (prints commands without executing)
354
+ const result = execSync(`make -n ${makeArgs.join(" ")}`, {
355
+ cwd,
356
+ encoding: "utf-8",
357
+ maxBuffer: MAKE_DRY_RUN_BUFFER_SIZE,
358
+ stdio: ["ignore", "pipe", "pipe"],
359
+ });
360
+ const count = countCompilationTargets(result);
361
+ logger.info(`Counted ${count} compilation targets from make dry run`, "CoreBuilder");
362
+ return count > 0 ? count : null;
363
+ } catch {
364
+ // Dry run failed, continue without progress tracking
365
+ logger.warn("Make dry run failed, build progress will not be tracked", "CoreBuilder");
366
+ return null;
367
+ }
368
+ };
369
+
370
+ /**
371
+ * Build a core from source
372
+ *
373
+ * @param coreName Name of the core to build
374
+ * @param onProgress Optional callback for progress updates
375
+ * @returns Path to the installed core file
376
+ */
377
+ export const buildCore = async (
378
+ coreName: string,
379
+ onProgress?: (progress: BuildProgress) => void
380
+ ): Promise<string> => {
381
+ const config = BUILD_CONFIGS[coreName];
382
+ if (!config) {
383
+ throw new CoreBuildError('NO_BUILD_CONFIG', coreName);
384
+ }
385
+
386
+ // Check prerequisites
387
+ const prereqs = checkBuildPrerequisites();
388
+ if (!prereqs.ok) {
389
+ throw new CoreBuildError('MISSING_TOOLS', prereqs.missing.join(', '));
390
+ }
391
+
392
+ const coresDir = getCoresDirectory();
393
+ const destPath = join(coresDir, config.installedFile);
394
+
395
+ // Check if already exists
396
+ if (existsSync(destPath)) {
397
+ onProgress?.({ phase: "complete", message: "Core already installed" });
398
+ return destPath;
399
+ }
400
+
401
+ ensureDirectory(coresDir);
402
+
403
+ // Create temp directory for build
404
+ const buildDir = join(tmpdir(), `emoemu-build-${coreName}-${Date.now()}`);
405
+ ensureDirectory(buildDir);
406
+
407
+ const repoDir = join(buildDir, coreName);
408
+
409
+ try {
410
+ // Clone repository at specific commit for reproducible builds
411
+ onProgress?.({
412
+ phase: "cloning",
413
+ message: `Cloning ${config.repo} at ${config.commit.slice(0, SHORT_COMMIT_HASH_LENGTH)}...`,
414
+ });
415
+
416
+ logger.info(`Cloning ${config.repo} at commit ${config.commit} to ${repoDir}`, "CoreBuilder");
417
+
418
+ // Use git init + fetch + checkout to get only the specific commit
419
+ // This is more efficient than cloning everything and ensures reproducibility
420
+ ensureDirectory(repoDir);
421
+
422
+ await runCommand("git", ["init"], {
423
+ cwd: repoDir,
424
+ onProgress,
425
+ phase: "cloning",
426
+ });
427
+
428
+ await runCommand("git", ["remote", "add", "origin", config.repo], {
429
+ cwd: repoDir,
430
+ onProgress,
431
+ phase: "cloning",
432
+ });
433
+
434
+ await runCommand("git", ["fetch", "--depth", "1", "origin", config.commit], {
435
+ cwd: repoDir,
436
+ onProgress,
437
+ phase: "cloning",
438
+ });
439
+
440
+ await runCommand("git", ["checkout", "FETCH_HEAD"], {
441
+ cwd: repoDir,
442
+ onProgress,
443
+ phase: "cloning",
444
+ });
445
+
446
+ // Apply source patches for macOS compatibility
447
+ applySourcePatches(repoDir);
448
+
449
+ // Create stub OpenGL library for mupen64plus (avoids macOS OpenGL loading hang)
450
+ let stubLibPath: string | null = null;
451
+ if (coreName === "mupen64plus_next") {
452
+ onProgress?.({
453
+ phase: "building",
454
+ message: "Creating stub OpenGL library...",
455
+ });
456
+ stubLibPath = createGLStubLibrary(buildDir);
457
+ }
458
+
459
+ // Build
460
+ onProgress?.({
461
+ phase: "building",
462
+ message: `Building ${config.description}...`,
463
+ });
464
+
465
+ const cpuCount = getCpuCount();
466
+ const baseArgs = ["-j" + cpuCount, ...config.buildArgs];
467
+
468
+ // Add stub library to link args for mupen64plus
469
+ const makeArgs = (stubLibPath && coreName === "mupen64plus_next")
470
+ ? [...baseArgs, `GL_LIB=-L${buildDir} -lGL_stub`]
471
+ : baseArgs;
472
+
473
+ logger.info(`Building with: make ${makeArgs.join(" ")}`, "CoreBuilder");
474
+
475
+ // Count compilation targets for progress tracking
476
+ onProgress?.({
477
+ phase: "building",
478
+ message: "Analyzing build targets...",
479
+ });
480
+ const totalTargets = getMakeTargetCount(repoDir, makeArgs);
481
+
482
+ await runCommand("make", makeArgs, {
483
+ cwd: repoDir,
484
+ onProgress,
485
+ phase: "building",
486
+ totalTargets,
487
+ });
488
+
489
+ // Install
490
+ onProgress?.({
491
+ phase: "installing",
492
+ message: "Installing core...",
493
+ });
494
+
495
+ const builtPath = join(repoDir, config.outputFile);
496
+ if (!existsSync(builtPath)) {
497
+ throw new CoreBuildError('OUTPUT_NOT_FOUND', builtPath);
498
+ }
499
+
500
+ // Copy stub library first (needed before fixing library paths)
501
+ if (stubLibPath && coreName === "mupen64plus_next") {
502
+ const stubDestPath = join(coresDir, GL_STUB_LIB_NAME);
503
+ copyFileSync(stubLibPath, stubDestPath);
504
+ execSync(`chmod 755 "${stubDestPath}"`);
505
+ logger.info(`Installed stub OpenGL library to ${stubDestPath}`, "CoreBuilder");
506
+ }
507
+
508
+ // Copy the core
509
+ copyFileSync(builtPath, destPath);
510
+
511
+ // Make executable
512
+ execSync(`chmod 755 "${destPath}"`);
513
+
514
+ // Fix library paths for mupen64plus to use absolute path to stub library
515
+ if (coreName === "mupen64plus_next") {
516
+ fixLibraryPaths(destPath, coresDir);
517
+ }
518
+
519
+ logger.info(`Installed ${coreName} to ${destPath}`, "CoreBuilder");
520
+
521
+ onProgress?.({
522
+ phase: "complete",
523
+ message: "Build complete!",
524
+ });
525
+
526
+ return destPath;
527
+ } catch (error) {
528
+ const errorMessage = getErrorMessage(error);
529
+ logger.error(`Failed to build ${coreName}: ${errorMessage}`, "CoreBuilder");
530
+
531
+ onProgress?.({
532
+ phase: "error",
533
+ message: `Build failed: ${errorMessage}`,
534
+ });
535
+
536
+ throw error;
537
+ } finally {
538
+ // Clean up build directory
539
+ try {
540
+ rmSync(buildDir, { recursive: true, force: true });
541
+ } catch {
542
+ // Ignore cleanup errors
543
+ }
544
+ }
545
+ };
546
+
547
+ /** Percentage multiplier for progress calculations */
548
+ const PERCENT_MULTIPLIER = 100;
549
+
550
+ /**
551
+ * Run a command with progress reporting
552
+ */
553
+ const runCommand = (
554
+ command: string,
555
+ args: string[],
556
+ options: {
557
+ cwd?: string;
558
+ onProgress?: (progress: BuildProgress) => void;
559
+ phase: BuildPhase;
560
+ /** Total number of compilation targets for progress tracking */
561
+ totalTargets?: number | null;
562
+ }
563
+ ): Promise<void> => {
564
+ return new Promise((resolve, reject) => {
565
+ const proc = spawn(command, args, {
566
+ cwd: options.cwd,
567
+ stdio: ["ignore", "pipe", "pipe"],
568
+ });
569
+
570
+ const outputLines: string[] = [];
571
+ const MAX_OUTPUT_LINES = 50;
572
+ let compiledCount = 0;
573
+
574
+ const handleOutput = (data: Buffer): void => {
575
+ const lines = data.toString().split("\n").filter(Boolean);
576
+ for (const line of lines) {
577
+ outputLines.push(line);
578
+ if (outputLines.length > MAX_OUTPUT_LINES) {
579
+ outputLines.shift();
580
+ }
581
+
582
+ // Track compilation progress
583
+ if (options.totalTargets && isCompilationLine(line)) {
584
+ compiledCount++;
585
+ }
586
+ }
587
+
588
+ // Report last line as progress
589
+ const lastLine = lines[lines.length - 1];
590
+ if (lastLine && options.onProgress) {
591
+ const progress: BuildProgress = {
592
+ phase: options.phase,
593
+ message: lastLine,
594
+ output: outputLines,
595
+ };
596
+
597
+ // Add progress percentage if we're tracking compilation
598
+ if (options.totalTargets && compiledCount > 0) {
599
+ progress.progressPercent = Math.min(
600
+ Math.round((compiledCount / options.totalTargets) * PERCENT_MULTIPLIER),
601
+ PERCENT_MULTIPLIER
602
+ );
603
+ progress.progressText = `${compiledCount} of ${options.totalTargets} files`;
604
+ }
605
+
606
+ options.onProgress(progress);
607
+ }
608
+ };
609
+
610
+ proc.stdout.on("data", handleOutput);
611
+ proc.stderr.on("data", handleOutput);
612
+
613
+ proc.on("close", (code) => {
614
+ if (code === 0) {
615
+ resolve();
616
+ } else {
617
+ const errorOutput = outputLines.slice(-ERROR_OUTPUT_LINES).join("\n");
618
+ reject(new Error(`${command} exited with code ${code}\n${errorOutput}`));
619
+ }
620
+ });
621
+
622
+ proc.on("error", (err) => {
623
+ reject(new Error(`Failed to start ${command}: ${err.message}`));
624
+ });
625
+ });
626
+ };
627
+
628
+ /**
629
+ * Get list of cores that can be built from source
630
+ */
631
+ export const getBuildableCores = (): Array<{ name: string; description: string }> => {
632
+ if (platform() !== "darwin" || arch() !== "arm64") {
633
+ return [];
634
+ }
635
+
636
+ return Object.entries(BUILD_CONFIGS)
637
+ .filter((entry): entry is [string, CoreBuildConfig] => entry[1] !== undefined)
638
+ .map(([name, config]) => ({
639
+ name,
640
+ description: config.description,
641
+ }));
642
+ };
643
+
644
+ export * from "./types";
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Core builder error types
3
+ */
4
+
5
+ import { createTypedError } from '../../utils/typedError';
6
+
7
+ export type CoreBuildErrorCode =
8
+ | 'NO_BUILD_CONFIG'
9
+ | 'MISSING_TOOLS'
10
+ | 'OUTPUT_NOT_FOUND';
11
+
12
+ const { TypedError, isTypedError } = createTypedError<CoreBuildErrorCode>('CoreBuildError');
13
+ export const CoreBuildError = TypedError;
14
+ export type CoreBuildError = InstanceType<typeof TypedError>;
15
+ export const isCoreBuildError = isTypedError;