Add support for building Apple bundles
Adds macOS (, Swift, GNUstep, whatever else) bundle support.
Still WIP but it can build a simple bundle at this point.
Right now, this contains two variants, of the API, the tip of the branch is a merge commit. One of them will go. These are the two variants:
nsbundle = module('nsbundle')
exe = executable(
'Example App',
'main.m',
dependencies: [
dependency('appleframeworks', modules: ['Foundation', 'AppKit']),
],
)
app = nsbundle.wrap_application(
exe,
resources: structured_sources('icon.icns'),
info_plist: 'Info.plist', # gets merged with some default generated options
)
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
)
Sample project: https://git.dblsaiko.net/MesonBundlesTest/
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. 🎉
I would make it a new module.
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?
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.
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 https://github.com/mesonbuild/meson/pull/14121#issuecomment-2585371033. 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
bundleforlayout. 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.
I've been reading https://github.com/mesonbuild/meson/issues/106 and I feel like some of that discussion is relevant for this too, particularly https://github.com/mesonbuild/meson/issues/106#issuecomment-2194682552 and https://github.com/mesonbuild/meson/issues/106#issuecomment-2195061610.
Right now, in the implementation in this PR, frameworks act a lot like dependencies (in that they add their headers to targets linked against) instead despite being library targets—and while that is how Rust and Java targets work as well, I don't think it fits very well with the rest of Meson's design for C style dependencies. Additionally really does not make it possible to build frameworks with minimal or no build script changes (CMake can do this though I've never tried it myself), perhaps eventually even just a configure time flag—which I think is sort of what you were talking about in https://github.com/mesonbuild/meson/pull/14121#issuecomment-2600976549, @jpakkane.
NB: It does not seem possible to specify that a framework depends on another, or a shared library, or an extra include dir, in the install interface, which is what the original issue is about. But whatever, if you want that, don't build frameworks.
The important difference to the above issue in this case is that both app bundles (which isn't relevant at all in the other issue) and framework bundles have a directory layout that is potentially relevant at runtime1, that means, for them to work correctly they have to exist in assembled form in the build directory already, as opposed to that structure being created at install time.
For this to work in a way that fits with the existing design, a couple things have to happen:
For frameworks, there should be a new function similar to pkg.generate()
- It takes the main library as dependency, plus the extra bundle options
- It returns a framework dependency object which can be added to target dependencies, like declare_dependency
- It creates the bundle in the build directory at build time, sort of like pkg.generate()
- It has an install option like a library target
- It should be possible to additionally install a .pc file
- This will work fine for Ninja but will probably need special handling for Xcode since there is only the single framework target instead of a separate one for each the library and the bundle (this is the reason I did not go this route initially)
- what should happen if this function is called multiple times with the same main library?
That is the easy case, since there is mostly precedent for this in declare_dependency/the pkg-config module. For applications there really isn't, but I think something analogous could be done there.
That would leave us with something like the following:
nsbundle = module('nsbundle')
mylib = shared_library('mylib', 'lib.m')
mylib_fw = nsbundle.framework(mylib, include_directories: 'include', install: true, resources: structured_sources(...), ...)
myapp_exe = executable('myexe', 'main.m', dependencies: [mylib_fw])
nsbundle.application(myapp_exe, resources: structured_sources(...), ...)
Unfortunately that means you will have both the app bundle and the original bare executable in the build directory, but oh well.
Another thing is that installing an app bundle or framework should disable install of the original library in some way, and its resource files which are already shipped in the bundle. Right now that probably needs some logic in the build script if you want to support building these.
What do you think?
1 https://developer.apple.com/documentation/foundation/nsbundle#1651491
I guess the big question here is whether one needs to create a result that is one bundle or several. The way Meson is currently set up is that there can really be one and this can already be done. IOW everything is installed in a single prefix and that is the end result. This you can sort of replicate with a "bundle" build directory layout. Dunno if Xcode can be made to natively understand this layout.
If you need to create multiple bundles, then things become more difficult. If you have ten bundles in the build dir, how would they be installed? Merging them into a single bundle does not seem like a reasonable thing to do, but OTOH most cases I know of really want to produce a single output "thing" and merge everything into it.
Yes, this is written with building multiple bundles per project in mind. Think e.g. a project that builds a couple graphical applications (app bundles) and also maybe a CLI tool, which could either be shipped inside the bundle like Apple sometimes does, or installed into /bin.
If you have ten bundles in the build dir, how would they be installed?
Right now, I'm thinking install each of them into prefix/Applications and prefix/Library/Frameworks respectively. Like a custom_target directory output (or less so, because this is actually aware of each contained file). This might even already work for the non-split-target app bundles (nsbundle.application()), I haven't tested it. (Ideally, this would be a different prefix, because for bundles you want it to be either / or ~/ instead of /usr or /usr/local, but I'm just using the existing prefix for now.)
Interestingly, Swift also supports installed FHS bundles on non-macOS, which place their resources in /usr/share/*.resources (https://github.com/swiftlang/swift-corelibs-foundation/blob/54c4e51c3b848a94576e08e0c05113b55683b050/Sources/CoreFoundation/CFBundle_Executable.c#L205). Since I don't see this documented anywhere, not sure anyone has ever used it though. Meh, maybe in the future.
Merging them into a single bundle does not seem like a reasonable thing to do, but OTOH most cases I know of really want to produce a single output "thing" and merge everything into it.
Yeah, generally they should be installed like any other installed target. Qt/Qt Tools might be a good example here for what this is going for, it builds app bundles each for Designer, Linguist, etc., at least the Homebrew package does.
What are you thinking of that wants to produce a single output? I don't think this is true for anything but the most simple cases (small C tool the produces only a binary for example)
Thanks for your work on this!
I plan to give this a test soon in VLC where this is something we need and report back.
Be aware that while it works, at least for app bundles, this is still very much WIP and I haven't touched the content of it in a while. For now I'm focused on other Swift support work, but I still plan on continuing this, and I'd love to hear feedback on how it works for you/what is missing.
(Maybe I should only support app bundles and generic .bundle bundles here instead of trying to handle frameworks too which are the more difficult case, because the former shouldn't need to be consumed in other targets)