"""
Platform, compiler, and linker detection utilities for openHiTLS build system.
This module provides independent detection for:
- Compiler type (gcc, clang, apple-clang)
- Linker type (gnu-ld, ld64, lld, gold)
- Operating system (linux, darwin, etc.)
The detection is dimension-independent: compiler choice doesn't imply linker choice,
and OS doesn't dictate compiler.
"""
import platform
import subprocess
import re
import os
from typing import Optional, Dict, Tuple
class CompilerDetector:
"""Detect compiler type by analyzing version output."""
COMPILER_TYPES = {
'gcc': 'gcc',
'clang': 'clang',
'apple-clang': 'apple-clang'
}
@staticmethod
def detect_compiler_type(compiler_cmd: Optional[str] = None) -> str:
"""
Detect compiler type from command.
Args:
compiler_cmd: Compiler command (default: $CC or 'cc')
Returns:
Compiler type: 'gcc', 'clang', or 'apple-clang'
"""
if compiler_cmd is None:
compiler_cmd = os.environ.get('CC', 'cc')
try:
result = subprocess.run(
[compiler_cmd, '--version'],
capture_output=True,
text=True,
timeout=5
)
version_output = result.stdout.lower()
if 'apple' in version_output and 'clang' in version_output:
return 'apple-clang'
elif 'clang' in version_output:
return 'clang'
elif 'gcc' in version_output or 'gnu' in version_output:
return 'gcc'
else:
return 'gcc'
except (subprocess.TimeoutExpired, subprocess.SubprocessError, FileNotFoundError) as e:
print(f"Warning: Failed to detect compiler type for '{compiler_cmd}': {e}")
return 'gcc'
@staticmethod
def get_compiler_info(compiler_cmd: Optional[str] = None) -> Dict[str, str]:
"""
Get detailed compiler information.
Returns:
Dict with 'type', 'version', 'path', 'output'
"""
if compiler_cmd is None:
compiler_cmd = os.environ.get('CC', 'cc')
info = {
'command': compiler_cmd,
'type': 'unknown',
'version': 'unknown',
'path': 'unknown',
'output': ''
}
try:
which_result = subprocess.run(
['which', compiler_cmd],
capture_output=True,
text=True,
timeout=5
)
if which_result.returncode == 0:
info['path'] = which_result.stdout.strip()
result = subprocess.run(
[compiler_cmd, '--version'],
capture_output=True,
text=True,
timeout=5
)
info['output'] = result.stdout
info['type'] = CompilerDetector.detect_compiler_type(compiler_cmd)
version_match = re.search(r'(\d+\.\d+\.\d+)', result.stdout)
if version_match:
info['version'] = version_match.group(1)
except Exception as e:
print(f"Warning: Failed to get compiler info: {e}")
return info
class LinkerDetector:
"""Detect linker type by analyzing version output and toolchain."""
LINKER_TYPES = {
'gnu-ld': 'gnu-ld',
'ld64': 'ld64',
'lld': 'lld',
'gold': 'gold'
}
@staticmethod
def detect_linker_type(linker_cmd: Optional[str] = None, compiler_cmd: Optional[str] = None) -> str:
"""
Detect linker type.
Strategy:
1. If linker_cmd provided, detect from that command
2. Otherwise, detect from compiler's default linker
3. Fall back to OS-based heuristic
Args:
linker_cmd: Explicit linker command (e.g., 'ld', 'ld64')
compiler_cmd: Compiler command to query for default linker
Returns:
Linker type: 'gnu-ld', 'ld64', 'lld', or 'gold'
"""
if linker_cmd:
return LinkerDetector._detect_from_command(linker_cmd)
if compiler_cmd:
linker_from_compiler = LinkerDetector._detect_from_compiler(compiler_cmd)
if linker_from_compiler:
return linker_from_compiler
return LinkerDetector._detect_from_os()
@staticmethod
def _detect_from_command(linker_cmd: str) -> str:
"""Detect linker type from direct command."""
try:
result = subprocess.run(
[linker_cmd, '--version'],
capture_output=True,
text=True,
timeout=5
)
version_output = result.stdout.lower()
if 'lld' in version_output:
return 'lld'
elif 'gnu' in version_output and 'gold' in version_output:
return 'gold'
elif 'gnu' in version_output:
return 'gnu-ld'
elif 'ld64' in version_output or 'darwin' in version_output:
return 'ld64'
result = subprocess.run(
[linker_cmd, '-v'],
capture_output=True,
text=True,
timeout=5
)
version_output = result.stdout.lower() + result.stderr.lower()
if 'ld64' in version_output or 'darwin' in version_output:
return 'ld64'
except Exception as e:
print(f"Warning: Failed to detect linker from command '{linker_cmd}': {e}")
return LinkerDetector._detect_from_os()
@staticmethod
def _detect_from_compiler(compiler_cmd: str) -> Optional[str]:
"""Detect linker by querying compiler."""
try:
result = subprocess.run(
[compiler_cmd, '-Wl,--version'],
capture_output=True,
text=True,
timeout=5
)
version_output = result.stdout.lower() + result.stderr.lower()
if 'lld' in version_output:
return 'lld'
elif 'gnu' in version_output and 'gold' in version_output:
return 'gold'
elif 'gnu' in version_output:
return 'gnu-ld'
elif 'ld64' in version_output or 'darwin' in version_output:
return 'ld64'
except Exception:
pass
return None
@staticmethod
def _detect_from_os() -> str:
"""Fallback: detect linker based on OS."""
system = platform.system().lower()
if system == 'darwin':
return 'ld64'
elif system == 'linux':
try:
subprocess.run(['ld.lld', '--version'], capture_output=True, timeout=5)
return 'lld'
except Exception:
pass
return 'gnu-ld'
else:
return 'gnu-ld'
@staticmethod
def get_linker_info(linker_cmd: Optional[str] = None, compiler_cmd: Optional[str] = None) -> Dict[str, str]:
"""
Get detailed linker information.
Returns:
Dict with 'type', 'version', 'path', 'output'
"""
info = {
'command': linker_cmd or 'auto',
'type': 'unknown',
'version': 'unknown',
'path': 'unknown',
'output': ''
}
info['type'] = LinkerDetector.detect_linker_type(linker_cmd, compiler_cmd)
if linker_cmd:
try:
which_result = subprocess.run(
['which', linker_cmd],
capture_output=True,
text=True,
timeout=5
)
if which_result.returncode == 0:
info['path'] = which_result.stdout.strip()
result = subprocess.run(
[linker_cmd, '--version'],
capture_output=True,
text=True,
timeout=5
)
info['output'] = result.stdout
version_match = re.search(r'(\d+\.\d+)', result.stdout)
if version_match:
info['version'] = version_match.group(1)
except Exception as e:
print(f"Warning: Failed to get linker info: {e}")
return info
class PlatformDetector:
"""Detect operating system and architecture."""
@staticmethod
def get_current_platform() -> str:
"""
Get normalized platform name.
Returns:
Platform name: 'linux', 'darwin', 'windows', etc.
"""
system = platform.system().lower()
if system == 'darwin':
return 'darwin'
elif system == 'linux':
return 'linux'
elif system == 'windows':
return 'windows'
else:
return system
@staticmethod
def get_architecture() -> str:
"""Get CPU architecture."""
machine = platform.machine().lower()
if machine in ('x86_64', 'amd64'):
return 'x86_64'
elif machine in ('aarch64', 'arm64'):
return 'aarch64'
elif machine.startswith('arm'):
return 'arm'
else:
return machine
@staticmethod
def get_platform_info() -> Dict[str, str]:
"""Get comprehensive platform information."""
return {
'os': PlatformDetector.get_current_platform(),
'arch': PlatformDetector.get_architecture(),
'system': platform.system(),
'release': platform.release(),
'machine': platform.machine(),
'python_version': platform.python_version()
}
class BuildEnvironment:
"""Complete build environment detection and reporting."""
@staticmethod
def detect(compiler_cmd: Optional[str] = None, linker_cmd: Optional[str] = None) -> Dict:
"""
Detect complete build environment.
Returns:
Dict with 'platform', 'compiler', 'linker' information
"""
env = {
'platform': PlatformDetector.get_platform_info(),
'compiler': CompilerDetector.get_compiler_info(compiler_cmd),
'linker': LinkerDetector.get_linker_info(linker_cmd, compiler_cmd)
}
return env
if __name__ == '__main__':
env = BuildEnvironment.detect()
print("Platform:", env['platform']['os'], env['platform']['arch'])
print("Compiler:", env['compiler']['type'], env['compiler']['version'])
print("Linker:", env['linker']['type'])