Add Vagrant, fixes for CI/CD pipeline, and Windows dev bootstrap script
This commit is contained in:
@@ -0,0 +1,252 @@
|
||||
# bootstrap.ps1 — Windows developer environment bootstrapper for Android File Handler
|
||||
# - Installs Chocolatey (if missing)
|
||||
# - Installs Vagrant (Chocolatey or direct from HashiCorp)
|
||||
# - Installs pyenv-win (if missing)
|
||||
# - Installs Python build dependencies (Chocolatey)
|
||||
# - Installs zlib from source (https://zlib.net/current/zlib.tar.gz)
|
||||
# - Installs libffi from GitHub releases (https://github.com/libffi/libffi)
|
||||
# - Installs latest Python (from pyproject.toml)
|
||||
# - Sets that Python as global default
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
Write-Host "=== Android File Handler Windows Dev Bootstrap ===" -ForegroundColor Cyan
|
||||
|
||||
# --- Install Chocolatey ---
|
||||
if (-not (Get-Command choco -ErrorAction SilentlyContinue)) {
|
||||
Write-Host "Installing Chocolatey..." -ForegroundColor Yellow
|
||||
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12
|
||||
Invoke-Expression ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))
|
||||
$env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path", "User")
|
||||
}
|
||||
|
||||
# --- Install Vagrant ---
|
||||
$vagrantInstalled = Get-Command vagrant -ErrorAction SilentlyContinue
|
||||
if (-not $vagrantInstalled) {
|
||||
Write-Host "Installing Vagrant..." -ForegroundColor Yellow
|
||||
$chocoAvailable = Get-Command choco -ErrorAction SilentlyContinue
|
||||
if ($chocoAvailable) {
|
||||
Write-Host " Installing via Chocolatey..." -ForegroundColor Yellow
|
||||
choco install vagrant -y --no-progress
|
||||
}
|
||||
else {
|
||||
Write-Host " Chocolatey not available — downloading from HashiCorp..." -ForegroundColor Yellow
|
||||
$vagrantVersion = "2.4.9"
|
||||
$msiUrl = "https://releases.hashicorp.com/vagrant/$vagrantVersion/vagrant_${vagrantVersion}_windows_amd64.msi"
|
||||
$msiPath = "$env:TEMP\vagrant_${vagrantVersion}_windows_amd64.msi"
|
||||
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12
|
||||
Write-Host " Downloading $msiUrl ..." -ForegroundColor Yellow
|
||||
(New-Object System.Net.WebClient).DownloadFile($msiUrl, $msiPath)
|
||||
Write-Host " Installing MSI..." -ForegroundColor Yellow
|
||||
$msiProc = Start-Process -FilePath "msiexec.exe" `
|
||||
-ArgumentList "/i", $msiPath, "/qn", "/norestart" `
|
||||
-Wait -PassThru
|
||||
if ($msiProc.ExitCode -ne 0) {
|
||||
Write-Host "ERROR: Vagrant MSI install failed with exit code $($msiProc.ExitCode)" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
Remove-Item $msiPath -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
$env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path", "User")
|
||||
Write-Host " Vagrant installed: $(vagrant --version)" -ForegroundColor Green
|
||||
}
|
||||
else {
|
||||
Write-Host "Vagrant already installed: $(vagrant --version)" -ForegroundColor Green
|
||||
}
|
||||
|
||||
# --- Install pyenv-win ---
|
||||
$pyenvRoot = "$env:USERPROFILE\.pyenv"
|
||||
$pyenvBin = "$pyenvRoot\pyenv-win\bin\pyenv.bat"
|
||||
if (-not (Test-Path $pyenvBin)) {
|
||||
Write-Host "Installing pyenv-win..." -ForegroundColor Yellow
|
||||
choco install pyenv-win -y --no-progress
|
||||
$env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path", "User")
|
||||
}
|
||||
else {
|
||||
Write-Host "pyenv-win already installed." -ForegroundColor Green
|
||||
}
|
||||
|
||||
# --- Install Python build dependencies ---
|
||||
# Package IDs verified against https://community.chocolatey.org/packages
|
||||
# zlib has no Chocolatey package; libffi has no Chocolatey package — both omitted
|
||||
$buildDeps = @('visualcpp-build-tools', 'make', 'openssl.light', 'bzip2', 'SQLite')
|
||||
$installedPkgs = choco list --no-progress --limit-output 2>&1 |
|
||||
Where-Object { $_ -match '\|' } |
|
||||
ForEach-Object { ($_ -split '\|')[0].ToLower() }
|
||||
$toInstall = @($buildDeps | Where-Object { $_.ToLower() -notin $installedPkgs })
|
||||
if ($toInstall.Count -gt 0) {
|
||||
Write-Host "Installing Python build dependencies: $($toInstall -join ', ')..." -ForegroundColor Yellow
|
||||
choco install -y --no-progress @toInstall
|
||||
} else {
|
||||
Write-Host "Python build dependencies already installed." -ForegroundColor Green
|
||||
}
|
||||
|
||||
|
||||
# Shared install root for libraries not on Chocolatey
|
||||
$devLibsRoot = "$env:USERPROFILE\.devlibs"
|
||||
New-Item -ItemType Directory -Path $devLibsRoot -Force | Out-Null
|
||||
|
||||
# --- Install zlib from source (https://zlib.net/current/zlib.tar.gz) ---
|
||||
$zlibInstallDir = "$devLibsRoot\zlib"
|
||||
if (-not (Test-Path "$zlibInstallDir\include\zlib.h")) {
|
||||
Write-Host "Installing zlib from source..." -ForegroundColor Yellow
|
||||
|
||||
# Locate MSVC build environment via vswhere
|
||||
$vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe"
|
||||
if (-not (Test-Path $vswhere)) {
|
||||
Write-Host "ERROR: vswhere.exe not found — Visual Studio Build Tools required to build zlib." -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
$vsPath = (& $vswhere -latest -products * -requires Microsoft.VisualCpp.Tools.HostX86.TargetX64 -property installationPath 2>&1).Trim()
|
||||
$vcvarsall = "$vsPath\VC\Auxiliary\Build\vcvarsall.bat"
|
||||
if (-not (Test-Path $vcvarsall)) {
|
||||
Write-Host "ERROR: vcvarsall.bat not found at $vcvarsall" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Download source tarball
|
||||
$zlibTar = "$env:TEMP\zlib.tar.gz"
|
||||
Write-Host " Downloading zlib source..." -ForegroundColor Yellow
|
||||
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12
|
||||
(New-Object System.Net.WebClient).DownloadFile('https://zlib.net/current/zlib.tar.gz', $zlibTar)
|
||||
|
||||
# Extract (tar.exe ships with Windows 10 1803+)
|
||||
$zlibBuildDir = "$env:TEMP\zlib-build"
|
||||
if (Test-Path $zlibBuildDir) { Remove-Item $zlibBuildDir -Recurse -Force }
|
||||
New-Item -ItemType Directory -Path $zlibBuildDir | Out-Null
|
||||
tar -xzf $zlibTar -C $zlibBuildDir --strip-components=1
|
||||
|
||||
# Build with MSVC nmake via a wrapper cmd script
|
||||
Write-Host " Building zlib with MSVC nmake..." -ForegroundColor Yellow
|
||||
$buildCmd = "$env:TEMP\zlib-build.cmd"
|
||||
"@echo off`r`ncall `"$vcvarsall`" x64`r`ncd /d `"$zlibBuildDir`"`r`nnmake -f win32\Makefile.msc`r`n" |
|
||||
Set-Content $buildCmd -Encoding ASCII
|
||||
$proc = Start-Process cmd.exe -ArgumentList "/c `"$buildCmd`"" -Wait -PassThru -NoNewWindow
|
||||
Remove-Item $buildCmd -Force -ErrorAction SilentlyContinue
|
||||
if ($proc.ExitCode -ne 0) {
|
||||
Write-Host "ERROR: zlib build failed (exit $($proc.ExitCode))" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Install headers and libs
|
||||
New-Item -ItemType Directory -Path "$zlibInstallDir\include" -Force | Out-Null
|
||||
New-Item -ItemType Directory -Path "$zlibInstallDir\lib" -Force | Out-Null
|
||||
New-Item -ItemType Directory -Path "$zlibInstallDir\bin" -Force | Out-Null
|
||||
Copy-Item "$zlibBuildDir\zlib.h" "$zlibInstallDir\include\"
|
||||
Copy-Item "$zlibBuildDir\zconf.h" "$zlibInstallDir\include\"
|
||||
Copy-Item "$zlibBuildDir\zlib.lib" "$zlibInstallDir\lib\"
|
||||
Copy-Item "$zlibBuildDir\zdll.lib" "$zlibInstallDir\lib\" -ErrorAction SilentlyContinue
|
||||
Copy-Item "$zlibBuildDir\zlib1.dll" "$zlibInstallDir\bin\" -ErrorAction SilentlyContinue
|
||||
|
||||
# Cleanup
|
||||
Remove-Item $zlibTar -Force -ErrorAction SilentlyContinue
|
||||
Remove-Item $zlibBuildDir -Recurse -Force -ErrorAction SilentlyContinue
|
||||
|
||||
Write-Host "zlib installed to $zlibInstallDir" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "zlib already installed at $zlibInstallDir" -ForegroundColor Green
|
||||
}
|
||||
|
||||
# Add zlib bin dir to user PATH (persists across sessions; also updates current session)
|
||||
$zlibBinDir = "$zlibInstallDir\bin"
|
||||
$userPath = [System.Environment]::GetEnvironmentVariable('Path', 'User')
|
||||
if ($userPath -notmatch [regex]::Escape($zlibBinDir)) {
|
||||
[System.Environment]::SetEnvironmentVariable('Path', "$userPath;$zlibBinDir", 'User')
|
||||
Write-Host " Added $zlibBinDir to user PATH" -ForegroundColor Green
|
||||
}
|
||||
$env:Path = [System.Environment]::GetEnvironmentVariable('Path', 'Machine') + ';' +
|
||||
[System.Environment]::GetEnvironmentVariable('Path', 'User')
|
||||
|
||||
# --- Install libffi from GitHub releases (https://github.com/libffi/libffi) ---
|
||||
$libffiInstallDir = "$devLibsRoot\libffi"
|
||||
if (-not (Test-Path "$libffiInstallDir\include\ffi.h")) {
|
||||
Write-Host "Fetching latest libffi release from GitHub..." -ForegroundColor Yellow
|
||||
|
||||
$apiResponse = Invoke-RestMethod `
|
||||
-Uri 'https://api.github.com/repos/libffi/libffi/releases/latest' `
|
||||
-Headers @{ 'User-Agent' = 'android-file-handler-bootstrap' }
|
||||
$libffiTag = $apiResponse.tag_name # e.g. "v3.5.2"
|
||||
$libffiVersion = $libffiTag -replace '^v', '' # e.g. "3.5.2"
|
||||
Write-Host " Latest libffi: $libffiTag" -ForegroundColor Yellow
|
||||
|
||||
$zipName = "libffi-$libffiVersion-x86-64bit-msvc-binaries.zip"
|
||||
$downloadUrl = "https://github.com/libffi/libffi/releases/download/$libffiTag/$zipName"
|
||||
$zipPath = "$env:TEMP\$zipName"
|
||||
|
||||
Write-Host " Downloading $zipName..." -ForegroundColor Yellow
|
||||
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12
|
||||
(New-Object System.Net.WebClient).DownloadFile($downloadUrl, $zipPath)
|
||||
|
||||
# Extract — zip contains a single top-level subdirectory
|
||||
$extractTemp = "$env:TEMP\libffi-extract"
|
||||
if (Test-Path $extractTemp) { Remove-Item $extractTemp -Recurse -Force }
|
||||
Expand-Archive -Path $zipPath -DestinationPath $extractTemp -Force
|
||||
|
||||
if (Test-Path $libffiInstallDir) { Remove-Item $libffiInstallDir -Recurse -Force }
|
||||
$innerDir = Get-ChildItem $extractTemp -Directory | Select-Object -First 1
|
||||
if ($innerDir) {
|
||||
Move-Item $innerDir.FullName $libffiInstallDir
|
||||
} else {
|
||||
# Flat zip — use the extract root directly
|
||||
Move-Item $extractTemp $libffiInstallDir
|
||||
}
|
||||
|
||||
Remove-Item $zipPath -Force -ErrorAction SilentlyContinue
|
||||
if (Test-Path $extractTemp) { Remove-Item $extractTemp -Recurse -Force -ErrorAction SilentlyContinue }
|
||||
|
||||
Write-Host "libffi $libffiVersion installed to $libffiInstallDir" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "libffi already installed at $libffiInstallDir" -ForegroundColor Green
|
||||
}
|
||||
|
||||
# --- Parse Python version from pyproject.toml ---
|
||||
$scriptDir = if ($PSScriptRoot) { $PSScriptRoot } else { Split-Path -Parent $MyInvocation.MyCommand.Path }
|
||||
$repoRoot = Resolve-Path (Join-Path $scriptDir "..\..\..")
|
||||
$pyprojectPath = Join-Path $repoRoot "pyproject.toml"
|
||||
if (-not (Test-Path $pyprojectPath)) {
|
||||
Write-Host "ERROR: pyproject.toml not found at $pyprojectPath" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
$pyproject = Get-Content $pyprojectPath -Raw
|
||||
$pyverMatch = $pyproject | Select-String -Pattern 'requires-python\s*=\s*"([^"]+)"' | ForEach-Object { $_.Matches[0].Groups[1].Value }
|
||||
if (-not $pyverMatch) {
|
||||
Write-Host "ERROR: Could not find requires-python in pyproject.toml" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
# Use the highest version in the range (e.g. ">=3.13, <3.15" → 3.14.x)
|
||||
$pyver = $pyverMatch -replace '[^0-9.,<>= ]', ''
|
||||
$maxVer = ($pyver -split ',') | Where-Object { $_ -match '<' } | ForEach-Object { $_ -replace '[^0-9.]', '' } | Sort-Object -Descending | Select-Object -First 1
|
||||
if ($maxVer) {
|
||||
$major, $minor = $maxVer -split '\.'
|
||||
$targetMinor = [int]$minor - 1
|
||||
$targetVer = "$major.$targetMinor.x"
|
||||
} else {
|
||||
$targetVer = $pyver -replace '[^0-9.]', ''
|
||||
}
|
||||
Write-Host "Target Python version: $targetVer" -ForegroundColor Cyan
|
||||
|
||||
# --- Install and set global Python version ---
|
||||
$pyenv = "pyenv"
|
||||
if (-not (Get-Command $pyenv -ErrorAction SilentlyContinue)) {
|
||||
$pyenv = "$pyenvBin"
|
||||
}
|
||||
|
||||
$versionPrefix = $targetVer -replace '\.x$', ''
|
||||
$installedVersions = (& $pyenv versions 2>&1) | Out-String
|
||||
if ($installedVersions -notmatch [regex]::Escape($versionPrefix)) {
|
||||
Write-Host "Installing Python $targetVer via pyenv-win..." -ForegroundColor Yellow
|
||||
& $pyenv install $targetVer -q
|
||||
} else {
|
||||
Write-Host "Python $targetVer already installed via pyenv." -ForegroundColor Green
|
||||
}
|
||||
|
||||
$currentGlobal = ((& $pyenv global 2>&1) | Out-String).Trim()
|
||||
if ($currentGlobal -notmatch [regex]::Escape($versionPrefix)) {
|
||||
& $pyenv global $targetVer
|
||||
Write-Host "pyenv-win global version set to $targetVer" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "pyenv-win global already set to $currentGlobal" -ForegroundColor Green
|
||||
}
|
||||
|
||||
Write-Host "=== Bootstrap complete ===" -ForegroundColor Cyan
|
||||
@@ -1,10 +1,13 @@
|
||||
import os
|
||||
block_cipher = None
|
||||
SPEC_DIR = os.path.dirname(os.path.abspath(SPEC))
|
||||
SRC_DIR = os.path.normpath(os.path.join(SPEC_DIR, '..', '..', 'src'))
|
||||
a = Analysis(
|
||||
['../../src/main.py'],
|
||||
pathex=['src'],
|
||||
[os.path.join(SRC_DIR, 'main.py')],
|
||||
pathex=[SRC_DIR],
|
||||
binaries=[],
|
||||
datas=[
|
||||
('../../src/gui', 'gui'),
|
||||
(os.path.join(SRC_DIR, 'gui'), 'gui'),
|
||||
],
|
||||
hiddenimports=[
|
||||
# GUI modules
|
||||
@@ -33,7 +36,8 @@ a = Analysis(
|
||||
'managers.transfer_manager',
|
||||
# Utility modules
|
||||
'utils',
|
||||
'utils.file_deduplication'
|
||||
'utils.file_deduplication',
|
||||
'utils.security_utils'
|
||||
],
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
import os
|
||||
block_cipher = None
|
||||
SPEC_DIR = os.path.dirname(os.path.abspath(SPEC))
|
||||
SRC_DIR = os.path.normpath(os.path.join(SPEC_DIR, '..', '..', 'src'))
|
||||
a = Analysis(
|
||||
['../../src/main.py'],
|
||||
pathex=['src'],
|
||||
[os.path.join(SRC_DIR, 'main.py')],
|
||||
pathex=[SRC_DIR],
|
||||
binaries=[],
|
||||
datas=[
|
||||
('../../src/gui', 'gui'),
|
||||
('../../scripts/debian_postinst.sh', 'scripts'),
|
||||
(os.path.join(SRC_DIR, 'gui'), 'gui'),
|
||||
(os.path.join(SPEC_DIR, '..', 'debian_postinst.sh'), 'scripts'),
|
||||
],
|
||||
hiddenimports=[
|
||||
# GUI modules
|
||||
@@ -35,7 +38,8 @@ a = Analysis(
|
||||
'managers.transfer_manager',
|
||||
# Utility modules
|
||||
'utils',
|
||||
'utils.file_deduplication'
|
||||
'utils.file_deduplication',
|
||||
'utils.security_utils'
|
||||
],
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import os
|
||||
block_cipher = None
|
||||
SPEC_DIR = os.path.dirname(os.path.abspath(SPEC))
|
||||
SRC_DIR = os.path.normpath(os.path.join(SPEC_DIR, '..', '..', 'src'))
|
||||
a = Analysis(
|
||||
['../../src/main.py'],
|
||||
pathex=['src'],
|
||||
[os.path.join(SRC_DIR, 'main.py')],
|
||||
pathex=[SRC_DIR],
|
||||
binaries=[],
|
||||
datas=[
|
||||
('../../src/gui', 'gui'),
|
||||
('../../scripts/rhel_postinst.sh', 'scripts'),
|
||||
(os.path.join(SRC_DIR, 'gui'), 'gui'),
|
||||
(os.path.join(SPEC_DIR, '..', 'rhel_postinst.sh'), 'scripts'),
|
||||
],
|
||||
hiddenimports=[
|
||||
# GUI modules
|
||||
@@ -34,7 +37,8 @@ a = Analysis(
|
||||
'managers.transfer_manager',
|
||||
# Utility modules
|
||||
'utils',
|
||||
'utils.file_deduplication'
|
||||
'utils.file_deduplication',
|
||||
'utils.security_utils'
|
||||
],
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
import os
|
||||
block_cipher = None
|
||||
SPEC_DIR = os.path.dirname(os.path.abspath(SPEC))
|
||||
SRC_DIR = os.path.normpath(os.path.join(SPEC_DIR, '..', '..', 'src'))
|
||||
a = Analysis(
|
||||
['../../src/main.py'],
|
||||
pathex=['../..'],
|
||||
[os.path.join(SRC_DIR, 'main.py')],
|
||||
pathex=[SRC_DIR],
|
||||
binaries=[],
|
||||
datas=[
|
||||
('../../src/gui', 'gui'),
|
||||
(os.path.join(SRC_DIR, 'gui'), 'gui'),
|
||||
],
|
||||
hiddenimports=[
|
||||
# GUI modules
|
||||
@@ -34,7 +37,8 @@ a = Analysis(
|
||||
'managers.transfer_manager',
|
||||
# Utility modules
|
||||
'utils',
|
||||
'utils.file_deduplication'
|
||||
'utils.file_deduplication',
|
||||
'utils.security_utils'
|
||||
],
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
@@ -66,5 +70,5 @@ exe = EXE(
|
||||
target_arch=None,
|
||||
codesign_identity=None,
|
||||
entitlements_file=None,
|
||||
icon='../../icon_media/robot_files_256.ico',
|
||||
icon=os.path.join(SPEC_DIR, '..', '..', 'icon_media', 'robot_files_256.ico'),
|
||||
)
|
||||
@@ -0,0 +1,298 @@
|
||||
# run_vagrant_tests.ps1 — Host-side orchestrator for Vagrant Windows 11 test cycle.
|
||||
#
|
||||
# Workflow:
|
||||
# 1. Build the installer locally (PyInstaller + Inno Setup)
|
||||
# 2. Ensure the Vagrant VM is running and has a "clean" snapshot
|
||||
# 3. Restore to the clean snapshot
|
||||
# 4. Push the installer to the VM
|
||||
# 5. Run the test script inside the VM via WinRM
|
||||
# 6. Retrieve screenshots and results to the host
|
||||
# 7. Print results summary to CLI
|
||||
# 8. Revert VM back to clean snapshot
|
||||
#
|
||||
# Usage (from repo root):
|
||||
# .\scripts\windows\run_vagrant_tests.ps1
|
||||
# .\scripts\windows\run_vagrant_tests.ps1 -SkipBuild
|
||||
# .\scripts\windows\run_vagrant_tests.ps1 -SkipRevert
|
||||
#
|
||||
# Prerequisites:
|
||||
# - Vagrant and Hyper-V enabled on the host
|
||||
# - Inno Setup 6 installed (default path or in PATH)
|
||||
# - Poetry environment set up
|
||||
# - The VM created and "clean" snapshot saved:
|
||||
# cd vagrant && vagrant up --provider=hyperv && vagrant snapshot save clean
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[switch]$SkipBuild,
|
||||
[switch]$SkipRevert,
|
||||
[string]$SnapshotName = "clean",
|
||||
[string]$LocalResultsDir = ""
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..\..")).Path
|
||||
$VagrantDir = Join-Path $RepoRoot "vagrant"
|
||||
$Timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
|
||||
|
||||
if (-not $LocalResultsDir) {
|
||||
$LocalResultsDir = Join-Path $RepoRoot "vagrant_test_results\$Timestamp"
|
||||
}
|
||||
|
||||
function Write-Step {
|
||||
param([string]$Message)
|
||||
Write-Host "`n>>> $Message" -ForegroundColor Cyan
|
||||
}
|
||||
|
||||
# ── Ensure we're in the vagrant directory ────────────────────────────────────
|
||||
|
||||
Push-Location $VagrantDir
|
||||
try {
|
||||
|
||||
# ── Step 1: Build the installer locally ──────────────────────────────────
|
||||
|
||||
$installerPath = Join-Path $RepoRoot "dist\android-file-handler-setup.exe"
|
||||
|
||||
if ($SkipBuild) {
|
||||
Write-Step "Skipping build (-SkipBuild specified)"
|
||||
if (-not (Test-Path $installerPath)) {
|
||||
Write-Host "ERROR: -SkipBuild was specified but installer not found at $installerPath" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
else {
|
||||
Write-Step "Building installer locally (PyInstaller + Inno Setup)"
|
||||
|
||||
# PyInstaller — compile the exe
|
||||
Push-Location $RepoRoot
|
||||
try {
|
||||
Write-Host " Running PyInstaller..." -ForegroundColor Yellow
|
||||
poetry run pyinstaller scripts\spec_scripts\android-file-handler-windows.spec --noconfirm
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host "ERROR: PyInstaller failed with exit code $LASTEXITCODE" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
$pyinstallerExe = Join-Path $RepoRoot "dist\android-file-handler-windows.exe"
|
||||
if (-not (Test-Path $pyinstallerExe)) {
|
||||
Write-Host "ERROR: PyInstaller output not found at $pyinstallerExe" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
Write-Host " PyInstaller output: $pyinstallerExe" -ForegroundColor Green
|
||||
|
||||
# Inno Setup — compile the installer
|
||||
Write-Host " Running Inno Setup..." -ForegroundColor Yellow
|
||||
$isccPaths = @(
|
||||
"C:\Program Files (x86)\Inno Setup 6\ISCC.exe",
|
||||
"C:\Program Files\Inno Setup 6\ISCC.exe"
|
||||
)
|
||||
$iscc = $isccPaths | Where-Object { Test-Path $_ } | Select-Object -First 1
|
||||
if (-not $iscc) {
|
||||
$iscc = Get-Command ISCC.exe -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Source
|
||||
}
|
||||
if (-not $iscc) {
|
||||
Write-Host "ERROR: Inno Setup (ISCC.exe) not found. Install Inno Setup 6 or add it to PATH." -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
& $iscc "scripts\windows\android-file-handler-setup.iss"
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host "ERROR: Inno Setup failed with exit code $LASTEXITCODE" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
if (-not (Test-Path $installerPath)) {
|
||||
Write-Host "ERROR: Installer not found at $installerPath after Inno Setup" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
Write-Host " Installer built: $installerPath" -ForegroundColor Green
|
||||
}
|
||||
finally {
|
||||
Pop-Location
|
||||
}
|
||||
}
|
||||
|
||||
# ── Step 2: Verify VM and snapshot exist ─────────────────────────────────
|
||||
|
||||
Write-Step "Checking Vagrant VM status"
|
||||
$status = vagrant status --machine-readable 2>&1 |
|
||||
Select-String "state,running" -Quiet
|
||||
|
||||
if (-not $status) {
|
||||
Write-Host "VM is not running. Starting it..." -ForegroundColor Yellow
|
||||
vagrant up --provider=hyperv
|
||||
}
|
||||
|
||||
Write-Step "Verifying '$SnapshotName' snapshot exists"
|
||||
$snapshots = vagrant snapshot list 2>&1
|
||||
if ($snapshots -notmatch [regex]::Escape($SnapshotName)) {
|
||||
Write-Host "Snapshot '$SnapshotName' not found. Creating it now..." -ForegroundColor Yellow
|
||||
vagrant snapshot save $SnapshotName
|
||||
}
|
||||
|
||||
# ── Step 3: Restore to clean snapshot ────────────────────────────────────
|
||||
|
||||
Write-Step "Restoring VM to '$SnapshotName' snapshot"
|
||||
vagrant snapshot restore $SnapshotName
|
||||
|
||||
# ── Step 4: Push installer to the VM ─────────────────────────────────────
|
||||
|
||||
Write-Step "Pushing installer to the VM"
|
||||
|
||||
$remoteInstallerDir = "C:\vagrant_installer"
|
||||
$remoteInstallerPath = "$remoteInstallerDir\android-file-handler-setup.exe"
|
||||
|
||||
vagrant winrm --shell powershell --command `
|
||||
"New-Item -ItemType Directory -Path '$remoteInstallerDir' -Force | Out-Null"
|
||||
|
||||
# Base64-encode the installer, transfer via WinRM, decode on the VM
|
||||
$installerBytes = [IO.File]::ReadAllBytes($installerPath)
|
||||
$installerSizeMB = [math]::Round($installerBytes.Length / 1MB, 1)
|
||||
Write-Host " Installer size: ${installerSizeMB} MB — transferring via WinRM..." -ForegroundColor Yellow
|
||||
|
||||
# Split into chunks to avoid WinRM message size limits (< 2MB per command)
|
||||
$chunkSize = 1 * 1024 * 1024 # 1 MB chunks
|
||||
$totalChunks = [math]::Ceiling($installerBytes.Length / $chunkSize)
|
||||
|
||||
vagrant winrm --shell powershell --command `
|
||||
"if (Test-Path '$remoteInstallerPath') { Remove-Item '$remoteInstallerPath' -Force }"
|
||||
|
||||
for ($idx = 0; $idx -lt $totalChunks; $idx++) {
|
||||
$offset = $idx * $chunkSize
|
||||
$length = [math]::Min($chunkSize, $installerBytes.Length - $offset)
|
||||
$chunk = New-Object byte[] $length
|
||||
[Array]::Copy($installerBytes, $offset, $chunk, 0, $length)
|
||||
$b64Chunk = [Convert]::ToBase64String($chunk)
|
||||
|
||||
$appendScript = if ($idx -eq 0) {
|
||||
"[IO.File]::WriteAllBytes('$remoteInstallerPath', [Convert]::FromBase64String('$b64Chunk'))"
|
||||
}
|
||||
else {
|
||||
"`$bytes = [Convert]::FromBase64String('$b64Chunk'); " +
|
||||
"`$stream = [IO.File]::Open('$remoteInstallerPath', 'Append'); " +
|
||||
"`$stream.Write(`$bytes, 0, `$bytes.Length); `$stream.Close()"
|
||||
}
|
||||
vagrant winrm --shell powershell --command $appendScript
|
||||
Write-Host " Chunk $($idx + 1)/$totalChunks transferred" -ForegroundColor DarkGray
|
||||
}
|
||||
|
||||
# Verify transfer
|
||||
$remoteSize = vagrant winrm --shell powershell --command `
|
||||
"(Get-Item '$remoteInstallerPath').Length"
|
||||
Write-Host " Remote file size: $($remoteSize.Trim()) bytes (expected: $($installerBytes.Length))" -ForegroundColor DarkGray
|
||||
|
||||
# ── Step 5: Run tests inside the VM ──────────────────────────────────────
|
||||
|
||||
Write-Step "Running test suite inside the VM"
|
||||
|
||||
$testExitCode = 0
|
||||
try {
|
||||
vagrant winrm --shell powershell --command `
|
||||
"Set-ExecutionPolicy -ExecutionPolicy Bypass -Scope Process -Force; & C:\vagrant_project\vagrant\test_installer.ps1 -InstallerPath 'C:\vagrant_installer\android-file-handler-setup.exe'"
|
||||
}
|
||||
catch {
|
||||
$testExitCode = 1
|
||||
Write-Host "Test script returned failures." -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
# ── Step 6: Retrieve results and screenshots ─────────────────────────────
|
||||
|
||||
Write-Step "Retrieving test results and screenshots"
|
||||
|
||||
if (-not (Test-Path $LocalResultsDir)) {
|
||||
New-Item -ItemType Directory -Path $LocalResultsDir -Force | Out-Null
|
||||
}
|
||||
|
||||
# Copy results from shared folder on VM via vagrant scp or direct file copy
|
||||
# The test writes to C:\vagrant_test_results inside the VM.
|
||||
# We retrieve via WinRM by reading files.
|
||||
try {
|
||||
# Get list of result files
|
||||
$remoteFiles = vagrant winrm --shell powershell --command `
|
||||
"if (Test-Path 'C:\vagrant_test_results') { Get-ChildItem 'C:\vagrant_test_results' -Name }" 2>&1 |
|
||||
Where-Object { $_ -is [string] -and $_.Trim() -ne "" }
|
||||
|
||||
foreach ($fileName in $remoteFiles) {
|
||||
$trimmedName = $fileName.Trim()
|
||||
if (-not $trimmedName) { continue }
|
||||
|
||||
$remotePath = "C:\vagrant_test_results\$trimmedName"
|
||||
$localPath = Join-Path $LocalResultsDir $trimmedName
|
||||
|
||||
if ($trimmedName -match '\.csv$') {
|
||||
# Text file — read content and save
|
||||
$content = vagrant winrm --shell powershell --command `
|
||||
"Get-Content -Path '$remotePath' -Raw"
|
||||
$content | Out-File -FilePath $localPath -Encoding utf8
|
||||
Write-Host " Retrieved: $trimmedName" -ForegroundColor DarkGray
|
||||
}
|
||||
elseif ($trimmedName -match '\.(png|jpg|bmp)$') {
|
||||
# Binary file — base64 encode, transfer, decode
|
||||
$b64 = vagrant winrm --shell powershell --command `
|
||||
"[Convert]::ToBase64String([IO.File]::ReadAllBytes('$remotePath'))"
|
||||
[IO.File]::WriteAllBytes($localPath, [Convert]::FromBase64String($b64.Trim()))
|
||||
Write-Host " Retrieved: $trimmedName" -ForegroundColor DarkGray
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Host " Warning: Could not retrieve some result files: $_" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
# ── Step 7: Print local summary ──────────────────────────────────────────
|
||||
|
||||
Write-Step "Test Results"
|
||||
|
||||
$csvPath = Join-Path $LocalResultsDir "test_results.csv"
|
||||
if (Test-Path $csvPath) {
|
||||
$results = Import-Csv $csvPath
|
||||
$results | Format-Table -AutoSize
|
||||
|
||||
$passCount = ($results | Where-Object { $_.Status -eq "PASS" }).Count
|
||||
$failCount = ($results | Where-Object { $_.Status -eq "FAIL" }).Count
|
||||
Write-Host " Passed: $passCount | Failed: $failCount | Total: $($passCount + $failCount)" -ForegroundColor $(if ($failCount -gt 0) { "Red" } else { "Green" })
|
||||
}
|
||||
else {
|
||||
Write-Host " No results CSV found. Check VM output above." -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
Write-Host "`n Results directory: $LocalResultsDir" -ForegroundColor White
|
||||
|
||||
# List screenshots
|
||||
$screenshots = Get-ChildItem $LocalResultsDir -Filter "*.png" -ErrorAction SilentlyContinue
|
||||
if ($screenshots) {
|
||||
Write-Host " Screenshots:" -ForegroundColor White
|
||||
foreach ($shot in $screenshots) {
|
||||
Write-Host " - $($shot.FullName)" -ForegroundColor DarkGray
|
||||
}
|
||||
}
|
||||
|
||||
# ── Step 8: Revert to clean snapshot ─────────────────────────────────────
|
||||
|
||||
if (-not $SkipRevert) {
|
||||
Write-Step "Reverting VM to '$SnapshotName' snapshot"
|
||||
vagrant snapshot restore $SnapshotName
|
||||
Write-Host "VM reverted to clean state." -ForegroundColor Green
|
||||
}
|
||||
else {
|
||||
Write-Host "`n -SkipRevert specified, VM left in current state." -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
# ── Exit ─────────────────────────────────────────────────────────────────
|
||||
|
||||
Write-Host ""
|
||||
if ($testExitCode -ne 0) {
|
||||
Write-Host "One or more tests FAILED." -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
else {
|
||||
Write-Host "All tests PASSED." -ForegroundColor Green
|
||||
exit 0
|
||||
}
|
||||
|
||||
}
|
||||
finally {
|
||||
Pop-Location
|
||||
}
|
||||
Reference in New Issue
Block a user