Skip to content

Instantly share code, notes, and snippets.

@ivanopcode
Last active November 6, 2024 18:17
Show Gist options
  • Save ivanopcode/d0219b082625584b1de215c1049a5522 to your computer and use it in GitHub Desktop.
Save ivanopcode/d0219b082625584b1de215c1049a5522 to your computer and use it in GitHub Desktop.
A shell script to convert Xcode archives to iOS App Package (.ipa) files
#!/bin/zsh
# mkipa - iOS Archive to IPA Converter
# Copyright (c) 2024 Ivan Oparin
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# mkipa - A tool to convert Xcode archives (.xcarchive) to iOS App Store Package (.ipa) files
#
# This script simplifies the process of creating IPA files from Xcode archives.
# It automatically extracts app information from the archive, creates the required
# directory structure, and packages the app into an IPA file with proper naming.
#
# Why is this useful?
# While Xcode provides GUI options for exporting signed IPAs, it doesn't offer a way
# to export unsigned builds. This tool fills that gap by allowing you to create IPAs
# for self-signing distribution, side-loading or verifiable builds.
#
# Features:
# - Extracts app version and build number from archive
# - Creates properly structured IPA with Payload directory
#
# Usage:
# mkipa <path_to_xcarchive> <output_directory> [-v|--verbose]
#
# Arguments:
# path_to_xcarchive: Path to the .xcarchive file
# output_directory: Directory where the IPA will be saved
# -v, --verbose: Optional flag to show detailed output
#
# Example:
# mkipa ~/Library/Developer/Xcode/Archives/2024-11-06/MyApp.xcarchive ~/Downloads
#
# Requirements:
# - macOS
# - Xcode Command Line Tools (for PlistBuddy)
# - ditto and zip commands
if [ "$#" -lt 2 ]; then
echo "Usage: $0 <path_to_xcarchive> <output_directory> [-v|--verbose]"
exit 1
fi
ARCHIVE_PATH="${1:A}"
OUTPUT_DIR="${2:A}"
VERBOSE=false
if [[ "${3:-}" == "-v" ]] || [[ "${3:-}" == "--verbose" ]]; then
VERBOSE=true
fi
# Spinner animation
spinner() {
local pid=$1
local delay=0.1
local spinstr='⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏'
printf "\r%s " "$2"
while [ "$(ps a | awk '{print $1}' | grep $pid)" ]; do
local temp=${spinstr#?}
printf "%c" "$spinstr"
local spinstr=$temp${spinstr%"$temp"}
sleep $delay
printf "\b"
done
printf "\r%s ✓\n" "$2"
}
log_verbose() {
if $VERBOSE; then
echo "$1"
fi
}
# Extract info
echo "Reading archive info..."
APP_PATH=$(/usr/libexec/PlistBuddy -c "Print :ApplicationProperties:ApplicationPath" "${ARCHIVE_PATH}/Info.plist")
VERSION=$(/usr/libexec/PlistBuddy -c "Print :ApplicationProperties:CFBundleShortVersionString" "${ARCHIVE_PATH}/Info.plist")
BUILD=$(/usr/libexec/PlistBuddy -c "Print :ApplicationProperties:CFBundleVersion" "${ARCHIVE_PATH}/Info.plist")
APP_NAME=$(basename "$APP_PATH" .app)
log_verbose "Detected values:"
log_verbose "ARCHIVE_PATH: $ARCHIVE_PATH"
log_verbose "APP_PATH: $APP_PATH"
log_verbose "VERSION: $VERSION"
log_verbose "BUILD: $BUILD"
log_verbose "APP_NAME: $APP_NAME"
# Setup directories
TMP_DIR=$(mktemp -d)
mkdir -p "${TMP_DIR}/Payload"
SOURCE="${ARCHIVE_PATH}/Products/${APP_PATH}"
DEST="${TMP_DIR}/Payload/"
if [ ! -d "$SOURCE" ]; then
echo "Error: Source directory does not exist: $SOURCE"
exit 1
fi
# Copy app
echo "Copying app bundle..."
(ditto "$SOURCE" "${DEST}/${APP_NAME}.app" > /dev/null 2>&1) &
spinner $! "Copying app bundle"
if [ ! -d "${TMP_DIR}/Payload/${APP_NAME}.app" ]; then
echo "Error: App was not copied correctly"
exit 1
fi
# Create IPA
cd "${TMP_DIR}"
OUTPUT_NAME="${APP_NAME}_${VERSION}+${BUILD}.ipa"
OUTPUT_PATH="${OUTPUT_DIR}/${OUTPUT_NAME}"
echo "Creating IPA archive..."
if $VERBOSE; then
zip -r "$OUTPUT_PATH" Payload/
else
(zip -r "$OUTPUT_PATH" Payload/ > /dev/null) &
spinner $! "Creating IPA archive"
fi
if [ ! -f "$OUTPUT_PATH" ]; then
echo "Error: IPA was not created"
exit 1
fi
# Cleanup
rm -rf "${TMP_DIR}"
echo "Successfully created IPA: $OUTPUT_PATH"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment