diff --git a/Plant3D2026/P_Rec_Annot/bundle/P_Rec_Annot.bundle/PackageContents.xml b/Plant3D2026/P_Rec_Annot/bundle/P_Rec_Annot.bundle/PackageContents.xml index 2bd6de8..5f4f93f 100644 --- a/Plant3D2026/P_Rec_Annot/bundle/P_Rec_Annot.bundle/PackageContents.xml +++ b/Plant3D2026/P_Rec_Annot/bundle/P_Rec_Annot.bundle/PackageContents.xml @@ -1,5 +1,5 @@ - + diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/CompositeModuleManifest.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/CompositeModuleManifest.cs new file mode 100644 index 0000000..4d16cea --- /dev/null +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/CompositeModuleManifest.cs @@ -0,0 +1,27 @@ +using System.Security.Cryptography; +using System.Text.Json; + +namespace PRecAnnot.Plant3D2026; + +internal sealed record CompositeAssemblyManifest(string Name, string Sha256, bool Required); +internal sealed record CompositeManifest(int SchemaVersion, string Package, string Version, IReadOnlyList Modules, IReadOnlyList Assemblies); + +internal static class CompositeModuleManifest +{ + public static CompositeManifest Validate() + { + var assemblyPath = typeof(EntryPoint).Assembly.Location; + var path = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(assemblyPath)!, "..", "module-manifest.json")); + if (!File.Exists(path)) throw new InvalidOperationException("COMPOSITE_MODULE_MANIFEST_MISSING"); + var manifest = JsonSerializer.Deserialize(File.ReadAllText(path), new JsonSerializerOptions { PropertyNameCaseInsensitive = true }) ?? throw new InvalidOperationException("COMPOSITE_MODULE_MANIFEST_INVALID"); + if (manifest.SchemaVersion != 1 || !string.Equals(manifest.Package, "P_Rec_Annot", StringComparison.Ordinal) || manifest.Modules.Count == 0) throw new InvalidOperationException("COMPOSITE_MODULE_MANIFEST_INVALID"); + foreach (var item in manifest.Assemblies.Where(value => value.Required)) + { + var file = Path.Combine(Path.GetDirectoryName(assemblyPath)!, item.Name); + if (!File.Exists(file)) throw new InvalidOperationException($"COMPOSITE_MODULE_ASSEMBLY_MISSING:{item.Name}"); + var hash = Convert.ToHexString(SHA256.HashData(File.ReadAllBytes(file))).ToLowerInvariant(); + if (!string.Equals(hash, item.Sha256, StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException($"COMPOSITE_MODULE_HASH_MISMATCH:{item.Name}"); + } + return manifest; + } +} diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/EntryPoint.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/EntryPoint.cs index e91b0e5..1751f59 100644 --- a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/EntryPoint.cs +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/EntryPoint.cs @@ -15,6 +15,8 @@ public sealed class EntryPoint : IExtensionApplication public void Initialize() { RuntimeDiagnostics.Write("AssemblyInitializeStarted"); + var manifest = CompositeModuleManifest.Validate(); + RuntimeDiagnostics.Write("CompositeModulesValidated", resultStatus: "Validated", details: new Dictionary { ["PackageVersion"] = manifest.Version, ["ModuleCount"] = manifest.Modules.Count }); RibbonIntegration.Register(); // Test automation is driven only by the explicitly generated /b script. // Initialize remains production-equivalent and must not orchestrate commands. diff --git a/Plant3D2026/P_Rec_Annot/tools/Build-Bundle.ps1 b/Plant3D2026/P_Rec_Annot/tools/Build-Bundle.ps1 index 7747dae..b50d3ee 100644 --- a/Plant3D2026/P_Rec_Annot/tools/Build-Bundle.ps1 +++ b/Plant3D2026/P_Rec_Annot/tools/Build-Bundle.ps1 @@ -8,4 +8,8 @@ New-Item -ItemType Directory -Force -Path $contents | Out-Null $output=Join-Path $PSScriptRoot "..\src\PRecAnnot.Plant3D2026\bin\$Configuration\net8.0-windows" Get-ChildItem $output -File -Filter '*.dll' | Copy-Item -Destination $contents -Force Copy-Item (Join-Path $PSScriptRoot '..\bundle\P_Rec_Annot.bundle\PackageContents.xml') $bundleRoot -Force +$modules=@('PHP_REC_ANNOT','AnnotateLineNumbers','AutoLinenumberConnections','AutoElevation','InsertElevation','ExtentsIntersect','DataLinksRead') +$assemblies=Get-ChildItem $contents -File -Filter '*.dll' | Sort-Object Name | ForEach-Object { [ordered]@{ name=$_.Name; sha256=(Get-FileHash $_.FullName -Algorithm SHA256).Hash.ToLowerInvariant(); required=$true } } +$manifest=[ordered]@{ schemaVersion=1; package='P_Rec_Annot'; version='0.2.0'; modules=$modules; assemblies=$assemblies } +$manifest | ConvertTo-Json -Depth 5 | Set-Content (Join-Path $bundleRoot 'Contents\module-manifest.json') -Encoding utf8 Write-Host 'Bundle assembly build completed; deployment is intentionally not performed.' diff --git a/Plant3D2026/P_Rec_Annot/tools/Validate-Bundle.ps1 b/Plant3D2026/P_Rec_Annot/tools/Validate-Bundle.ps1 index 7e58997..7596bc8 100644 --- a/Plant3D2026/P_Rec_Annot/tools/Validate-Bundle.ps1 +++ b/Plant3D2026/P_Rec_Annot/tools/Validate-Bundle.ps1 @@ -7,6 +7,11 @@ if(@($entry).Count -ne 1){throw 'Bundle must have exactly one entry assembly.'} if($entry.ModuleName -match '_netload|OrthoLoader|Settings.xml'){throw 'Legacy loading chain is forbidden.'} if($entry.ModuleName -notmatch 'PRecAnnot\.Plant3D2026\.dll'){throw 'Unexpected entry assembly.'} if(-not (Test-Path (Join-Path $BundleRoot 'Contents\Windows\PRecAnnot.Plant3D2026.dll'))){throw 'Entry assembly is missing.'} +$moduleManifestPath=Join-Path $BundleRoot 'Contents\module-manifest.json' +if(-not (Test-Path $moduleManifestPath)){throw 'Composite module manifest is missing.'} +$moduleManifest=Get-Content $moduleManifestPath -Raw | ConvertFrom-Json +if($moduleManifest.package -ne 'P_Rec_Annot' -or $moduleManifest.version -ne $xml.ApplicationPackage.AppVersion){throw 'Composite module manifest identity/version mismatch.'} +foreach($assembly in $moduleManifest.assemblies){$path=Join-Path $BundleRoot "Contents\Windows\$($assembly.name)"; if(-not (Test-Path $path)){throw "Manifest assembly missing: $($assembly.name)"}; if((Get-FileHash $path -Algorithm SHA256).Hash.ToLowerInvariant() -ne $assembly.sha256){throw "Manifest hash mismatch: $($assembly.name)"}} $forbidden='^(AcMgd|AcDbMgd|AcCoreMgd|AdWindows|PnP.*)\.dll$' $forbiddenFiles=Get-ChildItem $BundleRoot -Recurse -File | Where-Object { $_.Name -match $forbidden -or $_.Extension -in '.pdb','.cs' -or $_.FullName -match '\\(bin|obj)\\' } if($forbiddenFiles){throw "Bundle contains forbidden files: $($forbiddenFiles.FullName -join ', ')"}