用一臺Mac Mini部署了Jenkins之後,打完包要下載到自己電腦用iTunes安裝。
因此決定用Go在Mini 上也配置一個OTA在線安裝環境
1. 下載安裝Go
默認安裝路徑在/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 lang="en">
<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">
<button class="install">證書信任</button>
</a>
</br></br>
<a href="https://ip:1718/ipalist">
<button class="install">打包列表</button>
</a>
</body>
</html>
`
var ipalistHtml = `<!DOCTYPE html>
<html>
<head lang="en">
<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文件比較慢一點,然後就可以選擇安裝了
代碼耦合的,路徑寫在代碼裏,自行解耦合