搭建ipa內(nèi)網(wǎng)分發(fā)(OTA)

內(nèi)測(cè)包的分發(fā)懈万,前前后后也使用了很多方案凉逛,之前使用fir、pgyer捆姜,后來(lái)看到了開源的zealot妆够,可以部署在內(nèi)部服務(wù)器识啦,用著挺好。因?yàn)槭窃谕饩W(wǎng)服務(wù)器上神妹,所以隨著ipa包的變大及公司外網(wǎng)限速颓哮,每次安裝內(nèi)測(cè)包要5分鐘+...

因資源有限,就自己用打包機(jī)器(Mac mini)完成了整套配置

主要包括:

  • 自動(dòng)化打包產(chǎn)出ipa(基于Jenkins + fastlane + 自建zealot 已經(jīng)穩(wěn)定使用很久了 )
  • TLS證書 (支持ip訪問的自簽名證書鸵荠,生成腳本下面有給出)
  • 支持https的服務(wù)器(選擇miniserver 輕量易用)
  • 管理腳本 (基于fastlane 冕茅、 ruby,下面有給出)
itms-services://?action=download-manifest&url=https://xxxxxxx.plist

分發(fā)ipa蛹找,肯定離不開這個(gè)協(xié)議姨伤,plist文件的內(nèi)容也是固定的格式,這里需要注意的是庸疾,這個(gè)plist文件的url必須是https的乍楚,至于plist內(nèi)部的ipa文件的url,其實(shí)http也是沒有問題的届慈。

  • plist_URL (https) + ipa_URL (https) : 可以安裝徒溪,但必須要信任簽名CA證書忿偷,否則安裝失敗
  • plist_URL (https) + ipa_URL (http) : 可以安裝,無(wú)需信任自簽名CA證書 (我選用了這個(gè)組合臊泌,比較省事)注意:在iOS12這個(gè)組合無(wú)法安裝鲤桥,我之前測(cè)試用的系統(tǒng)版本比較高,如果想支持<=iOS12的測(cè)試機(jī)渠概,建議還是使用上面雙https的組合
自簽名證書(ip)

這里卡了很久茶凳,生成了很多證書都有問題,附上最終的腳本

#!/bin/bash

#創(chuàng)建根密鑰
openssl ecparam -out ROOT_CA_PRIVATEKEY.key -name secp384r1 -genkey

#創(chuàng)建根證書CSR
openssl req -new -sha256 -key ROOT_CA_PRIVATEKEY.key -out ROOT_CA_CSR.csr -subj "/C=CN/ST=SH/L=PD/OU=XYZ_iOS/O=XYZ_iOS/CN=XYZ_IOS_CA"

#創(chuàng)建一個(gè) CA 根證書的配置文件 
ROOT_CA_Path="./ROOT_CA.cnf"
(
cat << EOF
basicConstraints=critical,CA:TRUE
nsComment = "This Root certificate was generated by dadadongL"
keyUsage=critical, keyCertSign
subjectKeyIdentifier=hash
EOF
) > $ROOT_CA_Path

# 創(chuàng)建自簽名CA
openssl x509 -req -sha256 -days 3650 -extfile $ROOT_CA_Path -in ROOT_CA_CSR.csr -signkey ROOT_CA_PRIVATEKEY.key -out ROOT_CA_CERT.crt


# ??????自簽名的ip 記得改成自己的??????
ip_server="172.18.41.180"

# 創(chuàng)建證書的密鑰 和 CSR 文件
openssl req -newkey rsa:2048 -nodes -subj "/C=CN/ST=SH/O=XYZ_iOS/OU=XYZ_iOS/CN=$ip_server" -keyout server-key.key -out server-csr.csr

# 2.1 創(chuàng)建一個(gè)配置文件  很重要!! 不然瀏覽器任然會(huì)提示不安全 NET::ERR_CERT_COMMON_NAME_INVALID
# 需要把簽發(fā)的域名 或者IP地址 填到 [alt_names] 里面
ssl_cnf_path="./ssl.cnf"
(
cat << EOF
authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
subjectAltName = @alt_names

[alt_names]
IP.1 = $ip_server
EOF
) > $ssl_cnf_path

