问题点

最近在履行完 pod install 之后,Pods 工程中的 target 头文件权限为 Project,导致编译失利。

要处理这个问题很简单,把这个头文件修改为 Public。但这个治标不治本,每次履行完 pod install 之后,这些文件又会变成 Project。

思路

想一劳永逸的处理这个问题的第一时间就想到了 cocoapods 供给了 hook 办法 post_install ,那么问题又来了,怎样找到这个这个 hook 的点呢?

网上 google 了一遍,无果,都是关于怎样修改 build_setting 或者 header search path 的。

想到 Xcode 工程所有信息都会存在 xcproject 文件中, 就打开的 pod.xcproject 文件查找相关头文件,发现了文件的描述最后有一个 ATTRIBUTE 关键字,测验修改为 Public 之后回来 Xcode 检查,果然变成了 Public。

再去 google 一下 cocoapods 怎样修改这个属性,仍是无果,再就想测验下看有没有文章系统讲下 post_install 怎样用,就找到了这片文章,www.jianshu.com/p/d8eb397b8…

尽管没有得到有用的结果,但得到了启示,installer 其实便是个目标,通过这种打印的方式太繁琐了,cocoapods 是开源的,能够直接去源码中查找

看源码

查找 public_header 相关信息,在 pod_target_installer 中找到 add_files_to_build_phases 办法

def add_files_to_build_phases(native_target, test_native_targets, app_native_targets)
  target.file_accessors.each do |file_accessor|
    consumer = file_accessor.spec_consumer
    native_target =  case consumer.spec.spec_type
                     when :library
                       native_target
                     when :test
                       test_native_target_from_spec(consumer.spec, test_native_targets)
                     when :app
                       app_native_targets[consumer.spec]
                     end
    headers = file_accessor.headers
    public_headers = file_accessor.public_headers.map(&:realpath)
    private_headers = file_accessor.private_headers.map(&:realpath)
    other_source_files = file_accessor.other_source_files
    ...
end

能够发现是通过 file_accessor 获取到的,那就看下 file_accessor 是怎样获取的

在 Sanbox::FileAccessor 中找到了如下办法

def public_headers(include_frameworks = false)
  public_headers = public_header_files
  private_headers = private_header_files
  if public_headers.nil? || public_headers.empty?
    header_files = headers
  else
    header_files = public_headers
  end
  header_files += vendored_frameworks_headers if include_frameworks
  header_files - private_headers
end

再进一步检查办法调用看到逻辑比较深,就想先试试问题是不是出在这里,通过在 post_install 中打印出 public_headers 发现并无问题。

那就换个思路,看看这些文件是怎样写入到 project 文件中的,回到 pod_target_installer 的 add_files_to_build_phases 办法中

native_target.add_file_references(header_file_refs) do |build_file|
  add_header(file_accessor, build_file, public_headers, private_headers, native_target)
end

看到了添加文件途径到 target 的引证中的代码,再继续检查 add_header 办法的实现

def add_header(file_accessor, build_file, public_headers, private_headers, native_target)
  file_ref = build_file.file_ref
  acl = if !target.build_as_framework? # Headers are already rooted at ${PODS_ROOT}/Headers/P*/[pod]/...
          'Project'
        elsif public_headers.include?(file_ref.real_path)
          'Public'
        elsif private_headers.include?(file_ref.real_path)
          'Private'
        else
          'Project'
        end
  if target.build_as_framework? && !header_mappings_dir(file_accessor).nil? && acl != 'Project'
    relative_path = if mapping_dir = header_mappings_dir(file_accessor)
                      file_ref.real_path.relative_path_from(mapping_dir)
                    else
                      file_ref.real_path.relative_path_from(file_accessor.path_list.root)
                    end
    compile_build_phase_index = native_target.build_phases.index do |bp|
      bp.is_a?(Xcodeproj::Project::Object::PBXSourcesBuildPhase)
    end
    sub_dir = relative_path.dirname
    copy_phase_name = "Copy #{sub_dir} #{acl} Headers"
    copy_phase = native_target.copy_files_build_phases.find { |bp| bp.name == copy_phase_name } ||
      native_target.new_copy_files_build_phase(copy_phase_name)
    native_target.build_phases.move(copy_phase, compile_build_phase_index - 1) unless compile_build_phase_index.nil?
    copy_phase.symbol_dst_subfolder_spec = :products_directory
    copy_phase.dst_path = "$(#{acl.upcase}_HEADERS_FOLDER_PATH)/#{sub_dir}"
    copy_phase.add_file_reference(file_ref, true)
  else
    build_file.settings ||= {}
    build_file.settings['ATTRIBUTES'] = [acl]
  end
end

找到了设置 Public 仍是 Project 的关键代码 build_file.settings['ATTRIBUTES'] = [acl],需求获取到 native_target,检查 native_target 的定义,发现其类为 PBXNativeTarget,在项目中查找 class PBXNativeTarget 发现并无此类,此刻就陷入了窘境。

再想回到 installer 中看能否找到 target 的相关信息,意外的发现 pods_project 的类为 class Project < Xcodeproj::Project,是另一 module Xcodeproj 中 Project 的子类,那么 PBXNativeTarget 是否也会定义在其中呢!

马上去 github 中查找 xcodeproj,发现的确有另一个项目 github.com/CocoaPods/X…

找到了 PBXNativeTarget,并检查了 add_file_references 的实现

def add_file_references(file_references, compiler_flags = {})
  file_references.map do |file|
    extension = File.extname(file.path).downcase
    header_extensions = Constants::HEADER_FILES_EXTENSIONS
    is_header_phase = header_extensions.include?(extension)
    phase = is_header_phase ? headers_build_phase : source_build_phase
    unless build_file = phase.build_file(file)
      build_file = project.new(PBXBuildFile)
      build_file.file_ref = file
      phase.files << build_file
    end
    if compiler_flags && !compiler_flags.empty? && !is_header_phase
      (build_file.settings ||= {}).merge!('COMPILER_FLAGS' => compiler_flags) do |_, old, new|
        [old, new].compact.join(' ')
      end
    end
    yield build_file if block_given?
    build_file
  end
end

发现 build_file 是定义在 PBXHeadersBuildPhase 中,而且找到了与 Xcode 中对应的几项配置

def headers_build_phase
	find_or_create_build_phase_by_class(PBXHeadersBuildPhase)
end
def source_build_phase
  find_or_create_build_phase_by_class(PBXSourcesBuildPhase)
end
def frameworks_build_phase
  find_or_create_build_phase_by_class(PBXFrameworksBuildPhase)
end
...

至此,怎样通过 post_install 修改 Header 为 Public 就明了了

post_install do |installer|
   installer.pods_project.native_targets.each do |native_target|
       native_target.headers_build_phase.files.each do |file|
           file.settings ||= {}
           file.settings['ATTRIBUTES'] = ['Public']
       end
   end
end