param([switch]$SkipTests) $ErrorActionPreference = 'Stop' $root = Split-Path -Parent $PSScriptRoot & (Join-Path $PSScriptRoot 'build-launcher.ps1') & (Join-Path $PSScriptRoot 'sync.ps1') if (-not $SkipTests) { & (Join-Path $PSScriptRoot 'test.ps1') } $report = Join-Path $root 'artifacts\validation.json' if (-not (Test-Path -LiteralPath $report)) { throw 'Нет отчёта проверки. Сначала выполните scripts/test.ps1.' } $validated = Get-Content -LiteralPath $report -Raw | ConvertFrom-Json if ($validated.status -ne 'passed') { throw 'Отчёт проверки CAD содержит ошибки.' } if ($null -eq $validated.native_exit_code -or $validated.native_exit_code -ne 0) { throw 'Не подтверждено корректное завершение CAD-процесса. Запустите scripts/test.ps1.' } if (-not $validated.source_sha256 -or $validated.source_sha256.Count -eq 0) { throw 'В отчёте проверки нет снимка исходного кода. Выполните scripts/test.ps1.' } # Compare the complete current set too: checking only saved entries would let # newly added, untested commands enter a -SkipTests package unnoticed. $currentSources = foreach ($directory in @('src', 'tests', 'scripts')) { Get-ChildItem -LiteralPath (Join-Path $root $directory) -File -Recurse | Where-Object { $_.Name -notmatch '\.(pyc|FCBak)$' -and $_.FullName.Substring($root.Length + 1) -notmatch '(^|[\\/])(bin|obj|__pycache__)([\\/]|$)' } | ForEach-Object { $_.FullName.Substring($root.Length + 1) } } $validatedPaths = @($validated.source_sha256 | ForEach-Object { $_.path } | Sort-Object) $currentPaths = @($currentSources | Sort-Object) if (($validatedPaths -join "`n") -cne ($currentPaths -join "`n")) { throw 'Состав исходных файлов изменился после проверки. Выполните scripts/test.ps1.' } foreach ($file in $validated.source_sha256) { $current = (Get-FileHash -LiteralPath (Join-Path $root $file.path) -Algorithm SHA256).Hash.ToLowerInvariant() if ($current -ne $file.sha256) { throw "Исходный файл изменился после проверки: $($file.path). Выполните scripts/test.ps1." } } $enginePath = Join-Path $root ((Get-Content -LiteralPath (Join-Path $root 'data\runtime-path.txt') -Raw).Trim()) $engineRoot = Split-Path -Parent (Split-Path -Parent $enginePath) $workbench = Join-Path $engineRoot 'Mod\QInventor' if (-not (Test-Path -LiteralPath $workbench)) { $workbench = Join-Path (Split-Path -Parent $enginePath) 'Mod\QInventor' } $sourceRoot = Join-Path $root 'src\QInventor' $sourceFiles = @(Get-ChildItem -LiteralPath $sourceRoot -File -Recurse | Where-Object { $_.Extension -ne '.pyc' -and $_.FullName -notmatch '[\\/]__pycache__[\\/]' }) $moduleFiles = @(Get-ChildItem -LiteralPath $workbench -File -Recurse | Where-Object { $_.Extension -ne '.pyc' -and $_.FullName -notmatch '[\\/]__pycache__[\\/]' }) $sourceNames = @($sourceFiles | ForEach-Object { $_.FullName.Substring($sourceRoot.Length + 1) } | Sort-Object) $moduleNames = @($moduleFiles | ForEach-Object { $_.FullName.Substring($workbench.Length + 1) } | Sort-Object) if (($sourceNames -join "`n") -cne ($moduleNames -join "`n")) { throw 'Рабочая среда CAD содержит файлы, которых нет в проверенном снимке исходного кода.' } foreach ($file in $sourceFiles) { $nativeCopy = Join-Path $workbench $file.FullName.Substring($sourceRoot.Length + 1) if ((Get-FileHash -LiteralPath $file.FullName -Algorithm SHA256).Hash -ne (Get-FileHash -LiteralPath $nativeCopy -Algorithm SHA256).Hash) { throw "Рабочая среда CAD отличается от проверенного исходного кода: $($file.Name)" } } $release = Join-Path $root 'release' New-Item -ItemType Directory -Path $release -Force | Out-Null $sourceHashes = Get-ChildItem -LiteralPath (Join-Path $root 'src') -File -Recurse | Where-Object { $_.Extension -ne '.pyc' } | ForEach-Object { @{ path = $_.FullName.Substring($root.Length+1); sha256 = (Get-FileHash -LiteralPath $_.FullName -Algorithm SHA256).Hash.ToLowerInvariant() } } $manifest = @{ application = 'QInventor'; version = '1.0.0'; platform = 'Windows x64'; engine = 'FreeCAD 1.1.4 / Open CASCADE'; officialRuntimeArchiveSha256 = '4828741fc91ee37fafcdb97a1abacb18b04ba451ac4372d9ff7a7349b36f4d6d'; builtAtUtc = [DateTime]::UtcNow.ToString('o'); launcherSha256 = (Get-FileHash -LiteralPath (Join-Path $root 'QInventor.exe') -Algorithm SHA256).Hash.ToLowerInvariant(); validation = $validated; sources = @($sourceHashes) } $manifest | ConvertTo-Json -Depth 20 | Set-Content -LiteralPath (Join-Path $root 'manifest.json') -Encoding utf8 Copy-Item -LiteralPath $report -Destination (Join-Path $release 'validation.json') -Force $archive = Join-Path $release 'QInventor-1.0.0-win-x64-portable.zip' if (Test-Path -LiteralPath $archive) { Move-Item -LiteralPath $archive -Destination (Join-Path $release "QInventor-previous-$([DateTime]::Now.ToString('yyyyMMdd-HHmmss')).zip") } $files = @('QInventor.exe','QInventor.cmd','README.md','LICENSE','THIRD_PARTY_NOTICES.md','manifest.json', 'runtime','src','scripts','tests','docs','examples','data\runtime-path.txt') Push-Location $root try { & 'C:\Program Files\7-Zip\7z.exe' a -tzip $archive @files '-xr!__pycache__' '-xr!*.pyc' '-xr!*.pdb' '-xr!*.FCStd1' '-xr!*.FCBak' '-xr!obj' '-xr!artifacts' '-x!scripts\launcher\bin\*' -mx1 -bsp0 -bso0 if ($LASTEXITCODE -ne 0) { throw 'Не удалось создать переносимый архив.' } } finally { Pop-Location } # Do not globally exclude bin: the runtime's bin directory contains the CAD # executables. The launcher development bin is filtered by exact archive paths. & 'C:\Program Files\7-Zip\7z.exe' t $archive -bsp0 -bso0 if ($LASTEXITCODE -ne 0) { throw 'Проверка целостности переносимого архива завершилась с ошибкой.' } Get-FileHash -LiteralPath $archive -Algorithm SHA256 | Select-Object @{Name='Алгоритм';Expression={$_.Algorithm}}, @{Name='Контрольная сумма';Expression={$_.Hash}}, @{Name='Файл';Expression={$_.Path}} | Format-List Write-Host "Переносимый выпуск: $archive"