Untitled

 avatar
unknown
plain_text
10 months ago
33 kB
14
Indexable
# UEPluginManager.ps1 - Multi-functional UE Plugin Packager & Bootstrap Manager
# Commands:
#   package | install | add | init | list | clean
[CmdletBinding()] param(
    [Parameter(Position=0)]
    [ValidateSet("package", "install", "add", "init", "list", "clean")]
    [string]$Command = ""
)

$ErrorActionPreference = "Stop"
Add-Type -AssemblyName System.Windows.Forms -ErrorAction SilentlyContinue | Out-Null

# ========== UTILITIES ==========

function Pick-File($title,$filter="All (*.*)|*.*"){
  Write-Host "Opening 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
    $res = $dlg.ShowDialog()
    if($res -eq [System.Windows.Forms.DialogResult]::OK){ 
      Write-Host "Selected: $($dlg.FileName)" -ForegroundColor Green
      return $dlg.FileName 
    } else { 
      Write-Host "Cancelled." -ForegroundColor Yellow
      return $null 
    }
  } catch {
    Write-Host "Dialog failed: $($_.Exception.Message)" -ForegroundColor Red
    $path = Read-Host "Enter full file path"
    if(Test-Path $path){ return $path } else { return $null }
  }
}

function Pick-Folder($desc){
  Write-Host "Opening folder dialog: $desc" -ForegroundColor Gray
  try {
    $dlg=New-Object System.Windows.Forms.FolderBrowserDialog
    $dlg.Description=$desc; $dlg.ShowNewFolderButton=$true
    $res = $dlg.ShowDialog()
    if($res -eq [System.Windows.Forms.DialogResult]::OK){ 
      Write-Host "Selected: $($dlg.SelectedPath)" -ForegroundColor Green
      return $dlg.SelectedPath 
    } else { 
      Write-Host "Cancelled." -ForegroundColor Yellow
      return $null 
    }
  } catch {
    Write-Host "Dialog failed: $($_.Exception.Message)" -ForegroundColor Red
    $path = Read-Host "Enter full folder path"
    if(Test-Path $path){ return $path } else { return $null }
  }
}

function Ask($prompt,$def=""){
  if([string]::IsNullOrWhiteSpace($def)){ Read-Host $prompt } else {
    $v = Read-Host "$prompt [$def]"; if([string]::IsNullOrWhiteSpace($v)){ $def } else { $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 { (Get-Content -Raw -LiteralPath $p) | ConvertFrom-Json } catch { Write-Warning "Bad JSON: $p"; $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)){ @('Win64') } else { ($s -split '[,\s]+' | Where-Object { $_.Trim() } | ForEach-Object { $_.Trim() }) } }

# Coerce Hashtable -> PSCustomObject with string keys (PS 5.1-safe for ConvertTo-Json)
function New-JsonObject {
  param([Hashtable]$Map)
  $o = [pscustomobject]@{}
  foreach($k in $Map.Keys){
    $name = [string]$k
    $o | Add-Member -NotePropertyName $name -NotePropertyValue $Map[$k] -Force
  }
  return $o
}

# ========== SEMVER HELPERS ==========

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 }
  }
  $null
}
function Compare-SemVer($v1,$v2){
  $a=Parse-SemVer $v1; $b=Parse-SemVer $v2
  if(-not $a -or -not $b){ return [string]::Compare($v1,$v2) }
  if($a.Major -ne $b.Major){ return $a.Major - $b.Major }
  if($a.Minor -ne $b.Minor){ return $a.Minor - $b.Minor }
  if($a.Patch -ne $b.Patch){ return $a.Patch - $b.Patch }
  0
}
function Test-SemVerRange($version,$range){
  if([string]::IsNullOrWhiteSpace($range)){ return $true }
  $ver=Parse-SemVer $version; if(-not $ver){ return $false }
  if($range -match '^\^(\d+\.\d+\.\d+)$'){
    $t=Parse-SemVer $matches[1]
    return ($ver.Major -eq $t.Major -and ( ($ver.Minor -gt $t.Minor) -or ($ver.Minor -eq $t.Minor -and $ver.Patch -ge $t.Patch) ))
  }
  if($range -match '^~(\d+\.\d+\.\d+)$'){
    $t=Parse-SemVer $matches[1]
    return ($ver.Major -eq $t.Major -and $ver.Minor -eq $t.Minor -and $ver.Patch -ge $t.Patch)
  }
  ($version -eq $range)
}

