告别蒲公英-教你自建本地https服务搭建iOS itms-services扫码安装

7,070 阅读5分钟

前言

上周蒲公英在毫无征兆的情况下,把官网的域名给改了。本以为各种接口改成最新的域名就能接着用,但实际上jenkins打包自动上传到蒲公英成功后,iOS扫码安装失效了。众所周知苹果iOS包的网页扫码安装是由itms-services服务实现,但是前提是域名需要支持https,我们本地已经有一台jenkis打包机了,那我本地打完包后,把ipa包放到本地http服务,不经过域名,是否也能实现内网下扫码安装呢?答案接着往下看

什么是itms-services

itms-service是apple为iOS用户提供网页、扫码安装方式所使用的协议。这种方式不需要经过APP Store分发也可以安装iOS App。(前提是你的iPhone设备ID已经加入进去打包的开发者账号、或者使用企业账号(现在基本绝种))。

items-services协议本质就是访问一个https的远程plist描述文件,这个plist记载了app的信息和ipa包下载地址。 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>http://192.168.1.2/xxx.ipa</string>

</dict>

</array>

<key>metadata</key>

<dict>

<key>bundle-identifier</key>

<string>com.zxh.xpzc</string>

<key>bundle-version</key>

<string>1.0.0</string>

<key>kind</key>

<string>software</string>

<key>title</key>

<string>需要下载这个标题</string>

</dict>

</dict>

</array>

</dict>

</plist>

传统方案一般是打包机打完包后,生成上面的plist文件,把ipa上传cdn服务器上,然后plist文件也上传到cdn即可。

用户只要扫码或者浏览器打开:


itms-services://?action=download-manifest&url=https://XXXXXXXX/app.plist

就可以扫码安装。由于itms-services需要使用https链接,很多人默认以为一定要有一个域名加上配了https才行。实际上如果我本地开启一个https服务,直接https://192.168.3.x/install.plist是否可以呢?

搭建本地https服务

搭建本地https服务非常简单,我们只需要brew安装http-server


brew install http-server

在Mac下使用http-server开启HTTP服务后,要配置HTTPS,需要执行以下步骤:

  1. 生成SSL证书和私钥:可以使用openssl命令生成自签名的SSL证书和私钥。打开终端,执行以下命令:

openssl req -nodes -new -x509 -keyout server.key -out server.crt

这将生成一个名为server.key的私钥文件和一个名为server.crt的证书文件。

  1. 启动HTTP服务并配置HTTPS:在终端中,切换到你要启动HTTP服务的目录下,然后执行以下命令:

http-server -S -C server.crt -K server.key

这将启动一个使用HTTPS的HTTP服务。-S参数表示启用HTTPS,-C参数指定证书文件,-K参数指定私钥文件。

  1. 在浏览器中访问:打开浏览器,输入https://localhost:8080(假设HTTP服务监听在8080端口),即可访问使用HTTPS的本地HTTP服务。

请注意,由于自签名证书不受信任的CA颁发,浏览器可能会显示警告信息。如果需要使用受信任的证书,可以申请并购买来自公共CA的SSL证书,并将其配置到HTTP服务器中。

上面提到由于本地https是不受信任的,这导致如果ipa文件放到https服务下是不能直接下载的,所以我们这里取巧,同时开启https和http服务,https为了触发itms-servics协议,ipa文件则放到http服务下,这样就可以完整的实现本地iOS安装包扫码安装。同时启动http和https两个服务命令如下:


http-server -p 8081 & http-server -p 8082 -S -C server.crt -K server.key

本地自建服务兼容jenkins

之前编写的jenkins自动打包是打完包后分发是基于蒲公英的,如果要支持本地扫码安装那脚本需要兼容,脚本如下,这里只说明如何实现生成itms-services的plist文件,以及生成二维码,如何实现自动打包不做说明。

打完包生成了iPA后,我们把ipa挪到到http服务写入plist如下:


def write_local_itms_services(rootPath,ipa_directory,ipa_path):

pwd='python3 ./create_config.py --root_path {} --app_directory {} --app_url {} --build_version {}'.format(rootPath,ipa_directory,ipa_path,build_version)

os.system(pwd)


ipaPath=targerIPA_parth + '/' + ipa_filename + '/' + 'App.ipa'

httpIpaPath='http://192.168.41.3:8081' + '/' + ipa_filename + '/' + 'App.ipa'

write_local_itms_services(targerIPA_parth,ipa_filename,httpIpaPath)

生成plist脚本:


#!usr/bin/python3

# -*- coding:utf-8 -*-

import os

import sys

import plistlib

from datetime import datetime, timedelta, timezone

import argparse

parser = argparse.ArgumentParser()

parser.add_argument('--root_path', default='')

parser.add_argument('--app_directory', default='')

parser.add_argument('--app_url', default='')

parser.add_argument('--build_version', default=0)

parser.add_argument('--app_name', default='')

args = parser.parse_args()

