Skip to content

Instantly share code, notes, and snippets.

@lexrus
Forked from rjyo/build.py
Last active October 13, 2015 07:08
Show Gist options
  • Save lexrus/4158767 to your computer and use it in GitHub Desktop.
Save lexrus/4158767 to your computer and use it in GitHub Desktop.
OTA builder
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os
import sys
import json
import getopt
import urllib2
import commands
import string
import qrcode
import sys,re
from urllib2 import urlopen as U, Request as R
from json import loads as J
from urllib import urlencode
from base64 import b64encode
from subprocess import Popen, PIPE, call
from datetime import datetime
APP_NAME = ""
WORKSPACE = ""
SCHEME = ""
CONFIG_NAME = ""
IPA_NAME = ""
HTTP_URL = ""
SFTP_SERVER = ""
SFTP_PATH = ""
SFTP_PORT = 22
NOTIF_EMAIL = "[email protected]"
EMAIL_DOMAIN = "lexrus.mailgun.org"
# Add a Password item to your Keychain and set its comment to "mailgun api key" so that we can find it
MAILGUN_KEY = commands.getoutput('security find-generic-password -j "mailgun api key" -gw')
def clean():
call_shell(["xcodebuild", "clean"])
call_shell(["rm", "-rf", "Build"])
print "All build files cleared."
def xcodebuild():
print "Start building target %s ..." % APP_NAME
cmd = ["xcodebuild", "-workspace", WORKSPACE, "-scheme", SCHEME, "-configuration", CONFIG_NAME, "clean", "build"]
call(cmd)
def package():
print "Start packaging ..."
call_shell(["rm", "-rf", "Payload"])
os.mkdir("Payload")
call_shell(["mv", "Products/%s-iphoneos/%s.app" % (CONFIG_NAME, APP_NAME), "Payload"])
call_shell(["zip", "-r", IPA_NAME, "Payload"])
call_shell(["mv", IPA_NAME, "pkg"])
def prepare():
print "Start preparing files for deploy ..."
appName = "%s.app" % (APP_NAME)
call_shell(["rm", "-rf", "pkg"])
os.mkdir("pkg")
call_shell(["cp", "Products/%s-iphoneos/%s/Info.plist" % (CONFIG_NAME, appName), "Info.plist"])
pl = plist_to_dictionary("Info.plist")
global IPA_NAME
IPA_NAME = "%s_%s_%s.ipa" % (APP_NAME, pl["CFBundleShortVersionString"], pl["CFBundleVersion"])
call_shell(["cp", "Products/%s-iphoneos/%s/%s" % (CONFIG_NAME, appName, icon_file(pl)), "pkg/%s" % icon_file(pl)])
content = manifest(pl)
f = open("pkg/%s.plist" % IPA_NAME, "w")
f.write(content.encode('utf8'))
f.close()
content = index_html(pl)
f = open("pkg/index.html", "w")
f.write(content.encode('utf8'))
f.close()
qr = qrcode.QRCode(
version = 1,
error_correction = qrcode.constants.ERROR_CORRECT_L,
box_size = 7,
border = 2,
)
qr.add_data(HTTP_URL)
qr.make(fit=True)
qrImg = qr.make_image()
qrImg.save("pkg/qr.png", "PNG")
def sftp():
print ""
print "Start uploading files to %s:%s ..." % (SFTP_SERVER, str(SFTP_PORT))
(width, height) = terminal_size()
print "-" * width
cmd = ["scp", "-P", "%s" % str(SFTP_PORT), "-r", "pkg/.", "%s:%s" % (SFTP_SERVER, SFTP_PATH)]
call(cmd)
print "-" * width
print "Finished uploading files."
print "Download app at %s" % HTTP_URL
def send_notification():
print "Start sending notification emails ..."
f = open("pkg/index.html", "r")
content = f.read().decode('utf8')
f.close()
pl = plist_to_dictionary("Info.plist")
appFullName = "%s %s (%s)" % (pl["CFBundleName"], pl["CFBundleShortVersionString"], pl["CFBundleVersion"])
ret = send_mailgun(appFullName, content)
ret = json.loads(ret)
print ret["message"]
def manifest(pl):
template = '''
<?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>%s</string>
</dict>
<dict>
<key>kind</key>
<string>full-size-image</string>
<key>needs-shine</key>
<false/>
<key>url</key>
<string>%s</string>
</dict>
<dict>
<key>kind</key>
<string>display-image</string>
<key>needs-shine</key>
<false/>
<key>url</key>
<string>%s</string>
</dict>
</array>
<key>metadata</key>
<dict>
<key>bundle-identifier</key>
<string>%s</string>
<key>bundle-version</key>
<string>%s</string>
<key>kind</key>
<string>software</string>
<key>title</key>
<string>%s</string>
</dict>
</dict>
</array>
</dict>
</plist>
'''
appUrl = HTTP_URL + "/" + IPA_NAME
iconUrl = HTTP_URL + "/" + icon_file(pl)
return template % (appUrl, iconUrl, iconUrl, pl["CFBundleIdentifier"], pl["CFBundleVersion"], pl["CFBundleName"])
def index_html(pl):
template = u'''
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
<title>%s - Beta</title>
<style type="text/css">
body{text-align:center;}
#container{width:300px;margin:0 auto;}
h1{margin:0;padding:0;font-size:14px;}
p{font-size:13px;}
.install_button{line-height:44px;margin:.5em auto;background:#89c4dc;background-image:-webkit-linear-gradient(top,rgb(126,203,26),rgb(92,149,19));background-origin:padding-box;background-repeat:repeat;-webkit-box-shadow:rgba(0,0,0,0.36) 0px 1px 3px 0px;-webkit-font-smoothing:antialiased;-webkit-user-select:none;background-attachment:scroll;background-clip:border-box;background-color:rgba(0,0,0,0);border-color:#75bc18;border-bottom-left-radius:16px;border-bottom-right-radius:16px;border-bottom-style:none;border-bottom-width:0px;border-left-style:none;border-left-width:0px;border-right-style:none;border-right-width:0px;border-top-left-radius:16px;border-top-right-radius:16px;border-top-style:none;border-top-width:0px;box-shadow:rgba(0,0,0,0.359375) 0px 1px 3px 0px;color:rgb(0,140,221);cursor:pointer;display:inline-block;font-family:proxima-nova,arial,sans-serif;font-size:20px;margin:10px 0;padding:1px;position:relative;text-align:center;text-decoration:none;-webkit-box-shadow:0 1px 3px rgba(0,0,0,0.36);}
.install_button a{font-weight:bold;font-size:24px;-webkit-box-shadow:rgba(255,255,255,0.25) 0px 1px 0px 0px inset;-webkit-font-smoothing:antialiased;-webkit-user-select:none;background-attachment:scroll;background-clip:border-box;background-color:rgba(0,0,0,0);background-image:-webkit-linear-gradient(top,rgb(195,250,123),rgb(134,216,27) 85%%,rgb(180,231,114));background-origin:padding-box;background-repeat:repeat;border-bottom-color:rgb(255,255,255);border-bottom-left-radius:15px;border-bottom-right-radius:15px;border-bottom-style:none;border-bottom-width:0px;border-left-color:rgb(255,255,255);border-left-style:none;border-left-width:0px;border-right-color:rgb(255,255,255);border-right-style:none;border-right-width:0px;border-top-color:rgb(255,255,255);border-top-left-radius:15px;border-top-right-radius:15px;border-top-style:none;border-top-width:0px;box-shadow:rgba(255,255,255,0.246094) 0px 1px 0px 0px inset;color:#fff;cursor:pointer;display:block;font-family:proxima-nova,arial,sans-serif;font-size:14px;font-weight:bold;height:31px;line-height:31px;margin:0;padding:0;text-align:center;text-decoration:none;text-shadow:rgba(0,0,0,0.527344) 0px 1px 1px;width:298px;}
.last_updated{font-size:x-small;text-align:center;font-weight:bolder;}
.icon{border-radius:10px;box-shadow:1px 2px 3px #ccc;}
.release_notes{border:1px solid #999;padding:20px;border-radius:6px;overflow:hidden;}
.release_notes:before{font-size:10px;content:"更新内容";background:#999;margin:-20px;float:left;padding:2px 5px;border-radius:3px 0 6px 0;color:#fff;}
</style>
</head>
<body>
<div id="container">
<p><img class="icon" src='%s' height='57' width='57'/></p>
<h1>%s</h1>
<div class="install_button"><a href="itms-services://?action=download-manifest&url=%s/%s.plist">在 iOS 设备上点击安装</a></div>
<p class="release_notes">%s</p>
<p><a href="%s">%s</a></p>
<p><img src="%s/qr.png"/></p>
<small>%s</small>
</body>
</html>
'''
reason = get_editor_input()
icon = icon_file(pl)
iconUrl = "%s/%s" % (HTTP_URL, icon)
appFullName = "%s %s (%s)" % (pl["CFBundleName"], pl["CFBundleShortVersionString"], pl["CFBundleVersion"])
SHORT_URL = goo_gl(HTTP_URL)
timeStr = datetime.now().strftime("%Y/%m/%d %H:%M:%S")
print "appFullName" + appFullName
return template % (appFullName, iconUrl, appFullName, HTTP_URL, IPA_NAME, reason, SHORT_URL, SHORT_URL, HTTP_URL, timeStr)
def get_editor_input():
cmd = os.environ['EDITOR']
if not cmd:
cmd = "/usr/bin/vim"
tmpFile = "/tmp/%s.input" % IPA_NAME
f = open(tmpFile, "w")
f.write("# What's NEW in this build?\n\n\n")
f.close()
call([cmd, tmpFile])
f = open(tmpFile, "r")
contents = f.read().decode('utf8').split("\n")
f.close()
result = ""
for line in contents:
line = line.strip()
if line.find("#") == 0 or len(line) == 0:
continue
result += line + "<br/>"
if len(result) == 0:
result = "-"
return result
def icon_file(pl):
icon = ""
if "CFBundleIconFiles" in pl:
icon = pl["CFBundleIconFiles"][0]
elif "CFBundleIcons" in pl:
icon = pl["CFBundleIcons"]["CFBundlePrimaryIcon"]["CFBundleIconFiles"][0]
return string.replace(icon, "@2x", "")
def plist_to_dictionary(filename):
"Pipe the binary plist through plutil and parse the JSON output"
with open(filename, "rb") as f:
content = f.read()
args = ["plutil", "-convert", "json", "-o", "-", "--", "-"]
p = Popen(args, stdin=PIPE, stdout=PIPE)
p.stdin.write(content)
out, err = p.communicate()
return json.loads(out)
def call_shell(cmd):
try:
p = Popen(cmd, stdin=None, stdout=PIPE)
out, err = p.communicate()
retcode = p.returncode
if retcode < 0:
print >> sys.stderr, "Child was terminated by signal", -retcode
except OSError, e:
print >> sys.stderr, "Execution failed:", e
def terminal_size():
import os
env = os.environ
def ioctl_GWINSZ(fd):
try:
import fcntl, termios, struct, os
cr = struct.unpack('hh', fcntl.ioctl(fd, termios.TIOCGWINSZ,
'1234'))
except:
return None
return cr
cr = ioctl_GWINSZ(0) or ioctl_GWINSZ(1) or ioctl_GWINSZ(2)
if not cr:
try:
fd = os.open(os.ctermid(), os.O_RDONLY)
cr = ioctl_GWINSZ(fd)
os.close(fd)
except:
pass
if not cr:
try:
cr = (env['LINES'], env['COLUMNS'])
except:
cr = (25, 80)
return int(cr[1]), int(cr[0])
def send_mailgun(appName, message):
api_url = "https://api.mailgun.net/v2/%s/messages" % EMAIL_DOMAIN
data = {"from": "Lex <postmaster@%s>" % EMAIL_DOMAIN,
"to": NOTIF_EMAIL,
"subject": "%s is ready!" % appName,
"text": "%s is ready!" % appName,
"html": message.encode('utf-8')}
request = urllib2.Request(api_url)
print "Sending notification to:" + NOTIF_EMAIL
request.add_header('Authorization', 'Basic ' + b64encode("api:%s" % MAILGUN_KEY))
request.add_data(urlencode(data))
try:
r = urllib2.urlopen(request)
return r.read()
except Exception, e:
print e
def goo_gl(url):
API="https://www.googleapis.com/urlshortener/v1/url"
if re.match('http://goo\.gl/.+',url):return J(U(API+'?shortUrl=%s'%url).read())['longUrl']
else:return J(U(R(API,'{"longUrl":"%s"}'%url,{'Content-Type':'application/json'})).read())['id']
############################################################
# main
############################################################
def usage():
print "This tool is used to make it easy to build and distribute enterprise iOS app."
print "It also send out notification via eMails (via mailgun)"
print ""
print "# Usage: build.py [-h] -workspace workspace_name -scheme scheme_name -t target_name -s sftp_server -sp sftp_port -p sftp_path -u http_url [command]"
print "# command = clean|upload|build|notif"
def main(argv):
try:
opts, args = getopt.getopt(argv, "hw:e:t:s:o:p:u:n:m:d:c:",
["help", "workspace=", "scheme=", "target=", "server=", "server_port=", "path=", "http_url=", "notif_email=",
"mailgun_key=", "email_domain=", "configuration="])
except getopt.GetoptError:
usage()
sys.exit(2)
for opt, arg in opts:
if opt in ("-h", "--help"):
usage()
sys.exit()
if opt in ("-c", "--configuration"):
global CONFIG_NAME
CONFIG_NAME = arg
if opt in ("-w", "--workspace"):
global WORKSPACE
WORKSPACE = arg
if opt in ("-e", "--scheme"):
global SCHEME
SCHEME = arg
if opt in ("-t", "--target"):
global APP_NAME
APP_NAME = arg
if opt in ("-s", "--server"):
global SFTP_SERVER
SFTP_SERVER = arg
if opt in ("-o", "--server_port"):
global SFTP_PORT
SFTP_PORT = arg
if opt in ("-p", "--path"):
global SFTP_PATH
SFTP_PATH = arg
if opt in ("-u", "--http_url"):
global HTTP_URL
HTTP_URL = arg
if opt in ("-d", "--email_domain"):
global EMAIL_DOMAIN
EMAIL_DOMAIN = arg
if opt in ("-n", "--notif_email"):
global NOTIF_EMAIL
NOTIF_EMAIL = arg
if opt in ("-m", "--mailgun_key"):
global MAILGUN_KEY
MAILGUN_KEY = arg
if argv and argv[-1] == "clean":
clean()
sys.exit(0)
if argv and argv[-1] == "notif":
os.chdir("Build")
send_notification()
sys.exit(0)
if not APP_NAME or not SFTP_SERVER or not SFTP_PATH or not HTTP_URL:
usage()
sys.exit(2)
if argv and argv[-1] == "build":
clean()
xcodebuild()
os.chdir("Build")
prepare()
package()
sys.exit(0)
if argv and argv[-1] == "upload":
os.chdir("Build")
sftp()
sys.exit(0)
if __name__ == "__main__":
main(sys.argv[1:])
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment