@@ -45,7 +45,7 @@ export function generateLaunchdPlist(service: ServiceInstance): LaunchdPlist {
45
45
} )
46
46
47
47
// Find the executable path
48
- const executablePath = findBinaryInPath ( definition . executable ) || definition . executable
48
+ let executablePath = findBinaryInPath ( definition . executable ) || definition . executable
49
49
50
50
// Compute runtime PATH and dynamic library search paths for packages started by launchd
51
51
// launchd does not inherit shell env, so we must include env-specific paths here.
@@ -59,6 +59,26 @@ export function generateLaunchdPlist(service: ServiceInstance): LaunchdPlist {
59
59
? Boolean ( config . services . shouldAutoStart )
60
60
: Boolean ( service . enabled ) )
61
61
62
+ // For MySQL/MariaDB: resolve real binary path from shim to get proper library paths
63
+ if ( ( definition . name === 'mysql' || definition . name === 'mariadb' ) && executablePath && fs . existsSync ( executablePath ) ) {
64
+ try {
65
+ const content = fs . readFileSync ( executablePath , 'utf-8' )
66
+ if ( content . startsWith ( '#!/bin/sh' ) || content . startsWith ( '#!/usr/bin/env bash' ) ) {
67
+ // Extract real path from shim's exec line
68
+ const execMatch = content . match ( / e x e c \s + " ( [ ^ " ] + ) " / )
69
+ if ( execMatch && execMatch [ 1 ] ) {
70
+ executablePath = execMatch [ 1 ]
71
+ }
72
+ }
73
+ }
74
+ catch {
75
+ // Continue with original path if we can't read it
76
+ }
77
+ }
78
+
79
+ // Track library directories for wrapper script generation
80
+ let libraryDirs : string [ ] = [ ]
81
+
62
82
try {
63
83
const binDir = path . dirname ( executablePath )
64
84
const versionDir = path . dirname ( binDir )
@@ -76,7 +96,6 @@ export function generateLaunchdPlist(service: ServiceInstance): LaunchdPlist {
76
96
envVars . PATH = [ envBin , envSbin , basePath ] . filter ( Boolean ) . join ( ':' )
77
97
78
98
// Discover all package lib directories under this env root
79
- const libraryDirs : string [ ] = [ ]
80
99
81
100
const pushIfExists = ( p : string ) => {
82
101
try {
@@ -91,19 +110,36 @@ export function generateLaunchdPlist(service: ServiceInstance): LaunchdPlist {
91
110
pushIfExists ( path . join ( envRoot , 'lib64' ) )
92
111
93
112
// Scan domain/version directories for lib folders
113
+ // Recursively search up to 2 levels deep for version directories
114
+ const scanForLibraries = ( dir : string , depth : number = 0 ) => {
115
+ if ( depth > 2 ) return
116
+ try {
117
+ const entries = fs . readdirSync ( dir , { withFileTypes : true } )
118
+ for ( const entry of entries ) {
119
+ if ( ! entry . isDirectory ( ) ) continue
120
+
121
+ const entryPath = path . join ( dir , entry . name )
122
+
123
+ // If this is a version directory (starts with 'v'), check for lib
124
+ if ( entry . name . startsWith ( 'v' ) ) {
125
+ pushIfExists ( path . join ( entryPath , 'lib' ) )
126
+ pushIfExists ( path . join ( entryPath , 'lib64' ) )
127
+ }
128
+ // Otherwise recurse into subdirectories (unless it's a common system dir)
129
+ else if ( ! [ 'bin' , 'sbin' , 'share' , 'include' , 'etc' , 'pkgs' , '.tmp' ] . includes ( entry . name ) ) {
130
+ scanForLibraries ( entryPath , depth + 1 )
131
+ }
132
+ }
133
+ }
134
+ catch { }
135
+ }
136
+
94
137
try {
95
138
const entries = fs . readdirSync ( envRoot , { withFileTypes : true } )
96
139
for ( const entry of entries ) {
97
- if ( ! entry . isDirectory ( ) )
98
- continue
99
- if ( [ 'bin' , 'sbin' , 'share' , 'include' , 'etc' , 'pkgs' ] . includes ( entry . name ) )
100
- continue
101
- const domainPath = path . join ( envRoot , entry . name )
102
- const versions = fs . readdirSync ( domainPath , { withFileTypes : true } ) . filter ( v => v . isDirectory ( ) && v . name . startsWith ( 'v' ) )
103
- for ( const ver of versions ) {
104
- pushIfExists ( path . join ( domainPath , ver . name , 'lib' ) )
105
- pushIfExists ( path . join ( domainPath , ver . name , 'lib64' ) )
106
- }
140
+ if ( ! entry . isDirectory ( ) ) continue
141
+ if ( [ 'bin' , 'sbin' , 'share' , 'include' , 'etc' , 'pkgs' ] . includes ( entry . name ) ) continue
142
+ scanForLibraries ( path . join ( envRoot , entry . name ) , 0 )
107
143
}
108
144
}
109
145
catch { }
@@ -121,9 +157,27 @@ export function generateLaunchdPlist(service: ServiceInstance): LaunchdPlist {
121
157
// Best-effort only
122
158
}
123
159
160
+ // For macOS: launchd strips DYLD_* variables, so we need a wrapper script for databases
161
+ // that explicitly sets DYLD_LIBRARY_PATH before executing the binary
162
+ let finalExecutablePath = executablePath
163
+ if ( ( definition . name === 'mysql' || definition . name === 'mariadb' || definition . name === 'postgres' ) && platform ( ) === 'darwin' ) {
164
+ if ( libraryDirs && libraryDirs . length > 0 ) {
165
+ // Generate wrapper script
166
+ const wrapperDir = path . join ( homedir ( ) , '.local' , 'share' , 'launchpad' , 'wrappers' )
167
+ fs . mkdirSync ( wrapperDir , { recursive : true } )
168
+ const wrapperPath = path . join ( wrapperDir , `${ definition . name } -wrapper.sh` )
169
+
170
+ const wrapperContent = `#!/bin/bash
171
+ exec env DYLD_LIBRARY_PATH="${ libraryDirs . join ( ':' ) } " "${ executablePath } " "$@"
172
+ `
173
+ fs . writeFileSync ( wrapperPath , wrapperContent , { mode : 0o755 } )
174
+ finalExecutablePath = wrapperPath
175
+ }
176
+ }
177
+
124
178
return {
125
179
Label : `com.launchpad.${ definition . name || service . name } ` ,
126
- ProgramArguments : [ executablePath , ...resolvedArgs ] ,
180
+ ProgramArguments : [ finalExecutablePath , ...resolvedArgs ] ,
127
181
WorkingDirectory : definition . workingDirectory || dataDir || '' ,
128
182
EnvironmentVariables : {
129
183
...Object . fromEntries ( Object . entries ( definition . env || { } ) . map ( ( [ k , v ] ) => {
0 commit comments