mirror of
https://github.com/ZDoom/zdoom-macos-deps.git
synced 2024-11-25 13:21:05 +00:00
e96041ef05
set them explicitly for cmake based targets to use them in all checks during configuration step otherwise, some of cmake checks ignores prefix path for includes and libraries for example, `check_library_exists()` from sdl2 picks system iconv library without explicit flags set
593 lines
18 KiB
Python
593 lines
18 KiB
Python
#
|
|
# Helper module to build macOS version of various source ports
|
|
# Copyright (C) 2020-2023 Alexey Lysiuk
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
#
|
|
|
|
import copy
|
|
import os
|
|
import re
|
|
import shlex
|
|
import shutil
|
|
import subprocess
|
|
import typing
|
|
from pathlib import Path
|
|
from platform import machine
|
|
|
|
from ..state import BuildState
|
|
from ..utility import CommandLineOptions, symlink_directory
|
|
|
|
|
|
class Target:
|
|
DESTINATION_DEPS = 0
|
|
DESTINATION_OUTPUT = 1
|
|
|
|
def __init__(self, name=None):
|
|
self.name = name
|
|
self.destination = self.DESTINATION_DEPS
|
|
|
|
self.multi_platform = False
|
|
self.unsupported_architectures = ()
|
|
|
|
def prepare_source(self, state: BuildState):
|
|
""" Called when target is selected by name """
|
|
pass
|
|
|
|
def initialize(self, state: BuildState):
|
|
""" Called on all targets except the selected one before prefix directory creation """
|
|
pass
|
|
|
|
def detect(self, state: BuildState) -> bool:
|
|
"""
|
|
Called when target is selected by source code directory
|
|
Called on all targets until match is found
|
|
"""
|
|
return False
|
|
|
|
def configure(self, state: BuildState):
|
|
""" Called before selected target is about to build """
|
|
pass
|
|
|
|
def build(self, state: BuildState):
|
|
""" Does actual build """
|
|
pass
|
|
|
|
def post_build(self, state: BuildState):
|
|
""" Called after selected target is built """
|
|
pass
|
|
|
|
|
|
class BuildTarget(Target):
|
|
def __init__(self, name=None):
|
|
super().__init__(name)
|
|
|
|
self.src_root = ''
|
|
self.multi_platform = True
|
|
|
|
def configure(self, state: BuildState):
|
|
os.makedirs(state.build_path, exist_ok=True)
|
|
|
|
env = state.environment
|
|
env['PATH'] = os.pathsep.join([
|
|
str(state.bin_path),
|
|
env['PATH'],
|
|
])
|
|
|
|
if state.xcode:
|
|
return
|
|
|
|
if c_compiler := state.c_compiler():
|
|
env['CC'] = str(c_compiler)
|
|
if cxx_compiler := state.cxx_compiler():
|
|
env['CXX'] = str(cxx_compiler)
|
|
|
|
for prefix in ('C', 'CPP', 'CXX', 'OBJC', 'OBJCXX'):
|
|
state.update_flags_environment_variable(f'{prefix}FLAGS', state.compiler_flags())
|
|
|
|
state.update_flags_environment_variable('LDFLAGS', state.linker_flags())
|
|
|
|
# Avoid timestamp only differences in static libraries
|
|
env['ZERO_AR_DATE'] = '1'
|
|
|
|
def install(self, state: BuildState, options: typing.Optional[CommandLineOptions] = None, tool: str = 'gmake'):
|
|
if state.xcode:
|
|
return
|
|
|
|
if state.install_path.exists():
|
|
shutil.rmtree(state.install_path)
|
|
|
|
args = [tool]
|
|
args += options and options.to_list() or ['install']
|
|
|
|
subprocess.run(args, check=True, cwd=state.build_path, env=state.environment)
|
|
|
|
self.update_pc_files(state)
|
|
|
|
@staticmethod
|
|
def update_text_file(path: Path, processor: typing.Optional[typing.Callable] = None):
|
|
with open(path, 'r') as f:
|
|
content = f.readlines()
|
|
|
|
patched_content = []
|
|
|
|
for line in content:
|
|
patched_line = processor(line) if processor else line
|
|
|
|
if patched_line:
|
|
patched_content.append(patched_line)
|
|
|
|
if content == patched_content:
|
|
return
|
|
|
|
file_time = os.stat(path).st_mtime
|
|
|
|
with open(path, 'w') as f:
|
|
f.writelines(patched_content)
|
|
|
|
os.utime(path, (file_time, file_time))
|
|
|
|
@staticmethod
|
|
def _update_variables_file(path: Path, prefix_value: str,
|
|
processor: typing.Optional[typing.Callable] = None, quotes: bool = True):
|
|
prefix = 'prefix='
|
|
exec_prefix = 'exec_prefix='
|
|
includedir = 'includedir='
|
|
libdir = 'libdir='
|
|
|
|
def quote(value: str) -> str:
|
|
return f'"{value}"' if quotes else value
|
|
|
|
def patch_proc(line: str) -> str:
|
|
patched_line = line
|
|
|
|
if line.startswith(prefix):
|
|
patched_line = prefix + quote(prefix_value) + os.linesep
|
|
elif line.startswith(exec_prefix):
|
|
patched_line = exec_prefix + quote('${prefix}') + os.linesep
|
|
elif line.startswith(includedir):
|
|
patched_line = includedir + quote('${prefix}/include') + os.linesep
|
|
elif line.startswith(libdir):
|
|
patched_line = libdir + quote('${exec_prefix}/lib') + os.linesep
|
|
|
|
if processor:
|
|
patched_line = processor(path, patched_line)
|
|
|
|
return patched_line
|
|
|
|
BuildTarget.update_text_file(path, patch_proc)
|
|
|
|
@staticmethod
|
|
def update_config_script(path: Path, processor: typing.Optional[typing.Callable] = None):
|
|
BuildTarget._update_variables_file(path, r'$(cd "${0%/*}/.."; pwd)', processor)
|
|
|
|
@staticmethod
|
|
def update_pc_file(path: Path, processor: typing.Optional[typing.Callable] = None):
|
|
BuildTarget._update_variables_file(path, '', processor, quotes=False)
|
|
|
|
def update_pc_files(self, state: BuildState):
|
|
for root, _, files in os.walk(state.install_path, followlinks=True):
|
|
for filename in files:
|
|
if filename.endswith('.pc'):
|
|
file_path = Path(root) / filename
|
|
BuildTarget.update_pc_file(file_path, self._process_pkg_config)
|
|
|
|
@staticmethod
|
|
def _process_pkg_config(pcfile: Path, line: str) -> str:
|
|
assert pcfile
|
|
return line
|
|
|
|
def write_pc_file(self, state: BuildState,
|
|
filename=None, name=None, description=None, version='',
|
|
requires='', requires_private='', libs='', libs_private='', cflags=''):
|
|
pkgconfig_path = state.install_path / 'lib/pkgconfig'
|
|
os.makedirs(pkgconfig_path, exist_ok=True)
|
|
|
|
if not filename:
|
|
filename = self.name + '.pc'
|
|
if not name:
|
|
name = self.name
|
|
if not description:
|
|
description = self.name
|
|
if not libs:
|
|
libs = '-l' + self.name
|
|
|
|
pc_content = f'''prefix=
|
|
exec_prefix=${{prefix}}
|
|
libdir=${{exec_prefix}}/lib
|
|
includedir=${{prefix}}/include
|
|
|
|
Name: {name}
|
|
Description: {description}
|
|
Version: {version}
|
|
Requires: {requires}
|
|
Requires.private: {requires_private}
|
|
Libs: -L${{libdir}} {libs}
|
|
Libs.private: {libs_private}
|
|
Cflags: -I${{includedir}} {cflags}
|
|
'''
|
|
with open(pkgconfig_path / filename, 'w') as f:
|
|
f.write(pc_content)
|
|
|
|
@staticmethod
|
|
def make_platform_header(state: BuildState, header: str):
|
|
include_path = state.install_path / 'include'
|
|
header_parts = header.rsplit(os.sep, 1)
|
|
|
|
if len(header_parts) == 1:
|
|
header_parts.insert(0, '')
|
|
|
|
common_header = include_path / header
|
|
platform_header = include_path / header_parts[0] / f'_aedi_{state.architecture()}_{header_parts[1]}'
|
|
shutil.move(common_header, platform_header)
|
|
|
|
with open(common_header, 'w') as f:
|
|
f.write(f'''
|
|
#pragma once
|
|
|
|
#if defined(__x86_64__)
|
|
# include "_aedi_x86_64_{header_parts[1]}"
|
|
#elif defined(__aarch64__)
|
|
# include "_aedi_arm64_{header_parts[1]}"
|
|
#else
|
|
# error Unknown architecture
|
|
#endif
|
|
''')
|
|
|
|
def copy_to_bin(self, state: BuildState,
|
|
filename: typing.Optional[str] = None, new_filename: typing.Optional[str] = None):
|
|
bin_path = state.install_path / 'bin'
|
|
os.makedirs(bin_path, exist_ok=True)
|
|
|
|
if not filename:
|
|
filename = self.name
|
|
if not new_filename:
|
|
new_filename = filename
|
|
|
|
src_path = state.build_path / filename
|
|
dst_path = bin_path / new_filename
|
|
shutil.copy(src_path, dst_path)
|
|
|
|
|
|
class MakeTarget(BuildTarget):
|
|
def __init__(self, name=None):
|
|
super().__init__(name)
|
|
self.tool = 'gmake'
|
|
|
|
def configure(self, state: BuildState):
|
|
super().configure(state)
|
|
|
|
symlink_directory(state.source, state.build_path)
|
|
|
|
def build(self, state: BuildState):
|
|
assert not state.xcode
|
|
|
|
args = [
|
|
self.tool,
|
|
'-j', state.jobs,
|
|
]
|
|
|
|
if c_compiler := state.c_compiler():
|
|
args.append(f'CC={c_compiler}')
|
|
if cxx_compiler := state.cxx_compiler():
|
|
args.append(f'CXX={cxx_compiler}')
|
|
|
|
args += state.options.to_list()
|
|
|
|
work_path = state.build_path / self.src_root
|
|
subprocess.run(args, check=True, cwd=work_path, env=state.environment)
|
|
|
|
|
|
class ConfigureMakeTarget(MakeTarget):
|
|
def __init__(self, name=None):
|
|
super().__init__(name)
|
|
|
|
def configure(self, state: BuildState):
|
|
super().configure(state)
|
|
|
|
work_path = state.build_path / self.src_root
|
|
configure_path = work_path / 'configure'
|
|
|
|
common_args = [
|
|
configure_path,
|
|
f'--prefix={state.install_path}',
|
|
]
|
|
common_args += state.options.to_list()
|
|
|
|
disable_dependency_tracking = '--disable-dependency-tracking'
|
|
host = '--host=' + state.host()
|
|
|
|
args = copy.copy(common_args)
|
|
args.append(host)
|
|
args.append(disable_dependency_tracking)
|
|
|
|
try:
|
|
# Try with host and disabled dependency tracking first
|
|
subprocess.run(args, check=True, cwd=work_path, env=state.environment)
|
|
except subprocess.CalledProcessError:
|
|
# If it fails, try with disabled dependency tracking only
|
|
args = copy.copy(common_args)
|
|
args.append(disable_dependency_tracking)
|
|
|
|
try:
|
|
subprocess.run(args, check=True, cwd=work_path, env=state.environment)
|
|
except subprocess.CalledProcessError:
|
|
# Use only common command line arguments
|
|
subprocess.run(common_args, check=True, cwd=work_path, env=state.environment)
|
|
|
|
def build(self, state: BuildState):
|
|
# Clear configure script options
|
|
state.options = CommandLineOptions()
|
|
|
|
super().build(state)
|
|
|
|
|
|
class CMakeTarget(BuildTarget):
|
|
cached_project_name = None
|
|
|
|
def __init__(self, name=None):
|
|
super().__init__(name)
|
|
|
|
def detect(self, state: BuildState) -> bool:
|
|
if CMakeTarget.cached_project_name:
|
|
project_name = CMakeTarget.cached_project_name
|
|
else:
|
|
cmakelists_path = state.source / self.src_root / 'CMakeLists.txt'
|
|
|
|
if not cmakelists_path.exists():
|
|
return False
|
|
|
|
for line in open(cmakelists_path).readlines():
|
|
project_name = CMakeTarget._extract_project_name(line)
|
|
if project_name:
|
|
project_name = project_name.lower()
|
|
project_name = project_name.replace(' ', '-')
|
|
break
|
|
else:
|
|
return False
|
|
|
|
if project_name.startswith('lib'):
|
|
project_name = project_name[3:]
|
|
|
|
CMakeTarget.cached_project_name = project_name
|
|
|
|
return project_name == self.name
|
|
|
|
@staticmethod
|
|
def _extract_project_name(line: str):
|
|
project_name = None
|
|
|
|
# Try to get project name without whitespaces in it
|
|
match = re.search(r'^\s*project\s*\(\s*(\w[\w-]+)', line, re.IGNORECASE)
|
|
|
|
if not match:
|
|
# Try to get project name that contains whitespaces
|
|
match = re.search(r'^\s*project\s*\(\s*"?(\w[\s\w-]+)"?', line, re.IGNORECASE)
|
|
|
|
if match:
|
|
project_name = match.group(1)
|
|
|
|
return project_name
|
|
|
|
def configure(self, state: BuildState):
|
|
super().configure(state)
|
|
|
|
args = [
|
|
'cmake',
|
|
'-DCMAKE_BUILD_TYPE=Release',
|
|
f'-DCMAKE_INSTALL_PREFIX={state.install_path}',
|
|
f'-DCMAKE_PREFIX_PATH={state.prefix_path}',
|
|
]
|
|
|
|
opts = state.options
|
|
opts['CMAKE_C_FLAGS'] += state.compiler_flags()
|
|
opts['CMAKE_CXX_FLAGS'] += state.compiler_flags()
|
|
opts['CMAKE_EXE_LINKER_FLAGS'] += state.linker_flags()
|
|
opts['CMAKE_SHARED_LINKER_FLAGS'] += state.linker_flags()
|
|
|
|
if state.xcode:
|
|
args.append('-GXcode')
|
|
else:
|
|
args.append('-GUnix Makefiles')
|
|
|
|
if c_compiler := state.c_compiler():
|
|
args.append(f'-DCMAKE_C_COMPILER={c_compiler}')
|
|
if cxx_compiler := state.cxx_compiler():
|
|
args.append(f'-DCMAKE_CXX_COMPILER={cxx_compiler}')
|
|
|
|
architecture = state.architecture()
|
|
if architecture != machine():
|
|
args.append('-DCMAKE_SYSTEM_NAME=Darwin')
|
|
args.append('-DCMAKE_SYSTEM_PROCESSOR=' + 'aarch64' if architecture == 'arm64' else architecture)
|
|
|
|
sdk_path = state.sdk_path()
|
|
if sdk_path:
|
|
args.append(f'-DCMAKE_OSX_SYSROOT={sdk_path}')
|
|
|
|
os_version = state.os_version()
|
|
if os_version:
|
|
args.append('-DCMAKE_OSX_DEPLOYMENT_TARGET=' + str(os_version))
|
|
|
|
args += opts.to_list(CommandLineOptions.CMAKE_RULES)
|
|
args.append(state.source / self.src_root)
|
|
|
|
subprocess.run(args, check=True, cwd=state.build_path, env=state.environment)
|
|
|
|
def build(self, state: BuildState):
|
|
if state.xcode:
|
|
args = ['cmake', '--open', '.']
|
|
else:
|
|
args = ['gmake', '-j', state.jobs]
|
|
|
|
if state.verbose:
|
|
args.append('VERBOSE=1')
|
|
|
|
subprocess.run(args, check=True, cwd=state.build_path, env=state.environment)
|
|
|
|
|
|
class ConfigureMakeDependencyTarget(ConfigureMakeTarget):
|
|
def __init__(self, name=None):
|
|
super().__init__(name)
|
|
|
|
def post_build(self, state: BuildState):
|
|
self.install(state)
|
|
|
|
|
|
class ConfigureMakeStaticDependencyTarget(ConfigureMakeDependencyTarget):
|
|
def __init__(self, name=None):
|
|
super().__init__(name)
|
|
|
|
def configure(self, state: BuildState):
|
|
state.options['--enable-shared'] = 'no'
|
|
super().configure(state)
|
|
|
|
|
|
class CMakeStaticDependencyTarget(CMakeTarget):
|
|
def __init__(self, name=None):
|
|
super().__init__(name)
|
|
|
|
def configure(self, state: BuildState):
|
|
state.options['BUILD_SHARED_LIBS'] = 'NO'
|
|
super().configure(state)
|
|
|
|
def post_build(self, state: BuildState):
|
|
self.install(state)
|
|
|
|
def keep_module_target(self, state: BuildState, target: str, module_paths: typing.Sequence[Path] = ()):
|
|
import_patterns = (
|
|
r'list\s*\(APPEND\s+_cmake_import_check_targets\s+(?P<target>[\w:-]+)[\s)]',
|
|
r'list\s*\(APPEND\s+_cmake_import_check_files_for_(?P<target>[\w:-]+)\s',
|
|
)
|
|
import_regexes = [re.compile(regex, re.IGNORECASE) for regex in import_patterns]
|
|
|
|
def _keep_target(line: str):
|
|
for regex in import_regexes:
|
|
match = regex.match(line)
|
|
|
|
if match and match.group('target') != target:
|
|
return None
|
|
|
|
return line
|
|
|
|
probe_modules = False
|
|
|
|
if not module_paths:
|
|
default_modules_path = state.install_path / 'lib' / 'cmake' / self.name
|
|
default_module_name = 'targets-release.cmake'
|
|
module_paths = (
|
|
default_modules_path / default_module_name,
|
|
default_modules_path / (self.name + default_module_name)
|
|
)
|
|
probe_modules = True
|
|
|
|
for module_path in module_paths:
|
|
if not probe_modules or module_path.exists():
|
|
self.update_text_file(module_path, _keep_target)
|
|
|
|
|
|
class SingleExeCTarget(MakeTarget):
|
|
def __init__(self, name=None):
|
|
super().__init__(name)
|
|
self.options = ()
|
|
|
|
def configure(self, state: BuildState):
|
|
super().configure(state)
|
|
|
|
for option in self.options:
|
|
state.options[option] = None
|
|
|
|
def build(self, state: BuildState):
|
|
c_compiler = state.c_compiler()
|
|
assert c_compiler
|
|
|
|
args = [str(c_compiler), '-O3', '-o', self.name] + state.options.to_list()
|
|
|
|
for var in ('CFLAGS', 'LDFLAGS'):
|
|
args += shlex.split(state.environment[var])
|
|
|
|
subprocess.run(args, check=True, cwd=state.build_path, env=state.environment)
|
|
|
|
def post_build(self, state: BuildState):
|
|
self.copy_to_bin(state)
|
|
|
|
|
|
class MesonTarget(BuildTarget):
|
|
def __init__(self, name=None):
|
|
super().__init__(name)
|
|
|
|
def configure(self, state: BuildState):
|
|
super().configure(state)
|
|
|
|
args = [
|
|
state.bin_path / 'meson',
|
|
'setup',
|
|
f'--prefix={state.install_path}',
|
|
'--buildtype=release',
|
|
'--default-library=static',
|
|
]
|
|
|
|
if state.xcode:
|
|
args.append('--backend=xcode')
|
|
else:
|
|
cross_file_path = state.build_path / (state.architecture() + '.txt')
|
|
self._write_cross_file(cross_file_path, state)
|
|
args.append(f'--cross-file={cross_file_path}')
|
|
|
|
args += state.options.to_list(CommandLineOptions.CMAKE_RULES)
|
|
args.append(state.build_path)
|
|
args.append(state.source)
|
|
|
|
subprocess.run(args, check=True, cwd=state.build_path, env=state.environment)
|
|
|
|
def build(self, state: BuildState):
|
|
if state.xcode:
|
|
args = ['open', f'{self.name}.xcodeproj']
|
|
else:
|
|
args = [state.bin_path / 'meson', 'compile']
|
|
|
|
if state.verbose:
|
|
args.append('--verbose')
|
|
|
|
subprocess.run(args, check=True, cwd=state.build_path, env=state.environment)
|
|
|
|
def post_build(self, state: BuildState):
|
|
self.install(state, tool=state.bin_path / 'meson')
|
|
|
|
@staticmethod
|
|
def _write_cross_file(path: Path, state: BuildState):
|
|
c_compiler = state.c_compiler()
|
|
assert c_compiler
|
|
|
|
cxx_compiler = state.cxx_compiler()
|
|
assert cxx_compiler
|
|
|
|
cpu = state.architecture()
|
|
cpu_family = 'arm' if 'arm64' == cpu else cpu
|
|
|
|
with open(path, 'w') as f:
|
|
f.write(f'''
|
|
[binaries]
|
|
c = '{c_compiler}'
|
|
cpp = '{cxx_compiler}'
|
|
objc = '{c_compiler}'
|
|
objcpp = '{cxx_compiler}'
|
|
pkgconfig = '{state.prefix_path}/bin/pkg-config'
|
|
strip = '/usr/bin/strip'
|
|
|
|
[host_machine]
|
|
system = 'darwin'
|
|
cpu_family = '{cpu_family}'
|
|
cpu = '{cpu}'
|
|
endian = 'little'
|
|
''')
|