From 01ba443ac508d2ea2942277601b1a984d01112c4 Mon Sep 17 00:00:00 2001 From: jessikitty Date: Wed, 29 Apr 2026 14:38:31 +1000 Subject: [PATCH] fix: re-create Build-Plugin.ps1 with proper encoding Includes AssemblyResolve handler for reflection diagnostics, ManifestGenerator output capture, manifest validation with char-by-char CategoryTypeName comparison, and package verification. --- Build-Plugin.ps1 | 512 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 512 insertions(+) create mode 100644 Build-Plugin.ps1 diff --git a/Build-Plugin.ps1 b/Build-Plugin.ps1 new file mode 100644 index 0000000..2382fff --- /dev/null +++ b/Build-Plugin.ps1 @@ -0,0 +1,512 @@ +# Build-Plugin.ps1 +# Builds and packages the AD Compare plugin for Disco ICT import +# +# Usage: +# .\Build-Plugin.ps1 -DiscoBinPath "C:\Program Files\Disco\WebApp\bin" + +param( + [Parameter(Mandatory=$true)] + [string]$DiscoBinPath, + + [string]$Configuration = "Release" +) + +$ErrorActionPreference = "Stop" +$PluginDir = $PSScriptRoot +$PluginName = "Disco.Plugins.ADCompare" + +Write-Host "=== AD Compare Plugin Builder ===" -ForegroundColor Cyan +Write-Host "Disco bin path: $DiscoBinPath" -ForegroundColor Gray + +# --- Validate --- +if (-not (Test-Path $DiscoBinPath)) { + Write-Error "Disco bin path not found: $DiscoBinPath" + exit 1 +} + +$requiredDlls = @("Disco.Services.dll", "Disco.Models.dll", "Disco.Data.dll") +foreach ($dll in $requiredDlls) { + $dllPath = Join-Path $DiscoBinPath $dll + if (-not (Test-Path $dllPath)) { + Write-Error "Required assembly not found: $dllPath" + exit 1 + } +} +Write-Host "All required Disco assemblies found" -ForegroundColor Green + +# --- Show Disco Assembly Versions --- +Write-Host "`n--- Disco Assembly Versions ---" -ForegroundColor DarkGray +foreach ($dll in $requiredDlls) { + $dllPath = Join-Path $DiscoBinPath $dll + try { + $asmName = [System.Reflection.AssemblyName]::GetAssemblyName($dllPath) + Write-Host " $dll : v$($asmName.Version)" -ForegroundColor Gray + } catch { + Write-Warning " $dll : could not read version - $($_.Exception.Message)" + } +} + +# --- Find MSBuild --- +$msbuild = $null +$vsWhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" +if (Test-Path $vsWhere) { + $msbuild = & $vsWhere -latest -requires Microsoft.Component.MSBuild -find "MSBuild\**\Bin\MSBuild.exe" 2>$null | Select-Object -First 1 +} +if (-not $msbuild -or -not (Test-Path $msbuild)) { + $msbuild = "C:\Windows\Microsoft.NET\Framework64\v4.0.30319\MSBuild.exe" +} +if (-not (Test-Path $msbuild)) { + Write-Error "MSBuild not found." + exit 1 +} +Write-Host "Using MSBuild: $msbuild" -ForegroundColor Gray + +# --- Resolve all HintPaths to Disco bin --- +$csproj = Join-Path $PluginDir "$PluginName.csproj" +$csprojContent = Get-Content $csproj -Raw +$csprojContent = [regex]::Replace($csprojContent, '[^<]+\\([^\\<]+\.dll)', "$DiscoBinPath\`$1") + +$tempCsproj = Join-Path $PluginDir "$PluginName.build.csproj" +$csprojContent | Set-Content $tempCsproj -Encoding UTF8 +Write-Host "Assembly references resolved to $DiscoBinPath" -ForegroundColor Green + +# --- Build --- +Write-Host "`nBuilding $PluginName ($Configuration)..." -ForegroundColor Yellow +$buildOutput = Join-Path $PluginDir "bin\$Configuration" + +& $msbuild $tempCsproj /p:Configuration=$Configuration /p:OutputPath=$buildOutput /t:Build /v:minimal + +if ($LASTEXITCODE -ne 0) { + Remove-Item $tempCsproj -ErrorAction SilentlyContinue + Write-Error "Build failed!" + exit 1 +} +Remove-Item $tempCsproj -ErrorAction SilentlyContinue + +$pluginDll = Join-Path $buildOutput "$PluginName.dll" +if (-not (Test-Path $pluginDll)) { + Write-Error "Build output not found: $pluginDll" + exit 1 +} +Write-Host "Build successful: $pluginDll" -ForegroundColor Green + +# --- Copy Dependencies for Reflection and ManifestGenerator --- +foreach ($dll in $requiredDlls) { + Copy-Item (Join-Path $DiscoBinPath $dll) $buildOutput -Force -ErrorAction SilentlyContinue +} +$extraDlls = @("EntityFramework.dll", "System.Web.Mvc.dll", "System.Web.WebPages.dll", "System.Web.Razor.dll", "RazorGenerator.Mvc.dll", "Newtonsoft.Json.dll") +foreach ($dep in $extraDlls) { + $depPath = Join-Path $DiscoBinPath $dep + if (Test-Path $depPath) { Copy-Item $depPath $buildOutput -Force -ErrorAction SilentlyContinue } +} + +# --- Reflection-Based Feature Diagnostics --- +Write-Host "`n--- Plugin Assembly Diagnostics ---" -ForegroundColor Yellow + +$reflectionOk = $true +try { + # Set up AssemblyResolve handler so the CLR can find Disco.Services.dll etc. + # when we load the plugin assembly for reflection + $resolveHandler = [System.ResolveEventHandler]{ + param($sender, $args) + $shortName = ($args.Name -split ',')[0].Trim() + $probePaths = @($buildOutput, $DiscoBinPath) + foreach ($probe in $probePaths) { + $candidate = Join-Path $probe "$shortName.dll" + if (Test-Path $candidate) { + return [System.Reflection.Assembly]::LoadFrom($candidate) + } + } + return $null + } + [System.AppDomain]::CurrentDomain.add_AssemblyResolve($resolveHandler) + + $pluginAsm = [System.Reflection.Assembly]::LoadFrom($pluginDll) + + Write-Host " Plugin assembly loaded: $($pluginAsm.FullName)" -ForegroundColor Gray + + # Find all types that extend UIExtensionFeature + $uiExtTypes = @() + foreach ($t in $pluginAsm.GetExportedTypes()) { + $baseType = $t.BaseType + if ($baseType -ne $null -and $baseType.IsGenericType) { + $genDef = $baseType.GetGenericTypeDefinition() + if ($genDef.FullName -eq 'Disco.Services.Plugins.Features.UIExtension.UIExtensionFeature`1') { + $uiExtTypes += $t + } + } + } + + if ($uiExtTypes.Count -eq 0) { + Write-Warning " NO UIExtensionFeature implementations found in assembly!" + Write-Warning " ExecuteAction will never be called without UIExtension features." + $reflectionOk = $false + } else { + Write-Host " Found $($uiExtTypes.Count) UIExtension feature(s):" -ForegroundColor Green + } + + foreach ($t in $uiExtTypes) { + $baseType = $t.BaseType + $genArgs = $baseType.GetGenericArguments() + $modelType = $genArgs[0] + $genDefFullName = $baseType.GetGenericTypeDefinition().FullName + + Write-Host "`n Feature: $($t.FullName)" -ForegroundColor Cyan + Write-Host " Base type: $($baseType.FullName)" -ForegroundColor Gray + Write-Host " Generic definition: $genDefFullName" -ForegroundColor Gray + Write-Host " Model type: $($modelType.FullName)" -ForegroundColor Gray + Write-Host " Model assembly: $($modelType.Assembly.GetName().Name) v$($modelType.Assembly.GetName().Version)" -ForegroundColor Gray + + # Verify [PluginFeature] attribute + $attrs = $t.GetCustomAttributes($true) | Where-Object { $_.GetType().Name -eq 'PluginFeatureAttribute' } + if ($attrs) { + foreach ($attr in $attrs) { + Write-Host " [PluginFeature] Id: $($attr.Id)" -ForegroundColor Gray + Write-Host " [PluginFeature] Name: $($attr.Name)" -ForegroundColor Gray + } + } else { + Write-Warning " MISSING [PluginFeature] attribute! Disco cannot discover this feature." + $reflectionOk = $false + } + + # Verify ExecuteAction is overridden (not just the abstract base) + $execMethod = $t.GetMethod('ExecuteAction', [System.Reflection.BindingFlags]'Public,Instance') + if ($execMethod -and $execMethod.DeclaringType -eq $t) { + Write-Host " ExecuteAction: overridden (OK)" -ForegroundColor Green + } elseif ($execMethod) { + Write-Warning " ExecuteAction: NOT overridden (declared in $($execMethod.DeclaringType.Name))" + $reflectionOk = $false + } else { + Write-Warning " ExecuteAction: NOT FOUND" + $reflectionOk = $false + } + + # Verify Initialize is overridden and calls Register() + $initMethod = $t.GetMethod('Initialize', [System.Reflection.BindingFlags]'Public,Instance', $null, @([Type]'Disco.Data.Repository.DiscoDataContext'), $null) + if ($initMethod -and $initMethod.DeclaringType -eq $t) { + Write-Host " Initialize: overridden (OK)" -ForegroundColor Green + } else { + Write-Warning " Initialize: NOT overridden - Register() will never be called!" + $reflectionOk = $false + } + + # Check that the model type implements BaseUIModel + $baseUIModel = $modelType.GetInterface('BaseUIModel') + if ($baseUIModel) { + Write-Host " BaseUIModel: $($modelType.Name) implements BaseUIModel (OK)" -ForegroundColor Green + } else { + Write-Warning " BaseUIModel: $($modelType.Name) does NOT implement BaseUIModel!" + $reflectionOk = $false + } + } + + # Check the main plugin class + $pluginClass = $pluginAsm.GetExportedTypes() | Where-Object { + $_.BaseType -ne $null -and $_.BaseType.FullName -eq 'Disco.Services.Plugins.Plugin' + } + if ($pluginClass) { + Write-Host "`n Plugin class: $($pluginClass.FullName)" -ForegroundColor Cyan + $pluginAttrs = $pluginClass.GetCustomAttributes($true) | Where-Object { $_.GetType().Name -eq 'PluginAttribute' } + if ($pluginAttrs) { + foreach ($attr in $pluginAttrs) { + Write-Host " [Plugin] Id: $($attr.Id)" -ForegroundColor Gray + Write-Host " [Plugin] Name: $($attr.Name)" -ForegroundColor Gray + } + } else { + Write-Warning " MISSING [Plugin] attribute!" + } + } + +} catch { + Write-Warning " Reflection diagnostics failed: $($_.Exception.Message)" + if ($_.Exception.InnerException) { + Write-Warning " Inner exception: $($_.Exception.InnerException.Message)" + } + # For ReflectionTypeLoadException, show the loader exceptions + if ($_.Exception.InnerException -and $_.Exception.InnerException.GetType().Name -eq 'ReflectionTypeLoadException') { + $loaderExceptions = $_.Exception.InnerException.LoaderExceptions + if ($loaderExceptions) { + Write-Warning " Loader exceptions:" + $loaderExceptions | Select-Object -Unique | ForEach-Object { + Write-Warning " - $($_.Message)" + } + } + } + Write-Warning " This may indicate assembly load issues (missing dependencies, version mismatches)." + $reflectionOk = $false +} finally { + # Clean up the AssemblyResolve handler + if ($resolveHandler) { + [System.AppDomain]::CurrentDomain.remove_AssemblyResolve($resolveHandler) + } +} + +if ($reflectionOk) { + Write-Host "`n All reflection checks PASSED" -ForegroundColor Green +} else { + Write-Host "`n Some reflection checks FAILED - see warnings above" -ForegroundColor Red +} + +# --- Generate Manifest --- +Write-Host "`n--- Manifest Generation ---" -ForegroundColor Yellow + +$useManifestGen = $false +$manifestGenExe = Join-Path $DiscoBinPath "Disco.Services.Plugins.ManifestGenerator.exe" +if (-not (Test-Path $manifestGenExe)) { + $manifestGenExe = Join-Path (Split-Path $DiscoBinPath -Parent) "Disco.Services.Plugins.ManifestGenerator.exe" +} +if (Test-Path $manifestGenExe) { + Write-Host "Found ManifestGenerator: $manifestGenExe" -ForegroundColor Gray + $useManifestGen = $true + + # Capture stdout and stderr separately for diagnostics + $manifestGenOutput = & $manifestGenExe $pluginDll 2>&1 + $manifestGenExit = $LASTEXITCODE + + if ($manifestGenOutput) { + Write-Host " ManifestGenerator output:" -ForegroundColor DarkGray + $manifestGenOutput | ForEach-Object { + $line = $_.ToString() + if ($_ -is [System.Management.Automation.ErrorRecord]) { + Write-Host " [STDERR] $line" -ForegroundColor Red + } else { + Write-Host " $line" -ForegroundColor DarkGray + } + } + } + + if ($manifestGenExit -ne 0) { + Write-Warning "ManifestGenerator exited with code $manifestGenExit, falling back to manual manifest..." + $useManifestGen = $false + } else { + Write-Host "ManifestGenerator completed successfully" -ForegroundColor Green + } +} else { + Write-Host "ManifestGenerator not found at:" -ForegroundColor DarkGray + Write-Host " $DiscoBinPath" -ForegroundColor DarkGray + Write-Host " $(Split-Path $DiscoBinPath -Parent)" -ForegroundColor DarkGray + Write-Host "Falling back to manual manifest generation..." -ForegroundColor Yellow +} + +if (-not $useManifestGen) { + $version = [System.Reflection.AssemblyName]::GetAssemblyName($pluginDll).Version + + # CategoryTypeName must match the .NET generic type definition FullName. + # For UIExtensionFeature, this is UIExtensionFeature`1 (one backtick, then 1). + # In PowerShell double-quoted strings, `` (two backticks) produces one literal backtick. + $uiExtCategoryTypeName = "Disco.Services.Plugins.Features.UIExtension.UIExtensionFeature``1" + + $manifest = [ordered]@{ + Id = $PluginName + Name = "AD Compare" + Author = "Jess Rogerson" + Url = "https://gitea.hideawaygaming.com.au/jessikitty/disco-ad-compare-plugin" + Version = $version.ToString() + AssemblyPath = "$PluginName.dll" + TypeName = "Disco.Plugins.ADCompare.ADComparePlugin" + ConfigurationHandlerTypeName = "Disco.Plugins.ADCompare.ConfigurationHandler.ADCompareConfigurationHandler" + WebHandlerTypeName = "Disco.Plugins.ADCompare.WebHandler.ADCompareWebHandler" + Features = @( + [ordered]@{ + Id = "ADCompareDeviceUI" + Name = "Device Page - AD Compare" + TypeName = "Disco.Plugins.ADCompare.Features.DeviceUIExtension" + PrimaryFeature = $false + CategoryTypeName = $uiExtCategoryTypeName + }, + [ordered]@{ + Id = "ADCompareUserUI" + Name = "User Page - AD Compare" + TypeName = "Disco.Plugins.ADCompare.Features.UserUIExtension" + PrimaryFeature = $false + CategoryTypeName = $uiExtCategoryTypeName + } + ) + } + $manifestJson = $manifest | ConvertTo-Json -Depth 5 + $manifestJson | Set-Content (Join-Path $buildOutput "manifest.json") -Encoding UTF8 + Write-Host "Manual manifest.json created" -ForegroundColor Green +} + +# --- Validate Manifest --- +Write-Host "`n--- Manifest Validation ---" -ForegroundColor Yellow + +$manifestPath = Join-Path $buildOutput "manifest.json" +if (-not (Test-Path $manifestPath)) { + Write-Error "manifest.json NOT FOUND in build output! Plugin cannot be loaded by Disco." + exit 1 +} + +$manifestContent = Get-Content $manifestPath -Raw +$manifestObj = $manifestContent | ConvertFrom-Json + +# Basic structure checks +$manifestErrors = @() +$manifestWarnings = @() + +if (-not $manifestObj.Id) { $manifestErrors += "Missing 'Id'" } +if (-not $manifestObj.TypeName) { $manifestErrors += "Missing 'TypeName'" } +if (-not $manifestObj.AssemblyPath) { $manifestErrors += "Missing 'AssemblyPath'" } +if (-not $manifestObj.Version) { $manifestWarnings += "Missing 'Version'" } + +Write-Host " Id: $($manifestObj.Id)" -ForegroundColor Gray +Write-Host " Version: $($manifestObj.Version)" -ForegroundColor Gray +Write-Host " TypeName: $($manifestObj.TypeName)" -ForegroundColor Gray + +if (-not $manifestObj.Features -or $manifestObj.Features.Count -eq 0) { + $manifestErrors += "Features array is EMPTY or MISSING - no UIExtensions will be loaded!" +} else { + Write-Host " Features: $($manifestObj.Features.Count) found" -ForegroundColor Green + + # Expected CategoryTypeName (with literal backtick) + $expectedCategory = 'Disco.Services.Plugins.Features.UIExtension.UIExtensionFeature`1' + + foreach ($feature in $manifestObj.Features) { + Write-Host "`n Feature: $($feature.Id)" -ForegroundColor Cyan + Write-Host " Name: $($feature.Name)" -ForegroundColor Gray + Write-Host " TypeName: $($feature.TypeName)" -ForegroundColor Gray + Write-Host " CategoryTypeName: $($feature.CategoryTypeName)" -ForegroundColor Gray + Write-Host " PrimaryFeature: $($feature.PrimaryFeature)" -ForegroundColor Gray + + if (-not $feature.TypeName) { + $manifestErrors += "Feature '$($feature.Id)' is missing TypeName" + } + + if (-not $feature.CategoryTypeName) { + $manifestErrors += "Feature '$($feature.Id)' is missing CategoryTypeName" + } elseif ($feature.CategoryTypeName -ne $expectedCategory) { + $manifestErrors += "Feature '$($feature.Id)' CategoryTypeName MISMATCH!" + Write-Host " Expected: $expectedCategory" -ForegroundColor Red + Write-Host " Got: $($feature.CategoryTypeName)" -ForegroundColor Red + + # Char-by-char comparison to identify the exact difference + $expChars = $expectedCategory.ToCharArray() + $actChars = $feature.CategoryTypeName.ToCharArray() + for ($i = 0; $i -lt [Math]::Max($expChars.Length, $actChars.Length); $i++) { + $ec = if ($i -lt $expChars.Length) { $expChars[$i] } else { $null } + $ac = if ($i -lt $actChars.Length) { $actChars[$i] } else { $null } + if ($ec -ne $ac) { + $ecDisplay = if ($ec) { "'$ec' (0x{0:X2})" -f [int][char]$ec } else { '[END]' } + $acDisplay = if ($ac) { "'$ac' (0x{0:X2})" -f [int][char]$ac } else { '[END]' } + Write-Host " First difference at position ${i}: expected $ecDisplay vs got $acDisplay" -ForegroundColor Red + break + } + } + } else { + Write-Host " CategoryTypeName: VALID" -ForegroundColor Green + } + } +} + +# Show raw manifest for manual inspection +Write-Host "`n --- Raw manifest.json ---" -ForegroundColor DarkGray +$manifestContent -split "`n" | ForEach-Object { + Write-Host " $_" -ForegroundColor DarkGray +} +Write-Host " --- End manifest.json ---" -ForegroundColor DarkGray + +if ($manifestErrors.Count -gt 0) { + Write-Host "`n MANIFEST ERRORS:" -ForegroundColor Red + foreach ($err in $manifestErrors) { + Write-Host " [ERROR] $err" -ForegroundColor Red + } +} +if ($manifestWarnings.Count -gt 0) { + foreach ($warn in $manifestWarnings) { + Write-Host " [WARN] $warn" -ForegroundColor Yellow + } +} +if ($manifestErrors.Count -eq 0) { + Write-Host "`n All manifest checks PASSED" -ForegroundColor Green +} else { + Write-Host "`n Manifest has errors - UIExtensions will NOT render!" -ForegroundColor Red + Write-Host " Fix the errors above before installing the plugin." -ForegroundColor Red +} + +# --- Package --- +Write-Host "`nPackaging .discoPlugin file..." -ForegroundColor Yellow + +$includeFiles = @("$PluginName.dll", "$PluginName.pdb", "manifest.json") + +if (-not $version) { + $version = [System.Reflection.AssemblyName]::GetAssemblyName($pluginDll).Version +} + +$packageName = "$PluginName-$($version.ToString()).discoPlugin" +$packagePath = Join-Path $PluginDir $packageName + +Get-ChildItem $PluginDir -Filter "*.discoPlugin" | Remove-Item -Force +Get-ChildItem $PluginDir -Filter "*.zip" | Where-Object { $_.Name -like "$PluginName*" } | Remove-Item -Force + +$tempPkg = Join-Path $env:TEMP "discoplugin_$([guid]::NewGuid().ToString('N'))" +New-Item -ItemType Directory -Path $tempPkg -Force | Out-Null +$missingFiles = @() +foreach ($f in $includeFiles) { + $src = Join-Path $buildOutput $f + if (Test-Path $src) { + Copy-Item $src $tempPkg -Force + } else { + $missingFiles += $f + } +} + +if ($missingFiles.Count -gt 0) { + Write-Warning "Missing files not included in package: $($missingFiles -join ', ')" +} + +Write-Host "Files in package:" -ForegroundColor Gray +Get-ChildItem $tempPkg -File | ForEach-Object { + $size = [math]::Round($_.Length / 1KB, 1) + Write-Host " $($_.Name) ($size KB)" -ForegroundColor Gray +} + +$zipPath = Join-Path $PluginDir "$PluginName-$($version.ToString()).zip" +Compress-Archive -Path "$tempPkg\*" -DestinationPath $zipPath -Force +Rename-Item $zipPath $packageName -Force +Remove-Item $tempPkg -Recurse -Force + +# --- Final Package Verification --- +Write-Host "`n--- Package Verification ---" -ForegroundColor Yellow +try { + Add-Type -AssemblyName System.IO.Compression.FileSystem + $zip = [System.IO.Compression.ZipFile]::OpenRead($packagePath) + $entries = $zip.Entries | Select-Object -ExpandProperty Name + $zip.Dispose() + + $requiredEntries = @("$PluginName.dll", "manifest.json") + foreach ($req in $requiredEntries) { + if ($entries -contains $req) { + Write-Host " [OK] $req" -ForegroundColor Green + } else { + Write-Host " [MISSING] $req" -ForegroundColor Red + } + } + # Show any extras + foreach ($entry in $entries) { + if ($entry -notin $requiredEntries) { + Write-Host " [+] $entry" -ForegroundColor Gray + } + } +} catch { + Write-Warning " Could not verify package contents: $($_.Exception.Message)" +} + +$packageSize = [math]::Round((Get-Item $packagePath).Length / 1KB, 1) +Write-Host "`n=== Package created: $packagePath ($($packageSize) KB) ===" -ForegroundColor Green +Write-Host "Import into Disco ICT via: Configuration > Plugins > Install Plugin" -ForegroundColor Cyan + +# --- Summary --- +$hasIssues = (-not $reflectionOk) -or ($manifestErrors.Count -gt 0) +if ($hasIssues) { + Write-Host "`n=== BUILD COMPLETED WITH WARNINGS ===" -ForegroundColor Yellow + Write-Host "Review the diagnostics above before installing." -ForegroundColor Yellow + Write-Host "If UIExtensions still don't render after install:" -ForegroundColor Yellow + Write-Host " 1. Check Disco logs for feature initialization messages" -ForegroundColor Yellow + Write-Host " 2. View page source and search for 'layout_uiExtensions'" -ForegroundColor Yellow + Write-Host " - Present but empty = ExecuteAction returned Nothing()/null" -ForegroundColor Yellow + Write-Host " - Missing entirely = Feature not registered (Register() not called)" -ForegroundColor Yellow + Write-Host " 3. Check the installed manifest at:" -ForegroundColor Yellow + Write-Host " App_Data\Plugins\$PluginName\manifest.json" -ForegroundColor Yellow +} else { + Write-Host "`n=== BUILD COMPLETED SUCCESSFULLY ===" -ForegroundColor Green +}