#!python3
import os
import os.path
import re
import sys
import subprocess
import enum

__version__ = "1.2"

CREDENTIALS_FILE = "/var/_xcsbuildd/githubcredentials"


def warning(*objs):
    with open("/tmp/cavejohnson.log", "a") as file:
        file.write(" ".join(objs))
        file.write("\n")


def reSignIPAArgs(args):
    reSignIPA(args.new_mobileprovision_path, args.certificate_name, args.out_ipa_name, args.ipa_path)


def zipdir(path, zip_path):
    import zipfile
    with zipfile.ZipFile(zip_path, 'w') as zip:
        for root, dirs, files in os.walk(path):
            for file in files:
                full_path = os.path.join(root, file)
                correct_path = full_path[len(path):]
                zip.write(full_path, arcname=correct_path, compress_type=zipfile.ZIP_DEFLATED)


def reSignIPA(new_mobileprovision_path, certificate_name, out_ipa_name, ipa_path=None):
    if not ipa_path:
        ipa_path = os.environ["XCS_OUTPUT_DIR"] + "/" + os.environ["XCS_PRODUCT"]

    import plistlib
    # extract from mobileprovision
    entitlements = subprocess.check_output(["security", "cms", "-D", "-i", new_mobileprovision_path])
    entitlements = plistlib.loads(entitlements)

    info_plist = load_plist_ipa(ipa_path)

    if not entitlements["Entitlements"]["application-identifier"].endswith(info_plist["CFBundleIdentifier"]):
        print("Entitlements application-identifier %s doesn't match info_plist identifier %s" % (entitlements["Entitlements"]["application-identifier"], info_plist["CFBundleIdentifier"]))

    # todo: resign frameworks

    import tempfile
    import zipfile
    tempdir = tempfile.mkdtemp()
    zip_file = zipfile.ZipFile(ipa_path)
    zip_file.extractall(tempdir)
    warning("Working in", tempdir)

    # calculate appname
    import re
    not_app = list(filter(lambda x: re.match("Payload/.*.app/$", x), zip_file.namelist()))[0]  # like 'Payload/MyiOSApp.app/'
    appname = re.match("Payload/(.*).app/$", not_app).groups()[0] + ".app"
    payload_path = tempdir + "/Payload"

    app_path = payload_path + "/" + appname

    import shutil
    shutil.copyfile(new_mobileprovision_path, app_path + "/embedded.mobileprovision")

    # write entitlements to tempfile
    with open(tempdir + "/entitlements.plist", "wb") as fp:
        plistlib.dump(entitlements["Entitlements"], fp)
    warning("codesign begin")
    subprocess.check_call(["codesign", "--entitlements", tempdir + "/entitlements.plist", "-f", "-s", certificate_name, app_path])
    warning("codesign end")

    zipdir(payload_path, out_ipa_name)
    shutil.rmtree(tempdir)
    warning("done signing")


def xcodeGUITricksArgs(args):
    xcodeGUITricks(args.archive_path, args.new_ipa_path)


def xcodeGUITricks(archive_path, new_ipa_path):
    if not archive_path:
        archive_path = os.environ["XCS_ARCHIVE"]

    import tempfile
    tempdir = tempfile.mkdtemp()

    # First we copy the payload
    import shutil
    # There's only 1 app, right?
    appname = os.listdir(archive_path + "/Products/Applications")
    assert len(appname) == 1
    appname = appname[0]

    shutil.copytree(archive_path + "/Products/Applications/", tempdir + "/Payload")

    # next, we copy the swiftsupport
    shutil.copytree(archive_path + "/SwiftSupport", tempdir + "/SwiftSupport")

    # finally, we fix up the symbols
    # we need the app binary
    appbinary = archive_path + "/Products/Applications/" + appname + "/" + appname[:-4]  # .app, like MyAppName.app/MyAppName
    os.mkdir(tempdir + "/Symbols")
    # This was reverse-engineered by running a GUI export and poking in a file called IDEDistribustion.standard.log
    # Retrieve Xcode path from Xcode-select. Usefull when you have severall Xcode installations
    xcode_path = subprocess.check_output('xcode-select -p', shell=True).decode('ascii').strip()
    symbols_path = xcode_path + "usr/bin/symbols"
    subprocess.check_call([symbols_path, "-noTextInSOD", "-noDaemon", "-arch", "all", "-symbolsPackageDir", tempdir + "/Symbols", appbinary])

    # finally, let's call it a day
    zipdir(tempdir, new_ipa_path)
    shutil.rmtree(tempdir)


