Search code examples
macoscommand-linexcode4buildbuild-process

Distributing .app file after command line xcodebuild call


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.


Solution

  • 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.