spindb 0.36.1 → 0.36.2

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.
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Library environment utilities for dynamically-linked engine binaries.
3
+ *
4
+ * MariaDB, Redis, and Valkey hostdb binaries are linked against Homebrew's
5
+ * OpenSSL at absolute paths (e.g. /opt/homebrew/opt/openssl@3/lib/libssl.3.dylib).
6
+ * On systems without that library, they fail with cryptic dyld errors.
7
+ *
8
+ * This module provides:
9
+ * - getLibraryEnv(): sets DYLD_FALLBACK_LIBRARY_PATH / LD_LIBRARY_PATH so
10
+ * the dynamic linker checks {binPath}/lib first (preparing for when hostdb
11
+ * bundles dylibs alongside binaries).
12
+ * - detectLibraryError(): scans process output for library-loading patterns
13
+ * and returns an actionable error message.
14
+ */
15
+
16
+ import { platform as osPlatform } from 'os'
17
+ import { join } from 'path'
18
+
19
+ /**
20
+ * Returns env vars that point the dynamic linker at {binPath}/lib.
21
+ * On macOS: DYLD_FALLBACK_LIBRARY_PATH
22
+ * On Linux: LD_LIBRARY_PATH
23
+ * On Windows: returns undefined (not applicable).
24
+ *
25
+ * Usage: spread into spawn env: `{ ...process.env, ...getLibraryEnv(binPath) }`
26
+ */
27
+ export function getLibraryEnv(
28
+ binPath: string,
29
+ ): Record<string, string> | undefined {
30
+ const plat = osPlatform()
31
+ const libDir = join(binPath, 'lib')
32
+
33
+ if (plat === 'darwin') {
34
+ return { DYLD_FALLBACK_LIBRARY_PATH: libDir }
35
+ }
36
+ if (plat === 'linux') {
37
+ return { LD_LIBRARY_PATH: libDir }
38
+ }
39
+ return undefined
40
+ }
41
+
42
+ /**
43
+ * Scans stderr/log output for dynamic library loading errors and returns
44
+ * an actionable message, or null if no library error was detected.
45
+ */
46
+ export function detectLibraryError(
47
+ output: string,
48
+ engineName: string,
49
+ ): string | null {
50
+ if (!output) return null
51
+
52
+ const plat = osPlatform()
53
+ const lower = output.toLowerCase()
54
+
55
+ // macOS dyld errors
56
+ if (
57
+ lower.includes('library not loaded') ||
58
+ lower.includes('dyld:') ||
59
+ lower.includes('dyld[')
60
+ ) {
61
+ const needsOpenssl =
62
+ lower.includes('libssl') || lower.includes('libcrypto')
63
+
64
+ if (needsOpenssl && plat === 'darwin') {
65
+ return (
66
+ `${engineName} failed to start: missing OpenSSL libraries.\n` +
67
+ `The downloaded binary requires OpenSSL 3 which is not installed.\n` +
68
+ `Fix: brew install openssl@3\n` +
69
+ `Alternatively, re-download binaries after hostdb ships relocatable builds.`
70
+ )
71
+ }
72
+
73
+ return (
74
+ `${engineName} failed to start: a required dynamic library could not be loaded.\n` +
75
+ `This typically means the hostdb binary was built against libraries not present on this system.\n` +
76
+ (plat === 'darwin'
77
+ ? `Try: brew install openssl@3\n`
78
+ : `Try: sudo apt-get install libssl-dev (or the equivalent for your distro)\n`) +
79
+ `See: https://github.com/robertjbass/hostdb/issues`
80
+ )
81
+ }
82
+
83
+ // Linux GLIBC version errors
84
+ if (lower.includes('glibc') || lower.includes('libc.so')) {
85
+ return (
86
+ `${engineName} failed to start: incompatible system C library (GLIBC).\n` +
87
+ `The downloaded binary requires a newer GLIBC version than is installed.\n` +
88
+ `Options:\n` +
89
+ ` - Upgrade your OS to a newer version\n` +
90
+ ` - Use Docker: spindb can run inside containers with newer GLIBC\n` +
91
+ `See: https://github.com/robertjbass/hostdb/issues`
92
+ )
93
+ }
94
+
95
+ // Generic shared library errors on Linux
96
+ if (
97
+ lower.includes('error while loading shared libraries') ||
98
+ lower.includes('cannot open shared object file')
99
+ ) {
100
+ const needsOpenssl =
101
+ lower.includes('libssl') || lower.includes('libcrypto')
102
+
103
+ if (needsOpenssl) {
104
+ return (
105
+ `${engineName} failed to start: missing OpenSSL libraries.\n` +
106
+ `Fix: sudo apt-get install libssl-dev (Debian/Ubuntu)\n` +
107
+ ` sudo dnf install openssl-devel (Fedora/RHEL)\n` +
108
+ `See: https://github.com/robertjbass/hostdb/issues`
109
+ )
110
+ }
111
+
112
+ return (
113
+ `${engineName} failed to start: a required shared library is missing.\n` +
114
+ `Check the error output above for the specific library name and install it.\n` +
115
+ `See: https://github.com/robertjbass/hostdb/issues`
116
+ )
117
+ }
118
+
119
+ return null
120
+ }
@@ -52,6 +52,7 @@ import {
52
52
  type UserCredentials,
53
53
  } from '../../types'
