# Copyright 2014-2021 The Meson development team # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # http://www.apache.org/licenses/LICENSE-2.0 # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import annotations import uuid, os, operator import typing as T from . import backends from .. import build from .. import dependencies from .. import mesonlib from .. import mlog from ..mesonlib import MesonBugException, MesonException, OptionKey if T.TYPE_CHECKING: from ..interpreter import Interpreter INDENT = '\t' XCODETYPEMAP = {'c': 'sourcecode.c.c', 'a': 'archive.ar', 'cc': 'sourcecode.cpp.cpp', 'cxx': 'sourcecode.cpp.cpp', 'cpp': 'sourcecode.cpp.cpp', 'c++': 'sourcecode.cpp.cpp', 'm': 'sourcecode.c.objc', 'mm': 'sourcecode.cpp.objcpp', 'h': 'sourcecode.c.h', 'hpp': 'sourcecode.cpp.h', 'hxx': 'sourcecode.cpp.h', 'hh': 'sourcecode.cpp.hh', 'inc': 'sourcecode.c.h', 'swift': 'sourcecode.swift', 'dylib': 'compiled.mach-o.dylib', 'o': 'compiled.mach-o.objfile', 's': 'sourcecode.asm', 'asm': 'sourcecode.asm', } LANGNAMEMAP = {'c': 'C', 'cpp': 'CPLUSPLUS', 'objc': 'OBJC', 'objcpp': 'OBJCPLUSPLUS', 'swift': 'SWIFT_' } OPT2XCODEOPT = {'plain': None, '0': '0', 'g': '0', '1': '1', '2': '2', '3': '3', 's': 's', } BOOL2XCODEBOOL = {True: 'YES', False: 'NO'} LINKABLE_EXTENSIONS = {'.o', '.a', '.obj', '.so', '.dylib'} class FileTreeEntry: def __init__(self) -> None: self.subdirs = {} self.targets = [] class PbxArray: def __init__(self) -> None: self.items = [] def add_item(self, item: T.Union[PbxArrayItem, str], comment: str = '') -> None: if isinstance(item, PbxArrayItem): self.items.append(item) else: self.items.append(PbxArrayItem(item, comment)) def write(self, ofile: T.TextIO, indent_level: int) -> None: ofile.write('(\n') indent_level += 1 for i in self.items: if i.comment: ofile.write(indent_level*INDENT + f'{i.value} {i.comment},\n') else: ofile.write(indent_level*INDENT + f'{i.value},\n') indent_level -= 1 ofile.write(indent_level*INDENT + ');\n') class PbxArrayItem: def __init__(self, value: str, comment: str = ''): self.value = value if comment: if '/*' in comment: self.comment = comment else: self.comment = f'/* {comment} */' else: self.comment = comment class PbxComment: def __init__(self, text: str): assert isinstance(text, str) assert '/*' not in text self.text = f'/* {text} */' def write(self, ofile: T.TextIO, indent_level: int) -> None: ofile.write(f'\n{self.text}\n') class PbxDictItem: def __init__(self, key: str, value: T.Union[PbxArray, PbxDict, str, int], comment: str = ''): self.key = key self.value = value if comment: if '/*' in comment: self.comment = comment else: self.comment = f'/* {comment} */' else: self.comment = comment class PbxDict: def __init__(self) -> None: # This class is a bit weird, because we want to write PBX dicts in # defined order _and_ we want to write intermediate comments also in order. self.keys = set() self.items = [] def add_item(self, key: str, value: T.Union[PbxArray, PbxDict, str, int], comment: str = '') -> None: assert key not in self.keys item = PbxDictItem(key, value, comment) self.keys.add(key) self.items.append(item) def has_item(self, key): return key in self.keys def add_comment(self, comment: PbxComment) -> None: assert isinstance(comment, PbxComment) self.items.append(comment) def write(self, ofile: T.TextIO, indent_level: int) -> None: ofile.write('{\n') indent_level += 1 for i in self.items: if isinstance(i, PbxComment): i.write(ofile, indent_level) elif isinstance(i, PbxDictItem): if isinstance(i.value, (str, int)): if i.comment: ofile.write(indent_level*INDENT + f'{i.key} = {i.value} {i.comment};\n') else: ofile.write(indent_level*INDENT + f'{i.key} = {i.value};\n') elif isinstance(i.value, PbxDict): if i.comment: ofile.write(indent_level*INDENT + f'{i.key} {i.comment} = ') else: ofile.write(indent_level*INDENT + f'{i.key} = ') i.value.write(ofile, indent_level) elif isinstance(i.value, PbxArray): if i.comment: ofile.write(indent_level*INDENT + f'{i.key} {i.comment} = ') else: ofile.write(indent_level*INDENT + f'{i.key} = ') i.value.write(ofile, indent_level) else: print(i) print(i.key) print(i.value) raise RuntimeError('missing code') else: print(i) raise RuntimeError('missing code2') indent_level -= 1 ofile.write(indent_level*INDENT + '}') if indent_level == 0: ofile.write('\n') else: ofile.write(';\n') class XCodeBackend(backends.Backend): name = 'xcode' def __init__(self, build: T.Optional[build.Build], interpreter: T.Optional[Interpreter]): super().__init__(build, interpreter) self.project_uid = self.environment.coredata.lang_guids['default'].replace('-', '')[:24] self.buildtype = T.cast('str', self.environment.coredata.get_option(OptionKey('buildtype'))) self.project_conflist = self.gen_id() self.maingroup_id = self.gen_id() self.all_id = self.gen_id() self.all_buildconf_id = self.gen_id() self.buildtypes = [self.buildtype] self.test_id = self.gen_id() self.test_command_id = self.gen_id() self.test_buildconf_id = self.gen_id() self.regen_id = self.gen_id() self.regen_command_id = self.gen_id() self.regen_buildconf_id = self.gen_id() self.regen_dependency_id = self.gen_id() self.top_level_dict = PbxDict() self.generator_outputs = {} # In Xcode files are not accessed via their file names, but rather every one of them # gets an unique id. More precisely they get one unique id per target they are used # in. If you generate only one id per file and use them, compilation will work but the # UI will only show the file in one target but not the others. Thus they key is # a tuple containing the target and filename. self.buildfile_ids = {} # That is not enough, though. Each target/file combination also gets a unique id # in the file reference section. Because why not. This means that a source file # that is used in two targets gets a total of four unique ID numbers. self.fileref_ids = {} def write_pbxfile(self, top_level_dict, ofilename): tmpname = ofilename + '.tmp' with open(tmpname, 'w', encoding='utf-8') as ofile: ofile.write('// !$*UTF8*$!\n') top_level_dict.write(ofile, 0) os.replace(tmpname, ofilename) def gen_id(self) -> str: return str(uuid.uuid4()).upper().replace('-', '')[:24] def get_target_dir(self, target): dirname = os.path.join(target.get_subdir(), T.cast('str', self.environment.coredata.get_option(OptionKey('buildtype')))) #os.makedirs(os.path.join(self.environment.get_build_dir(), dirname), exist_ok=True) return dirname def get_custom_target_output_dir(self, target): dirname = target.get_subdir() os.makedirs(os.path.join(self.environment.get_build_dir(), dirname), exist_ok=True) return dirname def target_to_build_root(self, target): if self.get_target_dir(target) == '': return '' directories = os.path.normpath(self.get_target_dir(target)).split(os.sep) return os.sep.join(['..'] * len(directories)) def object_filename_from_source(self, target, source): # Xcode has the following naming scheme: # projectname.build/debug/prog@exe.build/Objects-normal/x86_64/func.o project = self.build.project_name buildtype = self.buildtype tname = target.get_id() arch = 'x86_64' if isinstance(source, mesonlib.File): source = source.fname stem = os.path.splitext(os.path.basename(source))[0] obj_path = f'{project}.build/{buildtype}/{tname}.build/Objects-normal/{arch}/{stem}.o' return obj_path def generate(self, capture: bool = False, vslite_ctx: dict = None) -> T.Optional[dict]: # Check for (currently) unexpected capture arg use cases - if capture: raise MesonBugException('We do not expect the xcode backend to generate with \'capture = True\'') if vslite_ctx: raise MesonBugException('We do not expect the xcode backend to be given a valid \'vslite_ctx\'') self.serialize_tests() # Cache the result as the method rebuilds the array every time it is called. self.build_targets = self.build.get_build_targets() self.custom_targets = self.build.get_custom_targets() self.generate_filemap() self.generate_buildstylemap() self.generate_build_phase_map() self.generate_build_configuration_map() self.generate_build_configurationlist_map() self.generate_project_configurations_map() self.generate_buildall_configurations_map() self.generate_test_configurations_map() self.generate_native_target_map() self.generate_native_frameworks_map() self.generate_custom_target_map() self.generate_generator_target_map() self.generate_source_phase_map() self.generate_target_dependency_map() self.generate_pbxdep_map() self.generate_containerproxy_map() self.generate_target_file_maps() self.generate_build_file_maps() self.proj_dir = os.path.join(self.environment.get_build_dir(), self.build.project_name + '.xcodeproj') os.makedirs(self.proj_dir, exist_ok=True) self.proj_file = os.path.join(self.proj_dir, 'project.pbxproj') objects_dict = self.generate_prefix(self.top_level_dict) objects_dict.add_comment(PbxComment('Begin PBXAggregateTarget section')) self.generate_pbx_aggregate_target(objects_dict) objects_dict.add_comment(PbxComment('End PBXAggregateTarget section')) objects_dict.add_comment(PbxComment('Begin PBXBuildFile section')) self.generate_pbx_build_file(objects_dict) objects_dict.add_comment(PbxComment('End PBXBuildFile section')) objects_dict.add_comment(PbxComment('Begin PBXBuildStyle section')) self.generate_pbx_build_style(objects_dict) objects_dict.add_comment(PbxComment('End PBXBuildStyle section')) objects_dict.add_comment(PbxComment('Begin PBXContainerItemProxy section')) self.generate_pbx_container_item_proxy(objects_dict) objects_dict.add_comment(PbxComment('End PBXContainerItemProxy section')) objects_dict.add_comment(PbxComment('Begin PBXFileReference section')) self.generate_pbx_file_reference(objects_dict) objects_dict.add_comment(PbxComment('End PBXFileReference section')) objects_dict.add_comment(PbxComment('Begin PBXFrameworksBuildPhase section')) self.generate_pbx_frameworks_buildphase(objects_dict) objects_dict.add_comment(PbxComment('End PBXFrameworksBuildPhase section')) objects_dict.add_comment(PbxComment('Begin PBXGroup section')) self.generate_pbx_group(objects_dict) objects_dict.add_comment(PbxComment('End PBXGroup section')) objects_dict.add_comment(PbxComment('Begin PBXNativeTarget section')) self.generate_pbx_native_target(objects_dict) objects_dict.add_comment(PbxComment('End PBXNativeTarget section')) objects_dict.add_comment(PbxComment('Begin PBXProject section')) self.generate_pbx_project(objects_dict) objects_dict.add_comment(PbxComment('End PBXProject section')) objects_dict.add_comment(PbxComment('Begin PBXShellScriptBuildPhase section')) self.generate_pbx_shell_build_phase(objects_dict) objects_dict.add_comment(PbxComment('End PBXShellScriptBuildPhase section')) objects_dict.add_comment(PbxComment('Begin PBXSourcesBuildPhase section')) self.generate_pbx_sources_build_phase(objects_dict) objects_dict.add_comment(PbxComment('End PBXSourcesBuildPhase section')) objects_dict.add_comment(PbxComment('Begin PBXTargetDependency section')) self.generate_pbx_target_dependency(objects_dict) objects_dict.add_comment(PbxComment('End PBXTargetDependency section')) objects_dict.add_comment(PbxComment('Begin XCBuildConfiguration section')) self.generate_xc_build_configuration(objects_dict) objects_dict.add_comment(PbxComment('End XCBuildConfiguration section')) objects_dict.add_comment(PbxComment('Begin XCConfigurationList section')) self.generate_xc_configurationList(objects_dict) objects_dict.add_comment(PbxComment('End XCConfigurationList section')) self.generate_suffix(self.top_level_dict) self.write_pbxfile(self.top_level_dict, self.proj_file) self.generate_regen_info() def get_xcodetype(self, fname): extension = fname.split('.')[-1] if extension == 'C': extension = 'cpp' xcodetype = XCODETYPEMAP.get(extension.lower()) if not xcodetype: xcodetype = 'sourcecode.unknown' return xcodetype def generate_filemap(self) -> None: self.filemap = {} # Key is source file relative to src root. self.target_filemap = {} for name, t in self.build_targets.items(): for s in t.sources: if isinstance(s, mesonlib.File): s = os.path.join(s.subdir, s.fname) self.filemap[s] = self.gen_id() for o in t.objects: if isinstance(o, str): o = os.path.join(t.subdir, o) self.filemap[o] = self.gen_id() self.target_filemap[name] = self.gen_id() def generate_buildstylemap(self) -> None: self.buildstylemap = {self.buildtype: self.gen_id()} def generate_build_phase_map(self) -> None: for tname, t in self.build_targets.items(): # generate id for our own target-name t.buildphasemap = {} t.buildphasemap[tname] = self.gen_id() # each target can have it's own Frameworks/Sources/..., generate id's for those t.buildphasemap['Frameworks'] = self.gen_id() t.buildphasemap['Resources'] = self.gen_id() t.buildphasemap['Sources'] = self.gen_id() def generate_build_configuration_map(self) -> None: self.buildconfmap = {} for t in self.build_targets: bconfs = {self.buildtype: self.gen_id()} self.buildconfmap[t] = bconfs for t in self.custom_targets: bconfs = {self.buildtype: self.gen_id()} self.buildconfmap[t] = bconfs def generate_project_configurations_map(self) -> None: self.project_configurations = {self.buildtype: self.gen_id()} def generate_buildall_configurations_map(self) -> None: self.buildall_configurations = {self.buildtype: self.gen_id()} def generate_test_configurations_map(self) -> None: self.test_configurations = {self.buildtype: self.gen_id()} def generate_build_configurationlist_map(self) -> None: self.buildconflistmap = {} for t in self.build_targets: self.buildconflistmap[t] = self.gen_id() for t in self.custom_targets: self.buildconflistmap[t] = self.gen_id() def generate_native_target_map(self) -> None: self.native_targets = {} for t in self.build_targets: self.native_targets[t] = self.gen_id() def generate_custom_target_map(self) -> None: self.shell_targets = {} self.custom_target_output_buildfile = {} self.custom_target_output_fileref = {} for tname, t in self.custom_targets.items(): self.shell_targets[tname] = self.gen_id() if not isinstance(t, build.CustomTarget): continue (srcs, ofilenames, cmd) = self.eval_custom_target_command(t) for o in ofilenames: self.custom_target_output_buildfile[o] = self.gen_id() self.custom_target_output_fileref[o] = self.gen_id() def generate_generator_target_map(self) -> None: # Generator objects do not have natural unique ids # so use a counter. self.generator_fileref_ids = {} self.generator_buildfile_ids = {} for tname, t in self.build_targets.items(): generator_id = 0 for genlist in t.generated: if not isinstance(genlist, build.GeneratedList): continue self.gen_single_target_map(genlist, tname, t, generator_id) generator_id += 1 # FIXME add outputs. for tname, t in self.custom_targets.items(): generator_id = 0 for genlist in t.sources: if not isinstance(genlist, build.GeneratedList): continue self.gen_single_target_map(genlist, tname, t, generator_id) generator_id += 1 def gen_single_target_map(self, genlist, tname, t, generator_id): k = (tname, generator_id) assert k not in self.shell_targets self.shell_targets[k] = self.gen_id() ofile_abs = [] for i in genlist.get_inputs(): for o_base in genlist.get_outputs_for(i): o = os.path.join(self.get_target_private_dir(t), o_base) ofile_abs.append(os.path.join(self.environment.get_build_dir(), o)) assert k not in self.generator_outputs self.generator_outputs[k] = ofile_abs buildfile_ids = [] fileref_ids = [] for i in range(len(ofile_abs)): buildfile_ids.append(self.gen_id()) fileref_ids.append(self.gen_id()) self.generator_buildfile_ids[k] = buildfile_ids self.generator_fileref_ids[k] = fileref_ids def generate_native_frameworks_map(self) -> None: self.native_frameworks = {} self.native_frameworks_fileref = {} for t in self.build_targets.values(): for dep in t.get_external_deps(): if isinstance(dep, dependencies.AppleFrameworks): for f in dep.frameworks: self.native_frameworks[f] = self.gen_id() self.native_frameworks_fileref[f] = self.gen_id() def generate_target_dependency_map(self) -> None: self.target_dependency_map = {} for tname, t in self.build_targets.items(): for target in t.link_targets: if isinstance(target, build.CustomTargetIndex): k = (tname, target.target.get_basename()) if k in self.target_dependency_map: continue else: k = (tname, target.get_basename()) assert k not in self.target_dependency_map self.target_dependency_map[k] = self.gen_id() for tname, t in self.custom_targets.items(): k = tname assert k not in self.target_dependency_map self.target_dependency_map[k] = self.gen_id() def generate_pbxdep_map(self) -> None: self.pbx_dep_map = {} self.pbx_custom_dep_map = {} for t in self.build_targets: self.pbx_dep_map[t] = self.gen_id() for t in self.custom_targets: self.pbx_custom_dep_map[t] = self.gen_id() def generate_containerproxy_map(self) -> None: self.containerproxy_map = {} for t in self.build_targets: self.containerproxy_map[t] = self.gen_id() def generate_target_file_maps(self) -> None: self.generate_target_file_maps_impl(self.build_targets) self.generate_target_file_maps_impl(self.custom_targets) def generate_target_file_maps_impl(self, targets): for tname, t in targets.items(): for s in t.sources: if isinstance(s, mesonlib.File): s = os.path.join(s.subdir, s.fname) if not isinstance(s, str): continue k = (tname, s) assert k not in self.buildfile_ids self.buildfile_ids[k] = self.gen_id() assert k not in self.fileref_ids self.fileref_ids[k] = self.gen_id() if not hasattr(t, 'objects'): continue for o in t.objects: if isinstance(o, build.ExtractedObjects): # Extracted objects do not live in "the Xcode world". continue if isinstance(o, mesonlib.File): o = os.path.join(o.subdir, o.fname) if isinstance(o, str): o = os.path.join(t.subdir, o) k = (tname, o) assert k not in self.buildfile_ids self.buildfile_ids[k] = self.gen_id() assert k not in self.fileref_ids self.fileref_ids[k] = self.gen_id() else: raise RuntimeError('Unknown input type ' + str(o)) def generate_build_file_maps(self) -> None: for buildfile in self.interpreter.get_build_def_files(): assert isinstance(buildfile, str) self.buildfile_ids[buildfile] = self.gen_id() self.fileref_ids[buildfile] = self.gen_id() def generate_source_phase_map(self) -> None: self.source_phase = {} for t in self.build_targets: self.source_phase[t] = self.gen_id() def generate_pbx_aggregate_target(self, objects_dict): self.custom_aggregate_targets = {} self.build_all_tdep_id = self.gen_id() # FIXME: filter out targets that are not built by default. target_dependencies = [self.pbx_dep_map[t] for t in self.build_targets] custom_target_dependencies = [self.pbx_custom_dep_map[t] for t in self.custom_targets] aggregated_targets = [] aggregated_targets.append((self.all_id, 'ALL_BUILD', self.all_buildconf_id, [], [self.regen_dependency_id] + target_dependencies + custom_target_dependencies)) aggregated_targets.append((self.test_id, 'RUN_TESTS', self.test_buildconf_id, [self.test_command_id], [self.regen_dependency_id, self.build_all_tdep_id])) aggregated_targets.append((self.regen_id, 'REGENERATE', self.regen_buildconf_id, [self.regen_command_id], [])) for tname, t in self.build.get_custom_targets().items(): ct_id = self.gen_id() self.custom_aggregate_targets[tname] = ct_id build_phases = [] dependencies = [self.regen_dependency_id] generator_id = 0 for s in t.sources: if not isinstance(s, build.GeneratedList): continue build_phases.append(self.shell_targets[(tname, generator_id)]) for d in s.depends: dependencies.append(self.pbx_custom_dep_map[d.get_id()]) generator_id += 1 build_phases.append(self.shell_targets[tname]) aggregated_targets.append((ct_id, tname, self.buildconflistmap[tname], build_phases, dependencies)) # Sort objects by ID before writing sorted_aggregated_targets = sorted(aggregated_targets, key=operator.itemgetter(0)) for t in sorted_aggregated_targets: agt_dict = PbxDict() name = t[1] buildconf_id = t[2] build_phases = t[3] dependencies = t[4] agt_dict.add_item('isa', 'PBXAggregateTarget') agt_dict.add_item('buildConfigurationList', buildconf_id, f'Build configuration list for PBXAggregateTarget "{name}"') bp_arr = PbxArray() agt_dict.add_item('buildPhases', bp_arr) for bp in build_phases: bp_arr.add_item(bp, 'ShellScript') dep_arr = PbxArray() agt_dict.add_item('dependencies', dep_arr) for td in dependencies: dep_arr.add_item(td, 'PBXTargetDependency') agt_dict.add_item('name', f'"{name}"') agt_dict.add_item('productName', f'"{name}"') objects_dict.add_item(t[0], agt_dict, name) def generate_pbx_build_file(self, objects_dict): for tname, t in self.build_targets.items(): for dep in t.get_external_deps(): if isinstance(dep, dependencies.AppleFrameworks): for f in dep.frameworks: fw_dict = PbxDict() fwkey = self.native_frameworks[f] if fwkey not in objects_dict.keys: objects_dict.add_item(fwkey, fw_dict, f'{f}.framework in Frameworks') fw_dict.add_item('isa', 'PBXBuildFile') fw_dict.add_item('fileRef', self.native_frameworks_fileref[f], f) for s in t.sources: in_build_dir = False if isinstance(s, mesonlib.File): if s.is_built: in_build_dir = True s = os.path.join(s.subdir, s.fname) if not isinstance(s, str): continue sdict = PbxDict() k = (tname, s) idval = self.buildfile_ids[k] fileref = self.fileref_ids[k] if in_build_dir: fullpath = os.path.join(self.environment.get_build_dir(), s) else: fullpath = os.path.join(self.environment.get_source_dir(), s) sdict.add_item('isa', 'PBXBuildFile') sdict.add_item('fileRef', fileref, fullpath) objects_dict.add_item(idval, sdict) for o in t.objects: if isinstance(o, build.ExtractedObjects): # Object files are not source files as such. We add them # by hand in linker flags. It is also not particularly # clear how to define build files in Xcode's file format. continue if isinstance(o, mesonlib.File): o = os.path.join(o.subdir, o.fname) elif isinstance(o, str): o = os.path.join(t.subdir, o) idval = self.buildfile_ids[(tname, o)] k = (tname, o) fileref = self.fileref_ids[k] assert o not in self.filemap self.filemap[o] = idval fullpath = os.path.join(self.environment.get_source_dir(), o) fullpath2 = fullpath o_dict = PbxDict() objects_dict.add_item(idval, o_dict, fullpath) o_dict.add_item('isa', 'PBXBuildFile') o_dict.add_item('fileRef', fileref, fullpath2) generator_id = 0 for g in t.generated: if not isinstance(g, build.GeneratedList): continue self.create_generator_shellphase(objects_dict, tname, generator_id) generator_id += 1 # Custom targets are shell build phases in Xcode terminology. for tname, t in self.custom_targets.items(): if not isinstance(t, build.CustomTarget): continue (srcs, ofilenames, cmd) = self.eval_custom_target_command(t) for o in ofilenames: custom_dict = PbxDict() objects_dict.add_item(self.custom_target_output_buildfile[o], custom_dict, f'/* {o} */') custom_dict.add_item('isa', 'PBXBuildFile') custom_dict.add_item('fileRef', self.custom_target_output_fileref[o]) generator_id = 0 for g in t.sources: if not isinstance(g, build.GeneratedList): continue self.create_generator_shellphase(objects_dict, tname, generator_id) generator_id += 1 def create_generator_shellphase(self, objects_dict, tname, generator_id): file_ids = self.generator_buildfile_ids[(tname, generator_id)] ref_ids = self.generator_fileref_ids[(tname, generator_id)] assert len(ref_ids) == len(file_ids) for file_o, ref_id in zip(file_ids, ref_ids): odict = PbxDict() objects_dict.add_item(file_o, odict) odict.add_item('isa', 'PBXBuildFile') odict.add_item('fileRef', ref_id) def generate_pbx_build_style(self, objects_dict): # FIXME: Xcode 9 and later does not uses PBXBuildStyle and it gets removed. Maybe we can remove this part. for name, idval in self.buildstylemap.items(): styledict = PbxDict() objects_dict.add_item(idval, styledict, name) styledict.add_item('isa', 'PBXBuildStyle') settings_dict = PbxDict() styledict.add_item('buildSettings', settings_dict) settings_dict.add_item('COPY_PHASE_STRIP', 'NO') styledict.add_item('name', f'"{name}"') def generate_pbx_container_item_proxy(self, objects_dict): for t in self.build_targets: proxy_dict = PbxDict() objects_dict.add_item(self.containerproxy_map[t], proxy_dict, 'PBXContainerItemProxy') proxy_dict.add_item('isa', 'PBXContainerItemProxy') proxy_dict.add_item('containerPortal', self.project_uid, 'Project object') proxy_dict.add_item('proxyType', '1') proxy_dict.add_item('remoteGlobalIDString', self.native_targets[t]) proxy_dict.add_item('remoteInfo', '"' + t + '"') def generate_pbx_file_reference(self, objects_dict): for tname, t in self.build_targets.items(): for dep in t.get_external_deps(): if isinstance(dep, dependencies.AppleFrameworks): for f in dep.frameworks: fw_dict = PbxDict() framework_fileref = self.native_frameworks_fileref[f] if objects_dict.has_item(framework_fileref): continue objects_dict.add_item(framework_fileref, fw_dict, f) fw_dict.add_item('isa', 'PBXFileReference') fw_dict.add_item('lastKnownFileType', 'wrapper.framework') fw_dict.add_item('name', f'{f}.framework') fw_dict.add_item('path', f'System/Library/Frameworks/{f}.framework') fw_dict.add_item('sourceTree', 'SDKROOT') for s in t.sources: in_build_dir = False if isinstance(s, mesonlib.File): if s.is_built: in_build_dir = True s = os.path.join(s.subdir, s.fname) if not isinstance(s, str): continue idval = self.fileref_ids[(tname, s)] fullpath = os.path.join(self.environment.get_source_dir(), s) src_dict = PbxDict() xcodetype = self.get_xcodetype(s) name = os.path.basename(s) path = s objects_dict.add_item(idval, src_dict, fullpath) src_dict.add_item('isa', 'PBXFileReference') src_dict.add_item('explicitFileType', '"' + xcodetype + '"') src_dict.add_item('fileEncoding', '4') if in_build_dir: src_dict.add_item('name', '"' + name + '"') # This makes no sense. This should say path instead of name # but then the path gets added twice. src_dict.add_item('path', '"' + name + '"') src_dict.add_item('sourceTree', 'BUILD_ROOT') else: src_dict.add_item('name', '"' + name + '"') src_dict.add_item('path', '"' + path + '"') src_dict.add_item('sourceTree', 'SOURCE_ROOT') generator_id = 0 for g in t.generated: if not isinstance(g, build.GeneratedList): continue outputs = self.generator_outputs[(tname, generator_id)] ref_ids = self.generator_fileref_ids[tname, generator_id] assert len(ref_ids) == len(outputs) for o, ref_id in zip(outputs, ref_ids): odict = PbxDict() name = os.path.basename(o) objects_dict.add_item(ref_id, odict, o) xcodetype = self.get_xcodetype(o) rel_name = mesonlib.relpath(o, self.environment.get_source_dir()) odict.add_item('isa', 'PBXFileReference') odict.add_item('explicitFileType', '"' + xcodetype + '"') odict.add_item('fileEncoding', '4') odict.add_item('name', f'"{name}"') odict.add_item('path', f'"{rel_name}"') odict.add_item('sourceTree', 'SOURCE_ROOT') generator_id += 1 for o in t.objects: if isinstance(o, build.ExtractedObjects): # Same as with pbxbuildfile. continue if isinstance(o, mesonlib.File): fullpath = o.absolute_path(self.environment.get_source_dir(), self.environment.get_build_dir()) o = os.path.join(o.subdir, o.fname) else: o = os.path.join(t.subdir, o) fullpath = os.path.join(self.environment.get_source_dir(), o) idval = self.fileref_ids[(tname, o)] rel_name = mesonlib.relpath(fullpath, self.environment.get_source_dir()) o_dict = PbxDict() name = os.path.basename(o) objects_dict.add_item(idval, o_dict, fullpath) o_dict.add_item('isa', 'PBXFileReference') o_dict.add_item('explicitFileType', '"' + self.get_xcodetype(o) + '"') o_dict.add_item('fileEncoding', '4') o_dict.add_item('name', f'"{name}"') o_dict.add_item('path', f'"{rel_name}"') o_dict.add_item('sourceTree', 'SOURCE_ROOT') for tname, idval in self.target_filemap.items(): target_dict = PbxDict() objects_dict.add_item(idval, target_dict, tname) t = self.build_targets[tname] fname = t.get_filename() reftype = 0 if isinstance(t, build.Executable): typestr = 'compiled.mach-o.executable' path = fname elif isinstance(t, build.SharedLibrary): typestr = self.get_xcodetype('dummy.dylib') path = fname else: typestr = self.get_xcodetype(fname) path = '"%s"' % t.get_filename() target_dict.add_item('isa', 'PBXFileReference') target_dict.add_item('explicitFileType', '"' + typestr + '"') if ' ' in path and path[0] != '"': target_dict.add_item('path', f'"{path}"') else: target_dict.add_item('path', path) target_dict.add_item('refType', reftype) target_dict.add_item('sourceTree', 'BUILT_PRODUCTS_DIR') for tname, t in self.custom_targets.items(): if not isinstance(t, build.CustomTarget): continue (srcs, ofilenames, cmd) = self.eval_custom_target_command(t) for s in t.sources: if isinstance(s, mesonlib.File): s = os.path.join(s.subdir, s.fname) elif isinstance(s, str): s = os.path.join(t.subdir, s) else: continue custom_dict = PbxDict() typestr = self.get_xcodetype(s) custom_dict.add_item('isa', 'PBXFileReference') custom_dict.add_item('explicitFileType', '"' + typestr + '"') custom_dict.add_item('name', f'"{s}"') custom_dict.add_item('path', f'"{s}"') custom_dict.add_item('refType', 0) custom_dict.add_item('sourceTree', 'SOURCE_ROOT') objects_dict.add_item(self.fileref_ids[(tname, s)], custom_dict) for o in ofilenames: custom_dict = PbxDict() typestr = self.get_xcodetype(o) custom_dict.add_item('isa', 'PBXFileReference') custom_dict.add_item('explicitFileType', '"' + typestr + '"') custom_dict.add_item('name', o) custom_dict.add_item('path', os.path.join(self.src_to_build, o)) custom_dict.add_item('refType', 0) custom_dict.add_item('sourceTree', 'SOURCE_ROOT') objects_dict.add_item(self.custom_target_output_fileref[o], custom_dict) for buildfile in self.interpreter.get_build_def_files(): basename = os.path.split(buildfile)[1] buildfile_dict = PbxDict() typestr = self.get_xcodetype(buildfile) buildfile_dict.add_item('isa', 'PBXFileReference') buildfile_dict.add_item('explicitFileType', '"' + typestr + '"') buildfile_dict.add_item('name', f'"{basename}"') buildfile_dict.add_item('path', f'"{buildfile}"') buildfile_dict.add_item('refType', 0) buildfile_dict.add_item('sourceTree', 'SOURCE_ROOT') objects_dict.add_item(self.fileref_ids[buildfile], buildfile_dict) def generate_pbx_frameworks_buildphase(self, objects_dict): for t in self.build_targets.values(): bt_dict = PbxDict() objects_dict.add_item(t.buildphasemap['Frameworks'], bt_dict, 'Frameworks') bt_dict.add_item('isa', 'PBXFrameworksBuildPhase') bt_dict.add_item('buildActionMask', 2147483647) file_list = PbxArray() bt_dict.add_item('files', file_list) for dep in t.get_external_deps(): if isinstance(dep, dependencies.AppleFrameworks): for f in dep.frameworks: file_list.add_item(self.native_frameworks[f], f'{f}.framework in Frameworks') bt_dict.add_item('runOnlyForDeploymentPostprocessing', 0) def generate_pbx_group(self, objects_dict): groupmap = {} target_src_map = {} for t in self.build_targets: groupmap[t] = self.gen_id() target_src_map[t] = self.gen_id() for t in self.custom_targets: groupmap[t] = self.gen_id() target_src_map[t] = self.gen_id() projecttree_id = self.gen_id() resources_id = self.gen_id() products_id = self.gen_id() frameworks_id = self.gen_id() main_dict = PbxDict() objects_dict.add_item(self.maingroup_id, main_dict) main_dict.add_item('isa', 'PBXGroup') main_children = PbxArray() main_dict.add_item('children', main_children) main_children.add_item(projecttree_id, 'Project tree') main_children.add_item(resources_id, 'Resources') main_children.add_item(products_id, 'Products') main_children.add_item(frameworks_id, 'Frameworks') main_dict.add_item('sourceTree', '""') self.add_projecttree(objects_dict, projecttree_id) resource_dict = PbxDict() objects_dict.add_item(resources_id, resource_dict, 'Resources') resource_dict.add_item('isa', 'PBXGroup') resource_children = PbxArray() resource_dict.add_item('children', resource_children) resource_dict.add_item('name', 'Resources') resource_dict.add_item('sourceTree', '""') frameworks_dict = PbxDict() objects_dict.add_item(frameworks_id, frameworks_dict, 'Frameworks') frameworks_dict.add_item('isa', 'PBXGroup') frameworks_children = PbxArray() frameworks_dict.add_item('children', frameworks_children) # write frameworks for t in self.build_targets.values(): for dep in t.get_external_deps(): if isinstance(dep, dependencies.AppleFrameworks): for f in dep.frameworks: frameworks_children.add_item(self.native_frameworks_fileref[f], f) frameworks_dict.add_item('name', 'Frameworks') frameworks_dict.add_item('sourceTree', '""') for tname, t in self.custom_targets.items(): target_dict = PbxDict() objects_dict.add_item(groupmap[tname], target_dict, tname) target_dict.add_item('isa', 'PBXGroup') target_children = PbxArray() target_dict.add_item('children', target_children) target_children.add_item(target_src_map[tname], 'Source files') if t.subproject: target_dict.add_item('name', f'"{t.subproject} • {t.name}"') else: target_dict.add_item('name', f'"{t.name}"') target_dict.add_item('sourceTree', '""') source_files_dict = PbxDict() objects_dict.add_item(target_src_map[tname], source_files_dict, 'Source files') source_files_dict.add_item('isa', 'PBXGroup') source_file_children = PbxArray() source_files_dict.add_item('children', source_file_children) for s in t.sources: if isinstance(s, mesonlib.File): s = os.path.join(s.subdir, s.fname) elif isinstance(s, str): s = os.path.join(t.subdir, s) else: continue source_file_children.add_item(self.fileref_ids[(tname, s)], s) source_files_dict.add_item('name', '"Source files"') source_files_dict.add_item('sourceTree', '""') # And finally products product_dict = PbxDict() objects_dict.add_item(products_id, product_dict, 'Products') product_dict.add_item('isa', 'PBXGroup') product_children = PbxArray() product_dict.add_item('children', product_children) for t in self.build_targets: product_children.add_item(self.target_filemap[t], t) product_dict.add_item('name', 'Products') product_dict.add_item('sourceTree', '""') def write_group_target_entry(self, objects_dict, t): tid = t.get_id() group_id = self.gen_id() target_dict = PbxDict() objects_dict.add_item(group_id, target_dict, tid) target_dict.add_item('isa', 'PBXGroup') target_children = PbxArray() target_dict.add_item('children', target_children) target_dict.add_item('name', f'"{t} · target"') target_dict.add_item('sourceTree', '""') source_files_dict = PbxDict() for s in t.sources: if isinstance(s, mesonlib.File): s = os.path.join(s.subdir, s.fname) elif isinstance(s, str): s = os.path.join(t.subdir, s) else: continue target_children.add_item(self.fileref_ids[(tid, s)], s) for o in t.objects: if isinstance(o, build.ExtractedObjects): # Do not show built object files in the project tree. continue if isinstance(o, mesonlib.File): o = os.path.join(o.subdir, o.fname) else: o = os.path.join(t.subdir, o) target_children.add_item(self.fileref_ids[(tid, o)], o) source_files_dict.add_item('name', '"Source files"') source_files_dict.add_item('sourceTree', '""') return group_id def add_projecttree(self, objects_dict, projecttree_id): root_dict = PbxDict() objects_dict.add_item(projecttree_id, root_dict, "Root of project tree") root_dict.add_item('isa', 'PBXGroup') target_children = PbxArray() root_dict.add_item('children', target_children) root_dict.add_item('name', '"Project root"') root_dict.add_item('sourceTree', '""') project_tree = self.generate_project_tree() self.write_tree(objects_dict, project_tree, target_children, '') def write_tree(self, objects_dict, tree_node, children_array, current_subdir): for subdir_name, subdir_node in tree_node.subdirs.items(): subdir_dict = PbxDict() subdir_children = PbxArray() subdir_id = self.gen_id() objects_dict.add_item(subdir_id, subdir_dict) children_array.add_item(subdir_id) subdir_dict.add_item('isa', 'PBXGroup') subdir_dict.add_item('children', subdir_children) subdir_dict.add_item('name', f'"{subdir_name}"') subdir_dict.add_item('sourceTree', '""') self.write_tree(objects_dict, subdir_node, subdir_children, os.path.join(current_subdir, subdir_name)) for target in tree_node.targets: group_id = self.write_group_target_entry(objects_dict, target) children_array.add_item(group_id) potentials = [os.path.join(current_subdir, 'meson.build'), os.path.join(current_subdir, 'meson.options'), os.path.join(current_subdir, 'meson_options.txt')] for bf in potentials: i = self.fileref_ids.get(bf, None) if i: children_array.add_item(i) def generate_project_tree(self): tree_info = FileTreeEntry() for tname, t in self.build_targets.items(): self.add_target_to_tree(tree_info, t) return tree_info def add_target_to_tree(self, tree_root, t): current_node = tree_root path_segments = t.subdir.split('/') for s in path_segments: if not s: continue if s not in current_node.subdirs: current_node.subdirs[s] = FileTreeEntry() current_node = current_node.subdirs[s] current_node.targets.append(t) def generate_pbx_native_target(self, objects_dict): for tname, idval in self.native_targets.items(): ntarget_dict = PbxDict() t = self.build_targets[tname] objects_dict.add_item(idval, ntarget_dict, tname) ntarget_dict.add_item('isa', 'PBXNativeTarget') ntarget_dict.add_item('buildConfigurationList', self.buildconflistmap[tname], f'Build configuration list for PBXNativeTarget "{tname}"') buildphases_array = PbxArray() ntarget_dict.add_item('buildPhases', buildphases_array) generator_id = 0 for g in t.generated: # Custom target are handled via inter-target dependencies. # Generators are built as a shellscriptbuildphase. if isinstance(g, build.GeneratedList): buildphases_array.add_item(self.shell_targets[(tname, generator_id)], f'Generator {generator_id}/{tname}') generator_id += 1 for bpname, bpval in t.buildphasemap.items(): buildphases_array.add_item(bpval, f'{bpname} yyy') ntarget_dict.add_item('buildRules', PbxArray()) dep_array = PbxArray() ntarget_dict.add_item('dependencies', dep_array) dep_array.add_item(self.regen_dependency_id) # These dependencies only tell Xcode that the deps must be built # before this one. They don't set up linkage or anything # like that. Those are set up in the XCBuildConfiguration. for lt in self.build_targets[tname].link_targets: # NOT DOCUMENTED, may need to make different links # to same target have different targetdependency item. if isinstance(lt, build.CustomTarget): dep_array.add_item(self.pbx_custom_dep_map[lt.get_id()], lt.name) elif isinstance(lt, build.CustomTargetIndex): dep_array.add_item(self.pbx_custom_dep_map[lt.target.get_id()], lt.target.name) else: idval = self.pbx_dep_map[lt.get_id()] dep_array.add_item(idval, 'PBXTargetDependency') for o in t.objects: if isinstance(o, build.ExtractedObjects): source_target_id = o.target.get_id() idval = self.pbx_dep_map[source_target_id] dep_array.add_item(idval, 'PBXTargetDependency') generator_id = 0 for o in t.generated: if isinstance(o, build.CustomTarget): dep_array.add_item(self.pbx_custom_dep_map[o.get_id()], o.name) elif isinstance(o, build.CustomTargetIndex): dep_array.add_item(self.pbx_custom_dep_map[o.target.get_id()], o.target.name) generator_id += 1 ntarget_dict.add_item('name', f'"{tname}"') ntarget_dict.add_item('productName', f'"{tname}"') ntarget_dict.add_item('productReference', self.target_filemap[tname], tname) if isinstance(t, build.Executable): typestr = 'com.apple.product-type.tool' elif isinstance(t, build.StaticLibrary): typestr = 'com.apple.product-type.library.static' elif isinstance(t, build.SharedLibrary): typestr = 'com.apple.product-type.library.dynamic' else: raise MesonException('Unknown target type for %s' % tname) ntarget_dict.add_item('productType', f'"{typestr}"') def generate_pbx_project(self, objects_dict): project_dict = PbxDict() objects_dict.add_item(self.project_uid, project_dict, 'Project object') project_dict.add_item('isa', 'PBXProject') attr_dict = PbxDict() project_dict.add_item('attributes', attr_dict) attr_dict.add_item('BuildIndependentTargetsInParallel', 'YES') project_dict.add_item('buildConfigurationList', self.project_conflist, f'Build configuration list for PBXProject "{self.build.project_name}"') project_dict.add_item('buildSettings', PbxDict()) style_arr = PbxArray() project_dict.add_item('buildStyles', style_arr) for name, idval in self.buildstylemap.items(): style_arr.add_item(idval, name) project_dict.add_item('compatibilityVersion', '"Xcode 3.2"') project_dict.add_item('hasScannedForEncodings', 0) project_dict.add_item('mainGroup', self.maingroup_id) project_dict.add_item('projectDirPath', '"' + self.environment.get_source_dir() + '"') project_dict.add_item('projectRoot', '""') targets_arr = PbxArray() project_dict.add_item('targets', targets_arr) targets_arr.add_item(self.all_id, 'ALL_BUILD') targets_arr.add_item(self.test_id, 'RUN_TESTS') targets_arr.add_item(self.regen_id, 'REGENERATE') for t in self.build_targets: targets_arr.add_item(self.native_targets[t], t) for t in self.custom_targets: targets_arr.add_item(self.custom_aggregate_targets[t], t) def generate_pbx_shell_build_phase(self, objects_dict): self.generate_test_shell_build_phase(objects_dict) self.generate_regen_shell_build_phase(objects_dict) self.generate_custom_target_shell_build_phases(objects_dict) self.generate_generator_target_shell_build_phases(objects_dict) def generate_test_shell_build_phase(self, objects_dict): shell_dict = PbxDict() objects_dict.add_item(self.test_command_id, shell_dict, 'ShellScript') shell_dict.add_item('isa', 'PBXShellScriptBuildPhase') shell_dict.add_item('buildActionMask', 2147483647) shell_dict.add_item('files', PbxArray()) shell_dict.add_item('inputPaths', PbxArray()) shell_dict.add_item('outputPaths', PbxArray()) shell_dict.add_item('runOnlyForDeploymentPostprocessing', 0) shell_dict.add_item('shellPath', '/bin/sh') cmd = mesonlib.get_meson_command() + ['test', '--no-rebuild', '-C', self.environment.get_build_dir()] cmdstr = ' '.join(["'%s'" % i for i in cmd]) shell_dict.add_item('shellScript', f'"{cmdstr}"') shell_dict.add_item('showEnvVarsInLog', 0) def generate_regen_shell_build_phase(self, objects_dict): shell_dict = PbxDict() objects_dict.add_item(self.regen_command_id, shell_dict, 'ShellScript') shell_dict.add_item('isa', 'PBXShellScriptBuildPhase') shell_dict.add_item('buildActionMask', 2147483647) shell_dict.add_item('files', PbxArray()) shell_dict.add_item('inputPaths', PbxArray()) shell_dict.add_item('outputPaths', PbxArray()) shell_dict.add_item('runOnlyForDeploymentPostprocessing', 0) shell_dict.add_item('shellPath', '/bin/sh') cmd = mesonlib.get_meson_command() + ['--internal', 'regencheck', os.path.join(self.environment.get_build_dir(), 'meson-private')] cmdstr = ' '.join(["'%s'" % i for i in cmd]) shell_dict.add_item('shellScript', f'"{cmdstr}"') shell_dict.add_item('showEnvVarsInLog', 0) def generate_custom_target_shell_build_phases(self, objects_dict): # Custom targets are shell build phases in Xcode terminology. for tname, t in self.custom_targets.items(): if not isinstance(t, build.CustomTarget): continue (srcs, ofilenames, cmd) = self.eval_custom_target_command(t, absolute_outputs=True) fixed_cmd, _ = self.as_meson_exe_cmdline(cmd[0], cmd[1:], capture=ofilenames[0] if t.capture else None, feed=srcs[0] if t.feed else None, env=t.env) custom_dict = PbxDict() objects_dict.add_item(self.shell_targets[tname], custom_dict, f'/* Custom target {tname} */') custom_dict.add_item('isa', 'PBXShellScriptBuildPhase') custom_dict.add_item('buildActionMask', 2147483647) custom_dict.add_item('files', PbxArray()) custom_dict.add_item('inputPaths', PbxArray()) outarray = PbxArray() custom_dict.add_item('name', '"Generate {}."'.format(ofilenames[0])) custom_dict.add_item('outputPaths', outarray) for o in ofilenames: outarray.add_item(os.path.join(self.environment.get_build_dir(), o)) custom_dict.add_item('runOnlyForDeploymentPostprocessing', 0) custom_dict.add_item('shellPath', '/bin/sh') workdir = self.environment.get_build_dir() quoted_cmd = [] for c in fixed_cmd: quoted_cmd.append(c.replace('"', chr(92) + '"')) cmdstr = ' '.join([f"\\'{x}\\'" for x in quoted_cmd]) custom_dict.add_item('shellScript', f'"cd {workdir}; {cmdstr}"') custom_dict.add_item('showEnvVarsInLog', 0) def generate_generator_target_shell_build_phases(self, objects_dict): for tname, t in self.build_targets.items(): generator_id = 0 for genlist in t.generated: if isinstance(genlist, build.GeneratedList): self.generate_single_generator_phase(tname, t, genlist, generator_id, objects_dict) generator_id += 1 for tname, t in self.custom_targets.items(): generator_id = 0 for genlist in t.sources: if isinstance(genlist, build.GeneratedList): self.generate_single_generator_phase(tname, t, genlist, generator_id, objects_dict) generator_id += 1 def generate_single_generator_phase(self, tname, t, genlist, generator_id, objects_dict): # TODO: this should be rewritten to use the meson wrapper, like the other generators do # Currently it doesn't handle a host binary that requires an exe wrapper correctly. generator = genlist.get_generator() exe = generator.get_exe() exe_arr = self.build_target_to_cmd_array(exe) workdir = self.environment.get_build_dir() gen_dict = PbxDict() objects_dict.add_item(self.shell_targets[(tname, generator_id)], gen_dict, f'"Generator {generator_id}/{tname}"') infilelist = genlist.get_inputs() outfilelist = genlist.get_outputs() gen_dict.add_item('isa', 'PBXShellScriptBuildPhase') gen_dict.add_item('buildActionMask', 2147483647) gen_dict.add_item('files', PbxArray()) gen_dict.add_item('inputPaths', PbxArray()) gen_dict.add_item('name', f'"Generator {generator_id}/{tname}"') commands = [["cd", workdir]] # Array of arrays, each one a single command, will get concatenated below. k = (tname, generator_id) ofile_abs = self.generator_outputs[k] outarray = PbxArray() gen_dict.add_item('outputPaths', outarray) for of in ofile_abs: outarray.add_item(of) for i in infilelist: # This might be needed to be added to inputPaths. It's not done yet as it is # unclear whether it is necessary, what actually happens when it is defined # and currently the build works without it. #infile_abs = i.absolute_path(self.environment.get_source_dir(), self.environment.get_build_dir()) infilename = i.rel_to_builddir(self.build_to_src) base_args = generator.get_arglist(infilename) for o_base in genlist.get_outputs_for(i): o = os.path.join(self.get_target_private_dir(t), o_base) args = [] for arg in base_args: arg = arg.replace("@INPUT@", infilename) arg = arg.replace('@OUTPUT@', o).replace('@BUILD_DIR@', self.get_target_private_dir(t)) arg = arg.replace("@CURRENT_SOURCE_DIR@", os.path.join(self.build_to_src, t.subdir)) args.append(arg) args = self.replace_outputs(args, self.get_target_private_dir(t), outfilelist) args = self.replace_extra_args(args, genlist) if generator.capture: # When capturing, stdout is the output. Forward it with the shell. full_command = ['('] + exe_arr + args + ['>', o, ')'] else: full_command = exe_arr + args commands.append(full_command) gen_dict.add_item('runOnlyForDeploymentPostprocessing', 0) gen_dict.add_item('shellPath', '/bin/sh') quoted_cmds = [] for cmnd in commands: q = [] for c in cmnd: if ' ' in c: q.append(f'\\"{c}\\"') else: q.append(c) quoted_cmds.append(' '.join(q)) cmdstr = '"' + ' && '.join(quoted_cmds) + '"' gen_dict.add_item('shellScript', cmdstr) gen_dict.add_item('showEnvVarsInLog', 0) def generate_pbx_sources_build_phase(self, objects_dict): for name in self.source_phase: phase_dict = PbxDict() t = self.build_targets[name] objects_dict.add_item(t.buildphasemap[name], phase_dict, 'Sources') phase_dict.add_item('isa', 'PBXSourcesBuildPhase') phase_dict.add_item('buildActionMask', 2147483647) file_arr = PbxArray() phase_dict.add_item('files', file_arr) for s in self.build_targets[name].sources: s = os.path.join(s.subdir, s.fname) if not self.environment.is_header(s): file_arr.add_item(self.buildfile_ids[(name, s)], os.path.join(self.environment.get_source_dir(), s)) generator_id = 0 for gt in t.generated: if isinstance(gt, build.CustomTarget): (srcs, ofilenames, cmd) = self.eval_custom_target_command(gt) for o in ofilenames: file_arr.add_item(self.custom_target_output_buildfile[o], os.path.join(self.environment.get_build_dir(), o)) elif isinstance(gt, build.CustomTargetIndex): for o in gt.get_outputs(): file_arr.add_item(self.custom_target_output_buildfile[o], os.path.join(self.environment.get_build_dir(), o)) elif isinstance(gt, build.GeneratedList): genfiles = self.generator_buildfile_ids[(name, generator_id)] generator_id += 1 for o in genfiles: file_arr.add_item(o) else: raise RuntimeError('Unknown input type: ' + str(gt)) phase_dict.add_item('runOnlyForDeploymentPostprocessing', 0) def generate_pbx_target_dependency(self, objects_dict): all_dict = PbxDict() objects_dict.add_item(self.build_all_tdep_id, all_dict, 'ALL_BUILD') all_dict.add_item('isa', 'PBXTargetDependency') all_dict.add_item('target', self.all_id) targets = [] targets.append((self.regen_dependency_id, self.regen_id, 'REGEN', None)) for t in self.build_targets: idval = self.pbx_dep_map[t] # VERIFY: is this correct? targets.append((idval, self.native_targets[t], t, self.containerproxy_map[t])) for t in self.custom_targets: idval = self.pbx_custom_dep_map[t] targets.append((idval, self.custom_aggregate_targets[t], t, None)) # self.containerproxy_map[t])) # Sort object by ID sorted_targets = sorted(targets, key=operator.itemgetter(0)) for t in sorted_targets: t_dict = PbxDict() objects_dict.add_item(t[0], t_dict, 'PBXTargetDependency') t_dict.add_item('isa', 'PBXTargetDependency') t_dict.add_item('target', t[1], t[2]) if t[3] is not None: t_dict.add_item('targetProxy', t[3], 'PBXContainerItemProxy') def generate_xc_build_configuration(self, objects_dict): # First the setup for the toplevel project. for buildtype in self.buildtypes: bt_dict = PbxDict() objects_dict.add_item(self.project_configurations[buildtype], bt_dict, buildtype) bt_dict.add_item('isa', 'XCBuildConfiguration') settings_dict = PbxDict() bt_dict.add_item('buildSettings', settings_dict) settings_dict.add_item('ARCHS', '"$(NATIVE_ARCH_ACTUAL)"') settings_dict.add_item('ONLY_ACTIVE_ARCH', 'YES') settings_dict.add_item('SWIFT_VERSION', '5.0') settings_dict.add_item('SDKROOT', '"macosx"') settings_dict.add_item('SYMROOT', '"%s/build"' % self.environment.get_build_dir()) bt_dict.add_item('name', f'"{buildtype}"') # Then the all target. for buildtype in self.buildtypes: bt_dict = PbxDict() objects_dict.add_item(self.buildall_configurations[buildtype], bt_dict, buildtype) bt_dict.add_item('isa', 'XCBuildConfiguration') settings_dict = PbxDict() bt_dict.add_item('buildSettings', settings_dict) settings_dict.add_item('SYMROOT', '"%s"' % self.environment.get_build_dir()) warn_array = PbxArray() warn_array.add_item('"$(inherited)"') settings_dict.add_item('WARNING_CFLAGS', warn_array) bt_dict.add_item('name', f'"{buildtype}"') # Then the test target. for buildtype in self.buildtypes: bt_dict = PbxDict() objects_dict.add_item(self.test_configurations[buildtype], bt_dict, buildtype) bt_dict.add_item('isa', 'XCBuildConfiguration') settings_dict = PbxDict() bt_dict.add_item('buildSettings', settings_dict) settings_dict.add_item('SYMROOT', '"%s"' % self.environment.get_build_dir()) warn_array = PbxArray() settings_dict.add_item('WARNING_CFLAGS', warn_array) warn_array.add_item('"$(inherited)"') bt_dict.add_item('name', f'"{buildtype}"') # Now finally targets. for target_name, target in self.build_targets.items(): self.generate_single_build_target(objects_dict, target_name, target) for target_name, target in self.custom_targets.items(): bt_dict = PbxDict() objects_dict.add_item(self.buildconfmap[target_name][buildtype], bt_dict, buildtype) bt_dict.add_item('isa', 'XCBuildConfiguration') settings_dict = PbxDict() bt_dict.add_item('buildSettings', settings_dict) settings_dict.add_item('ARCHS', '"$(NATIVE_ARCH_ACTUAL)"') settings_dict.add_item('ONLY_ACTIVE_ARCH', 'YES') settings_dict.add_item('SDKROOT', '"macosx"') settings_dict.add_item('SYMROOT', '"%s/build"' % self.environment.get_build_dir()) bt_dict.add_item('name', f'"{buildtype}"') def determine_internal_dep_link_args(self, target, buildtype): links_dylib = False dep_libs = [] for l in target.link_targets: if isinstance(target, build.SharedModule) and isinstance(l, build.Executable): continue if isinstance(l, build.CustomTargetIndex): rel_dir = self.get_custom_target_output_dir(l.target) libname = l.get_filename() elif isinstance(l, build.CustomTarget): rel_dir = self.get_custom_target_output_dir(l) libname = l.get_filename() else: rel_dir = self.get_target_dir(l) libname = l.get_filename() abs_path = os.path.join(self.environment.get_build_dir(), rel_dir, libname) dep_libs.append("'%s'" % abs_path) if isinstance(l, build.SharedLibrary): links_dylib = True if isinstance(l, build.StaticLibrary): (sub_libs, sub_links_dylib) = self.determine_internal_dep_link_args(l, buildtype) dep_libs += sub_libs links_dylib = links_dylib or sub_links_dylib return (dep_libs, links_dylib) def generate_single_build_target(self, objects_dict, target_name, target): for buildtype in self.buildtypes: dep_libs = [] links_dylib = False headerdirs = [] for d in target.include_dirs: for sd in d.incdirs: cd = os.path.join(d.curdir, sd) headerdirs.append(os.path.join(self.environment.get_source_dir(), cd)) headerdirs.append(os.path.join(self.environment.get_build_dir(), cd)) for extra in d.extra_build_dirs: headerdirs.append(os.path.join(self.environment.get_build_dir(), extra)) (dep_libs, links_dylib) = self.determine_internal_dep_link_args(target, buildtype) if links_dylib: dep_libs = ['-Wl,-search_paths_first', '-Wl,-headerpad_max_install_names'] + dep_libs dylib_version = None if isinstance(target, build.SharedLibrary): if isinstance(target, build.SharedModule): ldargs = [] else: ldargs = ['-dynamiclib'] ldargs += ['-Wl,-headerpad_max_install_names'] + dep_libs install_path = os.path.join(self.environment.get_build_dir(), target.subdir, buildtype) dylib_version = target.soversion else: ldargs = dep_libs install_path = '' if dylib_version is not None: product_name = target.get_basename() + '.' + dylib_version else: product_name = target.get_basename() ldargs += target.link_args # Swift is special. Again. You can't mix Swift with other languages # in the same target. Thus for Swift we only use if self.is_swift_target(target): linker, stdlib_args = target.compilers['swift'], [] else: linker, stdlib_args = self.determine_linker_and_stdlib_args(target) if not isinstance(target, build.StaticLibrary): ldargs += self.build.get_project_link_args(linker, target.subproject, target.for_machine) ldargs += self.build.get_global_link_args(linker, target.for_machine) cargs = [] for dep in target.get_external_deps(): cargs += dep.get_compile_args() ldargs += dep.get_link_args() for o in target.objects: # Add extracted objects to the link line by hand. if isinstance(o, build.ExtractedObjects): added_objs = set() for objname_rel in self.determine_ext_objs(o): objname_abs = os.path.join(self.environment.get_build_dir(), o.target.subdir, objname_rel) if objname_abs not in added_objs: added_objs.add(objname_abs) ldargs += [r'\"' + objname_abs + r'\"'] generator_id = 0 for o in target.generated: if isinstance(o, build.GeneratedList): outputs = self.generator_outputs[target_name, generator_id] generator_id += 1 for o_abs in outputs: if o_abs.endswith('.o') or o_abs.endswith('.obj'): ldargs += [r'\"' + o_abs + r'\"'] else: if isinstance(o, build.CustomTarget): (srcs, ofilenames, cmd) = self.eval_custom_target_command(o) for ofname in ofilenames: if os.path.splitext(ofname)[-1] in LINKABLE_EXTENSIONS: ldargs += [r'\"' + os.path.join(self.environment.get_build_dir(), ofname) + r'\"'] elif isinstance(o, build.CustomTargetIndex): for ofname in o.get_outputs(): if os.path.splitext(ofname)[-1] in LINKABLE_EXTENSIONS: ldargs += [r'\"' + os.path.join(self.environment.get_build_dir(), ofname) + r'\"'] else: raise RuntimeError(o) if isinstance(target, build.SharedModule): options = self.environment.coredata.options ldargs += linker.get_std_shared_module_link_args(options) elif isinstance(target, build.SharedLibrary): ldargs += linker.get_std_shared_lib_link_args() ldstr = ' '.join(ldargs) valid = self.buildconfmap[target_name][buildtype] langargs = {} for lang in self.environment.coredata.compilers[target.for_machine]: if lang not in LANGNAMEMAP: continue compiler = target.compilers.get(lang) if compiler is None: continue # Start with warning args warn_args = compiler.get_warn_args(target.get_option(OptionKey('warning_level'))) copt_proxy = target.get_options() std_args = compiler.get_option_compile_args(copt_proxy) # Add compile args added using add_project_arguments() pargs = self.build.projects_args[target.for_machine].get(target.subproject, {}).get(lang, []) # Add compile args added using add_global_arguments() # These override per-project arguments gargs = self.build.global_args[target.for_machine].get(lang, []) targs = target.get_extra_args(lang) args = warn_args + std_args + pargs + gargs + targs if lang == 'swift': # For some reason putting Swift module dirs in HEADER_SEARCH_PATHS does not work, # but adding -I/path to manual args does work. swift_dep_dirs = self.determine_swift_dep_dirs(target) for d in swift_dep_dirs: args += compiler.get_include_args(d, False) if args: lang_cargs = cargs if compiler and target.implicit_include_directories: # It is unclear what is the cwd when xcode runs. -I. does not seem to # add the root build dir to the search path. So add an absolute path instead. # This may break reproducible builds, in which case patches are welcome. lang_cargs += self.get_custom_target_dir_include_args(target, compiler, absolute_path=True) # Xcode cannot handle separate compilation flags for C and ObjectiveC. They are both # put in OTHER_CFLAGS. Same with C++ and ObjectiveC++. if lang == 'objc': lang = 'c' elif lang == 'objcpp': lang = 'cpp' langname = LANGNAMEMAP[lang] if langname in langargs: langargs[langname] += args else: langargs[langname] = args langargs[langname] += lang_cargs symroot = os.path.join(self.environment.get_build_dir(), target.subdir) bt_dict = PbxDict() objects_dict.add_item(valid, bt_dict, buildtype) bt_dict.add_item('isa', 'XCBuildConfiguration') settings_dict = PbxDict() bt_dict.add_item('buildSettings', settings_dict) settings_dict.add_item('COMBINE_HIDPI_IMAGES', 'YES') if isinstance(target, build.SharedModule): settings_dict.add_item('DYLIB_CURRENT_VERSION', '""') settings_dict.add_item('DYLIB_COMPATIBILITY_VERSION', '""') else: if dylib_version is not None: settings_dict.add_item('DYLIB_CURRENT_VERSION', f'"{dylib_version}"') if target.prefix: settings_dict.add_item('EXECUTABLE_PREFIX', target.prefix) if target.suffix: suffix = '.' + target.suffix settings_dict.add_item('EXECUTABLE_SUFFIX', suffix) settings_dict.add_item('GCC_GENERATE_DEBUGGING_SYMBOLS', BOOL2XCODEBOOL[target.get_option(OptionKey('debug'))]) settings_dict.add_item('GCC_INLINES_ARE_PRIVATE_EXTERN', 'NO') opt_flag = OPT2XCODEOPT[target.get_option(OptionKey('optimization'))] if opt_flag is not None: settings_dict.add_item('GCC_OPTIMIZATION_LEVEL', opt_flag) if target.has_pch: # Xcode uses GCC_PREFIX_HEADER which only allows one file per target/executable. Precompiling various header files and # applying a particular pch to each source file will require custom scripts (as a build phase) and build flags per each # file. Since Xcode itself already discourages precompiled headers in favor of modules we don't try much harder here. pchs = target.get_pch('c') + target.get_pch('cpp') + target.get_pch('objc') + target.get_pch('objcpp') # Make sure to use headers (other backends require implementation files like *.c *.cpp, etc; these should not be used here) pchs = [pch for pch in pchs if pch.endswith('.h') or pch.endswith('.hh') or pch.endswith('hpp')] if pchs: if len(pchs) > 1: mlog.warning(f'Unsupported Xcode configuration: More than 1 precompiled header found "{pchs!s}". Target "{target.name}" might not compile correctly.') relative_pch_path = os.path.join(target.get_subdir(), pchs[0]) # Path relative to target so it can be used with "$(PROJECT_DIR)" settings_dict.add_item('GCC_PRECOMPILE_PREFIX_HEADER', 'YES') settings_dict.add_item('GCC_PREFIX_HEADER', f'"$(PROJECT_DIR)/{relative_pch_path}"') settings_dict.add_item('GCC_PREPROCESSOR_DEFINITIONS', '""') settings_dict.add_item('GCC_SYMBOLS_PRIVATE_EXTERN', 'NO') header_arr = PbxArray() unquoted_headers = [] unquoted_headers.append(self.get_target_private_dir_abs(target)) if target.implicit_include_directories: unquoted_headers.append(os.path.join(self.environment.get_build_dir(), target.get_subdir())) unquoted_headers.append(os.path.join(self.environment.get_source_dir(), target.get_subdir())) if headerdirs: for i in headerdirs: i = os.path.normpath(i) unquoted_headers.append(i) for i in unquoted_headers: header_arr.add_item(f'"\\"{i}\\""') settings_dict.add_item('HEADER_SEARCH_PATHS', header_arr) settings_dict.add_item('INSTALL_PATH', f'"{install_path}"') settings_dict.add_item('LIBRARY_SEARCH_PATHS', '""') if isinstance(target, build.SharedModule): settings_dict.add_item('LIBRARY_STYLE', 'BUNDLE') settings_dict.add_item('MACH_O_TYPE', 'mh_bundle') elif isinstance(target, build.SharedLibrary): settings_dict.add_item('LIBRARY_STYLE', 'DYNAMIC') self.add_otherargs(settings_dict, langargs) settings_dict.add_item('OTHER_LDFLAGS', f'"{ldstr}"') settings_dict.add_item('OTHER_REZFLAGS', '""') if ' ' in product_name: settings_dict.add_item('PRODUCT_NAME', f'"{product_name}"') else: settings_dict.add_item('PRODUCT_NAME', product_name) settings_dict.add_item('SECTORDER_FLAGS', '""') settings_dict.add_item('SYMROOT', f'"{symroot}"') sysheader_arr = PbxArray() # XCode will change every -I flag that points inside these directories # to an -isystem. Thus set nothing in it since we control our own # include flags. settings_dict.add_item('SYSTEM_HEADER_SEARCH_PATHS', sysheader_arr) settings_dict.add_item('USE_HEADERMAP', 'NO') warn_array = PbxArray() settings_dict.add_item('WARNING_CFLAGS', warn_array) warn_array.add_item('"$(inherited)"') bt_dict.add_item('name', buildtype) def add_otherargs(self, settings_dict, langargs): for langname, args in langargs.items(): if args: quoted_args = [] for a in args: # This works but # a) it's ugly as sin # b) I don't know why it works or why every backslash must be escaped into eight backslashes a = a.replace(chr(92), 8*chr(92)) # chr(92) is backslash, this how we smuggle it in without Python's quoting grabbing it. a = a.replace(r'"', r'\\\"') if ' ' in a or "'" in a: a = r'\"' + a + r'\"' quoted_args.append(a) settings_dict.add_item(f'OTHER_{langname}FLAGS', '"' + ' '.join(quoted_args) + '"') def generate_xc_configurationList(self, objects_dict: PbxDict) -> None: # FIXME: sort items conf_dict = PbxDict() objects_dict.add_item(self.project_conflist, conf_dict, f'Build configuration list for PBXProject "{self.build.project_name}"') conf_dict.add_item('isa', 'XCConfigurationList') confs_arr = PbxArray() conf_dict.add_item('buildConfigurations', confs_arr) for buildtype in self.buildtypes: confs_arr.add_item(self.project_configurations[buildtype], buildtype) conf_dict.add_item('defaultConfigurationIsVisible', 0) conf_dict.add_item('defaultConfigurationName', self.buildtype) # Now the all target all_dict = PbxDict() objects_dict.add_item(self.all_buildconf_id, all_dict, 'Build configuration list for PBXAggregateTarget "ALL_BUILD"') all_dict.add_item('isa', 'XCConfigurationList') conf_arr = PbxArray() all_dict.add_item('buildConfigurations', conf_arr) for buildtype in self.buildtypes: conf_arr.add_item(self.buildall_configurations[buildtype], buildtype) all_dict.add_item('defaultConfigurationIsVisible', 0) all_dict.add_item('defaultConfigurationName', self.buildtype) # Test target test_dict = PbxDict() objects_dict.add_item(self.test_buildconf_id, test_dict, 'Build configuration list for PBXAggregateTarget "RUN_TEST"') test_dict.add_item('isa', 'XCConfigurationList') conf_arr = PbxArray() test_dict.add_item('buildConfigurations', conf_arr) for buildtype in self.buildtypes: conf_arr.add_item(self.test_configurations[buildtype], buildtype) test_dict.add_item('defaultConfigurationIsVisible', 0) test_dict.add_item('defaultConfigurationName', self.buildtype) # Regen target regen_dict = PbxDict() objects_dict.add_item(self.regen_buildconf_id, test_dict, 'Build configuration list for PBXAggregateTarget "REGENERATE"') regen_dict.add_item('isa', 'XCConfigurationList') conf_arr = PbxArray() regen_dict.add_item('buildConfigurations', conf_arr) for buildtype in self.buildtypes: conf_arr.add_item(self.test_configurations[buildtype], buildtype) regen_dict.add_item('defaultConfigurationIsVisible', 0) regen_dict.add_item('defaultConfigurationName', self.buildtype) for target_name in self.build_targets: t_dict = PbxDict() listid = self.buildconflistmap[target_name] objects_dict.add_item(listid, t_dict, f'Build configuration list for PBXNativeTarget "{target_name}"') t_dict.add_item('isa', 'XCConfigurationList') conf_arr = PbxArray() t_dict.add_item('buildConfigurations', conf_arr) idval = self.buildconfmap[target_name][self.buildtype] conf_arr.add_item(idval, self.buildtype) t_dict.add_item('defaultConfigurationIsVisible', 0) t_dict.add_item('defaultConfigurationName', self.buildtype) for target_name in self.custom_targets: t_dict = PbxDict() listid = self.buildconflistmap[target_name] objects_dict.add_item(listid, t_dict, f'Build configuration list for PBXAggregateTarget "{target_name}"') t_dict.add_item('isa', 'XCConfigurationList') conf_arr = PbxArray() t_dict.add_item('buildConfigurations', conf_arr) idval = self.buildconfmap[target_name][self.buildtype] conf_arr.add_item(idval, self.buildtype) t_dict.add_item('defaultConfigurationIsVisible', 0) t_dict.add_item('defaultConfigurationName', self.buildtype) def generate_prefix(self, pbxdict: PbxDict) -> PbxDict: pbxdict.add_item('archiveVersion', '1') pbxdict.add_item('classes', PbxDict()) pbxdict.add_item('objectVersion', '46') objects_dict = PbxDict() pbxdict.add_item('objects', objects_dict) return objects_dict def generate_suffix(self, pbxdict: PbxDict) -> None: pbxdict.add_item('rootObject', self.project_uid, 'Project object')