mirror of
https://github.com/ZDoom/zdoom-macos-deps.git
synced 2024-11-22 20:11:51 +00:00
4999b2ee4a
put all code into new package, build script will use it as a black box split build state from builder class, and pass state to target methods place base, main, dependency, special targets into separate files, and put them in own package This implements #19
416 lines
13 KiB
Python
416 lines
13 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 not state.xcode:
|
|
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 = 'make'):
|
|
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 []
|
|
|
|
work_path = state.build_path + self.src_root
|
|
subprocess.check_call(args, cwd=work_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_prefix_shell_script(path: str, processor: typing.Callable = None):
|
|
prefix = 'prefix='
|
|
|
|
def update_prefix(line: str) -> str:
|
|
if line.startswith(prefix):
|
|
patched_line = prefix + r'"$(cd "${0%/*}/.."; pwd)"' + os.linesep
|
|
else:
|
|
patched_line = line
|
|
|
|
if processor:
|
|
patched_line = processor(patched_line)
|
|
|
|
return patched_line
|
|
|
|
BuildTarget.update_text_file(path, update_prefix)
|
|
|
|
@staticmethod
|
|
def update_pc_file(path: str, processor: typing.Callable = None):
|
|
prefix = 'prefix='
|
|
|
|
def pc_proc(line: str) -> str:
|
|
patched_line = line
|
|
|
|
if line.startswith(prefix):
|
|
# Clear prefix variable
|
|
patched_line = prefix + os.linesep
|
|
|
|
if processor:
|
|
patched_line = processor(path, patched_line)
|
|
|
|
return patched_line
|
|
|
|
BuildTarget.update_text_file(path, pc_proc)
|
|
|
|
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
|
|
include_platform_path = include_path
|
|
|
|
header_parts = header.rsplit(os.sep, 1)
|
|
if len(header_parts) == 1:
|
|
header_parts.insert(0, '')
|
|
|
|
include_platform_path += header_parts[0] + os.sep + state.architecture()
|
|
os.makedirs(include_platform_path, exist_ok=True)
|
|
|
|
root_header = include_path + header
|
|
shutil.move(root_header, include_platform_path)
|
|
|
|
with open(root_header, 'w') as f:
|
|
f.write(f'''
|
|
#pragma once
|
|
|
|
#if defined(__x86_64__)
|
|
# include "x86_64/{header_parts[1]}"
|
|
#elif defined(__aarch64__)
|
|
# include "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 = 'make'
|
|
|
|
def configure(self, state: BuildState):
|
|
super().configure(state)
|
|
|
|
symlink_directory(state.source_path, 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_path + 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_path + self.src_root)
|
|
|
|
subprocess.check_call(args, cwd=state.build_path, env=self.environment)
|
|
|
|
def build(self, state: BuildState):
|
|
if state.xcode:
|
|
# TODO: support case-sensitive file system
|
|
args = ('open', self.name + '.xcodeproj')
|
|
else:
|
|
args = ['make', '-j', state.jobs]
|
|
|
|
if state.verbose:
|
|
args.append('VERBOSE=1')
|
|
|
|
subprocess.check_call(args, cwd=state.build_path)
|