54
54
  import { parseTSVToQueryResult } from '../../core/query-parser'
55
+ import { getLibraryEnv, detectLibraryError } from '../../core/library-env'
55
56
 
56
57
  const execAsync = promisify(exec)
57
58
 
@@ -256,18 +257,27 @@ export class MariaDBEngine extends BaseEngine {
256
257
  const cmd = `"${installDb}" --datadir="${dataDir}"`
257
258
 
258
259
  return new Promise((resolve, reject) => {
259
- exec(cmd, { timeout: 120000 }, async (error, stdout, stderr) => {
260
- if (error) {
261
- await cleanupOnFailure()
262
- reject(
263
- new Error(
264
- `MariaDB initialization failed with code ${error.code}: ${stderr || stdout || error.message}`,
265
- ),
266
- )
267
- } else {
268
- resolve(dataDir)
269
- }
270
- })
260
+ exec(
261
+ cmd,
262
+ { timeout: 120000, env: { ...process.env, ...getLibraryEnv(binPath) } },
263
+ async (error, stdout, stderr) => {
264
+ if (error) {
265
+ await cleanupOnFailure()
266
+ const libError = detectLibraryError(
267
+ stderr || stdout || error.message,
268
+ 'MariaDB',
269
+ )
270
+ reject(
271
+ new Error(
272
+ libError ||
273
+ `MariaDB initialization failed with code ${error.code}: ${stderr || stdout || error.message}`,
274
+ ),
275
+ )
276
+ } else {
277
+ resolve(dataDir)
278
+ }
279
+ },
280
+ )
271
281
  })
272
282
  }
273
283
 
