前几天升级Xcode到14.3版本,运行项目报错,于是写了iOS问题记录 - Xcode 14.3版本运行项目报错这篇文章。没想到除了运行项目有问题,打包项目也有问题。
- macOS: 13.3
- Xcode: 14.3
- CocoaPods: 1.12.0
[Xcode菜单栏] -> [Product] -> [Archive],进行打包操作。执行到Run custom shell script '[CP] Embed Pods Frameworks'
时报错,报错相关日志如下:
Symlinked...rsync --delete -av --filter P .*.?????? --links --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "../../../IntermediateBuildFilesPath/UninstalledProducts/iphoneos/xxx.framework" "/Users/xxx/Library/Developer/Xcode/DerivedData/app-dukdzczlzijlklamofogqicmtktj/Build/Intermediates.noindex/ArchiveIntermediates/app/InstallationBuildProductsLocation/Applications/app.app/Frameworks"building file list ... rsync: link_stat "xxx/../../../IntermediateBuildFilesPath/UninstalledProducts/iphoneos/xxx.framework" failed: No such file or directory(2)rsync error: some files could not be transferred (code 23) at /AppleInternal/Library/BuildRoots/97f6331a-ba75-11ed-a4bc-863efbbaf80d/Library/Caches/com.apple.xbs/Sources/rsync/rsync/main.c(996) [sender=2.6.9]Command PhaseScriptExecution failed with a nonzero exit code
从报错信息看,是因为文件或目录找不到报错。因为项目有改动,所以暂时不确定是不是Xcode 14.3版本的原因。找到一台装有14.2版本的电脑,拉取最新代码后执行打包操作,一切正常!那看来这锅Xcode得背,接下来就是找到具体原因和解决办法。
首先要确定这个错误是在执行什么代码的时候出现的,才能进一步分析。
找到Run custom shell script '[CP] Embed Pods Frameworks'
的右侧按钮,展开详情:
可以看到执行的shell脚本路径是:
/Users/xxx/Library/Developer/Xcode/DerivedData/app-dukdzczlzijlklamofogqicmtktj/Build/Intermediates.noindex/ArchiveIntermediates/app/IntermediateBuildFilesPath/app.build/Release-iphoneos/app.build/Script-8D57CFCFEA49D25397FFD044.sh
shell脚本内容:
#!/bin/sh"${PODS_ROOT}/Target Support Files/Pods-app/Pods-app-frameworks.sh"
PODS_ROOT
的值是什么呢?你可能会想,直接在shell脚本中加上一行echo "${PODS_ROOT}"
不就知道了?不行的,每次执行打包操作都会重新生成这个shell脚本,改动不会生效。其实会执行这个自定义shell脚本,是因为在这有设置:
PODS_ROOT
的定义在这:
验证这个很简单,只需要在这加上一行echo "${PODS_ROOT}"
:
重新执行打包操作,你会发现生成的shell脚本中也多了这一行:
#!/bin/shecho "${PODS_ROOT}""${PODS_ROOT}/Target Support Files/Pods-app/Pods-app-frameworks.sh"
同时在打包输出日志中也正常打印出PODS_ROOT
的值。不知道有没有人和我有一样的疑问,生成的shell脚本中并没有导入其他shell脚本或定义PODS_ROOT
,那么这个常量怎么来的?不难猜,PODS_ROOT
应该来自环境变量。老办法,加上env
命令打印一下环境变量:
重新执行打包操作,会打印一大堆环境变量,这里就不一一列出。看打印出来的环境变量,构建设置基本都在里面(没有一个个具体验证)。大致可以得出结论,在构建项目时,Xcode会把构建设置设为临时环境变量。
继续往下分析,找到Pods-app-frameworks.sh
文件,根据报错相关日志,报错应该发生在install_framework
函数中:
# Copies and strips a vendored frameworkinstall_framework(){ if [ -r "${BUILT_PRODUCTS_DIR}/$1" ]; then local source="${BUILT_PRODUCTS_DIR}/$1" elif [ -r "${BUILT_PRODUCTS_DIR}/$(basename "$1")" ]; then local source="${BUILT_PRODUCTS_DIR}/$(basename "$1")" elif [ -r "$1" ]; then local source="$1" fi local destination="${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" if [ -L "${source}" ]; then echo "Symlinked..." source="$(readlink "${source}")" fi if [ -d "${source}/${BCSYMBOLMAP_DIR}" ]; then # Locate and install any .bcsymbolmaps if present, and remove them from the .framework before the framework is copied find "${source}/${BCSYMBOLMAP_DIR}" -name "*.bcsymbolmap"|while read f; do echo "Installing $f" install_bcsymbolmap "$f" "$destination" rm "$f" done rmdir "${source}/${BCSYMBOLMAP_DIR}" fi # Use filter instead of exclude so missing patterns don't throw errors. echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${destination}\"" rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${destination}" local basename basename="$(basename -s .framework "$1")" binary="${destination}/${basename}.framework/${basename}" if ! [ -r "$binary" ]; then binary="${destination}/${basename}" elif [ -L "${binary}" ]; then echo "Destination binary is symlinked..." dirname="$(dirname "${binary}")" binary="${dirname}/$(readlink "${binary}")" fi # Strip invalid architectures so "fat" simulator / device frameworks work on device if [[ "$(file "$binary")" == *"dynamically linked shared library"* ]]; then strip_invalid_archs "$binary" fi # Resign the code if required by the build settings to avoid unstable apps code_sign_if_enabled "${destination}/$(basename "$1")" # Embed linked Swift runtime libraries. No longer necessary as of Xcode 7. if [ "${XCODE_VERSION_MAJOR}" -lt 7 ]; then local swift_runtime_libs swift_runtime_libs=$(xcrun otool -LX "$binary" | grep --color=never @rpath/libswift | sed -E s/@rpath\\/\(.+dylib\).*/\\1/g | uniq -u) for lib in $swift_runtime_libs; do echo "rsync -auv \"${SWIFT_STDLIB_PATH}/${lib}\" \"${destination}\"" rsync -auv "${SWIFT_STDLIB_PATH}/${lib}" "${destination}" code_sign_if_enabled "${destination}/${lib}" done fi}
执行rsync --delete...
命令的时候,source
变量的路径有问题。修改Pods-app-frameworks.sh
文件,增加一些日志打印用于追踪source
变量的变化。经测试,在执行source="$(readlink "${source}")"
之前,source
变量中的路径是绝对路径:
/Users/xxx/Library/Developer/Xcode/DerivedData/app-dukdzczlzijlklamofogqicmtktj/Build/Intermediates.noindex/ArchiveIntermediates/app/BuildProductsPath/Release-iphoneos/SDWebImage/SDWebImage.framework
执行后,变为相对路径:
../../../IntermediateBuildFilesPath/UninstalledProducts/iphoneos/SDWebImage.framework
通过访达的前往文件夹功能,找到绝对路径所指向的位置:
SDWebImage.framework
是一个替身(软链接/符号链接)。在SDWebImage
目录路径下执行ls -l
查看实际指向的路径:
lrwxr-xr-x 1 xxx staff 85 Apr 7 20:23 SDWebImage.framework -> ../../../IntermediateBuildFilesPath/UninstalledProducts/iphoneos/SDWebImage.frameworkdrwxr-xr-x 3 xxx staff 96 Apr 7 20:23 SDWebImage.framework.dSYM
看来readlink
命令的作用就是获取软连接所指向的实际路径,那这路径为什么报错呢?有对比才能找到问题所在,同样的项目用Xcode 14.2版本执行打包操作,在SDWebImage
目录路径下执行ls -l
查看实际指向的路径:
lrwxr-xr-x 1 xxx staff 212 Apr 7 20:30 SDWebImage.framework -> /Users/xxx/Library/Developer/Xcode/DerivedData/app-dukdzczlzijlklamofogqicmtktj/Build/Intermediates.noindex/ArchiveIntermediates/app/IntermediateBuildFilesPath/UninstalledProducts/iphoneos/SDWebImage.frameworkdrwxr-xr-x 3 xxx staff 96 Apr 7 20:30 SDWebImage.framework.dSYM
这么一对比,原因总算是找到了。Xcode 14.3版本构建时将软链接所指向的绝对路径改为了相对路径,导致找不到文件或目录。
那么该怎么修复呢?可以先看看readlink
命令的文档,使用man
(manual)命令查看:
man readlink
执行命令后得到的文档(省略部分):
NAME stat, readlink – display file statusSYNOPSIS stat [-FLnq] [-f format | -l | -r | -s | -x] [-t timefmt] [file ...] readlink [-fn] [file ...]DESCRIPTION The stat utility displays information about the file pointed to by file. Read, write, or execute permissions of the named file are not required, but all directories listed in the pathname leading to the file must be searchable. If no argument is given, stat displays information about the file descriptor for standard input. When invoked as readlink, only the target of the symbolic link is printed. If the given argument is not a symbolic link and the -f option is not specified, readlink will print nothing and exit with an error. If the -f option is specified, the output is canonicalized by following every symlink in every component of the given path recursively. readlink will resolve both absolute and relative paths, and return the absolute pathname corresponding to file. In this case, the argument does not need to be a symbolic link. The information displayed is obtained by calling lstat(2) with the given argument and evaluating the returned structure. The default format displays the st_dev, st_ino, st_mode, st_nlink, st_uid, st_gid, st_rdev, st_size, st_atime, st_mtime, st_ctime, st_birthtime, st_blksize, st_blocks, and st_flags fields, in that order....
输入q
退出文档查看。readlink
命令参数不多,其中有个-f
参数,这个参数的作用是递归找到第一个真实文件并返回该文件的绝对路径(个人理解)。举个例子🌰,假设A是真实文件,执行ln -s A B
命令创建软链接B,执行ln -s B C
命令创建软链接C,readlink C
命令获取的是B,readlink -f C
命令获取的是A的绝对路径。
修改install_framework
函数中的readlink
命令,加上-f
参数。重新执行打包操作,打包成功!分析到这,问题似乎已经解决,可是当我执行完pod install
命令后,Pods-app-frameworks.sh
的文件内容又恢复原状了。这么看来,每次执行pod install
命令都会重新生成Pods-app-frameworks.sh
文件,那如果能找到生成文件的代码,在源头修改不就能解决吗?
不得不说macOS的可视化搜索真的不好用,电脑上的隐藏文件已经设置为显示,以install_framework
为关键词搜索,搜不到有用的信息。没办法,只好用grep
命令来搜索:
grep -R install_framework ~
-R
表示递归搜索指定目录(~
)下的全部文件,这里对用户目录进行搜索,如果你已经确定CocoaPods包所在目录,则可以指定更详细的目录路径加快搜索。搜索后,找到关键的文件路径:
/Users/xxx/.rvm/gems/ruby-3.0.0/gems/cocoapods-1.12.0/lib/cocoapods/generator/embed_frameworks_script.rb
embed_frameworks_script.rb
文件内容:
require 'cocoapods/xcode'module Pod module Generator class EmbedFrameworksScript # @return [Hash{String => Array}] Multiple lists of frameworks per # configuration. # attr_reader :frameworks_by_config # @return [Hash{String => Array}] Multiple lists of frameworks per # configuration. # attr_reader :xcframeworks_by_config # @param [Hash{String => Array] frameworks_by_config # @see #frameworks_by_config # # @param [Hash{String => Array] xcframeworks_by_config # @see #xcframeworks_by_config # def initialize(frameworks_by_config, xcframeworks_by_config) @frameworks_by_config = frameworks_by_config @xcframeworks_by_config = xcframeworks_by_config end # Saves the resource script to the given pathname. # # @param [Pathname] pathname # The path where the embed frameworks script should be saved. # # @return [void] # def save_as(pathname) pathname.open('w') do |file| file.puts(script) end File.chmod(0755, pathname.to_s) end # @return [String] The contents of the embed frameworks script. # def generate script end private # @!group Private Helpers # @return [String] The contents of the embed frameworks script. # def script script = <<-SH.strip_heredoc#{Pod::Generator::ScriptPhaseConstants::DEFAULT_SCRIPT_PHASE_HEADER}if [ -z ${FRAMEWORKS_FOLDER_PATH+x} ]; then # If FRAMEWORKS_FOLDER_PATH is not set, then there's nowhere for us to copy # frameworks to, so exit 0 (signalling the script phase was successful). exit 0fiecho "mkdir -p ${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}"mkdir -p "${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}"COCOAPODS_PARALLEL_CODE_SIGN="${COCOAPODS_PARALLEL_CODE_SIGN:-false}"SWIFT_STDLIB_PATH="${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}"BCSYMBOLMAP_DIR="BCSymbolMaps"#{Pod::Generator::ScriptPhaseConstants::RSYNC_PROTECT_TMP_FILES}# Copies and strips a vendored frameworkinstall_framework(){ if [ -r "${BUILT_PRODUCTS_DIR}/$1" ]; then local source="${BUILT_PRODUCTS_DIR}/$1" elif [ -r "${BUILT_PRODUCTS_DIR}/$(basename "$1")" ]; then local source="${BUILT_PRODUCTS_DIR}/$(basename "$1")" elif [ -r "$1" ]; then local source="$1" fi local destination="${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" if [ -L "${source}" ]; then echo "Symlinked..." source="$(readlink "${source}")" fi if [ -d "${source}/${BCSYMBOLMAP_DIR}" ]; then # Locate and install any .bcsymbolmaps if present, and remove them from the .framework before the framework is copied find "${source}/${BCSYMBOLMAP_DIR}" -name "*.bcsymbolmap"|while read f; do echo "Installing $f" install_bcsymbolmap "$f" "$destination" rm "$f" done rmdir "${source}/${BCSYMBOLMAP_DIR}" fi # Use filter instead of exclude so missing patterns don't throw errors. echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter \\"- CVS/\\" --filter \\"- .svn/\\" --filter \\"- .git/\\" --filter \\"- .hg/\\" --filter \\"- Headers\\" --filter \\"- PrivateHeaders\\" --filter \\"- Modules\\" \\"${source}\\" \\"${destination}\\"" rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${destination}" local basename basename="$(basename -s .framework "$1")" binary="${destination}/${basename}.framework/${basename}" if ! [ -r "$binary" ]; then binary="${destination}/${basename}" elif [ -L "${binary}" ]; then echo "Destination binary is symlinked..." dirname="$(dirname "${binary}")" binary="${dirname}/$(readlink "${binary}")" fi # Strip invalid architectures so "fat" simulator / device frameworks work on device if [[ "$(file "$binary")" == *"dynamically linked shared library"* ]]; then strip_invalid_archs "$binary" fi # Resign the code if required by the build settings to avoid unstable apps code_sign_if_enabled "${destination}/$(basename "$1")" # Embed linked Swift runtime libraries. No longer necessary as of Xcode 7. if [ "${XCODE_VERSION_MAJOR}" -lt 7 ]; then local swift_runtime_libs swift_runtime_libs=$(xcrun otool -LX "$binary" | grep --color=never @rpath/libswift | sed -E s/@rpath\\\\/\\(.+dylib\\).*/\\\\1/g | uniq -u) for lib in $swift_runtime_libs; do echo "rsync -auv \\"${SWIFT_STDLIB_PATH}/${lib}\\" \\"${destination}\\"" rsync -auv "${SWIFT_STDLIB_PATH}/${lib}" "${destination}" code_sign_if_enabled "${destination}/${lib}" done fi}#{Pod::Generator::ScriptPhaseConstants::INSTALL_DSYM_METHOD}#{Pod::Generator::ScriptPhaseConstants::STRIP_INVALID_ARCHITECTURES_METHOD}#{Pod::Generator::ScriptPhaseConstants::INSTALL_BCSYMBOLMAP_METHOD}# Signs a framework with the provided identitycode_sign_if_enabled() { if [ -n "${EXPANDED_CODE_SIGN_IDENTITY:-}" -a "${CODE_SIGNING_REQUIRED:-}" != "NO" -a "${CODE_SIGNING_ALLOWED}" != "NO" ]; then # Use the current code_sign_identity echo "Code Signing $1 with Identity ${EXPANDED_CODE_SIGN_IDENTITY_NAME}" local code_sign_cmd="/usr/bin/codesign --force --sign ${EXPANDED_CODE_SIGN_IDENTITY} ${OTHER_CODE_SIGN_FLAGS:-} --preserve-metadata=identifier,entitlements '$1'" if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then code_sign_cmd="$code_sign_cmd &" fi echo "$code_sign_cmd" eval "$code_sign_cmd" fi} SH contents_by_config = Hash.new do |hash, key| hash[key] = '' end frameworks_by_config.each do |config, frameworks| frameworks.each do |framework| contents_by_config[config] << %( install_framework "#{framework.source_path}"\n) end end xcframeworks_by_config.each do |config, xcframeworks| xcframeworks.select { |xcf| xcf.build_type.dynamic_framework? }.each do |xcframework| target_name = xcframework.target_name name = xcframework.name contents_by_config[config] << %( install_framework "#{Target::BuildSettings::XCFRAMEWORKS_BUILD_DIR_VARIABLE}/#{target_name}/#{name}.framework"\n) end end script << "\n" unless contents_by_config.empty? contents_by_config.keys.sort.each do |config| contents = contents_by_config[config] next if contents.empty? script << %(if [[ "$CONFIGURATION" == "#{config}" ]]; then\n) script << contents script << "fi\n" end script << <<-SH.strip_heredoc if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then wait fi SH script end # @param [Xcode::FrameworkPaths] framework_path # the framework path containing the dSYM # # @return [String, Nil] the name of the dSYM binary, if found # def dsym_binary_name(framework_path) return nil if framework_path.dsym_path.nil? if (path = Pathname.glob(framework_path.dsym_path.join('Contents/Resources/DWARF', '**/*')).first) File.basename(path) end end end endend
关键代码是script
方法,方法内部可以分为两部分:
- 第一部分是
<<-SH.strip_heredoc...SH
,这是一个多行字符串(heredoc
),用于生成一些比较固定的内容。在Ruby语法中,多行字符串一般这样表示<
(XXX可以自定义,前后保持一致),加 -
是为了字符串内能缩进,strip_heredoc
方法用于删除多余的缩进 - 第二部分从
contents_by_config = Hash.new do |hash, key|
到方法结束,这部分代码会根据项目依赖的Pod库生成类似这样的内容:
if [[ "$CONFIGURATION" == "Debug" ]]; then install_framework "${BUILT_PRODUCTS_DIR}/SDWebImage/SDWebImage.framework"fiif [[ "$CONFIGURATION" == "Release" ]]; then install_framework "${BUILT_PRODUCTS_DIR}/SDWebImage/SDWebImage.framework"fiif [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then waitfi
以上只是简单了解一下Pods-app-frameworks.sh
文件内容是怎么生成的,如果你对这感兴趣,可以尝试自己调试CocoaPods源码,调试环境的搭建可以参考CocoaPods - 源码调试环境搭建。
将embed_frameworks_script.rb
文件中的source="$(readlink "${source}")"
改为source="$(readlink -f "${source}")"
,然后执行pod install
命令重新生成Pods-app-frameworks.sh
文件,接着重新打包,一切正常!
不过,这也不是长久之计,关键还是要CocoaPods修复这个问题。已经有人提了issue,连PR都有了,只不过还不知道啥时候发新版本。逛issue的过程中,发现了不同于前面的解决办法:
- 切换
Command Line Tools
版本
Xcode 14.2版本有7.15GB,重新下载有点费时间,所以先看看只下载Command Line Tools for Xcode 14.2(671MB)行不行,安装完成后发现不行,还是得下载Xcode 14.2版本。在Xcode 14.3版本中设置Command Line Tools
版本为14.2,尝试打包还是报错。看来这方法不太行,而且如果都安装有14.2版本,那直接用不就好了。
- 利用Hook修改
Pods-app-frameworks.sh
文件内容
前面的解决方法是在生成文件的时候加上-f
参数,而现在这个方法是在已经生成的文件上修改。
这想法很好,不过直接拿来用会有问题。一是替换内容的时候双引号没有转义,二是存在多target
的时候会找不到文件。除了解决这两个问题,再简单优化一下,不再需要手动设置项目名称:
post_install do |installer| installer.pods_project.targets.each do |target| shell_script_path = "Pods/Target Support Files/#{target.name}/#{target.name}-frameworks.sh" if File::exists?(shell_script_path) shell_script_input_lines = File.readlines(shell_script_path) shell_script_output_lines = shell_script_input_lines.map { |line| line.sub("source=\"$(readlink \"${source}\")\"", "source=\"$(readlink -f \"${source}\")\"") } File.open(shell_script_path, 'w') do |f| shell_script_output_lines.each do |line| f.write line end end end endend
如果没有看前面的问题分析,建议先看一下。解决问题的方法有很多,以下罗列一些,供大家随意选择。
2023/04/10更新:如果你的项目是Flutter项目,除了以下方法,还可以通过升级Flutter到3.7.10或更高版本的方式解决该问题。
- 升级CocoaPods版本
个人比较推荐的方法,但是可能暂时还无法使用。问题将在1.12.1版本修复,如果你遇到这个问题时,CocoaPods版本已经发布到1.12.1或更高版本,推荐通过升级到最新版本解决该问题。
- 修改
Podfile
文件
加上这段代码:
post_install do |installer| installer.pods_project.targets.each do |target| shell_script_path = "Pods/Target Support Files/#{target.name}/#{target.name}-frameworks.sh" if File::exists?(shell_script_path) shell_script_input_lines = File.readlines(shell_script_path) shell_script_output_lines = shell_script_input_lines.map { |line| line.sub("source=\"$(readlink \"${source}\")\"", "source=\"$(readlink -f \"${source}\")\"") } File.open(shell_script_path, 'w') do |f| shell_script_output_lines.each do |line| f.write line end end end endend
重新执行pod install
命令解决问题。你可能会遇到以下报错:
undefined method `exists?' for File:Class
从Ruby 3.2.0版本开始,exists?
方法被移除了,解决方法是替换为exist?
方法。参考文档:Ruby 3.2.0 Released。
- 修改
embed_frameworks_script.rb
文件
文件位于CocoaPods包下的lib/cocoapods/generator/embed_frameworks_script.rb
路径,将文件中的source="$(readlink "${source}")"
替换为source="$(readlink -f "${source}")"
,重新执行pod install
命令解决问题。
- 使用Xcode 14.2版本
既然都升级了,个人不是很推荐退回低版本,如果确实有需要,Xcode历史版本官方下载(需要登录)。
如果这篇文章对你有所帮助,请不要吝啬你的点赞👍加星🌟,谢谢~
来源地址:https://blog.csdn.net/crasowas/article/details/129974391