# ========== UE DISCOVERY ==========

function Find-UE-Installations{
  $c=@(); $roots=@("$env:ProgramFiles\Epic Games","${env:ProgramFiles(x86)}\Epic Games","$env:ProgramW6432\Epic Games")
  foreach($r in $roots){ if(Test-Path $r){
    Get-ChildItem -Path $r -Directory | Where-Object { $_.Name -match '^UE_\d+\.\d+' } | ForEach-Object {
      $uat=Join-Path $_.FullName "Engine\Build\BatchFiles\RunUAT.bat"
      if(Test-Path $uat){ $c += @{ Path=$_.FullName; Version=($_.Name -replace '^UE_',''); Display="$($_.Name) - $($_.FullName)" } }
    }
  } }
  $c | Sort-Object Version -Descending
}

function Prompt-UE-Selection{
  Write-Host ""
  Write-Host "=== Unreal Engine Selection ===" -ForegroundColor Cyan
  $inst = Find-UE-Installations
  if($inst.Count -gt 0){
    Write-Host "Found UE installations:" -ForegroundColor Yellow
    for($i=0;$i -lt $inst.Count;$i++){ Write-Host "  [$($i+1)] $($inst[$i].Display)" -ForegroundColor Green }
    Write-Host "  [0] Browse..." -ForegroundColor Gray
    do{ $s = Read-Host "Select UE (1-$($inst.Count)) or 0"; $n = $s -as [int] } while($n -lt 0 -or $n -gt $inst.Count)
    if($n -eq 0){ Pick-Folder "Select UE root (e.g. C:\Program Files\Epic Games\UE_5.5)" } else { $inst[$n-1].Path }
  } else {
    Write-Host "No UE installations found automatically." -ForegroundColor Yellow
    Pick-Folder "Select UE root (e.g. C:\Program Files\Epic Games\UE_5.5)"
  }
}

# ========== PACKAGING HELPERS ==========

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
  $f = Pick-File "Select the main .uplugin" "Unreal Plugin (*.uplugin)|*.uplugin|All (*.*)|*.*"
  if(-not $f){ Write-Host "No plugin selected." -ForegroundColor Red; return $null }
  Write-Host "Selected Plugin: $f" -ForegroundColor Green
  $f
}

function Find-Dependencies-OneLevel-Up($mainPluginPath,$depNames){
  Write-Host ""
  Write-Host "Looking for dependencies one level up..." -ForegroundColor Yellow
  $mainDir = Split-Path -Parent $mainPluginPath
  $rootDir = Split-Path -Parent $mainDir
  $found=@()
  foreach($dep in $depNames){
    Write-Host "  Looking for: $dep" -ForegroundColor Cyan
    $depU = Join-Path (Join-Path $rootDir $dep) ($dep + ".uplugin")
    if(Test-Path $depU){ Write-Host "    Found: $depU" -ForegroundColor Green; $found += $depU } else {
      Write-Host "    Not found at: $depU" -ForegroundColor Red
      $sub = Get-ChildItem -Path $rootDir -Directory -ErrorAction SilentlyContinue | Where-Object { $_.Name -eq $dep } | Select-Object -First 1
      if($sub){
        $u = Join-Path $sub.FullName ($dep + ".uplugin")
        if(Test-Path $u){ Write-Host "    Found in subdir: $u" -ForegroundColor Green; $found += $u }
      }
    }
  }
  $found
}

function Copy-PluginFolder($pluginUPluginPath,$destPluginsRoot){
  $src = Split-Path -Parent $pluginUPluginPath
  $name = [IO.Path]::GetFileNameWithoutExtension($pluginUPluginPath)
  $dst = Join-Path $destPluginsRoot $name
  if(Test-Path $dst){ Remove-Item -Recurse -Force -LiteralPath $dst }
  Write-Host "  Copying $name -> $dst" -ForegroundColor Green
  $null = & robocopy "$src" "$dst" /E /NFL /NDL /NJH /NJS /NP 2>$null
  if($LASTEXITCODE -ge 8){ Copy-Item -Path $src -Destination $dst -Recurse -Force }
  $dst
}

