Utilities for migrating an Xcode project from classic PBXGroup entries to filesystem-synchronized folders (PBXFileSystemSynchronizedRootGroup).
The repository is organized as a set of standalone Python scripts. Some are low-level step scripts, and some are umbrella/orchestration scripts that run multiple steps in sequence.
The migration flow implemented here is:
- Read the current group tree from
project.pbxproj. - Extract target membership information for every file in a group.
- Move files on disk so the filesystem matches the Xcode group hierarchy.
- Remove the old
PBXGroupand old file/build references fromproject.pbxproj. - Add a filesystem-synchronized folder reference back to the project.
- Recreate target membership behavior using
PBXFileSystemSynchronizedBuildFileExceptionSet. - Validate the resulting project structure and memberships.
These scripts are not generic yet. Several defaults are hard-coded for the current project:
- Project file:
ReaddleDocs2.xcodeproj/project.pbxproj - Workspace:
ReaddleDocs2.xcworkspace - Root group being migrated from:
Classes - Commit prefix used by batch scripts:
[DOC-15385] - migrate_batched.pybuilds theDocumentsscheme on an iOS simulator after each batch
If you want to use the scripts on another project, adjust those constants in the scripts first.
- Python 3
gitxcodebuild/ Xcode command line tools- A clean understanding of what is inside
Classes/, because some scripts move files and some orchestration scripts also reset or commit git state
migrate_all.py is the top-level script for running a full migration over many groups listed in an input file.
Usage:
python migrate_all.py [--verbose] <groups_file>Input file format:
UUID GroupName
It also accepts the Groups (...) section produced by list_groups.py.
What it does:
- Reads the list of groups to migrate.
- Migrates one group at a time.
- For each successful migration, runs
validate_project.py. - Commits successful migrations automatically.
- Rolls back failed migrations automatically.
- Writes progress to
<groups_file>.result.txt.
Options:
--verbose: runs each step with full subprocess output instead of compact status lines.
Important side effects:
- Stages and commits
ReaddleDocs2.xcodeproj/project.pbxprojandClasses/after each successful group. - On failure, discards changes in the project file and
Classes/.
Recommended workflow:
- Generate a candidate list with
list_groups.py. - Review and trim that list manually.
- Run
migrate_all.pyon the reviewed list. - Review the result file and the generated commits.
Single-group umbrella migration. This is the main entrypoint when you want to migrate one group end-to-end.
Usage:
python migrate_group.py [options] <group_path>
python migrate_group.py --restore-membership <membership.json> <folder_name>What it does in normal mode:
- Extracts memberships with
extract_target_memberships.py. - Moves files with
move_files_to_group_structure.py. - Removes the old group with
remove_old_group.py. - Restores the folder as a synced root group with
restore_target_memberships.py. - Validates and fixes memberships with
validate_memberships.py --fix.
Options:
--verbose: prints full output from each step.--dry-run: performs extraction only and prints a detailed report; does not modify files or the project.--group-id ID: disambiguates groups that share the same display name by forcing a specific UUID.--new-folder: if the target folder already exists, creates a new folder name with a numeric suffix instead of reusing the existing one.--restore-membership <file>: switches the script into repair mode; restores memberships for an already-existing synced folder using a simple membership JSON file.
Arguments:
<group_path>: group path likeClasses/Intents.<folder_name>in restore mode: the synced folder name to repair.
Generated artifacts:
/tmp/xcode_target_memberships_<group>.json/tmp/xcode_membership_<group>.json
Batch migrator with build verification and bisection. Use this when you want automatic build checks after each batch of migrations.
Usage:
python migrate_batched.py [--batch-size N] <groups_file>What it does:
- Splits the input list into batches.
- Migrates every group in a batch.
- Runs
xcodebuildafter the batch. - If the build fails, resets back to the pre-batch commit and recursively bisects the batch to identify the group that breaks the build.
- Commits successful groups.
- Writes results to
<groups_file>.batch_result.txt.
Options:
--batch-size N: number of groups to process before each build check. Default is10.
Important side effects:
- Uses git commits for successful migrations.
- Uses git rollback on failure.
- Uses
git reset --hardduring batch bisection. - Runs
git clean -fd Classes/.
Lists all direct children of the Classes group and separates them into classic groups, synced folders, and plain files.
Usage:
python list_groups.py [project_path]Arguments:
[project_path]: optional path toproject.pbxproj. Defaults toReaddleDocs2.xcodeproj/project.pbxproj.
Options:
- No named options.
Typical use:
python list_groups.py > groups.txtCompares one Xcode group against the corresponding directory on disk and reports differences.
Usage:
python audit_group.py [--group-id <UUID>] <group_path>What it does:
- Resolves the target group from the project file.
- Walks the Xcode group tree and the filesystem tree.
- Reports files or directories that exist only in Xcode or only on disk.
- Detects when the target is already a synced folder.
Options:
--group-id <UUID>: disambiguates duplicate group names.
Arguments:
<group_path>: path likeClasses/SmartNotifications.
Extracts the file inventory and target membership model for one group from project.pbxproj.
Usage:
python extract_target_memberships.py <group_name> <project_path> [--parent-path <path>] [--group-id <UUID>]What it writes:
/tmp/xcode_target_memberships_<group>.json: detailed extraction payload used by later steps./tmp/xcode_membership_<group>.json: simplifiedfile -> [targets]mapping.
Options:
--parent-path <path>: parent folder used to resolve on-disk paths. Default isClasses.--group-id <UUID>: disambiguates duplicate group names by forcing a specific group UUID.
Arguments:
<group_name>: leaf group name, not fullClasses/...path.<project_path>: path toproject.pbxproj.
Moves files on disk so the filesystem layout matches the relative paths stored in the extraction JSON.
Usage:
python move_files_to_group_structure.py <json_path> [--folder-name <name>]What it does:
- Creates missing directories.
- Uses
git mvfor tracked files. - Falls back to a normal move plus
git addfor untracked files. - Warns if extra unexpected items are present in the destination folder.
Options:
--folder-name <name>: overrides the destination top-level folder name, mainly for conflict resolution when the original name cannot be reused.
Arguments:
<json_path>: path to the detailed extraction JSON.
Deletes the old group representation from project.pbxproj.
Usage:
python remove_old_group.py <json_path> <project_path>What it removes:
PBXBuildFileentries- build phase references
PBXFileReferenceentriesPBXGroupentries- nested synced-folder references collected during extraction
Options:
- No named options.
Arguments:
<json_path>: path to the detailed extraction JSON.<project_path>: path toproject.pbxproj.
Creates or updates a filesystem-synchronized root group and recreates target-specific exclusions.
Usage:
python restore_target_memberships.py <json_path> <project_path> <group_name> [--folder-path <path>] [--dry-run]What it does:
- Adds a
PBXFileSystemSynchronizedRootGroup. - Creates
PBXFileSystemSynchronizedBuildFileExceptionSetentries. - Links the synced group into relevant targets.
- Writes the updated project file unless dry-run mode is enabled.
Options:
--folder-path <path>: overrides the folder path recorded in the new synced group, useful when the on-disk folder name differs from the original group name.--dry-run: computes and prints the operation without writing the modified project file.
Arguments:
<json_path>: path to the detailed extraction JSON.<project_path>: path toproject.pbxproj.<group_name>: display name for the synced group added back to the project.
Checks whether the synced-folder exception sets reproduce the original target memberships.
Usage:
python validate_memberships.py [--fix] <json_path> <project_path>What it does:
- Compares expected memberships from the extraction JSON with the current project file.
- Reports mismatches and extra files.
- Can rewrite exception sets and then re-run validation.
Options:
--fix: repairs mismatches by rewriting exception sets before validating again.
Arguments:
<json_path>: path to the detailed extraction JSON.<project_path>: path toproject.pbxproj.
Checks whether Xcode can still parse the workspace or project.
Usage:
python validate_project.py [workspace_or_project_path]What it does:
- Runs
xcodebuild -list. - Treats both command failure and
Unable to open project fileas validation failure.
Arguments:
[workspace_or_project_path]: optional.xcworkspaceor.xcodeprojpath. Defaults toReaddleDocs2.xcworkspace.
Options:
- No named options.
Lower-level helper that adds exception sets to an already-existing synced root group.
Usage:
python add_target_exceptions.py <json_path> <project_path> <group_name>What it does:
- Reads the extracted membership JSON.
- Computes files excluded from each target.
- Adds
PBXFileSystemSynchronizedBuildFileExceptionSetentries. - Updates the target synced group to reference those exception sets.
Options:
- No named options.
Arguments:
<json_path>: path to the detailed extraction JSON.<project_path>: path toproject.pbxproj.<group_name>: synced group name to update.
This script is effectively a low-level repair utility. In the normal migration flow, restore_target_memberships.py already handles this work.
python migrate_group.py --dry-run "Classes/Intents"
python migrate_group.py --verbose "Classes/Intents"python list_groups.py > groups.txt
python migrate_all.py --verbose groups.txtpython migrate_batched.py --batch-size 5 groups.txtpython migrate_group.py --restore-membership /tmp/xcode_membership_Statistics.json Statisticsmigrate_all.pyandmigrate_batched.pymodify git state automatically.migrate_batched.pyis the most invasive script because it performs hard resets during bisection.- The extraction and restore steps rely on the current structure of
project.pbxproj; if Xcode changes its serialization format, these scripts may need updates. - Most scripts assume the migration root is
Classes.