Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for building Apple bundles #14121

Draft
wants to merge 14 commits into
base: master
Choose a base branch
from
2 changes: 1 addition & 1 deletion docs/markdown/Commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ Builds a default or a specified target of a configured Meson project.
- `NAME`: name of the target from `meson.build` (e.g. `foo` from `executable('foo', ...)`).
- `SUFFIX`: name of the suffix of the target from `meson.build` (e.g. `exe` from `executable('foo', suffix: 'exe', ...)`).
- `PATH`: path to the target relative to the root `meson.build` file. Note: relative path for a target specified in the root `meson.build` is `./`.
- `TYPE`: type of the target. Can be one of the following: 'executable', 'static_library', 'shared_library', 'shared_module', 'custom', 'alias', 'run', 'jar'.
- `TYPE`: type of the target. Can be one of the following: 'executable', 'static_library', 'shared_library', 'shared_module', 'custom', 'alias', 'run', 'jar', 'nsapp', 'nsframework', 'nsbundle'.

`PATH`, `SUFFIX`, and `TYPE` can all be omitted if the resulting `TARGET` can be
used to uniquely identify the target in `meson.build`.
Expand Down
3 changes: 3 additions & 0 deletions docs/markdown/NSBundle-module.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# NSBundle module

TODO
1 change: 1 addition & 0 deletions docs/sitemap.txt
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ index.md
Icestorm-module.md
Java-module.md
Keyval-module.md
NSBundle-module.md
Pkgconfig-module.md
Python-3-module.md
Python-module.md
Expand Down
1 change: 1 addition & 0 deletions docs/theme/extra/templates/navbar_links.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
("Icestorm-module.html","Icestorm"), \
("Java-module.html","Java"), \
("Keyval-module.html","Keyval"), \
("NSBundle-module.html","NSBundle"), \
("Pkgconfig-module.html","Pkgconfig"), \
("Python-3-module.html","Python 3"), \
("Python-module.html","Python"), \
Expand Down
29 changes: 23 additions & 6 deletions mesonbuild/backend/backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from .. import mesonlib
from .. import mlog
from ..compilers import LANGUAGES_USING_LDFLAGS, detect, lang_suffixes
from ..compilers.mixins.clang import ClangCompiler
from ..mesonlib import (
File, MachineChoice, MesonException, OrderedSet,
ExecutableSerialisation, EnvironmentException,
Expand Down Expand Up @@ -343,22 +344,30 @@ def get_build_dir_include_args(self, target: build.BuildTarget, compiler: 'Compi
return compiler.get_include_args(curdir, False)

def get_target_filename_for_linking(self, target: T.Union[build.Target, build.CustomTargetIndex]) -> T.Optional[str]:
if isinstance(target, (build.BuildTarget, build.CustomTarget, build.CustomTargetIndex)):
target_filename = target.get_filename()

if isinstance(target, build.BundleTarget):
# TODO: what's the difference linking a framework like this vs. the bespoke framework arg on macOS's ld?
# (this also has the advantage of working on other platforms!)
target_filename = os.path.join(target_filename, target.get_bundle_info().get_executable_path())

# On some platforms (msvc for instance), the file that is used for
# dynamic linking is not the same as the dynamic library itself. This
# file is called an import library, and we want to link against that.
# On all other platforms, we link to the library directly.
if isinstance(target, build.SharedLibrary):
link_lib = target.get_import_filename() or target.get_filename()
link_lib = target.get_import_filename() or target_filename
# In AIX, if we archive .so, the blibpath must link to archived shared library otherwise to the .so file.
if mesonlib.is_aix() and target.aix_so_archive:
link_lib = re.sub('[.][a]([.]?([0-9]+))*([.]?([a-z]+))*', '.a', link_lib.replace('.so', '.a'))
return Path(self.get_target_dir(target), link_lib).as_posix()
elif isinstance(target, build.StaticLibrary):
return Path(self.get_target_dir(target), target.get_filename()).as_posix()
return Path(self.get_target_dir(target), target_filename).as_posix()
elif isinstance(target, (build.CustomTarget, build.CustomTargetIndex)):
if not target.is_linkable_target():
raise MesonException(f'Tried to link against custom target "{target.name}", which is not linkable.')
return Path(self.get_target_dir(target), target.get_filename()).as_posix()
return Path(self.get_target_dir(target), target_filename).as_posix()
elif isinstance(target, build.Executable):
if target.import_filename:
return Path(self.get_target_dir(target), target.get_import_filename()).as_posix()
Expand Down Expand Up @@ -817,6 +826,7 @@ def determine_rpath_dirs(self, target: T.Union[build.BuildTarget, build.CustomTa
# Need a copy here
result = OrderedSet(target.get_link_dep_subdirs())
else:
# TODO: Bundle handling
result = OrderedSet()
result.add('meson-out')
if isinstance(target, build.BuildTarget):
Expand Down Expand Up @@ -1077,11 +1087,18 @@ def generate_basic_compiler_args(self, target: build.BuildTarget, compiler: 'Com
commands += dep.get_exe_args(compiler)
# For 'automagic' deps: Boost and GTest. Also dependency('threads').
# pkg-config puts the thread flags itself via `Cflags:`
# Fortran requires extra include directives.
if compiler.language == 'fortran':
for lt in chain(target.link_targets, target.link_whole_targets):

for lt in chain(target.link_targets, target.link_whole_targets):
# Fortran requires extra include directives.
if compiler.language == 'fortran':
priv_dir = self.get_target_private_dir(lt)
commands += compiler.get_include_args(priv_dir, False)

# Add linked frameworks to include path
if isinstance(lt, build.FrameworkBundle):
# TODO: Handle this in the compiler class?
assert isinstance(compiler, ClangCompiler), 'Linking against frameworks requires clang'
commands += [f'-F{self.get_target_dir(lt)}', '-framework', lt.get_basename()]
return commands

def build_target_link_arguments(self, compiler: 'Compiler', deps: T.List[build.Target]) -> T.List[str]:
Expand Down
173 changes: 160 additions & 13 deletions mesonbuild/backend/ninjabackend.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from .. import environment, mesonlib
from .. import build
from .. import mlog
from .. import nsbundle
from .. import compilers
from ..arglist import CompilerArgs
from ..compilers import Compiler
Expand Down Expand Up @@ -954,7 +955,11 @@ def generate_target(self, target) -> None:
self.generate_generator_list_rules(target)

# Generate rules for building the remaining source files in this target
outname = self.get_target_filename(target)
if isinstance(target, build.BundleTarget):
outname = os.path.join(self.get_target_private_dir(target), target.get_executable_name())
else:
outname = self.get_target_filename(target)

obj_list = []
is_unity = target.is_unity
header_deps = []
Expand Down Expand Up @@ -1103,6 +1108,9 @@ def generate_target(self, target) -> None:
elem = NinjaBuildElement(self.all_outputs, linker.get_archive_name(outname), 'AIX_LINKER', [outname])
self.add_build(elem)

if isinstance(target, build.BundleTarget):
self.generate_bundle_target(target, elem)

def should_use_dyndeps_for_target(self, target: 'build.BuildTarget') -> bool:
if not self.ninja_has_dyndeps:
return False
Expand Down Expand Up @@ -1377,6 +1385,10 @@ def generate_rules(self) -> None:
extra='restat = 1'))
self.add_rule(NinjaRule('COPY_FILE', self.environment.get_build_command() + ['--internal', 'copy'],
['$in', '$out'], 'Copying $in to $out'))
self.add_rule(NinjaRule('SYMLINK_FILE', self.environment.get_build_command() + ['--internal', 'symlink'],
['$TARGET', '$out'], 'Creating symlink $out -> $TARGET'))
self.add_rule(NinjaRule('MERGE_PLIST', self.environment.get_build_command() + ['--internal', 'merge-plist'],
['$out', '$in'], 'Merging plist $in to $out'))

c = self.environment.get_build_command() + \
['--internal',
Expand Down Expand Up @@ -1485,6 +1497,101 @@ def generate_jar_target(self, target: build.Jar) -> None:
# Create introspection information
self.create_target_source_introspection(target, compiler, compile_args, src_list, gen_src_list)

def generate_bundle_target(self, target: build.BundleTarget, bin_elem: NinjaBuildElement) -> None:
bi: nsbundle.BundleInfo = target.get_bundle_info()

main_binary = bin_elem.outfilenames[0]
main_binary_dir, main_binary_fname = os.path.split(main_binary)
bundle_rel = self.get_target_filename(target)

presets_path = os.path.join(self.get_target_private_dir(target), 'Presets', 'Info.plist')
presets_fullpath = os.path.join(self.environment.get_build_dir(), presets_path)
os.makedirs(os.path.dirname(presets_fullpath), exist_ok=True)

with open(presets_fullpath, 'wb') as fp:
import plistlib

d = bi.get_configured_info_dict()
plistlib.dump(d, fp)

presets_file = File(True, *os.path.split(presets_path))

if bi.info_dict_file:
info_plist = self.generate_merge_plist(target, 'Info.plist', [presets_file, bi.info_dict_file])
else:
info_plist = presets_file

layout = build.StructuredSources()
layout.sources[str(bi.get_executable_folder_path())] += [File(True, main_binary_dir, main_binary_fname)]
layout.sources[str(bi.get_infoplist_path())] += [info_plist]

if bi.resources is not None:
for k, v in bi.resources.sources.items():
layout.sources[str(bi.get_unlocalized_resources_folder_path() / k)] += v

if bi.contents is not None:
for k, v in bi.contents.sources.items():
layout.sources[str(bi.get_contents_folder_path() / k)] += v

if bi.headers is not None:
for k, v in bi.headers.sources.items():
layout.sources[str(bi.get_public_headers_folder_path() / k)] += v

if bi.extra_binaries is not None:
for k, v in bi.extra_binaries.sources.items():
layout.sources[str(bi.get_executables_folder_path() / k)] += v

for file in layout.as_list():
if isinstance(file, GeneratedList):
self.generate_genlist_for_target(file, target)

elem = NinjaBuildElement(self.all_outputs, bundle_rel, 'phony', [])
elem.add_orderdep(self.__generate_sources_structure(Path(bundle_rel), layout, target=target)[0])

# Create links for versioned bundles (frameworks only)
def make_relative_link(src: PurePath, dst: PurePath):
src_rel = src.relative_to(dst.parent, walk_up=True)
dst_path = Path(bundle_rel) / dst
elem.add_orderdep(str(dst_path))
self._generate_symlink_target(str(src_rel), dst_path)

contents_dir = bi.get_contents_folder_path()

for path in bi.get_paths_to_link_contents():
can_link_whole_dir = True

for d in layout.sources.keys():
if PurePath(d).is_relative_to(path):
can_link_whole_dir = False
break

if can_link_whole_dir:
make_relative_link(contents_dir, path)
else:
names = set()

for d in layout.sources.keys():
d = PurePath(d)

if not d.parent.is_relative_to(contents_dir):
continue

d = d.relative_to(contents_dir).parts[0]
names.add(d)

for file in layout.sources[str(contents_dir)]:
if isinstance(file, File):
names.add(PurePath(file.fname).parts[0])
else:
names.update(file.get_outputs())

for n in names:
assert n != '.'

make_relative_link(contents_dir / n, path / n)

self.add_build(elem)

def generate_cs_resource_tasks(self, target) -> T.Tuple[T.List[str], T.List[str]]:
args = []
deps = []
Expand Down Expand Up @@ -1863,14 +1970,20 @@ def generate_cython_transpile(self, target: build.BuildTarget) -> \
def _generate_copy_target(self, src: FileOrString, output: Path) -> None:
"""Create a target to copy a source file from one location to another."""
if isinstance(src, File):
instr = src.absolute_path(self.environment.source_dir, self.environment.build_dir)
instr = src.rel_to_builddir(self.build_to_src)
else:
instr = src
elem = NinjaBuildElement(self.all_outputs, [str(output)], 'COPY_FILE', [instr])
elem.add_orderdep(instr)
self.add_build(elem)

def __generate_sources_structure(self, root: Path, structured_sources: build.StructuredSources) -> T.Tuple[T.List[str], T.Optional[str]]:
def _generate_symlink_target(self, src: str, output: Path) -> None:
"""Create a target to create a symbolic link."""
elem = NinjaBuildElement(self.all_outputs, [str(output)], 'SYMLINK_FILE', [])
elem.add_item('TARGET', src)
self.add_build(elem)

def __generate_sources_structure(self, root: Path, structured_sources: build.StructuredSources, target: T.Optional[build.Target] = None) -> T.Tuple[T.List[str], T.Optional[str]]:
first_file: T.Optional[str] = None
orderdeps: T.List[str] = []
for path, files in structured_sources.sources.items():
Expand All @@ -1882,10 +1995,18 @@ def __generate_sources_structure(self, root: Path, structured_sources: build.Str
if first_file is None:
first_file = str(out)
else:
if isinstance(file, GeneratedList):
if target is None:
raise MesonBugException('Encountered GeneratedList in StructuredSources with None target')

srcdir = Path(self.get_target_private_dir(target))
else:
srcdir = Path(file.subdir)

for f in file.get_outputs():
out = root / path / f
orderdeps.append(str(out))
self._generate_copy_target(str(Path(file.subdir) / f), out)
self._generate_copy_target(str(srcdir / f), out)
if first_file is None:
first_file = str(out)
return orderdeps, first_file
Expand Down Expand Up @@ -3127,6 +3248,15 @@ def generate_single_compile(self, target: build.BuildTarget, src,
element.add_dep(pch_dep)
for i in self.get_fortran_orderdeps(target, compiler):
element.add_orderdep(i)

for lt in itertools.chain(target.link_targets, target.link_whole_targets):
# Linking against frameworks also pulls in its headers, therefore wait for it to be assembled. This is not
# exactly ideal, since it will also wait for the library to be linked, but it's better than no dep at all.
# TODO: check whether linking against framework is possible when only headers have been installed into the
# bundle
if isinstance(lt, build.FrameworkBundle):
element.add_orderdep(self.get_target_filename(lt))

if dep_file:
element.add_item('DEPFILE', dep_file)
if compiler.get_language() == 'cuda':
Expand Down Expand Up @@ -3301,6 +3431,18 @@ def generate_shsym(self, target) -> None:
elem.add_item('CROSS', '--cross-host=' + self.environment.machines[target.for_machine].system)
self.add_build(elem)

def generate_merge_plist(self, target: build.BuildTarget, out_name: str, in_files: T.List[File]) -> File:
out_path = os.path.join(self.get_target_private_dir(target), out_name)
elem = NinjaBuildElement(
self.all_outputs,
out_path,
'MERGE_PLIST',
[f.rel_to_builddir(self.build_to_src) for f in in_files],
)
self.add_build(elem)

return File(True, self.get_target_private_dir(target), out_name)

def get_import_filename(self, target) -> str:
return os.path.join(self.get_target_dir(target), target.import_filename)

Expand Down Expand Up @@ -3551,6 +3693,11 @@ def generate_link(self, target: build.BuildTarget, outname, obj_list, linker: T.
self.get_target_dir(target))
else:
target_slashname_workaround_dir = self.get_target_dir(target)

if isinstance(target, build.BundleTarget):
target_slashname_workaround_dir = str(PurePath() / target_slashname_workaround_dir / target.get_filename() /
target.get_bundle_info().get_executable_folder_path())

(rpath_args, target.rpath_dirs_to_remove) = (
linker.build_rpath_args(self.environment,
self.environment.get_build_dir(),
Expand Down Expand Up @@ -3663,12 +3810,12 @@ def generate_shlib_aliases(self, target, outdir) -> None:
else:
self.implicit_meson_outs.append(aliasfile)

def generate_custom_target_clean(self, trees: T.List[str]) -> str:
def generate_dir_target_clean(self, trees: T.List[str]) -> str:
e = self.create_phony_target('clean-ctlist', 'CUSTOM_COMMAND', 'PHONY')
d = CleanTrees(self.environment.get_build_dir(), trees)
d_file = os.path.join(self.environment.get_scratch_dir(), 'cleantrees.dat')
e.add_item('COMMAND', self.environment.get_build_command() + ['--internal', 'cleantrees', d_file])
e.add_item('description', 'Cleaning custom target directories')
e.add_item('description', 'Cleaning directory target outputs')
self.add_build(e)
# Write out the data file passed to the script
with open(d_file, 'wb') as ofile:
Expand Down Expand Up @@ -3808,6 +3955,7 @@ def generate_ending(self) -> None:
if self.environment.machines[t.for_machine].is_aix():
linker, stdlib_args = t.get_clink_dynamic_linker_and_stdlibs()
t.get_outputs()[0] = linker.get_archive_name(t.get_outputs()[0])

targetlist.append(os.path.join(self.get_target_dir(t), t.get_outputs()[0]))

elem = NinjaBuildElement(self.all_outputs, targ, 'phony', targetlist)
Expand All @@ -3823,14 +3971,13 @@ def generate_ending(self) -> None:
# instead of files. This is needed because on platforms other than
# Windows, Ninja only deletes directories while cleaning if they are
# empty. https://github.com/mesonbuild/meson/issues/1220
ctlist = []
dir_output_list = []
for t in self.build.get_targets().values():
if isinstance(t, build.CustomTarget):
# Create a list of all custom target outputs
for o in t.get_outputs():
ctlist.append(os.path.join(self.get_target_dir(t), o))
if ctlist:
elem.add_dep(self.generate_custom_target_clean(ctlist))
for o in t.get_outputs():
if t.can_output_be_directory(o):
dir_output_list.append(os.path.join(self.get_target_dir(t), o))
if dir_output_list:
elem.add_dep(self.generate_dir_target_clean(dir_output_list))

if OptionKey('b_coverage') in self.environment.coredata.optstore and \
self.environment.coredata.optstore.get_value('b_coverage'):
Expand Down
Loading
Loading