Untitled
unknown
plain_text
10 months ago
45 kB
13
Indexable
# UEPluginManager.ps1 - Multi-functional UE Plugin Packager & Bootstrap Manager
# Usage: .\UEPluginManager.ps1 [command] [args]
# Commands: package, init, add, install, list, clean
# No args = package (backward compatibility with your existing workflow)
[CmdletBinding()] param(
[Parameter(Position=0)]
[string]$Command = "package"
)
$ErrorActionPreference = "Stop"
Add-Type -AssemblyName System.Windows.Forms -ErrorAction SilentlyContinue | Out-Null
# === UTILITY FUNCTIONS (keeping your existing ones) ===
function Pick-File($title,$filter="All (*.*)|*.*"){
Write-Host "Attempting to open file dialog: $title" -ForegroundColor Gray
try {
$dlg=New-Object System.Windows.Forms.OpenFileDialog
$dlg.Title=$title
$dlg.Filter=$filter
$dlg.Multiselect=$false
$dlg.CheckFileExists=$true
$dlg.CheckPathExists=$true
Write-Host "Showing file dialog..." -ForegroundColor Gray
$result = $dlg.ShowDialog()
Write-Host "File dialog result: $result" -ForegroundColor Gray
if($result -eq [System.Windows.Forms.DialogResult]::OK){
Write-Host "Selected file: $($dlg.FileName)" -ForegroundColor Green
return $dlg.FileName
} else {
Write-Host "File dialog cancelled or closed" -ForegroundColor Yellow
return $null
}
} catch {
Write-Host "Error with file dialog: $($_.Exception.Message)" -ForegroundColor Red
Write-Host "Falling back to text input..." -ForegroundColor Yellow
$path = Read-Host "Please enter the full path to your .uplugin file"
if(Test-Path $path) {
return $path
} else {
Write-Host "Path does not exist: $path" -ForegroundColor Red
return $null
}
}
}
function Pick-Folder($desc){
Write-Host "Attempting to open folder dialog: $desc" -ForegroundColor Gray
try {
$dlg=New-Object System.Windows.Forms.FolderBrowserDialog
$dlg.Description=$desc
$dlg.ShowNewFolderButton=$true
Write-Host "Showing folder dialog..." -ForegroundColor Gray
$result = $dlg.ShowDialog()
Write-Host "Folder dialog result: $result" -ForegroundColor Gray
if($result -eq [System.Windows.Forms.DialogResult]::OK){
Write-Host "Selected folder: $($dlg.SelectedPath)" -ForegroundColor Green
return $dlg.SelectedPath
} else {
Write-Host "Folder dialog cancelled or closed" -ForegroundColor Yellow
return $null
}
} catch {
Write-Host "Error with folder dialog: $($_.Exception.Message)" -ForegroundColor Red
Write-Host "Falling back to text input..." -ForegroundColor Yellow
$path = Read-Host "Please enter the full path to the folder"
if(Test-Path $path) {
return $path
} else {
Write-Host "Path does not exist: $path" -ForegroundColor Red
return $null
}
}
}
function Ask($prompt,$def=""){
if([string]::IsNullOrWhiteSpace($def)){
$v = Read-Host $prompt
} else {
$v = Read-Host "$prompt [$def]"
if([string]::IsNullOrWhiteSpace($v)){ $v = $def }
}
return $v
}
function Ask-YesNo($prompt, $default=$true){
$suffix = if($default) { "(Y/n)" } else { "(y/N)" }
$v = Read-Host "$prompt $suffix"
if([string]::IsNullOrWhiteSpace($v)){ return $default }
return ($v -match '^(?i)y|yes$')
}
function Ensure-Dir($p){
if(-not(Test-Path -LiteralPath $p)){
New-Item -ItemType Directory -Force -Path $p | Out-Null
}
}
function Read-Json($p){
if(-not (Test-Path $p)) { return $null }
try {
return (Get-Content -Raw -LiteralPath $p) | ConvertFrom-Json
} catch {
Write-Warning "Could not parse JSON: $p"
return $null
}
}
function Write-Json($obj,$p){
($obj|ConvertTo-Json -Depth 20) | Out-File -LiteralPath $p -Encoding UTF8
}
function Parse-Platforms($s){
if([string]::IsNullOrWhiteSpace($s)){ return @('Win64') }
return ($s -split '[,\s]+' | Where-Object { $_.Trim() } | ForEach-Object { $_.Trim() })
}
# === SEMVER FUNCTIONS (new for package manager) ===
function Parse-SemVer($version) {
if($version -match '^(\d+)\.(\d+)\.(\d+)(-.*)?$') {
return @{
Major = [int]$matches[1]
Minor = [int]$matches[2]
Patch = [int]$matches[3]
PreRelease = $matches[4]
Original = $version
}
}
return $null
}
function Compare-SemVer($v1, $v2) {
$ver1 = Parse-SemVer $v1
$ver2 = Parse-SemVer $v2
if(-not $ver1 -or -not $ver2) { return [string]::Compare($v1, $v2) }
if($ver1.Major -ne $ver2.Major) { return $ver1.Major - $ver2.Major }
if($ver1.Minor -ne $ver2.Minor) { return $ver1.Minor - $ver2.Minor }
if($ver1.Patch -ne $ver2.Patch) { return $ver1.Patch - $ver2.Patch }
return 0
}
function Test-SemVerRange($version, $range) {
if([string]::IsNullOrWhiteSpace($range)) { return $true }
$ver = Parse-SemVer $version
if(-not $ver) { return $false }
# Simple range parsing
if($range -match '^\^(\d+\.\d+\.\d+)$') {
$target = Parse-SemVer $matches[1]
return ($ver.Major -eq $target.Major -and
(($ver.Minor -gt $target.Minor) -or
($ver.Minor -eq $target.Minor -and $ver.Patch -ge $target.Patch)))
}
if($range -match '^~(\d+\.\d+\.\d+)$') {
$target = Parse-SemVer $matches[1]
return ($ver.Major -eq $target.Major -and
$ver.Minor -eq $target.Minor -and
$ver.Patch -ge $target.Patch)
}
# Exact match
return ($version -eq $range)
}
# === YOUR EXISTING PACKAGING FUNCTIONS (unchanged) ===
function Find-UE-Installations {
$candidates = @()
# Common Epic Games locations
$epicPaths = @(
"$env:ProgramFiles\Epic Games",
"${env:ProgramFiles(x86)}\Epic Games",
"$env:ProgramW6432\Epic Games"
)
foreach($path in $epicPaths) {
if(Test-Path $path) {
$ueVersions = Get-ChildItem -Path $path -Directory | Where-Object { $_.Name -match '^UE_\d+\.\d+' }
foreach($ue in $ueVersions) {
$runUAT = Join-Path $ue.FullName "Engine\Build\BatchFiles\RunUAT.bat"
if(Test-Path $runUAT) {
$candidates += @{
Path = $ue.FullName
Version = ($ue.Name -replace '^UE_', '')
Display = "$($ue.Name) - $($ue.FullName)"
}
}
}
}
}
return $candidates | Sort-Object Version -Descending
}
function Prompt-UE-Selection {
Write-Host ""
Write-Host "=== Unreal Engine Selection ===" -ForegroundColor Cyan
$installations = Find-UE-Installations
if($installations.Count -gt 0) {
Write-Host "Found UE installations:" -ForegroundColor Yellow
for($i = 0; $i -lt $installations.Count; $i++) {
Write-Host " [$($i+1)] $($installations[$i].Display)" -ForegroundColor Green
}
Write-Host " [0] Browse for different location" -ForegroundColor Gray
do {
$choice = Read-Host "Select UE installation (1-$($installations.Count) or 0 to browse)"
$choiceNum = $choice -as [int]
} while($choiceNum -lt 0 -or $choiceNum -gt $installations.Count)
if($choiceNum -eq 0) {
return Pick-Folder "Select UE root directory (e.g. C:\Program Files\Epic Games\UE_5.5)"
} else {
return $installations[$choiceNum - 1].Path
}
} else {
Write-Host "No UE installations found automatically." -ForegroundColor Yellow
return Pick-Folder "Select UE root directory (e.g. C:\Program Files\Epic Games\UE_5.5)"
}
}
function Prompt-Plugin-Selection {
Write-Host ""
Write-Host "=== Main Plugin Selection ===" -ForegroundColor Cyan
Write-Host "Select the .uplugin file you want to package..." -ForegroundColor Yellow
$selected = Pick-File "Select the main .uplugin file to package" "Unreal Plugin (*.uplugin)|*.uplugin|All (*.*)|*.*"
if(-not $selected) {
Write-Host "No plugin selected." -ForegroundColor Red
return $null
}
return $selected
}
function Find-Dependencies-OneLevel-Up($mainPluginPath, $depNames) {
Write-Host ""
Write-Host "Looking for dependencies one level up from main plugin..." -ForegroundColor Yellow
# Get the parent directory of the main plugin
$mainPluginDir = Split-Path -Parent $mainPluginPath
$oneLevelUp = Split-Path -Parent $mainPluginDir
Write-Host "Main plugin directory: $mainPluginDir" -ForegroundColor Gray
Write-Host "Searching for dependencies in: $oneLevelUp" -ForegroundColor Gray
$foundDeps = @()
foreach($depName in $depNames) {
Write-Host " Looking for: $depName" -ForegroundColor Cyan
# Look for DepName/DepName.uplugin in the parent directory
$depDir = Join-Path $oneLevelUp $depName
$depUplugin = Join-Path $depDir ($depName + ".uplugin")
if(Test-Path $depUplugin) {
Write-Host " Found: $depUplugin" -ForegroundColor Green
$foundDeps += $depUplugin
} else {
Write-Host " Not found at: $depUplugin" -ForegroundColor Red
# Also check if there's a subdirectory with the dependency
$subDirs = Get-ChildItem -Path $oneLevelUp -Directory -ErrorAction SilentlyContinue | Where-Object { $_.Name -eq $depName }
if($subDirs) {
$subDepUplugin = Join-Path $subDirs[0].FullName ($depName + ".uplugin")
if(Test-Path $subDepUplugin) {
Write-Host " Found in subdirectory: $subDepUplugin" -ForegroundColor Green
$foundDeps += $subDepUplugin
}
}
}
}
return $foundDeps
}
function Copy-PluginFolder($pluginUPluginPath, $destPluginsRoot){
$srcPluginDir = Split-Path -Parent $pluginUPluginPath
$name = [IO.Path]::GetFileNameWithoutExtension($pluginUPluginPath)
$destDir = Join-Path $destPluginsRoot $name
if(Test-Path $destDir){ Remove-Item -Recurse -Force -LiteralPath $destDir }
Write-Host " Copying $name -> $destDir" -ForegroundColor Green
# Use robocopy for reliable copying
$robocopyResult = & robocopy "$srcPluginDir" "$destDir" /E /NFL /NDL /NJH /NJS /NP 2>$null
if($LASTEXITCODE -ge 8){
# Fallback to PowerShell copy
Copy-Item -Path $srcPluginDir -Destination $destDir -Recurse -Force
}
return $destDir
}
function Try-BuildPlugin-Method1($runUAT, $pluginPath, $packageDir, $platforms) {
Write-Host ""
Write-Host "==> Trying Method 1: Direct BuildPlugin (no HostProject)" -ForegroundColor Yellow
$platArg = ($platforms -join '+')
$uatArgs = @(
'BuildPlugin',
('-Plugin="{0}"' -f $pluginPath),
('-Package="{0}"' -f $packageDir),
('-TargetPlatforms={0}' -f $platArg),
'-Rocket', '-NoP4'
)
Write-Host "UAT Command: $($uatArgs -join ' ')" -ForegroundColor Gray
try {
& $runUAT @uatArgs
return $LASTEXITCODE -eq 0
} catch {
Write-Host "Method 1 failed with exception: $($_.Exception.Message)" -ForegroundColor Red
return $false
}
}
function Try-BuildPlugin-Method2($runUAT, $pluginPath, $packageDir, $platforms, $workDir) {
Write-Host ""
Write-Host "==> Trying Method 2: BuildPlugin with minimal HostProject" -ForegroundColor Yellow
# Create minimal host project
$hostProjDir = Join-Path $workDir "HostProject"
$hostProjFile = Join-Path $hostProjDir "HostProject.uproject"
Ensure-Dir $hostProjDir
# Get engine version from plugin or use default
try {
$pluginJson = Read-Json $pluginPath
$engineVer = if($pluginJson.EngineVersion) { $pluginJson.EngineVersion } else { "5.5" }
} catch {
$engineVer = "5.5"
}
# Create simple .uproject
@"
{
"FileVersion": 3,
"EngineAssociation": "$engineVer",
"Category": "",
"Description": "Minimal host project for BuildPlugin"
}
"@ | Out-File -LiteralPath $hostProjFile -Encoding UTF8
$platArg = ($platforms -join '+')
$uatArgs = @(
'BuildPlugin',
('-Plugin="{0}"' -f $pluginPath),
('-Package="{0}"' -f $packageDir),
('-TargetPlatforms={0}' -f $platArg),
('-HostProject="{0}"' -f $hostProjFile),
'-Rocket', '-NoP4'
)
Write-Host "UAT Command: $($uatArgs -join ' ')" -ForegroundColor Gray
try {
& $runUAT @uatArgs
return $LASTEXITCODE -eq 0
} catch {
Write-Host "Method 2 failed with exception: $($_.Exception.Message)" -ForegroundColor Red
return $false
}
}
function Try-BuildPlugin-Method3($runUAT, $pluginPath, $depPluginPaths, $packageDir, $platforms, $workDir) {
Write-Host ""
Write-Host "==> Trying Method 3: BuildPlugin with dependency copying to HostProject" -ForegroundColor Yellow
# Create host project with copied dependencies
$hostProjDir = Join-Path $workDir "HostProjectWithDeps"
$hostProjFile = Join-Path $hostProjDir "HostProject.uproject"
$hostPluginsDir = Join-Path $hostProjDir "Plugins"
if(Test-Path $hostProjDir) { Remove-Item -Recurse -Force $hostProjDir }
Ensure-Dir $hostPluginsDir
# Copy all dependencies to host project
$copiedPaths = @()
foreach($depPath in $depPluginPaths) {
$copied = Copy-PluginFolder -pluginUPluginPath $depPath -destPluginsRoot $hostPluginsDir
$copiedPaths += $copied
}
# Also copy main plugin
$mainCopied = Copy-PluginFolder -pluginUPluginPath $pluginPath -destPluginsRoot $hostPluginsDir
$mainPluginName = [IO.Path]::GetFileNameWithoutExtension($pluginPath)
$copiedMainPlugin = Join-Path $hostPluginsDir "$mainPluginName\$mainPluginName.uplugin"
# Create enhanced .uproject
try {
$pluginJson = Read-Json $pluginPath
$engineVer = if($pluginJson.EngineVersion) { $pluginJson.EngineVersion } else { "5.5" }
} catch {
$engineVer = "5.5"
}
# Build plugin references
$pluginRefs = @()
foreach($depPath in $depPluginPaths) {
$depName = [IO.Path]::GetFileNameWithoutExtension($depPath)
$pluginRefs += @{ "Name" = $depName; "Enabled" = $true }
}
$pluginRefs += @{ "Name" = $mainPluginName; "Enabled" = $true }
$uprojectContent = @{
"FileVersion" = 3
"EngineAssociation" = $engineVer
"Category" = ""
"Description" = "Host project with dependencies for BuildPlugin"
"Plugins" = $pluginRefs
}
Write-Json $uprojectContent $hostProjFile
$platArg = ($platforms -join '+')
$uatArgs = @(
'BuildPlugin',
('-Plugin="{0}"' -f $copiedMainPlugin),
('-Package="{0}"' -f $packageDir),
('-TargetPlatforms={0}' -f $platArg),
('-HostProject="{0}"' -f $hostProjFile),
'-Rocket', '-NoP4'
)
Write-Host "UAT Command: $($uatArgs -join ' ')" -ForegroundColor Gray
try {
& $runUAT @uatArgs
return $LASTEXITCODE -eq 0
} catch {
Write-Host "Method 3 failed with exception: $($_.Exception.Message)" -ForegroundColor Red
return $false
}
}
function Get-PackagedPluginPath($packageDir, $pluginBase){
# Look for the packaged plugin directory
$expectedDir = Join-Path $packageDir $pluginBase
$expectedUplugin = Join-Path $expectedDir ($pluginBase + ".uplugin")
if(Test-Path $expectedUplugin){ return $expectedDir }
# Search recursively
$found = Get-ChildItem -Path $packageDir -Filter ($pluginBase + ".uplugin") -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1
if($found){ return $found.Directory.FullName }
return $null
}
# === NEW BOOTSTRAP PACKAGE MANAGER FUNCTIONS ===
function Initialize-PluginProject {
$projectDir = Get-Location
Write-Host "Initializing plugin project in: $projectDir" -ForegroundColor Cyan
# Check if we're in a UE project
$uprojectFiles = Get-ChildItem -Filter "*.uproject" -ErrorAction SilentlyContinue
if($uprojectFiles.Count -eq 0) {
Write-Warning "No .uproject file found. Are you in a UE project directory?"
if(-not (Ask-YesNo "Continue anyway?" $false)) {
return
}
}
$pluginsDir = Join-Path $projectDir "Plugins"
Ensure-Dir $pluginsDir
$requirementsFile = Join-Path $pluginsDir "plugins.requirements.json"
$lockFile = Join-Path $pluginsDir ".ueplugins.lock.json"
if(-not (Test-Path $requirementsFile)) {
$engineVer = Ask "Engine version" "5.5"
$requirements = [ordered]@{
engine = $engineVer
plugins = @{}
}
Write-Json $requirements $requirementsFile
Write-Host "Created: $requirementsFile" -ForegroundColor Green
} else {
Write-Host "Requirements file already exists: $requirementsFile" -ForegroundColor Yellow
}
Write-Host ""
Write-Host "Plugin project initialized!" -ForegroundColor Green
Write-Host " - Edit $requirementsFile to add plugin dependencies" -ForegroundColor Gray
Write-Host " - Run 'install' to resolve and download plugins" -ForegroundColor Gray
}
function Add-Plugin-Requirement($pluginSpec) {
$pluginsDir = Join-Path (Get-Location) "Plugins"
$requirementsFile = Join-Path $pluginsDir "plugins.requirements.json"
if(-not (Test-Path $requirementsFile)) {
Write-Host "No requirements file found. Run 'init' first." -ForegroundColor Red
return
}
if([string]::IsNullOrWhiteSpace($pluginSpec)) {
$pluginSpec = Read-Host "Enter plugin specification (name@version, e.g. MyPlugin@^1.0.0)"
}
if(-not ($pluginSpec -match '^(.+)@(.+)$')) {
Write-Host "Invalid format. Use: PluginName@VersionRange (e.g. MyPlugin@^1.0.0)" -ForegroundColor Red
return
}
$pluginName = $matches[1].Trim()
$versionRange = $matches[2].Trim()
$requirements = Read-Json $requirementsFile
if(-not $requirements) {
Write-Host "Could not read requirements file." -ForegroundColor Red
return
}
if(-not $requirements.plugins) {
$requirements.plugins = @{}
}
$requirements.plugins.$pluginName = $versionRange
Write-Json $requirements $requirementsFile
Write-Host "Added $pluginName@$versionRange to requirements" -ForegroundColor Green
}
function Get-Local-Nexus-Path {
$nexusPath = Join-Path (Get-Location) "Nexus"
if(-not (Test-Path $nexusPath)) {
Write-Host "No local Nexus directory found at: $nexusPath" -ForegroundColor Red
$create = Ask-YesNo "Create local Nexus directory?" $true
if($create) {
Ensure-Dir $nexusPath
Write-Host "Created local Nexus directory: $nexusPath" -ForegroundColor Green
Write-Host "Place your .zip and .manifest.json files in this directory" -ForegroundColor Yellow
} else {
return $null
}
}
return $nexusPath
}
function Scan-Local-Nexus($nexusPath) {
Write-Host "Scanning local Nexus: $nexusPath" -ForegroundColor Yellow
$manifests = @{}
$manifestFiles = Get-ChildItem -Path $nexusPath -Filter "*.manifest.json" -ErrorAction SilentlyContinue
foreach($manifestFile in $manifestFiles) {
$manifest = Read-Json $manifestFile.FullName
if($manifest -and $manifest.name) {
# Find corresponding ZIP file
$expectedZip = $manifestFile.FullName -replace '\.manifest\.json$', '.zip'
if(Test-Path $expectedZip) {
$manifest.zipPath = $expectedZip
if(-not $manifests[$manifest.name]) {
$manifests[$manifest.name] = @()
}
$manifests[$manifest.name] += $manifest
Write-Host " Found: $($manifest.name)@$($manifest.version) (engine: $($manifest.engine))" -ForegroundColor Green
} else {
Write-Warning "Manifest found but no corresponding ZIP: $($manifestFile.Name)"
}
}
}
# Sort versions for each plugin (newest first)
foreach($pluginName in $manifests.Keys) {
$manifests[$pluginName] = $manifests[$pluginName] | Sort-Object {
$ver = Parse-SemVer $_.version
if($ver) {
-$ver.Major * 10000 - $ver.Minor * 100 - $ver.Patch
} else {
$_.version
}
}
}
return $manifests
}
function Resolve-Plugin-Dependencies($requirements, $availableManifests, $targetEngine) {
Write-Host ""
Write-Host "=== Resolving Dependencies ===" -ForegroundColor Cyan
Write-Host "Target engine: $targetEngine" -ForegroundColor Gray
$resolved = @{}
$toProcess = @()
# Add root requirements
foreach($pluginName in $requirements.plugins.Keys) {
$versionRange = $requirements.plugins.$pluginName
$toProcess += @{ Name = $pluginName; Range = $versionRange; IsRoot = $true }
Write-Host "Root requirement: $pluginName@$versionRange" -ForegroundColor Cyan
}
while($toProcess.Count -gt 0) {
$current = $toProcess[0]
$toProcess = $toProcess[1..($toProcess.Count-1)]
$pluginName = $current.Name
$versionRange = $current.Range
Write-Host "Processing: $pluginName@$versionRange" -ForegroundColor Gray
if($resolved.ContainsKey($pluginName)) {
Write-Host " Already resolved: $pluginName@$($resolved[$pluginName].version)" -ForegroundColor Gray
continue
}
if(-not $availableManifests.ContainsKey($pluginName)) {
Write-Host " Plugin not found in Nexus: $pluginName" -ForegroundColor Red
throw "Plugin not found: $pluginName"
}
# Find compatible version
$compatibleVersions = $availableManifests[$pluginName] | Where-Object {
$_.engine -eq $targetEngine -and (Test-SemVerRange $_.version $versionRange)
}
if($compatibleVersions.Count -eq 0) {
Write-Host " No compatible version found: $pluginName@$versionRange (engine: $targetEngine)" -ForegroundColor Red
Write-Host " Available versions for $pluginName" -ForegroundColor Yellow
$availableManifests[$pluginName] | ForEach-Object {
Write-Host " $($_.version) (engine: $($_.engine))" -ForegroundColor Gray
}
throw "No compatible version: $pluginName@$versionRange for engine $targetEngine"
}
$selectedVersion = $compatibleVersions[0]
$resolved[$pluginName] = $selectedVersion
Write-Host " Selected: $pluginName@$($selectedVersion.version)" -ForegroundColor Green
# Add dependencies
if($selectedVersion.dependencies) {
foreach($dep in $selectedVersion.dependencies) {
if($dep.name -and -not $resolved.ContainsKey($dep.name)) {
$alreadyQueued = $toProcess | Where-Object { $_.Name -eq $dep.name }
if(-not $alreadyQueued) {
$depRange = if($dep.range) { $dep.range } else { "*" }
$toProcess += @{ Name = $dep.name; Range = $depRange; IsRoot = $false }
Write-Host " Added dependency: $($dep.name)@$depRange" -ForegroundColor Cyan
}
}
}
}
}
return $resolved
}
function Install-Resolved-Plugins($resolved, $pluginsDir) {
Write-Host ""
Write-Host "=== Installing Plugins ===" -ForegroundColor Cyan
foreach($pluginName in $resolved.Keys) {
$manifest = $resolved[$pluginName]
$zipPath = $manifest.zipPath
$pluginDir = Join-Path $pluginsDir $pluginName
Write-Host "Installing: $pluginName@$($manifest.version)" -ForegroundColor Yellow
Write-Host " From: $zipPath" -ForegroundColor Gray
Write-Host " To: $pluginDir" -ForegroundColor Gray
if(Test-Path $pluginDir) {
Write-Host " Removing existing: $pluginDir" -ForegroundColor Gray
Remove-Item -Recurse -Force $pluginDir
}
try {
# Extract ZIP to temp location first
$tempDir = Join-Path $env:TEMP "uepm_extract_$(Get-Random)"
Ensure-Dir $tempDir
Expand-Archive -Path $zipPath -DestinationPath $tempDir -Force
# Find the plugin folder in the extracted content (skip manifest.json)
$extractedPlugin = Get-ChildItem -Path $tempDir -Directory | Where-Object {
Test-Path (Join-Path $_.FullName ($_.Name + ".uplugin"))
} | Select-Object -First 1
if($extractedPlugin) {
Move-Item -Path $extractedPlugin.FullName -Destination $pluginDir
Write-Host " Installed successfully" -ForegroundColor Green
} else {
Write-Host " Could not find plugin folder in ZIP" -ForegroundColor Red
}
# Cleanup temp
Remove-Item -Recurse -Force $tempDir -ErrorAction SilentlyContinue
} catch {
Write-Host " Installation failed: $($_.Exception.Message)" -ForegroundColor Red
}
}
}
function Create-Lockfile($resolved, $requirements, $lockFilePath) {
$lockData = [ordered]@{
engine = $requirements.engine
resolved = @()
}
foreach($pluginName in ($resolved.Keys | Sort-Object)) {
$manifest = $resolved[$pluginName]
$lockData.resolved += [ordered]@{
name = $manifest.name
version = $manifest.version
platform = "Win64"
url = [IO.Path]::GetFileName($manifest.zipPath)
sha256 = if($manifest.sha256 -and $manifest.sha256.Win64) { $manifest.sha256.Win64 } else { "" }
}
}
Write-Json $lockData $lockFilePath
Write-Host "Created lockfile: $lockFilePath" -ForegroundColor Green
}
# === COMMAND HANDLERS ===
function Handle-Package-Command {
# This is your existing packaging workflow - unchanged
Write-Host "=== UE Plugin Builder with Dependency Resolution ===" -ForegroundColor Cyan
# 1. Select UE Installation
$ueRoot = Prompt-UE-Selection
if(-not $ueRoot) { throw "No UE installation selected" }
$runUAT = Join-Path $ueRoot "Engine\Build\BatchFiles\RunUAT.bat"
if(-not (Test-Path $runUAT)) { throw "RunUAT not found at: $runUAT" }
Write-Host ""
Write-Host "Selected UE: $ueRoot" -ForegroundColor Green
# 2. Select Main Plugin
$mainPluginPath = Prompt-Plugin-Selection
if(-not $mainPluginPath) { throw "No plugin selected" }
Write-Host ""
Write-Host "Selected Plugin: $mainPluginPath" -ForegroundColor Green
# 3. Get plugin info
try {
$pluginJson = Read-Json $mainPluginPath
$pluginBase = [IO.Path]::GetFileNameWithoutExtension($mainPluginPath)
$pluginName = if($pluginJson.FriendlyName) { $pluginJson.FriendlyName } else { $pluginBase }
$pluginVer = if($pluginJson.VersionName) { $pluginJson.VersionName } elseif($pluginJson.Version) { $pluginJson.Version.ToString() } else { "1.0.0" }
} catch {
throw "Could not read plugin file: $mainPluginPath"
}
# 4. Get build settings
Write-Host ""
Write-Host "=== Build Configuration ===" -ForegroundColor Cyan
$engineVer = Ask "Engine version" "5.5"
$platformsStr = Ask "Target platforms (comma-separated)" "Win64"
$platforms = Parse-Platforms $platformsStr
# 5. Select output directory
Write-Host ""
$artifactsDir = Pick-Folder "Select output directory for build artifacts"
if(-not $artifactsDir) { throw "No output directory selected" }
Write-Host ""
Write-Host "=== Plugin Information ===" -ForegroundColor Cyan
Write-Host " Name: $pluginName" -ForegroundColor Green
Write-Host " Base Name: $pluginBase" -ForegroundColor Green
Write-Host " Version: $pluginVer" -ForegroundColor Green
Write-Host " Engine: $engineVer" -ForegroundColor Green
Write-Host " Platforms: $($platforms -join ', ')" -ForegroundColor Green
Write-Host " Output: $artifactsDir" -ForegroundColor Green
# 6. Check for dependencies
$depNames = @()
if($pluginJson.Plugins) {
foreach($dep in $pluginJson.Plugins) {
if($dep.Name) { $depNames += $dep.Name }
}
}
$depNames = $depNames | Select-Object -Unique
$depPluginPaths = @()
if($depNames.Count -gt 0) {
Write-Host ""
Write-Host "=== Dependency Resolution ===" -ForegroundColor Cyan
Write-Host "Dependencies required: $($depNames -join ', ')" -ForegroundColor Yellow
# Look for dependencies one level up from the main plugin
$foundDeps = Find-Dependencies-OneLevel-Up -mainPluginPath $mainPluginPath -depNames $depNames
foreach($depName in $depNames) {
$found = $foundDeps | Where-Object { [IO.Path]::GetFileNameWithoutExtension($_) -eq $depName }
if($found) {
Write-Host ""
Write-Host "Found dependency: $depName at $found" -ForegroundColor Green
$useFound = Ask-YesNo "Use this dependency?" $true
if($useFound) {
$depPluginPaths += $found
Write-Host " Added: $found" -ForegroundColor Green
} else {
$manual = Pick-File "Locate $depName.uplugin manually" "Unreal Plugin (*.uplugin)|*.uplugin"
if($manual) {
$depPluginPaths += $manual
Write-Host " Added: $manual" -ForegroundColor Green
} else {
Write-Warning "Dependency $depName not provided - build may fail"
}
}
} else {
Write-Host ""
Write-Host "Could not find dependency: $depName" -ForegroundColor Red
$manual = Pick-File "Locate $depName.uplugin manually" "Unreal Plugin (*.uplugin)|*.uplugin"
if($manual) {
$depPluginPaths += $manual
Write-Host " Added: $manual" -ForegroundColor Green
} else {
Write-Warning "Dependency $depName not provided - build may fail"
}
}
}
} else {
Write-Host ""
Write-Host "No dependencies declared in plugin." -ForegroundColor Green
}
# 7. Create work directory and package directory
$timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
$buildStamp = "${pluginBase}_${pluginVer}_UE${engineVer}_$($platforms -join '+')_${timestamp}"
$workDir = Join-Path $artifactsDir "_Work_$buildStamp"
$packageDir = Join-Path $artifactsDir "Package_$buildStamp"
if(Test-Path $workDir) { Remove-Item -Recurse -Force $workDir }
if(Test-Path $packageDir) { Remove-Item -Recurse -Force $packageDir }
Ensure-Dir $workDir
Ensure-Dir $packageDir
Write-Host ""
Write-Host "=== Starting Build Process ===" -ForegroundColor Cyan
Write-Host "Work directory: $workDir" -ForegroundColor Gray
Write-Host "Package directory: $packageDir" -ForegroundColor Gray
# 8. Try multiple build methods
$buildSuccess = $false
$buildMethod = ""
try {
# Method 1: Direct build (works for plugins without dependencies)
if(-not $buildSuccess -and $depPluginPaths.Count -eq 0) {
if(Try-BuildPlugin-Method1 -runUAT $runUAT -pluginPath $mainPluginPath -packageDir $packageDir -platforms $platforms) {
$buildSuccess = $true
$buildMethod = "Direct BuildPlugin (no dependencies)"
}
}
# Method 2: Minimal host project
if(-not $buildSuccess) {
if(Try-BuildPlugin-Method2 -runUAT $runUAT -pluginPath $mainPluginPath -packageDir $packageDir -platforms $platforms -workDir $workDir) {
$buildSuccess = $true
$buildMethod = "BuildPlugin with minimal HostProject"
}
}
# Method 3: Host project with dependencies
if(-not $buildSuccess -and $depPluginPaths.Count -gt 0) {
if(Try-BuildPlugin-Method3 -runUAT $runUAT -pluginPath $mainPluginPath -depPluginPaths $depPluginPaths -packageDir $packageDir -platforms $platforms -workDir $workDir) {
$buildSuccess = $true
$buildMethod = "BuildPlugin with HostProject and dependencies"
}
}
} finally {
# Cleanup work directory
if(Test-Path $workDir) {
try { Remove-Item -Recurse -Force $workDir } catch { Write-Warning "Could not clean up work directory" }
}
}
if(-not $buildSuccess) {
Write-Host ""
Write-Host "=== BUILD FAILED ===" -ForegroundColor Red
Write-Host "All build methods failed. Check the UAT logs above for details." -ForegroundColor Red
throw "Plugin build failed with all attempted methods"
}
Write-Host ""
Write-Host "=== BUILD SUCCESSFUL ===" -ForegroundColor Green
Write-Host "Method used: $buildMethod" -ForegroundColor Yellow
# 9. Package the result WITH MANIFEST INSIDE ZIP
$packagedDir = Get-PackagedPluginPath -packageDir $packageDir -pluginBase $pluginBase
if(-not $packagedDir) {
throw "Could not locate packaged plugin in: $packageDir"
}
$zipPath = Join-Path $artifactsDir "${buildStamp}.zip"
if(Test-Path $zipPath) { Remove-Item $zipPath -Force }
Write-Host ""
Write-Host "Creating ZIP package with embedded manifest..." -ForegroundColor Yellow
# Create temp directory for packaging with manifest
$tempPackageDir = Join-Path $env:TEMP "uepm_package_$(Get-Random)"
Ensure-Dir $tempPackageDir
try {
# Copy plugin to temp directory
$tempPluginDir = Join-Path $tempPackageDir $pluginBase
Copy-Item -Path $packagedDir -Destination $tempPluginDir -Recurse -Force
# Create manifest for inside the ZIP
$zipFileName = [IO.Path]::GetFileName($zipPath)
# Create artifacts and sha256 hashtables with string keys
$artifactsHash = @{}
$sha256Hash = @{}
$artifactsHash[$platforms[0].ToString()] = $zipFileName
$depsArray = @()
if($depNames.Count -gt 0) {
$depsArray = $depNames | ForEach-Object {
@{
name = [string]$_
range = $null
}
}
}
$manifest = [ordered]@{
name = [string]$pluginName
id = [string]$pluginBase
version = [string]$pluginVer
engine = [string]$engineVer
platforms = @([string]$platforms[0])
artifacts = $artifactsHash
url = [string]$zipFileName
sha256 = $sha256Hash
buildMethod = [string]$buildMethod
timestamp = (Get-Date).ToString("o")
}
if($depsArray.Count -gt 0) {
$manifest.dependencies = $depsArray
}
# Write manifest INSIDE the package (next to plugin folder)
$packageManifestPath = Join-Path $tempPackageDir "manifest.json"
Write-Json $manifest $packageManifestPath
Write-Host " Added manifest.json to package" -ForegroundColor Green
# Create ZIP from temp directory
Compress-Archive -Path "$tempPackageDir\*" -DestinationPath $zipPath -Force
# Calculate hash and update external manifest
$hash = (Get-FileHash -Algorithm SHA256 -LiteralPath $zipPath).Hash.ToLower()
$sha256Hash[$platforms[0].ToString()] = $hash
$manifest.sha256 = $sha256Hash
# Create external manifest file (for Nexus)
$manifestPath = Join-Path $artifactsDir "${buildStamp}.manifest.json"
Write-Json $manifest $manifestPath
} finally {
# Cleanup temp directory
if(Test-Path $tempPackageDir) {
Remove-Item -Recurse -Force $tempPackageDir -ErrorAction SilentlyContinue
}
}
Write-Host ""
Write-Host "=== PACKAGING COMPLETE ===" -ForegroundColor Green
Write-Host "Build Method: $buildMethod" -ForegroundColor Cyan
Write-Host "Package ZIP : $zipPath" -ForegroundColor Cyan
Write-Host "SHA256 : $hash" -ForegroundColor Cyan
Write-Host "Manifest : $manifestPath" -ForegroundColor Cyan
Write-Host "Package Dir : $packageDir" -ForegroundColor Cyan
Write-Host ""
Write-Host "Plugin successfully built and packaged!" -ForegroundColor Green
Write-Host "ZIP contains both the plugin and manifest.json" -ForegroundColor Yellow
Write-Host ""
Write-Host "To test locally:" -ForegroundColor Yellow
Write-Host " 1. Copy both .zip and .manifest.json to a 'Nexus' folder" -ForegroundColor Gray
Write-Host " 2. Use 'install' command to test dependency resolution" -ForegroundColor Gray
}
function Handle-Install-Command {
Write-Host "=== UE Plugin Installer ===" -ForegroundColor Cyan
$pluginsDir = Join-Path (Get-Location) "Plugins"
$requirementsFile = Join-Path $pluginsDir "plugins.requirements.json"
$lockFile = Join-Path $pluginsDir ".ueplugins.lock.json"
if(-not (Test-Path $requirementsFile)) {
Write-Host "No requirements file found." -ForegroundColor Red
Write-Host "Run: .\UEPluginManager.ps1 init" -ForegroundColor Yellow
return
}
$requirements = Read-Json $requirementsFile
if(-not $requirements) {
Write-Host "Could not read requirements file." -ForegroundColor Red
return
}
if(-not $requirements.plugins -or $requirements.plugins.PSObject.Properties.Count -eq 0) {
Write-Host "No plugins specified in requirements." -ForegroundColor Yellow
Write-Host "Use: .\UEPluginManager.ps1 add PluginName@^1.0.0" -ForegroundColor Gray
return
}
# Get local Nexus path
$nexusPath = Get-Local-Nexus-Path
if(-not $nexusPath) { return }
# Scan available plugins
Write-Host ""
$availableManifests = Scan-Local-Nexus $nexusPath
Write-Host "Found $($availableManifests.Keys.Count) plugins in local Nexus" -ForegroundColor Green
# Resolve dependencies
try {
$resolved = Resolve-Plugin-Dependencies -requirements $requirements -availableManifests $availableManifests -targetEngine $requirements.engine
Write-Host ""
Write-Host "=== Resolution Summary ===" -ForegroundColor Cyan
foreach($pluginName in ($resolved.Keys | Sort-Object)) {
$manifest = $resolved[$pluginName]
Write-Host " $pluginName@$($manifest.version)" -ForegroundColor Green
}
# Install plugins
Install-Resolved-Plugins -resolved $resolved -pluginsDir $pluginsDir
# Create lockfile
Create-Lockfile -resolved $resolved -requirements $requirements -lockFilePath $lockFile
Write-Host ""
Write-Host "=== INSTALLATION COMPLETE ===" -ForegroundColor Green
Write-Host "Installed $($resolved.Keys.Count) plugins" -ForegroundColor Yellow
} catch {
Write-Host ""
Write-Host "=== RESOLUTION FAILED ===" -ForegroundColor Red
Write-Host $_.Exception.Message -ForegroundColor Red
}
}
function Handle-List-Command {
Write-Host "=== Plugin Status ===" -ForegroundColor Cyan
$pluginsDir = Join-Path (Get-Location) "Plugins"
$requirementsFile = Join-Path $pluginsDir "plugins.requirements.json"
$lockFile = Join-Path $pluginsDir ".ueplugins.lock.json"
# Show requirements
if(Test-Path $requirementsFile) {
$requirements = Read-Json $requirementsFile
Write-Host ""
Write-Host "Requirements (plugins.requirements.json):" -ForegroundColor Yellow
Write-Host " Engine: $($requirements.engine)" -ForegroundColor Gray
if($requirements.plugins) {
foreach($pluginName in $requirements.plugins.Keys) {
Write-Host " $pluginName@$($requirements.plugins.$pluginName)" -ForegroundColor Green
}
}
} else {
Write-Host ""
Write-Host "No requirements file found" -ForegroundColor Red
}
# Show lockfile
if(Test-Path $lockFile) {
$lockData = Read-Json $lockFile
Write-Host ""
Write-Host "Locked versions (.ueplugins.lock.json):" -ForegroundColor Yellow
if($lockData.resolved) {
foreach($plugin in $lockData.resolved) {
Write-Host " $($plugin.name)@$($plugin.version)" -ForegroundColor Green
}
}
} else {
Write-Host ""
Write-Host "No lockfile found" -ForegroundColor Red
}
# Show installed plugins
if(Test-Path $pluginsDir) {
$installedPlugins = Get-ChildItem -Path $pluginsDir -Directory | Where-Object {
Test-Path (Join-Path $_.FullName ($_.Name + ".uplugin"))
}
Write-Host ""
Write-Host "Installed plugins:" -ForegroundColor Yellow
if($installedPlugins.Count -gt 0) {
foreach($plugin in $installedPlugins) {
$upluginPath = Join-Path $plugin.FullName ($plugin.Name + ".uplugin")
$pluginJson = Read-Json $upluginPath
$version = if($pluginJson -and $pluginJson.VersionName) { $pluginJson.VersionName } else { "unknown" }
Write-Host " $($plugin.Name)@$version" -ForegroundColor Green
}
} else {
Write-Host " No plugins installed" -ForegroundColor Gray
}
}
# Check local Nexus
$nexusPath = Join-Path (Get-Location) "Nexus"
if(Test-Path $nexusPath) {
$availableManifests = Scan-Local-Nexus $nexusPath
Write-Host ""
Write-Host "Local Nexus ($nexusPath):" -ForegroundColor Yellow
if($availableManifests.Keys.Count -gt 0) {
foreach($pluginName in ($availableManifests.Keys | Sort-Object)) {
$versions = $availableManifests[$pluginName] | ForEach-Object { "$($_.version) (UE$($_.engine))" }
$versionList = $versions -join ', '
Write-Host " ${pluginName}: $versionList" -ForegroundColor Green
}
} else {
Write-Host " No plugins found" -ForegroundColor Gray
}
}
}
function Handle-Clean-Command {
Write-Host "=== Clean Plugin Installation ===" -ForegroundColor Cyan
$pluginsDir = Join-Path (Get-Location) "Plugins"
$lockFile = Join-Path $pluginsDir ".ueplugins.lock.json"
if(-not (Test-Path $pluginsDir)) {
Write-Host "No Plugins directory found" -ForegroundColor Yellow
return
}
$installedPlugins = Get-ChildItem -Path $pluginsDir -Directory | Where-Object {
Test-Path (Join-Path $_.FullName ($_.Name + ".uplugin"))
}
if($installedPlugins.Count -eq 0) {
Write-Host "No installed plugins found" -ForegroundColor Yellow
return
}
Write-Host ""
Write-Host "Found $($installedPlugins.Count) installed plugins:" -ForegroundColor Yellow
foreach($plugin in $installedPlugins) {
Write-Host " $($plugin.Name)" -ForegroundColor Gray
}
if(Ask-YesNo "Remove all installed plugins?" $false) {
foreach($plugin in $installedPlugins) {
Write-Host "Removing: $($plugin.Name)" -ForegroundColor Red
Remove-Item -Recurse -Force $plugin.FullName
}
if(Test-Path $lockFile) {
Write-Host "Removing lockfile: $lockFile" -ForegroundColor Red
Remove-Item $lockFile
}
Write-Host ""
Write-Host "Clean complete" -ForegroundColor Green
} else {
Write-Host "Clean cancelled" -ForegroundColor Yellow
}
}
function Show-Help {
Write-Host ""
Write-Host "=== UE Plugin Manager ===" -ForegroundColor Cyan
Write-Host ""
Write-Host "Commands:" -ForegroundColor Yellow
Write-Host " package - Package a plugin with dependencies (creates ZIP + manifest)" -ForegroundColor Green
Write-Host " init - Initialize plugin project (creates plugins.requirements.json)" -ForegroundColor Green
Write-Host " add - Add plugin to requirements (e.g. add MyPlugin@^1.0.0)" -ForegroundColor Green
Write-Host " install - Resolve dependencies and install plugins from local Nexus" -ForegroundColor Green
Write-Host " list - Show current requirements, lockfile, and installed plugins" -ForegroundColor Green
Write-Host " clean - Remove all installed plugins and lockfile" -ForegroundColor Green
Write-Host ""
Write-Host "Usage Examples:" -ForegroundColor Yellow
Write-Host " .\UEPluginManager.ps1 # Package (default - backward compatible)" -ForegroundColor Gray
Write-Host " .\UEPluginManager.ps1 package # Package a plugin" -ForegroundColor Gray
Write-Host " .\UEPluginManager.ps1 init # Initialize project" -ForegroundColor Gray
Write-Host " .\UEPluginManager.ps1 add MyPlugin@^1.0.0" -ForegroundColor Gray
Write-Host " .\UEPluginManager.ps1 install # Install from requirements" -ForegroundColor Gray
Write-Host " .\UEPluginManager.ps1 list # Show status" -ForegroundColor Gray
Write-Host ""
Write-Host "Local Nexus Setup:" -ForegroundColor Yellow
Write-Host " 1. Create a 'Nexus' folder in your project" -ForegroundColor Gray
Write-Host " 2. Place .zip and .manifest.json files from packaging in Nexus/" -ForegroundColor Gray
Write-Host " 3. Run 'install' to resolve and download from local Nexus" -ForegroundColor Gray
}
# === MAIN EXECUTION ===
try {
switch($Command.ToLower()) {
"package" { Handle-Package-Command }
"init" { Initialize-PluginProject }
"add" {
$pluginSpec = if($args.Count -gt 0) { $args[0] } else { $null }
Add-Plugin-Requirement $pluginSpec
}
"install" { Handle-Install-Command }
"list" { Handle-List-Command }
"clean" { Handle-Clean-Command }
"help" { Show-Help }
default {
if($Command -eq "package") {
Handle-Package-Command
} else {
Write-Host "Unknown command: $Command" -ForegroundColor Red
Show-Help
}
}
}
} catch {
Write-Host ""
Write-Host "=== ERROR ===" -ForegroundColor Red
Write-Host $_.Exception.Message -ForegroundColor Red
Write-Host ""
}Editor is loading...
Leave a Comment