# 簽發(fā) 證書有效期最長(zhǎng)為13個(gè)月 (398 天), 不然瀏覽器會(huì)顯示不安全
openssl x509 -req -CA ROOT_CA_CERT.crt -CAkey ROOT_CA_PRIVATEKEY.key -CAcreateserial -days 365 -sha256 -extfile $ssl_cnf_path -in server-csr.csr -out server-crt.crt

# 校驗(yàn)
openssl verify -CAfile ROOT_CA_CERT.crt server-crt.crt

ROOT_CA_CERT.crt 自簽名CA證書 (設(shè)備安裝&信任了這個(gè)根證書播揪,其簽發(fā)的證書都不會(huì)再被瀏覽器警告)
./server-crt.crt 待使用的證書
./server-key.key 待使用的證書私鑰

服務(wù)器搭建

這個(gè)有很多方案都可以慧妄,Mac自帶的Apache或者Nginx...
我選用了這個(gè) https://github.com/svenstaro/miniserve,直接映射磁盤指定目錄剪芍,并且輕量級(jí)塞淹。
因?yàn)橥瑫r(shí)需要兩個(gè)https 、http罪裹,所以起了兩個(gè)服務(wù) (記得自行配置開機(jī)自啟動(dòng))

## 端口號(hào) 證書路徑 自行修改
#  nohup commend &  這種格式是為了后臺(tái)運(yùn)行

cd ~/
# 啟動(dòng)https服務(wù)
nohup miniserve -p 8000 -z -v -t "iOS開發(fā)部FTP" -U -u --tls-cert ~/Desktop/online_auto_run/server-crt.crt --tls-key ~/Desktop/online_auto_run/server-key.key  ~/Documents/ftpRoot &

# 啟動(dòng)http服務(wù)
nohup miniserve -p 8001 -v -t "iOS開發(fā)部FTP" -U -u  ~/Documents/ftpRoot &

這樣就可以通過(guò)http://本機(jī)ip:8001/https://本機(jī):8000/進(jìn)行訪問了 (也可以當(dāng)做內(nèi)網(wǎng)的一個(gè)FTP用??)

管理腳本

我是基于fastlane 饱普、 ruby實(shí)現(xiàn)的,代碼都比較簡(jiǎn)單状共,最終會(huì)返回的html頁(yè)面地址套耕,內(nèi)網(wǎng)打開即可
主要步驟:

  1. 拷貝ipa文件到服務(wù)映射的磁盤目錄
  2. 生成plist文件
  3. 生成下載二維碼圖片
  4. 生成下載頁(yè)面html
