jondong
11/28/2012 - 3:02 AM

OTA builder

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 = "lohas@lexrus.mailgun.org"
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:])