Migrating from one project to another using python

I’ve been working a lot recently in migrating from a project to another, troubleshooting can be hard when you get stuck in a infinite uncommunicative progressbar “migrating…”. I created a custom python script that run in Unreal Engine Headless


TL;DR

  • run_migrate.ps1

    • run it from the source project folder;
    • it auto‑detects:
      • the current .uproject;
      • the target project at ..\game\*.uproject; (change this at your needs)
      • the engine path (reads EngineAssociation engine version from the uproject and checks the corresponding engine location from the windows registry)
    • sets UE_MIGRATE_DRYRUN and launches UnrealEditor‑Cmd.exe with
      -run=pythonscript -script="migrate.py";
    • the -DryRun switch = safe preview (no files copied).
  • migrate.py (runs inside the editor‑cmd)

    1. loads the Asset Registry and gathers every package under /Game/FolderToMigrate;
    2. walks only “hard” dependencies (textures, materials, etc.) and adds them to the list;
    3. filters anything not in ALLOWED_PREFIXES (or that matches EXCLUDED_PREFIXES);
    4. if UE_MIGRATE_DRYRUN==1 (or --dry-run) prints the list and exits — otherwise asks for TTY confirmation;
    5. finally calls AssetToolsHelpers.get_asset_tools().migrate_packages() with ignore_dependencies=True
      because we already resolved them ourselves.

:one: run_migrate.ps1

<#
 Usage:   .\run_migrate.ps1            → migrate with confirmation
          .\run_migrate.ps1 -DryRun   → preview, no files copied
#>

param(
    [Alias('dry-run','dry_run')][switch]$DryRun
)

function Write-Info   { param($m) ; Write-Host $m -ForegroundColor Cyan    }
function Write-Ok     { param($m) ; Write-Host $m -ForegroundColor Green   }
function Write-Warn   { param($m) ; Write-Host $m -ForegroundColor Yellow  }
function Write-ErrorC { param($m) ; Write-Host $m -ForegroundColor Red     }

# Locate source & target .uproject
$uproject    = Get-ChildItem $PSScriptRoot -Filter *.uproject | Select-Object -First 1
$destRoot    = Join-Path $PSScriptRoot "..\game"
$destUproject= Get-ChildItem $destRoot -Filter *.uproject -Recurse | Select-Object -First 1

# Resolve engine folder from EngineAssociation
$engineAssoc = (Get-Content $uproject.FullName -Raw | ConvertFrom-Json).EngineAssociation
function Get-EnginePath($assoc){
    $reg="Registry::HKEY_LOCAL_MACHINE\SOFTWARE\EpicGames\Unreal Engine\$assoc"
    if(Test-Path $reg){ return (Get-ItemProperty $reg -Name 'InstalledDirectory').InstalledDirectory }
}
$engineRoot  = Get-EnginePath $engineAssoc
$editorCmd   = Join-Path $engineRoot "Engine\Binaries\Win64\UnrealEditor-Cmd.exe"

# Wrapper for dry‑run var/args
$pyScript = Join-Path $PSScriptRoot "migrate.py"
$env:UE_MIGRATE_DRYRUN = $DryRun ? "1" : "0"

Write-Info  "┌──── ASSET MIGRATION ──────────────────────────────"
Write-Info  " Source Project : $($uproject.Name)"
Write-Info  " Target Project : $($destUproject.Name)"
Write-Info  " Engine         : $engineAssoc"
Write-Info  " Mode           : $(if($DryRun){'DRY‑RUN'}else{'LIVE'})"
Write-Info  "└───────────────────────────────────────────────────"

& "$editorCmd" $uproject.FullName `
        -stdout -FullStdOutLogOutput `
        -run=pythonscript -script="$pyScript"

if($LASTEXITCODE -ne 0){ Write-ErrorC "❌ ExitCode $LASTEXITCODE"; exit $LASTEXITCODE }
Write-Ok "✅ Done!"

:two: migrate.py

# --dry-run  → prints the package list and exits
# no flag    → asks for confirmation then migrates
import unreal, sys, os
from pathlib import Path

DRY_RUN = os.getenv("UE_MIGRATE_DRYRUN") == "1" or "--dry-run" in sys.argv

SOURCE_FOLDER     = "/Game/FolderToMigrate"
ALLOWED_PREFIXES  = [SOURCE_FOLDER]    # add more folders here
EXCLUDED_PREFIXES = []                 # or exclude them here

# Resolve folders
script_dir   = Path(__file__).resolve().parent
dest_content = (script_dir.parent / "game" / "Content").resolve()

log = unreal.log
reg = unreal.AssetRegistryHelpers.get_asset_registry()
queue = list({a.package_name for a in reg.get_assets_by_path(SOURCE_FOLDER, recursive=True)})
pkg_set = set(queue)

dep_opts = unreal.AssetRegistryDependencyOptions(
    include_soft_package_references=False,
    include_hard_package_references=True,
    include_searchable_names=False,
    include_soft_management_references=False,
    include_hard_management_references=False)

while queue:
    pkg = queue.pop()
    for dep in reg.get_dependencies(pkg, dep_opts):
        if dep not in pkg_set:
            pkg_set.add(dep); queue.append(dep)

def is_allowed(p):  # filter sub‑folders + excludes
    s = str(p)
    return any(s.startswith(ap) for ap in ALLOWED_PREFIXES) and \
           not any(s.startswith(ep) for ep in EXCLUDED_PREFIXES)

filtered = [p for p in pkg_set if is_allowed(p)]
log(f"[MIG] {len(filtered)} package(s) selected")

if DRY_RUN:
    unreal.log_warning("[MIG] ** DRY‑RUN **")
    for p in sorted(filtered): log(f"  {p}")
    sys.exit(0)

if os.isatty(0) and input("Proceed? [y/N] ").lower()!="y":
    sys.exit(0)

tools = unreal.AssetToolsHelpers.get_asset_tools()
opts  = unreal.MigrationOptions(); opts.ignore_dependencies = True
tools.migrate_packages(filtered, str(dest_content), options=opts)

log("[MIG] ✅ Migration completed"); print("✅ Done.")

How to use / customise

  1. Copy both scripts into the source project root.
  2. Create (or clone) the target project in ..\game\ relative to the scripts.
  3. In Python edit:
    • SOURCE_FOLDER if you want to migrate a different folder;
    • add/remove paths in ALLOWED_PREFIXES or EXCLUDED_PREFIXES.
  4. Open PowerShell and run:
.\run_migrate.ps1 -DryRun   # preview only
.\run_migrate.ps1           # actual copy (asks for confirmation)

Handy notes

  • No external plugins required: it relies only on AssetRegistry and AssetTools exposed by UE‑Python.
  • Soft dependencies (BP references, data‑only BPs, etc.) are ignored by design. If you need them, just tweak the flags in dep_opts.
  • Tested and works great on UE 5.6; some apis might change
  • If you prefer running the Python script from the editor UI, simply open any level and use Window :play_button: Developer Tools :play_button: Output Log
    py "migrate.py --dry-run".

Hope this helps someone out there!