mirror of
https://github.com/ZDoom/zdoom-macos-deps.git
synced 2024-11-25 21:31:25 +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
299 lines
11 KiB
Python
299 lines
11 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 hashlib
|
|
import os
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import typing
|
|
import urllib.request
|
|
from distutils.version import StrictVersion
|
|
from pathlib import Path
|
|
|
|
from .utility import CommandLineOptions
|
|
|
|
|
|
class BuildState:
|
|
def __init__(self):
|
|
self_path = Path(__file__)
|
|
self.root_path = self_path.parent.parent
|
|
self.deps_path = self.root_path / 'deps'
|
|
self.prefix_path = self.root_path / 'prefix'
|
|
self.bin_path = self.prefix_path / 'bin'
|
|
self.include_path = self.prefix_path / 'include'
|
|
self.lib_path = self.prefix_path / 'lib'
|
|
self.patch_path = self.root_path / 'patch'
|
|
self.source_path = self.root_path / 'source'
|
|
self.temp_path = self.root_path / 'temp'
|
|
|
|
self.source = Path()
|
|
self.external_source = True
|
|
|
|
self.build_path = None
|
|
self.native_build_path = None
|
|
|
|
self.output_path = None
|
|
self.install_path = None
|
|
|
|
self._compiler_flags = None
|
|
self._linker_flags = None
|
|
|
|
self.platform = None
|
|
self.xcode = False
|
|
self.verbose = False
|
|
self.jobs = 1
|
|
|
|
self.static_moltenvk = False
|
|
self.quasi_glib = False
|
|
|
|
self.environment = os.environ.copy()
|
|
self.options = CommandLineOptions()
|
|
|
|
def architecture(self) -> str:
|
|
return self.platform.architecture if self.platform else ''
|
|
|
|
def host(self) -> str:
|
|
return self.platform.host if self.platform else ''
|
|
|
|
def os_version(self) -> StrictVersion:
|
|
return self.platform.os_version if self.platform else None
|
|
|
|
def sdk_path(self) -> Path:
|
|
return self.platform.sdk_path if self.platform else None
|
|
|
|
def sdk_version(self) -> typing.Union[StrictVersion, None]:
|
|
if sdk_path := self.sdk_path():
|
|
if match := re.search(r'/MacOSX(\d+.\d+).sdk', str(sdk_path), re.IGNORECASE):
|
|
return StrictVersion(match[1])
|
|
|
|
return None
|
|
|
|
def c_compiler(self) -> Path:
|
|
return self.platform.c_compiler if self.platform else None
|
|
|
|
def cxx_compiler(self) -> Path:
|
|
return self.platform.cxx_compiler if self.platform else None
|
|
|
|
def compiler_flags(self) -> str:
|
|
if not self._compiler_flags:
|
|
self._compiler_flags = f'-I{self.include_path} -ffile-prefix-map={self.source}/='
|
|
|
|
return self._compiler_flags
|
|
|
|
def linker_flags(self) -> str:
|
|
if not self._linker_flags:
|
|
self._linker_flags = f'-L{self.lib_path}'
|
|
|
|
# Fix for Xcode 15.0 known issue with the new linker
|
|
# https://developer.apple.com/documentation/xcode-release-notes/xcode-15-release-notes#Known-Issues
|
|
# Binaries using symbols with a weak definition crash at runtime on iOS 14/macOS 12 or older.
|
|
# This impacts primarily C++ projects due to their extensive use of weak symbols. (114813650) (FB13097713)
|
|
# Workaround: Bump the minimum deployment target to iOS 15, macOS 12, watchOS 8 or tvOS 15,
|
|
# or add -Wl,-ld_classic to the OTHER_LDFLAGS build setting.
|
|
|
|
ld_classic_arg = '-Wl,-ld_classic'
|
|
check_args = ('clang', '-xc++', ld_classic_arg, '-')
|
|
check_code = b'int main() {}'
|
|
|
|
if subprocess.run(check_args, capture_output=True, input=check_code).returncode == 0:
|
|
self._linker_flags += f' {ld_classic_arg}'
|
|
os.unlink('a.out')
|
|
|
|
return self._linker_flags
|
|
|
|
def checkout_git(self, url: str, branch: typing.Optional[str] = None):
|
|
if self.source.exists():
|
|
return
|
|
|
|
args = ('git', 'clone', '--recurse-submodules', url, self.source)
|
|
subprocess.run(args, check=True, cwd=self.root_path, env=self.environment)
|
|
|
|
if branch:
|
|
args = ('git', 'checkout', '-b', branch, 'origin/' + branch)
|
|
subprocess.run(args, check=True, cwd=self.source, env=self.environment)
|
|
|
|
def download_source(self, url: str, checksum: str, patches: typing.Union[tuple, list, str, None] = None):
|
|
if self.external_source:
|
|
return
|
|
|
|
os.makedirs(self.source, exist_ok=True)
|
|
|
|
data, filepath = self._read_source_package(url)
|
|
self._verify_checksum(checksum, data, filepath)
|
|
|
|
first_path_component, extract_path = self._unpack_source_package(filepath)
|
|
|
|
if not patches:
|
|
pass
|
|
elif isinstance(patches, str):
|
|
self._apply_source_patch(extract_path, patches)
|
|
elif isinstance(patches, (tuple, list)):
|
|
for patch in patches:
|
|
self._apply_source_patch(extract_path, patch)
|
|
else:
|
|
assert False
|
|
|
|
# Adjust source and build paths according to extracted source code
|
|
self.source = extract_path
|
|
self.build_path = self.build_path / first_path_component
|
|
|
|
def _read_source_package(self, url: str) -> typing.Tuple[bytes, Path]:
|
|
filename = url.rsplit(os.sep, 1)[1]
|
|
filepath = self.source / filename
|
|
|
|
if filepath.exists():
|
|
# Read existing source package
|
|
with open(filepath, 'rb') as f:
|
|
data = f.read()
|
|
else:
|
|
# Download package with source code
|
|
print(f'Downloading {filename}')
|
|
|
|
response = urllib.request.urlopen(url)
|
|
|
|
try:
|
|
with open(filepath, 'wb') as f:
|
|
data = response.read()
|
|
f.write(data)
|
|
|
|
except IOError:
|
|
os.unlink(filepath)
|
|
raise
|
|
|
|
return data, filepath
|
|
|
|
@staticmethod
|
|
def _verify_checksum(checksum: str, data: bytes, filepath: Path) -> None:
|
|
file_hasher = hashlib.sha256()
|
|
file_hasher.update(data)
|
|
file_checksum = file_hasher.hexdigest()
|
|
|
|
if file_checksum != checksum:
|
|
filepath.unlink()
|
|
raise Exception(f'Checksum of {filepath} does not match, expected: {checksum}, actual: {file_checksum}')
|
|
|
|
def _unpack_source_package(self, filepath: Path) -> typing.Tuple[str, Path]:
|
|
args = ('tar', '-tf', filepath)
|
|
result = subprocess.run(args, check=True, env=self.environment, stdout=subprocess.PIPE)
|
|
|
|
file_paths_str = result.stdout.decode("utf-8")
|
|
file_paths = file_paths_str.split('\n')
|
|
file_paths.remove('')
|
|
assert len(file_paths) > 0
|
|
|
|
# Determine root path of source code to be extracted
|
|
# If all files and directories are stored in one top level directory, this directory is used as a root
|
|
# If there is no single top level directory, new root directory will be created
|
|
need_new_directory = False
|
|
first_path_component = ''
|
|
|
|
for path in file_paths:
|
|
if os.sep not in path:
|
|
need_new_directory = True
|
|
break
|
|
else:
|
|
current_first_path_component = path[:path.find(os.sep)]
|
|
|
|
if first_path_component:
|
|
if first_path_component != current_first_path_component:
|
|
need_new_directory = True
|
|
break
|
|
else:
|
|
first_path_component = current_first_path_component
|
|
|
|
work_path = self.source
|
|
|
|
if need_new_directory:
|
|
first_path_component = Path(filepath.name).stem
|
|
work_path /= first_path_component
|
|
|
|
extract_path = self.source / first_path_component
|
|
|
|
if not extract_path.exists():
|
|
os.makedirs(work_path, exist_ok=True)
|
|
|
|
# Extract source code package
|
|
try:
|
|
args = ('tar', '-xf', filepath)
|
|
subprocess.run(args, check=True, cwd=work_path, env=self.environment)
|
|
except (IOError, subprocess.CalledProcessError):
|
|
shutil.rmtree(extract_path, ignore_errors=True)
|
|
raise
|
|
|
|
return first_path_component, extract_path
|
|
|
|
def _apply_source_patch(self, extract_path: Path, patch: str):
|
|
patch_path = self.patch_path / (patch + '.diff')
|
|
assert patch_path.exists()
|
|
|
|
args = ['patch', '--strip=1', '--input=' + str(patch_path)]
|
|
|
|
# Check if patch is already applied
|
|
dry_run_args = args + ['--dry-run', '--force']
|
|
dry_run = subprocess.run(dry_run_args, cwd=extract_path, env=self.environment,
|
|
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
|
|
if dry_run.returncode == 0:
|
|
# Patch wasn't applied yet, do it now
|
|
subprocess.run(args, check=True, cwd=extract_path, env=self.environment)
|
|
|
|
def run_pkg_config(self, *args) -> str:
|
|
os.makedirs(self.build_path, exist_ok=True)
|
|
|
|
args = (self.bin_path / 'pkg-config',) + args
|
|
result = subprocess.run(args, check=True, cwd=self.build_path, env=self.environment, stdout=subprocess.PIPE)
|
|
|
|
return result.stdout.decode('utf-8').rstrip('\n')
|
|
|
|
def has_source_file(self, path: typing.Union[str, Path]):
|
|
return (self.source / path).exists()
|
|
|
|
def update_flags_environment_variable(self, name: str, value: str):
|
|
sdk_path = self.sdk_path()
|
|
if sdk_path:
|
|
value += f' -isysroot {sdk_path}'
|
|
|
|
os_version = self.os_version()
|
|
if os_version:
|
|
value += f' -mmacosx-version-min={os_version}'
|
|
|
|
env = self.environment
|
|
env[name] = env[name] + ' ' + value if name in env else value
|
|
|
|
def validate_minimum_version(self, version: str):
|
|
minimum_version = StrictVersion(version)
|
|
|
|
if os_version := self.os_version():
|
|
if os_version < minimum_version:
|
|
raise RuntimeError('Minimum OS version requirement is not met')
|
|
|
|
if sdk_version := self.sdk_version():
|
|
if sdk_version < minimum_version:
|
|
raise RuntimeError('Minimum SDK version requirement is not met')
|
|
|
|
def source_version(self):
|
|
version = ''
|
|
|
|
args = ('git', f'--git-dir={self.source}/.git', 'describe', '--tags')
|
|
git_describe = subprocess.run(args, env=self.environment, capture_output=True)
|
|
|
|
if git_describe.returncode == 0:
|
|
version = git_describe.stdout.decode('ascii')
|
|
|
|
return version
|