def uploadITMS(args):
    upload_itunesconnect(args.itunes_app_id, args.itunes_username, args.itunes_password, args.ipa_path)


def upload_itunesconnect(itunes_app_id, itunes_username, itunes_password, ipa_path=None):
    if not ipa_path:
        ipa_path = os.environ["XCS_OUTPUT_DIR"] + "/" + os.environ["XCS_PRODUCT"]

    data = load_plist_ipa(ipa_path)

    # first, we compute a path to the IPA
    # now we get a temp path to work in
    new_ipa_basename = "payload.ipa"
    import tempfile
    tpath = tempfile.mkdtemp()
    print("Working in path", tpath)
    packagepath = tpath + "/package.itmsp"
    new_ipa_path = packagepath + "/" + new_ipa_basename
    os.mkdir(packagepath)

    # copy the IPA to our temp path
    import shutil
    shutil.copyfile(ipa_path, new_ipa_path)

    # compute MD5
    import hashlib
    md5 = hashlib.md5()
    with open(new_ipa_path, 'rb') as f:
        for chunk in iter(lambda: f.read(8192), b''):
            md5.update(chunk)
    checksum = md5.hexdigest()

    # calculate filesize
    filesize = os.path.getsize(new_ipa_path)

    # Ok, here we go

    metadata_xml = """<?xml version="1.0" encoding="UTF-8"?>
<package version="software5.2" xmlns="http://apple.com/itunes/importer">
    <software_assets apple_id="{APP_ID}"
        bundle_short_version_string="{SHORT_VERSION_STRING}"
        bundle_version="{BUNDLE_VERSION}"
        bundle_identifier="{BUNDLE_IDENTIFIER}">
        <asset type="bundle">
            <data_file>
                <file_name>{IPA_NAME}</file_name>
                <checksum type="md5">{MD5}</checksum>
                <size>{FILESIZE}</size>
            </data_file>
        </asset>
    </software_assets>
</package>""".format(APP_ID=itunes_app_id, SHORT_VERSION_STRING=data["CFBundleShortVersionString"], BUNDLE_VERSION=data["CFBundleVersion"],
                     BUNDLE_IDENTIFIER=data["CFBundleIdentifier"], IPA_NAME=new_ipa_basename, MD5=checksum, FILESIZE=filesize)

    with open(packagepath + "/metadata.xml", "w") as f:
        f.write(metadata_xml)

    # run iTMSUploader
    subprocess.check_call(["/Applications/Xcode.app/Contents/Applications/Application Loader.app/Contents/itms/share/iTMSTransporter.woa/iTMSTransporter",
                           "-m", "upload", "-apple_id", itunes_app_id, "-u", itunes_username, "-p", itunes_password, "-f", packagepath])

    shutil.rmtree(tpath)


