Skip to content

Commit

Permalink
Add application bundle support
Browse files Browse the repository at this point in the history
  • Loading branch information
2xsaiko committed Jan 11, 2025
1 parent 6b99eeb commit b15692d
Show file tree
Hide file tree
Showing 10 changed files with 322 additions and 10 deletions.
10 changes: 8 additions & 2 deletions mesonbuild/ast/introspection.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

from .. import compilers, environment, mesonlib, optinterpreter, options
from .. import coredata as cdata
from ..build import Executable, Jar, SharedLibrary, SharedModule, StaticLibrary
from ..build import Executable, Jar, SharedLibrary, SharedModule, StaticLibrary, AppBundle
from ..compilers import detect_compiler_for
from ..interpreterbase import InvalidArguments, SubProject
from ..mesonlib import MachineChoice
Expand All @@ -29,7 +29,7 @@
# TODO: it would be nice to not have to duplicate this
BUILD_TARGET_FUNCTIONS = [
'executable', 'jar', 'library', 'shared_library', 'shared_module',
'static_library', 'both_libraries'
'static_library', 'both_libraries', 'app_bundle'
]

class IntrospectionHelper:
Expand Down Expand Up @@ -83,6 +83,7 @@ def __init__(self,
'shared_module': self.func_shared_module,
'static_library': self.func_static_lib,
'both_libraries': self.func_both_lib,
'app_bundle': self.func_app_bundle,
})

def func_project(self, node: BaseNode, args: T.List[TYPE_var], kwargs: T.Dict[str, TYPE_var]) -> None:
Expand Down Expand Up @@ -342,6 +343,9 @@ def func_library(self, node: BaseNode, args: T.List[TYPE_var], kwargs: T.Dict[st
def func_jar(self, node: BaseNode, args: T.List[TYPE_var], kwargs: T.Dict[str, TYPE_var]) -> T.Optional[T.Dict[str, T.Any]]:
return self.build_target(node, args, kwargs, Jar)

def func_app_bundle(self, node: BaseNode, args: T.List[TYPE_var], kwargs: T.Dict[str, TYPE_var]) -> T.Optional[T.Dict[str, T.Any]]:
return self.build_target(node, args, kwargs, AppBundle)

def func_build_target(self, node: BaseNode, args: T.List[TYPE_var], kwargs: T.Dict[str, TYPE_var]) -> T.Optional[T.Dict[str, T.Any]]:
if 'target_type' not in kwargs:
return None
Expand All @@ -360,6 +364,8 @@ def func_build_target(self, node: BaseNode, args: T.List[TYPE_var], kwargs: T.Di
return self.build_library(node, args, kwargs)
elif target_type == 'jar':
return self.build_target(node, args, kwargs, Jar)
elif target_type == 'app_bundle':
return self.build_target(node, args, kwargs, AppBundle)
return None

def is_subproject(self) -> bool:
Expand Down
69 changes: 68 additions & 1 deletion mesonbuild/backend/ninjabackend.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import json
import os
import pickle
import plistlib
import re
import subprocess
import typing as T
Expand Down Expand Up @@ -1103,6 +1104,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.AppBundle):
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 +1381,8 @@ 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('MERGE_PLIST', self.environment.get_build_command() + ['--internal', 'merge-plist'],
['$out', '$in'], 'Merging plist $inputs to $out'))

c = self.environment.get_build_command() + \
['--internal',
Expand Down Expand Up @@ -1485,6 +1491,51 @@ 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.AppBundle, bin_elem: NinjaBuildElement):
main_binary = target.get_filename()
bundle = f'{main_binary}.app'
bundle_rel = os.path.join(self.get_target_dir(target), bundle)

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:
dict = {
'CFBundleExecutable': main_binary,
'CFBundleInfoDictionaryVersion': '6.0',
'CFBundlePackageType': 'APPL',
'NSPrincipalClass': target.default_principal_class,
}
plistlib.dump(dict, fp)

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

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

layout = build.StructuredSources()
layout.sources[os.path.join(*target.bin_root())] += [target]
layout.sources[os.path.join(*target.info_root())] += [info_plist]

if target.bundle_resources is not None:
for k, v in target.bundle_resources.sources.items():
layout.sources[os.path.join(*target.resources_root(), k)] += v

if target.bundle_contents is not None:
for k, v in target.bundle_contents.sources.items():
layout.sources[os.path.join(*target.contents_root(), k)] += v

if target.bundle_extra_binaries is not None:
for k, v in target.bundle_extra_binaries.sources.items():
layout.sources[os.path.join(*target.bin_root(), k)] += v

elem = NinjaBuildElement(self.all_outputs, bundle_rel, 'phony', [])
elem.add_orderdep(self.__generate_sources_structure(Path(bundle_rel), layout)[0])
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,7 +1914,7 @@ 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])
Expand Down Expand Up @@ -3301,6 +3352,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 @@ -3808,6 +3871,10 @@ 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])

if isinstance(t, build.AppBundle):
targetlist.append(os.path.join(self.get_target_dir(t), t.get_outputs()[0] + '.app'))

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

elem = NinjaBuildElement(self.all_outputs, targ, 'phony', targetlist)
Expand Down
138 changes: 138 additions & 0 deletions mesonbuild/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ class DFeatures(TypedDict):
known_shmod_kwargs = known_build_target_kwargs | {'vs_module_defs', 'rust_abi'}
known_stlib_kwargs = known_build_target_kwargs | {'pic', 'prelink', 'rust_abi'}
known_jar_kwargs = known_exe_kwargs | {'main_class', 'java_resources'}
known_nsapp_kwargs = known_exe_kwargs | {'bundle_layout', 'bundle_resources', 'bundle_contents', 'bundle_extra_binaries', 'bundle_exe_dir_name', 'info_plist'}

