UEScripts/ue4-datasync.ps1

360 lines
12 KiB
PowerShell
Raw Normal View History

[CmdletBinding()] # Fail on unknown args
param (
[string]$mode,
[string]$root,
[string]$src,
[switch]$prune = $false,
[switch]$force = $false,
[switch]$nocloseeditor = $false,
[switch]$dryrun = $false,
[switch]$help = $false
)
function Print-Usage {
2022-04-19 12:07:09 +01:00
Write-Output "Steve's UE Map BuiltData Sync Tool"
Write-Output " Avoid storing Map_BuiltData.uasset files in source control, sync them directly instead"
Write-Output "Usage:"
Write-Output " ue4-datasync.ps1 [-mode:]<push|pull> [[-path:]syncpath] [Options]"
Write-Output " "
Write-Output " -mode : Whether to push or pull the built data from your filesystem"
Write-Output " -root : Root folder to sync files to/from. Project name will be appended to this path."
2022-04-19 12:07:09 +01:00
Write-Output " : Can be blank if specified in UESYNCROOT"
Write-Output " -src : Source folder (current folder if omitted)"
Write-Output " : (should be root of project)"
Write-Output " -prune : Clean up versions of the data older than the latest"
Write-Output " -force : Copy ALL BuiltData files regardless of size/timestamp checks"
Write-Output " -nocloseeditor : Don't close UE4 editor before pulling (may prevent success)"
Write-Output " -dryrun : Don't perform any actual actions, just report on what you would do"
Write-Output " -help : Print this help"
Write-Output " "
Write-Output "Environment Variables:"
2022-04-19 12:07:09 +01:00
Write-Output " UESYNCROOT : Root path to sync data. Subfolders for each project name."
Write-Output " UEINSTALL : Use a specific Unreal install."
Write-Output " : Default is to find one based on project version, under UEROOT"
Write-Output " UEROOT : Parent folder of all binary Unreal installs (detects version). "
Write-Output " : Default C:\Program Files\Epic Games"
Write-Output " "
}
. $PSScriptRoot\inc\ueeditor.ps1
. $PSScriptRoot\inc\filetools.ps1
function Get-Current-Umaps {
# Find all umaps which are tracked in git and get their LFS SHAs
$umapsOutput = git lfs ls-files -l -I *.umap
# Output is of the form
# b75b42e082ffb0deeb3fc7b40b2a221ded62872a2289bf6b63e275372849447b * Content/Maps/Subfolder/MyLevel.umap
foreach ($line in $umapsOutput) {
if ($line -match "^([a-f0-9]+)\s+\*\s+(.+)$") {
$oid = $matches[1]
$filename = $matches[2].Trim()
# returns multiple entries here
# use property bag for convenience
New-Object PSObject -Property @{Filename=$filename;Oid=$oid}
}
}
}
function Get-Remote-Builtdata-Path {
param (
[string]$filename,
[string]$oid,
[string]$syncdir
)
$subdir = [System.IO.Path]::GetDirectoryName($filename)
$basename = [System.IO.Path]::GetFileNameWithoutExtension($filename)
$remotesubdir = Join-Path $syncdir $subdir
$remotebuiltdata = Join-Path $remotesubdir "${basename}_BuiltData_${oid}.uasset"
return $remotebuiltdata
}
function Get-Builtdata-Paths {
param (
[object]$umap,
[string]$syncdir
)
$subdir = [System.IO.Path]::GetDirectoryName($umap.Filename)
$basename = [System.IO.Path]::GetFileNameWithoutExtension($umap.Filename)
$localbuiltdata = Join-Path $subdir "${basename}_BuiltData.uasset"
$remotesubdir = Join-Path $syncdir $subdir
$remotebuiltdata = Join-Path $remotesubdir "${basename}_BuiltData_$($umap.Oid).uasset"
return $localbuiltdata, $remotebuiltdata
}
$ErrorActionPreference = "Stop"
if ($help) {
Print-Usage
Exit 0
}
if ($mode -ne "push" -and $mode -ne "pull") {
Print-Usage
Write-Output "ERROR: Mode must be 'push' or 'pull'"
Exit 3
}
2022-04-19 12:07:09 +01:00
if (-not $root) {
$root = $Env:UESYNCROOT
}
# Backwards compat
if (-not $root) {
$root = $Env:UE4SYNCROOT
}
if (-not $root) {
Print-Usage
2022-04-19 12:07:09 +01:00
Write-Output "ERROR: Missing '-root' argument and no UESYNCROOT env var"
Exit 3
}
if (-not (Test-Path $root -PathType Container)) {
Print-Usage
Write-Output "ERROR: root path $root does not exist"
Exit 3
}
# confirm that umap files are tracked in LFS so SHAs are already there
$lfsTrackOutput = git lfs track
if (!$?) {
Write-Output "ERROR: failed to call 'git lfs track'"
Exit 5
}
$umapsOK = $false
foreach ($line in $lfsTrackOutput) {
if ($line -match "^\s+\*\.umap\s+.*$") {
$umapsOK = $true
break
}
}
if (-not $umapsOK) {
Write-Output "ERROR: .umap files are not tracked in LFS, cannot continue"
Exit 5
}
# Check for changes, ONLY to .umap files
$statusOutput = git status -uno --porcelain *.umap
$modifiedMaps = [System.Collections.ArrayList]@()
foreach ($line in $statusOutput) {
if ($line -match "^(?: [^\s]|[^\s] |[^\s][^\s])\s+(.+)$") {
$filename = $matches[1]
$modifiedMaps.Add($filename) > $null
Write-Warning "Uncommitted change: $filename (will be skipped)"
}
}
$result = 0
try {
if ($src -ne ".") { Push-Location $src }
Write-Output "-- Sync process starting --"
2022-04-19 12:07:09 +01:00
# Locate UE project file
$uprojfile = Get-ChildItem *.uproject | Select-Object -expand Name
if (-not $uprojfile) {
throw "No Unreal project file found in $(Get-Location)! Aborting."
}
if ($uprojfile -is [array]) {
throw "Multiple Unreal project files found in $(Get-Location)! Aborting."
}
# In PS 6.0+ we could use Split-Path -LeafBase but let's stick with built-in PS 5.1
$uprojname = [System.IO.Path]::GetFileNameWithoutExtension($uprojfile)
if ($dryrun) {
Write-Output "Would sync $uprojname"
} else {
Write-Output "Syncing $uprojname"
}
2022-04-19 12:07:09 +01:00
# Close UE as early as possible in pull mode
if ($mode -eq "pull" -and -not $nocloseeditor) {
2022-04-19 12:07:09 +01:00
# Check if UE is running, if so try to shut it gracefully
if ($dryrun) {
Write-Output "Would have closed UE Editor"
} else {
Close-UE-Editor $uprojname $dryrun
}
}
# Create project sync dir if necessary
$syncdir = Join-Path $root $uprojname
New-Item -ItemType Directory $syncdir -Force > $null
Write-Output "Sync project folder: $syncdir"
$umaps = Get-Current-Umaps
foreach ($umap in $umaps) {
$filename = $umap.Filename
$oid = $umap.Oid
Write-Verbose "Checking $filename ($oid)"
if ($modifiedMaps.Contains($filename)) {
Write-Verbose "Skipping $filename, uncommitted changes"
continue
}
$localbuiltdata, $remotebuiltdata = Get-Builtdata-Paths $umap $syncdir
$same = Compare-Files-Quick $localbuiltdata $remotebuiltdata
if ($same -and -not $force) {
Write-Verbose "Skipping $filename, matches"
continue
}
if ($mode -eq "push") {
Write-Verbose "$localbuiltdata -> $remotebuiltdata"
# In push mode, we only upload our builtdata if there is no existing
# entry for that OID by default (safest). Or, if forced to do so
if (-not (Test-Path $localbuiltdata -PathType Leaf)) {
Write-Warning "Skipping $filename, local file missing"
continue
}
if ($dryrun) {
Write-Output "Would have pushed: $filename ($oid)"
} else {
Write-Output "Push: $filename ($oid)"
2021-05-14 16:06:26 +01:00
$remotedir = [System.IO.Path]::GetDirectoryName($remotebuiltdata)
New-Item -ItemType Directory -Path $remotedir -Force > $null
Copy-Item $localbuiltdata $remotebuiltdata
}
} else {
Write-Verbose("$remotebuiltdata -> $localbuiltdata")
# In pull mode, we always pull if not same, or forced (checked already above)
if (-not (Test-Path $remotebuiltdata -PathType Leaf)) {
# If we don't have lighting data for this specific OID, we
# look back at the file history of the umap and use the latest
# one that does exist instead. E.g. lighting build may have been done, then
# small changes made to the umap afterward which would stop it matching
# but the lighting build for the previous OID was fine
# We don't use the latest file because that could be ahead of us
$foundInHistory = $false
$logOutput = git log -p --oneline -- $filename
Write-Verbose "No data for $filename HEAD revision, checking for latest available"
foreach ($line in $logOutput) {
if ($line -match "^\+oid sha256:([0-9a-f]*)$") {
$logoid = $matches[1]
# Ignore the latest one, we've already tried
if ($logoid -ne $oid) {
$testremotefile = Get-Remote-Builtdata-Path $filename $logoid $syncdir
if (Test-Path $testremotefile -PathType Leaf) {
$foundInHistory = $true
$remotebuiltdata = $testremotefile
$oid = $logoid
Write-Verbose "Found latest for $filename ($logoid)"
break
}
}
}
}
if ($foundInHistory) {
$same = Compare-Files-Quick $localbuiltdata $remotebuiltdata
if ($same -and -not $force) {
Write-Verbose "Skipping $filename, matches"
continue
}
} else {
Write-Warning "Skipping $filename, remote file missing"
continue
}
}
if ($dryrun) {
Write-Output "Would have pulled: $filename ($oid)"
} else {
Write-Output "Pull: $filename ($oid)"
2021-05-14 16:23:45 +01:00
$subdir = [System.IO.Path]::GetDirectoryName($localbuiltdata)
New-Item -ItemType Directory -Path $subdir -Force > $null
Copy-Item $remotebuiltdata $localbuiltdata
}
}
}
if ($prune) {
# Only keep latest for each map file
# We derive that from the current oids, which we always keep, and date
Write-Output "Pruning..."
foreach ($umap in $umaps) {
# We want to delete any files for this map which have a different OID
# and which are older (to prevent deletion if you're behind)
# Get our current one
$localfile, $remotefile = Get-Builtdata-Paths $umap $syncdir
$remotedir = [System.IO.Path]::GetDirectoryName($remotefile)
$basename = [System.IO.Path]::GetFileNameWithoutExtension($umap.Filename)
$matchingremotefiles = Get-ChildItem $remotedir -filter "${basename}_BuiltData_*.uasset" -ErrorAction Continue
if (-not (Test-Path $remotefile -PathType Leaf)) {
Write-Verbose "Skipping pruning old versions for $($umap.Filename) since our version isn't on remote"
continue
}
$ourfileprops = Get-ItemProperty -Path $remotefile
foreach ($file in $matchingremotefiles) {
Write-Verbose "Considering $($file.Name) for deletion"
if ($file.Name -notlike "*$($umap.Oid).uasset") {
# This is not our OID, check date
if ($file.LastWriteTime -le $ourfileprops.LastWriteTime) {
if ($dryrun) {
Write-Output "Would have pruned $($file.FullName)"
} else {
Write-Output "Pruning $($file.FullName)"
Remove-Item -Path $file.FullName -Force
}
} else {
Write-Verbose "Not pruning $($file.Name), date/time is later than ours"
}
} else {
Write-Verbose "Not pruning $($file.Name), this is our latest"
}
}
}
}
Write-Output "-- Sync process finished OK --"
} catch {
Write-Output "ERROR: $($_.Exception.Message)"
$result = 9
} finally {
if ($src -ne ".") { Pop-Location }
}
Exit $result