Skip to content

Commit 8d7637d

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

File tree

2 files changed

+253
-16
lines changed

2 files changed

+253
-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: 243 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,160 @@
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], 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+
9158
fn 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

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

Comments
 (0)