def set_github_status(repo, sha, token=None, integration_result=None, url=None, botname=None, verbosity=0):
    import github3
    if not token:
        token = github_auth()
    gh = github3.login(token=token)
    repo = repo.strip("/")
    (owner, reponame) = repo.split("/")
    r = gh.repository(owner, reponame)
    if not r:
        raise Exception("Trouble getting a repository for %s and %s" % (owner, reponame))

    # these constants are documented on http://faq.sealedabstract.com/xcodeCI/
    if not integration_result:
        xcs_status = os.environ["XCS_INTEGRATION_RESULT"]
    else:
        xcs_status = integration_result
    if xcs_status == "unknown":
        gh_state = "pending"
    elif xcs_status == "build-errors":
        gh_state = "error"
    elif xcs_status == "trigger-error":
        gh_state = "error"
    elif xcs_status == "test-failures" or xcs_status == "warnings" or xcs_status == "analyzer-warnings" or xcs_status == "test-failures":
        gh_state = "failure"
    elif xcs_status == "succeeded":
        gh_state = "success"
    else:
        raise Exception("Unknown xcs_status %s.  Please file a bug at http://github.com/drewcrawford/cavejohnson" % xcs_status)
    if not url:
        url = get_integration_url()
    if not botname:
        botname = get_botname()
    if verbosity >= 1:
        print("Setting GitHub status: `{}` for Xcode status: `{}` for commit: `{}`".format(gh_state, xcs_status, sha))
    r.create_status(sha=sha, state=gh_state, target_url=url, description=botname, context="CaveJohnson")


def install_mobileprovision_args(args):
    install_mobileprovision(args.provisioning_profile)


def install_mobileprovision(mobileprovision_path):
    import shutil
    basename = os.path.basename(mobileprovision_path)
    shutil.copyfile(mobileprovision_path, "/Library/Developer/XcodeServer/ProvisioningProfiles/" + basename)


def github_auth():
    if os.path.exists(CREDENTIALS_FILE):
        with open(CREDENTIALS_FILE) as f:
            token = f.read().strip()
            return token

    from github3 import authorize
    from getpass import getpass
    user = ''
    while not user:
        user = input("Username: ")
    password = ''
    while not password:
        password = getpass('Password for {0}: '.format(user))
    note = 'cavejohnson, teaching Xcode 6 CI new tricks'
    note_url = 'http://sealedabstract.com'
    scopes = ['repo:status', 'repo']
    auth = authorize(user, password, scopes, note, note_url)

    with open(CREDENTIALS_FILE, "w") as f:
        f.write(auth.token)

    return auth.token


# rdar://17923022
def get_sha():
    return get_repo_sha(get_git_directory())

def get_git_directory():
    for subdir in os.listdir('.'):
        if is_git_directory(subdir):
            return subdir
    assert False

def is_git_directory(path = '.'):
    return subprocess.call(['git', '-C', path, 'status'], stderr=subprocess.STDOUT, stdout = open(os.devnull, 'w')) == 0    

def get_repo_sha(repo):
    sha = subprocess.check_output(['git', 'rev-parse', 'HEAD'], cwd=repo).decode('ascii').strip()
    return sha

def update_git_submodules(repo):
    subprocess.call(['git', 'submodule', 'update', '--init', '--recursive'], cwd=repo)

def get_sha_from_log():
    sourceLogPath = os.path.join(os.environ["XCS_OUTPUT_DIR"], "sourceControl.log")
    with open(sourceLogPath) as sourceFile:
        sourceLog = sourceFile.read()
        match = re.search('"DVTSourceControlLocationRevisionKey"\s*\:\s*"(.*)"', sourceLog)
        if not match:
            raise Exception("No sha match in file.  Please file a bug at http://github.com/drewcrawford/cavejohnson and include the contents of %s" % sourceLogPath)
        return match.groups()[0]
    assert False

def get_origin(repo):
    origin = subprocess.check_output(['git', 'ls-remote', '--get-url'], cwd=repo).decode('ascii').strip()
    return origin

def get_repo():
    origin = get_origin(get_git_directory())
    if not origin:
        raise Exception("Unable to find repo.  Please file a bug at http://github.com/drewcrawford/cavejohnson and include the contents of %s" % sourceLogPath)
    githubRegex = re.compile('github.com(:)?', re.IGNORECASE)
    match = githubRegex.search(origin)
    assert match
    repo = origin[match.end():]
    repo = repo.replace("\/", "/")
    assert repo[-4:] == ".git"
    repo = repo[:-4]
    return repo