def _process_install_tag(install_tag: T.Optional[T.List[T.Optional[str]]],
num_outputs: int) -> T.List[T.Optional[str]]:
Expand Down Expand Up @@ -3003,6 +3004,143 @@ def get_classpath_args(self):
def get_default_install_dir(self) -> T.Union[T.Tuple[str, str], T.Tuple[None, None]]:
return self.environment.get_jar_dir(), '{jardir}'

class AppBundle(Executable):
known_kwargs = known_nsapp_kwargs

typename = 'bundle'

# As in _CFBundleGetPlatformExecutablesSubdirectoryName (CoreFoundation)
_exe_dir_names_by_system = {
'darwin': 'MacOS',
'windows': 'Windows',
'sunos': 'Solaris',
# _: 'HPUX',
'cygwin': 'Cygwin',
'linux': 'Linux',
'freebsd': 'FreeBSD',
'emscripten': 'WASI',
}

def __init__(
self,
name: str,
subdir: str,
subproject: SubProject,
for_machine: MachineChoice,
sources: T.List['SourceOutputs'],
structured_sources: T.Optional[StructuredSources],
objects: T.List[ObjectTypes],
environment: environment.Environment,
compilers: T.Dict[str, 'Compiler'],
kwargs,
):
self.bundle_resources: T.Optional[StructuredSources] = kwargs['bundle_resources']
self.bundle_contents: T.Optional[StructuredSources] = kwargs['bundle_contents']
self.bundle_extra_binaries: T.Optional[T.List[BuildTargetTypes]] = kwargs['bundle_extra_binaries']

super().__init__(
name,
subdir,
subproject,
for_machine,
sources,
structured_sources,
objects,
environment,
compilers,
kwargs,
)

if self.export_dynamic:
raise InvalidArguments('export_dynamic is not supported for application bundle targets.')

m = self.environment.machines[self.for_machine]

bundle_layout = kwargs['bundle_layout']

if bundle_layout is None:
if m.is_darwin() and m.subsystem != 'macos':
# iOS and derivatives use the flat bundle style.
bundle_layout = 'flat'
else:
bundle_layout = 'contents'

self.bundle_layout = bundle_layout

exe_dir_name = kwargs['bundle_exe_dir_name']

if exe_dir_name is None:
exe_dir_name = AppBundle._exe_dir_names_by_system.get(m.system, '')

self.exe_dir_name = exe_dir_name

if m.is_darwin() and m.subsystem != 'macos':
self.default_principal_class = 'UIApplication'
else:
self.default_principal_class = 'NSApplication'

info_plist = kwargs['info_plist']

if isinstance(info_plist, str):
if os.path.isabs(info_plist):
info_plist = File.from_absolute_file(info_plist)
else:
info_plist = File.from_source_file(self.environment.source_dir, self.subdir, info_plist)
elif isinstance(info_plist, File):
# When passing a generated file.
pass
elif isinstance(info_plist, (CustomTarget, CustomTargetIndex)):
# When passing output of a Custom Target
info_plist = File.from_built_file(info_plist.get_subdir(), info_plist.get_filename())
else:
raise InvalidArguments(
'info_plist must be either a string, '
'a file object, a Custom Target, or a Custom Target Index')

self.info_plist: T.Optional[File] = info_plist

# Names for these slightly named after their CoreFoundation names.
# 'oldstyle' is bundles with the executable in the top directory and a
# Resources folder, as used in Framework bundles and GNUstep,
# 'contents' is the standard macOS # application bundle layout with
# everything under a Contents directory, 'flat' is the iOS-style bundle
# with every file in the root directory of the bundle.
if self.bundle_layout not in ['oldstyle', 'contents', 'flat']:
raise InvalidArguments('bundle_layout must be one of \'oldstyle\', \'contents\', or \'flat\'.')

def type_suffix(self):
return "@app"

def contents_root(self) -> str:
return {
'oldstyle': [],
'contents': ['Contents'],
'flat': [],
}[self.bundle_layout]

def bin_root(self) -> T.List[str]:
return {
'oldstyle': [],
'contents': [x for x in ['Contents', self.exe_dir_name] if x],
'flat': [],
}[self.bundle_layout]

def resources_root(self) -> T.List[str]:
return {
'oldstyle': ['Resources'],
'contents': ['Contents', 'Resources'],
'flat': [],
}[self.bundle_layout]

def info_root(self) -> T.List[str]:
return {
# The only case where Info.plist is in the Resources directory.
'oldstyle': ['Resources'],
'contents': ['Contents'],
'flat': [],
}[self.bundle_layout]


@dataclass(eq=False)
class CustomTargetIndex(CustomTargetBase, HoldableObject):

Expand Down
2 changes: 1 addition & 1 deletion mesonbuild/envconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,7 @@ def is_darwin(self) -> bool:
"""
Machine is Darwin (iOS/tvOS/OS X)?
"""
return self.system in {'darwin', 'ios', 'tvos'}
return self.system == 'darwin'

def is_android(self) -> bool:
"""
Expand Down
Loading

0 comments on commit b15692d

Please sign in to comment.