mirror of
https://github.com/ZDoom/zdoom-macos-deps.git
synced 2024-12-14 22:31:24 +00:00
528 lines
16 KiB
Python
528 lines
16 KiB
Python
#
|
|
# Helper module to build macOS version of various source ports
|
|
# Copyright (C) 2020-2022 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 (
|
|
OS_VERSION_ARM64,
|
|
OS_VERSION_X86_64,
|
|
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
|
|
|
|
self.os_version = dict()
|
|
self.os_version['x86_64'] = OS_VERSION_X86_64
|
|
self.os_version['arm64'] = OS_VERSION_ARM64
|
|
|
|
self.sdk_version = dict()
|
|
self.sdk_version['x86_64'] = OS_VERSION_X86_64
|
|
self.sdk_version['arm64'] = OS_VERSION_ARM64
|
|
|
|
def configure(self, state: BuildState):
|
|
os_version = state.os_version()
|
|
if os_version and os_version < self.os_version[state.architecture()]:
|
|
raise RuntimeError('Minimum OS version requirement is not met')
|
|
|
|
if sdk_version := state.sdk_version():
|
|
if sdk_version < self.sdk_version[state.architecture()]:
|
|
raise RuntimeError('Minimum SDK version requirement is not met')
|
|
|
|
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 ('CPP', 'C', 'CXX', 'OBJC', 'OBJCXX'):
|
|
var_name = f'{prefix}FLAGS'
|
|
|
|
state.update_environment(var_name, f'-I{state.include_path}')
|
|
state.set_sdk(var_name)
|
|
state.set_os_version(var_name)
|
|
|
|
ldflags = 'LDFLAGS'
|
|
|
|
state.update_environment(ldflags, f'-L{state.lib_path}')
|
|
state.set_sdk(ldflags)
|
|
state.set_os_version(ldflags)
|
|
|
|
def install(self, state: BuildState, options: 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.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)
|
|
|
|
with open(path, 'w') as f:
|
|
f.writelines(patched_content)
|
|
|
|
@staticmethod
|
|
def _update_variables_file(path: Path, prefix_value: str, processor: 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.Callable = None):
|
|
BuildTarget._update_variables_file(path, r'$(cd "${0%/*}/.."; pwd)', processor)
|
|
|
|
@staticmethod
|
|
def update_pc_file(path: Path, processor: 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: str = None, new_filename: 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_C_FLAGS="-ffile-prefix-map={state.source}/="',
|
|
f'-DCMAKE_CXX_FLAGS="-ffile-prefix-map={state.source}/="',
|
|
f'-DCMAKE_INSTALL_PREFIX={state.install_path}',
|
|
f'-DCMAKE_PREFIX_PATH={state.prefix_path}',
|
|
]
|
|
|
|
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 += state.options.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):
|
|
import_patterns = (
|
|
r'list\s*\(APPEND\s+_IMPORT_CHECK_TARGETS\s+(?P<target>\w+::[\w-]+)[\s)]',
|
|
r'list\s*\(APPEND\s+_IMPORT_CHECK_FILES_FOR_(?P<target>\w+::[\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
|
|
|
|
module = 'targets-release.cmake'
|
|
|
|
for probe_module in (module, self.name + module):
|
|
module_path = state.install_path / 'lib' / 'cmake' / self.name / probe_module
|
|
|
|
if 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)
|