def get_repo_from_log():
    sourceLogPath = os.path.join(os.environ["XCS_OUTPUT_DIR"], "sourceControl.log")
    with open(sourceLogPath) as sourceFile:
        sourceLog = sourceFile.read()
        match = re.search('"DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey"\s*\:\s*"(.*)"', sourceLog)
        if not match:
            raise Exception("No repo match in file.  Please file a bug at http://github.com/drewcrawford/cavejohnson and include the contents of %s" % sourceLogPath)
        XcodeFunkyRepo = match.groups()[0]  # some funky string like "github.com:drewcrawford\/DCAKit.git"
        githubRegex = re.compile('github.com(:)?', re.IGNORECASE)
        match = githubRegex.search(XcodeFunkyRepo)
        assert match
        XcodeFunkyRepo = XcodeFunkyRepo[match.end():]
        XcodeFunkyRepo = XcodeFunkyRepo.replace("\/", "/")
        assert XcodeFunkyRepo[-4:] == ".git"
        XcodeFunkyRepo = XcodeFunkyRepo[:-4]
        return XcodeFunkyRepo
    assert False


def load_plist_ipa(ipa_path):
    # we have to read the plist inside the IPA
    import zipfile
    zip_file = zipfile.ZipFile(ipa_path)
    import re
    # search for info plist inside IPA
    info_plists = list(filter(lambda x: re.match("Payload/[^/]*/Info.plist", x), zip_file.namelist()))
    assert len(info_plists) == 1

    with zip_file.open(info_plists[0]) as plistfile:
        # some hackery to read into RAM because zip_file doesn't support 'seek' as plistlib requires
        plistdata = plistfile.read()

    import plistlib
    data = plistlib.loads(plistdata)
    return data


def load_plist(plistpath):
    if not os.path.exists(plistpath):
        output = subprocess.check_output(["find", ".", "-name", "*.plist"]).decode('utf-8')
        print(output)
        raise Exception("No such plist exists.  Try one of the strings shown in the log.")

    import plistlib
    with open(plistpath, "rb") as f:
        data = plistlib.load(f)
    return data

def set_plist_value_for_key(plistpath, value, key):
    data = load_plist(plistpath)
    data[key] = value
    print("Setting value `{}` for key `{}` in plist `{}`".format(value, key, plistpath))
    import plistlib
    with open(plistpath, "wb") as f:
        plistlib.dump(data, f)

def set_build_number(plistpath):
    data = load_plist(plistpath)
    # see xcdoc://?url=developer.apple.com/library/etc/redirect/xcode/ios/602958/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
    # but basically this is the only valid format
    # unofficially, however, sometimes a buildno (and minor) is omitted.
    import re
    match = re.match("(\d+)\.?(\d*)\.?(\d*)", data["CFBundleVersion"])
    if not match:
        raise Exception("Can't figure out CFBundleVersion.  Please file a bug at http://github.com/drewcrawford/cavejohnson and include the string %s" % data["CFBundleVersion"])
    (major, minor, build) = match.groups()
    if minor == "":
        minor = "0"
    integrationBuildVersion = "{}.{}.{}".format(major, minor, os.environ["XCS_INTEGRATION_NUMBER"])    
    set_plist_value_for_key(plistpath, integrationBuildVersion, "CFBundleVersion")


def get_integration_url():
    hostname = subprocess.check_output(["hostname"]).decode('utf-8').strip()
    if hostname.endswith(".private"):
        raise Exception("The hostname %s is invalid" % hostname)
    return "https://" + hostname + "/xcode/bots/" + os.environ["XCS_BOT_TINY_ID"] + "/integrations"


def get_botname():
    return os.environ["XCS_BOT_NAME"]


def get_commit_log():
    token = github_auth()
    import github3
    gh = github3.login(token=token)
    (owner, reponame) = get_repo().split("/")
    r = gh.repository(owner, reponame)
    if not r:

        raise Exception("Trouble getting a repository for %s and %s" % (owner, reponame))
    commit = r.git_commit(get_sha())
    return commit.to_json()["message"]


class HockeyAppNotificationType(enum.Enum):
    dont_notify = 0
    notify_testers_who_can_install = 1
    notify_all_testers = 2


