1010 lines
47 KiB
PowerShell
1010 lines
47 KiB
PowerShell
[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('<TargetFramework>net40</TargetFramework>') -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
|
|
}
|
|
}
|