#  " --- 嘗試 配置本次打包的 內(nèi)網(wǎng)下載配置 --- "
    #入?yún)?pj_scheme: app scheme 用來(lái)生成路徑(不用中文的title是因?yàn)?路徑是url的一部分)
    #入?yún)?pj_env: ”1“/”0“ 用來(lái)生成路徑
    #入?yún)?pj_ver: 主版本號(hào) 用來(lái)生成路徑的一部分 
    #入?yún)?pj_ipa_path: 本次打包的ipa文件路徑 (絕對(duì)路徑)
    #入?yún)?pj_main_bundleID: 主包名即可 用來(lái)生成plist文件
    #入?yún)?pj_title: app名稱 用來(lái)生成plist文件
    #返回值: {pageUrl:  xxxxxxxx.html }
    lane :try_moveIPA_to_serverPath do |variable|
        ipa_server_rootpath = File.expand_path("~/Documents/ftpRoot/itms-services")
        if Dir.exist?(ipa_server_rootpath) == false 
            puts "未檢測(cè)到 ipa_server 目錄,跳過(guò)內(nèi)網(wǎng)下載處理~"
            next {}
        end
        puts "檢測(cè)到 ipa_server 目錄峡继,自動(dòng)配置當(dāng)前ipa支持內(nèi)網(wǎng)下載 ~~~~"

        pj_scheme = variable[:pj_scheme]
        pj_env = variable[:pj_env] == "1" ? "_product_" : "_test_"      
          
        env_Dir_path = File.join(ipa_server_rootpath, pj_scheme, pj_env) 
        if Dir.exist?(env_Dir_path) 
            puts "自動(dòng)刪除 3 天前的包...."
            Dir.entries(env_Dir_path).each do |item|
                # 去除.開頭的文件
                next if File.basename(item).start_with?(".")
                # 
                item_abs_path = File.join(env_Dir_path, item)
                sh("rm", "-R", item_abs_path) if (Time.new - File.mtime(item_abs_path) > (3 * 24 * 3600))
            end
        else
            # 創(chuàng)建文件夾
            sh("mkdir", "-p", env_Dir_path)
        end

        pj_ver = variable[:pj_ver]

        new_pj_ver = "#{pj_ver}-" + Time.new.strftime('%Y-%m-%d_%H-%M')
        current_dir_path = File.join(env_Dir_path, new_pj_ver)
        sh("rm", "-R", current_dir_path) if Dir.exist?(current_dir_path) 
        sh("mkdir", "-p", current_dir_path)


        # 為了在安裝tsl證書前能訪問html冯袍, ci服務(wù)器上跑了兩個(gè)服務(wù)
        http_server_domain = "http://本機(jī)IP:8001/itms-services"
        https_server_domain = "https://本機(jī)IP:8000/itms-services"
        root_CA_URL = "http://本機(jī)IP:8001/CA/ROOT_CA_CERT.crt"

        # copy過(guò)來(lái)ipa
        puts "copy ipa 文件...."
        sh("cp", "-f", File.expand_path(variable[:pj_ipa_path]), "#{current_dir_path}/ipa.ipa")
        ipa_URL = File.join(http_server_domain, pj_scheme, pj_env, new_pj_ver, "ipa.ipa")

        # 生成plist
        puts "生成mainfest.plist文件...."
        plist_base64 = "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPCFET0NUWVBFIHBsaXN0IFBVQkxJQyAiLS8vQXBwbGUvL0RURCBQTElTVCAxLjAvL0VOIiAiaHR0cDovL3d3dy5hcHBsZS5jb20vRFREcy9Qcm9wZXJ0eUxpc3QtMS4wLmR0ZCI+CjxwbGlzdCB2ZXJzaW9uPSIxLjAiPgo8ZGljdD4KCTxrZXk+aXRlbXM8L2tleT4KCTxhcnJheT4KCQk8ZGljdD4KCQkJPGtleT5hc3NldHM8L2tleT4KCQkJPGFycmF5PgoJCQkJPGRpY3Q+CgkJCQkJPGtleT5raW5kPC9rZXk+CgkJCQkJPHN0cmluZz5zb2Z0d2FyZS1wYWNrYWdlPC9zdHJpbmc+CgkJCQkJPGtleT51cmw8L2tleT4KCQkJCQk8c3RyaW5nPl9hcHBfaXBhVVJMXzwvc3RyaW5nPgoJCQkJPC9kaWN0PgoJCQk8L2FycmF5PgoJCQk8a2V5Pm1ldGFkYXRhPC9rZXk+CgkJCTxkaWN0PgoJCQkJPGtleT5idW5kbGUtaWRlbnRpZmllcjwva2V5PgoJCQkJPHN0cmluZz5fYXBwX2J1bmRsZWlkXzwvc3RyaW5nPgoJCQkJPGtleT5idW5kbGUtdmVyc2lvbjwva2V5PgoJCQkJPHN0cmluZz5fYXBwX3Zlcl88L3N0cmluZz4KCQkJCTxrZXk+a2luZDwva2V5PgoJCQkJPHN0cmluZz5zb2Z0d2FyZTwvc3RyaW5nPgoJCQkJPGtleT50aXRsZTwva2V5PgoJCQkJPHN0cmluZz5fYXBwX3RpdGxlXzwvc3RyaW5nPgoJCQk8L2RpY3Q+CgkJPC9kaWN0PgoJPC9hcnJheT4KPC9kaWN0Pgo8L3BsaXN0Pgo="
        plist_Content = Base64.decode64(plist_base64)
        # 有幾個(gè)占位符需要替換掉 _app_title_ 、_app_ver_ 碾牌、 _app_bundleid_ 康愤、 _app_ipaURL_ (其實(shí)只有_app_ipaURL_是核心)
        plist_Content = plist_Content.gsub("_app_ipaURL_", ipa_URL)
        plist_Content = plist_Content.gsub("_app_ver_", pj_ver)
        plist_Content = plist_Content.gsub("_app_bundleid_", variable[:pj_main_bundleID])
        plist_Content = plist_Content.gsub("_app_title_", variable[:pj_title])
        # 寫入文件
        File.open("#{current_dir_path}/manifest.plist", "w+:utf-8") do |lines|  #讀寫模式。如果文件存在舶吗,則重寫已存在的文件征冷。如果文件不存在,則創(chuàng)建一個(gè)新文件用于讀寫
            lines.write(plist_Content) 
        end
        plist_URL = File.join(https_server_domain, pj_scheme, pj_env, new_pj_ver, "manifest.plist")

        install_URL = "itms-services://?action=download-manifest&url=#{plist_URL}"
        

        # 創(chuàng)建安裝二維碼圖片文件
        qr = RQRCode::QRCode.new(install_URL, :level=>:h)
        png = qr.as_png(
            resize_gte_to: false,
            resize_exactly_to: false,
            fill: 'white',
            color: 'black',
            size: 180,
            border_modules: 0,
            module_px_size: 0,
            file: "#{current_dir_path}/QRImg.png" # path to write
        )
        qR_Img_URL = File.join(http_server_domain, pj_scheme, pj_env, new_pj_ver, "QRImg.png")


        # 生成html
        html_base64 = "PCFET0NUWVBFIGh0bWw+CjxodG1sPgogICAgPGhlYWQ+CiAgICAgIDxtZXRhIG5hbWU9InZpZXdwb3J0ImNvbnRlbnQ9IndpZHRoPWRldmljZS13aWR0aCwgaW5pdGlhbC1zY2FsZT0xLjAsIG1pbmltdW0tc2NhbGU9MC41LCBtYXhpbXVtLXNjYWxlPTIuMCwgdXNlci1zY2FsYWJsZT15ZXMiLz4KICAgICAgPG1ldGEgaHR0cC1lcXVpdj0iQ29udGVudC1UeXBlIiBjb250ZW50PSJ0ZXh0L2h0bWw7IGNoYXJzZXQ9VVRGLTgiPgogICAgPC9oZWFkPgogICAgPGJvZHk+CiAgICAgICAgPGg0PuaJi+acuummluasoeS4i+i9veivt+WFiCLngrnlh7vlronoo4VTU0zor4HkuaYi77yM5bm25qC55o2u5o+Q56S65a6J6KOFL+S/oeS7u+ivgeS5pjwvaDQ+CiAgICAgICAgPGEgdGl0bGU9ImlQaG9uZSIgaHJlZj0iX1Jvb3RfQ0FfVVJMXyI+6aaW5qyh6ZyA54K55q2k5a6J6KOFU1NM6K+B5LmmPC9hPgogICAgICAgIOWuieijhea1geeoi+WQjCLmipPljIXor4HkuaYi77yM5a6J6KOF5ZCO6ZyA6KaB5omL5Yqo5byA5ZCv5Y+XIFNTTCDkv6Hku7vvvIjliY3lvoDigJzorr7nva7igJ0+4oCc6YCa55So4oCdPuKAnOWFs+S6juacrOacuuKAnT7igJzor4Hkuabkv6Hku7vorr7nva7igJ3vvIkKICAgICAgICA8aHI+CiAgICAgICAgPGg0PuWmguaenOS9oOaYr+aJi+acuuaJk+W8gOeahOacrOmhtemdojwvaDQ+CiAgICAgICAgPGEgaHJlZj0iX2l0bXMtc2VydmljZXNfdXJsXyIgY2xhc3M9ImFwcF9saW5rIj7ngrnlh7vlronoo4U8L2E+CiAgICAgICAgPGhyPgogICAgICAgIDxoND7lpoLmnpzkvaDmmK/nlLXohJHmiZPlvIDnmoTmnKzpobXpnaLvvIzor7fnlKjmiYvmnLrmiavov5nkuKrkuoznu7TnoIHlronoo4U8L2g0PgogICAgICAgIDxpbWcgc3JjPSJfcXJfaW1hZ2VfdXJsXyI+CiAgICA8L2JvZHk+CjwvaHRtbD4="
        html_Content = Base64.decode64(html_base64).force_encoding("UTF-8")
        # 有幾個(gè)占位符需要替換掉 _itms-services_url_ 誓琼、 _qr_image_url_ 检激、 _Root_CA_URL_
        html_Content = html_Content.gsub("_itms-services_url_", install_URL)
        html_Content = html_Content.gsub("_qr_image_url_", qR_Img_URL)
        html_Content = html_Content.gsub("_Root_CA_URL_", root_CA_URL)
        # 寫入文件
        File.open("#{current_dir_path}/index.html", "w+:utf-8") do |lines|  #讀寫模式。如果文件存在腹侣,則重寫已存在的文件叔收。如果文件不存在,則創(chuàng)建一個(gè)新文件用于讀寫
            lines.write(html_Content) 
        end

        # 這個(gè)html 用http訪問
        {"pageUrl" => File.join(http_server_domain, pj_scheme, pj_env, new_pj_ver, "index.html")}        
    end
