[CmdletBinding()] param( [string]$OutputDirectory = '' ) $ErrorActionPreference = 'Stop' $projectRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path if (-not $OutputDirectory) { $OutputDirectory = Join-Path $projectRoot 'dist' } if (-not [System.IO.Path]::IsPathRooted($OutputDirectory)) { $OutputDirectory = Join-Path $projectRoot $OutputDirectory } $outputRoot = [System.IO.Path]::GetFullPath($OutputDirectory) $projectFull = [System.IO.Path]::GetFullPath($projectRoot) if (-not $outputRoot.StartsWith($projectFull, [StringComparison]::OrdinalIgnoreCase)) { throw 'OutputDirectory must stay inside the N7Cockpit project.' } $version = Get-Date -Format 'yyyyMMdd-HHmmss' $stagingRoot = Join-Path $outputRoot "portable-staging-$version" $packageRoot = Join-Path $stagingRoot 'N7Cockpit' $zipPath = Join-Path $outputRoot "N7Cockpit-portable-$version.zip" New-Item -ItemType Directory -Force -Path $packageRoot | Out-Null $rootFiles = @( '.gitignore', 'build.gradle', 'gradle.properties', 'gradlew', 'gradlew.bat', 'README.md', 'MIGRATION_GUIDE.md', 'settings.gradle' ) foreach ($relative in $rootFiles) { Copy-Item -LiteralPath (Join-Path $projectRoot $relative) ` -Destination (Join-Path $packageRoot $relative) } $directoryPairs = @( @('gradle', 'gradle'), @('app\src', 'app\src') ) foreach ($pair in $directoryPairs) { $destination = Join-Path $packageRoot $pair[1] New-Item -ItemType Directory -Force -Path (Split-Path -Parent $destination) | Out-Null Copy-Item -LiteralPath (Join-Path $projectRoot $pair[0]) ` -Destination $destination -Recurse } New-Item -ItemType Directory -Force -Path (Join-Path $packageRoot 'app') | Out-Null Copy-Item -LiteralPath (Join-Path $projectRoot 'app\build.gradle') ` -Destination (Join-Path $packageRoot 'app\build.gradle') Copy-Item -LiteralPath (Join-Path $projectRoot 'app\proguard-rules.pro') ` -Destination (Join-Path $packageRoot 'app\proguard-rules.pro') New-Item -ItemType Directory -Force -Path (Join-Path $packageRoot 'scripts') | Out-Null $portableScripts = @( 'initialize-migration.ps1', 'verify-migration-environment.ps1', 'check-server-a.ps1', 'test-server-a-dry-run.ps1', 'build-migrated-apk.ps1', 'install-migrated-apk.ps1', 'create-portable-package.ps1' ) foreach ($script in $portableScripts) { Copy-Item -LiteralPath (Join-Path $projectRoot "scripts\$script") ` -Destination (Join-Path $packageRoot "scripts\$script") } New-Item -ItemType Directory -Force -Path (Join-Path $packageRoot 'config') | Out-Null Copy-Item -LiteralPath (Join-Path $projectRoot 'config\migration.example.psd1') ` -Destination (Join-Path $packageRoot 'config\migration.example.psd1') $forbiddenNames = @( 'migration.psd1', 'device-token.txt', 'local.properties' ) $forbidden = Get-ChildItem -Recurse -Force -File -LiteralPath $packageRoot | Where-Object { $_.Name -in $forbiddenNames -or $_.Extension -in @('.jks', '.p12') } if ($forbidden) { throw "Portable package contains forbidden files: $($forbidden.FullName -join ', ')" } $files = Get-ChildItem -Recurse -Force -File -LiteralPath $packageRoot | Sort-Object FullName $manifest = [ordered]@{ package = 'N7Cockpit portable source package' generated_at = (Get-Date).ToUniversalTime().ToString('o') contains_credentials = $false default_execution_target = 'dry_run' file_count = $files.Count files = @($files | ForEach-Object { [ordered]@{ path = $_.FullName.Substring($packageRoot.Length + 1).Replace('\', '/') size = $_.Length sha256 = (Get-FileHash -LiteralPath $_.FullName -Algorithm SHA256).Hash } }) } $manifest | ConvertTo-Json -Depth 5 | Set-Content ` -LiteralPath (Join-Path $packageRoot 'PACKAGE-MANIFEST.json') -Encoding UTF8 Compress-Archive -LiteralPath $packageRoot -DestinationPath $zipPath ` -CompressionLevel Optimal $zipHash = (Get-FileHash -LiteralPath $zipPath -Algorithm SHA256).Hash $checksumPath = "$zipPath.sha256" "$zipHash $([System.IO.Path]::GetFileName($zipPath))" | Set-Content ` -LiteralPath $checksumPath -Encoding ASCII $resolvedStaging = [System.IO.Path]::GetFullPath($stagingRoot) if (-not $resolvedStaging.StartsWith($outputRoot, [StringComparison]::OrdinalIgnoreCase)) { throw 'Refusing to remove staging directory outside output root.' } Remove-Item -LiteralPath $resolvedStaging -Recurse -Force [pscustomobject]@{ Passed = $true Zip = $zipPath Sha256 = $zipHash ChecksumFile = $checksumPath CredentialFilesIncluded = $false }