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 ] ,
21+ file_contents[ 1 ] ,
22+ file_contents[ 2 ] ,
23+ file_contents[ 3 ] ,
24+ ] ) ;
25+
26+ // Mach-O magic numbers
27+ magic == 0xfeedface || // 32-bit
28+ magic == 0xfeedfacf || // 64-bit
29+ magic == 0xcafebabe || // Fat binary
30+ magic == 0xcefaedfe || // 32-bit swapped
31+ magic == 0xcffaedfe // 64-bit swapped
32+ }
33+
34+ fn find_python_library_macos ( ) -> Result < String , String > {
35+ eprintln ! ( "fix-python-soname: Looking for Python framework on macOS..." ) ;
36+
37+ // Python versions from 3.20 down to 3.8
38+ let mut python_versions = Vec :: new ( ) ;
39+ for major in ( 8 ..=20 ) . rev ( ) {
40+ // Framework paths (highest priority)
41+ python_versions. push ( format ! ( "Python.framework/Versions/3.{}/Python" , major) ) ;
42+ }
43+
44+ eprintln ! (
45+ "fix-python-soname: Looking for versions: {:?}" ,
46+ & python_versions[ 0 ..6 ]
47+ ) ;
48+
49+ // macOS Python search paths (ordered by priority)
50+ let mut lib_paths = vec ! [
51+ // Homebrew paths (most common first)
52+ "/opt/homebrew/opt/python@3.13/Frameworks" ,
53+ "/opt/homebrew/opt/python@3.12/Frameworks" ,
54+ "/opt/homebrew/opt/python@3.11/Frameworks" ,
55+ "/opt/homebrew/opt/python@3.10/Frameworks" ,
56+ "/opt/homebrew/opt/python@3.9/Frameworks" ,
57+ "/opt/homebrew/opt/python@3.8/Frameworks" ,
58+ // Intel Mac Homebrew
59+ "/usr/local/opt/python@3.13/Frameworks" ,
60+ "/usr/local/opt/python@3.12/Frameworks" ,
61+ "/usr/local/opt/python@3.11/Frameworks" ,
62+ "/usr/local/opt/python@3.10/Frameworks" ,
63+ "/usr/local/opt/python@3.9/Frameworks" ,
64+ "/usr/local/opt/python@3.8/Frameworks" ,
65+ // System Python frameworks
66+ "/Library/Frameworks" ,
67+ "/System/Library/Frameworks" ,
68+ ] ;
69+
70+ // Check for active virtual environments first
71+ if let Ok ( venv) = env:: var ( "VIRTUAL_ENV" ) {
72+ let venv_fw = format ! ( "{}/Frameworks" , venv) ;
73+ lib_paths. insert ( 0 , Box :: leak ( venv_fw. into_boxed_str ( ) ) ) ;
74+ }
75+
76+ // Add user-specific paths
77+ if let Ok ( home) = env:: var ( "HOME" ) {
78+ // pyenv installations
79+ let pyenv_versions = format ! ( "{}/.pyenv/versions" , home) ;
80+ if let Ok ( entries) = fs:: read_dir ( & pyenv_versions) {
81+ for entry in entries. flatten ( ) {
82+ if entry. file_type ( ) . map ( |t| t. is_dir ( ) ) . unwrap_or ( false ) {
83+ let version_fw = format ! ( "{}/Frameworks" , entry. path( ) . display( ) ) ;
84+ lib_paths. push ( Box :: leak ( version_fw. into_boxed_str ( ) ) ) ;
85+ }
86+ }
87+ }
88+ }
89+
90+ eprintln ! (
91+ "fix-python-soname: Searching in {} framework directories..." ,
92+ lib_paths. len( )
93+ ) ;
94+
95+ // First try exact version matches
96+ for lib_name in & python_versions {
97+ for lib_path in & lib_paths {
98+ let full_path = format ! ( "{}/{}" , lib_path, lib_name) ;
99+ if std:: path:: Path :: new ( & full_path) . exists ( ) {
100+ eprintln ! (
101+ "fix-python-soname: Found Python framework: {} at {}" ,
102+ lib_name, full_path
103+ ) ;
104+ return Ok ( full_path) ;
105+ }
106+ }
107+ }
108+
109+ eprintln ! ( "fix-python-soname: No exact match found, searching for any Python.framework..." ) ;
110+
111+ // If no exact match found, search directories for any Python frameworks
112+ for lib_path in & lib_paths {
113+ if let Ok ( entries) = fs:: read_dir ( lib_path) {
114+ let mut found_frameworks: Vec < ( String , u32 , u32 ) > = Vec :: new ( ) ;
115+
116+ for entry in entries. flatten ( ) {
117+ if let Some ( name) = entry. file_name ( ) . to_str ( ) {
118+ if name == "Python.framework" {
119+ // Check for version directories
120+ let versions_dir = entry. path ( ) . join ( "Versions" ) ;
121+ if let Ok ( version_entries) = fs:: read_dir ( & versions_dir) {
122+ for version_entry in version_entries. flatten ( ) {
123+ if let Some ( version_name) = version_entry. file_name ( ) . to_str ( ) {
124+ if let Some ( version_start) = version_name. find ( "3." ) {
125+ let version_part = & version_name[ version_start + 2 ..] ;
126+ if let Ok ( minor) = version_part. parse :: < u32 > ( ) {
127+ let python_path = version_entry. path ( ) . join ( "Python" ) ;
128+ if python_path. exists ( ) {
129+ found_frameworks. push ( (
130+ python_path. to_string_lossy ( ) . to_string ( ) ,
131+ 3 ,
132+ minor,
133+ ) ) ;
134+ }
135+ }
136+ }
137+ }
138+ }
139+ }
140+ }
141+ }
142+ }
143+
144+ // Sort by version (newest first)
145+ found_frameworks. sort_by ( |a, b| b. 2 . cmp ( & a. 2 ) . then ( b. 1 . cmp ( & a. 1 ) ) ) ;
146+
147+ if let Some ( ( framework_path, _, _) ) = found_frameworks. first ( ) {
148+ eprintln ! (
149+ "fix-python-soname: Found Python framework: {} in {}" ,
150+ framework_path, lib_path
151+ ) ;
152+ return Ok ( framework_path. clone ( ) ) ;
153+ }
154+ }
155+ }
156+
157+ Err (
158+ "No Python framework found on the system. Searched in:\n " . to_string ( )
159+ + & lib_paths[ ..10 ] . join ( "\n " )
160+ + "\n ... and more" ,
161+ )
162+ }
163+
9164fn find_python_library ( ) -> Result < String , String > {
10165 // Generate Python versions from 3.20 down to 3.8
11166 let mut python_versions = Vec :: new ( ) ;
@@ -265,7 +420,7 @@ fn find_python_library() -> Result<String, String> {
265420}
266421
267422fn main ( ) -> Result < ( ) , Box < dyn std:: error:: Error > > {
268- eprintln ! ( "fix-python-soname: Starting soname patcher..." ) ;
423+ eprintln ! ( "fix-python-soname: Starting binary patcher..." ) ;
269424
270425 let args: Vec < String > = env:: args ( ) . collect ( ) ;
271426 eprintln ! ( "fix-python-soname: Arguments: {:?}" , args) ;
@@ -277,22 +432,38 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
277432 let node_file_path = & args[ 1 ] ;
278433 eprintln ! ( "fix-python-soname: Processing file: {}" , node_file_path) ;
279434
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..." ) ;
435+ // Read the file first to detect format
436+ eprintln ! ( "fix-python-soname: Reading binary file..." ) ;
285437 let file_contents =
286438 fs:: read ( node_file_path) . map_err ( |error| format ! ( "Failed to read file: {error}" ) ) ?;
287439 eprintln ! (
288- "fix-python-soname: ELF file size: {} bytes" ,
440+ "fix-python-soname: Binary file size: {} bytes" ,
289441 file_contents. len( )
290442 ) ;
291443
444+ // Detect binary format and process accordingly
445+ if is_elf_binary ( & file_contents) {
446+ eprintln ! ( "fix-python-soname: Detected ELF binary (Linux)" ) ;
447+ process_elf_binary ( & file_contents, node_file_path)
448+ } else if is_macho_binary ( & file_contents) {
449+ eprintln ! ( "fix-python-soname: Detected Mach-O binary (macOS)" ) ;
450+ process_macho_binary ( & file_contents, node_file_path)
451+ } else {
452+ Err ( "Unsupported binary format. Only ELF (Linux) and Mach-O (macOS) are supported." . into ( ) )
453+ }
454+ }
455+
456+ fn process_elf_binary (
457+ file_contents : & [ u8 ] ,
458+ node_file_path : & str ,
459+ ) -> Result < ( ) , Box < dyn std:: error:: Error > > {
460+ // Find the local Python library (Linux)
461+ let new_python_lib = find_python_library ( ) ?;
462+
292463 // Parse the ELF file
293464 eprintln ! ( "fix-python-soname: Parsing ELF file..." ) ;
294465 let mut elf =
295- ElfContainer :: parse ( & file_contents) . map_err ( |error| format ! ( "Failed to parse ELF: {error}" ) ) ?;
466+ ElfContainer :: parse ( file_contents) . map_err ( |error| format ! ( "Failed to parse ELF: {error}" ) ) ?;
296467
297468 // Get the list of needed libraries
298469 eprintln ! ( "fix-python-soname: Getting needed libraries..." ) ;
@@ -359,3 +530,82 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
359530
360531 Ok ( ( ) )
361532}
533+
534+ fn process_macho_binary (
535+ file_contents : & [ u8 ] ,
536+ node_file_path : & str ,
537+ ) -> Result < ( ) , Box < dyn std:: error:: Error > > {
538+ // Find the local Python framework (macOS)
539+ let new_python_framework = find_python_library_macos ( ) ?;
540+
541+ // Parse the Mach-O file
542+ eprintln ! ( "fix-python-soname: Parsing Mach-O file..." ) ;
543+ let mut macho = MachoContainer :: parse ( file_contents)
544+ . map_err ( |error| format ! ( "Failed to parse Mach-O: {error}" ) ) ?;
545+
546+ // Get the list of linked libraries (equivalent to needed libs on ELF)
547+ eprintln ! ( "fix-python-soname: Getting linked libraries..." ) ;
548+
549+ // Access the libs field based on the macho type
550+ let libs = match & macho. inner {
551+ arwen:: macho:: MachoType :: SingleArch ( single) => & single. inner . libs ,
552+ arwen:: macho:: MachoType :: Fat ( fat) => {
553+ if fat. archs . is_empty ( ) {
554+ return Err ( "No architectures found in fat binary" . into ( ) ) ;
555+ }
556+ & fat. archs [ 0 ] . inner . inner . libs // Use first architecture
557+ }
558+ } ;
559+
560+ eprintln ! ( "fix-python-soname: Linked libraries: {:?}" , libs) ;
561+
562+ // Find the existing Python framework dependency
563+ let python_framework = libs
564+ . iter ( )
565+ . find ( |lib| lib. contains ( "Python.framework" ) || lib. contains ( "Python" ) )
566+ . ok_or ( "No Python framework dependency found in the binary" ) ?;
567+
568+ eprintln ! (
569+ "fix-python-soname: Current Python framework: {}" ,
570+ python_framework
571+ ) ;
572+
573+ // Check if already pointing to the correct framework
574+ if python_framework == & new_python_framework {
575+ eprintln ! ( "fix-python-soname: Already using the correct Python framework" ) ;
576+ return Ok ( ( ) ) ;
577+ }
578+
579+ eprintln ! (
580+ "fix-python-soname: Replacing with: {}" ,
581+ new_python_framework
582+ ) ;
583+
584+ // Use change_install_name to replace the Python framework path
585+ eprintln ! ( "fix-python-soname: Changing install name..." ) ;
586+ macho
587+ . change_install_name ( python_framework, & new_python_framework)
588+ . map_err ( |error| format ! ( "Failed to change install name: {error}" ) ) ?;
589+
590+ // Create backup
591+ let file_path = Path :: new ( node_file_path) ;
592+ let backup_path = file_path. with_extension ( "node.bak" ) ;
593+ eprintln ! (
594+ "fix-python-soname: Creating backup at: {}" ,
595+ backup_path. display( )
596+ ) ;
597+ fs:: copy ( file_path, & backup_path) . map_err ( |error| format ! ( "Failed to create backup: {error}" ) ) ?;
598+ eprintln ! ( "fix-python-soname: Backup created successfully" ) ;
599+
600+ // Write the modified file
601+ eprintln ! ( "fix-python-soname: Writing modified Mach-O file..." ) ;
602+ fs:: write ( node_file_path, & macho. data )
603+ . map_err ( |error| format ! ( "Failed to write Mach-O: {error}" ) ) ?;
604+
605+ eprintln ! (
606+ "fix-python-soname: Successfully updated: {}" ,
607+ node_file_path
608+ ) ;
609+
610+ Ok ( ( ) )
611+ }
0 commit comments