通知

執(zhí)行完成后傲隶,只需要改下釘釘通知/郵件通知饺律,這個(gè)比較簡(jiǎn)單就不贅述了。
我們的差不多就這樣子:


釘釘群通知
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末伦籍,一起剝皮案震驚了整個(gè)濱河市蓝晒,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌帖鸦,老刑警劉巖芝薇,帶你破解...
    沈念sama閱讀 218,755評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異作儿,居然都是意外死亡洛二,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門攻锰,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)晾嘶,“玉大人,你說(shuō)我怎么就攤上這事娶吞±萦兀” “怎么了?”我有些...
    開封第一講書人閱讀 165,138評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵妒蛇,是天一觀的道長(zhǎng)机断。 經(jīng)常有香客問我,道長(zhǎng)绣夺,這世上最難降的妖魔是什么吏奸? 我笑而不...
    開封第一講書人閱讀 58,791評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮陶耍,結(jié)果婚禮上奋蔚,老公的妹妹穿的比我還像新娘。我一直安慰自己烈钞,他們只是感情好泊碑,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著毯欣,像睡著了一般蛾狗。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上仪媒,一...
    開封第一講書人閱讀 51,631評(píng)論 1 305
  • 那天沉桌,我揣著相機(jī)與錄音,去河邊找鬼算吩。 笑死留凭,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的偎巢。 我是一名探鬼主播蔼夜,決...
    沈念sama閱讀 40,362評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼压昼!你這毒婦竟也來(lái)了求冷?” 一聲冷哼從身側(cè)響起瘤运,我...
    開封第一講書人閱讀 39,264評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎匠题,沒想到半個(gè)月后拯坟,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,724評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡韭山,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年郁季,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片钱磅。...
    茶點(diǎn)故事閱讀 40,040評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡梦裂,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出盖淡,到底是詐尸還是另有隱情年柠,我是刑警寧澤,帶...
    沈念sama閱讀 35,742評(píng)論 5 346
  • 正文 年R本政府宣布褪迟,位于F島的核電站彪杉,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏牵咙。R本人自食惡果不足惜派近,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望洁桌。 院中可真熱鬧渴丸,春花似錦、人聲如沸另凌。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)吠谢。三九已至土童,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間工坊,已是汗流浹背献汗。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留王污,地道東北人罢吃。 一個(gè)月前我還...
    沈念sama閱讀 48,247評(píng)論 3 371
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像昭齐,于是被迫代替她去往敵國(guó)和親尿招。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容