Psellos
Contemporary Development With Functional Programming

Run iOS Simulator from the Command Line

November 14, 2012

I like to be able to run apps in the iOS Simulator from outside Xcode—it’s simpler and faster a lot of the time. This page shows how to do it under recent versions of OS X (Lion and Mountain Lion). It should be useful to those trying out OCaml iOS Simulator apps built with OCamlXSim or to anybody who wants to start up simulated iOS apps quickly and/or programmatically.

Lion desktop with Psi example app in iOS Simulator

If you search the web, you’ll find a few descriptions of the undocumented command-line options for the iOS Simulator. (A good keyword to start with is "SimulateApplication"—include the quotes.) These options let you start up a single app in the simulator with a specified device type and SDK. However, apps started in this way sometimes run freakishly slow. It seems that some supporting parts of the simulated environment are not always initialized properly when using this method.

Another problem with this approach is that it doesn’t simulate the full iOS environment. When you exit your app, for example, the simulation is terminated. So you can’t test interactions between your app and others.

Things work better if you install your iOS app as a package in the simulator’s file system, and then start the simulator as an ordinary Mac app. I’ve written a script named runsim that does this. The latest version of runsim is 2.0. You can download it from the following link:

The full text of the script also appears at the end of this page.

Keep in mind that the iOS Simulator’s file system is undocumented, so runsim is using an unsupported interface. I can’t promise that it will work for you, but it works very well for me.

For this latest release of runsim I spent a few days finding a good way to actually launch an app in the simulator. After tracing launchd and its daemonical helpers, looking for a low-level way to do it, I decided it would be better to use a supported Apple tool.

A commenter, Shail Choksi, pointed out that the command-line version of Instruments will start an iOS app for you programmatically. That’s what I ended up using. So, runsim now lets you start up an iOS app in the simulator without any human intervention, and doesn’t have to stray too far from supported methods to do it.

To use the script, download runsim from the above link or copy and paste the text at the end of this page onto a file named runsim. Mark it as a script with chmod:

$ chmod +x runsim

To demonstrate how runsim works, I’ll use Psi, an iOS app I wrote to be as small as possible. You can read more about Psi in Tiny iOS App in One Source File.

The script will perform one or more of five actions, selected by five options -i, -s, -r, -d and -l. I’ll go through them in turn.

$ runsim -iphone Psi  file
$

(Technically, this is the -i option with the value phone.)

Install Psi (or any given executable) as an iPhone app, with the specified supporting files. Psi doesn’t require any supporting files, but most apps will require at least a few.

To install an app as an iPad app, use -ipad rather than -iphone.

$ runsim -s
$

Start up the iPhone simulator. It will contain a standard set of default apps (such as Mobile Safari) and whatever apps you’ve installed. Your apps will generally be on the second screenful; click and swipe to the left to see them. You can start them as usual, by clicking on their icons.

$ runsim -r Psi
$

Run Psi (or any installed app) in the simulator. The app opens and starts immediately, avoiding the need for human interaction at startup. If the simulator isn’t already running, it will be started first as with the -s option.

As mentioned above, this option uses Instruments, which causes a few complexities. See the Instruments section below for more information.

$ runsim -d Psi
$

Uninstall Psi (or any installed app). Be careful with this. I’ve never tried uninstalling an app while it’s running in the simulator, but I wouldn’t expect it to work out well. I would also avoid uninstalling apps that have been installed through Xcode while Xcode is running.

$ runsim -l
Psi
$

List the installed apps. If you’ve installed apps through Xcode, they’ll be included in the list.

If you don’t specify one of the options, runsim 2.0 assumes the -iphone and -s options. That is, it installs an executable as an iPhone app and starts up the simulator. This is compatible with the way it worked in version 1.0.

The first figure below shows what you see when you say runsim Psi and then swipe to the left in the simulator. The second figure shows how Psi looks when it’s running. It just draws the Greek letter psi (upper case).

Psi example app in iOS Simulator home screen
Psi example app in iOS Simulator home screen

Instruments

