天天看点

本地部署iOS应用OTA安装 Go + Goland详细实现步骤

用一台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文件比较慢一点,然后就可以选择安装了

代码耦合的,路径写在代码里,自行解耦合