Skip to content

Commit 23f705a

Browse files
committed
chore: wip
1 parent d4fa4fe commit 23f705a

File tree

7 files changed

+383
-29
lines changed

7 files changed

+383
-29
lines changed

docs/.vitepress/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ const sidebar = [
6565
text: 'Features',
6666
items: [
6767
{ text: 'Package Management', link: '/features/package-management' },
68+
{ text: 'Service Management', link: '/features/service-management' },
6869
{ text: 'Environment Management', link: '/features/environment-management' },
6970
{ text: 'Cache Management', link: '/features/cache-management' },
7071
{ text: 'Shim Creation', link: '/features/shim-creation' },

docs/index.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@ features:
2828
- title: "Executable Shims"
2929
icon: "🔄"
3030
details: "Create lightweight executable scripts that automatically run the correct versions of your tools with full environment context."
31-
- title: "Pantry-Powered"
31+
- title: "Custom & Pantry-Powered"
3232
icon: "🛠️"
33-
details: "Built on top of pkgx's Pantry for fast package installations."
33+
details: "Using custom builds, and built on top of pkgx's Pantry for fast package installations."
3434
- title: "Runtime Installation"
3535
icon: "🚀"
3636
details: "Direct installation of development runtimes like Bun and Node.js from official sources with automatic platform detection."

packages/launchpad/src/binary-downloader.ts

Lines changed: 186 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -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(/libicu\w+\.(\d+)\.dylib/)
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

Comments
 (0)