Fix Python loading on macOS without wrangle_bundle.

The goal of this work is to let developers run Python things
on macOS on their builds without generating a full
relocatable and redistributable build.

This is a WIP.
This commit is contained in:
Adam Wolf 2021-05-28 02:59:56 -05:00
parent 1b94534451
commit 3100cd3599
2 changed files with 195 additions and 29 deletions

View File

@ -0,0 +1,180 @@
# RefixupMacOS.cmake
# Adjust rpaths and dependency information on macOS
# after fixup_bundle.
# Some of this comes from GetPrerequisites.cmake.
# This is not intended to make an install completely
# redistributable and relocatable.
# TODO: Check style things
function( refix_kicad_bundle target )
# target should be the path to the kicad.app directory
string( TIMESTAMP start_time )
cleanup_python( ${target} )
file( GLOB_RECURSE items ${target}/*.dylib ${target}/*.so ${target}/*.kiface )
foreach( item ${items} )
message( "Refixing '${item}'" )
refix_prereqs( ${item} )
endforeach( )
# For binaries, we need to fix the prereqs and the rpaths
file( GLOB subdirs ${target}/Contents/Applications/*.app )
foreach( subdir ${subdirs} )
file( GLOB binaries ${subdir}/Contents/MacOS/* )
foreach( binary ${binaries} )
message( "Refixing '${binary}'" )
refix_rpaths( ${binary} )
refix_prereqs( ${binary} )
endforeach( )
endforeach( )
file( GLOB pythonbinbinaries ${target}/Contents/Frameworks/Python.framework/Versions/3.*/bin/python3 )
foreach( pythonbinbinary ${pythonbinbinaries} )
message( "Refixing '${pythonbinbinary}'" )
refix_rpaths( ${pythonbinbinary} )
refix_prereqs( ${pythonbinbinary} )
endforeach()
file( GLOB pythonresbinaries ${target}/Contents/Frameworks/Python.framework/Versions/3.*/Resources/Python.app/Contents/MacOS/Python )
foreach( pythonresbinary ${pythonresbinaries} )
message( "Refixing '${pythonresbinary}'" )
refix_rpaths( ${pythonresbinary} )
refix_prereqs( ${pythonresbinary} )
endforeach()
file( GLOB binaries ${target}/Contents/MacOS/* )
foreach( binary ${binaries} )
message( "Refixing '${binary}'" )
refix_rpaths( ${binary} )
refix_prereqs( ${binary} )
endforeach( )
string( TIMESTAMP end_time )
# message( "Refixing start time: ${start_time}\nRefixing end time: ${end_time}" )
endfunction( )
function( cleanup_python bundle)
# Remove extra Python
file( REMOVE_RECURSE ${bundle}/Contents/MacOS/Python )
# Make sure Python's Current is a symlink to 3.x
file( REMOVE_RECURSE ${bundle}/Contents/Frameworks/Python.framework/Versions/Current )
file( GLOB python_version LIST_DIRECTORIES true RELATIVE ${bundle}/Contents/Frameworks/Python.framework/Versions ${bundle}/Contents/Frameworks/Python.framework/Versions/3* )
execute_process( COMMAND ln -s ${python_version} ${bundle}/Contents/Frameworks/Python.framework/Versions/Current )
endfunction()
function( refix_rpaths binary )
get_filename_component( executable_path ${binary} DIRECTORY )
set( desired_rpaths )
file( RELATIVE_PATH relative_kicad_framework_path ${executable_path} ${target}/Contents/Frameworks )
string( REGEX REPLACE "/+$" "" relative_kicad_framework_path "${relative_kicad_framework_path}" ) # remove trailing slash
file( RELATIVE_PATH relative_python_framework_path ${executable_path} ${target}/Contents/Frameworks/Python.framework )
string( REGEX REPLACE "/+$" "" relative_python_framework_path "${relative_python_framework_path}" ) # remove trailing slash
list( APPEND desired_rpaths "@executable_path/${relative_kicad_framework_path}" "@executable_path/${relative_python_framework_path}" )
get_item_rpaths( ${binary} old_rpaths )
foreach( desired_rpath ${desired_rpaths} )
execute_process(
COMMAND install_name_tool -add_rpath ${desired_rpath} ${binary}
RESULT_VARIABLE add_rpath_rv
OUTPUT_VARIABLE add_rpath_ov
ERROR_VARIABLE add_rpath_ev
)
if( NOT add_rpath_rv STREQUAL "0" )
message( FATAL_ERROR "adding rpath failed: ${add_rpath_rv}\n${add_rpath_ev}" )
endif( )
endforeach( )
endfunction( )
function( refix_prereqs target )
# Replace '@executable_path/../Frameworks/' in dependencies with '@rpath/'
execute_process(
COMMAND otool -L ${target}
RESULT_VARIABLE gp_rv
OUTPUT_VARIABLE gp_cmd_ov
ERROR_VARIABLE gp_ev
)
if( NOT gp_rv STREQUAL "0" )
message( FATAL_ERROR "otool failed: ${gp_rv}\n${gp_ev}" )
endif( )
string( REPLACE ";" "\\;" candidates "${gp_cmd_ov}" )
string( REPLACE "\n" "${eol_char};" candidates "${candidates}" )
# check for install id and remove it from list, since otool -L can include a
# reference to itself
set( gp_install_id )
execute_process(
COMMAND otool -D ${target}
RESULT_VARIABLE otool_rv
OUTPUT_VARIABLE gp_install_id_ov
ERROR_VARIABLE otool_ev
)
if( NOT otool_rv STREQUAL "0" )
message( FATAL_ERROR "otool -D failed: ${otool_rv}\n${otool_ev}" )
endif()
# second line is install name
string( REGEX REPLACE ".*:\n" "" gp_install_id "${gp_install_id_ov}" )
if( gp_install_id )
# trim
string( REGEX MATCH "[^\n ].*[^\n ]" gp_install_id "${gp_install_id}" )
endif( )
set( changes "" )
set( otool_regex "^\t([^\t]+) \\(compatibility version ([0-9]+.[0-9]+.[0-9]+), current version ([0-9]+.[0-9]+.[0-9]+)\\)${eol_char}$" )
foreach( candidate ${candidates} )
if( "${candidate}" MATCHES "${gp_regex}" )
string( REGEX REPLACE "${otool_regex}" "\\1" raw_prereq "${candidate}" )
if ( raw_prereq MATCHES "^@executable_path/\\.\\./\\.\\./.*/Contents/MacOS/Python$" )
set( changed_prereq "@rpath/Versions/Current/Python" )
elseif ( raw_prereq MATCHES "^@executable_path/\\.\\./Frameworks/" )
string( REPLACE "@executable_path/../Frameworks/"
"@rpath/" changed_prereq
"${raw_prereq}" )
elseif ( raw_prereq MATCHES "^@executable_path/\\.\\./PlugIns/" )
string( REPLACE "@executable_path/../PlugIns/"
"@rpath/../PlugIns/" changed_prereq
"${raw_prereq}" )
else( )
continue( )
endif( )
# Because of the above continue( ) in the else, we know we changed the prereq if we're here
if( raw_prereq STREQUAL gp_install_id )
set( cmd install_name_tool -id ${changed_prereq} "${target}" )
execute_process( COMMAND ${cmd} RESULT_VARIABLE install_name_tool_result )
if( NOT install_name_tool_result EQUAL 0 )
string( REPLACE ";" "' '" msg "'${cmd}'" )
message( FATAL_ERROR "Command failed setting install id:\n ${msg}" )
endif( )
continue( )
endif( )
if ( NOT raw_prereq STREQUAL changed_prereq )
# we know we need to change this prereq
set( changes ${changes} "-change" "${raw_prereq}" "${changed_prereq}" )
endif( )
endif( )
endforeach( )
if( changes )
set( cmd install_name_tool ${changes} "${target}" )
execute_process( COMMAND ${cmd} RESULT_VARIABLE install_name_tool_result )
if( NOT install_name_tool_result EQUAL 0 )
string( REPLACE ";" "' '" msg "'${cmd}'" )
message( FATAL_ERROR "Command failed:\n ${msg}" )
endif( )
endif( )
endfunction( )

View File

@ -142,11 +142,11 @@ if( APPLE )
# Find python if it is requested
if( ${SCRIPTING_HELPER} )
file( GLOB WXPYTHON_DIR RELATIVE ${OSX_BUNDLE_BUILD_DIR}/${OSX_BUNDLE_PYTHON_SITE_PACKAGES_DIR} ${OSX_BUNDLE_INSTALL_PYTHON_SITE_PACKAGES_DIR}/wx-?.?-osx_cocoa )
file( GLOB PYTHON_SCRIPTING_SO ${OSX_BUNDLE_INSTALL_PYTHON_SITE_PACKAGES_DIR}/*.so )
set( BUNDLE_FIX_LIBS \${BUNDLE_FIX_LIBS} \${PYTHON_SCRIPTING_SO} )
file( GLOB PYTHON_SCRIPTING_SO ${OSX_BUNDLE_INSTALL_PYTHON_SITE_PACKAGES_DIR}/\${WXPYTHON_DIR}/wx/*.so )
set( BUNDLE_FIX_LIBS \${BUNDLE_FIX_LIBS} \${PYTHON_SCRIPTING_SO} )
# file( GLOB WXPYTHON_DIR RELATIVE ${OSX_BUNDLE_BUILD_DIR}/${OSX_BUNDLE_PYTHON_SITE_PACKAGES_DIR} ${OSX_BUNDLE_INSTALL_PYTHON_SITE_PACKAGES_DIR}/wx-?.?-osx_cocoa )
# file( GLOB PYTHON_SCRIPTING_SO ${OSX_BUNDLE_INSTALL_PYTHON_SITE_PACKAGES_DIR}/*.so )
# set( BUNDLE_FIX_LIBS \${BUNDLE_FIX_LIBS} \${PYTHON_SCRIPTING_SO} )
# file( GLOB PYTHON_SCRIPTING_SO ${OSX_BUNDLE_INSTALL_PYTHON_SITE_PACKAGES_DIR}/\${WXPYTHON_DIR}/wx/*.so )
# set( BUNDLE_FIX_LIBS \${BUNDLE_FIX_LIBS} \${PYTHON_SCRIPTING_SO} )
endif()
# override default embedded path settings
@ -156,7 +156,7 @@ if( APPLE )
include( ${CMAKE_MODULE_PATH}/BundleUtilities.cmake )
if ( ${PYTHON_FRAMEWORK_HELPER} )
# This idea here is to (eventually) repair anything that fixup_bundle doesn't handle
# This idea here is to repair anything that fixup_bundle doesn't handle
# properly for our setup with both Python.framework *and* symlinked subapps
# that's needed for *running* here
@ -166,8 +166,7 @@ if( APPLE )
# Of course, making it all work right here would be even slicker,
# but if wishes were horses...
# At this point, the above is not true. The bottom comes from what we were doing in KiCad patches
# in kicad-mac-builder for Python 2, but currently the effectual fixing for Python3 is happening in dyldstyle.
# It would be awesome if we find a better solution (or BundleUtilities works for our corner case better)
execute_process( COMMAND cp -RP ${PYTHON_FRAMEWORK} ${OSX_BUNDLE_INSTALL_LIB_DIR}/)
# We're using cp -RP because CMake's COPY_RESOLVED_BUNDLE... and COPY_DIRECTORY don't handle symlinks correctly
@ -182,27 +181,8 @@ if( APPLE )
IGNORE_ITEM \"Python;python;python3;python3.8;pythonw;pythonw3;pythonw3.8\"
)
# BundleUtilities clobbers the rpaths and install_names that we carefully setup in Python.framework, even if we mark Python things as IGNORE_ITEMs.
# python/site-packages/_pcbnew.so
execute_process( COMMAND install_name_tool -change @executable_path/../../Contents/MacOS/Python.framework/Versions/3.8/Python @rpath/Python.framework/Python ${OSX_BUNDLE_INSTALL_PYTHON_SITE_PACKAGES_DIR}/_pcbnew.so )
execute_process( COMMAND install_name_tool -change @executable_path/../../Contents/MacOS/Python.framework/Python @rpath/Python.framework/Python ${OSX_BUNDLE_INSTALL_PYTHON_SITE_PACKAGES_DIR}/_pcbnew.so )
execute_process( COMMAND install_name_tool -change @executable_path/../../Contents/MacOS/Python.framework/Versions/3.8/Resources/Python.app/Contents/MacOS/Python @rpath/Python.framework/Python ${OSX_BUNDLE_INSTALL_PYTHON_SITE_PACKAGES_DIR}/_pcbnew.so )
# _pcbnew.kiface
execute_process( COMMAND install_name_tool -change @executable_path/../../Contents/MacOS/Python @rpath/Python.framework/Python ${OSX_BUNDLE_INSTALL_KIFACE_DIR}/_pcbnew.kiface )
execute_process( COMMAND install_name_tool -add_rpath @executable_path/../Frameworks ${OSX_BUNDLE_INSTALL_KIFACE_DIR}/_pcbnew.kiface )
# Python.framework/Versions/Current/bin/python3
execute_process( COMMAND install_name_tool -change @executable_path/../../Contents/MacOS/Python.framework/Python @rpath/Python.framework/Python ${OSX_BUNDLE_INSTALL_LIB_DIR}/Python.framework/Versions/Current/bin/python3 )
execute_process( COMMAND install_name_tool -add_rpath @executable_path/../../../../ ${OSX_BUNDLE_INSTALL_LIB_DIR}/Python.framework/Versions/Current/bin/python3 )
# Python.framework/Versions/Current/Resources/Python.app/Contents/MacOS/Python
execute_process( COMMAND install_name_tool -change @executable_path/../../Contents/MacOS/Python.framework/Versions/3.8/Resources/Python.app/Contents/MacOS/Python @rpath/Python.framework/Python ${OSX_BUNDLE_INSTALL_LIB_DIR}/Python.framework/Versions/Current/Resources/Python.app/Contents/MacOS/Python )
execute_process( COMMAND install_name_tool -add_rpath @executable_path/../Frameworks ${OSX_BUNDLE_INSTALL_LIB_DIR}/Python.framework/Resources/Python.app/Contents/MacOS/Python )
execute_process( COMMAND install_name_tool -add_rpath @executable_path/../../../../../../../ ${OSX_BUNDLE_INSTALL_LIB_DIR}/Python.framework/Resources/Python.app/Contents/MacOS/Python )
execute_process( COMMAND install_name_tool -add_rpath @executable_path/../../../../../ ${OSX_BUNDLE_INSTALL_LIB_DIR}/Python.framework/Resources/Python.app/Contents/MacOS/Python )
# BundleUtilities clobbers the rpaths and install_names that we carefully setup in Python.framework, even if
# we mark Python things as IGNORE_ITEMs. We'll refix them later.
else()
fixup_bundle( ${OSX_BUNDLE_INSTALL_BIN_DIR}/kicad
\"\${BUNDLE_FIX_LIBS}\"
@ -242,6 +222,12 @@ if( APPLE )
move_to_main_bundle( \"pcb_calculator.app\" \"PCB Calculator.app\" )
move_to_main_bundle( \"pcbnew.app\" \"Pcbnew.app\" )
move_to_main_bundle( \"pl_editor.app\" \"Page Layout Editor.app\" )
if ( ${PYTHON_FRAMEWORK_HELPER} )
include( ${CMAKE_MODULE_PATH}/RefixupMacOS.cmake )
refix_kicad_bundle(${OSX_BUNDLE_INSTALL_DIR})
endif( )
" COMPONENT Runtime
)
endif()