I already searched this issue on other posts, but nothing so far. So here I am.
I'd like to create a bundle that is portable. Portable as in "I can run it on any OS X machine, even if my required libs (Qt) are not installed". Unfortunately, I can't figure out how to use fixup_bundle() (which seems the right tool for it) to achieve this goal.
Here is my minimal CMake generated C++ project :
main.cpp
#include <QString>
#include <iostream>
int main()
{
QString s("Hello, world!");
std::cout << s.toStdString() << std::endl;
return 0;
}
CMakeLists.txt
cmake_minimum_required(VERSION 2.8.11)
project(test)
# That part because I use a custom build of Qt. That's not the
# relevant part (I guess?)
FILE(GLOB QTROOTS path_to_qt/Qt-4.8.1/osx/bin/qmake)
find_program(QT_QMAKE_EXECUTABLE NAMES qmake PATHS ${QTROOTS})
find_package(Qt4 COMPONENTS QtCore REQUIRED)
include(${QT_USE_FILE})
add_executable(test MACOSX_BUNDLE main.cpp)
target_link_libraries(test ${QT_LIBRARIES})
install(SCRIPT bundle.cmake)
bundle.cmake
INCLUDE(BundleUtilities)
fixup_bundle(test.app "" "")
Here's the resulting test.app structure
test.app
- Contents
- Info.plist
- Frameworks
- QtCore.framework
- Versions
- 4
- QtCore
- MacOS
- test
Everything that is needed seems to be in the bundle. Everything compiles smoothly, runs well, but when I invoke fixup_bundle, that's what I get :
vincent@hpcd0016-lion:tmp/test_bundle/ (0) > make install
[100%] Built target test
Install the project...
-- Install configuration: ""
-- fixup_bundle
-- app='test.app'
-- libs=''
-- dirs=''
-- fixup_bundle: preparing...
-- fixup_bundle: copying...
-- 1/4: *NOT* copying '/Users/vincent/tmp/test_bundle/test.app/Contents/MacOS/test'
-- 2/4: copying 'path_to_qt/Qt-4.8.1/osx/lib//QtCore.framework/Versions/4/QtCore'
-- fixup_bundle: fixing...
-- 3/4: fixing up '/Users/vincent/tmp/test_bundle/test.app/Contents/MacOS/test'
exe_dotapp_dir/='test.app/Contents/MacOS/'
item_substring='/Users/vincent/t'
resolved_embedded_item='/Users/vincent/tmp/test_bundle/test.app/Contents/MacOS/test'
Install or copy the item into the bundle before calling fixup_bundle.
Or maybe there's a typo or incorrect path in one of the args to fixup_bundle?
CMake Error at /Applications/CMake 2.8-11.app/Contents/share/cmake-2.8/Modules/BundleUtilities.cmake:568 (message):
cannot fixup an item that is not in the bundle...
Call Stack (most recent call first):
/Applications/CMake 2.8-11.app/Contents/share/cmake-2.8/Modules/BundleUtilities.cmake:656 (fixup_bundle_item)
bundle.cmake:2 (fixup_bundle)
cmake_install.cmake:31 (INCLUDE)
make: *** [install] Error 1
There's the dependencies path (given by otool -L) :
vincent@hpcd0016-lion:test.app/Contents/ (0) > otool -L test.app/Contents/MacOS/test
test.app/Contents/MacOS/test:
path_to_qt/Qt-4.8.1/osx/lib//QtCore.framework/Versions/4/QtCore (compatibility version 4.8.0, current version 4.8.1)
/usr/lib/libstdc++.6.dylib (compatibility version 7.0.0, current version 56.0.0)
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 169.3.0)
vincent@hpcd0016-lion:tmp/test_bundle/ (0) > otool -L test.app/Contents/Frameworks/QtCore.framework/Versions/4/QtCore
test.app/Contents/Frameworks/QtCore.framework/Versions/4/QtCore:
path_to_qt/Qt-4.8.1/osx/lib//QtCore.framework/Versions/4/QtCore (compatibility version 4.8.0, current version 4.8.1)
/usr/lib/libz.1.dylib (compatibility version 1.0.0, current version 1.2.5)
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 159.1.0)
/System/Library/Frameworks/ApplicationServices.framework/Versions/A/ApplicationServices (compatibility version 1.0.0, current version 41.0.0)
/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation (compatibility version 150.0.0, current version 635.15.0)
/System/Library/Frameworks/Security.framework/Versions/A/Security (compatibility version 1.0.0, current version 55010.0.0)
/usr/lib/libstdc++.6.dylib (compatibility version 7.0.0, current version 52.0.0)
/usr/lib/libgcc_s.1.dylib (compatibility version 1.0.0, current version 1094.0.0)
/System/Library/Frameworks/CoreServices.framework/Versions/A/CoreServices (compatibility version 1.0.0, current version 53.0.0)
Obviously, fixup_bundle did not fix up the bundle for the binaries still have their ids set to my machine's paths.
What am I doing wrong on this simple example?
I added this line at the top of my CMakeLists.txt
set(CMAKE_INSTALL_PREFIX ${CMAKE_BINARY_DIR})
And that was it.
By default, apparently, CMAKE_INSTALL_PREFIX is set to /usr/local on my machine. If changing it to my current working directory solved the issue, that means CMake was trying to perform some operations on /usr/local (which it is not allowed to do). So why the error message does not mention such a right access error?
I don't know if I haven't read enough documentation, or if the documentation needs some precisions...
In addition, I actually had to be even more explicit about the install path (i.e. within the .app).
Like this:
set(CMAKE_INSTALL_PREFIX ${CMAKE_BINARY_DIR})
install(CODE "
include(BundleUtilities)
fixup_bundle(${CMAKE_INSTALL_PREFIX}/MyApp.app \"\" \"\")
" COMPONENT Runtime)
(N.B. no separate SCRIPT but rather embedded CODE - shouldn't make a difference).
To respond to timlukins' answer, if you're going to invoke any CMake variables in the install code then you have to be very careful to escape them against early evaluation.
The code you posted will resolve CMAKE_INSTALL_PREFIX
immediately, before it embeds your code into the install script. Which means the cmake_install.cmake
file will contain:
include(BundleUtilities)
fixup_bundle(/usr/local/MyApp.app "" "")
(Or whatever path you had CMAKE_INSTALL_PREFIX
set to when generating the build tree.) That's not what you want, since it makes your build unrelocatable even before it's installed.
You need to protect the CMAKE_INSTALL_PREFIX
invocation so it makes it into the install script intact, and will use the install-time value. There's two ways you can do that.
install(CODE "
include(BundleUtilities)
fixup_bundle(\"\$\{CMAKE_INSTALL_PREFIX\}/MyApp.app\" \"\" \"\")
" COMPONENT Runtime)
install(CODE [[
include(BundleUtilities)
fixup_bundle("${CMAKE_INSTALL_PREFIX}/MyApp.app" "" "")
]] COMPONENT Runtime)
No variable names will be recognized or references replaced inside a bracket argument, so it's safe to use them unescaped. (Generator expressions will still be processed, which can sometimes be quite handy. Not for this, exactly, but I did just think of one way...)
To oarfish's comment question:
How did you solve the problem that qt platform plugin (libqcocoa.dylib) is not bundled with the rest?
Here's how I might handle that. It's lightly tested, seems to at least be on the right track. Though I can't help feeling like there must be an easier way to handle the output paths.
install(CODE [[
include(BundleUtilities)
# You could also do this part before the CODE using
# install(TARGETS MyApp BUNDLE DESTINATION foo)
#
file(INSTALL DESTINATION "${CMAKE_INSTALL_PREFIX}/foo"
TYPE DIRECTORY FILES "$<TARGET_BUNDLE_DIR:MyApp>")
# This string is crazy-long enough that it's worth folding into a var...
set (_plugin_file "$<TARGET_FILE_NAME:Qt5::QCocoaIntegrationPlugin>")
# Ditto the output paths for our installation
set (_appdir "${CMAKE_INSTALL_PREFIX}/foo/MyApp.app")
set (_outdir "${_appdir}/Contents/plugins/platforms")
file(INSTALL DESTINATION "${_outdir}"
TYPE FILE FILES "$<TARGET_FILE:Qt5::QCocoaIntegrationPlugin>")
fixup_bundle("${_appdir}" "${_outdir}/${_plugin_file}" "")
]] COMPONENT Runtime)
As soon as you generate the build system, you can read the cmake_install.cmake
file that's output into the binary directory for this CMakeLists.txt
file, to see if the install(CODE...)
produced what you intended.
If you're wondering where that ${_outdir}
path came from, that's Word of God.
From Qt's "Using qt.conf
":
Absolute paths are used as specified in the
qt.conf
file. All paths are relative to thePrefix
. [...] On macOS, thePrefix
is relative to theContents
in the application bundle. For example,application.app/Contents/plugins/
is the default location for loading Qt plugins. Note that the plugins need to be placed in specific sub-directories under theplugins
directory (see How to Create Qt Plugins for details).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With