class HockeyAppStatusType(enum.Enum):
    dont_allow_to_download_or_install = 0
    allow_to_download_or_install = 1


class HockeyAppMandatoryType(enum.Enum):
    not_mandatory = 0
    mandatory = 1


def upload_hockeyapp(token, appid, notification=None, status=None, mandatory=None, tags=None, profile=None):
    import requests
    old_ipa_path = os.path.join(os.environ["XCS_OUTPUT_DIR"], os.environ["XCS_PRODUCT"])
    if not os.path.exists(old_ipa_path):
        raise Exception("Can't find %s." % old_ipa_path)
    dsym_path = "/tmp/cavejohnson.dSYM.zip"
    subprocess.check_output("cd %s && zip -r %s dSYMs" % (os.environ["XCS_ARCHIVE"], dsym_path), shell=True)
    if not os.path.exists(dsym_path):
        raise Exception("Error processing dsym %s" % dsym_path)
    # resign IPA
    new_ipa_path = os.path.join(os.environ["XCS_OUTPUT_DIR"], "resigned.ipa")
    f = open("/tmp/xcodebuildlog", "w")
    data = ["Signing", "xcodebuild", "-exportArchive", "-exportFormat", "IPA", "-archivePath", os.environ["XCS_ARCHIVE"], "-exportPath", new_ipa_path, "-exportProvisioningProfile", profile]
    f.write("".join(data))
    f.close()

    output = subprocess.check_output(["xcodebuild", "-exportArchive", "-exportFormat", "IPA", "-archivePath", os.environ["XCS_ARCHIVE"], "-exportPath", new_ipa_path, "-exportProvisioningProfile", profile])
    print(output)
    with open(dsym_path, "rb") as dsym:
        with open(new_ipa_path, "rb") as ipa:
            files = {"ipa": ipa, "dsym": dsym}
            data = {"notes": get_commit_log(), "notes_type": "1", "commit_sha": get_sha(), "build_server_url": get_integration_url()}

            if notification:
                data["notify"] = notification.value
            if status:
                data["status"] = status.value
            if mandatory:
                data["mandatory"] = mandatory.value
            if tags:
                data["tags"] = tags

            r = requests.post("https://rink.hockeyapp.net/api/2/apps/%s/app_versions/upload" % appid, data=data, files=files, headers={"X-HockeyAppToken": token})
            if r.status_code != 201:
                print(r.text)
                raise Exception("Hockeyapp returned error code %d" % r.status_code)


def setGithubStatus(args):
    if not args.sha:
        args.sha = get_sha()
    if not args.repo:
        args.repo = get_repo()
    set_github_status(args.repo, args.sha, token=args.token, integration_result=args.integration_result, url=args.url, botname=args.bot_name, verbosity=args.verbose)


def getGithubRepo(args):
    print(get_repo())


def getSha(args):
    print(get_sha())

def setGithubAuthToken(args):
    whoami = subprocess.check_output(["whoami"]).strip().decode("utf-8")
    if whoami != "_xcsbuildd":
        print("%s is not _xcsbuildd" % whoami)
        print("Sorry, you need to call like 'sudo -u _xcsbuildd cavejohnson setGithubAuthToken --token github_auth_token'")
        sys.exit(1)
    with open(CREDENTIALS_FILE, "w") as f:
        f.write(args.token)

def setGithubCredentials(args):
    whoami = subprocess.check_output(["whoami"]).strip().decode("utf-8")
    if whoami != "_xcsbuildd":
        print("%s is not _xcsbuildd" % whoami)
        print("Sorry, you need to call like 'sudo -u _xcsbuildd cavejohnson setGithubCredentials'")
        sys.exit(1)
    github_auth()

def updateGitSubmodules(args):
    update_git_submodules(get_git_directory())

def getIntegrationURL(args):
    print(get_integration_url())

def setBuildNumber(args):
    set_build_number(args.plist_path)

def setPlistValueForKey(args):
    set_plist_value_for_key(args.plist_path, args.value, args.key)

