40

For the purposes of CI, I need to be able to generate an XCARCHIVE and an IPA file in our nightly build. The IPA is for our testers, to be signed with our ad-hoc keys, and the XCARCHIVE is to send to the client so that they can import it into Xcode and submit it to the app store when they're happy with it.

Generating the IPA is simple enough with a bit of googling, however how to generate the .XCARCHIVE file is what eludes me. The closest I've found is:

xcodebuild -scheme myscheme archive

However, this stores the .xcarchive in some hard-to-find folder, eg:

/Users/me/Library/Developer/Xcode/Archives/2011-12-14/MyApp 14-12-11 11.42 AM.xcarchive

Is there some way to control where the archive is put, what its name is, and how to avoid having to re-compile it? I guess the best possible outcome would be to generate the xcarchive from the DSYM and APP that are generated when you do an 'xcodebuild build' - is this possible?

jscs
  • 63,694
  • 13
  • 151
  • 195
Chris
  • 39,719
  • 45
  • 189
  • 235

7 Answers7

60

Xcode 5 now supports an -archivePath option:

xcodebuild -scheme myscheme archive -archivePath /path/to/AppName.xcarchive

You can also now export a signed IPA from the archive you just built:

xcodebuild -exportArchive -exportFormat IPA -exportProvisioningProfile my_profile_name -archivePath /path/to/AppName.xcarchive -exportPath /path/to/AppName.ipa
rectalogic
  • 1,266
  • 11
  • 12
  • Great! Didn't know `xcodebuild` has this feature. Going to be so useful. – Jonathan Crooke Feb 19 '14 at 20:34
  • Much better solution. Thank you. – Sebastian Dwornik Mar 06 '14 at 02:29
  • Glad they added that argument...this is the correct answer, be sure to include the filename of the xcarchive and not just the path...Thanks!!! – Vincil Bishop Nov 03 '14 at 16:36
  • 2
    Tried that in Xcode 6.1 and end up with an empty folder at `-archivePath` and the real archive in the default location. – OrangeDog Nov 25 '14 at 15:16
  • 1
    `-exportFormat` and `-exportProvisioningProfile` are now deprecated in favor of `-exportOptionsPlist`, which annoyingly is harder to get a particular mobileprovision used with. See `man xcodebuild` and `xcodebuild -help` (note there are things covered in each that are not covered in the other.) – leander Dec 14 '15 at 18:03
43

Starting with Xcode 4 Preview 5 there are three environment variables that are accessible in the scheme archive's post-actions.

ARCHIVE_PATH: The path to the archive.
ARCHIVE_PRODUCTS_PATH: The installation location for the archived product.
ARCHIVE_DSYMS_PATH: The path to the product’s dSYM files.

You could move/copy the archive in here. I wanted to have a little more control over the process in a CI script, so I saved a temporary file that could easily be sourced in my CI script that contained these values.

BUILD_DIR=$PROJECT_DIR/build
echo "ARCHIVE_PATH=\"$ARCHIVE_PATH\"" > $BUILD_DIR/archive_paths.sh
echo "ARCHIVE_PRODUCTS_PATH=\"$ARCHIVE_PRODUCTS_PATH\"" >> $BUILD_DIR/archive_paths.sh
echo "ARCHIVE_DSYMS_PATH=\"$ARCHIVE_DSYMS_PATH\"" >> $BUILD_DIR/archive_paths.sh
echo "INFOPLIST_PATH=\"$INFOPLIST_PATH\"" >> $BUILD_DIR/archive_paths.sh

Then in my CI script I can run the following:

xcodebuild -alltargets -scheme [Scheme Name] -configuration [Config Name] clean archive
source build/archive_paths.sh
ARCHIVE_NAME=AppName-$APP_VERSION-$APP_BUILD.xcarchive
cp -r "$ARCHIVE_PATH" "$BUILD_DIR/$ARCHIVE_NAME"
Dov
  • 15,530
  • 13
  • 76
  • 177
