记一次 iOS 打包机重构过程 ,附详细图文过程(Jenkins, nginx),可供参考

880 阅读19分钟

背景: 包机的单一业务, 仅仅供内部人员构建 iOS 项目, 重新整理了打包机代码, 只用 Python, 也方便以后维护, 记录一下整个过程

准备工作:

1,安装 Python3.0 , 下载 PyCharm CE, CE 是社区薅羊毛版, 安装 pip

2,因为 Apple 的 itms-services 协议只支持 https, 需要先搞一个内网服务器

3,配置 homebrew 环境: brew.sh/, 如果卡住的话可以用国内清华源, 贼快, 这里不细说

4,安装 nginx, brew install nginx, 记住 nginx 配置文件目录, 等会儿要用, 控制台会输出

截屏2022-09-20 10.27.24.png

5,安装 Jenkins

brew install jenkins-lts

作者已经将 Jenkins 布置在 8080 端口

终端输入

brew services restart jenkins-lts

浏览器输入 http://ip地址:8080, 可以访问 Jenkins, 具体怎么用, 稍后再说

image.png

nginx 两个常用命令:

brew service restart nginx

brew services stop nginx

Jenkins 两个常用命令:

brew services restart jenkins-lts

brew services stop jenkins-lts

配置好环境以后, 就需要布置内网服务器, 因为 Apple 的 tms-services 协议只支持 https, 需要搞一下 ssl 证书,去腾讯云申请一个域名来举例,打开腾讯云, 购买一个域名, 需要实名认证, 实名认证后半个小时左右就注册完成

截屏2022-09-19 11.54.46.png

等待

截屏2022-09-19 11.58.15.png

DNS 解析, 映射到打包机的ip, 查看 ip 地址后进行映射

截屏2022-09-19 14.04.36.png

映射

截屏2022-09-19 14.10.17.png

申请免费 SSl 证书

截屏2022-09-19 14.14.57.png

申请

截屏2022-09-19 14.15.42.png

选择创建的域名

截屏2022-09-19 14.16.46.png

接下来就是等待

截屏2022-09-19 14.17.52.png

完成审核后下载 ssl 证书, 会有四个文件, 记下文件的路径, 供 nginx 配置用

截屏2022-09-19 14.20.16.png

点击下载 nginx

截屏2022-09-19 14.21.55.png

下载 ssl 证书, 会有四个文件, 记下文件的路径, 供 nginx 配置用

截屏2022-09-19 14.23.51.png

打开刚才安装 nginx 的目录, 找到 nginx.conf 文件

image.png

为了方便, 去掉注释代码, 只保留 https 的配置

#user  nobody;
worker_processes  1;
events {
    worker_connections  1024;
}

#user  nobody;
worker_processes  1;
events {
    worker_connections  1024;
}

