FSA全栈行动 FSA全栈行动
首页
  • 移动端文章

    • Android
    • iOS
    • Flutter
  • 学习笔记

    • 《Kotlin快速入门进阶》笔记
    • 《Flutter从入门到实战》笔记
    • 《Flutter复习》笔记
前端
后端
  • 学习笔记

    • 《深入浅出设计模式Java版》笔记
  • 逆向
  • 分类
  • 标签
  • 归档
  • LinXunFeng
  • GitLqr

公众号:FSA全栈行动

记录学习过程中的知识
首页
  • 移动端文章

    • Android
    • iOS
    • Flutter
  • 学习笔记

    • 《Kotlin快速入门进阶》笔记
    • 《Flutter从入门到实战》笔记
    • 《Flutter复习》笔记
前端
后端
  • 学习笔记

    • 《深入浅出设计模式Java版》笔记
  • 逆向
  • 分类
  • 标签
  • 归档
  • LinXunFeng
  • GitLqr
  • AndroidUI

  • Android第三方SDK

  • Android混淆

  • Android仓库

  • Android新闻

  • Android系统开发

  • Android源码

  • Android注解AOP

  • Android脚本

  • AndroidTv开发

  • AndroidNDK

  • Android音视频

  • Android热修复

  • Android性能优化

  • Android云游戏

  • Android插件化

  • iOSUI

  • iOS工具

    • iOS - 解决Transporter一直卡正在验证的问题
    • iOS - 解决SecurityEnvSDK与SGMain的冲突问题
    • iOS - 实现25秒完成测试包出包
      • 一、背景
      • 二、思考
      • 三、解决方案
        • 1、app 转 ipa
        • 2、获取 app 包路径
      • 四、实践
        • 1、保存 app 路径
        • 2、处理 app 包并上传至蒲公英
      • 五、实现一键
      • 六、其它
  • iOS底层原理与应用

  • iOS组件化

  • iOS音视频

  • iOS疑难杂症

  • iOS之Swift

  • iOS之RxSwift

  • iOS开源项目

  • iOS逆向

  • Flutter开发

  • 移动端
  • iOS工具
LinXunFeng
2022-01-25
目录

iOS - 实现25秒完成测试包出包

欢迎关注微信公众号:[FSA全栈行动 👋]

# 一、背景

在日常研发中,被提 bug 是家常便饭,当 bug 修复完成后便提交代码,然后触发构建机打出测试包,自动上传至蒲公英后提供测试,但是我们的构建机打一个包的时间近 25分钟,而且有时还会莫名其妙的出现失败,效率偏低。那什么办法减少这个时间呢?

经过下面的方案调整后,我将时间测试包的出包时间降低至 20秒 左右,极速出包 😃

# 二、思考

我们在编译运行时,xcode 就已经打出了后缀为 app 的包,并且是经过描述文件签名的,所以可安装于那些 UDID 有记录于描述文件中的测试机,该 app 包存放于 DerivedData,其具体路径如下:

/Users/lxf/Library/Developer/Xcode/DerivedData/LXFCardsLayout-xxx/Build/Products/Debug-iphoneos/LXFCardsLayout.app

所以这从这里开始,我们需要思考的是如何得到该 app 包的路径,以及拿到 app 包后要如何处理才能安装到测试机

# 三、解决方案

# 1、app 转 ipa

上面提到的两个问题中,第二个很好处理,我们只要新建一个名为 Payload 的文件夹,将 LXFCardsLayout.app 放入其中,接着进行 zip 压缩,将 zip 后缀改成 ipa 即可。

这里需要注意的是:该 app 包是根据当前真机的架构打包而来,像我们的测试机是 arm64 架构的,所以打出来的是仅包含 arm64 架构,armv7 的设备就不可使用,但我们的测试机都是 arm64 架构的,所以问题不大。在使用的过程中大家请注意自己的设备架构~

# 2、获取 app 包路径

在编译的过程中,有一个环境变量 BUILD_DIR,可以取到如下路径

/Users/lxf/Library/Developer/Xcode/DerivedData/LXFCardsLayout-xxx/Build/Products

不过在 Archive的时候,得到的路径如下:

/Users/lxf/Library/Developer/Xcode/DerivedData/LXFCardsLayout-xxx/Build/Intermediates.noindex/ArchiveIntermediates/LXFCardsLayout-Swift/BuildProductsPath

在 shell 环境中,我们可以使用 % 取到 LXFCardsLayout-xxx为至的路径,即把 Build/及后边的内容全部删掉

${BUILD_DIR%Build/*}

# 四、实践

# 1、保存 app 路径

在 iOS 项目下新建一个 script 目录,存放 python 脚本 save_build_config.py

# -*- coding: UTF-8 -*-
# -*- author: LinXunFeng -*-
import os
from configparser import ConfigParser


def handle_build_config():
    """保存编译时的一些配置"""
    build_dir_path = os.getenv("BUILD_DIR")  # 编译地址
    if build_dir_path is None:
        return
    
    build_str_index = build_dir_path.find('Build/')
    if build_str_index is not None:
        build_dir_path = build_dir_path[0:build_str_index]
    print(build_dir_path)
    save_config('build_dir_path', build_dir_path)


def save_config(key, value):
    """
    保存配置
    :param key: 键
    :param value: 值
    :return:
    """
    section_name = 'project'
    config_file_name = 'build_time_conf.ini'
    config = ConfigParser()
    config.read(config_file_name)
    if not config.has_section(section_name):
        config.add_section(section_name)
    config.set(section_name, key, value)
    with open(config_file_name, 'w') as f:
        config.write(f)


if __name__ == '__main__':
    handle_build_config()

注:脚本中的 section_name = 'project',可以调整成其它名称,如:项目名,但需要与下文另一脚本中的 section_name 保持一致!

新建 Run Script

填写如下内容

# LinXunFeng 项目其它配置

cd script
python3 save_build_config.py # 记录编译时配置

该操作的用意:在编译的过程中,将 app 包所在路径保持至 script 目录的 build_time_conf.ini 文件中,并使用 build_dir_path 做为其 key。

注:鉴于多人协作下,该 build_time_conf.ini 文件必定不可能相同,所以建议将该 build_time_conf.ini 文件添加至 .gitignore 中

# 2、处理 app 包并上传至蒲公英

push_dev_ipa.py

# -*- coding: utf-8 -*-
# -*- author: LinXunFeng -*-

import getopt, os, sys, shutil, time, json
from utils import file_util as FileUtil
from utils import upload_pgyer as PgyerUtil
from configparser import ConfigParser
from enum import Enum


class AppackSetKey(Enum):
    """appack_set的键"""
    PGYER_API_KEY = "pgyer_api_key"
    PGYER_USER_KEY = "pgyer_user_key"
    PGYER_PASSWORD_KEY = "pgyer_api_password"


def get_build_dir_path(config_ini_path):
    """获取项目的编译目录路径"""
    section_name = 'project'
    config = ConfigParser()
    config.read(config_ini_path)
    if not config.has_section(section_name):
        return ""
    else:
        return config.get(section_name, 'build_dir_path')


def get_build_config_ini_path(project_path):
    """获取build_conf.ini文件路径"""
    return os.path.join(project_path, 'script', 'build_time_conf.ini')


def get_pgyer_config(project_path):
    """获取蒲公英的相关配置"""
    config_set_json = os.path.join(project_path, 'fastlane', 'appack_set.json')
    json_data = json.loads(FileUtil.read_file(config_set_json))
    # print(json_data)
    pgyer_api_key = json_data[AppackSetKey.PGYER_API_KEY.value]
    pgyer_user_key = json_data[AppackSetKey.PGYER_USER_KEY.value]
    pgyer_password = json_data[AppackSetKey.PGYER_PASSWORD_KEY.value]
    return pgyer_api_key, pgyer_user_key, pgyer_password


def handle(project_path, target_name):
    app_name = target_name + '.app'

    config_ini_path = get_build_config_ini_path(project_path)
    build_dir_path = get_build_dir_path(config_ini_path)
    print('build_dir_path -- ', build_dir_path)
    app_path = os.path.join(build_dir_path, 'Build/Products/Debug-iphoneos', app_name)
    # print(app_path)
    # cur_path = os.path.abspath('.')
    script_path = os.path.join(project_path, 'script')
    temp_path = os.path.join(script_path, 'temp')
    payload_path = os.path.join(temp_path, 'Payload')
    payload_app_path = os.path.join(payload_path, app_name)

    if os.path.exists(temp_path):
        shutil.rmtree(temp_path)  # 移除Payload
        time.sleep(1)  # 等删除完
    os.makedirs(payload_path)  # 创建Payload
    new_path = shutil.copytree(app_path, payload_app_path)
    # print(new_path)
    ipa_path = shutil.make_archive(payload_path, 'zip', temp_path)
    ipa_path = shutil.move(ipa_path, os.path.join(temp_path, target_name + '.ipa'))
    print(ipa_path)

    # 上传至蒲公英
    def payer_upload_callback():
        shutil.rmtree(temp_path)  # 删除temp目录

    pgyer_api_key, pgyer_user_key, pgyer_password = get_pgyer_config(project_path)
    PgyerUtil.upload_to_pgyer(ipa_path, pgyer_api_key, pgyer_user_key, password=pgyer_password, callbcak=payer_upload_callback)


if __name__ == "__main__":
    argv = sys.argv[1:]
    project_path = ""  # 项目路径
    target_name = ""  # target名称

    try:
        opts, args = getopt.getopt(argv, "p:t:", ["path=", "target_name="])
    except getopt.GetoptError:
        print('push_dev_ipa.py -p "项目路径" -t "target名"')
        sys.exit(2)

    print(opts)
    for opt, arg in opts:
        if opt in ["-p", "--path"]:
            project_path = arg
            if len(project_path) == 0:
                print('请输入项目的地址')
                sys.exit('请输入项目的地址')
        if opt in ["-t", "--target_name"]:
            target_name = arg

    # print(project_path)
    handle(project_path, target_name)

配置说明:

config_set_json = os.path.join(project_path, 'fastlane', 'appack_set.json')

我们项目中与蒲公英配置相关的内容存入于 项目/fastlane/appack_set.json,可以根据自身实际情况进行调整

{
  ...
  "pgyer_api_key": "api_key_xxx",
  "pgyer_user_key": "user_key_xxx",
  "pgyer_api_password": "api_password_xxx"
  ...
}

在使用该脚本前请根据 【Mac上pyenv的安装与使用】 配置虚拟环境 env383_ScriptBox,完成后激活该虚拟环境,按如下命令安装依赖包

pip install -r requirements.txt

使用:

python push_dev_ipa.py -p "项目路径" -t "target名"

# 五、实现一键

使用 Shuttle (opens new window) 将该命令保存起来,实现在编译完成后,一键打包上传至蒲公英

{
    "更新测试包": [
        {
            "cmd": "cd /Users/lxf/Desktop/github/script_box; python push_dev_ipa.py -p '/Users/lxf/Desktop/github/LXFCardsLayout/'",
            "inTerminal": "new",
            "name": "上传测试包至蒲公英",
            "title": "上传测试包至蒲公英"
        }
    ]
}

此处说明一下 cd 命令的用意:

cd /Users/lxf/Desktop/github/script_box

在 script_box 下存有一个名为 .python-version 的文件,里面仅写了 env383_ScriptBox,这是一个 python 虚拟环境,其作用是:当进入包含该文件所在目录时,会自动切换至该 python 虚拟环境。

我使用的是 pyenv 来实现 python 虚拟环境,详细说明可以看我的另一篇文章:【Mac上pyenv的安装与使用】

# 六、其它

Shuttle 虽然好用,但是在后续使用过程中,你一定会遇到多处配置相同的情况,如果你想解决该问题,不妨看我的解决方案:【Jsonnet - json数据模板语言】

脚本链接:LinXunFeng/script_box: 脚本工具箱 (github.com) (opens new window)

#iOS#Python#Shuttle
iOS - 解决SecurityEnvSDK与SGMain的冲突问题
iOS - 通过runtime获取某个类中所有的变量和方法

← iOS - 解决SecurityEnvSDK与SGMain的冲突问题 iOS - 通过runtime获取某个类中所有的变量和方法→

最近更新
01
Flutter - Xcode16 还原编译速度
04-05
02
AI - 免费的 Cursor 平替方案
03-30
03
Android - 2025年安卓真的闭源了吗
03-28
更多文章>
Theme by Vdoing | Copyright © 2020-2025 FSA全栈行动
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式
×