Aron
  • 621
  • 5
  • 5
  • 4
    Thank you thank you thank you! How on earth did you find this? I've combed the docs for days looking for something like this. – jemmons Aug 06 '12 at 16:55
  • to build on @jemmons comment...where DID you find this? After days of combing through everyone's custom scripts I'd love to read some official documentation – evanflash Dec 20 '12 at 16:55
  • This is the only mention I can find. Crappy documentation at its best. http://developer.apple.com/library/ios/releasenotes/developertools/rn-xcode/#//apple_ref/doc/uid/TP40001051-SW72 – Stefan Fisk Jun 14 '13 at 17:20
  • I'm not sure why, but my "ARCHIVE_"paths are coming up empty. :( (XCode 5.0.2) – Sebastian Dwornik Mar 05 '14 at 20:07
  • 1
    If I build within the IDE these parameters get filled. But if I build on the command line using xcodebuild then they don't get set. :/ doing [xcodebuild -sdk iphoneos -project "${PROJECT_NAME}" -scheme "myApp Adhoc" -configuration "Release (Adhoc) - QA" clean archive] – Sebastian Dwornik Mar 05 '14 at 20:48
  • Are those environment variables not available anymore? When I `echo` them out in my Run Scripts, they don't return anything. – Isuru May 16 '20 at 03:47
11

I have just solved this one - just add the argument -archivePath to your xcode build command line, given the initial question that would mean:

xcodebuild -scheme myscheme archive

becomes ...

xcodebuild -scheme myscheme archive -archivePath Build/Archive

(Note: paths are relative, I output my build to $PWD/Build)

This will then place your .app folder in:

Build/Archive.xarchive/Products/Application

If your build target already has your signing certificate and provisioning profile in it you can then create your IPA file without re-signing using the following command:

xcrun -v -sdk iphoneos PackageApplication -v `pwd`'/Build/Archive.xarchive/Products/Application/my.app' -o `pwd`'/myapp.ipa'