# >>> THIS is the critical part you were missing: Method 3 with Plugins[] in the .uproject
function Try-BuildPlugin-Methods($runUAT,$pluginPath,$depPluginPaths,$packageDir,$platforms,$workDir){
  $ok=$false; $method=""

  Write-Host ""
  Write-Host "==> Trying Method 1: Direct BuildPlugin (no HostProject)" -ForegroundColor Yellow
  if($depPluginPaths.Count -eq 0){
    $args=@('BuildPlugin',("-Plugin=""$pluginPath"""),("-Package=""$packageDir"""),("-TargetPlatforms={0}" -f ($platforms -join '+')),'-Rocket','-NoP4')
    Write-Host "UAT: $($args -join ' ')" -ForegroundColor Gray
    try{ & $runUAT @args; if($LASTEXITCODE -eq 0){ $ok=$true; $method="Direct BuildPlugin (no dependencies)" } } catch { }
  } else {
    Write-Host "Skipping: plugin declares dependencies." -ForegroundColor Gray
  }

  if(-not $ok){
    Write-Host ""
    Write-Host "==> Trying Method 2: Minimal HostProject" -ForegroundColor Yellow
    $hostDir = Join-Path $workDir "HostProject"
    $hostProj = Join-Path $hostDir "HostProject.uproject"
    Ensure-Dir $hostDir
    try { $pj = Read-Json $pluginPath; $eng = if($pj.EngineVersion){ $pj.EngineVersion } else { "5.5" } } catch { $eng="5.5" }
@"
{
  "FileVersion": 3,
  "EngineAssociation": "$eng",
  "Category": "",
  "Description": "Minimal host project for BuildPlugin"
}
"@ | Out-File -LiteralPath $hostProj -Encoding UTF8

    $args=@('BuildPlugin',("-Plugin=""$pluginPath"""),("-Package=""$packageDir"""),("-TargetPlatforms={0}" -f ($platforms -join '+')),("-HostProject=""$hostProj"""),'-Rocket','-NoP4')
    Write-Host "UAT: $($args -join ' ')" -ForegroundColor Gray
    try{ & $runUAT @args; if($LASTEXITCODE -eq 0){ $ok=$true; $method="BuildPlugin with minimal HostProject" } } catch { }
  }

  if(-not $ok){
    Write-Host ""
    Write-Host "==> Trying Method 3: HostProject with dependencies (copied + enabled)" -ForegroundColor Yellow
    $hostDir = Join-Path $workDir "HostProjectWithDeps"
    $hostProj = Join-Path $hostDir "HostProject.uproject"
    $hostPlugins = Join-Path $hostDir "Plugins"
    if(Test-Path $hostDir){ Remove-Item -Recurse -Force $hostDir }
    Ensure-Dir $hostPlugins

    # Copy deps
    $copied=@()
    foreach($d in $depPluginPaths){ $copied += (Copy-PluginFolder -pluginUPluginPath $d -destPluginsRoot $hostPlugins) }

    # Copy main
    $mainCopiedDir = Copy-PluginFolder -pluginUPluginPath $pluginPath -destPluginsRoot $hostPlugins
    $mainName = [IO.Path]::GetFileNameWithoutExtension($pluginPath)
    $copiedMainUplugin = Join-Path $hostPlugins "$mainName\$mainName.uplugin"

    # Create .uproject with Plugins[] enabled
    try { $pj = Read-Json $pluginPath; $eng = if($pj.EngineVersion){ $pj.EngineVersion } else { "5.5" } } catch { $eng="5.5" }
    $pluginRefs = @()
    foreach($p in $depPluginPaths){
      $dn = [IO.Path]::GetFileNameWithoutExtension($p)
      $pluginRefs += @{ "Name" = $dn; "Enabled" = $true }
    }
    $pluginRefs += @{ "Name" = $mainName; "Enabled" = $true }

    $uprojectContent = @{
      "FileVersion" = 3
      "EngineAssociation" = $eng
      "Category" = ""
      "Description" = "Host project with dependencies for BuildPlugin"
      "Plugins" = $pluginRefs
    }
    Write-Json $uprojectContent $hostProj

    $args=@('BuildPlugin',("-Plugin=""$copiedMainUplugin"""),("-Package=""$packageDir"""),("-TargetPlatforms={0}" -f ($platforms -join '+')),("-HostProject=""$hostProj"""),'-Rocket','-NoP4')
    Write-Host "UAT: $($args -join ' ')" -ForegroundColor Gray
    try{ & $runUAT @args; if($LASTEXITCODE -eq 0){ $ok=$true; $method="BuildPlugin with HostProject and dependencies" } } catch { }
  }

  @{ Success=$ok; Method=$method }
}

