zdoom-macos-deps/aedi/target/base.py
alexey.lysiuk 4999b2ee4a split build script onto several modules
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
2021-01-14 10:34:20 +02:00

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)