(Note: xcrun doesn't like relative paths hence the pwd)

The -v args dump lots of useful information - this command can fail to sign properly and still exit with code 0, sigh!

If you are finding that you can't run the built .ipa it's probably a signing issue that you can do a double check on using:

codesign --verify -vvvv myapp.app

If it's signed correctly and un-tampered with the output will have this in:

myapp.app: valid on disk
myapp.app: satisfies its Designated Requirement

If not you will see something similar to this:

Codesign check fails : /blahpath/myapp.app: a sealed resource is missing or invalid
file modified: /blahpath/ls-ios-develop.app/Assets.car

... which generally means you are trying to use an intermediate output directory rather than the proper archive.

Oly Dungey
  • 1,603
  • 19
  • 20
  • `xcrun PackageApplication` is deprecated, I believe; `xcodebuild -exportArchive` after an `xcodebuild archive` seems to be preferred now (apparently was the case sometime around Xcode 6, and things started breaking in PackageApplication around Xcode 7). Sadly, the functionality sets are not identical; it seems hard to get a particular provisioning profile used if it wasn't already set up in the pbxproj when using `-exportArchive`. – leander Dec 14 '15 at 18:06
2

Here's a bit of bash that I've come up with for our Jenkins CI system. These commands should be run in a script immediately after the xcodebuild archive command finishes.

BUILD_DIR="${WORKSPACE}/build"
XCODE_SCHEME="myscheme"

# Common path and partial filename
ARCHIVE_BASEPATH="${HOME}/Library/Developer/Xcode/Archives/$(date +%Y-%m-%d)/${XCODE_SCHEME}"

# Find the latest .xcarchive for the given scheme
NEW_ARCHIVE=$(ls -td "${ARCHIVE_BASEPATH}"* | head -n 1)

# Zip it up so non-Apple systems won't treat it as a dir
pushd "${NEW_ARCHIVE%/*}"
zip -r "${BUILD_DIR}/${NEW_ARCHIVE##*/}.zip" "${NEW_ARCHIVE##*/}"
popd

# Optional, disk cleanup
rm -rf "${NEW_ARCHIVE}"

The BUILD_DIR is used to collect artifacts so that it's easy to archive them from Jenkins with a glob such as build/*.ipa,build/*.zip

phatblat
  • 3,804
  • 3
  • 33
  • 31
2

My current solution is to rename the user's existing archives folder, run the build, and do a 'find' to copy the archives where i want, then delete the archives folder and rename the old folder back as it was, with code like this in my ruby build script:

# Move the existing archives out of the way
system('mv ~/Library/Developer/Xcode/Archives ~/Library/Developer/Xcode/OldArchivesTemp')
# Build the .app, the .DSYM, and the .xcarchive
system("xcodebuild -scheme \"#{scheme}\" clean build archive CONFIGURATION_BUILD_DIR=\"#{build_destination_folder}\"")
# Find the xcarchive wherever it was placed and copy it where i want it
system("find ~/Library/Developer/Xcode/Archives -name *.xcarchive -exec cp -r {} \"#{build_destination_folder}\" \";\"")
# Delete the new archives folder with this new xcarchive
system('rm -rf ~/Library/Developer/Xcode/Archives')
# Put the old archives back
system('mv ~/Library/Developer/Xcode/OldArchivesTemp ~/Library/Developer/Xcode/Archives')

Its a bit hacky but i don't see a better solution currently. At least it preserves the user's 'archives' folder and all their pre-existing archives.

--Important note!--

I since found out that the line of code where i find the archive and cp it to the folder i want doesn't copy the symlinks inside the archive correctly, thus breaking the code signing in the app. You'll want to replace that with a 'mv' or something that maintains symlinks. Cheers!

Chris
  • 39,719
  • 45
  • 189
  • 235
  • This is a great find, but doesn't work for a CI system with lots of concurrent builds running at the same time. I'm going to try out this bit of bashery: export ARCHIVE_BASEPATH="${HOME}/Library/Developer/Xcode/Archives/$(date +%Y-%m-%d)/${SCHEME}" && \ ls -td "${ARCHIVE_BASEPATH}"* | \ head -n 1 Where SCHEME is the string name of the Xcode scheme being built (possibly containing spaces). This will still have a race condition if two different CI builds are currently building the same scheme. – phatblat Jul 10 '13 at 17:49
0

Similar to the others, but perhaps a little simpler since I try to record the .xcarchive file's location. (I also don't move the archives folder, so this will work better if you're doing multiple builds at the same time.)

My caller build script generates a new tempfile and sets its path to an environment variable named XCARCHIVE_PATH_TMPFILE. This environment variable is available in my scheme's Archive post-action shell script, which then that writes the .xcarchive's path to that file. The build script that can then read that file after it calls xcodebuild archive.

post-action shell script

echo $ARCHIVE_PATH > "$XCARCHIVE_PATH_TMPFILE"
zekel
  • 9,227
  • 10
  • 65
  • 96
0

On Xcode 4.6 it is possible to specify a post-build action for the scheme to be compiled into an xcarchive:

echo "ARCHIVE_PATH=\"$ARCHIVE_PATH\"" > $PROJECT_DIR/archive_paths.sh

A build script can be used to check if $ARCHIVE_PATH is defined after running xcodebuild and if this is the case, the output xcarchive can be moved into a designated folder.

This method is not very maintainable if the targets in the project are a large number, as for each one it is necessary to tag the corresponding scheme as 'shared' and add the post-build action.

To address this problem, I have created a build script that generates the archive path programmatically by extracting the last build that matches the target name on the current day. This method works reliably as long as there aren't multiple builds with the same target name running on the machine (this may be a problem in production environments where multiple concurrent builds are run).

#!/bin/bash
#
# Script to archive an existing xcode project to a target location.
# The script checks for a post-build action that defines the $ARCHIVE_PATH as follows:
# echo "ARCHIVE_PATH=\"$ARCHIVE_PATH\"" > $PROJECT_DIR/archive_paths.sh
# If such post-build action does not exist or sourcing it doesn't define the $ARCHIVE_PATH   
# variable, the script tries to generate it programmatically by finding the latest build
# in the expected archiving folder
#

post_build_script=archive_paths.sh
build_errors_file=build_errors.log
OUTPUT=output/
XCODEBUILD_CMD='/Applications/Xcode.app/Contents/Developer/usr/bin/xcodebuild'
TARGET_SDK=iphoneos

function archive()
{
    echo "Archiving target '$1'"

    # Delete $post_build_script if it already exists as it should be generated by a 
    # post-build action
    rm -f $post_build_script

    # Use custom provisioning profile and code sign identity if specified, otherwise
    # default to project settings
    # Note: xcodebuild always returns 0 even if the build failed. We look for failure in
    # the stderr output instead
    if [[ ! -z "$2" ]] && [[ ! -z "$3" ]]; then 
        ${XCODEBUILD_CMD} clean archive -scheme $1 -sdk "${TARGET_SDK}" \
        "CODE_SIGN_IDENTITY=$3" "PROVISIONING_PROFILE=$2" 2>$build_errors_file
    else
        ${XCODEBUILD_CMD} clean archive -scheme $1 -sdk "${TARGET_SDK}"
        2>$build_errors_file  
    fi

    errors=`grep -wc "The following build commands failed" $build_errors_file`
    if [ "$errors" != "0" ]
    then
        echo "BUILD FAILED. Error Log:"
        cat $build_errors_file
        rm $build_errors_file
        exit 1
    fi
    rm $build_errors_file

    # Check if archive_paths.sh exists
    if [ -f "$post_build_script" ]; then
        source "$post_build_script"
        if [ -z "$ARCHIVE_PATH" ]; then
            echo "'$post_build_script' exists but ARCHIVE_PATH was not set.
              Enabling auto-detection" 
        fi
    fi
    if [ -z "$ARCHIVE_PATH" ]; then
        # This is the format of the xcarchive path:
        # /Users/$USER/Library/Developer/Xcode/Archives/`date +%Y-%m-%d`/$1\ 
        # `date +%d-%m-%Y\ %H.%M`.xcarchive
        # In order to avoid mismatches with the hour/minute of creation of the archive and
        # the current time, we list all archives with the correct target that have been
        # built in the current day (this may fail if the build wraps around midnight) and
        # fetch the correct file with a combination of ls and grep.
        # This script can break only if there are multiple targets with exactly the same
        # name running at the same time.
        EXTRACTED_LINE=$(ls -lrt /Users/$USER/Library/Developer/Xcode/Archives/`date
          +%Y-%m-%d`/ | grep $1\ `date +%d-%m-%Y` | tail -n 1)
        if [ "$EXTRACTED_LINE" == "" ]; then
            echo "Error: couldn't fetch archive path"
            exit 1
        fi
        # ls -lrt prints lines with the following format
        # drwxr-xr-x  5 mario  1306712193  170 25 Jul 17:17 ArchiveTest 25-07-2013
        #   17.17.xcarchive
        # We can split this line with the " " separator and take the latest bit:
        #   17.17.xcarchive
        FILE_NAME_SUFFIX=$(echo $EXTRACTED_LINE | awk '{split($0,a," "); print a[11]}')
        if [ "$FILE_NAME_SUFFIX" == "" ]; then
            echo "Error: couldn't fetch archive path"
            exit 1
        fi
        # Finally, we can put everything together to generate the path to the xcarchive
        ARCHIVE_PATH="/Users/$USER/Library/Developer/Xcode/Archives/`date 
          +%Y-%m-%d`/$1 `date +%d-%m-%Y` $FILE_NAME_SUFFIX/"
    fi

    # Create output folder if it doesn't already exist
    mkdir -p "$OUTPUT"

    # Move archived xcarchive build to designated output folder
    mv -v "$ARCHIVE_PATH" "$OUTPUT"
}


# Check number of command line args
if [ $# -lt 1 ]; then
    echo "Syntax: `basename $0` <target name> [/path/to/provisioning-profile]
      [<code sign identity]"
    exit 1
fi

if [ ! -z "$2" ]; then
    PROVISIONING_PROFILE="$2"
fi

if [ ! -z "$3" ]; then
    SIGN_PROVISIONING_PROFILE="$3"
else
    if [ ! -z "$PROVISIONING_PROFILE" ]; then
        SIGN_PROVISIONING_PROFILE=$(cat "$PROVISIONING_PROFILE" | egrep -a -o
          '[A-Fa-f0-9]{8}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{12}')
    fi
fi


archive "$1" "$PROVISIONING_PROFILE" "$SIGN_PROVISIONING_PROFILE"

Full source code with an example Xcode project can be found here:

https://github.com/bizz84/Xcode-xcarchive-command

bizz84
  • 1,964
  • 21
  • 34