From 591d969e513de3cc9a4a49eb889fa654673aef6a Mon Sep 17 00:00:00 2001 From: Marco Rebhan Date: Thu, 2 Jan 2025 21:35:49 +0100 Subject: [PATCH 01/17] Add MachineInfo.is_macos() function --- mesonbuild/envconfig.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/mesonbuild/envconfig.py b/mesonbuild/envconfig.py index 4055b21761c5..b95ecd9ec5db 100644 --- a/mesonbuild/envconfig.py +++ b/mesonbuild/envconfig.py @@ -315,6 +315,12 @@ def is_darwin(self) -> bool: """ return self.system in {'darwin', 'ios', 'tvos'} + def is_macos(self) -> bool: + """ + Machine is macOS? + """ + return self.system == 'darwin' and self.subsystem == 'macos' + def is_android(self) -> bool: """ Machine is Android? From 358343ec1a5ae0ad44fe8dd22b2cd99985797929 Mon Sep 17 00:00:00 2001 From: Marco Rebhan Date: Wed, 15 Jan 2025 21:17:07 +0100 Subject: [PATCH 02/17] Add MachineInfo.is_wasm() function --- mesonbuild/build.py | 2 +- mesonbuild/envconfig.py | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/mesonbuild/build.py b/mesonbuild/build.py index c72857d2c1ae..6aea45b456f8 100644 --- a/mesonbuild/build.py +++ b/mesonbuild/build.py @@ -2004,7 +2004,7 @@ def post_init(self) -> None: # Executable for Windows or C#/Mono if machine.is_windows() or machine.is_cygwin() or 'cs' in self.compilers: self.suffix = 'exe' - elif machine.system.startswith('wasm') or machine.system == 'emscripten': + elif machine.is_wasm(): self.suffix = 'js' elif ('c' in self.compilers and self.compilers['c'].get_id().startswith('armclang') or 'cpp' in self.compilers and self.compilers['cpp'].get_id().startswith('armclang')): diff --git a/mesonbuild/envconfig.py b/mesonbuild/envconfig.py index b95ecd9ec5db..fddcdc7c36f3 100644 --- a/mesonbuild/envconfig.py +++ b/mesonbuild/envconfig.py @@ -373,6 +373,12 @@ def is_irix(self) -> bool: """Machine is IRIX?""" return self.system.startswith('irix') + def is_wasm(self) -> bool: + """ + Machine is WASM? + """ + return self.system.startswith('wasm') or self.system == 'emscripten' + # Various prefixes and suffixes for import libraries, shared libraries, # static libraries, and executables. # Versioning is added to these names in the backends as-needed. From 9f29c5df2a9b92fccab785b30d52222a06b10297 Mon Sep 17 00:00:00 2001 From: Marco Rebhan Date: Thu, 2 Jan 2025 21:35:49 +0100 Subject: [PATCH 03/17] Add merge-plist script The Ninja bundle builder uses this to merge a user supplied Info.plist with some Meson-generated data. --- mesonbuild/mesonmain.py | 3 +- mesonbuild/scripts/merge_plist.py | 70 +++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 mesonbuild/scripts/merge_plist.py diff --git a/mesonbuild/mesonmain.py b/mesonbuild/mesonmain.py index 2c1ca97a386f..7ed8130498b6 100644 --- a/mesonbuild/mesonmain.py +++ b/mesonbuild/mesonmain.py @@ -209,7 +209,8 @@ def run_script_command(script_name: str, script_args: T.List[str]) -> int: 'delsuffix': 'delwithsuffix', 'gtkdoc': 'gtkdochelper', 'hotdoc': 'hotdochelper', - 'regencheck': 'regen_checker'} + 'regencheck': 'regen_checker', + 'merge-plist': 'merge_plist'} module_name = script_map.get(script_name, script_name) try: diff --git a/mesonbuild/scripts/merge_plist.py b/mesonbuild/scripts/merge_plist.py new file mode 100644 index 000000000000..f0e93a0277f9 --- /dev/null +++ b/mesonbuild/scripts/merge_plist.py @@ -0,0 +1,70 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: 2025 Marco Rebhan + +from __future__ import annotations + +""" +Helper script to merge two plist files. +""" + +import plistlib +import typing as T + + +def run(args: T.List[str]) -> int: + if len(args) < 1: + return 1 + + [out, *inputs] = args + + data: T.Any = {} + + for path in inputs: + try: + fp = open(path, 'rb') + except OSError as e: + print(f'merge-plist: cannot open \'{path}\': {e.strerror}') + return 1 + + with fp: + try: + new_data = plistlib.load(fp) + except plistlib.InvalidFileException as e: + print(f'merge-plist: cannot parse \'{path}\': {e}') + return 1 + except OSError as e: + print(f'merge-plist: cannot read \'{path}\': {e}') + return 1 + + data = merge(data, new_data) + + try: + ofp = open(out, 'wb') + except OSError as e: + print(f'merge-plist: cannot create \'{out}\': {e.strerror}') + return 1 + + with ofp: + try: + plistlib.dump(data, ofp) + except OSError as e: + print(f'merge-plist: cannot write \'{path}\': {e}') + return 1 + + return 0 + + +def merge(prev: T.Any, next: T.Any) -> T.Any: + if isinstance(prev, dict) and isinstance(next, dict): + out = prev.copy() + + for k, v in next.items(): + if k in out: + out[k] = merge(out[k], v) + else: + out[k] = v + return out + elif isinstance(prev, list) and isinstance(next, list): + return prev + next + else: + return next From 45dbdfcdec7ad9d4881ce24c713a232ff6f0ded8 Mon Sep 17 00:00:00 2001 From: Marco Rebhan Date: Thu, 2 Jan 2025 21:35:49 +0100 Subject: [PATCH 04/17] ninja: Add rule for merge-plist --- mesonbuild/backend/ninjabackend.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/mesonbuild/backend/ninjabackend.py b/mesonbuild/backend/ninjabackend.py index 58d2e8fae2e4..3bf7311a99d6 100644 --- a/mesonbuild/backend/ninjabackend.py +++ b/mesonbuild/backend/ninjabackend.py @@ -1378,6 +1378,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 $in to $out')) c = self.environment.get_build_command() + \ ['--internal', @@ -3310,6 +3312,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) From 1b0729424692b1a19a8317265b03f30133117496 Mon Sep 17 00:00:00 2001 From: Marco Rebhan Date: Wed, 15 Jan 2025 21:17:07 +0100 Subject: [PATCH 05/17] Add symlink script --- mesonbuild/scripts/symlink.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 mesonbuild/scripts/symlink.py diff --git a/mesonbuild/scripts/symlink.py b/mesonbuild/scripts/symlink.py new file mode 100644 index 000000000000..c5f3065a16d3 --- /dev/null +++ b/mesonbuild/scripts/symlink.py @@ -0,0 +1,31 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: 2025 Marco Rebhan + +from __future__ import annotations + +""" +Helper script to link files at build time. +""" + +import os +import typing as T + + +def run(args: T.List[str]) -> int: + if len(args) != 2: + return 1 + + src, dst = args + + try: + try: + os.remove(dst) + except FileNotFoundError: + pass + + os.symlink(src, dst) + except OSError as e: + print(f'symlink: cannot create link \'{dst}\' -> \'{src}\': {e.strerror}') + return 1 + + return 0 From d473eff27ececcfa2cbc3be046d199722a241993 Mon Sep 17 00:00:00 2001 From: Marco Rebhan Date: Wed, 15 Jan 2025 21:17:07 +0100 Subject: [PATCH 06/17] ninja: Add rule for symlink --- mesonbuild/backend/ninjabackend.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/mesonbuild/backend/ninjabackend.py b/mesonbuild/backend/ninjabackend.py index 3bf7311a99d6..e10c50dc6f83 100644 --- a/mesonbuild/backend/ninjabackend.py +++ b/mesonbuild/backend/ninjabackend.py @@ -1378,6 +1378,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('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')) @@ -1873,6 +1875,12 @@ def _generate_copy_target(self, src: FileOrString, output: Path) -> None: elem.add_orderdep(instr) self.add_build(elem) + 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) -> T.Tuple[T.List[str], T.Optional[str]]: first_file: T.Optional[str] = None orderdeps: T.List[str] = [] From 4ce4acd1eb10cebec4b4149489cfbd30d763922e Mon Sep 17 00:00:00 2001 From: Marco Rebhan Date: Thu, 2 Jan 2025 21:35:49 +0100 Subject: [PATCH 07/17] ninja: Use relative path for source file in copy target In case the source file is built by a target, the input file path needs to be the same as the target that generates the input file in order for Ninja to set up the target dependency. --- mesonbuild/backend/ninjabackend.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mesonbuild/backend/ninjabackend.py b/mesonbuild/backend/ninjabackend.py index e10c50dc6f83..1f5d7b4e7390 100644 --- a/mesonbuild/backend/ninjabackend.py +++ b/mesonbuild/backend/ninjabackend.py @@ -1868,7 +1868,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]) From e80e6d2bd325d20ff77af3820153e40ec3765a7d Mon Sep 17 00:00:00 2001 From: Marco Rebhan Date: Sun, 12 Jan 2025 20:56:35 +0100 Subject: [PATCH 08/17] Allow targets to declare outputs as directories --- mesonbuild/backend/ninjabackend.py | 17 ++++++++--------- mesonbuild/build.py | 6 ++++++ 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/mesonbuild/backend/ninjabackend.py b/mesonbuild/backend/ninjabackend.py index 1f5d7b4e7390..e43c69bd4869 100644 --- a/mesonbuild/backend/ninjabackend.py +++ b/mesonbuild/backend/ninjabackend.py @@ -3694,12 +3694,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: @@ -3855,14 +3855,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'): diff --git a/mesonbuild/build.py b/mesonbuild/build.py index 6aea45b456f8..09b7347ec1bc 100644 --- a/mesonbuild/build.py +++ b/mesonbuild/build.py @@ -710,6 +710,9 @@ def is_linkable_target(self) -> bool: def get_outputs(self) -> T.List[str]: return [] + def can_output_be_directory(self, output: str) -> bool: + return False + def should_install(self) -> bool: return False @@ -2742,6 +2745,9 @@ def get_custom_install_mode(self) -> T.Optional['FileMode']: def get_outputs(self) -> T.List[str]: return self.outputs + def can_output_be_directory(self, output: str) -> bool: + return True + def get_filename(self) -> str: return self.outputs[0] From 6865a74828e0026b241f1f575c4e90704143a62f Mon Sep 17 00:00:00 2001 From: Marco Rebhan Date: Wed, 15 Jan 2025 21:17:07 +0100 Subject: [PATCH 09/17] Add common bundle logic --- mesonbuild/build.py | 16 +++ mesonbuild/nsbundle.py | 245 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 261 insertions(+) create mode 100644 mesonbuild/nsbundle.py diff --git a/mesonbuild/build.py b/mesonbuild/build.py index 09b7347ec1bc..08e55f071e32 100644 --- a/mesonbuild/build.py +++ b/mesonbuild/build.py @@ -27,6 +27,7 @@ PerMachineDefaultable, MesonBugException, EnvironmentVariables, pickle_load, ) +from .nsbundle import BundleInfo, BundleType from .options import OptionKey from .compilers import ( @@ -3018,6 +3019,21 @@ 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 BundleTargetBase(Target): + @abc.abstractmethod + def get_bundle_info(self) -> BundleInfo: + pass + + @abc.abstractmethod + def get_bundle_type(self) -> BundleType: + pass + + @abc.abstractmethod + def get_executable_name(self) -> str: + pass + + @dataclass(eq=False) class CustomTargetIndex(CustomTargetBase, HoldableObject): diff --git a/mesonbuild/nsbundle.py b/mesonbuild/nsbundle.py new file mode 100644 index 000000000000..0e1dea11ed7b --- /dev/null +++ b/mesonbuild/nsbundle.py @@ -0,0 +1,245 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: 2025 Marco Rebhan + +from __future__ import annotations + +from dataclasses import dataclass, field +from enum import Enum +from pathlib import PurePath + +from .envconfig import MachineInfo +from .mesonlib import File, MachineChoice + +import typing as T + +if T.TYPE_CHECKING: + from .build import BuildTargetTypes, BundleTargetBase, StructuredSources + + +PlistDataType = T.Union[str, bool, T.List['PlistDataType'], T.Dict[str, 'PlistDataType']] + +# https://developer.apple.com/library/archive/documentation/DeveloperTools/Reference/XcodeBuildSettingRef/1-Build_Setting_Reference/build_setting_ref.html +# https://developer.apple.com/documentation/xcode/build-settings-reference + + +class BundleLayout(Enum): + """ + 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. + """ + + OLDSTYLE = 'oldstyle' + FLAT = 'flat' + CONTENTS = 'contents' + + +class BundleType(Enum): + """ + Subset of Xcode "product types". Affect the bundle defaults. + """ + + APPLICATION = 'application' + FRAMEWORK = 'framework' + BUNDLE = 'bundle' + + +@dataclass(eq=False) +class BundleInfo: + """ + Backend-agnostic bundle info. + """ + + # How exactly bundle paths etc. are realized depends on the target. + owner: BundleTargetBase = field(repr=False, hash=False, compare=False) + + # Layout settings + layout: T.Optional[BundleLayout] = None + version: T.Optional[str] = None + executable_folder_name: T.Optional[str] = None + + # Info.plist settings + info_dict: T.Dict[str, PlistDataType] = field(default_factory=dict) + info_dict_file: T.Optional[File] = None + + # Contents + resources: T.Optional[StructuredSources] = None + contents: T.Optional[StructuredSources] = None + headers: T.Optional[StructuredSources] = None + extra_binaries: T.List[BuildTargetTypes] = field(default_factory=list) + + def _m(self) -> MachineInfo: + return self.owner.environment.machines[self.owner.for_machine] + + def _type(self) -> BundleType: + return self.owner.get_bundle_type() + + def get_layout(self) -> BundleLayout: + if self.layout is not None: + return self.layout + + if self._type() == BundleType.FRAMEWORK: + return BundleLayout.OLDSTYLE + + m = self._m() + + if m.is_darwin() and not m.is_macos(): + return BundleLayout.FLAT + + return BundleLayout.CONTENTS + + def get_version(self) -> T.Optional[str]: + # Versioned bundles contain symlinks, don't build them on Windows + if self.owner.environment.machines[MachineChoice.HOST].is_windows(): + return None + + # Only support frameworks as versioned bundles. + if self._type() != BundleType.FRAMEWORK: + return None + + if self.version is not None: + version = self.version + else: + version = 'A' + + return version + + def get_configured_info_dict(self) -> T.Dict[str, PlistDataType]: + """ + Returns Info.plist contents known at configure time (without the user-supplied Info.plist data merged) + """ + + d = {'CFBundleInfoDictionaryVersion': '6.0'} + + t = self._type() + + if t == BundleType.APPLICATION: + m = self._m() + + if m.is_darwin() and not m.is_macos(): + principal_class = 'UIApplication' + else: + principal_class = 'NSApplication' + + d.update({ + 'CFBundleExecutable': self.get_executable_name(), + 'CFBundlePackageType': 'APPL', + 'NSPrincipalClass': principal_class, + }) + elif t == BundleType.FRAMEWORK: + d.update({ + 'CFBundleExecutable': self.get_executable_name(), + 'CFBundlePackageType': 'FMWK', + }) + else: + d.update({ + 'CFBundlePackageType': 'BNDL', + }) + + d.update(self.info_dict) + return d + + def get_paths_to_link_contents(self) -> T.List[PurePath]: + version = self.get_version() + + if version is None: + return [] + + assert version != 'Current' + + return [ + PurePath(), + PurePath() / 'Versions' / 'Current' + ] + + def get_contents_folder_path(self) -> PurePath: + p = PurePath() + + version = self.get_version() + + if version is not None: + p = p / 'Versions' / version + + if self.get_layout() == BundleLayout.CONTENTS: + p = p / 'Contents' + + return p + + def get_unlocalized_resources_folder_path(self) -> PurePath: + contents = self.get_contents_folder_path() + + if self.get_layout() == BundleLayout.FLAT: + return contents + else: + return contents / 'Resources' + + def get_executable_folder_name(self) -> str: + if self.get_layout() != BundleLayout.CONTENTS: + return '' + + if self.executable_folder_name is not None: + return self.executable_folder_name + + m = self._m() + + # As in _CFBundleGetPlatformExecutablesSubdirectoryName (CoreFoundation) + if m.is_darwin(): + return 'MacOS' + elif m.is_windows(): + return 'Windows' + elif m.is_sunos(): + return 'Solaris' + elif m.is_cygwin(): + return 'Cygwin' + elif m.is_linux(): + return 'Linux' + elif m.is_freebsd(): + return 'FreeBSD' + elif m.is_wasm(): + return 'WASI' + else: + return '' + + def get_executable_folder_path(self) -> PurePath: + return self.get_contents_folder_path() / self.get_executable_folder_name() + + def get_executable_name(self) -> str: + return self.owner.get_executable_name() + + def get_executable_path(self) -> PurePath: + return self.get_executable_folder_path() / self.get_executable_name() + + def get_infoplist_path(self) -> PurePath: + if self.get_layout() == BundleLayout.OLDSTYLE: + return self.get_unlocalized_resources_folder_path() + else: + return self.get_contents_folder_path() + + def get_public_headers_folder_path(self) -> PurePath: + return self.get_contents_folder_path() / 'Headers' + + def get_private_headers_folder_path(self) -> PurePath: + return self.get_contents_folder_path() / 'PrivateHeaders' + + def get_modules_folder_path(self) -> PurePath: + return self.get_contents_folder_path() / 'Modules' + + def get_wrapper_extension(self) -> str: + t = self._type() + + if t == BundleType.APPLICATION: + m = self._m() + + if m.is_darwin() and not m.is_macos(): + return 'ipa' + else: + return 'app' + elif t == BundleType.FRAMEWORK: + return 'framework' + else: + return 'bundle' + + def get_wrapper_name(self) -> str: + return f'{self.owner.name}.{self.get_wrapper_extension()}' From a2d42433fd9e393cf3cfd00472a6036efc68dd51 Mon Sep 17 00:00:00 2001 From: Marco Rebhan Date: Sat, 11 Jan 2025 22:56:16 +0100 Subject: [PATCH 10/17] Add empty nsbundle module --- docs/markdown/NSBundle-module.md | 3 +++ docs/sitemap.txt | 1 + docs/theme/extra/templates/navbar_links.html | 1 + mesonbuild/modules/nsbundle.py | 24 ++++++++++++++++++++ 4 files changed, 29 insertions(+) create mode 100644 docs/markdown/NSBundle-module.md create mode 100644 mesonbuild/modules/nsbundle.py diff --git a/docs/markdown/NSBundle-module.md b/docs/markdown/NSBundle-module.md new file mode 100644 index 000000000000..640ba6618f86 --- /dev/null +++ b/docs/markdown/NSBundle-module.md @@ -0,0 +1,3 @@ +# NSBundle module + +TODO diff --git a/docs/sitemap.txt b/docs/sitemap.txt index b5e0f8157cdb..24a95811eded 100644 --- a/docs/sitemap.txt +++ b/docs/sitemap.txt @@ -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 diff --git a/docs/theme/extra/templates/navbar_links.html b/docs/theme/extra/templates/navbar_links.html index 65a21a2603f6..9de2548f0f3e 100644 --- a/docs/theme/extra/templates/navbar_links.html +++ b/docs/theme/extra/templates/navbar_links.html @@ -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"), \ diff --git a/mesonbuild/modules/nsbundle.py b/mesonbuild/modules/nsbundle.py new file mode 100644 index 000000000000..dfd3ce9c28c1 --- /dev/null +++ b/mesonbuild/modules/nsbundle.py @@ -0,0 +1,24 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: 2025 Marco Rebhan + +from __future__ import annotations + +import typing as T + +from . import NewExtensionModule, ModuleInfo + +if T.TYPE_CHECKING: + from mesonbuild.interpreter import Interpreter + + +def initialize(*args: T.Any, **kwargs: T.Any) -> NSBundleModule: + return NSBundleModule(*args, **kwargs) + + +class NSBundleModule(NewExtensionModule): + INFO = ModuleInfo('nsbundle', '1.7.99') + + def __init__(self, interpreter: Interpreter): + super().__init__() + self.methods.update({ + }) From b64f42769add07bac90305a56ae519107ec8f714 Mon Sep 17 00:00:00 2001 From: Marco Rebhan Date: Sun, 12 Jan 2025 16:05:23 +0100 Subject: [PATCH 11/17] Allow modules to construct BuildTarget subclasses from function args --- mesonbuild/interpreter/interpreter.py | 19 +++++++++++++------ mesonbuild/modules/__init__.py | 12 ++++++++++++ 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/mesonbuild/interpreter/interpreter.py b/mesonbuild/interpreter/interpreter.py index 66ea24cf3468..91604b51337b 100644 --- a/mesonbuild/interpreter/interpreter.py +++ b/mesonbuild/interpreter/interpreter.py @@ -131,6 +131,8 @@ ProgramVersionFunc = T.Callable[[T.Union[ExternalProgram, build.Executable, OverrideProgram]], str] +BT = T.TypeVar('BT', bound=build.BuildTarget) + def _project_version_validator(value: T.Union[T.List, str, mesonlib.File, None]) -> T.Optional[str]: if isinstance(value, list): @@ -3277,6 +3279,9 @@ def add_target(self, name: str, tobj: build.Target) -> None: if idname not in self.coredata.target_guids: self.coredata.target_guids[idname] = str(uuid.uuid4()).upper() + if isinstance(tobj, build.BuildTarget): + self.project_args_frozen = True + @FeatureNew('both_libraries', '0.46.0') def build_both_libraries(self, node: mparser.BaseNode, args: T.Tuple[str, SourcesVarargsType], kwargs: kwtypes.Library) -> build.BothLibraries: shared_lib = self.build_target(node, args, kwargs, build.SharedLibrary) @@ -3399,6 +3404,12 @@ def build_target(self, node: mparser.BaseNode, args: T.Tuple[str, SourcesVarargs kwargs: T.Union[kwtypes.Executable, kwtypes.StaticLibrary, kwtypes.SharedLibrary, kwtypes.SharedModule, kwtypes.Jar], targetclass: T.Type[T.Union[build.Executable, build.StaticLibrary, build.SharedModule, build.SharedLibrary, build.Jar]] ) -> T.Union[build.Executable, build.StaticLibrary, build.SharedModule, build.SharedLibrary, build.Jar]: + target = self.create_build_target(node, args, kwargs, targetclass) + self.add_target(target.name, target) + return target + + def create_build_target(self, node: mparser.BaseNode, args: T.Tuple[str, SourcesVarargsType], + kwargs: T.Dict[str, TYPE_var], targetclass: T.Type[BT]) -> BT: name, sources = args for_machine = kwargs['native'] if kwargs.get('rust_crate_type') == 'proc-macro': @@ -3512,12 +3523,8 @@ def build_target(self, node: mparser.BaseNode, args: T.Tuple[str, SourcesVarargs if kwargs['implib'] is None: kwargs['implib'] = False - target = targetclass(name, self.subdir, self.subproject, for_machine, srcs, struct, objs, - self.environment, self.compilers[for_machine], kwargs) - - self.add_target(name, target) - self.project_args_frozen = True - return target + return targetclass(name, self.subdir, self.subproject, for_machine, srcs, struct, objs, + self.environment, self.compilers[for_machine], kwargs) def add_stdlib_info(self, target): for l in target.compilers.keys(): diff --git a/mesonbuild/modules/__init__.py b/mesonbuild/modules/__init__.py index f9374cc1d48c..8984cfacd267 100644 --- a/mesonbuild/modules/__init__.py +++ b/mesonbuild/modules/__init__.py @@ -17,10 +17,14 @@ if T.TYPE_CHECKING: from ..interpreter import Interpreter from ..interpreter.interpreter import ProgramVersionFunc + from ..interpreter.type_checking import SourcesVarargsType from ..interpreterbase import TYPE_var, TYPE_kwargs from ..programs import OverrideProgram from ..dependencies import Dependency +BT = T.TypeVar('BT', bound=build.BuildTarget) + + class ModuleState: """Object passed to all module methods. @@ -157,6 +161,14 @@ def process_include_dirs(self, dirs: T.Iterable[T.Union[str, IncludeDirs]]) -> T def add_language(self, lang: str, for_machine: MachineChoice) -> None: self._interpreter.add_languages([lang], True, for_machine) + # TODO: is this fine? + def create_build_target(self, targetclass: T.Type[BT], args: T.Tuple[str, SourcesVarargsType], + kwargs: T.Dict[str, TYPE_var]) -> BT: + """ + Instantiate (but don't add) a target deriving from BuildTarget. + """ + return self._interpreter.create_build_target(self.current_node, args, kwargs, targetclass) + class ModuleObject(HoldableObject): """Base class for all objects returned by modules """ From 42bd148a18d250a831f873844c222e4842db44a2 Mon Sep 17 00:00:00 2001 From: Marco Rebhan Date: Thu, 2 Jan 2025 21:35:49 +0100 Subject: [PATCH 12/17] Add bundle target types --- docs/markdown/Commands.md | 2 +- mesonbuild/build.py | 146 +++++++++++++++++-- mesonbuild/environment.py | 8 + mesonbuild/interpreter/interpreter.py | 11 +- mesonbuild/interpreter/interpreterobjects.py | 6 + mesonbuild/interpreter/kwargs.py | 19 +++ mesonbuild/interpreter/type_checking.py | 34 +++++ mesonbuild/mcompile.py | 3 + 8 files changed, 208 insertions(+), 21 deletions(-) diff --git a/docs/markdown/Commands.md b/docs/markdown/Commands.md index a99deb4941a9..2813d9d9dfbe 100644 --- a/docs/markdown/Commands.md +++ b/docs/markdown/Commands.md @@ -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`. diff --git a/mesonbuild/build.py b/mesonbuild/build.py index 08e55f071e32..8054d15f36a6 100644 --- a/mesonbuild/build.py +++ b/mesonbuild/build.py @@ -27,7 +27,7 @@ PerMachineDefaultable, MesonBugException, EnvironmentVariables, pickle_load, ) -from .nsbundle import BundleInfo, BundleType +from .nsbundle import BundleInfo, BundleLayout, BundleType from .options import OptionKey from .compilers import ( @@ -105,6 +105,13 @@ class DFeatures(TypedDict): 'win_subsystem', } +bundle_kwargs = { + 'bundle_resources', + 'bundle_contents', + 'bundle_extra_binaries', + 'info_plist', +} + known_build_target_kwargs = ( buildtarget_kwargs | lang_arg_kwargs | @@ -118,6 +125,8 @@ 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_kwargs | {'bundle_layout', 'bundle_exe_dir_name'} +known_nsframework_kwargs = known_exe_kwargs | bundle_kwargs | {'framework_headers'} def _process_install_tag(install_tag: T.Optional[T.List[T.Optional[str]]], num_outputs: int) -> T.List[T.Optional[str]]: @@ -1727,21 +1736,7 @@ def process_vs_module_defs_kw(self, kwargs: T.Dict[str, T.Any]) -> None: return path: T.Union[str, File, CustomTarget, CustomTargetIndex] = kwargs['vs_module_defs'] - if isinstance(path, str): - if os.path.isabs(path): - self.vs_module_defs = File.from_absolute_file(path) - else: - self.vs_module_defs = File.from_source_file(self.environment.source_dir, self.subdir, path) - elif isinstance(path, File): - # When passing a generated file. - self.vs_module_defs = path - elif isinstance(path, (CustomTarget, CustomTargetIndex)): - # When passing output of a Custom Target - self.vs_module_defs = File.from_built_file(path.get_subdir(), path.get_filename()) - else: - raise InvalidArguments( - 'vs_module_defs must be either a string, ' - 'a file object, a Custom Target, or a Custom Target Index') + self.vs_module_defs = _source_input_to_file(self, 'vs_module_defs', path) self.process_link_depends(path) def extract_targets_as_list(self, kwargs: T.Dict[str, T.Union[LibTypes, T.Sequence[LibTypes]]], key: T.Literal['link_with', 'link_whole']) -> T.List[LibTypes]: @@ -1766,6 +1761,25 @@ def get(self, lib_type: T.Literal['static', 'shared']) -> LibTypes: """Base case used by BothLibraries""" return self + +def _source_input_to_file(t: Target, kw: str, source: T.Union[str, File, CustomTarget, CustomTargetIndex]) -> File: + if isinstance(source, str): + if os.path.isabs(source): + return File.from_absolute_file(source) + else: + return File.from_source_file(t.environment.source_dir, t.subdir, source) + elif isinstance(source, File): + # When passing a generated file. + return source + elif isinstance(source, (CustomTarget, CustomTargetIndex)): + # When passing output of a Custom Target + return File.from_built_file(source.get_subdir(), source.get_filename()) + else: + raise InvalidArguments( + f'{kw} must be either a string, ' + 'a file object, a Custom Target, or a Custom Target Index') + + class FileInTargetPrivateDir: """Represents a file with the path '/path/to/build/target_private_dir/fname'. target_private_dir is the return value of get_target_private_dir which is e.g. 'subdir/target.p'. @@ -3034,6 +3048,106 @@ def get_executable_name(self) -> str: pass +def _fill_bundle_info_from_kwargs(bi: BundleInfo, kwargs) -> None: + bi.resources = kwargs['bundle_resources'] + bi.contents = kwargs['bundle_contents'] + bi.extra_binaries = kwargs.get('bundle_extra_binaries', []) + + if kwargs['info_plist'] is not None: + bi.info_dict_file = _source_input_to_file(bi.owner, 'info_plist', kwargs['info_plist']) + + +class AppBundle(Executable, BundleTargetBase): + known_kwargs = known_nsapp_kwargs + + typename = 'nsapp' + + 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): + super().__init__(name, subdir, subproject, for_machine, sources, structured_sources, objects, environment, + compilers, kwargs) + + self.bundle_info = BundleInfo(self) + _fill_bundle_info_from_kwargs(self.bundle_info, kwargs) + + if kwargs['bundle_layout'] is not None: + try: + self.bundle_info.layout = BundleLayout(kwargs['bundle_layout']) + except ValueError: + raise MesonException('{!r} is not a valid value for bundle_layout'.format(kwargs['bundle_layout'])) + if kwargs['bundle_exe_dir_name'] is not None: + self.bundle_info.executable_folder_name: T.Optional[str] = kwargs['bundle_exe_dir_name'] + + def type_suffix(self) -> str: + return '@nsapp' + + def post_init(self) -> None: + super().post_init() + self.outputs[0] = self.get_filename() + + def get_filename(self) -> str: + return self.bundle_info.get_wrapper_name() + + def get_bundle_info(self) -> BundleInfo: + return self.bundle_info + + def get_bundle_type(self) -> BundleType: + return BundleType.APPLICATION + + def get_executable_name(self) -> str: + return self.filename + + def can_output_be_directory(self, output: str) -> bool: + return output == self.get_filename() + + def get_default_install_dir(self) -> T.Union[T.Tuple[str, str], T.Tuple[None, None]]: + return self.environment.get_app_dir(), '{appdir}' + + +class FrameworkBundle(SharedLibrary, BundleTargetBase): + known_kwargs = known_nsframework_kwargs + + typename = 'nsframework' + + 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): + super().__init__(name, subdir, subproject, for_machine, sources, structured_sources, objects, environment, + compilers, kwargs) + + self.bundle_info = BundleInfo(self) + _fill_bundle_info_from_kwargs(self.bundle_info, kwargs) + self.bundle_info.headers = kwargs['framework_headers'] + + def type_suffix(self) -> str: + return '@nsframework' + + def post_init(self) -> None: + super().post_init() + self.outputs[0] = self.get_filename() + + def get_filename(self) -> str: + return self.bundle_info.get_wrapper_name() + + def get_bundle_info(self) -> BundleInfo: + return self.bundle_info + + def get_bundle_type(self) -> BundleType: + return BundleType.FRAMEWORK + + def get_executable_name(self) -> str: + return self.filename + + def can_output_be_directory(self, output: str) -> bool: + return output == self.get_filename() + + def get_default_install_dir(self) -> T.Union[T.Tuple[str, str], T.Tuple[None, None]]: + return self.environment.get_framework_dir(), '{frameworkdir}' + + @dataclass(eq=False) class CustomTargetIndex(CustomTargetBase, HoldableObject): diff --git a/mesonbuild/environment.py b/mesonbuild/environment.py index c09d7e312ab7..d6c843951874 100644 --- a/mesonbuild/environment.py +++ b/mesonbuild/environment.py @@ -935,6 +935,14 @@ def get_static_lib_dir(self) -> str: "Install dir for the static library" return self.get_libdir() + def get_app_dir(self) -> str: + """Install dir for application bundles""" + return f"{self.get_prefix()}/Applications" + + def get_framework_dir(self) -> str: + """Install dir for framework bundles""" + return f"{self.get_prefix()}/Library/Frameworks" + def get_prefix(self) -> str: return self.coredata.get_option(OptionKey('prefix')) diff --git a/mesonbuild/interpreter/interpreter.py b/mesonbuild/interpreter/interpreter.py index 91604b51337b..4af1a591abb5 100644 --- a/mesonbuild/interpreter/interpreter.py +++ b/mesonbuild/interpreter/interpreter.py @@ -429,6 +429,8 @@ def build_holder_map(self) -> None: build.SharedModule: OBJ.SharedModuleHolder, build.Executable: OBJ.ExecutableHolder, build.Jar: OBJ.JarHolder, + build.AppBundle: OBJ.AppBundleHolder, + build.FrameworkBundle: OBJ.FrameworkBundleHolder, build.CustomTarget: OBJ.CustomTargetHolder, build.CustomTargetIndex: OBJ.CustomTargetIndexHolder, build.Generator: OBJ.GeneratorHolder, @@ -3442,16 +3444,17 @@ def create_build_target(self, node: mparser.BaseNode, args: T.Tuple[str, Sources kwargs['dependencies'] = extract_as_list(kwargs, 'dependencies') kwargs['extra_files'] = self.source_strings_to_files(kwargs['extra_files']) self.check_sources_exist(os.path.join(self.source_root, self.subdir), sources) - if targetclass not in {build.Executable, build.SharedLibrary, build.SharedModule, build.StaticLibrary, build.Jar}: + if targetclass not in {build.Executable, build.SharedLibrary, build.SharedModule, build.StaticLibrary, + build.Jar, build.AppBundle, build.FrameworkBundle}: mlog.debug('Unknown target type:', str(targetclass)) raise RuntimeError('Unreachable code') self.__process_language_args(kwargs) - if targetclass is build.StaticLibrary: + if issubclass(targetclass, build.StaticLibrary): for lang in compilers.all_languages - {'java'}: deps, args = self.__convert_file_args(kwargs.get(f'{lang}_static_args', [])) kwargs['language_args'][lang].extend(args) kwargs['depend_files'].extend(deps) - elif targetclass is build.SharedLibrary: + elif issubclass(targetclass, build.SharedLibrary): for lang in compilers.all_languages - {'java'}: deps, args = self.__convert_file_args(kwargs.get(f'{lang}_shared_args', [])) kwargs['language_args'][lang].extend(args) @@ -3496,7 +3499,7 @@ def create_build_target(self, node: mparser.BaseNode, args: T.Tuple[str, Sources kwargs['include_directories'] = self.extract_incdirs(kwargs) - if targetclass is build.Executable: + if issubclass(targetclass, build.Executable): kwargs = T.cast('kwtypes.Executable', kwargs) if kwargs['gui_app'] is not None: if kwargs['win_subsystem'] is not None: diff --git a/mesonbuild/interpreter/interpreterobjects.py b/mesonbuild/interpreter/interpreterobjects.py index f4a2b4107ed3..a81a6adb6d62 100644 --- a/mesonbuild/interpreter/interpreterobjects.py +++ b/mesonbuild/interpreter/interpreterobjects.py @@ -1032,6 +1032,12 @@ class SharedModuleHolder(BuildTargetHolder[build.SharedModule]): class JarHolder(BuildTargetHolder[build.Jar]): pass +class AppBundleHolder(BuildTargetHolder[build.AppBundle]): + pass + +class FrameworkBundleHolder(BuildTargetHolder[build.FrameworkBundle]): + pass + class CustomTargetIndexHolder(ObjectHolder[build.CustomTargetIndex]): def __init__(self, target: build.CustomTargetIndex, interp: 'Interpreter'): super().__init__(target, interp) diff --git a/mesonbuild/interpreter/kwargs.py b/mesonbuild/interpreter/kwargs.py index 87f121e90b0f..c70f16bc6b18 100644 --- a/mesonbuild/interpreter/kwargs.py +++ b/mesonbuild/interpreter/kwargs.py @@ -453,6 +453,25 @@ class Library(_BuildTarget, _SharedLibMixin, _StaticLibMixin, _LibraryMixin): masm_shared_args: NotRequired[T.List[str]] +class BundleShared(TypedDict): + + bundle_resources: T.Optional[build.StructuredSources] + bundle_contents: T.Optional[build.StructuredSources] + bundle_extra_binaries: T.Optional[build.StructuredSources] + info_plist: T.Optional[T.Union[str, File, build.CustomTarget, build.CustomTargetIndex]] + + +class AppBundle(Executable, BundleShared): + + bundle_exe_dir_name: T.Optional[str] + bundle_layout: T.Optional[str] + + +class FrameworkBundle(SharedLibrary, BundleShared): + + pass + + class BuildTarget(Library): target_type: Literal['executable', 'shared_library', 'static_library', diff --git a/mesonbuild/interpreter/type_checking.py b/mesonbuild/interpreter/type_checking.py index ed34be950065..a0b0fe2bfd53 100644 --- a/mesonbuild/interpreter/type_checking.py +++ b/mesonbuild/interpreter/type_checking.py @@ -792,6 +792,38 @@ def _convert_darwin_versions(val: T.List[T.Union[str, int]]) -> T.Optional[T.Tup for a in _LANGUAGE_KWS], ] +_BUNDLE_KWS: T.List[KwargInfo] = [ + KwargInfo('bundle_resources', (StructuredSources, NoneType)), + KwargInfo('bundle_contents', (StructuredSources, NoneType)), + KwargInfo('bundle_extra_binaries', (StructuredSources, NoneType)), + KwargInfo('info_plist', (str, File, CustomTarget, CustomTargetIndex, NoneType)), +] + +_EXCLUSIVE_NSAPP_KWS: T.List[KwargInfo] = [ + KwargInfo('bundle_layout', (str, NoneType)), + KwargInfo('bundle_exe_dir_name', (StructuredSources, NoneType)), +] + +_EXCLUSIVE_NSFRAMEWORK_KWS: T.List[KwargInfo] = [ + KwargInfo('framework_headers', (StructuredSources, NoneType)), +] + +NSAPP_KWS: T.List[KwargInfo] = [ + *EXECUTABLE_KWS, + *_BUNDLE_KWS, + *_EXCLUSIVE_NSAPP_KWS, + INCLUDE_DIRECTORIES, + DEPENDENCIES_KW, +] + +NSFRAMEWORK_KWS: T.List[KwargInfo] = [ + *SHARED_LIB_KWS, + *_BUNDLE_KWS, + *_EXCLUSIVE_NSFRAMEWORK_KWS, + INCLUDE_DIRECTORIES, + DEPENDENCIES_KW, +] + _SHARED_STATIC_ARGS: T.List[KwargInfo[T.List[str]]] = [ *[l.evolve(name=l.name.replace('_', '_static_'), since='1.3.0') for l in _LANGUAGE_KWS], @@ -818,6 +850,8 @@ def _convert_darwin_versions(val: T.List[T.Union[str, int]]) -> T.Optional[T.Tup *_EXCLUSIVE_SHARED_MOD_KWS, *_EXCLUSIVE_STATIC_LIB_KWS, *_EXCLUSIVE_EXECUTABLE_KWS, + *_EXCLUSIVE_NSAPP_KWS, + *_EXCLUSIVE_NSFRAMEWORK_KWS, *_SHARED_STATIC_ARGS, *[a.evolve(deprecated='1.3.0', deprecated_message='The use of "jar" in "build_target()" is deprecated, and this argument is only used by jar()') for a in _EXCLUSIVE_JAR_KWS], diff --git a/mesonbuild/mcompile.py b/mesonbuild/mcompile.py index 2f5708c86521..087f720775b8 100644 --- a/mesonbuild/mcompile.py +++ b/mesonbuild/mcompile.py @@ -91,6 +91,9 @@ def _is_valid_type(type: str) -> bool: 'alias', 'run', 'jar', + 'nsapp', + 'nsframework', + 'nsbundle', } return type in allowed_types From fe585de63151156aeb76d7f9705c8cb297ca00a5 Mon Sep 17 00:00:00 2001 From: Marco Rebhan Date: Sat, 11 Jan 2025 22:56:16 +0100 Subject: [PATCH 13/17] Construct bundle targets via nsbundle module --- mesonbuild/modules/nsbundle.py | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/mesonbuild/modules/nsbundle.py b/mesonbuild/modules/nsbundle.py index dfd3ce9c28c1..0b46f2f1cf47 100644 --- a/mesonbuild/modules/nsbundle.py +++ b/mesonbuild/modules/nsbundle.py @@ -5,10 +5,15 @@ import typing as T -from . import NewExtensionModule, ModuleInfo +from . import NewExtensionModule, ModuleInfo, ModuleReturnValue +from mesonbuild.build import AppBundle, FrameworkBundle +from mesonbuild.interpreter.type_checking import NSAPP_KWS, NSFRAMEWORK_KWS, SOURCES_VARARGS +from mesonbuild.interpreterbase.decorators import FeatureNew, typed_kwargs, typed_pos_args if T.TYPE_CHECKING: - from mesonbuild.interpreter import Interpreter + from . import ModuleState + from mesonbuild.interpreter.type_checking import SourcesVarargsType + from mesonbuild.interpreter import Interpreter, kwargs as kwtypes def initialize(*args: T.Any, **kwargs: T.Any) -> NSBundleModule: @@ -21,4 +26,22 @@ class NSBundleModule(NewExtensionModule): def __init__(self, interpreter: Interpreter): super().__init__() self.methods.update({ + 'application': self.application, + 'framework': self.framework, }) + + @FeatureNew('nsbundle.application', '1.7.99') + @typed_pos_args('nsbundle.application', (str,), varargs=SOURCES_VARARGS) + @typed_kwargs('nsbundle.application', *NSAPP_KWS, allow_unknown=True) + def application(self, state: ModuleState, args: T.Tuple[str, SourcesVarargsType], kwargs: kwtypes.AppBundle + ) -> ModuleReturnValue: + tgt = state.create_build_target(AppBundle, args, kwargs) + return ModuleReturnValue(tgt, [tgt]) + + @FeatureNew('nsbundle.framework', '1.7.99') + @typed_pos_args('nsbundle.framework', (str,), varargs=SOURCES_VARARGS) + @typed_kwargs('nsbundle.framework', *NSFRAMEWORK_KWS, allow_unknown=True) + def framework(self, state: ModuleState, args: T.Tuple[str, SourcesVarargsType], kwargs: kwtypes.FrameworkBundle + ) -> ModuleReturnValue: + tgt = state.create_build_target(FrameworkBundle, args, kwargs) + return ModuleReturnValue(tgt, [tgt]) From 58e296c06c6056d0aa18f15cfdbf5735f76e94e8 Mon Sep 17 00:00:00 2001 From: Marco Rebhan Date: Thu, 2 Jan 2025 21:35:49 +0100 Subject: [PATCH 14/17] ninja: Bundle target support --- mesonbuild/backend/ninjabackend.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/mesonbuild/backend/ninjabackend.py b/mesonbuild/backend/ninjabackend.py index 0581cc387fc4..32eb186187f1 100644 --- a/mesonbuild/backend/ninjabackend.py +++ b/mesonbuild/backend/ninjabackend.py @@ -956,7 +956,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.BundleTargetBase): + 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 = [] @@ -1105,6 +1109,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.BundleTargetBase): + self.generate_bundle_target(target, elem.outfilenames[0]) + def should_use_dyndeps_for_target(self, target: 'build.BuildTarget') -> bool: if not self.ninja_has_dyndeps: return False From e82ba23b0d6e0a693ab56ecaee56ae8e15c55262 Mon Sep 17 00:00:00 2001 From: Marco Rebhan Date: Thu, 2 Jan 2025 21:35:49 +0100 Subject: [PATCH 15/17] ninja: Base bundle target support --- mesonbuild/backend/ninjabackend.py | 108 ++++++++++++++++++++++++++++- 1 file changed, 106 insertions(+), 2 deletions(-) diff --git a/mesonbuild/backend/ninjabackend.py b/mesonbuild/backend/ninjabackend.py index e43c69bd4869..0581cc387fc4 100644 --- a/mesonbuild/backend/ninjabackend.py +++ b/mesonbuild/backend/ninjabackend.py @@ -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 @@ -1490,6 +1491,100 @@ 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.BundleTargetBase, main_exe: str) -> None: + bi: nsbundle.BundleInfo = target.get_bundle_info() + + main_exe_dir, main_exe_fname = os.path.split(main_exe) + 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_exe_dir, main_exe_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 v in bi.extra_binaries: + layout.sources[str(bi.get_executable_folder_path())] += 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 = [] @@ -1881,7 +1976,7 @@ def _generate_symlink_target(self, src: str, output: Path) -> None: elem.add_item('TARGET', src) 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_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(): @@ -1893,10 +1988,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 @@ -3840,6 +3943,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) From 06952aad5e13afe122eb8da2db1633d869b73d0e Mon Sep 17 00:00:00 2001 From: Marco Rebhan Date: Thu, 23 Jan 2025 16:35:56 +0100 Subject: [PATCH 16/17] Framework target link support --- mesonbuild/backend/backends.py | 29 +++++++++++++++++++++++------ mesonbuild/backend/ninjabackend.py | 14 ++++++++++++++ mesonbuild/build.py | 5 ++++- 3 files changed, 41 insertions(+), 7 deletions(-) diff --git a/mesonbuild/backend/backends.py b/mesonbuild/backend/backends.py index 8d9796db95c6..13c4273be617 100644 --- a/mesonbuild/backend/backends.py +++ b/mesonbuild/backend/backends.py @@ -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, @@ -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.BundleTargetBase): + # 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() @@ -822,6 +831,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): @@ -1083,11 +1093,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]: diff --git a/mesonbuild/backend/ninjabackend.py b/mesonbuild/backend/ninjabackend.py index 32eb186187f1..c0c888ba9833 100644 --- a/mesonbuild/backend/ninjabackend.py +++ b/mesonbuild/backend/ninjabackend.py @@ -3250,6 +3250,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': @@ -3692,6 +3701,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.BundleTargetBase): + 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(), diff --git a/mesonbuild/build.py b/mesonbuild/build.py index 8054d15f36a6..10560be89e77 100644 --- a/mesonbuild/build.py +++ b/mesonbuild/build.py @@ -1128,7 +1128,10 @@ def get_transitive_link_deps_mapping(self, prefix: str) -> T.Mapping[str, str]: def get_link_dep_subdirs(self) -> T.AbstractSet[str]: result: OrderedSet[str] = OrderedSet() for i in self.link_targets: - if not isinstance(i, StaticLibrary): + if isinstance(i, FrameworkBundle): + result.add(str(pathlib.PurePath() / i.get_subdir() / i.get_filename() / + i.get_bundle_info().get_executable_folder_path())) + elif not isinstance(i, StaticLibrary): result.add(i.get_subdir()) result.update(i.get_link_dep_subdirs()) return result From 811694323d67d5e003f9860a5f37159d0a1ba487 Mon Sep 17 00:00:00 2001 From: Marco Rebhan Date: Sat, 1 Feb 2025 18:29:19 +0100 Subject: [PATCH 17/17] Bundle API v3 --- mesonbuild/backend/backends.py | 2 + mesonbuild/backend/ninjabackend.py | 6 ++ mesonbuild/build.py | 69 ++++++++++++++++++ mesonbuild/interpreter/interpreter.py | 3 +- mesonbuild/interpreter/interpreterobjects.py | 3 + mesonbuild/modules/nsbundle.py | 76 +++++++++++++++++++- 6 files changed, 157 insertions(+), 2 deletions(-) diff --git a/mesonbuild/backend/backends.py b/mesonbuild/backend/backends.py index 8d9796db95c6..710b420da45b 100644 --- a/mesonbuild/backend/backends.py +++ b/mesonbuild/backend/backends.py @@ -302,6 +302,8 @@ def get_target_filename(self, t: T.Union[build.Target, build.CustomTargetIndex], filename = t.get_outputs()[0] elif isinstance(t, build.CustomTargetIndex): filename = t.get_outputs()[0] + elif isinstance(t, build.BundleTarget): + filename = t.get_filename() else: assert isinstance(t, build.BuildTarget), t filename = t.get_filename() diff --git a/mesonbuild/backend/ninjabackend.py b/mesonbuild/backend/ninjabackend.py index 0581cc387fc4..46641d3baccb 100644 --- a/mesonbuild/backend/ninjabackend.py +++ b/mesonbuild/backend/ninjabackend.py @@ -895,6 +895,8 @@ def generate_target(self, target) -> None: self.generate_custom_target(target) if isinstance(target, build.RunTarget): self.generate_run_target(target) + if isinstance(target, build.BundleTarget): + return self.generate_bundle_target3(target) compiled_sources: T.List[str] = [] source2object: T.Dict[str, str] = {} name = target.get_id() @@ -1585,6 +1587,10 @@ def make_relative_link(src: PurePath, dst: PurePath): self.add_build(elem) + def generate_bundle_target3(self, target: build.BundleTarget) -> None: + main_exe = os.path.join(self.get_target_dir(target.main_exe), target.main_exe.get_filename()) + self.generate_bundle_target(target, main_exe) + def generate_cs_resource_tasks(self, target) -> T.Tuple[T.List[str], T.List[str]]: args = [] deps = [] diff --git a/mesonbuild/build.py b/mesonbuild/build.py index 08e55f071e32..df937f8b0091 100644 --- a/mesonbuild/build.py +++ b/mesonbuild/build.py @@ -3034,6 +3034,75 @@ def get_executable_name(self) -> str: pass +class BundleTarget(BundleTargetBase): + def __init__(self, name: str, subdir: str, subproject: str, environment: environment.Environment, main_exe: T.Union[Executable, SharedLibrary], bundle_type: BundleType): + super().__init__(name, subdir, subproject, True, main_exe.for_machine, environment) + + self.main_exe: T.Union[Executable, SharedLibrary] = main_exe + self.bundle_type: BundleType = bundle_type + self.install_dir: T.Optional[str] = None + + self.bundle_info: BundleInfo = BundleInfo(self) + + if bundle_type == BundleType.APPLICATION: + assert isinstance(main_exe, Executable) + elif bundle_type == BundleType.FRAMEWORK: + assert isinstance(main_exe, SharedLibrary) + + def __repr__(self): + repr_str = "<{0} {1}: {2}>" + return repr_str.format(self.__class__.__name__, self.get_id(), self.get_filename()) + + def __str__(self): + return f"{self.name}" + + def get_default_install_dir(self) -> T.Union[T.Tuple[str, str], T.Tuple[None, None]]: + if self.bundle_type == BundleType.APPLICATION: + return self.environment.get_app_dir(), '{appdir}' + elif self.bundle_type == BundleType.FRAMEWORK: + return self.environment.get_framework_dir(), '{frameworkdir}' + else: + return (None, None) + + def get_custom_install_dir(self) -> T.List[T.Union[str, Literal[False]]]: + return self.install_dir + + def type_suffix(self) -> str: + if self.bundle_type == BundleType.APPLICATION: + return '@nsapp' + elif self.bundle_type == BundleType.FRAMEWORK: + return '@nsframework' + else: + return '@nsbundle' + + @property + def typename(self) -> str: + if self.bundle_type == BundleType.APPLICATION: + return 'nsapp' + elif self.bundle_type == BundleType.FRAMEWORK: + return 'nsframework' + else: + return 'nsbundle' + + def get_bundle_info(self) -> BundleInfo: + return self.bundle_info + + def get_bundle_type(self) -> BundleType: + return self.bundle_type + + def get_executable_name(self) -> str: + return self.main_exe.get_filename() + + def get_filename(self) -> str: + return self.bundle_info.get_wrapper_name() + + def can_output_be_directory(self, output: str) -> bool: + return output == self.get_filename() + + def get_outputs(self) -> T.List[str]: + return [self.get_filename()] + + @dataclass(eq=False) class CustomTargetIndex(CustomTargetBase, HoldableObject): diff --git a/mesonbuild/interpreter/interpreter.py b/mesonbuild/interpreter/interpreter.py index 66ea24cf3468..5397bbc442d9 100644 --- a/mesonbuild/interpreter/interpreter.py +++ b/mesonbuild/interpreter/interpreter.py @@ -427,6 +427,7 @@ def build_holder_map(self) -> None: build.SharedModule: OBJ.SharedModuleHolder, build.Executable: OBJ.ExecutableHolder, build.Jar: OBJ.JarHolder, + build.BundleTarget: OBJ.BundleTargetHolder, build.CustomTarget: OBJ.CustomTargetHolder, build.CustomTargetIndex: OBJ.CustomTargetIndexHolder, build.Generator: OBJ.GeneratorHolder, @@ -483,7 +484,7 @@ def process_new_values(self, invalues: T.List[T.Union[TYPE_var, ExecutableSerial for v in invalues: if isinstance(v, ObjectHolder): raise InterpreterException('Modules must not return ObjectHolders') - if isinstance(v, (build.BuildTarget, build.CustomTarget, build.RunTarget)): + if isinstance(v, (build.BuildTarget, build.CustomTarget, build.RunTarget, build.BundleTarget)): self.add_target(v.name, v) elif isinstance(v, list): self.process_new_values(v) diff --git a/mesonbuild/interpreter/interpreterobjects.py b/mesonbuild/interpreter/interpreterobjects.py index f4a2b4107ed3..7eec87f50bfd 100644 --- a/mesonbuild/interpreter/interpreterobjects.py +++ b/mesonbuild/interpreter/interpreterobjects.py @@ -1032,6 +1032,9 @@ class SharedModuleHolder(BuildTargetHolder[build.SharedModule]): class JarHolder(BuildTargetHolder[build.Jar]): pass +class BundleTargetHolder(ObjectHolder[build.BundleTarget]): + pass + class CustomTargetIndexHolder(ObjectHolder[build.CustomTargetIndex]): def __init__(self, target: build.CustomTargetIndex, interp: 'Interpreter'): super().__init__(target, interp) diff --git a/mesonbuild/modules/nsbundle.py b/mesonbuild/modules/nsbundle.py index dfd3ce9c28c1..451d5afd1179 100644 --- a/mesonbuild/modules/nsbundle.py +++ b/mesonbuild/modules/nsbundle.py @@ -5,11 +5,51 @@ import typing as T -from . import NewExtensionModule, ModuleInfo +from . import NewExtensionModule, ModuleInfo, ModuleReturnValue +from mesonbuild import build, dependencies +from mesonbuild.interpreter.type_checking import KwargInfo, NoneType +from mesonbuild.interpreterbase.decorators import ContainerTypeInfo, FeatureNew, typed_kwargs, typed_pos_args +from mesonbuild.nsbundle import BundleType if T.TYPE_CHECKING: + from typing_extensions import TypedDict + + from . import ModuleState from mesonbuild.interpreter import Interpreter + class CommonKws(TypedDict): + contents: T.Optional[build.StructuredSources] + resources: T.Optional[build.StructuredSources] + extra_binaries: T.List[T.Union[str, build.File, build.Executable, build.CustomTarget, build.CustomTargetIndex]] + info_plist: T.Optional[T.Union[str, build.File, build.CustomTarget, build.CustomTargetIndex]] + + class ApplicationKws(CommonKws): + layout: T.Optional[str] + executable_folder_name: T.Optional[str] + + class FrameworkKws(CommonKws): + headers: T.Optional[build.StructuredSources] + + +COMMON_KWS: T.List[KwargInfo] = [ + KwargInfo('contents', (NoneType, build.StructuredSources)), + KwargInfo('resources', (NoneType, build.StructuredSources)), + KwargInfo('extra_binaries', ContainerTypeInfo(list, (str, build.File, build.Executable, build.CustomTarget, + build.CustomTargetIndex)), default=[], listify=True), + KwargInfo('info_plist', (NoneType, str, build.File, build.CustomTarget, build.CustomTargetIndex)) +] + +APPLICATION_KWS: T.List[KwargInfo] = [ + *COMMON_KWS, + KwargInfo('layout', (NoneType, str)), + KwargInfo('executable_folder_name', (NoneType, str)), +] + +FRAMEWORK_KWS: T.List[KwargInfo] = [ + *COMMON_KWS, + KwargInfo('headers', (NoneType, build.StructuredSources)), +] + def initialize(*args: T.Any, **kwargs: T.Any) -> NSBundleModule: return NSBundleModule(*args, **kwargs) @@ -21,4 +61,38 @@ class NSBundleModule(NewExtensionModule): def __init__(self, interpreter: Interpreter): super().__init__() self.methods.update({ + 'wrap_application': self.wrap_application, + 'wrap_framework': self.wrap_framework, }) + + @FeatureNew('nsbundle.wrap_application', '1.7.99') + @typed_pos_args('nsbundle.wrap_application', build.Executable) + @typed_kwargs('nsbundle.wrap_application', *APPLICATION_KWS) + def wrap_application(self, state: ModuleState, args: T.Tuple[build.Executable], kwargs: ApplicationKws + ) -> ModuleReturnValue: + (main_exe,) = args + + tgt = build.BundleTarget(main_exe.name, state.subdir, state.subproject, state.environment, main_exe, BundleType.APPLICATION) + tgt.bundle_info.resources = kwargs['resources'] + tgt.bundle_info.contents = kwargs['contents'] + tgt.bundle_info.extra_binaries = kwargs['extra_binaries'] + + if kwargs['info_plist'] is not None: + tgt.bundle_info.info_dict_file = build._source_input_to_file(tgt, 'info_plist', kwargs['info_plist']) + + return ModuleReturnValue(tgt, [tgt]) + + @FeatureNew('nsbundle.wrap_framework', '1.7.99') + @typed_pos_args('nsbundle.wrap_framework', (build.SharedLibrary, dependencies.InternalDependency)) + @typed_kwargs('nsbundle.wrap_framework', *FRAMEWORK_KWS) + def wrap_framework(self, state: ModuleState, + args: T.Tuple[T.Union[build.SharedLibrary, dependencies.InternalDependency]], + kwargs: FrameworkKws) -> ModuleReturnValue: + (main_lib,) = args + + # TODO + if not isinstance(main_lib, dependencies.InternalDependency): + main_lib = dependencies.InternalDependency(state.project_version, [], [], [], [main_lib], [], [], [], [], + {}, [], [], []) + + return ModuleReturnValue(main_lib, [])