I'm building/archiving my Mac app for distribution from a command line call (below), with Xcode 4.3 installed. To be clear, I didn't have a working solution for this problem earlier to Xcode 4.3, so advice for earlier Xcode releases could easily still be valid. Here's the call:
/usr/bin/xcodebuild -project "ProjectPath/Project.pbxproj" -scheme "Project" -sdk macosx10.7 archive
This runs successfully, and it generates an .xcarchive
file, located in my ~/Library/Developer/Xcode/Archives/<date>
folder. What's the proper way to get the path the the archive file generated? I'm looking for a way to get a path to the .app
file contained therein, so I can distribute it.
I've looked at the MAN page for xcodebuild
(and done copious searching online) and didn't find any clues there.
Building on the answer provided here, I came up with a satisfactory multi-part solution. The key to it all, was to use the environment variables Xcode creates during the build.
First, I have a post-action on the Archive phase of my build scheme (pasted into the Xcode project's UI). It calls a Python script I wrote (provided in the next section), passing it the names of the environment variables I want to pull out, and a path to a text file:
# Export the archive paths to be used after Archive finishes
"${PROJECT_DIR}/Script/grab_env_vars.py" "${PROJECT_DIR}/build/archive-env.txt"
"ARCHIVE_PATH" "ARCHIVE_PRODUCTS_PATH" "ARCHIVE_DSYMS_PATH"
"INSTALL_PATH" "WRAPPER_NAME"
That script then writes them to a text file in key = value
pairs:
import sys
import os
def main(args):
if len(args) < 2:
print('No file path passed in to grab_env_vars')
return
if len(args) < 3:
print('No environment variable names passed in to grab_env_vars')
output_file = args[1]
output_path = os.path.dirname(output_file)
if not os.path.exists(output_path):
os.makedirs(output_path)
with open(output_file, 'w') as f:
for i in range(2, len(args)):
arg_name = args[i]
arg_value = os.environ[arg_name]
#print('env {}: {}'.format(arg_name, arg_value))
f.write('{} = {}\n'.format(arg_name, arg_value))
def get_archive_vars(path):
return dict((line.strip().split(' = ') for line in file(path)))
if __name__ == '__main__':
main(sys.argv)
Then, finally, in my build script (also Python), I parse out those values and can get to the path of the archive, and the app bundle therein:
env_vars = grab_env_vars.get_archive_vars(ENV_FILE)
archive_path = env_vars['ARCHIVE_PRODUCTS_PATH']
install_path = env_vars['INSTALL_PATH'][1:] #Chop off the leading '/' for the join below
wrapper_name = env_vars['WRAPPER_NAME']
archived_app = os.path.join(archive_path, install_path, wrapper_name)
This was the way I solved it, and it should be pretty easily adaptable to other scripting environments. It makes sense with my constraints: I wanted to have as little code as possible in the project, I prefer Python scripting to Bash, and this script is easily reusable in other projects and for other purposes.