11use arwen:: elf:: ElfContainer ;
2+ use arwen:: macho:: MachoContainer ;
23use std:: {
34 collections:: HashMap ,
45 env,
56 fs:: { self , File } ,
67 path:: Path ,
78} ;
89
10+ fn is_elf_binary ( file_contents : & [ u8 ] ) -> bool {
11+ file_contents. len ( ) >= 4 && & file_contents[ 0 ..4 ] == b"\x7f ELF"
12+ }
13+
14+ fn is_macho_binary ( file_contents : & [ u8 ] ) -> bool {
15+ if file_contents. len ( ) < 4 {
16+ return false ;
17+ }
18+
19+ let magic = u32:: from_ne_bytes ( [
20+ file_contents[ 0 ] , file_contents[ 1 ] ,
21+ file_contents[ 2 ] , file_contents[ 3 ]
22+ ] ) ;
23+
24+ // Mach-O magic numbers
25+ magic == 0xfeedface || // 32-bit
26+ magic == 0xfeedfacf || // 64-bit
27+ magic == 0xcafebabe || // Fat binary
28+ magic == 0xcefaedfe || // 32-bit swapped
29+ magic == 0xcffaedfe // 64-bit swapped
30+ }
31+
32+ fn find_python_library_macos ( ) -> Result < String , String > {
33+ eprintln ! ( "fix-python-soname: Looking for Python framework on macOS..." ) ;
34+
35+ // Python versions from 3.20 down to 3.8
36+ let mut python_versions = Vec :: new ( ) ;
37+ for major in ( 8 ..=20 ) . rev ( ) {
38+ // Framework paths (highest priority)
39+ python_versions. push ( format ! ( "Python.framework/Versions/3.{}/Python" , major) ) ;
40+ }
41+
42+ eprintln ! (
43+ "fix-python-soname: Looking for versions: {:?}" ,
44+ & python_versions[ 0 ..6 ]
45+ ) ;
46+
47+ // macOS Python search paths (ordered by priority)
48+ let mut lib_paths = vec ! [
49+ // Homebrew paths (most common first)
50+ "/opt/homebrew/opt/[email protected] /Frameworks" , 51+ "/opt/homebrew/opt/[email protected] /Frameworks" , 52+ "/opt/homebrew/opt/[email protected] /Frameworks" , 53+ "/opt/homebrew/opt/[email protected] /Frameworks" , 54+ "/opt/homebrew/opt/[email protected] /Frameworks" , 55+ "/opt/homebrew/opt/[email protected] /Frameworks" , 56+ // Intel Mac Homebrew
57+ "/usr/local/opt/[email protected] /Frameworks" , 58+ "/usr/local/opt/[email protected] /Frameworks" , 59+ "/usr/local/opt/[email protected] /Frameworks" , 60+ "/usr/local/opt/[email protected] /Frameworks" , 61+ "/usr/local/opt/[email protected] /Frameworks" , 62+ "/usr/local/opt/[email protected] /Frameworks" , 63+ // System Python frameworks
64+ "/Library/Frameworks" ,
65+ "/System/Library/Frameworks" ,
66+ ] ;
67+
68+ // Check for active virtual environments first
69+ if let Ok ( venv) = env:: var ( "VIRTUAL_ENV" ) {
70+ let venv_fw = format ! ( "{}/Frameworks" , venv) ;
71+ lib_paths. insert ( 0 , Box :: leak ( venv_fw. into_boxed_str ( ) ) ) ;
72+ }
73+
74+ // Add user-specific paths
75+ if let Ok ( home) = env:: var ( "HOME" ) {
76+ // pyenv installations
77+ let pyenv_versions = format ! ( "{}/.pyenv/versions" , home) ;
78+ if let Ok ( entries) = fs:: read_dir ( & pyenv_versions) {
79+ for entry in entries. flatten ( ) {
80+ if entry. file_type ( ) . map ( |t| t. is_dir ( ) ) . unwrap_or ( false ) {
81+ let version_fw = format ! ( "{}/Frameworks" , entry. path( ) . display( ) ) ;
82+ lib_paths. push ( Box :: leak ( version_fw. into_boxed_str ( ) ) ) ;
83+ }
84+ }
85+ }
86+ }
87+
88+ eprintln ! (
89+ "fix-python-soname: Searching in {} framework directories..." ,
90+ lib_paths. len( )
91+ ) ;
92+
93+ // First try exact version matches
94+ for lib_name in & python_versions {
95+ for lib_path in & lib_paths {
96+ let full_path = format ! ( "{}/{}" , lib_path, lib_name) ;
97+ if std:: path:: Path :: new ( & full_path) . exists ( ) {
98+ eprintln ! (
99+ "fix-python-soname: Found Python framework: {} at {}" ,
100+ lib_name, full_path
101+ ) ;
102+ return Ok ( full_path) ;
103+ }
104+ }
105+ }
106+
107+ eprintln ! ( "fix-python-soname: No exact match found, searching for any Python.framework..." ) ;
108+
109+ // If no exact match found, search directories for any Python frameworks
110+ for lib_path in & lib_paths {
111+ if let Ok ( entries) = fs:: read_dir ( lib_path) {
112+ let mut found_frameworks: Vec < ( String , u32 , u32 ) > = Vec :: new ( ) ;
113+
114+ for entry in entries. flatten ( ) {
115+ if let Some ( name) = entry. file_name ( ) . to_str ( ) {
116+ if name == "Python.framework" {
117+ // Check for version directories
118+ let versions_dir = entry. path ( ) . join ( "Versions" ) ;
119+ if let Ok ( version_entries) = fs:: read_dir ( & versions_dir) {
120+ for version_entry in version_entries. flatten ( ) {
121+ if let Some ( version_name) = version_entry. file_name ( ) . to_str ( ) {
122+ if let Some ( version_start) = version_name. find ( "3." ) {
123+ let version_part = & version_name[ version_start + 2 ..] ;
124+ if let Ok ( minor) = version_part. parse :: < u32 > ( ) {
125+ let python_path = version_entry. path ( ) . join ( "Python" ) ;
126+ if python_path. exists ( ) {
127+ found_frameworks. push ( ( python_path. to_string_lossy ( ) . to_string ( ) , 3 , minor) ) ;
128+ }
129+ }
130+ }
131+ }
132+ }
133+ }
134+ }
135+ }
136+ }
137+
138+ // Sort by version (newest first)
139+ found_frameworks. sort_by ( |a, b| b. 2 . cmp ( & a. 2 ) . then ( b. 1 . cmp ( & a. 1 ) ) ) ;
140+
141+ if let Some ( ( framework_path, _, _) ) = found_frameworks. first ( ) {
142+ eprintln ! (
143+ "fix-python-soname: Found Python framework: {} in {}" ,
144+ framework_path, lib_path
145+ ) ;
146+ return Ok ( framework_path. clone ( ) ) ;
147+ }
148+ }
149+ }
150+
151+ Err (
152+ "No Python framework found on the system. Searched in:\n " . to_string ( )
153+ + & lib_paths[ ..10 ] . join ( "\n " )
154+ + "\n ... and more" ,
155+ )
156+ }
157+
9158fn find_python_library ( ) -> Result < String , String > {
10159 // Generate Python versions from 3.20 down to 3.8
11160 let mut python_versions = Vec :: new ( ) ;
@@ -265,7 +414,7 @@ fn find_python_library() -> Result<String, String> {
265414}
266415
267416fn main ( ) -> Result < ( ) , Box < dyn std:: error:: Error > > {
268- eprintln ! ( "fix-python-soname: Starting soname patcher..." ) ;
417+ eprintln ! ( "fix-python-soname: Starting binary patcher..." ) ;
269418
270419 let args: Vec < String > = env:: args ( ) . collect ( ) ;
271420 eprintln ! ( "fix-python-soname: Arguments: {:?}" , args) ;
@@ -277,22 +426,35 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
277426 let node_file_path = & args[ 1 ] ;
278427 eprintln ! ( "fix-python-soname: Processing file: {}" , node_file_path) ;
279428
280- // Find the local Python library
281- let new_python_lib = find_python_library ( ) ?;
282-
283- // Read the file
284- eprintln ! ( "fix-python-soname: Reading ELF file..." ) ;
429+ // Read the file first to detect format
430+ eprintln ! ( "fix-python-soname: Reading binary file..." ) ;
285431 let file_contents =
286432 fs:: read ( node_file_path) . map_err ( |error| format ! ( "Failed to read file: {error}" ) ) ?;
287433 eprintln ! (
288- "fix-python-soname: ELF file size: {} bytes" ,
434+ "fix-python-soname: Binary file size: {} bytes" ,
289435 file_contents. len( )
290436 ) ;
291437
438+ // Detect binary format and process accordingly
439+ if is_elf_binary ( & file_contents) {
440+ eprintln ! ( "fix-python-soname: Detected ELF binary (Linux)" ) ;
441+ process_elf_binary ( & file_contents, node_file_path)
442+ } else if is_macho_binary ( & file_contents) {
443+ eprintln ! ( "fix-python-soname: Detected Mach-O binary (macOS)" ) ;
444+ process_macho_binary ( & file_contents, node_file_path)
445+ } else {
446+ Err ( "Unsupported binary format. Only ELF (Linux) and Mach-O (macOS) are supported." . into ( ) )
447+ }
448+ }
449+
450+ fn process_elf_binary ( file_contents : & [ u8 ] , node_file_path : & str ) -> Result < ( ) , Box < dyn std:: error:: Error > > {
451+ // Find the local Python library (Linux)
452+ let new_python_lib = find_python_library ( ) ?;
453+
292454 // Parse the ELF file
293455 eprintln ! ( "fix-python-soname: Parsing ELF file..." ) ;
294456 let mut elf =
295- ElfContainer :: parse ( & file_contents) . map_err ( |error| format ! ( "Failed to parse ELF: {error}" ) ) ?;
457+ ElfContainer :: parse ( file_contents) . map_err ( |error| format ! ( "Failed to parse ELF: {error}" ) ) ?;
296458
297459 // Get the list of needed libraries
298460 eprintln ! ( "fix-python-soname: Getting needed libraries..." ) ;
@@ -359,3 +521,76 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
359521
360522 Ok ( ( ) )
361523}
524+
525+ fn process_macho_binary ( file_contents : & [ u8 ] , node_file_path : & str ) -> Result < ( ) , Box < dyn std:: error:: Error > > {
526+ // Find the local Python framework (macOS)
527+ let new_python_framework = find_python_library_macos ( ) ?;
528+
529+ // Parse the Mach-O file
530+ eprintln ! ( "fix-python-soname: Parsing Mach-O file..." ) ;
531+ let mut macho =
532+ MachoContainer :: parse ( file_contents) . map_err ( |error| format ! ( "Failed to parse Mach-O: {error}" ) ) ?;
533+
534+ // Get the list of linked libraries (equivalent to needed libs on ELF)
535+ eprintln ! ( "fix-python-soname: Getting linked libraries..." ) ;
536+
537+ // Access the libs field based on the macho type
538+ let libs = match & macho. inner {
539+ arwen:: macho:: MachoType :: SingleArch ( single) => & single. inner . libs ,
540+ arwen:: macho:: MachoType :: Fat ( fat) => {
541+ if fat. archs . is_empty ( ) {
542+ return Err ( "No architectures found in fat binary" . into ( ) ) ;
543+ }
544+ & fat. archs [ 0 ] . inner . inner . libs // Use first architecture
545+ }
546+ } ;
547+
548+ eprintln ! ( "fix-python-soname: Linked libraries: {:?}" , libs) ;
549+
550+ // Find the existing Python framework dependency
551+ let python_framework = libs
552+ . iter ( )
553+ . find ( |lib| lib. contains ( "Python.framework" ) || lib. contains ( "Python" ) )
554+ . ok_or ( "No Python framework dependency found in the binary" ) ?;
555+
556+ eprintln ! (
557+ "fix-python-soname: Current Python framework: {}" ,
558+ python_framework
559+ ) ;
560+
561+ // Check if already pointing to the correct framework
562+ if python_framework == & new_python_framework {
563+ eprintln ! ( "fix-python-soname: Already using the correct Python framework" ) ;
564+ return Ok ( ( ) ) ;
565+ }
566+
567+ eprintln ! ( "fix-python-soname: Replacing with: {}" , new_python_framework) ;
568+
569+ // Use change_install_name to replace the Python framework path
570+ eprintln ! ( "fix-python-soname: Changing install name..." ) ;
571+ macho
572+ . change_install_name ( python_framework, & new_python_framework)
573+ . map_err ( |error| format ! ( "Failed to change install name: {error}" ) ) ?;
574+
575+ // Create backup
576+ let file_path = Path :: new ( node_file_path) ;
577+ let backup_path = file_path. with_extension ( "node.bak" ) ;
578+ eprintln ! (
579+ "fix-python-soname: Creating backup at: {}" ,
580+ backup_path. display( )
581+ ) ;
582+ fs:: copy ( file_path, & backup_path) . map_err ( |error| format ! ( "Failed to create backup: {error}" ) ) ?;
583+ eprintln ! ( "fix-python-soname: Backup created successfully" ) ;
584+
585+ // Write the modified file
586+ eprintln ! ( "fix-python-soname: Writing modified Mach-O file..." ) ;
587+ fs:: write ( node_file_path, & macho. data )
588+ . map_err ( |error| format ! ( "Failed to write Mach-O: {error}" ) ) ?;
589+
590+ eprintln ! (
591+ "fix-python-soname: Successfully updated: {}" ,
592+ node_file_path
593+ ) ;
594+
595+ Ok ( ( ) )
596+ }
0 commit comments