http {
    include       mime.types;
    default_type  application/octet-stream;
    sendfile        on;
    keepalive_timeout  65;
    server {
        listen 443 ssl;
        server_name <--申请的域名-->;
        ssl_certificate <--下载的 pem 文件路径-->;
        ssl_certificate_key <--下载的 key 文件路径-->
        ssl_session_timeout 5m;
        ssl_protocols TLSv1.2 TLSv1.3;
        ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE;
        ssl_prefer_server_ciphers on;
        location / {
            root html;
            index  index.html index.htm;
        }
    }
    include servers/*;
}

把域名信息, 刚才下载的 ssl 信息里面的两个文件路径填写上去

截屏2022-09-20 10.35.29.png

保存之后终端输入

brew service restart nginx

在浏览器输入 www.projectpackage.xyz, 能显示 nginx 的欢迎页, 说明nginx配置完成,此域名只映射到当前电脑 ip ,只能在内网访问, 外网挂上公司vpn也可以访问, Jenkins 也是一样

截屏2022-09-20 10.43.07.png

写代码前, 先创建两个文件夹, 文件名随意

一个文件夹存放各个项目打包的 ipa 文件, 公开出来, 供内部人员访问, 用 ipa 拿符号表或者其他用途

一个文件夹存放源码, 不公开

截屏2022-10-08 18.42.04.png

然后打开 PyCharm, 准备开始写代码

以码云为例, 创建远程仓库, 将项目克隆到本地(刚才创建的 Source 文件夹内), 添加一些常用库之后推上去

iShot_2022-10-08_10.52.22.png

代码思路, 大致分为七个步骤:

1, git 操作, 获取最近 gitlog

2, pod 操作

3, Archive 并 导出 ipa 到当前项目文件夹

4, 生成本地安装HTML

5, 上传到蒲公英, 生成蒲公英下载页面

6, 群通知

7, Jenkins 流程

每个步骤都写成一个方法, 每个方法写完之后都调试一下

步骤一: pip 安装 git 库, 终端输入

pip install gitpython

导入库:

from git.repo import Repo

定义两个全局变量, 源码路径 和 git 分支

git_branch = 'develop'

code_path = '/Users/petter/Documents/Package/Source/ikun-a'

git_branch 暂时写死, 后面会从 Jenkins 配置中读取

方法代码:

# 格式化输出
def format_output(text: str):
    print(text.center(40, '*'))
    pass

# 获取最近10次提交信息
def git_operation():
    repo = Repo(code_path)
    # 还原
    repo.git.reset('--hard')
    # 清除未跟踪文件
    repo.git.clean('-dxf')
    # 切换分支
    repo.git.checkout(git_branch)
    # pull
    repo.git.pull()
    format_output('git 完成')
    # 获取 log 信息
    commit_log = repo.git.log('--pretty={"%an","%s","%cd"}', max_count=10, date='format:%Y-%m-%d %H:%M')
    print(commit_log)
    return commit_log

第一个方法为输出分割线的共用方法, 运行一下, 可以看到最近的提交信息

iShot_2022-10-11_17.44.14.png

第二步:

执行 pod update 命令, 拉取第三方库, 这步比较简单, 只需要执行 pod 命令就OK了

导入 os 库, import os

方法代码:

# pod
def pod_operation():
    os.chdir(code_path)
    pod_install = 'pod update --verbose --no-repo-update'
    os.system(pod_install)
    format_output('pod 完成')

运行一下

iShot_2022-10-11_17.50.47.png

第三步:

用 Xcode 指令 Archive 并导出 ipa 到指定文件夹

终端输入 man xcodebuild 查看官方说明, 用这个指令来 Archive

iShot_2022-10-11_17.57.52.png

这个指令来 export

iShot_2022-10-11_18.00.08.png

这个指令有一个关键性参数, 就是 -sdk 后面的参数, 也就是你打出的包所支持的系统版本, 高版本可以覆盖低版本, 但是低版本不能覆盖高版本, 这也就是如果 Xcode 如果版本过低的话 Apple 会强制你升级 Xcode, 否则你无法 archive, 这个参数可以通过 xcodebuild -showsdks 来查看

iShot_2022-10-08_18.06.43.png

-sdk 后面的参数得与之相对应, 但是想着能偷懒就偷懒的原则, 我们可以通过 Xcode.app 的文件夹名来提取这个参数, 这样不管以后 iOS 升级到哪个版本, 都可以不用通过指令去查询这个参数, 实际上 Xcode 的 archive 也是从其 app 内部读取的 sdk 信息, 因为 -sdk 参数之后也可以填写 sdk 的具体路径, 而路径就在 Xcode 里面, iPhoneos 路径: /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs

写个方法提取这个参数:

# 读取 iPhoneos 打包参数, 此参数根据 Xcode 版本而变化, 可通过 xcodebuild -showsdks 指令查询 iOS sdk 名称
def iphoneos_version():
    sdk_path = '/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/'
    file_list = os.listdir(sdk_path)
    file_list_last = file_list[len(file_list) - 1]
    file_list_last_com = file_list_last.split('iPhoneOS')
    file_list_last_com.remove('')
    file_list_last_com_sdk = file_list_last_com[0]
    iphoneos_sdk_com = file_list_last_com_sdk.split('.sdk')
    iphoneos_sdk_version = iphoneos_sdk_com[0]
    format_output(f'sdkVersion: {iphoneos_sdk_version}')
    return iphoneos_sdk_version

打印一下

iShot_2022-10-11_18.19.49.png

这样就能一劳永逸, 以后 iOS 升级就不用考虑这个参数了

这个参数解决了, 开始 archive, 这步是用来代替手动点击 Xcode菜单栏 -> Product -> Archive, 所以要对齐 Xcode 的 Archive 格式, 这是 Xcode 的格式

image.png

以当天的时间为文件夹名, 当天打包的都放在此文件夹内, 文件夹内再用项目名+时间命名, 最终生成 xcarchive 格式文件, 指令生成的 xcarchive 仅仅是以项目名来命名的

所以大概思路是, 先根据当前日期判断文件夹是否存在, 如果不存在, 先创建文件夹, 如果存在, 就直接放里面, 再定义相关变量拼接指令, 最后再修改名称, 以日期和项目名来命名, 然后再导出 ipa

导出 ipa 包需要一个 plist 文件(ExportOptions.plist), 里面包含 打包所需信息, 放在源码的根目录下

根据项目实际情况, 填写对应信息后导出 plist 放在源码根目录下

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>compileBitcode</key>
    <false/>
    <key>destination</key>
    <string>export</string>
    <key>method</key>
    <string>development</string>
    <key>provisioningProfiles</key>
    <dict>
        <key><--项目的 bundle id--></key>
        <string><--证书名称--></string>
    </dict>
    <key>signingCertificate</key>
    <string>Apple Development</string>
    <key>signingStyle</key>
    <string>manual</string>
    <key>stripSwiftSymbols</key>
    <true/>
    <key>teamID</key>
    <string><--项目证书所在的 teamId--></string>
    <key>thinning</key>
    <string>&lt;none&gt;</string>
</dict>
</plist>

把项目的.p12 和 profile 文件, 同样放在项目内, 双击安装证书, 此电脑就具有此项目的证书信息

image.png

push 到仓库

注意: 如果新增测试设备, 项目的管理人员需要同步更新 profile 文件, 下次打包的时候会重新拉去新的 profile 文件

定义四个全局变量:

# 项目名称, xcworkspace 名称
project_name = 'IkunA'
# Archive 存放路径, 根据本地 Xcode 路径设置
xcode_archive_path = '/Users/petter/Library/Developer/Xcode/Archives/'
# ipa 导出路径, 放在本地服务路径下, 以项目名分类
export_ipa_path = f'/Users/petter/Documents/Package/IPA/{project_name}'
# ipa 导出所需 plist 文件, 放在源码根目录下
export_ipa_plist_path = f'{code_path}/ExportOptions.plist'

export_ipa_path 为前面步骤中存放 IPA 路径, 不用手动创建文件文件夹, 导出 ipa 文件时候会自动创建

方法代码:

# archive 并 export
def archive_operation():
    time_start = time.localtime()
    time_start_count = time.time()
    # 先 clean 一下
    command_clean = f'xcodebuild clean -workspace {project_name}.xcodeproj/project.xcworkspace -scheme {project_name}'
    os.system(command_clean)
    work_space = f'{project_name}.xcworkspace'
    # archive存放文件路径, 时间格式:2022-05-31
    archive_path_name = time.strftime('%Y-%m-%d', time_start)
    # 当天多次 archive 均放在此文件文件文件夹内
    archive_put_path = xcode_archive_path + archive_path_name
    # 导出文件临时命名, archive 之后重命名
    archive_generate_file_path = '{}-{:02d}-{:02d}.{:02d}.{:02d}'. \
        format(time_start.tm_year,
               time_start.tm_mon,
               time_start.tm_mday,
               time_start.tm_hour,
               time_start.tm_min)

    # 生成 archive 的文件名, 对齐手动打包命名规范
    # eg: XXX.2022-05-01 15.13, 单数 0 自动补齐
    archive_generate_file_path_change = '{}-{:02d}-{:02d}, {:02d}.{:02d}'. \
        format(time_start.tm_year,
               time_start.tm_mon,
               time_start.tm_mday,
               time_start.tm_hour,
               time_start.tm_min)

    # 判断目录是否存在, 如果没有, 先创建
    if not os.path.exists(archive_put_path):
        os.makedirs(archive_put_path)
    # 生成 archive 文件存放路径, 还是对齐 xcode 手动打包路径
    # 打包参数, 可通过 Application 路径拼接生成, 可通过指令 xcodebuild -showsdks
    # 如果通过指令查询, 需要 xcode 版本保持更新, 否则可能失败
    archive_param_iphone_sdk = f'iphoneos{iphoneos_version()}'
    # 打包类型, 可通过  xcodebuild -list 查询
    archive_param_type = 'Release'
    # 打包指令
    archive_command_base = ' -workspace ' + \
                           work_space + \
                           ' -scheme ' + project_name + \
                           ' -configuration ' + archive_param_type + \
                           ' -sdk ' + \
                           archive_param_iphone_sdk
    archive_command_clean = 'xcodebuild clean' + archive_command_base
    archive_command_path = ' -archivePath ' + archive_put_path + '/' + archive_generate_file_path
    archive_command_generate = 'xcodebuild archive' + archive_command_path + archive_command_base

    print(archive_command_generate)
    os.system(archive_command_clean)
    os.system(archive_command_generate)

    # 导出 ipa 包
    # 创建 ipa 包文件夹, 以项目名和时间创建文件夹
    export_archive_file = export_ipa_path + '/' + f'{project_name}' + archive_generate_file_path
    # 如果文件夹不存在先创建
    if not os.path.exists(export_ipa_path):
        os.makedirs(export_ipa_path)
    if not os.path.exists(export_archive_file):
        os.makedirs(export_archive_file)

    # ipa 路径 (本地安装需要用)
    export_archive_path = archive_put_path + '/' + archive_generate_file_path + '.xcarchive'
    ipa_command = f'xcodebuild -exportArchive' \
                  f' -archivePath {export_archive_path} ' \
                  f'-exportPath {export_archive_file} ' \
                  f'-exportOptionsPlist {export_ipa_plist_path}'
    print(ipa_command)
    os.system(ipa_command)

    # 更改名称, 对齐 xcode 手动打包格式
    archive_file_list = os.listdir(archive_put_path)
    for archive_name in archive_file_list:
        if archive_generate_file_path in archive_name:
            os.rename(f'{archive_put_path}/{archive_name}',
                      f'{archive_put_path}/{project_name} {archive_generate_file_path_change}.xcarchive')

    time_end = time.time()
    archive_time_spend = int((time_end - time_start_count) / 60)
    format_output(f'本次导出共耗时{archive_time_spend}min')
    # 返回 ipa 文件夹名称, 供本地安装使用
    format_output(f'文件夹路径: {archive_generate_file_path}')
    return archive_generate_file_path

调用方法后, Xcode 的 Archive 路径下会有一个 xcarchive 文件, 命名格式和手动打包的一样 同时之前创建的 IPA/项目名 路径下会有一个以打包时间和项目名命名的 ipa 文件夹, 里面有 ipa 文件

iShot_2022-10-11_18.47.39.png

方法返回的是本次导出的 ipa 文件夹名称, 拼接后可得到路径, 为下一步制作 HTML 做准备

第四步:

这步开始前, 需要将之前 nginx.conf 中的配置做修改, 之前 https 中的配置指向的是 nginx 的默认的 HTML 文件, 现在需要改成指向之前创建的 IPA 文件夹, 供内部人员访问

#user  nobody;
worker_processes  1;
events {
    worker_connections  1024;
}

#user  nobody;
worker_processes  1;
events {
    worker_connections  1024;
}

http {
    include       mime.types;
    default_type  application/octet-stream;
    sendfile        on;
    keepalive_timeout  65;
    server {
        listen 443 ssl;
        server_name <--申请的域名-->;
        ssl_certificate <--下载的 pem 文件路径-->;
        ssl_certificate_key <--下载的 key 文件路径-->
        ssl_session_timeout 5m;
        ssl_protocols TLSv1.2 TLSv1.3;
        ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE;
        ssl_prefer_server_ciphers on;
        location / {
            root /Users/petter/Documents/Package/IPA;
            autoindex on;
        }
    }
    include servers/*;
}

改完之后保存一下, 重新输入 brew services restart nginx 指令, 这个时候再访问 www.projectpackage.xyz, 访问的就是内部服务器的根目录, 内部人员就可以下载各个时间打包的 ipa 文件了

iShot_2022-10-11_19.08.11.png

生成下载网页, 定义四个全局变量:

# 内部服务器根目录
service_local = 'https://申请的域名'
# 项目 bundle ID
bundle_id = '项目的 bundleId'
# html 代码, 赋值后写入本地
code_html = """
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
        <title>{TITLE}</title>
        <style>
            h1 {{
                text-align: center;
                font-size: 32px;
                font-weight:bold;
            }}
            p {{
                text-align: center;
            }}
            pre {{
                text-align: center;
            }}
            .round-button {{
                display:block;
                height:64px;
                width:200px;
                line-height:64px;
                border: 2px solid #f5f5f5;
                border-radius: 10px;
                color:#f5f5f5;
                text-align:center;
                text-decoration:none;
                background: #FF4500;
                box-shadow: 0 0 3px gray;
                font-size:18px;
                font-weight:bold;
                margin: auto;
            }}
            .round-button:hover {{
                background: #FF4500;
            }}
            .version {{
                font-size: 18px;
            }}
        </style>
    </head>
    <body>
        <h1>{MESSAGE}</h1>
        <h1>{SUBMESSAGE}</h1>
        <h1>{VERSION}</h1>
        <p><img src="{QR_NAME}"><br></p>
        <p><a href="itms-services://?action=download-manifest&url={MANIFEST_URL}" class="round-button">点击安装</a></p>
        </br>
        <pre>○○○○○○○○○○  git_log:  ○○○○○○○○○○
            {GITLOG}
        </pre>
    </body>
</html>
"""
# 根据 itms-services 要求, 需要提供一个 plist 文件, 格式固定, 把 plist 内容定义成一个变量, 然后根据项目进行赋值后写入本地
code_plist = """
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>items</key>
    <array>
        <dict>
            <key>assets</key>
            <array>
                <dict>
                    <key>kind</key>
                    <string>software-package</string>
                    <key>url</key>
                    <string>{IPA_URL}</string>
                </dict>
            </array>
            <key>metadata</key>
            <dict>
                <key>bundle-identifier</key>
                <string>{BUNDLE_ID}</string>
                <key>bundle-version</key>
                <string>{BUNDLE_VERSION}</string>
                <key>kind</key>
                <string>software</string>
                <key>title</key>
                <string>{TITLE}</string>
            </dict>
        </dict>
    </array>
</dict>
</plist>
"""

code_htmlcode_plist 两个字符串变量格式固定, 赋值后写入本地 安装二维码库

pip install qrcode

import qrcode

编写两个方法:

# 读取项目当前设置的版本号
def current_version():
    file_xcode_project = open(f'{code_path}/{project_name}.xcodeproj/project.pbxproj')
    lines_code_project = file_xcode_project.readlines()
    for content in lines_code_project:
        if 'MARKETING_VERSION' in content:
            splits = content.split('=')
            info = splits[1].split(';')
            if info[0].strip() != '1.0':
                return info[0].strip()
    file_xcode_project.close()
    pass

# 生成静态网页
def local_install(file_name: str, log: str, version: str):
    # 拼接文件名
    path_name = project_name + file_name
    # 复制 plist 内容, 主要是将 ipa 文件放在 https 域名下
    install_url = f'{service_local}/{project_name}/{project_name}{file_name}/{project_name}.ipa'
    plist_content = code_plist.format(IPA_URL=install_url, BUNDLE_ID=bundle_id, BUNDLE_VERSION=version, TITLE=project_name)
    # 写入 plist
    with open(f'{export_ipa_path}/{path_name}/install.plist', 'w') as file:
        file.write(plist_content.strip())
    # 生成二维码, 放在对应项目下
    qr_image = qrcode.make(f'{service_local}/{project_name}/{path_name}/install.html')
    qr_image.save(r'{}/{}/download.png'.format(export_ipa_path, path_name))
    # HTML 参数复制, 主要是将 install.plist 位置告诉 Safari
    main_url = f'{service_local}/{project_name}/{path_name}/install.plist'
    html_content = code_html.format(TITLE=project_name,
                                    MESSAGE=f'{project_name}',
                                    SUBMESSAGE=file_name,
                                    VERSION=f'版本号:{version}',
                                    MANIFEST_URL=main_url,
                                    QR_NAME='download.png', GITLOG=log)
    with open(f'{export_ipa_path}/{path_name}/install.html', 'w') as file:
        file.write(html_content)

    dowload_url = f'{service_local}/{project_name}/{path_name}/install.html'
    print(f'内网下载路径: {dowload_url}')
    return dowload_url

第一个方法是通过读取 xcodeproj 获取当前项目设置的版本号 第二个方法生成静态网页

iShot_2022-10-09_11.26.17.png

调用方法跑起来, 最终会得到一个内网下载路径, 连接 WIFI 可下载, 外网通过 VPN 也可以下载,页面上展示打包日期, 版本号, 修改日志, 二维码等基本信息

iShot_2022-10-11_19.50.27.png

ipa 包文件夹内会多出三个文件, 生成的二维码图片, plist 和 HTML 文件

iShot_2022-10-09_11.50.46.png

打开链接

image.png

点击安装

1901665630796_.pic_hd.jpg

返回桌面查看

IMG_5572.PNG

第五步:

此步骤是增加一个备用下载地址, 万一哪天公司网络挂了, 本地包不能用的时候可以用第三方托管平台, 这里用的是蒲公英平台

蒲公英 API 文档: www.pgyer.com/doc/view/ap…

去蒲公英申请 APIkey

我们需要做的就是将第三步打包的 ipa 文件按照文档上传到蒲公英服务器上就可以了 Python 导入 json , request 库

import requests
import json

按照文档, 封装成一个方法:

def ipa_upload_pgy(ipa_path: str):
    print(ipa_path)
    url_pgy = 'https://www.pgyer.com/apiv2/app/getCOSToken'
    api_key = '你申请的蒲公英key'
    param_upload_info = {
        '_api_key': api_key,
        'buildType': 'ios',
        'buildPassword': '1234',
        'buildInstallType': 2
    }
    request_upload_info = requests.post(url_pgy, param_upload_info)
    content_upload_info = request_upload_info.content.decode('utf-8')
    dic_upload_info = json.loads(content_upload_info)
    print('蒲公英上传参数{}'.format(dic_upload_info))

    if dic_upload_info['code'] == 0:
        print('开始上传蒲公英')
        key = dic_upload_info['data']['key']
        print('key:\n{}'.format(key))
        endpoint = dic_upload_info['data']['endpoint']
        print('endpoint:\n{}'.format(endpoint))
        x_token = dic_upload_info['data']['params']['x-cos-security-token']
        print('token:\n{}'.format(x_token))
        signature = dic_upload_info['data']['params']['signature']
        print('signature:\n{}'.format(signature))
        second_key = dic_upload_info['data']['params']['key']
        print('secondkey:{}'.format(second_key))

        line1 = 'curl -S -# -w \'%{{http_code}}\n\' --form-string \'key={}\' '.format(key)
        line2 = '--form-string \'signature={}\' '.format(signature)
        line3 = '--form-string \'x-cos-security-token={}\' '.format(x_token)
        line4 = '-F \'file=@{}\' {}'.format(ipa_path, endpoint)

        upload_curl = line1 + line2 + line3 + line4
        os.system(upload_curl)
        pass
    if dic_upload_info['code'] == 0:
        # 拼接 pgy 下载地址
        key = dic_upload_info['data']['key']
        pgy_download: str = 'https://www.pgyer.com/{}'.format(str(key).split('.')[0])
        print(f'蒲公英下载地址{pgy_download}')
        return pgy_download
    return '暂无'

调用该方法, 最终会获取到第三方平台的安装地址

iShot_2022-10-09_14.27.14.png

iShot_2022-10-09_14.23.47.png

第六步:

内网和外网下载链接都有了, 再结合 webhook 功能将消息转发到工作群当中, 不同平台的 webhook 参照各个平台的文档来,这里以钉钉为例

官方文档: open.dingtalk.com/document/ro…

方法代码:

# 发送钉钉群
def send_message(git_log: str, inner_url: str, outer_url: str, path_name: str):
    msg = f'此次打出的包为:\n {project_name} iOS 内部包 \n ' \
          f'打包日期: {path_name} \n ' \
          f'下载地址1: {inner_url} (需要链接公司 WIFI 才可访问) \n ' \
          f'下载地址2: {outer_url}\n 访问密码: 1234 \n ' \
          f'更改日志 \n {git_log} '
    print(msg)
    send_token = '群机器人 token'
    url_dingding = 'https://oapi.dingtalk.com/robot/send'
    dingding_param = {
        'msgtype': 'text',
        'text': {"content": msg}
    }
    requests.post(url=url_dingding, params={'access_token': send_token}, json=dingding_param)
    pass

最终这些信息会自动发送到你设置的群里面

image.png

调用全部方法

git_log = git_operation()
pod_operation()
iphoneos_version()
ipa_file_name = archive_operation()
inner_download_url = local_install(ipa_file_name, git_log, current_version())
outer_download_url = ipa_upload_pgy(f'{export_ipa_path}/{project_name}{ipa_file_name}/{project_name}.ipa')
send_message(git_log=git_log, inner_url=inner_download_url, outer_url=outer_download_url, path_name=ipa_file_name)

至此, Python 文件代码编写完毕, 文章最后会贴上全部代码供参考

第七步:

以上操作都是在 PyCharm 中运行的, 现在在 Jenkins 中创建项目, 通过 Jenkins 来执行该脚本

浏览器输入 http://ip地址:8080 进入 Jenkins

新建任务

iShot_2022-10-09_15.26.26.png

填写信息

iShot_2022-10-09_15.27.23.png

选择参数化

iShot_2022-10-09_15.29.08.png

字符参数

iShot_2022-10-09_15.30.05.png

填写打包分支参数

image.png

在 Build Steps 选择 Shell

iShot_2022-10-09_15.31.56.png

填写脚本执行路径, 然后保存

iShot_2022-10-09_15.34.50.png

配置好之后将最开始定义的全局变量 git_branch 改成从 Jenkins 中读取

# 需要打包的分支
git_branch = sys.argv[1]

在项目源码中创建 package.py , 将刚才写的所有代码复制进去, push 到仓库

image.png

再打开 Jenkins, 点击刚才创建的项目

image.png

点击构建

iShot_2022-10-09_15.36.54.png

输入参数

image.png

点击开始构建

点击小圆圈可以查看控制台, 如果进程失败会控制台出现失败原因, 根据失败原因可以排查问题

iShot_2022-10-09_16.09.36.png

我这里前几次都失败了, 报了找不到库的报错

解决办法: www.it610.com/article/140…

最终是打包成功, 由于这是测试包, 没啥内容, 如果是真实项目, 打包时间由根据你项目的大小和项目的进度决定

iShot_2022-10-09_16.14.44.png

试试打包链接和蒲公英链接, 均可安装

整理下整个流程, 如何创建一个新项目的打包服务:

1, 打包机的维护人员将项目的仓库 clone 到创建的源码文件夹中, 将源码路径给项目开发者

2, 项目开发者拿到路径后在项目源码根目录下添加 package.py 文件, 添加全部代码, 根据项目信息填写参数, 包括源码路径(打包机人员给), 项目名称, bundleid

3, 项目开发者将 ExportOptions.plst 文件放在根目录下, 同时在根目录下创建 certificate 文件夹, 将 p12 和 profile 文件放入该文件夹内

4, 项目开发者将上面两步新增的内容 push 到仓库, 双击 certificate 里面的文件

5, 打包机维护人员 pull 一下代码, 在 Jenkins 新建 item

6, 建好之后内部人员访问 http:// 打包机ip地址:8080 , 通过创建的 item 就可以随时随地的构建项目, 手机也可以

附上 Python 全部代码, 填写 <---XXX---> 内参数即可用

# coding: utf-8

import sys
from git.repo import Repo
import os
import time
import qrcode
import requests
import json

# 需要打包的分支
git_branch = sys.argv[1]
# 源码路径
code_path = <----源码路径---->
# 项目名称, xcworkspace 名称
project_name = <----项目名称---->
# Archive 存放路径, 根据本地 Xcode 路径设置
xcode_archive_path = '/Users/petter/Library/Developer/Xcode/Archives/'
# ipa 导出路径, 放在本地服务路径下, 以项目名分类
export_ipa_path = f'/Users/petter/Documents/Package/IPA/{project_name}'
# ipa 导出所需 plist 文件, 放在源码根目录下
export_ipa_plist_path = f'{code_path}/ExportOptions.plist'
# 内部服务器根目录
service_local = 'https://www.projectpackage.xyz'
# 项目 bundle ID
bundle_id = <----bundle id---->
# html 代码, 赋值后写入本地
code_html = """
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
        <title>{TITLE}</title>
        <style>
            h1 {{
                text-align: center;
                font-size: 32px;
                font-weight:bold;
            }}
            p {{
                text-align: center;
            }}
            pre {{
                text-align: center;
            }}
            .round-button {{
                display:block;
                height:64px;
                width:200px;
                line-height:64px;
                border: 2px solid #f5f5f5;
                border-radius: 10px;
                color:#f5f5f5;
                text-align:center;
                text-decoration:none;
                background: #FF4500;
                box-shadow: 0 0 3px gray;
                font-size:18px;
                font-weight:bold;
                margin: auto;
            }}
            .round-button:hover {{
                background: #FF4500;
            }}
            .version {{
                font-size: 18px;
            }}
        </style>
    </head>
    <body>
        <h1>{MESSAGE}</h1>
        <h1>{SUBMESSAGE}</h1>
        <h1>{VERSION}</h1>
        <p><img src="{QR_NAME}"><br></p>
        <p><a href="itms-services://?action=download-manifest&url={MANIFEST_URL}" class="round-button">点击安装</a></p>
        </br>
        <pre>○○○○○○○○○○  git_log:  ○○○○○○○○○○
            {GITLOG}
        </pre>
    </body>
</html>
"""
# 根据 itms-services 要求, 需要提供一个 plist 文件, 格式固定, 把 plist 内容定义成一个变量, 然后根据项目进行赋值后写入本地
code_plist = """
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>items</key>
    <array>
        <dict>
            <key>assets</key>
            <array>
                <dict>
                    <key>kind</key>
                    <string>software-package</string>
                    <key>url</key>
                    <string>{IPA_URL}</string>
                </dict>
            </array>
            <key>metadata</key>
            <dict>
                <key>bundle-identifier</key>
                <string>{BUNDLE_ID}</string>
                <key>bundle-version</key>
                <string>{BUNDLE_VERSION}</string>
                <key>kind</key>
                <string>software</string>
                <key>title</key>
                <string>{TITLE}</string>
            </dict>
        </dict>
    </array>
</dict>
</plist>
"""


# 格式化输出
def format_output(text: str):
    print(text.center(40, '*'))
    pass

# 获取最近10次提交信息
def git_operation():
    repo = Repo(code_path)
    # 还原
    repo.git.reset('--hard')
    # 清除未跟踪文件
    repo.git.clean('-dxf')
    # 切换分支
    repo.git.checkout(git_branch)
    # pull
    repo.git.pull()
    format_output('git 完成')
    # 获取 log 信息
    commit_log = repo.git.log('--pretty={"%an","%s","%cd"}', max_count=10, date='format:%Y-%m-%d %H:%M')
    print(commit_log)
    return commit_log

# pod
def pod_operation():
    os.chdir(code_path)
    pod_install = 'pod update --verbose --no-repo-update'
    os.system(pod_install)
    format_output('pod 完成')
    pass

# 读取 iPhoneos 打包参数, 此参数根据 Xcode 版本而变化, 可通过 xcodebuild -showsdks 指令查询 iOS sdk 名称
def iphoneos_version():
    sdk_path = '/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/'
    file_list = os.listdir(sdk_path)
    file_list_last = file_list[len(file_list) - 1]
    file_list_last_com = file_list_last.split('iPhoneOS')
    file_list_last_com.remove('')
    file_list_last_com_sdk = file_list_last_com[0]
    iphoneos_sdk_com = file_list_last_com_sdk.split('.sdk')
    iphoneos_sdk_version = iphoneos_sdk_com[0]
    format_output(f'sdkVersion: {iphoneos_sdk_version}')
    return iphoneos_sdk_version


# archive 并 export
def archive_operation():
    time_start = time.localtime()
    time_start_count = time.time()
    # 先 clean 一下
    command_clean = f'xcodebuild clean -workspace {project_name}.xcodeproj/project.xcworkspace -scheme {project_name}'
    os.system(command_clean)
    work_space = f'{project_name}.xcworkspace'
    # archive存放文件路径, 对齐 xcode 手动 archive 时间格式:2022-05-31
    archive_path_name = time.strftime('%Y-%m-%d', time_start)
    # 当天多次 archive 均放在此文件文件文件夹内
    archive_put_path = xcode_archive_path + archive_path_name
    # 导出文件临时命名, archive 之后重命名
    archive_generate_file_path = '{}-{:02d}-{:02d}.{:02d}.{:02d}'. \
        format(time_start.tm_year,
               time_start.tm_mon,
               time_start.tm_mday,
               time_start.tm_hour,
               time_start.tm_min)

    # 生成 archive 的文件名, 对齐手动打包命名规范
    # eg: XXX.2022-05-01 15.13, 单数 0 自动补齐
    archive_generate_file_path_change = '{}-{:02d}-{:02d}, {:02d}.{:02d}'. \
        format(time_start.tm_year,
               time_start.tm_mon,
               time_start.tm_mday,
               time_start.tm_hour,
               time_start.tm_min)

    # 判断目录是否存在, 如果没有, 先创建
    if not os.path.exists(archive_put_path):
        os.makedirs(archive_put_path)
    # 生成 archive 文件存放路径, 还是对齐 xcode 手动打包路径
    # 打包参数, 可通过 Application 路径拼接生成, 可通过指令 xcodebuild -showsdks
    # 如果通过指令查询, 需要 xcode 版本保持更新, 否则可能失败
    archive_param_iphone_sdk = f'iphoneos{iphoneos_version()}'
    # 打包类型, 可通过  xcodebuild -list 查询
    archive_param_type = 'Release'
    # 打包指令
    archive_command_base = ' -workspace ' + \
                           work_space + \
                           ' -scheme ' + project_name + \
                           ' -configuration ' + archive_param_type + \
                           ' -sdk ' + \
                           archive_param_iphone_sdk
    archive_command_clean = 'xcodebuild clean' + archive_command_base
    archive_command_path = ' -archivePath ' + archive_put_path + '/' + archive_generate_file_path
    archive_command_generate = 'xcodebuild archive' + archive_command_path + archive_command_base

    print(archive_command_generate)
    os.system(archive_command_clean)
    os.system(archive_command_generate)

    # 导出 ipa 包
    # 创建 ipa 包文件夹, 以项目名和时间创建文件夹
    export_archive_file = export_ipa_path + '/' + f'{project_name}' + archive_generate_file_path
    # 如果文件夹不存在先创建
    if not os.path.exists(export_ipa_path):
        os.makedirs(export_ipa_path)
    if not os.path.exists(export_archive_file):
        os.makedirs(export_archive_file)

    # ipa 路径 (本地安装需要用)
    export_archive_path = archive_put_path + '/' + archive_generate_file_path + '.xcarchive'
    ipa_command = f'xcodebuild -exportArchive' \
                  f' -archivePath {export_archive_path} ' \
                  f'-exportPath {export_archive_file} ' \
                  f'-exportOptionsPlist {export_ipa_plist_path}'
    print(ipa_command)
    os.system(ipa_command)

    # 更改名称, 对齐 xcode 手动打包格式
    archive_file_list = os.listdir(archive_put_path)
    for archive_name in archive_file_list:
        if archive_generate_file_path in archive_name:
            os.rename(f'{archive_put_path}/{archive_name}',
                      f'{archive_put_path}/{project_name} {archive_generate_file_path_change}.xcarchive')

    time_end = time.time()
    archive_time_spend = int((time_end - time_start_count) / 60)
    format_output(f'本次导出共耗时{archive_time_spend}min')
    # 返回 ipa 文件夹名称, 供本地安装使用
    format_output(f'文件夹路径: {archive_generate_file_path}')
    return archive_generate_file_path

# 读取项目当前设置的版本号
def current_version():
    file_xcode_project = open(f'{code_path}/{project_name}.xcodeproj/project.pbxproj')
    lines_code_project = file_xcode_project.readlines()
    for content in lines_code_project:
        if 'MARKETING_VERSION' in content:
            splits = content.split('=')
            info = splits[1].split(';')
            if info[0].strip() != '1.0':
                return info[0].strip()
    file_xcode_project.close()
    pass

# 生成静态网页
def local_install(file_name: str, log: str, version: str):
    # 拼接文件名
    path_name = project_name + file_name
    # 复制 plist 内容, 主要是将 ipa 文件放在 https 域名下
    install_url = f'{service_local}/{project_name}/{project_name}{file_name}/{project_name}.ipa'
    plist_content = code_plist.format(IPA_URL=install_url, BUNDLE_ID=bundle_id, BUNDLE_VERSION=version, TITLE=project_name)
    # 写入 plist
    with open(f'{export_ipa_path}/{path_name}/install.plist', 'w') as file:
        file.write(plist_content.strip())
    # 生成二维码, 放在对应项目下
    qr_image = qrcode.make(f'{service_local}/{project_name}/{path_name}/install.html')
    qr_image.save(r'{}/{}/download.png'.format(export_ipa_path, path_name))
    # HTML 参数复制, 主要是将 install.plist 位置告诉 Safari
    main_url = f'{service_local}/{project_name}/{path_name}/install.plist'
    html_content = code_html.format(TITLE=project_name,
                                    MESSAGE=f'{project_name}',
                                    SUBMESSAGE=file_name,
                                    VERSION=f'版本号:{version}',
                                    MANIFEST_URL=main_url,
                                    QR_NAME='download.png', GITLOG=log)
    with open(f'{export_ipa_path}/{path_name}/install.html', 'w') as file:
        file.write(html_content)

    dowload_url = f'{service_local}/{project_name}/{path_name}/install.html'
    print(f'内网下载路径: {dowload_url}')
    return dowload_url

# 上传 ipa 到蒲公英, 按照蒲公英 API 文档来, 生成外网下载地址
def ipa_upload_pgy(ipa_path: str):
    print(ipa_path)
    url_pgy = 'https://www.pgyer.com/apiv2/app/getCOSToken'
    api_key = '4c484ffe8871cc6a5b21b02832c05a16'
    param_upload_info = {
        '_api_key': api_key,
        'buildType': 'ios',
        'buildPassword': '1234',
        'buildInstallType': 2
    }
    request_upload_info = requests.post(url_pgy, param_upload_info)
    content_upload_info = request_upload_info.content.decode('utf-8')
    dic_upload_info = json.loads(content_upload_info)
    print('蒲公英上传参数{}'.format(dic_upload_info))

    if dic_upload_info['code'] == 0:
        print('开始上传蒲公英')
        key = dic_upload_info['data']['key']
        print('key:\n{}'.format(key))
        endpoint = dic_upload_info['data']['endpoint']
        print('endpoint:\n{}'.format(endpoint))
        x_token = dic_upload_info['data']['params']['x-cos-security-token']
        print('token:\n{}'.format(x_token))
        signature = dic_upload_info['data']['params']['signature']
        print('signature:\n{}'.format(signature))
        second_key = dic_upload_info['data']['params']['key']
        print('secondkey:{}'.format(second_key))

        line1 = 'curl -S -# -w \'%{{http_code}}\n\' --form-string \'key={}\' '.format(key)
        line2 = '--form-string \'signature={}\' '.format(signature)
        line3 = '--form-string \'x-cos-security-token={}\' '.format(x_token)
        line4 = '-F \'file=@{}\' {}'.format(ipa_path, endpoint)

        upload_curl = line1 + line2 + line3 + line4
        os.system(upload_curl)
        pass
    if dic_upload_info['code'] == 0:
        # 拼接 pgy 下载地址
        key = dic_upload_info['data']['key']
        pgy_download: str = 'https://www.pgyer.com/{}'.format(str(key).split('.')[0])
        print(f'蒲公英下载地址{pgy_download}')
        return pgy_download
    return '暂无'

# 发送钉钉群
def send_message(git_log: str, inner_url: str, outer_url: str, path_name: str):
    msg = f'此次打出的包为:\n {project_name} iOS 内部包 \n ' \
          f'打包日期: {path_name} \n ' \
          f'下载地址1: {inner_url} (需要链接公司 WIFI 才可访问) \n ' \
          f'下载地址2: {outer_url}\n 访问密码: 1234 \n ' \
          f'更改日志 \n {git_log} '
    print(msg)
    send_token = '群机器人 token'
    url_dingding = 'https://oapi.dingtalk.com/robot/send'
    dingding_param = {
        'msgtype': 'text',
        'text': {"content": msg}
    }
    requests.post(url=url_dingding, params={'access_token': send_token}, json=dingding_param)
    pass

git_log = git_operation()
pod_operation()
iphoneos_version()
ipa_file_name = archive_operation()
inner_download_url = local_install(ipa_file_name, git_log, current_version())
outer_download_url = ipa_upload_pgy(f'{export_ipa_path}/{project_name}{ipa_file_name}/{project_name}.ipa')
send_message(git_log=git_log, inner_url=inner_download_url, outer_url=outer_download_url, path_name=ipa_file_name)

总结:

1,整个过程不算复杂, 但是如果一台新的 Mac , 对环境配置有点复杂, 需要安装 Xcode, cocoapods, homebrew, 替换系统自带旧版本 Python , JAVA , 等等, 出现了很多次 ssl connect failed 错误

2, Python 语法只会皮毛, 代码写的不怎么规范, 后期待优化

3, 脚本内容缺少定时清理过期 ipa 包, 时间长了内存会满, 后期加上

原创内容, 转载需注明!!!