Art-Of-Coding

Copy Pods Resource 导致app打包失败的暂时解决方案

Copy Pods Resource 导致app打包失败的暂时解决方案

起因

最近公司在推进app组件模块化,但是从某一天开始,打包机上的Jenkins再也无法成功archive。Jenkins返回的错误码是65,虽说是错误但是并没有错误日志,就这么神奇地Build Failed了。

Verify final result code for completed build operation
Build operation failed without specifying any errors. Individual build tasks may have failed for unknown reasons.
One possible cause is if there are too many (possibly zombie) processes; in this case, rebooting may fix the problem.
Some individual build task failures (up to 12) may be listed below.

既然是跟组件包相关,我们首先尝试了回退代码。但是距离上一次打包成功时间相差甚远,因此有大量的merge request被merge进主分支。在进行了一系列的尝试之后,依然打包失败,于是放弃。

后来我们发现在自己的电脑上是可以成功archive,但是在打包机上不行(Mac mini和Mac Pro都不行)。打包机会注入一个企业证书进行打包发布,而本地则是用普通的公司证书。所以我们猜测可能是企业证书引起的原因。对比了半天发现工程目录下有一个.mobileprovision文件,删掉后竟然可以成功archive!但是现实又泼了一盆冷水,第二次打包依然失败。如此看来,这个错误的发生是概率性的,且概率十分大。

无奈,继续看日志。在删掉项目中Build Phases中其他无关的项目之后,我们发现删掉之中的[CP] Copy Pods Resources工程就能够成功打包,加上就失败。了解了这个部分的结构后,我们猜测可能是这个阶段中Input FilesOutput Files文件数过多导致的。为了验证我们的猜测,我们删掉了拆分出来的组件中的资源目录,果然打包成功。

分析

因为我们在组件化的过程中,为了简化拆分工作,我们只是设置了podspec中的resource作为资源索引目录,而没有采用cocoapods推荐的resource_bundles作为资源文件的载体(对原代码改动太大,容易出错)。这样cocoapods在pod install的时候,所有资源会直接拷贝入主项目。而cocoapods的实现方式就是在Build Phases中加入[CP] Copy Pods Resources这个阶段,之中加入了在Pod中所有的资源文件,导致这个阶段的文件数量十分庞大。这么庞大的数量可能触发了Xcode的某些bug,导致打包失败。因此,一个暂行的解决方案就是在打包时,把资源文件从Pods中移到主项目中。

方案

代码是Ruby写的,用了Xcodeproj这个项目,如下。

#!/usr/bin/ruby

require 'xcodeproj'
require 'fileutils'

def add_file_resources(direc, current_group, main_target)
	Dir.glob(direc) do |item|
		next if item == '.' or item == ".." or item == '.DS_Store'
		isAsset = item.include? ".xcassets"
		if File.directory?(item) and !isAsset
			new_folder = File.basename(item)
			created_group = current_group.new_group(new_folder)
			add_file_resources("#{item}/*", created_group, main_target)
		else 
            # i = current_group.new_file(item)
			i = current_group.new_reference(item)
			puts "[Project] add: #{item}"
			# main_target.add_file_references([i])
			main_target.add_resources([i])
		end
	end
end

def move_assets_folder(path, depth)  
	Dir.entries(path).each do |sub| 
		next if sub == '.' or sub == '..'  
		if File.directory?("#{path}/#{sub}")  
			next if depth > 3
			if "#{sub}" == "Assets"
				current_project = path.split('/').last
				FileUtils.mv "#{path}/#{sub}", "./podsAsset/#{current_project}"
				puts "[Folder] #{path}/#{sub} => podsAsset/#{current_project}"
			else
				move_assets_folder("#{path}/#{sub}", depth + 1)  
			end
		end  
	end  
end

if(File.exist?("podsAsset"))
	puts "[Folder] 'podsAsset' already exist, remove it!"
	FileUtils.rm_rf("podsAsset")
end
FileUtils.mkdir_p("podsAsset")

move_assets_folder("modules", 1)

project = Xcodeproj::Project.open("./xxx.xcodeproj")
target = project.targets.first

group = project.new_group('podsAsset')
group.set_source_tree('SOURCE_ROOT')
add_file_resources("podsAsset/*", group, target)
project.save

puts "[Done]"

我们一些和工程关系较密切的组件直接放在了根目录的modules文件夹中,因此只要把这个目录下的组件的资源文件拷入主项目应该就没有问题。具体思路就是在当前目录的modules文件中遍历寻找Assets文件夹(组件化规定),如果找到则移动到根目录下的podsAsset文件夹下。为了精准找到的正确的Assets文件夹,我设置了一个最大搜索深度3。在遍历出所有目录后,再调用Xcodeproj中的相关方法将目录下的所有文件加入到工程中。

在工程打包前先执行这个ruby脚本,设置证书后,打包成功。

后记

至此,问题基本解决。但是这只能算是暂时性的解决方案。我们还需要再讨论下组件化中资源文件的组织方式,如果可以的话新组件还是应该放在一个独立的bundle中。

除此之外,还有一个未解的问题。为什么部分机器上无法archive,而在我们开发机器中没出现这个问题。如果各位对这个问题有更加深入的了解或者有更加优雅的解决方案,欢迎告知!