function Get-PackagedPluginPath($packageDir,$pluginBase){
  $expect = Join-Path $packageDir $pluginBase
  if(Test-Path (Join-Path $expect ($pluginBase + ".uplugin"))){ return $expect }
  $found = Get-ChildItem -Path $packageDir -Filter ($pluginBase + ".uplugin") -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1
  if($found){ return $found.Directory.FullName }
  $null
}

# ========== BOOTSTRAP "NEXUS" (LOCAL REPO) ==========

function Initialize-PluginProject{
  $root = Get-Location
  Write-Host "Initializing plugin project in: $root" -ForegroundColor Cyan
  $pluginsDir = Join-Path $root "Plugins"
  Ensure-Dir $pluginsDir
  $req = Join-Path $pluginsDir "plugins.requirements.json"
  if(-not(Test-Path $req)){
    $engine = Ask "Engine version" "5.5"
    $data = [ordered]@{ engine=$engine; plugins=@{} }
    Write-Json $data $req
    Write-Host "Created: $req" -ForegroundColor Green
  } else { Write-Host "Requirements exists: $req" -ForegroundColor Yellow }
  Write-Host "Initialized." -ForegroundColor Green
}

function Add-Plugin-Requirement($pluginName,$versionRange){
  $pluginsDir = Join-Path (Get-Location) "Plugins"
  $req = Join-Path $pluginsDir "plugins.requirements.json"
  if(-not(Test-Path $req)){ Write-Host "Run 'init' first." -ForegroundColor Red; return }
  $j = Read-Json $req; if(-not $j){ Write-Host "Bad requirements." -ForegroundColor Red; return }
  if(-not $j.plugins){ $j.plugins=@{} }
  $j.plugins.$pluginName = $versionRange
  Write-Json $j $req
  Write-Host "Added $pluginName@$versionRange" -ForegroundColor Green
}

function Get-Local-Nexus-Path{
  $nx = Join-Path (Get-Location) "Nexus"
  if(-not(Test-Path $nx)){
    Write-Host "No local Nexus at: $nx" -ForegroundColor Yellow
    if(Ask-YesNo "Create local Nexus directory?" $true){ Ensure-Dir $nx; Write-Host "Created: $nx" -ForegroundColor Green } else { return $null }
  }
  $nx
}

function Scan-Local-Nexus($nexusPath){
  Write-Host "Scanning local Nexus: $nexusPath" -ForegroundColor Yellow
  $manifests=@{}; $zips = Get-ChildItem -Path $nexusPath -Filter "*.zip" -ErrorAction SilentlyContinue
  foreach($z in $zips){
    $m = $z.FullName -replace '\.zip$','.manifest.json'
    if(Test-Path $m){
      $doc = Read-Json $m
      if($doc -and $doc.name){
        if(-not $manifests[$doc.name]){ $manifests[$doc.name]=@() }
        $doc | Add-Member -NotePropertyName 'zipPath' -NotePropertyValue $z.FullName -Force
        $manifests[$doc.name] += $doc
      }
    }
  }
  foreach($k in $manifests.Keys){
    $manifests[$k] = $manifests[$k] | Sort-Object { $v=Parse-SemVer $_.version; if($v){ "{0}.{1}.{2}" -f $v.Major,$v.Minor,$v.Patch } else { $_.version } } -Descending
  }
  return $manifests
}

function Resolve-Plugin-Dependencies($requirements,$available,$targetEngine){
  Write-Host ""
  Write-Host "=== Resolving Dependencies ===" -ForegroundColor Cyan
  $resolved=@{}; $queue=@()
  foreach($k in $requirements.plugins.Keys){ $queue += @{ Name=$k; Range=$requirements.plugins.$k; IsRoot=$true } }
  while($queue.Count -gt 0){
    $cur=$queue[0]; if($queue.Count -gt 1){ $queue=$queue[1..($queue.Count-1)] } else { $queue=@() }
    $name=$cur.Name; $range=$cur.Range
    Write-Host "Processing: $name@$range" -ForegroundColor Gray
    if($resolved.ContainsKey($name)){ Write-Host "  Already resolved." -ForegroundColor Gray; continue }
    if(-not $available.ContainsKey($name)){ throw "Plugin not found: $name" }
    $cands = $available[$name] | Where-Object { $_.engine -eq $targetEngine -and (Test-SemVerRange $_.version $range) }
    if($cands.Count -eq 0){ throw "No compatible version: $name@$range (engine $targetEngine)" }
    $sel = $cands[0]; $resolved[$name]=$sel
    Write-Host "  Selected: $name@$($sel.version)" -ForegroundColor Green
    if($sel.dependencies){
      foreach($d in $sel.dependencies){
        if($d.name -and -not $resolved.ContainsKey($d.name)){
          if(-not ($queue | Where-Object { $_.Name -eq $d.name })){
            $queue += @{ Name=$d.name; Range=$d.range; IsRoot=$false }
            Write-Host "    Added dep: $($d.name)@$($d.range)" -ForegroundColor Cyan
          }
        }
      }
    }
  }
  $resolved
}

function Install-Resolved-Plugins($resolved,$pluginsDir){
  Write-Host ""
  Write-Host "=== Installing Plugins ===" -ForegroundColor Cyan
  Ensure-Dir $pluginsDir
  foreach($name in $resolved.Keys){
    $m = $resolved[$name]; $zip=$m.zipPath; $dst=Join-Path $pluginsDir $name
    Write-Host "Installing: $name@$($m.version)" -ForegroundColor Yellow
    Write-Host "  From: $zip" -ForegroundColor Gray
    Write-Host "  To:   $dst" -ForegroundColor Gray
    if(Test-Path $dst){ Write-Host "  Removing existing..." -ForegroundColor Gray; Remove-Item -Recurse -Force $dst }
    $tmp = Join-Path $env:TEMP "uepm_extract_$(Get-Random)"; Ensure-Dir $tmp
    try{
      Expand-Archive -Path $zip -DestinationPath $tmp -Force
      $plug = Get-ChildItem -Path $tmp -Directory | Where-Object { Test-Path (Join-Path $_.FullName ($_.Name + ".uplugin")) } | Select-Object -First 1
      if($plug){ Move-Item -Path $plug.FullName -Destination $dst; Write-Host "  Installed" -ForegroundColor Green } else { Write-Host "  Could not find plugin folder in ZIP" -ForegroundColor Red }
    } finally { if(Test-Path $tmp){ Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue } }
  }
}

function Create-Lockfile($resolved,$requirements,$lockFilePath){
  $lock=[ordered]@{ engine=$requirements.engine; resolved=@() }
  foreach($k in ($resolved.Keys | Sort-Object)){
    $m=$resolved[$k]
    $sha = ""
    if($m.sha256 -and ($m.sha256 | Get-Member -Name Win64 -MemberType NoteProperty -ErrorAction SilentlyContinue)){
      $sha = $m.sha256.Win64
    }
    $lock.resolved += [ordered]@{ name=$m.name; version=$m.version; platform="Win64"; url=[IO.Path]::GetFileName($m.zipPath); sha256=$sha }
  }
  Write-Json $lock $lockFilePath
  Write-Host "Created lockfile: $lockFilePath" -ForegroundColor Green
}

# ========== COMMAND HANDLERS ==========

