mirror of
https://github.com/ZDoom/zdoom-macos-deps.git
synced 2024-12-01 16:11:23 +00:00
76f9cae583
* strict target name matching * support two kinds of cmake module filename
472 lines
15 KiB
Python
472 lines
15 KiB
Python
#
|
|
# Helper module to build macOS version of various source ports
|
|
# Copyright (C) 2020-2021 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
|
|
from platform import machine
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import typing
|
|
|
|
from ..utility import CommandLineOptions, symlink_directory
|
|
from ..state import BuildState
|
|
|
|
|
|
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):
|
|
pass
|
|
|
|
def initialize(self, state: BuildState):
|
|
pass
|
|
|
|
def detect(self, state: BuildState) -> bool:
|
|
return False
|
|
|
|
def configure(self, state: BuildState):
|
|
pass
|
|
|
|
def build(self, state: BuildState):
|
|
pass
|
|
|
|
def post_build(self, state: BuildState):
|
|
pass
|
|
|
|
|
|
class BuildTarget(Target):
|
|
def __init__(self, name=None):
|
|
super().__init__(name)
|
|
|
|
self.src_root = ''
|
|
self.environment = os.environ.copy()
|
|
self.options = CommandLineOptions()
|
|
self.multi_platform = True
|
|
|
|
def configure(self, state: BuildState):
|
|
os.makedirs(state.build_path, exist_ok=True)
|
|
|
|
env = self.environment
|
|
env['PATH'] = env['PATH'] \
|
|
+ os.pathsep + '/Applications/CMake.app/Contents/bin' \
|
|
+ os.pathsep + state.bin_path
|
|
|
|
if state.xcode:
|
|
return
|
|
|
|
env['CC'] = state.c_compiler()
|
|
env['CXX'] = state.cxx_compiler()
|
|
|
|
for prefix in ('CPP', 'C', 'CXX', 'OBJC', 'OBJCXX'):
|
|
varname = f'{prefix}FLAGS'
|
|
|
|
self._update_env(varname, f'-I{state.include_path}')
|
|
self._set_sdk(state, varname)
|
|
self._set_os_version(state, varname)
|
|
|
|
ldflags = 'LDFLAGS'
|
|
|
|
self._update_env(ldflags, f'-L{state.lib_path}')
|
|
self._set_sdk(state, ldflags)
|
|
self._set_os_version(state, ldflags)
|
|
|
|
def _update_env(self, name: str, value: str):
|
|
env = self.environment
|
|
env[name] = env[name] + ' ' + value if name in env else value
|
|
|
|
def _set_sdk(self, state: BuildState, varname: str):
|
|
sdk_path = state.sdk_path()
|
|
if sdk_path:
|
|
self._update_env(varname, '-isysroot ' + sdk_path)
|
|
|
|
def _set_os_version(self, state: BuildState, varname: str):
|
|
os_version = state.os_version()
|
|
if os_version:
|
|
self._update_env(varname, '-mmacosx-version-min=' + os_version)
|
|
|
|
def install(self, state: BuildState, options: CommandLineOptions = None, tool: str = 'gmake'):
|
|
if state.xcode:
|
|
return
|
|
|
|
if os.path.exists(state.install_path):
|
|
shutil.rmtree(state.install_path)
|
|
|
|
args = [tool, 'install']
|
|
args += options and options.to_list() or []
|
|
|
|
subprocess.check_call(args, cwd=state.build_path, env=self.environment)
|
|
|
|
self.update_pc_files(state)
|
|
|
|
@staticmethod
|
|
def update_text_file(path: str, 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: str, 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: str, processor: typing.Callable = None):
|
|
BuildTarget._update_variables_file(path, r'$(cd "${0%/*}/.."; pwd)', processor)
|
|
|
|
@staticmethod
|
|
def update_pc_file(path: str, 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 = root + os.sep + filename
|
|
BuildTarget.update_pc_file(file_path, self._process_pkg_config)
|
|
|
|
@staticmethod
|
|
def _process_pkg_config(pcfile: str, 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 + os.sep + 'include' + os.sep
|
|
header_parts = header.rsplit(os.sep, 1)
|
|
|
|
if len(header_parts) == 1:
|
|
header_parts.insert(0, '')
|
|
|
|
common_header = include_path + header
|
|
platform_header = f'{include_path}{header_parts[0]}/_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,
|
|
'CC=' + state.c_compiler(),
|
|
'CXX=' + state.cxx_compiler(),
|
|
]
|
|
args += self.options.to_list()
|
|
|
|
work_path = state.build_path + self.src_root
|
|
subprocess.check_call(args, cwd=work_path, env=self.environment)
|
|
|
|
|
|
class ConfigureMakeTarget(BuildTarget):
|
|
def __init__(self, name=None):
|
|
super().__init__(name)
|
|
self.make = MakeTarget(name)
|
|
|
|
def initialize(self, state: BuildState):
|
|
super().initialize(state)
|
|
self.make.initialize(state)
|
|
|
|
def configure(self, state: BuildState):
|
|
super().configure(state)
|
|
self.make.configure(state)
|
|
|
|
work_path = state.build_path + self.src_root
|
|
configure_path = work_path + os.sep + 'configure'
|
|
|
|
common_args = [
|
|
configure_path,
|
|
'--prefix=' + state.install_path,
|
|
]
|
|
common_args += self.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.check_call(args, cwd=work_path, env=self.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.check_call(args, cwd=work_path, env=self.environment)
|
|
except subprocess.CalledProcessError:
|
|
# Use only common command line arguments
|
|
subprocess.check_call(common_args, cwd=work_path, env=self.environment)
|
|
|
|
def build(self, state: BuildState):
|
|
assert not state.xcode
|
|
self.make.build(state)
|
|
|
|
|
|
class CMakeTarget(BuildTarget):
|
|
def __init__(self, name=None):
|
|
super().__init__(name)
|
|
|
|
def detect(self, state: BuildState) -> bool:
|
|
src_root = self.src_root and os.sep + self.src_root or ''
|
|
cmakelists_path = state.source + src_root + os.sep + 'CMakeLists.txt'
|
|
|
|
if not os.path.exists(cmakelists_path):
|
|
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
|
|
|
|
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'project\s*\(\s*(\w[\w-]+)', line, re.IGNORECASE)
|
|
|
|
if not match:
|
|
# Try to get project name that contains whitespaces
|
|
match = re.search(r'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',
|
|
'-DCMAKE_INSTALL_PREFIX=' + state.install_path,
|
|
'-DCMAKE_PREFIX_PATH=' + state.prefix_path,
|
|
]
|
|
|
|
if state.xcode:
|
|
args.append('-GXcode')
|
|
else:
|
|
args.append('-GUnix Makefiles')
|
|
args.append('-DCMAKE_C_COMPILER=' + state.c_compiler())
|
|
args.append('-DCMAKE_CXX_COMPILER=' + state.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)
|
|
|
|
os_version = state.os_version()
|
|
if os_version:
|
|
args.append('-DCMAKE_OSX_DEPLOYMENT_TARGET=' + os_version)
|
|
|
|
sdk_path = state.sdk_path()
|
|
if sdk_path:
|
|
args.append('-DCMAKE_OSX_SYSROOT=' + sdk_path)
|
|
|
|
args += self.options.to_list(CommandLineOptions.CMAKE_RULES)
|
|
args.append(state.source + self.src_root)
|
|
|
|
subprocess.check_call(args, cwd=state.build_path, env=self.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.check_call(args, cwd=state.build_path, env=self.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)
|
|
|
|
self.options['--enable-shared'] = 'no'
|
|
|
|
|
|
class CMakeStaticDependencyTarget(CMakeTarget):
|
|
def __init__(self, name=None):
|
|
super().__init__(name)
|
|
|
|
# Set commonly used variables for static libraries
|
|
opts = self.options
|
|
opts['BUILD_SHARED_LIBS'] = 'NO'
|
|
opts['ENABLE_SHARED'] = 'NO'
|
|
opts['LIBTYPE'] = 'STATIC'
|
|
|
|
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 not match:
|
|
continue
|
|
else:
|
|
if match.group('target') != target:
|
|
return None
|
|
|
|
return line
|
|
|
|
module = 'targets-release.cmake'
|
|
|
|
for probe_module in (module, self.name + module):
|
|
module_path = f'{state.install_path}/lib/cmake/{self.name}/{probe_module}'
|
|
|
|
if os.path.exists(module_path):
|
|
self.update_text_file(module_path, _keep_target)
|