Files
fourdst/fourdst/core/platform.py

364 lines
12 KiB
Python

# fourdst/core/platform.py
import json
import platform
import shutil
import subprocess
from pathlib import Path
from fourdst.core.config import ABI_CACHE_FILE, CACHE_PATH
from fourdst.core.utils import run_command
ABI_DETECTOR_CPP_SRC = """
#include <iostream>
#include <string>
#include <vector>
#ifdef __GNUC__
#if __has_include(<gnu/libc-version.h>)
#include <gnu/libc-version.h>
#endif
#endif
int main() {
std::string os;
std::string compiler;
std::string compiler_version;
std::string stdlib;
std::string stdlib_version;
std::string abi;
#if defined(__APPLE__) && defined(__MACH__)
os = "macos";
#elif defined(__linux__)
os = "linux";
#elif defined(_WIN32)
os = "windows";
#else
os = "unknown_os";
#endif
#if defined(__clang__)
compiler = "clang";
compiler_version = __clang_version__;
#elif defined(__GNUC__)
compiler = "gcc";
compiler_version = std::to_string(__GNUC__) + "." + std::to_string(__GNUC_MINOR__) + "." + std::to_string(__GNUC_PATCHLEVEL__);
#elif defined(_MSC_VER)
compiler = "msvc";
compiler_version = std::to_string(_MSC_VER);
#else
compiler = "unknown_compiler";
compiler_version = "0";
#endif
#if defined(_LIBCPP_VERSION)
stdlib = "libc++";
stdlib_version = std::to_string(_LIBCPP_VERSION);
abi = "libc++_abi"; // On libc++, the ABI is tightly coupled with the library itself.
#elif defined(__GLIBCXX__)
stdlib = "libstdc++";
#if defined(_GLIBCXX_USE_CXX11_ABI)
abi = _GLIBCXX_USE_CXX11_ABI == 1 ? "cxx11_abi" : "pre_cxx11_abi";
#else
abi = "pre_cxx11_abi";
#endif
#if __has_include(<gnu/libc-version.h>)
stdlib_version = gnu_get_libc_version();
#else
stdlib_version = "unknown";
#endif
#else
stdlib = "unknown_stdlib";
abi = "unknown_abi";
#endif
std::cout << "os=" << os << std::endl;
std::cout << "compiler=" << compiler << std::endl;
std::cout << "compiler_version=" << compiler_version << std::endl;
std::cout << "stdlib=" << stdlib << std::endl;
if (!stdlib_version.empty()) {
std::cout << "stdlib_version=" << stdlib_version << std::endl;
}
// Always print the ABI key for consistent parsing
std::cout << "abi=" << abi << std::endl;
return 0;
}
"""
ABI_DETECTOR_MESON_SRC = """
project('abi-detector', 'cpp', default_options : ['cpp_std=c++23'])
executable('detector', 'main.cpp')
"""
def _detect_and_cache_abi() -> dict:
"""
Compiles and runs a C++ program to detect the compiler ABI, then caches it.
Falls back to platform-based detection if meson is not available (e.g., in packaged apps).
"""
import sys
import logging
# Use logging instead of print to avoid stdout contamination
logger = logging.getLogger(__name__)
logger.info("Performing one-time native C++ ABI detection...")
# Check if meson is available
meson_available = shutil.which("meson") is not None
if not meson_available:
logger.warning("Meson not available, using fallback platform detection")
return _fallback_platform_detection()
temp_dir = CACHE_PATH / "abi_detector"
if temp_dir.exists():
shutil.rmtree(temp_dir)
temp_dir.mkdir(parents=True)
try:
(temp_dir / "main.cpp").write_text(ABI_DETECTOR_CPP_SRC)
(temp_dir / "meson.build").write_text(ABI_DETECTOR_MESON_SRC)
logger.info(" - Configuring detector...")
run_command(["meson", "setup", "build"], cwd=temp_dir)
logger.info(" - Compiling detector...")
run_command(["meson", "compile", "-C", "build"], cwd=temp_dir)
detector_exe = temp_dir / "build" / "detector"
logger.info(" - Running detector...")
proc = subprocess.run([str(detector_exe)], check=True, capture_output=True, text=True)
abi_details = {}
for line in proc.stdout.strip().split('\n'):
if '=' in line:
key, value = line.split('=', 1)
abi_details[key] = value.strip()
arch = platform.machine()
stdlib_version = abi_details.get('stdlib_version', 'unknown')
abi_string = f"{abi_details['compiler']}-{abi_details['stdlib']}-{stdlib_version}-{abi_details['abi']}"
platform_data = {
"os": abi_details['os'],
"arch": arch,
"triplet": f"{arch}-{abi_details['os']}",
"abi_signature": abi_string,
"details": abi_details,
"is_native": True,
"cross_file": None,
"docker_image": None
}
with open(ABI_CACHE_FILE, 'w') as f:
json.dump(platform_data, f, indent=4)
logger.info(f" - ABI details cached to {ABI_CACHE_FILE}")
return platform_data
except Exception as e:
logger.warning(f"ABI detection failed: {e}, falling back to platform detection")
return _fallback_platform_detection()
finally:
if temp_dir.exists():
shutil.rmtree(temp_dir)
def _fallback_platform_detection() -> dict:
"""
Fallback platform detection that doesn't require external tools.
Used when meson is not available (e.g., in packaged applications).
"""
import sys
import logging
logger = logging.getLogger(__name__)
logger.info("Using fallback platform detection (no external tools required)")
# Get basic platform information
arch = platform.machine()
system = platform.system().lower()
# Map common architectures
arch_mapping = {
'x86_64': 'x86_64',
'AMD64': 'x86_64',
'arm64': 'aarch64',
'aarch64': 'aarch64',
'i386': 'i686',
'i686': 'i686'
}
normalized_arch = arch_mapping.get(arch, arch)
# Detect compiler and stdlib based on platform
if system == 'darwin':
# macOS
os_name = 'darwin'
compiler = 'clang'
stdlib = 'libc++'
# Get macOS version for stdlib version
mac_version = platform.mac_ver()[0]
stdlib_version = mac_version.split('.')[0] if mac_version else 'unknown'
abi = 'cxx11'
elif system == 'linux':
# Linux
os_name = 'linux'
# Try to detect if we're using GCC or Clang
compiler = 'gcc' # Default assumption
stdlib = 'libstdc++'
stdlib_version = '11' # Common default
abi = 'cxx11'
elif system == 'windows':
# Windows
os_name = 'windows'
compiler = 'msvc'
stdlib = 'msvcrt'
stdlib_version = 'unknown'
abi = 'cxx11'
else:
# Unknown system
os_name = system
compiler = 'unknown'
stdlib = 'unknown'
stdlib_version = 'unknown'
abi = 'unknown'
abi_string = f"{compiler}-{stdlib}-{stdlib_version}-{abi}"
platform_data = {
"os": os_name,
"arch": normalized_arch,
"triplet": f"{normalized_arch}-{os_name}",
"abi_signature": abi_string,
"details": {
"compiler": compiler,
"stdlib": stdlib,
"stdlib_version": stdlib_version,
"abi": abi,
"os": os_name,
"detection_method": "fallback"
},
"is_native": True,
"cross_file": None,
"docker_image": None
}
# Cache the result
try:
CACHE_PATH.mkdir(parents=True, exist_ok=True)
with open(ABI_CACHE_FILE, 'w') as f:
json.dump(platform_data, f, indent=4)
logger.info(f"Fallback platform data cached to {ABI_CACHE_FILE}")
except Exception as e:
logger.warning(f"Failed to cache platform data: {e}")
return platform_data
def get_platform_identifier() -> dict:
"""
Gets the native platform identifier, using a cached value if available.
"""
if ABI_CACHE_FILE.exists():
with open(ABI_CACHE_FILE, 'r') as f:
plat = json.load(f)
else:
plat = _detect_and_cache_abi()
plat['type'] = 'native'
return plat
def _parse_version(version_str: str) -> tuple:
"""Parses a version string like '12.3.1' into a tuple of integers."""
return tuple(map(int, (version_str.split('.') + ['0', '0'])[:3]))
def is_abi_compatible(host_platform: dict, binary_platform: dict) -> tuple[bool, str]:
"""
Checks if a binary's platform is compatible with the host's platform.
This is more nuanced than a simple string comparison, allowing for forward compatibility.
- macOS: A binary for an older OS version can run on a newer one, if the toolchain matches.
- Linux: A binary for an older GLIBC version can run on a newer one.
"""
required_keys = ['os', 'arch', 'abi_signature']
if not all(key in host_platform for key in required_keys):
return False, f"Host platform data is malformed. Missing keys: {[k for k in required_keys if k not in host_platform]}"
if not all(key in binary_platform for key in required_keys):
return False, f"Binary platform data is malformed. Missing keys: {[k for k in required_keys if k not in binary_platform]}"
host_os = host_platform.get('os') or host_platform.get('details', {}).get('os')
binary_os = binary_platform.get('os') or binary_platform.get('details', {}).get('os')
host_arch = host_platform.get('arch') or host_platform.get('details', {}).get('arch')
binary_arch = binary_platform.get('arch') or binary_platform.get('details', {}).get('arch')
if host_os != binary_os:
return False, f"OS mismatch: host is {host_os}, binary is {binary_os}"
if host_arch != binary_arch:
return False, f"Architecture mismatch: host is {host_arch}, binary is {binary_arch}"
host_sig = host_platform['abi_signature']
binary_sig = binary_platform['abi_signature']
try:
host_parts = host_sig.split('-')
binary_parts = binary_sig.split('-')
# Find version numbers in any position
host_ver_str = next((p for p in host_parts if p[0].isdigit()), None)
binary_ver_str = next((p for p in binary_parts if p[0].isdigit()), None)
if not host_ver_str or not binary_ver_str:
return False, "Could not extract version from ABI signature"
host_ver = _parse_version(host_ver_str)
binary_ver = _parse_version(binary_ver_str)
if host_platform['os'] == 'macos':
# For macOS, also check for clang and libc++
if 'clang' not in binary_sig:
return False, "Toolchain mismatch: 'clang' not in binary signature"
if 'libc++' not in binary_sig:
return False, "Toolchain mismatch: 'libc++' not in binary signature"
if host_ver < binary_ver:
return False, f"macOS version too old: host is {host_ver_str}, binary needs {binary_ver_str}"
return True, "Compatible"
elif host_platform['os'] == 'linux':
if host_ver < binary_ver:
return False, f"GLIBC version too old: host is {host_ver_str}, binary needs {binary_ver_str}"
return True, "Compatible"
except (IndexError, ValueError, StopIteration):
return False, "Malformed ABI signature string"
return False, "Unknown compatibility check failure"
def get_macos_targeted_platform_identifier(target_version: str) -> dict:
"""
Generates a platform identifier for a specific target macOS version.
"""
host_platform = get_platform_identifier()
host_details = host_platform['details']
compiler = host_details.get('compiler', 'clang')
stdlib = host_details.get('stdlib', 'libc++')
abi = host_details.get('abi', 'libc++_abi')
arch = platform.machine()
abi_string = f"{compiler}-{stdlib}-{target_version}-{abi}"
return {
"triplet": f"{arch}-macos",
"abi_signature": abi_string,
"details": {
"os": "macos",
"compiler": compiler,
"compiler_version": host_details.get('compiler_version'),
"stdlib": stdlib,
"stdlib_version": target_version,
"abi": abi,
},
"is_native": True,
"cross_file": None,
"docker_image": None,
"arch": arch
}