The -r option of runsim runs instruments, the command-line version of Instruments. This introduces some extra complexity. If you haven’t yet agreed to the license terms of Xcode, instruments writes a message telling you how to agree to the terms, then exits without doing anything. You can follow the instructions to agree to the terms from the command line. You can also just start up Xcode, which will guide you through the acceptance with a GUI.

Although instruments doesn’t really need to take control of the iOS app that it starts up, it doesn’t know this. The first time you use the -r option in each login session, instruments will prompt for your password (if you’re a developer) or for the name and password of a developer (if OS X doesn’t consider you to be a developer).

To be considered a developer by OS X, you must be a member of the _developer group. I suspect (but haven’t verified) that all OS X admins are automatically made a member of this group. To add a user to this group, use dscl:

$ sudo dscl . append /Groups/_developer GroupMembership username

Other Details

An installation of Xcode can have simulators for different versions of iOS. runsim 2.0 installs apps into the iOS 6.0 simulator. If you have trouble locating your apps, you may need to change the simulated version of iOS. Change the version to 6.0 in the Hardware -> Version menu of the simulator.

Every app in iOS must contain an Info.plist file describing the properties of the app. If there is one in the current directory, runsim uses it. Otherwise it fabricates a reasonable one for you.

Since iOS 5, you can have a nib file for your app, or you can have a storyboard file, or you can have neither. I’ve personally always wanted the “neither” option—it makes it a lot easier to create a small example to test or demonstrate something about iOS (which I seem to want to do quite often).

When you install an app with the -i option, runsim assumes that the first specified file ending with a .nib or .storyboard suffix is the startup file. If no such file is specified, runsim assumes your main nib or storyboard file is named the same as the executable with a .nib or .storyboard suffix. If it doesn’t find any of these files, it assumes that you don’t have a startup file.

Be aware that the script copies files into the simulator’s space in your home directory. You’ll find them in Library/Application Support under iPhone Simulator/6.0/Applications. You may want to clear them out periodically, though they shouldn’t do much harm.

If your Xcode is installed in a non-standard place (not in the /Applications folder), create a file named runsim.xcloc with the full path of Xcode.app.

To make other changes, you’ll need to edit the script. One thing that might need changing is the version number of the simulator that you want to run—as I mentioned, runsim as given installs apps in the iOS 6.0 Simulator. runsim copies some associated images (Icon.png and Default.png) if they’re present in the current directory. There may be other files like this that you want to treat specially.

I got help on runsim from my OCaml-on-iOS colleagues at Sakhalin. You can basically figure out everything by just looking at the file structure that Xcode creates for you, and doing some guessing. But it’s always great to have help with the guessing. Many thanks to Sakhalin.

If you have any comments, corrections, or suggestions, leave them below or email me at jeffsco@psellos.com. I’d be especially interested if someone can verify that the script works properly with storyboard files.

Posted by: Jeffrey

Appendix

Finally, here is the text of runsim:

#!/bin/bash
#
# runsim   Install and run apps in the iOS Simulator
#
# Copyright (c) 2012 Psellos   http://psellos.com/
# Licensed under the MIT License:
#     http://www.opensource.org/licenses/mit-license.php
#
USAGE='usage:  runsim  [ -i { phone | pad } ] [ -srdl ]  executable  file ...'
#
# -iphone   Install as iPhone app
# -ipad     Install as iPad app
# -s        Start iOS Simulator
# -r        Run the app in the simulator
# -d        Delete the installed app
# -l        List names of installed apps
#
# file ...  Additional files to install with the executable
#
# Default flags are -iphone -s (install as iPhone app and start simulator).
#
# Currently the -r flag uses Instruments and thus requires
# authentication as a member of the _developer group.
#
VERSION=2.0.0

INSTALL=n
START=n
RUN=n
DELETE=n
LIST=n
while getopts i:srdl opt; do
    case "$opt" in
    i)
        INSTALL=y
        case "$OPTARG" in
        phone) FAMILY=1 ;;
        pad) FAMILY=2 ;;
        *)
            echo "runsim: unrecognized device family: $OPTARG" >&2
            echo "$USAGE" >&2
            exit 1
            ;;
        esac
        ;;
    s) START=y ;;
    r) RUN=y ;;
    d) DELETE=y ;;
    l) LIST=y ;;
    ?) echo "$USAGE" >&2; exit 1 ;;
    esac
