-
-
Save lexrus/4158767 to your computer and use it in GitHub Desktop.
OTA builder
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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