Welcome to follow the official wechat account: FSA Full stack action 👋

The background,

In daily research and development, it is common for bugs to be mentioned. When the bug is fixed, the code is submitted, and then the test package is triggered by the construction machine, which is automatically uploaded to dandelion to provide the test. However, it takes nearly 25 minutes for our construction machine to make a package, and sometimes it fails inexplicably, resulting in low efficiency. What can be done to reduce this time?

After the adjustment of the following scheme, I reduced the package exit time of the test package to about 20 seconds, and the package exit speed is 😃

Second, the thinking

After compiling and running, Xcode has printed the package with suffix app, which is signed by the description file, so it can be installed on those test machines whose UDID is recorded in the description file. The APP package is stored in DerivedData, and its specific path is as follows:

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

Note: Lxfcardslayout-xxx is randomly named!

So from here, what we need to think about is how to get the path of the app package, and what to do after getting the app package to install into the test machine

Iii. Solutions

1.appipa

Of the two problems mentioned above, the second one is easy to solve. You just create a new folder named Payload, put lxfcardslayout. app in it, and zip it, changing the zip suffix to IPA.

Note here: this app package is packaged according to the current architecture of the real machine. For example, our test machine is arm64 architecture, so the printed device only contains ARM64 architecture, ARMV7 devices cannot be used, but our test machine is ARM64 architecture, so there is no problem. Please pay attention to your own device architecture in the process of use

2, getappPackage path

During compilation, there is an environment variable BUILD_DIR, which can be taken to the following path

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

However, when archiving, the obtained path is as follows:

/Users/lxf/Library/Developer/Xcode/DerivedData/LXFCardsLayout-xxx/Build/Intermediates.noindex/ArchiveIntermediates/LXFCa rdsLayout-Swift/BuildProductsPathCopy the code

In a shell environment, we can use % to get to LXFCardsLayout-xxx, that is, to delete Build/ and everything after it

${BUILD_DIR%Build/*}
Copy the code

Four, the practice

1, save,appThe path

Create a script directory under your iOS project to store the Python script save_build_config.py

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


def handle_build_config() :
    "" save some configuration at compile time ""
    build_dir_path = os.getenv("BUILD_DIR")  # compile address
    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()
Copy the code

Note: the section_name in the script = ‘project’ can be changed to a different name, such as the project name, but it needs to be the same as the section_name in another script below!

A new Run Script

Fill in the following

#Other configurations of LinXunFeng projectCD script python3 save_build_config.py # Records the compile time configurationCopy the code

Build_time_conf. ini file in the script directory and use build_dir_path as its key during compilation.

Note: Since the build_time_conf.ini file must not be the same in multiple collaborations, it is recommended to add the build_time_conf.ini file to.gitignore

2, processing,appPackage and upload to dandelion

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 key "" "
    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) :
    """ Get the build directory path of the project """
    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) :
    """ Get the build_conf.ini file path """
    return os.path.join(project_path, 'script'.'build_time_conf.ini')


def get_pgyer_config(project_path) :
    """ Get the configuration of the dandelion. ""
    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)  # remove content
        time.sleep(1)  # Wait until the deletion is complete
    os.makedirs(payload_path)  # to create content
    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)

    # Upload to Dandelion
    def payer_upload_callback() :
        shutil.rmtree(temp_path)  Delete temp directory

    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 = ""  # Project path
    target_name = ""  # the name of the target

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

    print(opts)
    for opt, arg in opts:
        if opt in ["-p"."--path"]:
            project_path = arg
            if len(project_path) == 0:
                print('Please enter the address of the project')
                sys.exit('Please enter the address of the project')
        if opt in ["-t"."--target_name"]:
            target_name = arg

    # print(project_path)
    handle(project_path, target_name)
Copy the code

Configuration description:

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

The contents related to the dandelion configuration in our project are stored in the project /fastlane/appack_set.json, which can be adjusted according to the actual situation

{..."pgyer_api_key": "api_key_xxx"."pgyer_user_key": "user_key_xxx"."pgyer_api_password": "api_password_xxx". }Copy the code

Before using the script, configure env383_ScriptBox according to “PyEnv Installation and Use on Mac”, activate the virtual environment, and install the dependency packages as follows

pip install -r requirements.txt
Copy the code

Use:

Python push_dev_ipa.py -p "project path" -t "target name"Copy the code

Five, to achieve a key

Use Shuttle to save the command. After compiling, you can upload the command to dandelion with one click

{
    "Update test pack": [{"cmd": "cd /Users/lxf/Desktop/github/script_box; python push_dev_ipa.py -p '/Users/lxf/Desktop/github/LXFCardsLayout/'"."inTerminal": "new"."name": "Upload test package to Dandelion"."title": "Upload test package to Dandelion"}}]Copy the code

Here’s what the CD command does:

cd /Users/lxf/Desktop/github/script_box
Copy the code

Script_box contains a.python-version file called env383_ScriptBox, which is a Python virtual environment that automatically switches to the Python virtual environment when entering the directory containing the file.

I use PyEnv to implement the Python virtual environment. For details, see my other article: pyEnv installation and Use on Mac.

Sixth, other

Although the Shuttle is easy to use, you may encounter the same configuration in the subsequent use of the Shuttle. If you want to solve this problem, please refer to my solution: [Jsonnet-JSON Data Template Language]

LinXunFeng/script_box: Script Toolkit (github.com)