@@ -687,7 +687,15 @@ Thanks for helping us make Launchpad better! 🙏
687687 if ( ! fs . existsSync ( binDir ) )
688688 return
689689
690- // Find Launchpad-installed readline library
690+ // Ensure PHP runtime dependencies are installed into this environment
691+ try {
692+ await this . ensurePhpDependenciesInstalled ( )
693+ }
694+ catch ( err ) {
695+ console . warn ( `⚠️ Failed to install PHP dependencies: ${ err instanceof Error ? err . message : String ( err ) } ` )
696+ }
697+
698+ // Find Launchpad-installed libraries for PHP dependencies
691699 const launchpadLibraryPaths = await this . findLaunchpadLibraryPaths ( )
692700
693701 // Get all PHP binaries
@@ -704,6 +712,14 @@ Thanks for helping us make Launchpad better! 🙏
704712 // Move original binary
705713 fs . renameSync ( originalBinary , shimPath )
706714
715+ // First, on macOS fix absolute Homebrew dylib references to Launchpad-managed libs
716+ try {
717+ await this . fixMacOSDylibs ( packageDir , launchpadLibraryPaths )
718+ }
719+ catch ( err ) {
720+ console . warn ( `⚠️ Could not fix macOS dylib references: ${ err instanceof Error ? err . message : String ( err ) } ` )
721+ }
722+
707723 // Create wrapper script with library paths including Launchpad libraries
708724 let libraryPaths = '/usr/local/lib:/usr/lib:/lib'
709725 if ( launchpadLibraryPaths . length > 0 ) {
@@ -729,14 +745,100 @@ exec "${shimPath}" "$@"
729745
730746 console . log ( `🔗 Created PHP shims with Launchpad library paths for ${ binaries . length } binaries` )
731747
732- // Also try to create Homebrew-compatible symlinks for the precompiled binaries
733- await this . createHomebrewCompatSymlinks ( launchpadLibraryPaths )
748+ // Do not rely on Homebrew; shims use Launchpad-managed library paths only
734749 }
735750 catch ( error ) {
736751 console . warn ( `⚠️ Failed to create PHP shims: ${ error instanceof Error ? error . message : String ( error ) } ` )
737752 }
738753 }
739754
755+ /**
756+ * Fix macOS dylib references in php.original that point to Homebrew paths
757+ */
758+ private async fixMacOSDylibs ( packageDir : string , launchpadLibraryPaths : string [ ] ) : Promise < void > {
759+ if ( process . platform !== 'darwin' )
760+ return
761+
762+ const phpOriginal = path . join ( packageDir , 'bin' , 'php.original' )
763+ if ( ! fs . existsSync ( phpOriginal ) )
764+ return
765+
766+ try {
767+ const { execSync } = await import ( 'node:child_process' )
768+ const output = execSync ( `otool -L "${ phpOriginal } "` , { encoding : 'utf8' } )
769+
770+ const lines = output . split ( '\n' ) . slice ( 1 ) . map ( l => l . trim ( ) ) . filter ( Boolean )
771+ const candidates : Array < { oldPath : string , libName : string } > = [ ]
772+ for ( const line of lines ) {
773+ const match = line . match ( / ^ ( \S + ) \( / )
774+ if ( ! match )
775+ continue
776+ const oldPath = match [ 1 ]
777+ if ( oldPath . includes ( '/opt/homebrew/' ) || oldPath . includes ( '/usr/local/opt/' ) || oldPath . includes ( '/Cellar/' ) ) {
778+ const libName = path . basename ( oldPath )
779+ candidates . push ( { oldPath, libName } )
780+ }
781+ }
782+
783+ if ( candidates . length === 0 )
784+ return
785+
786+ // Build search directories from known Launchpad libraries and env lib dirs
787+ const searchDirs = new Set < string > ( )
788+ for ( const p of launchpadLibraryPaths ) searchDirs . add ( p )
789+
790+ // Also scan installPath packages for lib directories
791+ try {
792+ const domains = fs . readdirSync ( this . installPath )
793+ for ( const domain of domains ) {
794+ const domainPath = path . join ( this . installPath , domain )
795+ if ( ! fs . existsSync ( domainPath ) || ! fs . statSync ( domainPath ) . isDirectory ( ) )
796+ continue
797+ try {
798+ const versions = fs . readdirSync ( domainPath ) . filter ( v => v . startsWith ( 'v' ) )
799+ for ( const v of versions ) {
800+ const libDir = path . join ( domainPath , v , 'lib' )
801+ if ( fs . existsSync ( libDir ) )
802+ searchDirs . add ( libDir )
803+ }
804+ }
805+ catch { }
806+ }
807+ }
808+ catch { }
809+
810+ // For each candidate, try to find a replacement in our search dirs and rewrite
811+ for ( const { oldPath, libName } of candidates ) {
812+ let replacement : string | null = null
813+ for ( const dir of searchDirs ) {
814+ const testPath = path . join ( dir , libName )
815+ if ( fs . existsSync ( testPath ) ) {
816+ replacement = testPath
817+ break
818+ }
819+ }
820+
821+ if ( replacement ) {
822+ try {
823+ execSync ( `install_name_tool -change "${ oldPath } " "${ replacement } " "${ phpOriginal } "` )
824+ if ( config . verbose ) {
825+ console . log ( `🔧 Rewrote dylib reference: ${ oldPath } → ${ replacement } ` )
826+ }
827+ }
828+ catch ( e ) {
829+ console . warn ( `⚠️ install_name_tool failed for ${ libName } : ${ e instanceof Error ? e . message : String ( e ) } ` )
830+ }
831+ }
832+ }
833+ }
834+ catch ( error ) {
835+ // Non-fatal on systems without Xcode tools
836+ if ( config . verbose ) {
837+ console . warn ( `⚠️ otool/install_name_tool unavailable: ${ error instanceof Error ? error . message : String ( error ) } ` )
838+ }
839+ }
840+ }
841+
740842 /**
741843 * Find Launchpad-installed library paths for PHP dependencies
742844 */
@@ -771,7 +873,7 @@ exec "${shimPath}" "$@"
771873 // Also check global installation as fallback
772874 const globalDepDir = path . join ( process . env . HOME || '' , '.local' , 'share' , 'launchpad' , 'global' , depName )
773875 if ( fs . existsSync ( globalDepDir ) ) {
774- const versions = fs . readdirSync ( globalDepDir ) . filter ( d => d . startsWith ( 'v' ) ) . sort ( ) . reverse ( )
876+ const versions = fs . readdirSync ( globalDepDir ) . filter ( ( d : string ) => d . startsWith ( 'v' ) ) . sort ( ) . reverse ( )
775877 for ( const version of versions ) {
776878 const libDir = path . join ( globalDepDir , version , 'lib' )
777879 if ( fs . existsSync ( libDir ) ) {
@@ -792,9 +894,89 @@ exec "${shimPath}" "$@"
792894 console . warn ( `⚠️ Error finding Launchpad libraries: ${ error instanceof Error ? error . message : String ( error ) } ` )
793895 }
794896
897+ // Do not include Homebrew compatibility paths
898+
795899 return paths
796900 }
797901
902+ /**
903+ * Ensure PHP dynamic dependencies from pantry are installed in the current environment
904+ */
905+ private async ensurePhpDependenciesInstalled ( ) : Promise < void > {
906+ try {
907+ const deps = await this . getPhpDependenciesFromPantry ( )
908+ const depsSet = new Set < string > ( deps )
909+
910+ // Best-effort: detect required ICU major version and include matching unicode.org
911+ const icuMajor = await this . detectRequiredIcuMajor ( )
912+ if ( icuMajor ) {
913+ // Prefer caret range for the detected major
914+ depsSet . add ( `unicode.org@^${ icuMajor } .0.0` )
915+ }
916+
917+ const finalDeps = Array . from ( depsSet )
918+ if ( finalDeps . length === 0 )
919+ return
920+
921+ // Install all dependencies into this.installPath
922+ const { install } = await import ( './install' )
923+ // Suppress extra summary
924+ const original = process . env . LAUNCHPAD_SUPPRESS_INSTALL_SUMMARY
925+ process . env . LAUNCHPAD_SUPPRESS_INSTALL_SUMMARY = 'true'
926+ try {
927+ await install ( finalDeps , this . installPath )
928+ }
929+ finally {
930+ if ( original === undefined )
931+ delete process . env . LAUNCHPAD_SUPPRESS_INSTALL_SUMMARY
932+ else
933+ process . env . LAUNCHPAD_SUPPRESS_INSTALL_SUMMARY = original
934+ }
935+ }
936+ catch ( error ) {
937+ // Non-fatal, shims may still work without explicit deps
938+ if ( config . verbose ) {
939+ console . warn ( `⚠️ Could not ensure PHP dependencies: ${ error instanceof Error ? error . message : String ( error ) } ` )
940+ }
941+ }
942+ }
943+
944+ /**
945+ * Detect required ICU major version from php.original dylib references (macOS only)
946+ */
947+ private async detectRequiredIcuMajor ( ) : Promise < number | null > {
948+ if ( process . platform !== 'darwin' )
949+ return null
950+ try {
951+ const phpOriginal = path . join ( this . installPath , 'php.net' )
952+ if ( ! fs . existsSync ( phpOriginal ) )
953+ return null
954+
955+ // Find version directories
956+ const versions = fs . readdirSync ( phpOriginal ) . filter ( v => v . startsWith ( 'v' ) )
957+ if ( versions . length === 0 )
958+ return null
959+
960+ // Pick latest
961+ const latest = versions . sort ( ) . reverse ( ) [ 0 ]
962+ const originalPath = path . join ( phpOriginal , latest , 'bin' , 'php.original' )
963+ if ( ! fs . existsSync ( originalPath ) )
964+ return null
965+
966+ const { execSync } = await import ( 'node:child_process' )
967+ const output = execSync ( `otool -L "${ originalPath } "` , { encoding : 'utf8' } )
968+ const match = output . match ( / l i b i c u \w + \. ( \d + ) \. d y l i b / )
969+ if ( match ) {
970+ const major = Number . parseInt ( match [ 1 ] , 10 )
971+ return Number . isFinite ( major ) ? major : null
972+ }
973+ return null
974+ }
975+ catch {
976+ return null
977+ }
978+ }
979+
798980 /**
799981 * Create Homebrew-compatible symlinks for precompiled PHP binaries
800982 */
0 commit comments