function Handle-Package-Command{
  Write-Host "=== UE Plugin Packager ===" -ForegroundColor Cyan

  # Select UE
  $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

  # Select plugin
  $main = Prompt-Plugin-Selection
  if(-not $main) { throw "No plugin selected" }

  # Plugin info
  try {
    $pj = Read-Json $main
    $pluginBase = [IO.Path]::GetFileNameWithoutExtension($main)
    $pluginName = if($pj.FriendlyName) { $pj.FriendlyName } else { $pluginBase }
    $pluginVer = if($pj.VersionName) { $pj.VersionName } elseif($pj.Version) { $pj.Version.ToString() } else { "1.0.0" }
  } catch {
    throw "Could not read plugin file: $main"
  }

  # Build config
  Write-Host ""
  Write-Host "=== Build Configuration ===" -ForegroundColor Cyan
  $engineVer = Ask "Engine version" "5.5"
  $platforms = Parse-Platforms (Ask "Target platforms (comma-separated)" "Win64")

  # Output dir
  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

  # Dependencies
  $depNames=@()
  if($pj.Plugins){ foreach($dep in $pj.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

    $foundDeps = Find-Dependencies-OneLevel-Up -mainPluginPath $main -depNames $depNames
    foreach($depName in $depNames) {
      $found = $foundDeps | Where-Object { [IO.Path]::GetFileNameWithoutExtension($_) -eq $depName } | Select-Object -First 1
      if($found) {
        Write-Host "Found dependency: $depName at $found" -ForegroundColor Green
        if(Ask-YesNo "Use this dependency?" $true){ $depPluginPaths += $found } else {
          $manual = Pick-File "Locate $depName.uplugin manually" "Unreal Plugin (*.uplugin)|*.uplugin"
          if($manual){ $depPluginPaths += $manual } else { Write-Warning "Dependency $depName not provided - build may fail" }
        }
      } else {
        Write-Host "Could not find dependency: $depName" -ForegroundColor Red
        $manual = Pick-File "Locate $depName.uplugin manually" "Unreal Plugin (*.uplugin)|*.uplugin"
        if($manual){ $depPluginPaths += $manual } else { Write-Warning "Dependency $depName not provided - build may fail" }
      }
    }
  } else {
    Write-Host ""
    Write-Host "No dependencies declared in plugin." -ForegroundColor Green
  }

  # Work dirs
  $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

  try {
    $buildResult = Try-BuildPlugin-Methods -runUAT $runUAT -pluginPath $main -depPluginPaths $depPluginPaths -packageDir $packageDir -platforms $platforms -workDir $workDir
    if(-not $buildResult.Success) {
      Write-Host ""
      Write-Host "=== BUILD FAILED ===" -ForegroundColor Red
      Write-Host "All build methods failed. Check UAT output above." -ForegroundColor Red
      throw "Plugin build failed with all attempted methods"
    }
    Write-Host ""
    Write-Host "=== BUILD SUCCESSFUL ===" -ForegroundColor Green
    Write-Host "Method used: $($buildResult.Method)" -ForegroundColor Yellow
  } finally {
    if(Test-Path $workDir) {
      try { Remove-Item -Recurse -Force $workDir } catch { Write-Warning "Could not clean up work directory" }
    }
  }

  # Locate packaged plugin dir
  $packagedDir = Get-PackagedPluginPath -packageDir $packageDir -pluginBase $pluginBase
  if(-not $packagedDir) { throw "Could not locate packaged plugin in: $packageDir" }

  # Create ZIP that includes plugin folder + manifest.json (sibling)
  $zipPath = Join-Path $artifactsDir "${buildStamp}.zip"
  if(Test-Path $zipPath) { Remove-Item $zipPath -Force }

  Write-Host ""
  Write-Host "Creating ZIP package..." -ForegroundColor Yellow
  Write-Host "  Source: $packagedDir" -ForegroundColor Gray
  Write-Host "  ZIP:    $zipPath" -ForegroundColor Gray

  $tempPackageDir = Join-Path $env:TEMP "uepm_package_$(Get-Random)"
  Ensure-Dir $tempPackageDir
  try {
    # Copy plugin folder into temp root
    $tempPluginDir = Join-Path $tempPackageDir $pluginBase
    Copy-Item -Path $packagedDir -Destination $tempPluginDir -Recurse -Force

    # Manifest inside zip (no sha256 self reference)
    $zipFileName = [IO.Path]::GetFileName($zipPath)
    $artifactsMap = @{}
    $shaMap = @{}
    foreach($p in $platforms){ $artifactsMap[$p] = $zipFileName }

    $manifestInside = [ordered]@{
      name        = [string]$pluginName
      id          = [string]$pluginBase
      version     = [string]$pluginVer
      engine      = [string]$engineVer
      platforms   = @($platforms | ForEach-Object { [string]$_ })
      artifacts   = (New-JsonObject $artifactsMap)
      url         = [string]$zipFileName
      sha256      = (New-JsonObject $shaMap)   # empty in-zip manifest
      buildMethod = [string]$buildResult.Method
      timestamp   = (Get-Date).ToString("o")
    }
    if($depNames.Count -gt 0){
      $manifestInside.dependencies = @($depNames | ForEach-Object { [pscustomobject]@{ name=[string]$_; range=$null } })
    }

    $packageManifestPath = Join-Path $tempPackageDir "manifest.json"
    Write-Json ([pscustomobject]$manifestInside) $packageManifestPath

    Compress-Archive -Path "$tempPackageDir\*" -DestinationPath $zipPath -Force

    $hash = (Get-FileHash -Algorithm SHA256 -LiteralPath $zipPath).Hash.ToLower()
    Write-Host "  SHA256: $hash" -ForegroundColor Gray

    # External manifest with sha256
    $shaFilled=@{}; foreach($p in $platforms){ $shaFilled[$p] = $hash }
    $manifestOutside = $manifestInside.PSObject.Copy()
    $manifestOutside.sha256 = (New-JsonObject $shaFilled)

    $manifestPath = Join-Path $artifactsDir "${buildStamp}.manifest.json"
    Write-Json ([pscustomobject]$manifestOutside) $manifestPath

    Write-Host ""
    Write-Host "=== PACKAGING COMPLETE ===" -ForegroundColor Green
    Write-Host "Build Method: $($buildResult.Method)" -ForegroundColor Cyan
    Write-Host "Package ZIP : $zipPath" -ForegroundColor Cyan  
    Write-Host "SHA256      : $hash" -ForegroundColor Cyan
    Write-Host "Manifest    : $manifestPath" -ForegroundColor Cyan
    Write-Host ""
    Write-Host "ZIP contains:  /$pluginBase  and  /manifest.json" -ForegroundColor Yellow
  } finally {
    if(Test-Path $tempPackageDir) {
      Remove-Item -Recurse -Force $tempPackageDir -ErrorAction SilentlyContinue
    }
  }
}

function Handle-Init-Command{ Initialize-PluginProject }

function Handle-Add-Command($pluginSpec){
  if([string]::IsNullOrWhiteSpace($pluginSpec)){ $pluginSpec = Read-Host "Enter plugin spec (name@version)" }
  if($pluginSpec -match '^(.+)@(.+)$'){ Add-Plugin-Requirement -pluginName $matches[1] -versionRange $matches[2] } else {
    Write-Host "Invalid spec. Example: MyPlugin@^1.0.0" -ForegroundColor Red
  }
}

function Handle-Install-Command{
  Write-Host "=== UE Plugin Installer ===" -ForegroundColor Cyan
  $pluginsDir = Join-Path (Get-Location) "Plugins"
  $reqFile = Join-Path $pluginsDir "plugins.requirements.json"
  $lockFile = Join-Path $pluginsDir ".ueplugins.lock.json"
  if(-not(Test-Path $reqFile)){ Write-Host "No requirements file. Run 'init' first." -ForegroundColor Red; return }
  $requirements = Read-Json $reqFile; if(-not $requirements){ Write-Host "Could not read requirements." -ForegroundColor Red; return }
  if(-not $requirements.plugins -or $requirements.plugins.PSObject.Properties.Count -eq 0){ Write-Host "No plugins specified. Use 'add'." -ForegroundColor Yellow; return }

  $nexus = Get-Local-Nexus-Path; if(-not $nexus){ return }
  $available = Scan-Local-Nexus $nexus
  Write-Host "Found $($available.Keys.Count) plugins in local Nexus" -ForegroundColor Green

  try {
    $resolved = Resolve-Plugin-Dependencies -requirements $requirements -available $available -targetEngine $requirements.engine
    
    Write-Host ""
    Write-Host "=== Resolution Summary ===" -ForegroundColor Cyan
    foreach($k in ($resolved.Keys | Sort-Object)){ $m=$resolved[$k]; Write-Host "  $k@$($m.version)" -ForegroundColor Green }
    
    Install-Resolved-Plugins -resolved $resolved -pluginsDir $pluginsDir
    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"
  $reqFile = Join-Path $pluginsDir "plugins.requirements.json"
  $lockFile = Join-Path $pluginsDir ".ueplugins.lock.json"

  if(Test-Path $reqFile){
    $req = Read-Json $reqFile
    Write-Host ""
    Write-Host "Requirements (plugins.requirements.json):" -ForegroundColor Yellow
    Write-Host "  Engine: $($req.engine)" -ForegroundColor Gray
    if($req.plugins){
      foreach($k in $req.plugins.Keys){ Write-Host "  $k@$($req.plugins.$k)" -ForegroundColor Green }
    }
  } else { Write-Host "No requirements file found." -ForegroundColor Red }

  if(Test-Path $lockFile){
    $lock = Read-Json $lockFile
    Write-Host ""
    Write-Host "Locked versions (.ueplugins.lock.json):" -ForegroundColor Yellow
    if($lock.resolved){ foreach($p in $lock.resolved){ Write-Host "  $($p.name)@$($p.version)" -ForegroundColor Green } }
  } else { Write-Host "No lockfile found." -ForegroundColor Red }

  if(Test-Path $pluginsDir){
    $installed = Get-ChildItem -Path $pluginsDir -Directory | Where-Object { Test-Path (Join-Path $_.FullName ($_.Name + ".uplugin")) }
    Write-Host ""
    Write-Host "Installed plugins:" -ForegroundColor Yellow
    if($installed.Count -gt 0){
      foreach($d in $installed){
        $up = Join-Path $d.FullName ($d.Name + ".uplugin"); $j=Read-Json $up
        $ver = if($j -and $j.VersionName){ $j.VersionName } else { "unknown" }
        Write-Host "  $($d.Name)@$ver" -ForegroundColor Green
      }
    } else { Write-Host "  (none)" -ForegroundColor Gray }
  }

  $nexus = Join-Path (Get-Location) "Nexus"
  if(Test-Path $nexus){
    $avail = Scan-Local-Nexus $nexus
    Write-Host ""
    Write-Host "Local Nexus ($nexus):" -ForegroundColor Yellow
    if($avail.Keys.Count -gt 0){
      foreach($n in ($avail.Keys | Sort-Object)){
        $vers = $avail[$n] | ForEach-Object { $_.version }
        Write-Host ("  {0}: {1}" -f $n, ($vers -join ', '))  # formatting avoids the $n: pitfall
      }
    } else { Write-Host "  (empty)" -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 }
  $installed = Get-ChildItem -Path $pluginsDir -Directory | Where-Object { Test-Path (Join-Path $_.FullName ($_.Name + ".uplugin")) }
  if($installed.Count -eq 0){ Write-Host "No installed plugins found" -ForegroundColor Yellow; return }
  Write-Host ""
  Write-Host "Found $($installed.Count) installed plugins:" -ForegroundColor Yellow
  foreach($p in $installed){ Write-Host "  $($p.Name)" -ForegroundColor Gray }
  if(Ask-YesNo "Remove all installed plugins?" $false){
    foreach($p in $installed){ Write-Host "Removing: $($p.Name)" -ForegroundColor Red; Remove-Item -Recurse -Force $p.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 "package    - Package plugin (ZIP contains plugin folder + manifest.json); external manifest has SHA256" -ForegroundColor Green
  Write-Host "init       - Initialize Plugins/plugins.requirements.json" -ForegroundColor Green  
  Write-Host "add        - Add requirement: add MyPlugin@^1.0.0" -ForegroundColor Green
  Write-Host "install    - Resolve + install from local 'Nexus' (zip + .manifest.json pairs)" -ForegroundColor Green
  Write-Host "list       - Show requirements, lockfile, installed, local Nexus" -ForegroundColor Green
  Write-Host "clean      - Remove installed plugins + lockfile" -ForegroundColor Green
}

# ========== MAIN ==========
if([string]::IsNullOrWhiteSpace($Command)){ Show-Help; return }
try{
  switch($Command.ToLower()){
    "package" { Handle-Package-Command }
    "init"    { Handle-Init-Command }
    "add"     { $spec = if($args.Count -gt 0){ $args[0] } else { $null }; Handle-Add-Command $spec }
    "install" { Handle-Install-Command }
    "list"    { Handle-List-Command }
    "clean"   { Handle-Clean-Command }
    default   { 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