用一台Mac Mini部署了Jenkins之后,打完包要下载到自己电脑用iTunes安装。
因此决定用Go在Mini 上也配置一个OTA在线安装环境
1. 下载安装Go
https://golang.org/dl/
默认安装路径在/usr/local/go,添加环境
vi ~/.bash_profile
添加内容 export PATH=$PATH:/usr/local/go/bin
source ~/.bash_profile
2. 安装Goland
使用量并不大,使用Goland直接run
建一个工程文件夹GoProject,下面都在这个文件夹下
新建ota.go
菜单->run->edit configurations 添加go build
接下来参考https://blog.csdn.net/sydnash/article/details/54691878
3.自建https证书
新建文件夹myssl
cd myssl
sudo openssl genrsa -out server.key 1024
sudo openssl req -new -key server.key -out server.csr //填写信息时Common Name为本机ip地址,服务器有域名使用域名
sudo openssl genrsa -out ca.key 1024
sudo openssl req -new -x509 -days 365 -key ca.key -out ca.crt
在myssl中新建文件夹demoCA
在demoCA新建文件夹newcerts,新建文件index.txt和serial,serial添加文本内容:01
生成ca文件
sudo openssl ca -in server.csr -out server.crt -cert ca.crt -keyfile ca.key
如果出现错误
Using configuration from /private/etc/ssl/openssl.cnf
variable lookup failed for ca::default_ca
首先确保安装 openssl。安装: brew install openssl
拷贝文件
sudo cp /usr/local/etc/openssl/openssl.cnf /private/etc/ssl/openssl.cnf
生成证书后,在GoProject中新建文件夹ssl,将ca.crt拷贝一份过来
4. OTA现在必须使用https
固定链接格式读取plist安装
链接:itms-services://?action=download-manifest&url=https://xxx/xxx.plist
Jenkins打包后没有提供ota的plist文件 需要靠自己创建
在GoProject中新建文件夹OtaInstall,用于保存创建的plist文件。同时存放57*57和512*512两张应用icon图片
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>https://xxx/xxx.ipa</string>
</dict>
<dict>
<key>kind</key>
<string>display-image</string>
<key>url</key>
<string>https://xxx/57*57.png</string>
</dict>
<dict>
<key>kind</key>
<string>full-size-image</string>
<key>url</key>
<string>https://xxx/512*512.png</string>
</dict>
</array>
<key>metadata</key>
<dict>
<key>bundle-identifier</key>
<string>应用buildle-id</string>
<key>bundle-version</key>
<string>版本号</string>
<key>kind</key>
<string>software</string>
<key>title</key>
<string>应用名</string>
</dict>
</dict>
</array>
</dict>
</plist>
5. Go实现
package main
import (
"bufio"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"os"
"strings"
)
var installTemplate = `
<!DOCTYPE html>
<html>
<head >
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1, minimum-scale=1, user-scalable=no">
<meta content="telephone=no" name="format-detection"/>
<title>应用名-在线安装</title>
</head>
<style>
html {
width: 100%;
height: 100%
}
body {
background-color: #fafafa;
font-family: "Microsoft YaHei";
color: #0a0909;
-webkit-touch-callout: none;
-webkit-user-select: none;
margin-right:auto;
margin-left:auto;
text-align:center;
}
div, p, header, footer, h1, h2, h3, h4, h5, h6, span, i, b, em, ul, li, dl, dt, dd, body, input, select, form, button {
margin: 0;
padding: 0
}
ul, li {
list-style: none
}
img {
border: 0 none
}
input, img {
vertical-align: middle
}
* {
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
outline: 0;
box-sizing: border-box;
}
a {
text-align:center;
margin-right:auto;
margin-left:auto;
}
h1, h2, h3, h4, h5, h6 {
font-weight: normal;
}
.title {
font-size: 18px;
margin-bottom: 20px;
}
.install {
width: 300px;
height: 40px;
border: 1px solid #ccc;
background: transparent;
border-radius: 6px;
font-size: 14px;
margin-bottom: 10px;
display: block;
margin-right:auto;
margin-left:auto;
}
</style>
<body>
</br></br></br>
<p class="title">iOS应用OTA安装</p>
</br></br>
<a title="iPhone" href="http://ip:1717/static/ca.crt" target="_blank" rel="external nofollow" >
<button class="install">证书信任</button>
</a>
</br></br>
<a href="https://ip:1718/ipalist" target="_blank" rel="external nofollow" >
<button class="install">打包列表</button>
</a>
</body>
</html>
`
var ipalistHtml = `<!DOCTYPE html>
<html>
<head >
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1, minimum-scale=1, user-scalable=no">
<meta content="telephone=no" name="format-detection"/>
<title>应用名-打包列表</title>
</head>
<style>
html {
width: 100%;
height: 100%
}
body {
width: 100%;
height: 100%;
background-color: #fafafa;
font-family: "Microsoft YaHei";
color: #0a0909;
-webkit-touch-callout: none;
-webkit-user-select: none;
}
</style>
<body>
</br></br></br>
`
func main() {
///安装页和列表页,两个静态页面
http.Handle("/install/", http.HandlerFunc(install))
http.Handle("/ipalist/", http.HandlerFunc(ipalist))
///静态文件服务
///将ca证书文件夹开放提供下载 设置安装后 需要在设备 设置-关于本机-证书信任,信任证书
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./ssl"))))
///开放打包存放路径
http.Handle("/ota/", http.StripPrefix("/ota/", http.FileServer(http.Dir("/xxx/Package"))))
///开放存放ota plist文件夹
http.Handle("/otainstall/", http.StripPrefix("/otainstall/", http.FileServer(http.Dir("./OtaInstall"))))
///监听1717端口 提供http服务 用于证书安装
go func() {
http.ListenAndServe(":1717", nil)
}()
///使用之前自建的证书,监听1718端口 提供https服务 用于应用安装
err := http.ListenAndServeTLS(":1718", "./myssl/server.crt", "./myssl/server.key", nil)
if err != nil {
log.Fatal("ListenAndServe:", err)
}
}
///install 页面 证书信任 包列表
func install(w http.ResponseWriter, req *http.Request) {
fmt.Fprint(w, installTemplate)
}
///包列表 从Jenkins打包保存路径读取所有打包文件夹
func ipalist(w http.ResponseWriter, req *http.Request) {
dir_list, e := ioutil.ReadDir("/xxx/Package")
if e != nil {
fmt.Fprintf(w, "get ipa list error:%s", e.Error())
return
}
///文件列表倒序
dir_list = reverse(dir_list)
var html string = ipalistHtml
for _, f := range dir_list {
///不是文件夹过滤掉
if f.IsDir() == false {
continue
}
var name string = f.Name()
subList, _ := ioutil.ReadDir("/xxx/Package/" + name)
///如果文件夹里没有子文件 则忽略(这里只是粗略的过滤,过滤掉空文件夹)
if len(subList) == 0 {
continue
}
///从.xcarchive文件里读取info.plist获取version和build
var version, build = getIPAVersionWithFileName(name)
///如果没有ota在线安装的plist配置文件则创建一个
createOTAPlistIfNeededWithFileName(name)
///作为一个a标签拼接到html
var a = "<div style=\"text-align:center;\"><a" + " href=\"itms-services://?action=download-manifest&url=https://ip:1718/otainstall/"+ name + ".plist\" >" + name + " [" + version + "]" + "[build " + build + "]" + "</a></div> </br>"
html += a
}
html += "</body></html>"
///显示html静态页面
fmt.Fprintf(w, html)
}
///将文件列表倒序
func reverse(s [] os.FileInfo) []os.FileInfo {
for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
s[i], s[j] = s[j], s[i]
}
return s
}
///判断是否已经有ota安装plist文件,如果没有创建则创建并写入内容
func createOTAPlistIfNeededWithFileName(filename string) {
var path = "./OtaInstall/" + filename + ".plist"
exist, err := pathExists(path)
if err != nil {
return
}
if exist {
return
}
var version,_ = getIPAVersionWithFileName(filename)
///拼接plist内容
var content = `
<?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>https://ip:1718/ota/`+ filename + "/" + filename + `.ipa</string>
</dict>
<dict>
<key>kind</key>
<string>display-image</string>
<key>url</key>
<string>https://ip:1718/otainstall/icon57.png</string>
</dict>
<dict>
<key>kind</key>
<string>full-size-image</string>
<key>url</key>
<string>https://ip:1718/otainstall/icon512.png</string>
</dict>
</array>
<key>metadata</key>
<dict>
<key>bundle-identifier</key>
<string>com.xxx.xxx</string>
<key>bundle-version</key>
<string>` + version + `</string>
<key>kind</key>
<string>software</string>
<key>title</key>
<string>应用名</string>
</dict>
</dict>
</array>
</dict>
</plist>`
f, err := os.Create(path) //创建文件
if err != nil{
return
}
///写入文件
io.WriteString(f,content)
}
///读取包version和build
///内容比较简单,用比较笨的办法做的
///一行一行读取 找到<key>CFBundleShortVersionString</key>和<key>CFBundleVersion</key>
func getIPAVersionWithFileName(filename string)(v string, build string){
var path = "/xxx/Package/" + filename + "/xxx.xcarchive/Info.plist"
fi, err := os.Open(path)
if err != nil {
fmt.Printf("Error: %s\n", err)
return "1.0.0","1"
}
defer fi.Close()
br := bufio.NewReader(fi)
var version string = "1.0.0"
var buildNum string = "1"
for {
a, _, c := br.ReadLine()
if c == io.EOF {
break
}
var text = string(a)
if strings.Contains(text, "<key>CFBundleShortVersionString</key>") {
a, _, _ := br.ReadLine()
text = string(a)
var sub string = strings.Split(text,"<string>")[1]
version = strings.Split(sub, "</string>")[0]
}
if strings.Contains(text, "<key>CFBundleVersion</key>") {
a, _, _ := br.ReadLine()
text = string(a)
var sub string = strings.Split(text,"<string>")[1]
buildNum = strings.Split(sub, "</string>")[0]
}
}
return version, buildNum
}
///判断文件或者文件夹是否存在
func pathExists(path string) (bool, error) {
_, err := os.Stat(path)
if err == nil {
return true, nil
}
if os.IsNotExist(err) {
return false, nil
}
return false, err
}
运行后,在同一局域网络下就可以使用了
Jenkins打包后刷新列表页,第一次因为写plist文件比较慢一点,然后就可以选择安装了
代码耦合的,路径写在代码里,自行解耦合