Your iOS app's size under control. Caliper analyzes bundle sizes down to the module level, tracks team ownership, and generates beautiful interactive reports. Know exactly what's taking up space, who owns it, and where to optimize—all from a single command.
|
Module Size Breakdown |
Size Insights |
Module Ownership |
# Build
swift build -c release
# Analyze IPA with all features
.build/release/caliper \
--ipa-path MyApp.ipa \
--link-map-path MyApp-LinkMap.txt \
--ownership-file module-ownership.yml \
--package-resolved-path Package.resolvedGenerates report.json and report.html in the current directory.
- Binary Size Analysis - Accurate per-module binary sizes from LinkMap files
- Asset Tracking - Detailed breakdown of images, storyboards, and resources
- Module Ownership - Track which team owns which modules
- Version Tracking - Swift package version information from Package.resolved
- Interactive Reports - Searchable HTML reports with filtering and sorting
- Size Metrics - Both compressed (IPA) and uncompressed (installed) sizes
- Automatic App Detection - Identifies and tags main app module automatically
git clone https://github.com/kibotu/caliper.git
cd caliper
swift build -c release
# Binary will be at: .build/release/calipermake install
# Installs to /usr/local/bin/caliper# Install
mint install kibotu/caliper
# Run
mint run kibotu/caliper --ipa-path MyApp.ipa
# Or install globally
mint install kibotu/caliper@main
caliper --ipa-path MyApp.ipaMinimum required input - analyzes bundle structure and resources:
.build/release/caliper --ipa-path MyApp.ipaAdd LinkMap for accurate per-module binary sizes:
.build/release/caliper \
--ipa-path MyApp.ipa \
--link-map-path MyApp-LinkMap.txtHow to generate LinkMap:
- Xcode → Build Settings
- Search for "Write Link Map File"
- Set to
YES - Build your app
- Find LinkMap at:
~/Library/Developer/Xcode/DerivedData/YourApp-xxx/Build/Intermediates.noindex/YourApp.build/Release-iphoneos/YourApp.build/YourApp-LinkMap-normal-arm64.txt
Track which team owns which modules:
.build/release/caliper \
--ipa-path MyApp.ipa \
--link-map-path MyApp-LinkMap.txt \
--ownership-file module-ownership.ymlmodule-ownership.yml example:
# Pattern matching with wildcards
- identifier: "*CoreFeature*"
owner: Core Team
internal: true
- identifier: "MyApp"
owner: App Team
internal: true
- identifier: "ThirdParty*"
owner: ExternalPattern syntax:
*= any characters?= single characterinternal: true= marks as first-party codeowner= team/group name for reporting
Note: The main app module is automatically tagged with owner: "App" and internal: true even without an ownership file.
Include Swift package version information:
.build/release/caliper \
--ipa-path MyApp.ipa \
--link-map-path MyApp-LinkMap.txt \
--package-resolved-path Package.resolvedWhere to find Package.resolved:
- Xcode projects:
YourProject.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved - SPM projects:
Package.resolvedin project root
For namespaced packages (e.g., internal packages):
.build/release/caliper \
--ipa-path MyApp.ipa \
--link-map-path MyApp-LinkMap.txt \
--package-resolved-path Package.resolved \
--package-mapping-file package-name-mapping.ymlpackage-name-mapping.yml example:
- moduleName: AdjustSDK
packageIdentity: com.company.adjust-sdk
- moduleName: InternalCore
packageIdentity: internal.core-framework| Parameter | Required | Description |
|---|---|---|
--ipa-path |
✅ Yes | Path to the IPA file to analyze |
--link-map-path |
⬜ No | Path to LinkMap file for accurate binary sizes |
--ownership-file |
⬜ No | YAML file with module ownership patterns |
--package-resolved-path |
⬜ No | Path to Package.resolved for version tracking |
--package-mapping-file |
⬜ No | YAML file for namespaced package mappings |
| Data Type | Source | Compression | Notes |
|---|---|---|---|
| Module Names | IPA structure (.framework, .bundle) | - | Extracted from bundle hierarchy |
| Binary Sizes | LinkMap file | Uncompressed | Compiled code size per module |
| Asset Sizes | IPA + .car files | Both | Images: compressed (IPA) + uncompressed (installed) |
| Resource Files | IPA archive | Compressed | .plist, .strings, .nib, .storyboardc, etc. |
| Package Versions | Package.resolved | - | Swift package dependency versions |
| Total IPA Size | IPA file | Compressed | Download/App Store size |
| Total Install Size | Unzipped IPA | Uncompressed | Actual installed app size |
| Asset Catalog Details | .car files via assetutil | Uncompressed | Parsed with xcrun assetutil |
- Compressed (in IPA): What users download from App Store
- Uncompressed: Actual size on device after installation
- Binary sizes (from LinkMap): Always uncompressed executable code
- Asset compression: Varies by file type (PNG, JPEG, etc.)
{
"app": {
"name": "MyApp",
"version": "1.2.3",
"bundleId": "com.company.myapp"
},
"modules": {
"CoreModule": {
"name": "CoreModule",
"owner": "Core Team",
"internal": true,
"version": "2.1.0",
"binarySize": 1234567,
"imageSize": 234567,
"imageFileSize": 345678,
"proguard": 2345678,
"resources": {
"png": { "size": 123456, "count": 42 },
"storyboardc": { "size": 45678, "count": 3 }
},
"top": {
"Assets.car": 98765,
"Background.png": 12345
}
}
},
"totalPackageSize": 12345678,
"totalInstallSize": 23456789
}Field descriptions:
| Field | Unit | Description |
|---|---|---|
binarySize |
bytes | Compiled code size (from LinkMap) |
imageSize |
bytes | Compressed image assets in IPA |
imageFileSize |
bytes | Uncompressed image assets |
proguard |
bytes | Total uncompressed module size |
resources |
object | File types with size and count |
top |
object | Top 30 largest files in module |
totalPackageSize |
bytes | IPA file size (compressed) |
totalInstallSize |
bytes | Installed app size (uncompressed) |
Interactive web interface with:
- 🔍 Search and filter modules
- 📊 Sort by size, binary size, or name
- 📂 Expandable module details
- 🎨 Resource breakdowns by file type
- 📈 Top 10 largest files per module
- 👥 Filter by owner/team
- 🏷️ Internal vs external module filtering
pipeline {
agent any
stages {
stage('Build App') {
steps {
// Your app build steps here
sh 'xcodebuild -configuration Release ...'
}
}
stage('Analyze App Size') {
steps {
// Clone and build Caliper
dir('caliper') {
git url: 'https://github.com/kibotu/caliper.git'
sh 'swift build -c release'
}
// Run analysis
sh '''
caliper/.build/release/caliper \
--ipa-path build/MyApp.ipa \
--link-map-path build/LinkMap.txt \
--ownership-file config/module-ownership.yml \
--package-resolved-path Package.resolved
'''
// Archive reports
archiveArtifacts artifacts: 'report.json,report.html', allowEmptyArchive: false
// Publish HTML report
publishHTML([
reportDir: '.',
reportFiles: 'report.html',
reportName: 'App Size Report',
keepAll: true,
alwaysLinkToLastBuild: true
])
}
}
stage('Check Size Thresholds') {
steps {
script {
// Parse JSON and check thresholds
def report = readJSON file: 'report.json'
def maxSize = 100 * 1024 * 1024 // 100 MB
if (report.totalPackageSize > maxSize) {
error "App size ${report.totalPackageSize} exceeds threshold ${maxSize}"
}
}
}
}
}
}name: App Size Analysis
on:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
analyze:
runs-on: macos-13
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: '15.0'
- name: Build App
run: |
xcodebuild -workspace MyApp.xcworkspace \
-scheme MyApp \
-configuration Release \
-archivePath build/MyApp.xcarchive \
archive
xcodebuild -exportArchive \
-archivePath build/MyApp.xcarchive \
-exportPath build \
-exportOptionsPlist ExportOptions.plist
- name: Checkout Caliper
uses: actions/checkout@v4
with:
repository: kibotu/caliper
path: caliper
- name: Build Caliper
run: |
cd caliper
swift build -c release
- name: Analyze App Size
run: |
caliper/.build/release/caliper \
--ipa-path build/MyApp.ipa \
--link-map-path build/LinkMap.txt \
--ownership-file config/module-ownership.yml \
--package-resolved-path MyApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
- name: Upload Reports
uses: actions/upload-artifact@v4
with:
name: size-reports
path: |
report.json
report.html
retention-days: 90
- name: Comment PR with Size
if: github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const report = JSON.parse(fs.readFileSync('report.json', 'utf8'));
const sizeMB = (report.totalPackageSize / 1024 / 1024).toFixed(2);
const installMB = (report.totalInstallSize / 1024 / 1024).toFixed(2);
const body = `## 📊 App Size Report
- **IPA Size:** ${sizeMB} MB
- **Install Size:** ${installMB} MB
- **Modules:** ${Object.keys(report.modules).length}
[View detailed report](../actions/runs/${context.runId})`;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: body
});Caliper requires the following command-line tools to be installed:
| Tool | Usage | Installation | Version Check |
|---|---|---|---|
| Swift | Build Caliper | Xcode Command Line Tools | swift --version |
| unzip | Extract IPA files | Pre-installed on macOS | unzip -v |
| xcrun | Run Xcode tools | Xcode Command Line Tools | xcrun --version |
| assetutil | Parse .car asset catalogs | Part of iOS SDK | xcrun --sdk iphoneos assetutil --version |
xcode-select --install# Check Swift
swift --version
# Should show: Swift version 6.0 or later
# Check unzip
which unzip
# Should show: /usr/bin/unzip
# Check xcrun
xcrun --version
# Should show: xcrun version X.X
# Check assetutil availability
xcrun --sdk iphoneos assetutil --version 2>/dev/null && echo "✅ assetutil available" || echo "❌ assetutil not found"- macOS 14.0 or later
- Xcode 16.0 or later
- Swift 6.0 or later
- Xcode Command Line Tools (includes unzip, xcrun, assetutil)
Before diving into analysis, make sure you have the right build configuration. Here's a quick checklist to maximize the value you'll get from Caliper:
Build Settings (for Release configuration):
-
LD_GENERATE_MAP_FILE=YES(enables LinkMap) -
DEAD_CODE_STRIPPING=YES(removes unused code) -
STRIP_INSTALLED_PRODUCT=YES(strips debug symbols) -
STRIP_SWIFT_SYMBOLS=YES(removes reflection metadata) -
SWIFT_OPTIMIZATION_LEVEL=-Osize(if size > speed) - Build configuration = Release (Debug builds are much larger!)
Build Command:
- Using
CODE_SIGNING_REQUIRED=NOfor faster CI builds - Building with
ONLY_ACTIVE_ARCH=NOto include all architectures - Using
-sdk iphoneos(not simulator)
Optional but Valuable:
- Create
module-ownership.ymlto map modules to teams - Create
package-name-mapping.ymlif you use forked dependencies - Save Package.resolved for version tracking
"I can't find the LinkMap file!"
- Make sure you built with Release configuration (not Debug)
- Check that
LD_GENERATE_MAP_FILEis set toYESin Build Settings - Clean your build folder and rebuild to ensure it generates fresh
- Use this command to locate it:
find ~/Library/Developer/Xcode/DerivedData -name "*LinkMap-normal-arm64.txt" -type f - The path varies between regular builds and archives—check both locations mentioned earlier
- Verify you're building for device (
-sdk iphoneos), not simulator
"My build fails with code signing errors"
- If using unsigned builds, check for required entitlements (HealthKit, Apple Pay, iCloud, etc.)
- These entitlements require proper code signing—you can't skip it
- Options:
- Create a separate "size-analysis" scheme without these entitlements
- Use proper code signing (slower but necessary)
- Temporarily remove the capabilities for analysis builds
"My binary size seems wrong or too small"
- LinkMap only shows compiled code, not resources or embedded frameworks
- Make sure you're analyzing arm64 (device), not x86_64/arm64 (simulator)
- Debug builds are 2-3x larger than Release—always use Release for analysis
- If the number seems too small, you might be missing dynamic frameworks
- Universal builds include multiple architectures—check you're analyzing the right one
"Asset sizes don't match what I expect"
- Asset catalog sizes shown by
assetutilare uncompressed (installed size on device) - IPA sizes are compressed (download size from App Store)
- Both numbers are correct, they just measure different things
- App Thinning further reduces what users actually download
- Example: 50 MB assets → 30 MB in IPA → 20 MB after thinning for specific device
"Modules aren't matching teams in the ownership report"
- Check your
module-ownership.ymlpatterns—are they specific enough? - Module names might not match what you expect—check the report to see actual names first
- Wildcards are your friend:
*Feature*is more forgiving than exact matches - Order matters: more specific patterns should come before generic ones
- SPM packages often have different module names than repository names
"The HTML report won't open or looks broken"
- Make sure you're opening
report.htmlin a modern browser (Chrome, Firefox, Safari) - Check that
report.jsonexists in the same directory - Some browsers block local file access—try hosting it with
python3 -m http.serverand open via localhost - If the JSON is very large (>50 MB), it might be slow to load—be patient
"Caliper crashes or hangs"
- Very large LinkMap files (>500 MB) can be slow to parse—give it time
- Make sure you have enough RAM (LinkMap parsing can use 2-3 GB for large apps)
- Check that your input files aren't corrupted (try opening them manually)
- If analyzing a massive app (>1 GB), expect analysis to take several minutes
Solution: Install full Xcode (not just Command Line Tools):
# Install from Mac App Store or:
xcode-select --install
sudo xcode-select --switch /Applications/Xcode.appSolution: Enable LinkMap generation in Xcode:
- Project Settings → Build Settings
- Search: "Write Link Map File"
- Set to:
YES - Clean and rebuild
Solution: Use --package-mapping-file to map module names to package identities.
Solution: The tool processes files sequentially. Progress is shown in the terminal. For very large IPAs (>500MB), analysis may take 2-5 minutes.
Inspired by Spotify's Ruler - adapted for iOS with native Swift implementation and iOS-specific features like asset catalog parsing and LinkMap analysis.
Contributions welcome! This tool helps iOS teams monitor and optimize app size metrics.
Copyright 2025 Jan Rabe & CHECK24
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.