class Dongjia::Binarization

Public Class Methods

load_private_config(ctx, binary) click to toggle source

加载配置

# File lib/dongjia_binarization.rb, line 26
def load_private_config(ctx, binary)
  @@ctx = ctx

  submodule_info = load_submodules_info
  @@submodules = submodule_info.to_h { |s|
    [
      s[:name],
      { sha: s[:sha], binary: s[:binary], }
    ]
  }
  
  @@enabled ||= binary["enabled"]
  @@server_host = binary["server_host"]
  @@target_name = (binary["target_name"] || "").strip
  ignores = binary["ignores"] || []

  if @@enabled
    @@submodules = query_components_existing(submodule_info).to_h { |s|
      [
        s["name"], 
        { sha: s["sha"], binary: s["binary"], }
      ]
    }.each { |k, v|
      ignored = ignores.include?(k)
      v[:binary] &&= !ignored && @@enabled
      v[:ignored] = ignored
    }
  end
rescue => e
  puts "Binarization error: #{e}"
end
process(installer) click to toggle source

开始处理二进制

# File lib/dongjia_binarization.rb, line 92
def process(installer)
  return if @@ctx == nil || @@ctx.sandbox_root == nil

  path = File.join(@@ctx.sandbox_root, "Pods.xcodeproj")
  @@pods_proj = Xcodeproj::Project.open(path) if File.exist?(path)
  @@pods_target = (@@target_name.empty?) ? @@pods_proj.targets.first : @@pods_proj.target_by_name("Pods-#{@@target_name}")
  if @@pods_target.nil?
    Pod::UI.warn("[Binarization] 未完成二进制转换,target_name 配置错误")
    return
  end

  setup_pod_cfg(installer)
  
  download_frameworks

  each_pod_proj do |name, cfg|
    process_pods_project_dependencies(name)

    # 处理 Binary Group
    process_binary_group(name)

    # 处理 XXX-xcframeworks.sh 脚本
    process_xcframeworks_shell(name)

    process_pod_xcconfig(name)
  end

  # 更新依赖关系
  each_pod_proj do |name, cfg|
    target = cfg[:target]
    next if target.nil?
    delete_list = []
    add_list = []
    target.dependencies.each { |dep|
      dep_pod_name = real_name(dep.name)
      dep_cfg = @@pod_cfg[dep_pod_name]
      next if dep_cfg.nil?
      if dep_cfg[:target].name != dep.name
        add_list << dep_cfg[:target]
        delete_list << dep
      end
    }
    target.dependencies.delete_if { |dep| delete_list.include?(dep) }
    add_list.each { |t| target.add_dependency(t) }
    cfg[:save] ||= !delete_list.empty? || !add_list.empty?
  end

  # 保存
  save_projects

  process_aggregate_target_xcconfig(installer.aggregate_targets.first.name)
end
remove_dirty_pod_projects() click to toggle source

移除脏工程

# File lib/dongjia_binarization.rb, line 59
def remove_dirty_pod_projects
  if @@enabled
    @@submodules
      .filter { |k,v| !v[:binary] }
      .map { |k,v| k }
      .each { |name|
        path = File.join(@@ctx.sandbox_root, "#{name}.xcodeproj")
        next unless File.exist?(path)
        proj = Xcodeproj::Project.open(path)
        if proj.targets.map(&:name).include?("#{name}-Binary")
          FileUtils.rm_rf(path)
        end
      }
  else
    # 不启用,清除所有包含 -Binary 的工程
    removing_paths = []
    Dir.foreach(@@ctx.sandbox_root) { |filename|
      next if File.extname(filename) != '.xcodeproj'
      pod_name = filename.delete_suffix('.xcodeproj')
      next if pod_name == "Pods"
      process_pod_xcconfig(pod_name)
      path = File.join(@@ctx.sandbox_root, filename)
      proj = Xcodeproj::Project.open(path)
      included_binary_target = proj.targets.map(&:name).any? { |target_name| 
        target_name.end_with?("-Binary") 
      }
      removing_paths << path if included_binary_target
    }
    removing_paths.each { |path| FileUtils.rm_rf(path) }
  end
end
xcframeworks_shell(pod_name) click to toggle source
# File lib/dongjia_binarization.rb, line 465
    def self.xcframeworks_shell(pod_name)
      <<-DESC
#!/bin/sh
set -e
set -u
set -o pipefail

function on_error {
  echo "$(realpath -mq "${0}"):$1: error: Unexpected failure"
}
trap 'on_error $LINENO' ERR


# This protects against multiple targets copying the same framework dependency at the same time. The solution
# was originally proposed here: https://lists.samba.org/archive/rsync/2008-February/020158.html
RSYNC_PROTECT_TMP_FILES=(--filter "P .*.??????")


copy_dir()
{
  local source="$1"
  local destination="$2"

  # 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/\\" \\"${source}\\" \\"${destination}\\""
  rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" "${source}" "${destination}"
}

SELECT_SLICE_RETVAL=""

select_slice() {
  local paths=("$@")
  # Locate the correct slice of the .xcframework for the current architectures
  local target_path=""

  # Split archs on space so we can find a slice that has all the needed archs
  local target_archs=$(echo $ARCHS | tr " " "\\n")

  local target_variant=""
  if [[ "$PLATFORM_NAME" == *"simulator" ]]; then
    target_variant="simulator"
  fi
  if [[ ! -z ${EFFECTIVE_PLATFORM_NAME+x} && "$EFFECTIVE_PLATFORM_NAME" == *"maccatalyst" ]]; then
    target_variant="maccatalyst"
  fi
  for i in ${!paths[@]}; do
    local matched_all_archs="1"
    for target_arch in $target_archs
    do
      if ! [[ "${paths[$i]}" == *"$target_variant"* ]]; then
        matched_all_archs="0"
        break
      fi

      # Verifies that the path contains the variant string (simulator or maccatalyst) if the variant is set.
      if [[ -z "$target_variant" && ("${paths[$i]}" == *"simulator"* || "${paths[$i]}" == *"maccatalyst"*) ]]; then
        matched_all_archs="0"
        break
      fi

      # This regex matches all possible variants of the arch in the folder name:
      # Let's say the folder name is: ios-armv7_armv7s_arm64_arm64e/CoconutLib.framework
      # We match the following: -armv7_, _armv7s_, _arm64_ and _arm64e/.
      # If we have a specific variant: ios-i386_x86_64-simulator/CoconutLib.framework
      # We match the following: -i386_ and _x86_64-
      # When the .xcframework wraps a static library, the folder name does not include
      # any .framework. In that case, the folder name can be: ios-arm64_armv7
      # We also match _armv7$ to handle that case.
      local target_arch_regex="[_\\-]${target_arch}([\\/_\\-]|$)"
      if ! [[ "${paths[$i]}" =~ $target_arch_regex ]]; then
        matched_all_archs="0"
        break
      fi
    done

    if [[ "$matched_all_archs" == "1" ]]; then
      # Found a matching slice
      echo "Selected xcframework slice ${paths[$i]}"
      SELECT_SLICE_RETVAL=${paths[$i]}
      break
    fi
  done
}

