Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add support for building Apple bundles #14121

Draft
wants to merge 11 commits into
base: master
Choose a base branch
from

Conversation

2xsaiko
Copy link

@2xsaiko 2xsaiko commented Jan 11, 2025

Adds macOS (, Swift, GNUstep, whatever else) bundle support.

Still WIP but it can build a simple bundle at this point.

nsbundle = module('nsbundle')

tgt = nsbundle.application(
  'Example App',
  'main.m',
  dependencies: [
    dependency('appleframeworks', modules: ['Foundation', 'AppKit']),
  ],
  bundle_resources: structured_sources('icon.icns'),
  info_plist: 'Info.plist', # gets merged with some default generated options
)

TODO:

  • Framework bundle support
  • Maybe other bundle (.bundle) support
  • Xcode backend support
  • The code is probably sucky because I have no idea how this codebase works
  • Tests
  • Documentation
  • (whatever I can't think of at the moment)

Maybe:

  • Nested framework support (Contents/Frameworks, Contents/Shared Frameworks) & rpath patching on the built executable

Don't plan on writing:

  • VS backend support (don't have Windows so can't test it)

Closes #48. 🎉

@bonzini
Copy link
Collaborator

bonzini commented Jan 11, 2025

I would make it a new module.

@2xsaiko
Copy link
Author

2xsaiko commented Jan 11, 2025

The app_bundle function? I can probably do that. I looked into it originally but then didn't do it because this is 90% the same as executable() and also this way you can use build_target to switch between executable and app_bundle depending on build configuration without having to duplicate the target definition.

What's your alternative for using build_target like that?

Copy link
Member

@eli-schwartz eli-schwartz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not familiar enough with Apple bundles to say much about the topic, but I have a few observations regarding the coding style.

Comment on lines 3024 to 3031
def __init__(
self,
name: str,
subdir: str,
subproject: SubProject,
for_machine: MachineChoice,
sources: T.List['SourceOutputs'],
structured_sources: T.Optional[StructuredSources],
objects: T.List[ObjectTypes],
environment: environment.Environment,
compilers: T.Dict[str, 'Compiler'],
kwargs,
):
self.bundle_resources: T.Optional[StructuredSources] = kwargs['bundle_resources']
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do not use the "black" deformatter.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which formatter should I use?

Comment on lines 3041 to 3050
super().__init__(
name,
subdir,
subproject,
for_machine,
sources,
structured_sources,
objects,
environment,
compilers,
kwargs,
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same.

Comment on lines 3114 to 3129
def contents_root(self) -> str:
return {
'oldstyle': [],
'contents': ['Contents'],
'flat': [],
}[self.bundle_layout]

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

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

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Surely there must be a better way to do this?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this is super ugly. I do want to find a better way to write this. Could at least make this return a str and keep the rest the same but I'm still not satisfied with that.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There, I suppose the simplest way is probably the best here.

Comment on lines 313 to 316
"""
Machine is Darwin (iOS/tvOS/OS X)?
"""
return self.system in {'darwin', 'ios', 'tvos'}
return self.system == 'darwin'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will probably break existing use cases in the wild. It also looks like it conflicts with changes you made elsewhere? What is the purpose of the change -- the commit message doesn't have much in the way of "why"s and definitely doesn't mention this change at all.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I still need to split this change out. From what I can see the system is never 'ios' or 'tvos' but always 'darwin', those variants are subsystem instead (e.g. see cross/iphone.txt).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was first added in f59786e. I suppose people are supposed to be able to both use system='ios' and system='darwin' subsystem='ios'? I'll revert this then.

Comment on lines 1913 to 1917
def func_app_bundle(
self, node: mparser.BaseNode, args: T.Tuple[str, SourcesVarargsType], kwargs: kwtypes.BuildTarget
) -> build.AppBundle:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The black deformatter strikes again...

Comment on lines 3030 to 3034
sources: T.List['SourceOutputs'],
structured_sources: T.Optional[StructuredSources],
objects: T.List[ObjectTypes],
environment: environment.Environment,
compilers: T.Dict[str, 'Compiler'],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When adding brand new code, avoid quoting annotations. Since meson moved to python 3.7 as the minimum, we started using from __future__ import annotations which has the equivalent effect, so the manual quoting is no longer needed -- and makes lines slightly longer, which adds up and eventually tends to cause overly long lines which then need wrapping.

Comment on lines 3111 to 3112
def type_suffix(self):
return "@app"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can add trivial type annotation. Also, double quotes -> single quotes.

@@ -14,6 +14,7 @@
import json
import os
import pickle
import plistlib
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This import is only needed on a single platform, and only some of the time. Maybe it would be a good idea to delay importing it until the function that uses it.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be used on any platform (specifically I also want a reasonable build system for GNUstep on Linux). But yeah, I'll move the import into the function.

@2xsaiko 2xsaiko force-pushed the push-xtkkyrnnxouu branch 10 times, most recently from 6e7e637 to 7976bcc Compare January 16, 2025 00:56
@jpakkane
Copy link
Member

I have been thinking about this also and maybe having a top level function for this is not the best thing to do. Because in practice it would mean that most build files just end up with if macos; app_bundle(...) else executable(...). It also get annoying if you want to bundle some helper exe in you bundle but itself builds as a bundle.

One alternate way of doing this would be to have a new option bundle for layout. This would set things up so that the build directory would be a whole bundle (you'd need additional functions to specify plists and all that). This is important because, from what I can tell, some functionality of macOS is not available unless the thing you are running is an app bundle. FWICT this is also, roughly, how Xcode sets up its projects.

@2xsaiko
Copy link
Author

2xsaiko commented Jan 20, 2025

I have been thinking about this also and maybe having a top level function for this is not the best thing to do. Because in practice it would mean that most build files just end up with if macos; app_bundle(...) else executable(...).

Yeah, this is why I asked about using build_target with types added by a module in #14121 (comment). I wouldn't want that if/else either because that's awful to use.

Speaking of that, I just thought about a possible way to have the public interface in a module only: Have build_target also be able to take opaque type objects that you can return from a module, i.e. build_target(target_type: nsbundle.application_type()) or whatever. Is that a good idea?

It also get annoying if you want to bundle some helper exe in you bundle but itself builds as a bundle.

Right now, what I have in mind is something like nsbundle.application(..., executables: [foo_exe]) which will be placed into the bundle's executables directory (i.e. Xcode's EXECUTABLES_FOLDER_PATH).

One alternate way of doing this would be to have a new option bundle for layout. This would set things up so that the build directory would be a whole bundle (you'd need additional functions to specify plists and all that).

Which build directory? There are no per-target build directories except the private one, aren't there? This would have to be a per-target build directory since the bundle should effectively act like a single file, and that's how it already works right now. It should not contain "build residue" such as the private directory and whatnot, and you should be able to create more than one bundle target per project. Otherwise I don't see how this would be more practical to use than the current workaround in the documentation.

This is important because, from what I can tell, some functionality of macOS is not available unless the thing you are running is an app bundle.

Yeah. This is working fine already though. Building a project with an app bundle target as of right now builds an app bundle in the same place an executable would be, which you can run from the build directory.

The Ninja bundle builder uses this to merge a user supplied Info.plist
with some Meson-generated data.
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

OS X application bundle support
4 participants