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/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 8d7d157e1506..93a971500bc7 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/backend/backends.py b/mesonbuild/backend/backends.py index 18caf7bbe8a7..60704508025b 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.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() @@ -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): @@ -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]: diff --git a/mesonbuild/backend/ninjabackend.py b/mesonbuild/backend/ninjabackend.py index 70122c3794bc..041a4d60ab2f 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 @@ -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 = [] @@ -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 @@ -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', @@ -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 = [] @@ -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(): @@ -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 @@ -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': @@ -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) @@ -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(), @@ -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: @@ -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) @@ -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'): diff --git a/mesonbuild/build.py b/mesonbuild/build.py index 5d35e0833943..5f3f5261e538 100644 --- a/mesonbuild/build.py +++ b/mesonbuild/build.py @@ -26,6 +26,7 @@ PerMachineDefaultable, MesonBugException, EnvironmentVariables, pickle_load, ) +from .nsbundle import BundleInfo, BundleLayout, BundleType from .options import OptionKey from .compilers import ( @@ -103,6 +104,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 | @@ -116,6 +124,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]]: @@ -709,6 +719,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 @@ -1114,7 +1127,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 @@ -1722,21 +1738,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]: @@ -1761,6 +1763,25 @@ def get(self, lib_type: T.Literal['static', 'shared', 'auto']) -> 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'. @@ -2003,7 +2024,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')): @@ -2733,6 +2754,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] @@ -3003,6 +3027,121 @@ 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 BundleTarget(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 + + +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, BundleTarget): + 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, BundleTarget): + 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/envconfig.py b/mesonbuild/envconfig.py index 4055b21761c5..fddcdc7c36f3 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? @@ -367,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. 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 02a59e3986d5..70513c01ecd2 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): @@ -427,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, @@ -3250,6 +3254,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) @@ -3372,6 +3379,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': @@ -3404,16 +3417,17 @@ def build_target(self, node: mparser.BaseNode, args: T.Tuple[str, SourcesVarargs 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) @@ -3458,7 +3472,7 @@ def build_target(self, node: mparser.BaseNode, args: T.Tuple[str, SourcesVarargs 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: @@ -3485,12 +3499,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/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 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/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 """ diff --git a/mesonbuild/modules/nsbundle.py b/mesonbuild/modules/nsbundle.py new file mode 100644 index 000000000000..d76600f84186 --- /dev/null +++ b/mesonbuild/modules/nsbundle.py @@ -0,0 +1,47 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: 2025 Marco Rebhan + +from __future__ import annotations + +import typing as T + +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 . 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: + return NSBundleModule(*args, **kwargs) + + +class NSBundleModule(NewExtensionModule): + INFO = ModuleInfo('nsbundle', '1.6.99') + + def __init__(self, interpreter: Interpreter): + super().__init__() + self.methods.update({ + 'application': self.application, + 'framework': self.framework, + }) + + @FeatureNew('nsbundle.application', '1.6.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.6.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]) diff --git a/mesonbuild/nsbundle.py b/mesonbuild/nsbundle.py new file mode 100644 index 000000000000..2b00223671eb --- /dev/null +++ b/mesonbuild/nsbundle.py @@ -0,0 +1,251 @@ +# 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, BundleTarget, 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: BundleTarget = 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_executables_folder_path(self) -> PurePath: + if self.get_layout() == BundleType.CONTENTS: + return self.get_contents_folder_path() / 'Executables' + else: + return self.get_unlocalized_resources_folder_path() + + 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()}' 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 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