done
shift $(($OPTIND - 1))

case "$INSTALL$START$RUN$DELETE$LIST" in nnnnn)
    INSTALL=y
    FAMILY=1
    START=y
esac

if [ "$INSTALL$RUN$DELETE" != nnn -a $# -lt 1 ]; then
    echo 'runsim: need an executable name for -i -r or -d' >&2
    echo "$USAGE" >&2
    exit 1
fi
EXEC="$1"
shift

APPDIR="$HOME/Library/Application Support/\
iPhone Simulator/6.0/Applications"

TRCSUB=Contents/Applications/Instruments.app\
/Contents/PlugIns/AutomationInstrument.bundle\
/Contents/Resources/Automation.tracetemplate

xcodeloc() {
    # Get location of Xcode, otherwise use default
    if [ -f runsim.xcloc ]; then
        cat runsim.xcloc
    else
        echo /Applications/Xcode.app
    fi
}

appuuid() {
    # Get UUID for an app. If installed, re-use existing one. Otherwise
    # create a new one and return it.
    #
    for f in "$APPDIR"/*/"$1.app" ; do
        if [ -d "$f" ]; then
            basename "$(dirname "$f")"
            return 0
        fi
    done
    uuidgen
}

install() {
    # Install executable $EXEC and associated files into simulator's
    # file system.
    #

    # Figure out startup file, if any. If a nibfile or storyboard file
    # is given, the first one is the startup file. Otherwise if there's
    # a file $EXEC.nib or $EXEC.storyboard, that is the startup file.
    # Otherwise there is no startup file.
    #
    NIBFILE=
    STORYFILE=
    if [ -f "$EXEC.nib" ]; then
        NIBFILE="$EXEC"
    elif [ -f "$EXEC.storyboard" ]; then
        STORYFILE="$EXEC"
    fi
    for f ; do
        case "$f" in
        *.nib)
            STORYFILE=; NIBFILE="$(basename "$f" .nib)"; break ;;
        *.storyboard)
            NIBFILE=; STORYFILE="$(basename "$f" .storyboard)"; break ;;
        esac
    done

    UUID=$(appuuid "$EXEC")

    # Install app and associated files.
    #
    TOPDIR="$APPDIR/$UUID"
    mkdir -p "$TOPDIR"
    mkdir -p "$TOPDIR/Documents"
    mkdir -p "$TOPDIR/Library"
    mkdir -p "$TOPDIR/tmp"
    mkdir -p "$TOPDIR/$EXEC.app"

    cp "$EXEC" "$TOPDIR/$EXEC.app"

    if [ "$NIBFILE" != "" ]; then
        cp "$NIBFILE.nib" "$TOPDIR/$EXEC.app"
    elif [ "$STORYFILE" != "" ]; then
        cp "$STORYFILE.storyboard" "$TOPDIR/$EXEC.app"
    fi

    # If an Info.plist exists, use it.  Otherwise make one.
    if [ -f Info.plist ] ; then
        plutil -convert xml1 -o "$TOPDIR/$EXEC.app/Info.plist" Info.plist
    else
        cat > "$TOPDIR/$EXEC.app/Info.plist" <<HERE1
<?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>CFBundleDevelopmentRegion</key>
        <string>English</string>
        <key>CFBundleDisplayName</key>
        <string>$EXEC</string>
        <key>CFBundleExecutable</key>
        <string>$EXEC</string>
        <key>CFBundleIconFile</key>
        <string>Icon.png</string>
        <key>CFBundleIdentifier</key>
        <string>com.example.$EXEC</string>
        <key>CFBundleInfoDictionaryVersion</key>
        <string>6.0</string>
        <key>CFBundleName</key>
        <string>$EXEC</string>
        <key>CFBundlePackageType</key>
        <string>APPL</string>
        <key>CFBundleSignature</key>
        <string>????</string>
        <key>CFBundleShortVersionString</key>
        <string>1.0</string>
        <key>CFBundleVersion</key>
        <string>1.0.0</string>
        <key>UIStatusBarStyle</key>
        <string>UIStatusBarStyleBlackOpaque</string>
        <key>LSRequiresIPhoneOS</key>
        <true/>
HERE1
        if [ "$NIBFILE" != "" ]; then
            cat >> "$TOPDIR/$EXEC.app/Info.plist" << HERE2
        <key>NSMainNibFile</key>
        <string>$NIBFILE</string>
HERE2
        elif [ "$STORYFILE" != "" ]; then
            cat >> "$TOPDIR/$EXEC.app/Info.plist" << HERE3
        <key>NSMainStoryboardFile</key>
        <string>$STORYFILE</string>
HERE3
        fi
        cat >> "$TOPDIR/$EXEC.app/Info.plist" <<HERE4
</dict>
</plist>
HERE4
    fi

    # Add device specifications to Info.plist (normally done by Xcode).
    # Without these, Instruments reports the app as AWOL.
    #
    python -c '
import plistlib
import sys
p = plistlib.readPlist(sys.argv[1])
p["CFBundleSupportedPlatforms"] = ["iPhoneSimulator"]
p["DTPlatformName"] = "iphonesimulator"
p["DTSDKName"] = "iphonesimulator6.0"
p["UIDeviceFamily"] = ['$FAMILY']
plistlib.writePlist(p, sys.argv[1])
' "$TOPDIR/$EXEC.app/Info.plist"

    echo -n 'AAPL????' > "$TOPDIR/$EXEC.app/PkgInfo"

    # Install conventional image files if they exist.
    #
    if [ -f Icon.png ]; then
        cp Icon.png "$TOPDIR/$EXEC.app"
    fi
    if [ -f Default.png ]; then
        cp Default.png "$TOPDIR/$EXEC.app"
    fi

    # Install any other given files.
    #
    for f; do
        if [ "$f" = "$NIBFILE.nib" ]; then continue; fi
        if [ "$f" = "$STORYFILE.storyboard" ]; then continue; fi
        cp "$f" "$TOPDIR/$EXEC.app"
    done
}


start() {
    # Start the iOS Simulator
    #
    open "$(xcodeloc)"/Contents/\
Developer/Platforms/iPhoneSimulator.platform/\
Developer/Applications/iPhone\ Simulator.app
}


run() {
    # Run the app inside iOS Simulator by asking Instruments to trace it
    # with null trace. If you haven't agreed to the licensing terms of
    # Xcode, this will fail until you do.  The first time in each login
    # session, this will ask for authentication as an admin or
    # developer.
    #
    TOPDIR="$APPDIR/$(appuuid "$EXEC")"
    if [ ! -d "$TOPDIR/$EXEC.app" ]; then
        echo "runsim: app \"$EXEC\" not installed" >&2
        exit 1
    fi
    (instruments -D /tmp/runsim$$.trace -t "$(xcodeloc)/$TRCSUB" \
            "$TOPDIR/$EXEC.app" < /dev/null 2>&1 > /dev/null | \
            grep 'xcodebuild -license' >&2 ; \
        rm -rf /tmp/runsim$$.trace) &
}

delete() {
    # Delete an installed app.
    #
    TOPDIR="$APPDIR/$(appuuid "$EXEC")"
    if [ ! -d "$TOPDIR" ]; then
        echo "runsim: app \"$EXEC\" not installed" >&2
        exit 1
    fi
    rm -rf "$TOPDIR"
}

list() {
    # List installed apps.
    #
    for f in "$APPDIR"/*/*.app ; do
        if [ -d "$f" ]; then
            basename "$f" .app
        fi
    done
}


case $INSTALL in y) install "$@" ;; esac
case $START in y) start ;; esac
case $RUN in y) run ;; esac
case $DELETE in y) delete ;; esac
case $LIST in y) list ;; esac

Comments

blog comments powered by Disqus