def uploadHockeyApp(args):
    notify = None
    if args.notification_settings == "dont_notify":
        notify = HockeyAppNotificationType.dont_notify
    elif args.notification_settings == "notify_testers_who_can_install":
        notify = HockeyAppNotificationType.notify_testers_who_can_install
    elif args.notification_settings == "notify_all_testers":
        notify = HockeyAppNotificationType.notify_all_testers

    availability = None
    if args.availability_settings == "dont_allow_to_download_or_install":
        availability = HockeyAppStatusType.dont_allow_to_download_or_install
    elif args.availability_settings == "allow_to_download_or_install":
        availability = HockeyAppStatusType.allow_to_download_or_install

    if args.mandatory:
        mandatory = HockeyAppMandatoryType.mandatory
    else:
        mandatory = HockeyAppMandatoryType.not_mandatory

    upload_hockeyapp(args.token, args.app_id, notification=notify, status=availability, mandatory=mandatory, tags=args.restrict_to_tag, profile=args.resign_with_profile)


def main_func():
    import argparse
    parser = argparse.ArgumentParser(prog='CaveJohnson')
    subparsers = parser.add_subparsers(help='sub-command help')

    parser_geturl = subparsers.add_parser('getIntegrationURL', help='Gets the integration URL for the XCS server')
    parser_geturl.set_defaults(func=getIntegrationURL)

    # create the parser for the "setGithubStatus" command
    parser_ghstatus = subparsers.add_parser('setGithubStatus', help='Sets the GitHub status to an appropriate value inside a trigger.  Best to run both before and after build.')
    parser_ghstatus.add_argument("--token", default=None, help="GitHub token (by default, we pull it out of storage with setGithubCredentials)")
    parser_ghstatus.add_argument("--sha", default=None, help="SHA to set status for.  By default, we work this out from XCS. See getSha for details.")
    parser_ghstatus.add_argument("--repo", default=None, help="Repo to set status for.  By default, we work this out from XCS.  See getGithubRepo for details.")
    parser_ghstatus.add_argument("--integration-result", default=None, help="XCS_INTEGRATION_RESULT to parse.  See http://faq.sealedabstract.com/xcodeCI/ for valid values.")
    parser_ghstatus.add_argument("--bot-name", default=None, help="Name of bot.")
    parser_ghstatus.add_argument("--url", default=None, help="URL for more details about this integration.")
    parser_ghstatus.add_argument("--verbose", '-v', action='count', default=0)

    parser_ghstatus.set_defaults(func=setGithubStatus)

    parser_ghrepo = subparsers.add_parser('getGithubRepo', help='Detects the GitHub repo inside a trigger.')
    parser_ghrepo.set_defaults(func=getGithubRepo)

    parser_getsha = subparsers.add_parser('getSha', help="Detects the git sha of what is being integrated")
    parser_getsha.set_defaults(func=getSha)

    parser_authenticate = subparsers.add_parser('setGithubCredentials', help="Sets the credentials that will be used to talk to GitHub.")
    parser_authenticate.set_defaults(func=setGithubCredentials)

    parser_token = subparsers.add_parser('setGithubAuthToken', help="Sets an application access token used to communicate with GitHub. Generate this token via your application settings on github.com.")
    parser_token.add_argument('--token', help="GitHub application access token.", required=True)
    parser_token.set_defaults(func=setGithubAuthToken)

    parser_updategitsubmodules = subparsers.add_parser('updateGitSubmodules', help="Update the repo's git submodules. Xcode Bots may not do so automatically.\nIf you use this feature, please help us!  Visit https://github.com/drewcrawford/CaveJohnson/issues/14 for details.")
    parser_updategitsubmodules.set_defaults(func=updateGitSubmodules)

    parser_buildnumber = subparsers.add_parser('setBuildNumber', help="Sets the build number (CFBundleVersion) based on the bot integration count to building")
    parser_buildnumber.add_argument('--plist-path', help="path for the plist to edit", required=True)
    parser_buildnumber.set_defaults(func=setBuildNumber)

    parser_plistvaluekey = subparsers.add_parser('setPlistValueForKey', help="Sets a value in the given plist.")
    parser_plistvaluekey.add_argument('--plist-path', help="path for the plist to edit", required=True)
    parser_plistvaluekey.add_argument('--value', help="value to be added to the plist", required=True)
    parser_plistvaluekey.add_argument('--key', help="plist key under which to add the value", required=True)
    parser_plistvaluekey.set_defaults(func=setPlistValueForKey)

    parser_hockeyapp = subparsers.add_parser('uploadHockeyApp', help="Uploads an app to HockeyApp")
    parser_hockeyapp.add_argument("--token", required=True, help="Hockeyapp token")
    parser_hockeyapp.add_argument("--app-id", required=True, help="Hockeyapp app ID")
    parser_hockeyapp.add_argument("--notification-settings", choices=["dont_notify", "notify_testers_who_can_install", "notify_all_testers"], default=None)
    parser_hockeyapp.add_argument("--availability-settings", choices=["dont_allow_to_download_or_install", "allow_to_download_or_install"], default=None)
    parser_hockeyapp.add_argument("--mandatory", action='store_true', default=False, help="Makes the build mandatory (users must install)")
    parser_hockeyapp.add_argument("--restrict-to-tag", action='append', default=None, help="Restricts the build's availability to users with certain tags")
    parser_hockeyapp.add_argument("--resign-with-profile", default=None, help="Resign the archive with the specified provisioning profile name.")
    parser_hockeyapp.set_defaults(func=uploadHockeyApp)

    parser_uploadipa = subparsers.add_parser('uploadiTunesConnect', help="Upload the IPA to iTunesConnect (e.g. new TestFlight)")
    parser_uploadipa.add_argument("--itunes-app-id", required=True, help="iTunes app ID")
    parser_uploadipa.add_argument("--itunes-username", required=True, help="iTunes username (technical role or better)")
    parser_uploadipa.add_argument("--itunes-password", required=True, help="iTunes password")
    parser_uploadipa.add_argument("--ipa-path", default=None, help="IPA path.  If unspecified, guesses based on XCS settings.  Note that if reSignIPA is used, this should not be left blank.")
    parser_uploadipa.set_defaults(func=uploadITMS)

    parser_resignipa = subparsers.add_parser('reSignIPA', help="Resign IPA with given provisioning profile")
    parser_resignipa.add_argument("--ipa-path", default=None, help="IPA path.  If unspecified, guesses based on XCS settings.")
    parser_resignipa.add_argument("--new-mobileprovision-path", required=True, help="Path to the mobileprovision to resign with.")
    parser_resignipa.add_argument("--certificate-name", required=True, help="Full name of the certificate to resign with (like 'iPhone Distribution: DrewCrawfordApps LLC (P5GM95Q9VV)')")
    parser_resignipa.add_argument("--out-ipa-name", required=True, help="Name (path) of the resigned IPA file")
    parser_resignipa.set_defaults(func=reSignIPAArgs)

    parser_installmobileprovision = subparsers.add_parser('installMobileProvision', help="Installs the provisioning profile for XCS use")
    parser_installmobileprovision.add_argument("--provisioning-profile", required=True, help="Path to the provisioning profile.")
    parser_installmobileprovision.set_defaults(func=install_mobileprovision_args)

    parser_xcodeGUITricks = subparsers.add_parser('xcodeGUITricks', help="Converts Xcode Archives into IPAs in the way that the Xcode GUI does (swiftsupport + symbols).  Works around rdar://19432441 and rdar://19432725.")
    parser_xcodeGUITricks.add_argument("--archive-path", default=None, help="Path to the Xcode Archive.  If none, guesses based on XCS settings.")
    parser_xcodeGUITricks.add_argument("--new-ipa-path", required=True, help="Path to the output IPA file")
    parser_xcodeGUITricks.set_defaults(func=xcodeGUITricksArgs)

    def usage(args):
        parser.print_help()
    # parser.set_defaults(func=usage)
    args = parser.parse_args()
    if hasattr(args, "func"):
        args.func(args)
    else:
        usage(args)