@@ -293,6 +303,7 @@ export class MariaDBEngine extends BaseEngine {
293
303
  return new Promise((resolve, reject) => {
294
304
  const proc = spawn(installDb, args, {
295
305
  stdio: ['ignore', 'pipe', 'pipe'],
306
+ env: { ...process.env, ...getLibraryEnv(binPath) },
296
307
  })
297
308
 
298
309
  let stdout = ''
@@ -310,9 +321,11 @@ export class MariaDBEngine extends BaseEngine {
310
321
  resolve(dataDir)
311
322
  } else {
312
323
  await cleanupOnFailure()
324
+ const libError = detectLibraryError(stderr || stdout, 'MariaDB')
313
325
  reject(
314
326
  new Error(
315
- `MariaDB initialization failed with code ${code}: ${stderr || stdout}`,
327
+ libError ||
328
+ `MariaDB initialization failed with code ${code}: ${stderr || stdout}`,
316
329
  ),
317
330
  )
318
331
  }
@@ -384,11 +397,14 @@ export class MariaDBEngine extends BaseEngine {
384
397
 
385
398
  let proc: ReturnType<typeof spawn> | null = null
386
399
 
400
+ const libraryEnv = getLibraryEnv(binPath)
401
+
387
402
  if (isWindows()) {
388
403
  proc = spawn(mysqld, args, {
389
404
  stdio: ['ignore', 'pipe', 'pipe'],
390
405
  detached: true,
391
406
  windowsHide: true,
407
+ env: { ...process.env, ...libraryEnv },
392
408
  })
393
409
 
394
410
  proc.stdout?.on('data', (data: Buffer) => {
@@ -403,6 +419,7 @@ export class MariaDBEngine extends BaseEngine {
403
419
  proc = spawn(mysqld, args, {
404
420
  stdio: ['ignore', 'ignore', 'ignore'],
405
421
  detached: true,
422
+ env: { ...process.env, ...libraryEnv },
406
423
  })
407
424
  proc.unref()
408
425
  }
@@ -465,7 +482,21 @@ export class MariaDBEngine extends BaseEngine {
465
482
  if (proc) {
466
483
  proc.removeListener('error', errorHandler)
467
484
  }
468
- reject(new Error('MariaDB failed to start within timeout'))
485
+
486
+ // Check log file for library errors
487
+ let libError: string | null = null
488
+ try {
489
+ const logContent = await readFile(logFile, 'utf-8')
490
+ libError = detectLibraryError(logContent, 'MariaDB')
491
+ } catch {
492
+ // Log file might not exist
493
+ }
494
+
495
+ reject(
496
+ new Error(
497
+ libError || 'MariaDB failed to start within timeout',
498
+ ),
499
+ )
469
500
  }
470
501
  }
471
502
  }
@@ -45,6 +45,7 @@ import {
45
45
  type UserCredentials,
46
46
  } from '../../types'
47
47
  import { parseRedisResult } from '../../core/query-parser'
48
+ import { getLibraryEnv, detectLibraryError } from '../../core/library-env'
48
49
 
49
50
  const execAsync = promisify(exec)
50
51
 
@@ -477,6 +478,12 @@ export class RedisEngine extends BaseEngine {
477
478
 
478
479
  logDebug(`Using redis-server for version ${version}: ${redisServer}`)
479
480
 
481
+ // Compute library fallback paths from the binary directory
482
+ // redisServer is e.g. /path/to/redis-8.4.0-darwin-arm64/bin/redis-server
483
+ // We need the parent directory (without /bin/redis-server)
484
+ const binBaseDir = binaryPath || this.getBinaryPath(version)
485
+ const libraryEnv = getLibraryEnv(binBaseDir)
486
+
480
487
  const containerDir = paths.getContainerPath(name, { engine: ENGINE })
481
488
  const configPath = join(containerDir, 'redis.conf')
482
489
  const dataDir = paths.getContainerDataPath(name, { engine: ENGINE })
@@ -533,6 +540,7 @@ export class RedisEngine extends BaseEngine {
533
540
  stdio: ['ignore', 'pipe', 'pipe'],
534
541
  detached: true,
535
542
  windowsHide: true,
543
+ env: { ...process.env, ...libraryEnv },
536
544
  }
537
545
 
538
546
  // Convert Windows path to Cygwin format for MSYS2/Cygwin-built binaries
@@ -603,6 +611,16 @@ export class RedisEngine extends BaseEngine {
603
611
  logContent = '(log file not found or empty)'
604
612
  }
605
613
 
614
+ // Check for library loading errors first
615
+ const libError = detectLibraryError(
616
+ stderrOutput + logContent,
617
+ 'Redis',
618
+ )
619
+ if (libError) {
620
+ reject(new Error(libError))
621
+ return
622
+ }
623
+
606
624
  const errorDetails = [
607
625
  portError || 'Redis failed to start within timeout.',
608
626
  `Binary: ${redisServer}`,
@@ -625,6 +643,7 @@ export class RedisEngine extends BaseEngine {
625
643
  return new Promise((resolve, reject) => {
626
644
  const proc = spawn(redisServer, [configPath], {
627
645
  stdio: ['ignore', 'pipe', 'pipe'],
646
+ env: { ...process.env, ...libraryEnv },
628
647
  })
629
648
 
630
649
  let stdout = ''
@@ -678,6 +697,16 @@ export class RedisEngine extends BaseEngine {
678
697
  logContent = '(log file not found or empty)'
679
698
  }
680
699
 
700
+ // Check for library loading errors
701
+ const libError = detectLibraryError(
702
+ stderr + logContent,
703
+ 'Redis',
704
+ )
705
+ if (libError) {
706
+ reject(new Error(libError))
707
+ return
708
+ }
709
+
681
710
  const errorDetails = [
682
711
  'Redis failed to start within timeout.',
683
712
  `Binary: ${redisServer}`,
@@ -700,6 +729,16 @@ export class RedisEngine extends BaseEngine {
700
729
  logContent = ''
701
730
  }
702
731
 
732
+ // Check for library loading errors on non-zero exit
733
+ const libError = detectLibraryError(
734
+ stderr + stdout + logContent,
735
+ 'Redis',
736
+ )
737
+ if (libError) {
738
+ reject(new Error(libError))
739
+ return
740
+ }
741
+
703
742
  const errorDetails = [
704
743
  stderr || stdout || `redis-server exited with code ${code}`,
705
744
  logContent ? `Log content:\n${logContent}` : '',
@@ -45,6 +45,7 @@ import {
45
45
  type UserCredentials,
46
46
  } from '../../types'
47
47
  import { parseRedisResult } from '../../core/query-parser'
48
+ import { getLibraryEnv, detectLibraryError } from '../../core/library-env'
48
49
 
49
50
  const execAsync = promisify(exec)
50
51
 
@@ -486,6 +487,10 @@ export class ValkeyEngine extends BaseEngine {
486
487
 
487
488
  logDebug(`Using valkey-server for version ${version}: ${valkeyServer}`)
488
489
 
490
+ // Compute library fallback paths from the binary directory
491
+ const binBaseDir = binaryPath || this.getBinaryPath(version)
492
+ const libraryEnv = getLibraryEnv(binBaseDir)
493
+
489
494
  const containerDir = paths.getContainerPath(name, { engine: ENGINE })
490
495
  const configPath = join(containerDir, 'valkey.conf')
491
496
  const dataDir = paths.getContainerDataPath(name, { engine: ENGINE })
@@ -542,6 +547,7 @@ export class ValkeyEngine extends BaseEngine {
542
547
  stdio: ['ignore', 'pipe', 'pipe'],
543
548
  detached: true,
544
549
  windowsHide: true,
550
+ env: { ...process.env, ...libraryEnv },
545
551
  }
546
552
 
547
553
  // Convert Windows path to Cygwin format for Cygwin-built binaries
@@ -612,6 +618,16 @@ export class ValkeyEngine extends BaseEngine {
612
618
  logContent = '(log file not found or empty)'
613
619
  }
614
620
 
621
+ // Check for library loading errors first
622
+ const libError = detectLibraryError(
623
+ stderrOutput + logContent,
624
+ 'Valkey',
625
+ )
626
+ if (libError) {
627
+ reject(new Error(libError))
628
+ return
629
+ }
630
+
615
631
  const errorDetails = [
616
632
  portError || 'Valkey failed to start within timeout.',
617
633
  `Binary: ${valkeyServer}`,
@@ -634,6 +650,7 @@ export class ValkeyEngine extends BaseEngine {
634
650
  return new Promise((resolve, reject) => {
635
651
  const proc = spawn(valkeyServer, [configPath], {
636
652
  stdio: ['ignore', 'pipe', 'pipe'],
653
+ env: { ...process.env, ...libraryEnv },
637
654
  })
638
655
 
639
656
  let stdout = ''
@@ -678,6 +695,23 @@ export class ValkeyEngine extends BaseEngine {
678
695
  reject(new Error(portError))
679
696
  return
680
697
  }
698
+
699
+ // Check for library loading errors
700
+ let logContent = ''
701
+ try {
702
+ logContent = await readFile(logFile, 'utf-8')
703
+ } catch {
704
+ logContent = ''
705
+ }
706
+ const libError = detectLibraryError(
707
+ stderr + logContent,
708
+ 'Valkey',
709
+ )
710
+ if (libError) {
711
+ reject(new Error(libError))
712
+ return
713
+ }
714
+
681
715
  reject(
682
716
  new Error(
683
717
  `Valkey failed to start within timeout. Check logs at: ${logFile}`,
@@ -685,6 +719,12 @@ export class ValkeyEngine extends BaseEngine {
685
719
  )
686
720
  }
687
721
  } else {
722
+ // Check for library loading errors on non-zero exit
723
+ const libError = detectLibraryError(stderr || stdout, 'Valkey')
724
+ if (libError) {
725
+ reject(new Error(libError))
726
+ return
727
+ }
688
728
  reject(
689
729
  new Error(
690
730
  stderr || stdout || `valkey-server exited with code ${code}`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spindb",
3
- "version": "0.36.1",
3
+ "version": "0.36.2",
4
4
  "author": "Bob Bass <bob@bbass.co>",
5
5
  "license": "PolyForm-Noncommercial-1.0.0",
6
6
  "description": "Zero-config Docker-free local database containers. Create, backup, and clone a variety of popular databases.",