[CmdletBinding(DefaultParameterSetName = 'Build')] param( [Parameter(Mandatory = $true)][string]$RepoRoot, [Parameter(Mandatory = $true, ParameterSetName = 'Build')][string]$OutputDirectory, [Parameter(Mandatory = $true, ParameterSetName = 'Build')] [ValidatePattern('^[A-Fa-f0-9]{40}$')][string]$ExpectedSourceCommit, [Parameter(ParameterSetName = 'Build')][string]$MSBuildPath = '', [Parameter(ParameterSetName = 'Build')][string]$EditBinPath = '', [Parameter(ParameterSetName = 'Build')][string]$CefRedistPackagePath = '', [Parameter(ParameterSetName = 'Build')][string]$SignToolPath = '', [Parameter(ParameterSetName = 'Build')] [ValidatePattern('^$|^[A-Fa-f0-9]{40}$')][string]$AuthenticodeCertificateThumbprint = '', [Parameter(ParameterSetName = 'Build')] [ValidateSet('CurrentUser', 'LocalMachine')][string]$CertificateStoreLocation = 'CurrentUser', [Parameter(ParameterSetName = 'Build')][string]$TimestampUrl = '', [Parameter(Mandatory = $true, ParameterSetName = 'Inspect')] [switch]$ValidateRepositoryOnly ) Set-StrictMode -Version 2.0 $ErrorActionPreference = 'Stop' $CefRedistPackageId = 'cef.redist.x86' $CefRedistPackageVersion = '87.1.13' $CefRedistPackageSha256 = ` '34dfe2504c1ffaef02eab1f38578701b045439349997b6465fd5dd6659fab021' $CefGlueSha256 = ` '5057f66d83727e73ce926f918c459fa4a29c9e0a78a2654c09e58210f4355cd7' $CefLibcefSha256 = ` 'a8d4c9974cbdfc989f9c993cba0a388d69e2c5c009661b2c064baaf016e84cfd' function Test-RegularFile([string]$Path, [long]$MaximumBytes) { if (-not [IO.File]::Exists($Path)) { return $false } $item = Get-Item -LiteralPath $Path -Force return $item.Length -gt 0 -and $item.Length -le $MaximumBytes -and (($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -eq 0) } function Assert-NoHardcodedSqlCredentials([string]$RuntimeDirectory) { if (-not [IO.Directory]::Exists($RuntimeDirectory)) { throw 'legacy_runtime_directory_missing' } $pattern = '(?i)(?:User\s+ID|UID)\s*=\s*[A-Za-z0-9_.@\-]{1,128}\s*;\s*' + '(?:Password|PWD)\s*=\s*[^;\s"''{}+]{1,256}\s*;' $candidates = @(Get-ChildItem -LiteralPath $RuntimeDirectory -File -Force | Where-Object { $_.Name -ieq 'Ls_ERP.exe' -or $_.Name -ieq 'lserp-cli.exe' -or ($_.Name -ilike 'Lskj.*.dll') } | Sort-Object Name) if ($candidates.Count -lt 4 -or $candidates.Count -gt 256) { throw 'legacy_runtime_credential_scan_scope_invalid' } $latin1 = [Text.Encoding]::GetEncoding(28591) foreach ($candidate in $candidates) { if (-not (Test-RegularFile $candidate.FullName 128MB)) { throw ('legacy_runtime_credential_scan_file_invalid:' + $candidate.Name) } [byte[]]$bytes = [IO.File]::ReadAllBytes($candidate.FullName) $views = @( $latin1.GetString($bytes), [Text.Encoding]::Unicode.GetString($bytes), [Text.Encoding]::BigEndianUnicode.GetString($bytes) ) foreach ($view in $views) { if ([Text.RegularExpressions.Regex]::IsMatch( $view, $pattern, [Text.RegularExpressions.RegexOptions]::CultureInvariant)) { throw ('legacy_runtime_hardcoded_sql_credential:' + $candidate.Name) } } } } function Get-RelativePath([string]$Root, [string]$Path) { $rootFull = [IO.Path]::GetFullPath($Root).TrimEnd([char[]]@('\', '/')) $pathFull = [IO.Path]::GetFullPath($Path) $prefix = $rootFull + [IO.Path]::DirectorySeparatorChar $comparison = if ($env:OS -eq 'Windows_NT') { [StringComparison]::OrdinalIgnoreCase } else { [StringComparison]::Ordinal } if (-not $pathFull.StartsWith($prefix, $comparison)) { throw 'relative_path_outside_root' } return $pathFull.Substring($prefix.Length).Replace('\', '/') } function Get-ProjectContract([string]$Path) { if (-not (Test-RegularFile $Path 4MB)) { throw "project_file_invalid" } $text = [IO.File]::ReadAllText($Path, (New-Object Text.UTF8Encoding($false, $true))) $xml = New-Object Xml.XmlDocument $xml.PreserveWhitespace = $true $xml.LoadXml($text) $namespace = New-Object Xml.XmlNamespaceManager($xml.NameTable) $namespace.AddNamespace('m', 'http://schemas.microsoft.com/developer/msbuild/2003') $assembly = $xml.SelectSingleNode('//m:AssemblyName', $namespace) $framework = $xml.SelectSingleNode('//m:TargetFrameworkVersion', $namespace) $platforms = @($xml.SelectNodes('//m:PlatformTarget', $namespace) | ForEach-Object { ([string]$_.InnerText).Trim() } | Select-Object -Unique) $references = @($xml.SelectNodes('//m:ProjectReference/@Include', $namespace) | ForEach-Object { ([string]$_.Value).Replace('/', '\') }) $compile = @($xml.SelectNodes('//m:Compile/@Include', $namespace) | ForEach-Object { ([string]$_.Value).Replace('/', '\') }) if ($null -eq $assembly -or $null -eq $framework -or $platforms.Count -eq 0) { throw "project_contract_incomplete" } return [ordered]@{ assemblyName = ([string]$assembly.InnerText).Trim() targetFramework = ([string]$framework.InnerText).Trim() platformTargets = $platforms projectReferences = $references compileItems = $compile } } function Assert-Contains([object[]]$Values, [string]$Expected, [string]$Code) { if ($Values -notcontains $Expected) { throw $Code } } function Test-RepositoryContract([string]$Root) { $paths = [ordered]@{ solution = Join-Path $Root '插件库\Lskj.LserpAll\Lskj.LserpAll.sln' main = Join-Path $Root '插件库\Lskj.Main\Lskj.Main.csproj' cli = Join-Path $Root '插件库\Lskj.Cli\Lskj.Cli.csproj' bridge = Join-Path $Root '插件库\Lskj.AgentBridge\Lskj.AgentBridge.csproj' kernel = Join-Path $Root '插件库\Lskj.CommandKernel\Lskj.CommandKernel.csproj' business = Join-Path $Root '插件库\Lskj.Business\Lskj.Business.csproj' control = Join-Path $Root '插件库\Lskj.Control\Lskj.Control.csproj' compatibility = Join-Path $Root ` '插件库\Lskj.LegacyApiCompatibility.Tests\Lskj.LegacyApiCompatibility.Tests.csproj' } $main = Get-ProjectContract $paths.main $cli = Get-ProjectContract $paths.cli $bridge = Get-ProjectContract $paths.bridge $kernel = Get-ProjectContract $paths.kernel if ($main.assemblyName -ne 'Ls_ERP' -or $cli.assemblyName -ne 'lserp-cli' -or $bridge.assemblyName -ne 'Lskj.AgentBridge' -or $kernel.assemblyName -ne 'Lskj.CommandKernel') { throw 'legacy_assembly_name_mismatch' } foreach ($project in @($main, $cli, $bridge, $kernel)) { if ($project.targetFramework -ne 'v4.0' -or $project.platformTargets.Count -ne 1 -or $project.platformTargets[0] -ne 'x86') { throw 'legacy_framework_or_platform_mismatch' } } Assert-Contains $main.projectReferences '..\Lskj.AgentBridge\Lskj.AgentBridge.csproj' ` 'main_bridge_reference_missing' Assert-Contains $main.projectReferences '..\Lskj.CommandKernel\Lskj.CommandKernel.csproj' ` 'main_kernel_reference_missing' Assert-Contains $main.compileItems 'Hosting\ErpAgentBridgeBootstrap.cs' ` 'main_bridge_bootstrap_missing' Assert-Contains $main.compileItems 'Hosting\ModuleDiagnosticCommandHandlers.cs' ` 'main_module_diagnostic_handlers_missing' Assert-Contains $main.compileItems 'Hosting\BusinessWorkflowRegistration.cs' ` 'main_workflow_registration_missing' Assert-Contains $main.compileItems 'Hosting\SqlWorkflowProcedureGateway.cs' ` 'main_workflow_gateway_missing' Assert-Contains $main.compileItems 'Hosting\SqlDynamicModuleWriteAdapter.cs' ` 'main_dynamic_module_write_adapter_missing' Assert-Contains $main.compileItems 'Hosting\DynamicModuleWriteAvailability.cs' ` 'main_dynamic_module_write_availability_missing' Assert-Contains $main.compileItems 'Hosting\SqlDynamicModuleUpdateAdapter.cs' ` 'main_dynamic_module_update_adapter_missing' Assert-Contains $main.compileItems 'Hosting\DynamicModuleUpdateAvailability.cs' ` 'main_dynamic_module_update_availability_missing' Assert-Contains $main.compileItems 'Hosting\DynamicModuleUpdateCommandHandlers.cs' ` 'main_dynamic_module_update_handlers_missing' Assert-Contains $cli.projectReferences '..\Lskj.Main\Lskj.Main.csproj' ` 'cli_main_reference_missing' Assert-Contains $cli.projectReferences '..\Lskj.AgentBridge\Lskj.AgentBridge.csproj' ` 'cli_bridge_reference_missing' Assert-Contains $cli.projectReferences '..\Lskj.CommandKernel\Lskj.CommandKernel.csproj' ` 'cli_kernel_reference_missing' Assert-Contains $cli.compileItems 'BridgeCliClient.cs' 'cli_bridge_client_missing' Assert-Contains $cli.compileItems 'BridgeCommands.cs' 'cli_bridge_commands_missing' Assert-Contains $cli.compileItems 'WorkflowCommands.cs' ` 'cli_workflow_commands_missing' Assert-Contains $bridge.compileItems 'WorkflowWriteIntegrationEvidence.cs' ` 'bridge_write_evidence_verifier_missing' Assert-Contains $bridge.compileItems 'CustomerAcceptanceBundleEvidence.cs' ` 'bridge_customer_acceptance_bundle_verifier_missing' Assert-Contains $bridge.compileItems 'DynamicModuleWriteAcceptance.cs' ` 'bridge_dynamic_module_write_acceptance_missing' Assert-Contains $bridge.compileItems 'DynamicModuleUpdateAcceptance.cs' ` 'bridge_dynamic_module_update_acceptance_missing' Assert-Contains $kernel.compileItems 'DynamicModuleOperations.cs' ` 'kernel_dynamic_module_operations_missing' Assert-Contains $kernel.compileItems 'DynamicModuleLookupResolution.cs' ` 'kernel_dynamic_module_lookup_resolution_missing' Assert-Contains $kernel.compileItems 'DynamicModuleNativeExecution.cs' ` 'kernel_dynamic_module_native_execution_missing' Assert-Contains $kernel.compileItems 'DynamicModuleWrites.cs' ` 'kernel_dynamic_module_writes_missing' Assert-Contains $kernel.compileItems 'DynamicModuleUpdates.cs' ` 'kernel_dynamic_module_updates_missing' Assert-Contains $kernel.compileItems '..\Lskj.Cli\ModuleInspector.cs' ` 'kernel_module_inspector_missing' $mainProjectText = [IO.File]::ReadAllText( $paths.main, (New-Object Text.UTF8Encoding($false, $true))) if (-not $mainProjectText.Contains('$(LegacyEditBinPath)') -or $mainProjectText -match '(?i)[A-Z]:\\[^\r\n]*editbin\.exe') { throw 'main_editbin_contract_invalid' } foreach ($bindingProjectPath in @($paths.main, $paths.control)) { if (-not (Test-RegularFile $bindingProjectPath 4MB)) { throw 'cef_binding_project_missing' } $bindingProjectText = [IO.File]::ReadAllText( $bindingProjectPath, (New-Object Text.UTF8Encoding($false, $true))) if (-not $bindingProjectText.Contains( 'Xilium.CefGlue, Version=87.1.1.0, Culture=neutral')) { throw 'cef_glue_reference_contract_invalid' } } if (-not (Test-RegularFile $paths.compatibility 1MB)) { throw 'net40_compatibility_gate_missing' } $compatibilityText = [IO.File]::ReadAllText( $paths.compatibility, (New-Object Text.UTF8Encoding($false, $true))) if (-not $compatibilityText.Contains('net40') -or -not $compatibilityText.Contains('Microsoft.NETFramework.ReferenceAssemblies.net40') -or -not $compatibilityText.Contains('ModuleDiagnosticCommandHandlers.cs')) { throw 'net40_compatibility_gate_invalid' } if (-not (Test-RegularFile $paths.solution 8MB)) { throw 'legacy_solution_missing' } $solutionText = [IO.File]::ReadAllText( $paths.solution, (New-Object Text.UTF8Encoding($false, $true))) $solutionContracts = @( 'Lskj.Cli", "..\Lskj.Cli\Lskj.Cli.csproj", "{A7D3D7C2-8F1B-4E48-9E30-8AB657CFC104}"', 'Lskj.Main", "..\Lskj.Main\Lskj.Main.csproj", "{BCA1E2B3-C4AB-4D2C-B519-3DCFDB5B83D6}"', 'Lskj.CommandKernel", "..\Lskj.CommandKernel\Lskj.CommandKernel.csproj", "{84D4754E-9D47-4E60-A8E5-0AD860C319F0}"', 'Lskj.AgentBridge", "..\Lskj.AgentBridge\Lskj.AgentBridge.csproj", "{8DA49516-B088-49CC-BE29-2E7EC3CC1777}"', '{A7D3D7C2-8F1B-4E48-9E30-8AB657CFC104}.Release|Mixed Platforms.ActiveCfg = Release|x86', '{BCA1E2B3-C4AB-4D2C-B519-3DCFDB5B83D6}.Release|Mixed Platforms.ActiveCfg = Release|x86', '{84D4754E-9D47-4E60-A8E5-0AD860C319F0}.Release|Mixed Platforms.ActiveCfg = Release|x86', '{8DA49516-B088-49CC-BE29-2E7EC3CC1777}.Release|Mixed Platforms.ActiveCfg = Release|x86' ) foreach ($solutionContract in $solutionContracts) { if (-not $solutionText.Contains($solutionContract)) { throw 'legacy_solution_configuration_invalid' } } $cefContract = 'packages\cef.redist.x86.87.1.13\build\cef.redist.x86.props' $cefProjectPaths = @( $paths.business, $paths.control, (Join-Path $Root '插件库\Lskj.AutoCreatWord\Lskj.AutoCreatWord.csproj'), (Join-Path $Root '插件库\Lskj.EmbeCad\Lskj.EmbeCad.csproj'), (Join-Path $Root '插件库\Lskj.PubModelAdd2\Lskj.PubModelAdd2.csproj'), (Join-Path $Root '插件库\Lskj.PubModelAdd3\Lskj.PubModelAdd3.csproj'), (Join-Path $Root '插件库\Lskj.PubSpec\Lskj.PubSpec.csproj') ) foreach ($cefProjectPath in $cefProjectPaths) { if (-not (Test-RegularFile $cefProjectPath 4MB)) { throw 'cef_redist_project_missing' } $cefProjectText = [IO.File]::ReadAllText( $cefProjectPath, (New-Object Text.UTF8Encoding($false, $true))) $cefVersions = @([regex]::Matches( $cefProjectText, 'cef\.redist\.x86\.[0-9]+\.[0-9]+\.[0-9]+') | ForEach-Object { $_.Value } | Select-Object -Unique) if (-not $cefProjectText.Contains($cefContract) -or $cefVersions.Count -ne 1 -or $cefVersions[0] -ne ('cef.redist.x86.' + $CefRedistPackageVersion)) { throw 'cef_redist_contract_invalid' } } $cefPackageConfigPaths = @( (Join-Path $Root '插件库\Lskj.AutoCreatWord\packages.config'), (Join-Path $Root '插件库\Lskj.Control\packages.config'), (Join-Path $Root '插件库\Lskj.EmbeCad\packages.config') ) foreach ($cefPackageConfigPath in $cefPackageConfigPaths) { if (-not (Test-RegularFile $cefPackageConfigPath 1MB)) { throw 'cef_redist_package_config_missing' } $cefPackageConfigText = [IO.File]::ReadAllText( $cefPackageConfigPath, (New-Object Text.UTF8Encoding($false, $true))) if (-not $cefPackageConfigText.Contains( ('id="cef.redist.x86" version="' + $CefRedistPackageVersion + '"'))) { throw 'cef_redist_package_config_invalid' } } $dependencyFiles = @( '引用DLL\Newtonsoft.Json.dll', '引用DLL\Xilium.CefGlue.dll', '引用DLL\DevExpress\DevExpress.Data.v15.2.dll', '引用DLL\DevExpress\DevExpress.Utils.v15.2.dll', '引用DLL\DevExpress\DevExpress.XtraEditors.v15.2.dll', '引用DLL\DevExpress\DevExpress.XtraGrid.v15.2.dll', '引用DLL\DevExpress\DevExpress.XtraPrinting.v15.2.dll', '引用DLL\DevExpress\DevExpress.XtraTreeList.v15.2.dll' ) foreach ($relative in $dependencyFiles) { if (-not (Test-RegularFile (Join-Path $Root $relative) 256MB)) { throw 'legacy_reference_dependency_missing' } } if ((Get-FileHash -LiteralPath (Join-Path $Root '引用DLL\Xilium.CefGlue.dll') ` -Algorithm SHA256).Hash.ToLowerInvariant() -ne $CefGlueSha256) { throw 'cef_glue_binding_hash_mismatch' } return [ordered]@{ repositoryContractValid = $true mainAssembly = $main.assemblyName cliAssembly = $cli.assemblyName targetFramework = 'v4.0' platform = 'x86' solutionConfiguration = 'Release|Mixed Platforms' devExpressContract = '15.2' cefRedistContract = [ordered]@{ packageId = $CefRedistPackageId version = $CefRedistPackageVersion sha256 = $CefRedistPackageSha256 } projects = $paths } } function Resolve-CefRedistPackage([string]$ExplicitPath) { $candidates = New-Object System.Collections.Generic.List[string] if (-not [string]::IsNullOrWhiteSpace($ExplicitPath)) { $candidates.Add([IO.Path]::GetFullPath($ExplicitPath)) } if (-not [string]::IsNullOrWhiteSpace($env:NUGET_PACKAGES)) { $candidates.Add((Join-Path $env:NUGET_PACKAGES ("{0}\{1}\{0}.{1}.nupkg" -f ` $CefRedistPackageId, $CefRedistPackageVersion))) } $userProfile = [Environment]::GetFolderPath([Environment+SpecialFolder]::UserProfile) if (-not [string]::IsNullOrWhiteSpace($userProfile)) { $candidates.Add((Join-Path $userProfile (".nuget\packages\{0}\{1}\{0}.{1}.nupkg" -f ` $CefRedistPackageId, $CefRedistPackageVersion))) } foreach ($candidate in $candidates) { if (-not (Test-RegularFile $candidate 256MB)) { continue } $hash = (Get-FileHash -LiteralPath $candidate -Algorithm SHA256).Hash.ToLowerInvariant() if ($hash -ne $CefRedistPackageSha256) { throw 'cef_redist_package_hash_mismatch' } return $candidate } throw 'cef_redist_package_required' } function Expand-PinnedNuGetPackage([string]$PackagePath, [string]$Destination) { Add-Type -AssemblyName System.IO.Compression.FileSystem if ([IO.Directory]::Exists($Destination) -or [IO.File]::Exists($Destination)) { throw 'cef_redist_destination_must_be_new' } [IO.Directory]::CreateDirectory($Destination) | Out-Null $destinationRoot = [IO.Path]::GetFullPath($Destination).TrimEnd( [char[]]@('\', '/')) + [IO.Path]::DirectorySeparatorChar $archive = [IO.Compression.ZipFile]::OpenRead($PackagePath) try { if ($archive.Entries.Count -le 0 -or $archive.Entries.Count -gt 2000) { throw 'cef_redist_archive_entry_count_invalid' } [long]$expandedBytes = 0 foreach ($entry in $archive.Entries) { $relative = $entry.FullName.Replace('\', '/') if ([string]::IsNullOrWhiteSpace($relative) -or $relative.StartsWith('/') -or $relative.Contains(':') -or $relative.Split('/') -contains '..') { throw 'cef_redist_archive_path_invalid' } $expandedBytes += [long]$entry.Length if ($entry.Length -lt 0 -or $entry.Length -gt 512MB -or $expandedBytes -gt 1GB) { throw 'cef_redist_archive_size_invalid' } $full = [IO.Path]::GetFullPath((Join-Path $Destination ` ($relative.Replace('/', [string][IO.Path]::DirectorySeparatorChar)))) if (-not $full.StartsWith($destinationRoot, [StringComparison]::OrdinalIgnoreCase)) { throw 'cef_redist_archive_path_invalid' } if ([string]::IsNullOrEmpty($entry.Name)) { [IO.Directory]::CreateDirectory($full) | Out-Null continue } [IO.Directory]::CreateDirectory([IO.Path]::GetDirectoryName($full)) | Out-Null $input = $entry.Open() $output = [IO.File]::Open( $full, [IO.FileMode]::CreateNew, [IO.FileAccess]::Write, [IO.FileShare]::None) try { $input.CopyTo($output); $output.Flush() } finally { $output.Dispose(); $input.Dispose() } } } finally { $archive.Dispose() } if (-not (Test-RegularFile (Join-Path $Destination ` 'build\cef.redist.x86.props') 4MB)) { throw 'cef_redist_props_missing' } } function Copy-CefRuntime([string]$ExpandedPackage, [string]$RuntimeDirectory) { $source = Join-Path $ExpandedPackage 'CEF' if (-not [IO.Directory]::Exists($source)) { throw 'cef_runtime_source_missing' } $sourceItem = Get-Item -LiteralPath $source -Force if (($sourceItem.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) { throw 'cef_runtime_source_reparse_forbidden' } $sourceRoot = [IO.Path]::GetFullPath($source).TrimEnd([char[]]@('\', '/')) + [IO.Path]::DirectorySeparatorChar $runtimeRoot = [IO.Path]::GetFullPath($RuntimeDirectory).TrimEnd( [char[]]@('\', '/')) + [IO.Path]::DirectorySeparatorChar $files = @(Get-ChildItem -LiteralPath $source -Recurse -File -Force | Sort-Object FullName) if ($files.Count -le 0 -or $files.Count -gt 500) { throw 'cef_runtime_file_count_invalid' } $relativePaths = New-Object ` 'System.Collections.Generic.HashSet[string]' ([StringComparer]::OrdinalIgnoreCase) [long]$totalBytes = 0 foreach ($file in $files) { if (($file.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0 -or $file.Length -le 0 -or $file.Length -gt 512MB) { throw 'cef_runtime_file_invalid' } $totalBytes += [long]$file.Length if ($totalBytes -gt 1GB) { throw 'cef_runtime_total_size_invalid' } $relative = (Get-RelativePath $source $file.FullName).Replace('\', '/') if ([string]::IsNullOrWhiteSpace($relative) -or $relative.StartsWith('/') -or $relative.Contains(':') -or $relative.Split('/') -contains '..' -or -not $relativePaths.Add($relative)) { throw 'cef_runtime_path_invalid' } $destination = [IO.Path]::GetFullPath((Join-Path $RuntimeDirectory ` ($relative.Replace('/', [string][IO.Path]::DirectorySeparatorChar)))) if (-not $destination.StartsWith( $runtimeRoot, [StringComparison]::OrdinalIgnoreCase)) { throw 'cef_runtime_path_invalid' } [IO.Directory]::CreateDirectory([IO.Path]::GetDirectoryName($destination)) | Out-Null if ([IO.File]::Exists($destination)) { if (-not (Test-RegularFile $destination 512MB) -or (Get-FileHash -LiteralPath $destination -Algorithm SHA256).Hash -ne (Get-FileHash -LiteralPath $file.FullName -Algorithm SHA256).Hash) { throw 'cef_runtime_conflict' } } else { [IO.File]::Copy($file.FullName, $destination, $false) } } $requiredHashes = [ordered]@{ 'libcef.dll' = $CefLibcefSha256 'chrome_elf.dll' = 'b6194e1b093a000a7b9a883edfd4114a9e79332775970a20a36da1dd1837e8f3' 'icudtl.dat' = '8364e6c6bf5744357199de0de3f6ba30846ccda70288675b75059e6fd52241f3' 'locales/zh-CN.pak' = '6be9ef1c87b3162253090c00487238bbf4e5466d235cb49fdb69915a03c480cf' 'locales/en-US.pak' = 'f9e993df87cad724a36be1efb4f5a71322c9de4d0885419e5f13ca564115dce7' } foreach ($relative in $requiredHashes.Keys) { $destination = Join-Path $RuntimeDirectory ` ($relative.Replace('/', [string][IO.Path]::DirectorySeparatorChar)) if (-not (Test-RegularFile $destination 512MB) -or (Get-FileHash -LiteralPath $destination -Algorithm SHA256).Hash.ToLowerInvariant() -ne $requiredHashes[$relative]) { throw 'cef_runtime_required_file_invalid' } } } function Assert-CefBindingVersion([string]$RuntimeDirectory) { $bindingPath = Join-Path $RuntimeDirectory 'Xilium.CefGlue.dll' $nativePath = Join-Path $RuntimeDirectory 'libcef.dll' if (-not (Test-RegularFile $bindingPath 64MB) -or -not (Test-RegularFile $nativePath 512MB)) { throw 'cef_runtime_binding_missing' } if ((Get-FileHash -LiteralPath $bindingPath -Algorithm SHA256).Hash.ToLowerInvariant() -ne $CefGlueSha256) { throw 'cef_runtime_binding_hash_mismatch' } if ((Get-FileHash -LiteralPath $nativePath -Algorithm SHA256).Hash.ToLowerInvariant() -ne $CefLibcefSha256) { throw 'cef_runtime_native_hash_mismatch' } $bindingInfo = [Diagnostics.FileVersionInfo]::GetVersionInfo($bindingPath) $nativeInfo = [Diagnostics.FileVersionInfo]::GetVersionInfo($nativePath) $bindingVersion = [string]$bindingInfo.ProductVersion $nativeVersion = [string]$nativeInfo.ProductVersion if ([string]::IsNullOrWhiteSpace($nativeVersion)) { $nativeVersion = [string]$nativeInfo.FileVersion } if (-not $bindingVersion.StartsWith('87.1.1', [StringComparison]::Ordinal) -or (-not [string]::IsNullOrWhiteSpace($nativeVersion) -and -not $nativeVersion.StartsWith('87.', [StringComparison]::Ordinal))) { throw 'cef_runtime_binding_version_mismatch' } } function Resolve-Tool([string]$ExplicitPath, [string]$ExpectedName) { if ([string]::IsNullOrWhiteSpace($ExplicitPath)) { return $null } $full = [IO.Path]::GetFullPath($ExplicitPath) if ([IO.Path]::GetFileName($full) -ne $ExpectedName -or -not (Test-RegularFile $full 128MB)) { throw ("{0}_invalid" -f $ExpectedName.ToLowerInvariant()) } return $full } function Find-MSBuild([string]$ExplicitPath) { $resolved = Resolve-Tool $ExplicitPath 'MSBuild.exe' if ($null -ne $resolved) { return $resolved } $command = Get-Command MSBuild.exe -ErrorAction SilentlyContinue if ($null -ne $command -and (Test-RegularFile $command.Source 128MB)) { return $command.Source } $programFilesX86 = [Environment]::GetFolderPath([Environment+SpecialFolder]::ProgramFilesX86) $vswhere = Join-Path $programFilesX86 'Microsoft Visual Studio\Installer\vswhere.exe' if (Test-RegularFile $vswhere 32MB) { $found = @(& $vswhere -latest -products '*' -requires Microsoft.Component.MSBuild ` -find 'MSBuild\**\Bin\MSBuild.exe' 2>$null) foreach ($candidate in $found) { if (Test-RegularFile ([string]$candidate) 128MB) { return [string]$candidate } } } $framework = Join-Path $env:WINDIR 'Microsoft.NET\Framework\v4.0.30319\MSBuild.exe' if (Test-RegularFile $framework 128MB) { return $framework } throw 'msbuild_not_found' } function Find-EditBin([string]$ExplicitPath) { $resolved = Resolve-Tool $ExplicitPath 'editbin.exe' if ($null -ne $resolved) { return $resolved } $command = Get-Command editbin.exe -ErrorAction SilentlyContinue if ($null -ne $command -and (Test-RegularFile $command.Source 128MB)) { return $command.Source } $programFilesX86 = [Environment]::GetFolderPath([Environment+SpecialFolder]::ProgramFilesX86) $vswhere = Join-Path $programFilesX86 'Microsoft Visual Studio\Installer\vswhere.exe' if (Test-RegularFile $vswhere 32MB) { $installations = @(& $vswhere -products '*' ` -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 ` -property installationPath 2>$null) foreach ($installation in $installations) { $toolRoot = Join-Path ([string]$installation) 'VC\Tools\MSVC' if (-not [IO.Directory]::Exists($toolRoot)) { continue } $candidates = @(Get-ChildItem -LiteralPath $toolRoot -Directory -Force | Sort-Object Name -Descending | ForEach-Object { Join-Path $_.FullName 'bin\Hostx64\x86\editbin.exe' Join-Path $_.FullName 'bin\Hostx64\x64\editbin.exe' }) foreach ($candidate in $candidates) { if (Test-RegularFile $candidate 128MB) { return $candidate } } } } throw 'editbin_not_found' } function Find-SignTool([string]$ExplicitPath) { $resolved = Resolve-Tool $ExplicitPath 'signtool.exe' if ($null -ne $resolved) { return $resolved } $command = Get-Command signtool.exe -ErrorAction SilentlyContinue if ($null -ne $command -and (Test-RegularFile $command.Source 128MB)) { return $command.Source } $programFilesX86 = [Environment]::GetFolderPath([Environment+SpecialFolder]::ProgramFilesX86) foreach ($kitsVersion in @('10', '8.1')) { $binRoot = Join-Path $programFilesX86 ("Windows Kits\{0}\bin" -f $kitsVersion) if (-not [IO.Directory]::Exists($binRoot)) { continue } $candidates = @(Get-ChildItem -LiteralPath $binRoot -Directory -Force | Sort-Object Name -Descending | ForEach-Object { Join-Path $_.FullName 'x86\signtool.exe' Join-Path $_.FullName 'x64\signtool.exe' }) $candidates += @(Join-Path $binRoot 'x86\signtool.exe') foreach ($candidate in $candidates) { if (Test-RegularFile $candidate 128MB) { return $candidate } } } throw 'signtool_not_found' } function Get-SigningCertificate( [string]$Thumbprint, [string]$StoreLocation) { $normalized = $Thumbprint.Replace(' ', '').ToUpperInvariant() $path = "Cert:\{0}\My\{1}" -f $StoreLocation, $normalized if (-not (Test-Path -LiteralPath $path)) { throw 'signing_certificate_missing' } $certificate = Get-Item -LiteralPath $path $codeSigningOid = '1.3.6.1.5.5.7.3.3' $hasCodeSigningEku = @($certificate.EnhancedKeyUsageList | Where-Object { $_.ObjectId.Value -eq $codeSigningOid }).Count -gt 0 $now = [DateTime]::UtcNow if (-not $certificate.HasPrivateKey -or -not $hasCodeSigningEku -or $certificate.NotBefore.ToUniversalTime() -gt $now -or $certificate.NotAfter.ToUniversalTime() -le $now) { throw 'signing_certificate_invalid' } return $certificate } function Read-U16([byte[]]$Bytes, [int]$Offset) { if ($Offset -lt 0 -or $Offset + 2 -gt $Bytes.Length) { throw 'pe_offset_invalid' } return [BitConverter]::ToUInt16($Bytes, $Offset) } function Read-U32([byte[]]$Bytes, [int]$Offset) { if ($Offset -lt 0 -or $Offset + 4 -gt $Bytes.Length) { throw 'pe_offset_invalid' } return [BitConverter]::ToUInt32($Bytes, $Offset) } function Convert-RvaToFileOffset([byte[]]$Bytes, [int]$PeOffset, [uint32]$Rva) { $sectionCount = Read-U16 $Bytes ($PeOffset + 6) $optionalSize = Read-U16 $Bytes ($PeOffset + 20) $sectionTable = $PeOffset + 24 + $optionalSize for ($index = 0; $index -lt $sectionCount; $index++) { $section = $sectionTable + ($index * 40) $virtualSize = Read-U32 $Bytes ($section + 8) $virtualAddress = Read-U32 $Bytes ($section + 12) $rawSize = Read-U32 $Bytes ($section + 16) $rawPointer = Read-U32 $Bytes ($section + 20) $span = [Math]::Max([long]$virtualSize, [long]$rawSize) if ([long]$Rva -ge [long]$virtualAddress -and [long]$Rva -lt ([long]$virtualAddress + $span)) { $offset = [long]$rawPointer + ([long]$Rva - [long]$virtualAddress) if ($offset -lt 0 -or $offset -gt [int]::MaxValue -or $offset -ge $Bytes.Length) { throw 'pe_rva_invalid' } return [int]$offset } } throw 'pe_rva_unmapped' } function Get-ManagedPeInfo([string]$Path) { if (-not (Test-RegularFile $Path 512MB)) { throw 'managed_binary_invalid' } [byte[]]$bytes = [IO.File]::ReadAllBytes($Path) if ((Read-U16 $bytes 0) -ne 0x5A4D) { throw 'pe_dos_signature_invalid' } $peOffset = [int](Read-U32 $bytes 0x3C) if ((Read-U32 $bytes $peOffset) -ne 0x00004550) { throw 'pe_signature_invalid' } $machine = Read-U16 $bytes ($peOffset + 4) $characteristics = Read-U16 $bytes ($peOffset + 22) $optional = $peOffset + 24 if ((Read-U16 $bytes $optional) -ne 0x010B) { throw 'pe32_required' } if ((Read-U32 $bytes ($optional + 92)) -lt 15) { throw 'cli_directory_missing' } $cliRva = Read-U32 $bytes ($optional + 96 + (14 * 8)) if ($cliRva -eq 0) { throw 'cli_header_missing' } $cliOffset = Convert-RvaToFileOffset $bytes $peOffset $cliRva $metadataRva = Read-U32 $bytes ($cliOffset + 8) $corFlags = Read-U32 $bytes ($cliOffset + 16) $metadataOffset = Convert-RvaToFileOffset $bytes $peOffset $metadataRva if ((Read-U32 $bytes $metadataOffset) -ne 0x424A5342) { throw 'cli_metadata_signature_invalid' } $versionLength = [int](Read-U32 $bytes ($metadataOffset + 12)) if ($versionLength -le 0 -or $versionLength -gt 128 -or $metadataOffset + 16 + $versionLength -gt $bytes.Length) { throw 'cli_metadata_version_invalid' } $runtimeVersion = [Text.Encoding]::ASCII.GetString( $bytes, $metadataOffset + 16, $versionLength).Trim([char]0).Trim() $assemblyName = [Reflection.AssemblyName]::GetAssemblyName($Path).Name return [ordered]@{ assemblyName = $assemblyName machine = ("0x{0:x4}" -f $machine) pe32 = $true ilOnly = (($corFlags -band 0x1) -ne 0) bit32Required = (($corFlags -band 0x2) -ne 0) largeAddressAware = (($characteristics -band 0x20) -ne 0) corFlags = ("0x{0:x8}" -f $corFlags) runtimeVersion = $runtimeVersion } } function Write-NewUtf8Json([string]$Path, [object]$Value) { $json = ($Value | ConvertTo-Json -Depth 12) $encoding = New-Object Text.UTF8Encoding($false, $true) [byte[]]$body = $encoding.GetBytes($json + [Environment]::NewLine) $stream = [IO.File]::Open( $Path, [IO.FileMode]::CreateNew, [IO.FileAccess]::Write, [IO.FileShare]::None) try { $stream.Write($body, 0, $body.Length) $stream.Flush() } finally { $stream.Dispose() } } $root = [IO.Path]::GetFullPath($RepoRoot).TrimEnd([char[]]@('\', '/')) if (-not [IO.Directory]::Exists($root)) { throw 'repository_root_missing' } $rootItem = Get-Item -LiteralPath $root -Force if (($rootItem.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) { throw 'repository_root_reparse_forbidden' } $contract = Test-RepositoryContract $root if ($PSCmdlet.ParameterSetName -eq 'Inspect') { [ordered]@{ schemaVersion = '1.0' repositoryContractValid = $true contract = $contract } | ConvertTo-Json -Depth 8 exit 0 } if ($env:OS -ne 'Windows_NT') { throw 'windows_required' } $target = [IO.Path]::GetFullPath($OutputDirectory).TrimEnd([char[]]@('\', '/')) if (-not [IO.Path]::IsPathRooted($target) -or [IO.Directory]::Exists($target) -or [IO.File]::Exists($target) -or $target -eq [IO.Path]::GetPathRoot($target) -or $target -eq $root) { throw 'output_directory_must_be_new_and_specific' } $parent = [IO.Path]::GetDirectoryName($target) if ([string]::IsNullOrWhiteSpace($parent)) { throw 'output_parent_invalid' } [IO.Directory]::CreateDirectory($parent) | Out-Null $parentItem = Get-Item -LiteralPath $parent -Force if (($parentItem.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) { throw 'output_parent_reparse_forbidden' } $referenceAssemblies = Join-Path ` ([Environment]::GetFolderPath([Environment+SpecialFolder]::ProgramFilesX86)) ` 'Reference Assemblies\Microsoft\Framework\.NETFramework\v4.0\mscorlib.dll' if (-not (Test-RegularFile $referenceAssemblies 64MB)) { throw 'dotnet_framework_4_targeting_pack_missing' } $msbuild = Find-MSBuild $MSBuildPath $editbin = Find-EditBin $EditBinPath $cefRedistPackage = Resolve-CefRedistPackage $CefRedistPackagePath $signingEnabled = -not [string]::IsNullOrWhiteSpace($AuthenticodeCertificateThumbprint) $signTool = $null $signingCertificate = $null $timestampUri = $null if ($signingEnabled) { if (-not [Uri]::TryCreate($TimestampUrl, [UriKind]::Absolute, [ref]$timestampUri) -or $timestampUri.Scheme -ne 'https') { throw 'https_timestamp_url_required' } $signTool = Find-SignTool $SignToolPath $signingCertificate = Get-SigningCertificate ` $AuthenticodeCertificateThumbprint $CertificateStoreLocation } elseif (-not [string]::IsNullOrWhiteSpace($SignToolPath) -or -not [string]::IsNullOrWhiteSpace($TimestampUrl)) { throw 'signing_parameters_incomplete' } $gitCommand = Get-Command git.exe -ErrorAction SilentlyContinue if ($null -eq $gitCommand) { $gitCommand = Get-Command git -ErrorAction SilentlyContinue } if ($null -eq $gitCommand) { throw 'git_not_found' } $sourceCommit = ((& $gitCommand.Source -C $root rev-parse HEAD 2>$null) | Out-String).Trim() if ($LASTEXITCODE -ne 0 -or $sourceCommit -notmatch '^[A-Fa-f0-9]{40}$') { throw 'source_commit_unavailable' } if ($sourceCommit.ToLowerInvariant() -ne $ExpectedSourceCommit.ToLowerInvariant()) { throw 'source_commit_mismatch' } $dirty = @(& $gitCommand.Source -C $root status --porcelain=v1 --untracked-files=all 2>$null) if ($LASTEXITCODE -ne 0) { throw 'source_status_unavailable' } if ($dirty.Count -ne 0) { throw 'source_worktree_must_be_clean' } $temporary = Join-Path $parent ('.lserp-legacy-build-' + [Guid]::NewGuid().ToString('N')) $sourceCheckout = Join-Path $parent ('.lserp-legacy-source-' + [Guid]::NewGuid().ToString('N')) $runtime = Join-Path $temporary 'Runtime' $logPath = Join-Path $temporary 'MSBUILD.log' $evidencePath = Join-Path $temporary 'LEGACY-BUILD-EVIDENCE.json' [IO.Directory]::CreateDirectory($runtime) | Out-Null $completed = $false try { & $gitCommand.Source clone --quiet --no-hardlinks --no-checkout $root $sourceCheckout if ($LASTEXITCODE -ne 0 -or -not [IO.Directory]::Exists($sourceCheckout)) { throw 'source_checkout_clone_failed' } & $gitCommand.Source -C $sourceCheckout checkout --quiet --detach $sourceCommit if ($LASTEXITCODE -ne 0) { throw 'source_checkout_failed' } $checkoutCommit = ((& $gitCommand.Source -C $sourceCheckout rev-parse HEAD 2>$null) | Out-String).Trim() if ($LASTEXITCODE -ne 0 -or $checkoutCommit.ToLowerInvariant() -ne $sourceCommit.ToLowerInvariant()) { throw 'source_checkout_commit_mismatch' } $buildContract = Test-RepositoryContract $sourceCheckout $cefDestination = Join-Path $sourceCheckout ("插件库\Lskj.LserpAll\packages\{0}.{1}" -f ` $CefRedistPackageId, $CefRedistPackageVersion) Expand-PinnedNuGetPackage $cefRedistPackage $cefDestination $outDir = $runtime.TrimEnd([char[]]@('\', '/')) + [IO.Path]::DirectorySeparatorChar $devExpressReferencePath = Join-Path $sourceCheckout '引用DLL\DevExpress' $arguments = @( $buildContract.projects.solution, '/nologo', '/m', '/t:Lskj_Cli:Rebuild', '/p:Configuration=Release', '/p:Platform=Mixed Platforms', ('/p:OutDir=' + $outDir), ('/p:ReferencePath=' + $devExpressReferencePath), ('/p:LegacyEditBinPath=' + $editbin), '/p:DebugSymbols=false', '/p:DebugType=None' ) & $msbuild @arguments 2>&1 | Tee-Object -LiteralPath $logPath $buildExitCode = $LASTEXITCODE if ($buildExitCode -ne 0) { throw 'legacy_msbuild_failed' } Copy-CefRuntime $cefDestination $runtime $required = [ordered]@{ erp = 'Ls_ERP.exe' cli = 'lserp-cli.exe' bridge = 'Lskj.AgentBridge.dll' kernel = 'Lskj.CommandKernel.dll' core = 'Lskj.Core.dll' json = 'Newtonsoft.Json.dll' cefGlue = 'Xilium.CefGlue.dll' cefNative = 'libcef.dll' cefChromeElf = 'chrome_elf.dll' cefIcu = 'icudtl.dat' cefZhCn = 'locales\zh-CN.pak' cefEnUs = 'locales\en-US.pak' devexpressData = 'DevExpress.Data.v15.2.dll' devexpressEditors = 'DevExpress.XtraEditors.v15.2.dll' devexpressGrid = 'DevExpress.XtraGrid.v15.2.dll' } foreach ($name in $required.Values) { if (-not (Test-RegularFile (Join-Path $runtime $name) 512MB)) { throw ("required_build_artifact_missing:{0}" -f $name) } } Assert-CefBindingVersion $runtime $signedRelativeFiles = @( 'Ls_ERP.exe', 'lserp-cli.exe', 'Lskj.AgentBridge.dll', 'Lskj.CommandKernel.dll', 'Lskj.Core.dll' ) Assert-NoHardcodedSqlCredentials $runtime if ($signingEnabled) { foreach ($relative in $signedRelativeFiles) { $fileToSign = Join-Path $runtime $relative $signArguments = @( 'sign', '/nologo', '/sha1', $signingCertificate.Thumbprint, '/s', 'My', '/fd', 'SHA256', '/tr', $timestampUri.AbsoluteUri, '/td', 'SHA256', $fileToSign ) if ($CertificateStoreLocation -eq 'LocalMachine') { $signArguments = @('sign', '/nologo', '/sm', '/sha1', $signingCertificate.Thumbprint, '/s', 'My', '/fd', 'SHA256', '/tr', $timestampUri.AbsoluteUri, '/td', 'SHA256', $fileToSign) } & $signTool @signArguments 2>&1 | Tee-Object -LiteralPath $logPath -Append if ($LASTEXITCODE -ne 0) { throw 'legacy_authenticode_signing_failed' } $signature = Get-AuthenticodeSignature -LiteralPath $fileToSign if ($signature.Status -ne [System.Management.Automation.SignatureStatus]::Valid -or $null -eq $signature.SignerCertificate -or $signature.SignerCertificate.Thumbprint -ne $signingCertificate.Thumbprint) { throw 'legacy_authenticode_verification_failed' } } } $erpInfo = Get-ManagedPeInfo (Join-Path $runtime $required.erp) $cliInfo = Get-ManagedPeInfo (Join-Path $runtime $required.cli) if ($erpInfo.assemblyName -ne 'Ls_ERP' -or $cliInfo.assemblyName -ne 'lserp-cli' -or $erpInfo.machine -ne '0x014c' -or $cliInfo.machine -ne '0x014c' -or -not $erpInfo.pe32 -or -not $cliInfo.pe32 -or -not $erpInfo.ilOnly -or -not $cliInfo.ilOnly -or -not $erpInfo.bit32Required -or -not $cliInfo.bit32Required -or -not $erpInfo.largeAddressAware -or -not $erpInfo.runtimeVersion.StartsWith('v4.0', [StringComparison]::Ordinal) -or -not $cliInfo.runtimeVersion.StartsWith('v4.0', [StringComparison]::Ordinal)) { throw 'legacy_binary_contract_invalid' } $sourceEvidencePaths = @( '插件库\Lskj.Main\Lskj.Main.csproj', '插件库\Lskj.Main\FrmMain.cs', '插件库\Lskj.Main\Hosting\ErpAgentBridgeBootstrap.cs', '插件库\Lskj.Main\Hosting\ModuleDiagnosticCommandHandlers.cs', '插件库\Lskj.Main\Hosting\BusinessWorkflowRegistration.cs', '插件库\Lskj.Main\Hosting\SqlWorkflowProcedureGateway.cs', '插件库\Lskj.Main\Hosting\SqlDynamicModuleWriteAdapter.cs', '插件库\Lskj.Main\Hosting\DynamicModuleWriteAvailability.cs', '插件库\Lskj.Main\Hosting\SqlDynamicModuleUpdateAdapter.cs', '插件库\Lskj.Main\Hosting\DynamicModuleUpdateAvailability.cs', '插件库\Lskj.Main\Hosting\DynamicModuleUpdateCommandHandlers.cs', '插件库\Lskj.Cli\Lskj.Cli.csproj', '插件库\Lskj.Cli\CliApplication.cs', '插件库\Lskj.Cli\BridgeCliClient.cs', '插件库\Lskj.Cli\BridgeCommands.cs', '插件库\Lskj.Cli\WorkflowCommands.cs', '插件库\Lskj.Cli\ModuleInspector.cs', '插件库\Lskj.Control\Lskj.Control.csproj', '插件库\Lskj.AgentBridge\AgentBridgeRuntime.cs', '插件库\Lskj.AgentBridge\BusinessAcceptanceEvidence.cs', '插件库\Lskj.AgentBridge\WorkflowWriteIntegrationEvidence.cs', '插件库\Lskj.AgentBridge\CustomerAcceptanceBundleEvidence.cs', '插件库\Lskj.AgentBridge\DynamicModuleWriteAcceptance.cs', '插件库\Lskj.AgentBridge\DynamicModuleUpdateAcceptance.cs', '插件库\Lskj.Data\Lskj.Data.csproj', '插件库\Lskj.LegacyApiCompatibility.Tests\Lskj.LegacyApiCompatibility.Tests.csproj', '插件库\Lskj.CommandKernel\CommandDispatcher.cs', '插件库\Lskj.CommandKernel\ModuleMenuDiscovery.cs', '插件库\Lskj.CommandKernel\DynamicModuleOperations.cs', '插件库\Lskj.CommandKernel\DynamicModuleLookupResolution.cs', '插件库\Lskj.CommandKernel\DynamicModuleNativeExecution.cs', '插件库\Lskj.CommandKernel\DynamicModuleWrites.cs', '插件库\Lskj.CommandKernel\DynamicModuleUpdates.cs' ) $sourceFiles = @($sourceEvidencePaths | ForEach-Object { $full = Join-Path $sourceCheckout $_ if (-not (Test-RegularFile $full 8MB)) { throw 'source_evidence_file_missing' } [ordered]@{ path = $_.Replace('\', '/') sha256 = (Get-FileHash -LiteralPath $full -Algorithm SHA256).Hash.ToLowerInvariant() } }) $files = @() foreach ($file in @(Get-ChildItem -LiteralPath $temporary -Recurse -File -Force | Where-Object { $_.FullName -ne $evidencePath } | Sort-Object FullName)) { if (($file.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0 -or $file.Length -le 0 -or $file.Length -gt 512MB) { throw 'build_artifact_file_invalid' } $files += [ordered]@{ path = (Get-RelativePath $temporary $file.FullName) sizeBytes = $file.Length sha256 = (Get-FileHash -LiteralPath $file.FullName -Algorithm SHA256).Hash.ToLowerInvariant() } } if ($files.Count -le 0 -or $files.Count -gt 2000) { throw 'build_artifact_count_invalid' } Remove-Item -LiteralPath $sourceCheckout -Recurse -Force if ([IO.Directory]::Exists($sourceCheckout)) { throw 'source_checkout_cleanup_failed' } $sourceCheckout = '' $remainingHardGates = @( 'customer Windows ERP startup and named-pipe ACL integration', 'customer SQL Server transaction, rollback, idempotency and audit evidence', 'signed workflow acceptance manifests bound to final runtime configuration' ) if (-not $signingEnabled) { $remainingHardGates = @( 'valid Authenticode signatures for final legacy binaries and installer' ) + $remainingHardGates } $report = [ordered]@{ schemaVersion = '1.0' generatedAtUtc = [DateTime]::UtcNow.ToString('o') buildVerified = $true releaseReadiness = $false sourceCommit = $sourceCommit.ToLowerInvariant() sourceWorktreeDirty = $false configuration = 'Release' platform = 'x86' targetFramework = 'v4.0' devExpressContract = '15.2' dependencies = [ordered]@{ cefRedistX86 = [ordered]@{ packageId = $CefRedistPackageId version = $CefRedistPackageVersion sha256 = $CefRedistPackageSha256 } } msbuildVersion = ([Diagnostics.FileVersionInfo]::GetVersionInfo($msbuild).FileVersion) editbinVersion = ([Diagnostics.FileVersionInfo]::GetVersionInfo($editbin).FileVersion) authenticode = [ordered]@{ signed = $signingEnabled certificateThumbprint = if ($signingEnabled) { $signingCertificate.Thumbprint.ToUpperInvariant() } else { $null } certificateStoreLocation = if ($signingEnabled) { $CertificateStoreLocation } else { $null } timestampUrl = if ($signingEnabled) { $timestampUri.AbsoluteUri } else { $null } files = if ($signingEnabled) { $signedRelativeFiles } else { @() } } erpBinary = $erpInfo cliBinary = $cliInfo sourceFiles = $sourceFiles files = $files remainingHardGates = $remainingHardGates } Write-NewUtf8Json $evidencePath $report [IO.Directory]::Move($temporary, $target) $completed = $true [ordered]@{ buildVerified = $true releaseReadiness = $false outputDirectory = $target evidencePath = (Join-Path $target 'LEGACY-BUILD-EVIDENCE.json') sourceCommit = $sourceCommit.ToLowerInvariant() } | ConvertTo-Json -Depth 4 } finally { if (-not [string]::IsNullOrWhiteSpace($sourceCheckout) -and [IO.Directory]::Exists($sourceCheckout)) { Remove-Item -LiteralPath $sourceCheckout -Recurse -Force } if (-not $completed -and [IO.Directory]::Exists($temporary)) { Remove-Item -LiteralPath $temporary -Recurse -Force } }