Skip to content

Commit de35cfb

Browse files
committed
Make macOS use dynamic lookup for its linking
1 parent ababe10 commit de35cfb

File tree

2 files changed

+268
-16
lines changed

2 files changed

+268
-16
lines changed

fix-python-soname.js

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,22 +42,24 @@ function isDevInstall() {
4242
return false
4343
}
4444

45-
// Only patch soname on Linux
46-
if (platform !== 'linux') {
45+
// Only patch on Linux and macOS
46+
if (platform !== 'linux' && platform !== 'darwin') {
4747
console.log(`No need to fix soname on platform: ${platform}`)
4848
process.exit(0)
4949
}
5050

51-
// Get the node file path
52-
const nodeFilePath = path.join(__dirname, `python-node.linux-${arch}-gnu.node`)
51+
// Get the node file path based on platform
52+
const nodeFilePath = platform === 'linux'
53+
? path.join(__dirname, `python-node.linux-${arch}-gnu.node`)
54+
: path.join(__dirname, `python-node.darwin-${arch}.node`)
5355
if (!fs.existsSync(nodeFilePath)) {
5456
if (isDevInstall()) {
5557
// No .node file found during dev install - this is expected, skip silently
56-
console.log(`${nodeFilePath} not found during development install, skipping soname fix`)
58+
console.log(`${nodeFilePath} not found during development install, skipping binary patching`)
5759
process.exit(0)
5860
} else {
5961
// No .node file found when installed as dependency - this is an error
60-
console.error(`Error: Could not find "${nodeFilePath}" to fix soname`)
62+
console.error(`Error: Could not find "${nodeFilePath}" to patch binary`)
6163
process.exit(1)
6264
}
6365
}
@@ -67,7 +69,7 @@ const wasmPath = path.join(__dirname, 'fix-python-soname.wasm')
6769
if (!fs.existsSync(wasmPath)) {
6870
if (isDevInstall()) {
6971
// WASM file not found during dev install - this is expected, skip with warning
70-
console.log('WASM file not found during development install, skipping soname fix')
72+
console.log('WASM file not found during development install, skipping binary patching')
7173
process.exit(0)
7274
} else {
7375
// WASM file not found when installed as dependency - this is an error
@@ -76,7 +78,7 @@ if (!fs.existsSync(wasmPath)) {
7678
}
7779
}
7880

79-
console.log(`Running soname fix on ${nodeFilePath}`)
81+
console.log(`Running binary patch on ${nodeFilePath}`)
8082

8183
// Create a WASI instance
8284
const wasi = new WASI({

fix-python-soname/src/main.rs

Lines changed: 258 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,166 @@
11
use arwen::elf::ElfContainer;
2+
use arwen::macho::MachoContainer;
23
use 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"\x7fELF"
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+
9164
fn 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

267422
fn 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

Comments
 (0)