JFAppUrl = args.app_url

JFAppDirectory = args.app_directory

JFBuildVersion = args.build_version

JFPackageRootPath = args.root_path

JFAppName = args.app_name

print(args)

def readInfoPlist(inputFilePath):

if not os.path.isfile(inputFilePath):

return None

with open(inputFilePath, 'rb') as fp:

infoPList = plistlib.load(fp)

print(infoPList)

return infoPList

def writePlist(outFilePath,fileName,packageName,title):

pl = {'items': [{

'assets': [

{'kind': 'software-package', 'url': JFAppUrl}, 

{'kind': 'display-image', 'url': 'https://xxx.jpg'}, 

{'kind': 'full-size-image', 'url': 'https://xxx.jpg'}], 

'metadata': 

{'bundle-identifier': packageName, 'bundle-version': JFBuildVersion, 'kind': 'software', 'title': title}

}]}

with open(outFilePath, 'wb') as fp:

plistlib.dump(pl, fp)

plistFileOut = JFPackageRootPath + "/"+ JFAppDirectory + "/" + "iOS_" + JFBuildVersion + ".plist"

infoPList = None

ipaFileName = 'App'

packageName = 'com.xxxx.xxxx'

appVersion = JFBuildVersion

title = JFAppName + '_' + JFBuildVersion

#写plist文件,用来网页安装

print(ipaFileName,packageName,appVersion,title)

writePlist(plistFileOut,ipaFileName,packageName,title)

写入plist后,我们生成二维码扫码安装,然后把二维码发到飞书(也是脚本实现)即可。

生成二维码脚本:


def write_qr_code(rootPath,ipaDirName):

saveRqCodeFileName="rqcode_" + build_number + ".png"

saveFilePath = rootPath + '/' + ipaDirName + '/' + saveRqCodeFileName

pwd = 'python3 ./createRqCode.py --build_number {} --qrcode_save_file {} --version {} --dir_name {}'.format(build_number,saveFilePath,build_version,ipaDirName)

print(pwd)

os.system(pwd)

return saveRqCodeFileName

write_local_itms_services(targerIPA_parth,ipa_filename,httpIpaPath)

imgName=write_qr_code(targerIPA_parth,ipa_filename)

//生成二维码图片保存路径,推送到飞书

imgPath=ipaPath=targerIPA_parth + '/' + ipa_filename + '/' + imgName


生成带itms-services协议二维码图片脚本:


from PIL import Image, ImageFont, ImageDraw

import os, sys

import argparse

parser = argparse.ArgumentParser()

parser.add_argument('--build_number', default=0)

parser.add_argument('--app_name', default='')

parser.add_argument('--version', default='')

parser.add_argument('--dir_name', default='')

parser.add_argument('--qrcode_save_file', default='')

args = parser.parse_args()

JFBuildNumber = args.build_number

JFAppName = args.app_name

JFVersion = args.version

JFDirName = args.dir_name

JFQRCodeSaveFile = args.qrcode_save_file

plistFileName= "iOS_" + JFVersion + ".plist"

InstallUrl='itms-services://?action=download-manifest&url=https://192.168.41.3:8082/' + JFDirName + '/' + plistFileName

pkEnvStr = "测试"

textContent = JFBuildNumber

packageName = JFAppName + pkEnvStr

imagSize = 512

imagSaveFile = "/tmp/number.png"

JFColor = (0,255,0,255)

path=os.getcwd()+'/script/'

font = ImageFont.truetype(path + "FZZZHONGJW.ttf", size=int(imagSize / 6))

img = Image.new("RGBA",(imagSize,imagSize),color=0)

# 写文字

draw = ImageDraw.Draw(img)

textSize = draw.textbbox((0,0),text=packageName, font=font)

draw.text(xy=((imagSize- textSize[2]) / 2, (imagSize - textSize[3]) / 4), text=packageName, font=font,fill=JFColor)

textSize = draw.textbbox((0,0),text=JFBuildNumber, font=font)

draw.text(xy=((imagSize- textSize[2]) / 2, (imagSize - textSize[3]) / 1.5), text=JFBuildNumber, font=font,fill=JFColor)

img.save(imagSaveFile)

# 输出

# img.show()

print(JFQRCodeSaveFile)

from MyQR import myqr

version, level, qr_name = myqr.run(

words=InstallUrl,

version=1,

level='H',

picture=imagSaveFile,

colorized=True,

contrast=1.0,

brightness=1.0,

save_name=JFQRCodeSaveFile,

save_dir=os.getcwd()

)

结论

IMG_4122 2.jpg

脚本根据不同环境情况可能要略微调整,仅供参考,就这样成功生成二维码发送到飞书后,扫码即可实现本地https服务安装测试iOS包啦(前提是手机连接的wifi和打包机开启的https服务在同一个内网下),再也不用蒲公英分发啦,打包速度提升100s+(不需要上传到蒲公英服务器,本地上传时间为0)。