install_library() {
  local source="$1"
  local name="$2"
  local destination="${PODS_XCFRAMEWORKS_BUILD_DIR}/${name}"

  # Libraries can contain headers, module maps, and a binary, so we'll copy everything in the folder over

  local source="$binary"
  echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter \\"- CVS/\\" --filter \\"- .svn/\\" --filter \\"- .git/\\" --filter \\"- .hg/\\" \\"${source}/*\\" \\"${destination}\\""
  rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" "${source}/*" "${destination}"
}

# Copies a framework to derived data for use in later build phases
install_framework()
{
  local source="$1"
  local name="$2"
  local destination="${PODS_XCFRAMEWORKS_BUILD_DIR}/${name}"

  if [ ! -d "$destination" ]; then
    mkdir -p "$destination"
  fi

  copy_dir "$source" "$destination"
  echo "Copied $source to $destination"
}

install_xcframework_library() {
  local basepath="$1"
  local name="$2"
  local paths=("$@")

  # Locate the correct slice of the .xcframework for the current architectures
  select_slice "${paths[@]}"
  local target_path="$SELECT_SLICE_RETVAL"
  if [[ -z "$target_path" ]]; then
    echo "warning: [CP] Unable to find matching .xcframework slice in '${paths[@]}' for the current build architectures ($ARCHS)."
    return
  fi

  install_framework "$basepath/$target_path" "$name"
}

install_xcframework() {
  local basepath="$1"
  local name="$2"
  local package_type="$3"
  local paths=("$@")

  # Locate the correct slice of the .xcframework for the current architectures
  select_slice "${paths[@]}"
  local target_path="$SELECT_SLICE_RETVAL"
  if [[ -z "$target_path" ]]; then
    echo "warning: [CP] Unable to find matching .xcframework slice in '${paths[@]}' for the current build architectures ($ARCHS)."
    return
  fi
  local source="$basepath/$target_path"

  local destination="${PODS_XCFRAMEWORKS_BUILD_DIR}/${name}"

  if [ ! -d "$destination" ]; then
    mkdir -p "$destination"
  fi

  copy_dir "$source/" "$destination"

  echo "Copied $source to $destination"
}

install_xcframework "${PODS_ROOT}/_Frameworks/#{pod_name}.xcframework" "#{pod_name}" "framework" "ios-arm64_armv7" "ios-x86_64-simulator"
DESC
    end

Private Class Methods

add_binary_target(proj, name) click to toggle source
# File lib/dongjia_binarization.rb, line 354
def add_binary_target(proj, name)
  source_target = proj.target_by_name(name)
  bin_target = proj.new_aggregate_target("#{name}-Binary", [], :ios, '10.0')
  
  # 处理 xcconfig 引用
  cfg_list0 = source_target.build_configuration_list
  cfg_list1 = bin_target.build_configuration_list
  cfg_list1["Debug"].base_configuration_reference ||= cfg_list0["Debug"].base_configuration_reference
  cfg_list1["Release"].base_configuration_reference ||= cfg_list0["Release"].base_configuration_reference

  # 处理 Build Phases
  phase = bin_target.new_shell_script_build_phase("[CP] Copy XCFrameworks")
  phase.shell_script = "\"${PODS_ROOT}/Target Support Files/#{name}/#{name}-xcframeworks.sh\""

  # 添加依赖
  bin_target.dependencies.replace(source_target.dependencies)
  
  return bin_target
end
download_frameworks() click to toggle source

下载 framework 到 Pods/_Frameworks 目录下

# File lib/dongjia_binarization.rb, line 395
def download_frameworks
  return unless @@enabled
  framework_root = File.join(@@ctx.sandbox_root, "_Frameworks")
  cache_root = File.join(framework_root, "Caches")
  FileUtils.mkdir_p(cache_root) unless File.exist?(cache_root)

  @@submodules.each { |k, v|
    if !v[:binary] || !v[:sha]
      puts "#{k} 未发现二进制版本" if @@enabled && !v[:ignored]
      next
    end

    module_cache_dir = File.join(cache_root, k)
    FileUtils.mkdir_p(module_cache_dir) unless File.exist?(module_cache_dir)

    filename = "#{v[:sha]}.zip"
    binary_dir = File.join(module_cache_dir, v[:sha])
    if not File.exist?(binary_dir)
      # 目录不存在,下载 zip 包并解压
      binary_zip_path = File.join(module_cache_dir, filename)
      puts "Downloading #{k} (#{v[:sha]})"
      url = URI::join(@@server_host, "binary/#{k}/#{filename}")
      Down.download(url, destination: binary_zip_path)
      Archive::Zip.extract(binary_zip_path, binary_dir)
      FileUtils.rm_f(binary_zip_path)
    end
    target = File.expand_path(File.join(binary_dir, "#{k}.xcframework"))
    FileUtils.ln_s(target, framework_root, force: true)
  }
end
each_pod_proj() { |name, cfg| ... } click to toggle source

遍历 Pods 目录下的 xcodeproj

# File lib/dongjia_binarization.rb, line 183
def each_pod_proj
  Dir.foreach(@@ctx.sandbox_root) do |filename|
    next if File.extname(filename) != '.xcodeproj'
    
    name = File.basename(filename, '.xcodeproj')
    next if name == 'Pods'

    cfg = @@pod_cfg[name]
    next if cfg.nil?

    yield(name, cfg) if block_given?
  end
end
get_wifi_ssid() click to toggle source

获取当前 Wi-Fi 的 SSID

# File lib/dongjia_binarization.rb, line 454
def get_wifi_ssid
  lines = `/System/Library/PrivateFrameworks/Apple80211.framework/Resources/airport -I`.split("\n").map { |line| line.strip }
  target = lines.find { |x| x.start_with?("SSID: ") }
  target.nil? ? "" : target.delete_prefix("SSID: ")
end
git_sha_value(component_name) click to toggle source

获取子模块的哈希值

# File lib/dongjia_binarization.rb, line 223
def git_sha_value(component_name)
  git_dir = ".git/modules/componentsOnRemote/#{component_name}"
  git_sha = File.read(File.join(git_dir, "HEAD")).strip
  if git_sha.start_with?("ref: ")
    head_file = git_sha[5, git_sha.length]
    head_path = File.join(git_dir, head_file)
    if File.exist?(head_path)
      git_sha = File.read(head_path).strip
    else
      git_sha = nil
    end
  end
  git_sha
end
load_submodules_info() click to toggle source

加载子模块信息

# File lib/dongjia_binarization.rb, line 219
def load_submodules_info
  return [] unless File.exist?("./componentsOnRemote")

  # 获取子模块的哈希值
  def git_sha_value(component_name)
    git_dir = ".git/modules/componentsOnRemote/#{component_name}"
    git_sha = File.read(File.join(git_dir, "HEAD")).strip
    if git_sha.start_with?("ref: ")
      head_file = git_sha[5, git_sha.length]
      head_path = File.join(git_dir, head_file)
      if File.exist?(head_path)
        git_sha = File.read(head_path).strip
      else
        git_sha = nil
      end
    end
    git_sha
  end

  Dir.foreach("./componentsOnRemote").to_a
    .delete_if { |x| x.start_with?('.') }
    .delete_if { |x|
      # 如果子模块初始化错误,会导致目录下没有内容,需要排除掉
      Dir.foreach("./componentsOnRemote/#{x}").to_a.all? { |f| f.start_with?(".") }
    }
    .map { |comp|
      {
        name: comp,
        sha: git_sha_value(comp),
        binary: false,
      }
    }
end
process_aggregate_target_xcconfig(aggregate_target_name) click to toggle source

处理集成对象的 xcconfig(项目的 xcconfig)

# File lib/dongjia_binarization.rb, line 333
def process_aggregate_target_xcconfig(aggregate_target_name)
  # 处理 Pods/Target Support Files/Pods-项目名/Pods-项目名.debug(release).xcconfig
  # 所有被二进制化的 pod,增加 "${PODS_XCFRAMEWORKS_BUILD_DIR}/#{pod_name}"
  ["debug", "release"].each { |cfg_name|
    cfg_path = Pathname(File.join(@@ctx.sandbox_root, "Target Support Files", aggregate_target_name, "#{aggregate_target_name}.#{cfg_name}.xcconfig"))
    next if not File.exist?(cfg_path)
    cfg = Xcodeproj::Config.new(cfg_path)
    fsp = cfg.attributes["FRAMEWORK_SEARCH_PATHS"].split(" ") || ["$(inherited)"]
    @@submodules.each { |k, v|
      path = "\"${PODS_XCFRAMEWORKS_BUILD_DIR}/#{k}\""
      if v[:binary] == true
        fsp << path if not fsp.include?(path)
      else
        fsp.delete_if { |x| x == path }
      end
    }
    cfg.attributes["FRAMEWORK_SEARCH_PATHS"] = fsp.flatten.uniq.join(" ")
    cfg.save_as(cfg_path)
  }
end
process_binary_group(name) click to toggle source
# File lib/dongjia_binarization.rb, line 426
def process_binary_group(name)
  cfg = @@pod_cfg[name]
  proj = cfg[:project]
  binary_group = proj.groups.find { |g| g.name == "Binary" }
  if cfg[:binary] == true
    if binary_group == nil
      binary_group = proj.new_group("Binary")
      binary_group.new_reference("_Frameworks/#{name}.xcframework")
      proj.sort
    end
  else
    if binary_group != nil
      binary_group.remove_from_project
    end
  end
end
process_pod_xcconfig(name) click to toggle source

处理组件 pod 的 xcconfig

# File lib/dongjia_binarization.rb, line 297
def process_pod_xcconfig(name)
  # 根据 pod 是源码还是二进制包,切换路径为 PODS_CONFIGURATION_BUILD_DIR 或 PODS_XCFRAMEWORKS_BUILD_DIR
  ["debug", "release"].each { |cfg_name|
    cfg_path = Pathname(File.join(@@ctx.sandbox_root, "Target Support Files", name, "#{name}.#{cfg_name}.xcconfig"))
    next if not File.exist?(cfg_path)
    cfg = Xcodeproj::Config.new(cfg_path)
    fsp_str = cfg.attributes["FRAMEWORK_SEARCH_PATHS"] || "$(inherited)"
    fsp = fsp_str.split(" ")

    # 源码的 prefix
    source_prefix = "\"${PODS_CONFIGURATION_BUILD_DIR}/"
    # 二进制包的 prefix
    binary_prefix = "\"${PODS_XCFRAMEWORKS_BUILD_DIR}/"

    fsp.map! { |path|
      if path.start_with?(source_prefix)
        pod_name = path[source_prefix.length, path.length].delete_suffix("\"")
        sm = @@submodules[pod_name]
        if !sm.nil? && sm[:binary]
          path = path.gsub("PODS_CONFIGURATION_BUILD_DIR", "PODS_XCFRAMEWORKS_BUILD_DIR")
        end
      elsif path.start_with?(binary_prefix)
        pod_name = path[binary_prefix.length, path.length].delete_suffix("\"")
        sm = @@submodules[pod_name]
        if !sm.nil? && !sm[:binary]
          path = path.gsub("PODS_XCFRAMEWORKS_BUILD_DIR", "PODS_CONFIGURATION_BUILD_DIR")
        end
      end
      path
    }
    cfg.attributes["FRAMEWORK_SEARCH_PATHS"] = fsp.flatten.uniq.join(" ")
    cfg.save_as(cfg_path)
  }
end
process_pods_project_dependencies(name) click to toggle source

处理 Pods.xcodeproj 的依赖

# File lib/dongjia_binarization.rb, line 254
def process_pods_project_dependencies(name)
  cfg = @@pod_cfg[name]
  proj = cfg[:project]
  changed = false

  latest_target = nil
  removing_target_name = nil

  if cfg[:binary] == true
    # 使用 Binary 版本的 target,如果不存在则创建一个
    binary_target = proj.target_by_name("#{name}-Binary")
    if binary_target == nil
      binary_target = add_binary_target(proj, name)
      changed = true
    end

    latest_target = binary_target
    removing_target_name = name
  else
    # 使用源码版本的 target
    source_target = proj.target_by_name(name)
    source_target.build_configurations.each { |cfg|
      if cfg.build_settings["BUILD_LIBRARY_FOR_DISTRIBUTION"] != "YES"
        cfg.build_settings["BUILD_LIBRARY_FOR_DISTRIBUTION"] = "YES"
        changed = true
      end
    }

    latest_target = source_target
    removing_target_name = "#{name}-Binary"
  end

  cfg[:target] = latest_target

  # 更新依赖
  changed = update_pods_target_dependency(removing_target_name, latest_target) || changed
  changed = proj.targets.map(&:name).include?(removing_target_name) || changed
  proj.targets.delete_if { |t| t.name == removing_target_name }

  cfg[:save] = changed
end
process_xcframeworks_shell(name) click to toggle source
# File lib/dongjia_binarization.rb, line 443
def process_xcframeworks_shell(name)
  cfg = @@pod_cfg[name]
  return if cfg[:binary] != true
  script_path = File.join(@@ctx.sandbox_root, "Target Support Files", name, "#{name}-xcframeworks.sh")
  File.open(script_path, 'w') do |f|
    f.write(xcframeworks_shell(name))
  end
  FileUtils.chmod('a+x', script_path)
end
query_components_existing(components) click to toggle source

向服务器查询哪些 Pod 需要打包

# File lib/dongjia_binarization.rb, line 198
def query_components_existing(components)
  return components unless @@enabled
  result = components
  begin
    uri = URI.parse(URI::join(@@server_host, 'api/binary/checkup').to_s)
    headers = {
      'Content-Type': 'application/json'
    }
    req = Net::HTTP::Post.new(uri, headers)
    req.body = {components: components}.to_json
    resp = Net::HTTP.start(uri.host, uri.port) do |http|
      http.request(req)
    end
    result = JSON.parse(resp.body).dig('res', 'components')
  rescue => e
    puts "Binarization error: #{e}"
  end
  return result
end
real_name(name) click to toggle source

获取实际的 pod 名,如果有 -Binary 后缀,则去除后缀

# File lib/dongjia_binarization.rb, line 160
def real_name(name)
  name = name.delete_suffix("-Binary") if name.end_with?("-Binary")
  return name
end
save_projects() click to toggle source
# File lib/dongjia_binarization.rb, line 165
def save_projects
  if Pod::Config.instance.verbose == true
    pods_should_save = @@pod_cfg.filter { |k, v| v[:save] }.keys
    Pod::UI.notice "Saving Pods project" unless pods_should_save.empty?
    pods_should_save.each { |name|
      puts "  #{name}"
    }
  end
  
  should_save = false
  each_pod_proj do |name, cfg|
    cfg[:project].save if cfg[:save]
    should_save ||= cfg[:save]
  end
  @@pods_proj.save if !@@pods_proj.nil? && should_save
end
setup_pod_cfg(installer) click to toggle source

加载配置

# File lib/dongjia_binarization.rb, line 148
def setup_pod_cfg(installer)
  installer.analysis_result.podfile_dependency_cache.podfile_dependencies.each { |dep|
    name = dep.name.split('/').first
    cfg = @@submodules[name] || {}
    cfg[:binary] = false unless cfg[:binary]
    cfg[:save] = false unless cfg[:save]
    cfg[:project] = Xcodeproj::Project.open(File.join(@@ctx.sandbox_root, "#{name}.xcodeproj"))
    @@pod_cfg[name] = cfg
  }
end
update_pods_target_dependency(removing_target_name, new_target) click to toggle source

更新 Pods.xcodeproj 中 target 的依赖关系

# File lib/dongjia_binarization.rb, line 375
def update_pods_target_dependency(removing_target_name, new_target)
  dep_names = @@pods_target.dependencies.map(&:name)
  changed = false

  index = dep_names.index(removing_target_name)
  if index
    @@pods_target.dependencies.delete_at(index)
    changed = true
  end

  index = dep_names.index(new_target.name)
  unless index
    @@pods_target.add_dependency(new_target)
    changed = true
  end

  return changed
end