feat: engineer SBAS timeseries production workflow
This commit is contained in:
@@ -0,0 +1,417 @@
|
||||
# Clone bootstrap helper for fresh Windows deployments.
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[switch]$InitWindowsConda,
|
||||
[switch]$InitFrontend,
|
||||
[switch]$BuildFrontend,
|
||||
[switch]$InitWslConda,
|
||||
[switch]$All,
|
||||
[switch]$SkipChecks
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||
$ProjectRoot = Split-Path -Parent $ScriptDir
|
||||
Set-Location -LiteralPath $ProjectRoot
|
||||
|
||||
if ($All) {
|
||||
$InitWindowsConda = $true
|
||||
$InitFrontend = $true
|
||||
$BuildFrontend = $true
|
||||
$InitWslConda = $true
|
||||
}
|
||||
|
||||
$EnvPath = Join-Path $ProjectRoot ".env"
|
||||
$EnvExamplePath = Join-Path $ProjectRoot ".env.example"
|
||||
$WindowsCondaSpec = Join-Path $ProjectRoot "environment.yml"
|
||||
$WslCondaSpec = Join-Path $ProjectRoot "deploy\wsl\conda\insar_wsl_v1.environment.yml"
|
||||
$FrontendDir = Join-Path $ProjectRoot "frontend"
|
||||
$CheckRuntimeScript = Join-Path $ProjectRoot "scripts\check_runtime_config.py"
|
||||
|
||||
function Write-Section {
|
||||
param([string]$Title)
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "==> $Title" -ForegroundColor Cyan
|
||||
}
|
||||
|
||||
function Write-Info {
|
||||
param([string]$Message)
|
||||
|
||||
Write-Host "[INFO] $Message" -ForegroundColor DarkCyan
|
||||
}
|
||||
|
||||
function Write-WarnMessage {
|
||||
param([string]$Message)
|
||||
|
||||
Write-Host "[WARN] $Message" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
function Get-FirstNonEmptyValue {
|
||||
param(
|
||||
[object[]]$Values,
|
||||
[string]$Fallback = ""
|
||||
)
|
||||
|
||||
foreach ($value in $Values) {
|
||||
$trimmed = "$value".Trim().Trim('"').Trim("'")
|
||||
if ($trimmed) {
|
||||
return $trimmed
|
||||
}
|
||||
}
|
||||
|
||||
return $Fallback
|
||||
}
|
||||
|
||||
function Read-EnvFile {
|
||||
param([string]$Path)
|
||||
|
||||
$map = @{}
|
||||
if (-not (Test-Path -LiteralPath $Path)) {
|
||||
return $map
|
||||
}
|
||||
|
||||
foreach ($line in Get-Content -LiteralPath $Path) {
|
||||
$trimmed = $line.Trim()
|
||||
if (-not $trimmed -or $trimmed.StartsWith("#") -or -not $trimmed.Contains("=")) {
|
||||
continue
|
||||
}
|
||||
|
||||
$parts = $trimmed.Split("=", 2)
|
||||
$key = $parts[0].Trim()
|
||||
$value = $parts[1].Trim().Trim('"').Trim("'")
|
||||
$map[$key] = $value
|
||||
}
|
||||
|
||||
return $map
|
||||
}
|
||||
|
||||
function Resolve-CommandPath {
|
||||
param([string[]]$Candidates)
|
||||
|
||||
foreach ($candidate in $Candidates) {
|
||||
$trimmed = "$candidate".Trim().Trim('"').Trim("'")
|
||||
if (-not $trimmed) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (Test-Path -LiteralPath $trimmed) {
|
||||
$resolved = Resolve-Path -LiteralPath $trimmed -ErrorAction SilentlyContinue
|
||||
if ($resolved) {
|
||||
return $resolved.Path
|
||||
}
|
||||
return $trimmed
|
||||
}
|
||||
|
||||
$command = Get-Command -Name $trimmed -ErrorAction SilentlyContinue | Select-Object -First 1
|
||||
if ($command) {
|
||||
if ($command.Source) {
|
||||
return $command.Source
|
||||
}
|
||||
if ($command.Path) {
|
||||
return $command.Path
|
||||
}
|
||||
return $trimmed
|
||||
}
|
||||
}
|
||||
|
||||
return $null
|
||||
}
|
||||
|
||||
function Get-CondaEnvNameFromSpec {
|
||||
param(
|
||||
[string]$SpecPath,
|
||||
[string]$FallbackName
|
||||
)
|
||||
|
||||
if (-not (Test-Path -LiteralPath $SpecPath)) {
|
||||
return $FallbackName
|
||||
}
|
||||
|
||||
$match = Select-String -Path $SpecPath -Pattern '^\s*name:\s*([^\s#]+)\s*$' | Select-Object -First 1
|
||||
if ($match -and $match.Matches.Count -gt 0) {
|
||||
return $match.Matches[0].Groups[1].Value.Trim()
|
||||
}
|
||||
|
||||
return $FallbackName
|
||||
}
|
||||
|
||||
function Get-WindowsCondaEnvExists {
|
||||
param(
|
||||
[string]$CondaExe,
|
||||
[string]$EnvName
|
||||
)
|
||||
|
||||
try {
|
||||
$envsJson = & "$CondaExe" env list --json 2>$null
|
||||
if ($LASTEXITCODE -ne 0 -or -not $envsJson) {
|
||||
return $false
|
||||
}
|
||||
|
||||
$envsInfo = $envsJson | ConvertFrom-Json
|
||||
foreach ($envPath in ($envsInfo.envs | Where-Object { $_ })) {
|
||||
if ((Split-Path -Leaf "$envPath") -eq $EnvName) {
|
||||
return $true
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
return $false
|
||||
}
|
||||
|
||||
return $false
|
||||
}
|
||||
|
||||
function Resolve-CondaEnvPythonPath {
|
||||
param(
|
||||
[string]$CondaExe,
|
||||
[string]$EnvName
|
||||
)
|
||||
|
||||
if (-not $CondaExe -or -not $EnvName) {
|
||||
return $null
|
||||
}
|
||||
|
||||
try {
|
||||
$envsJson = & "$CondaExe" env list --json 2>$null
|
||||
if ($LASTEXITCODE -ne 0 -or -not $envsJson) {
|
||||
return $null
|
||||
}
|
||||
|
||||
$envsInfo = $envsJson | ConvertFrom-Json
|
||||
foreach ($envPath in ($envsInfo.envs | Where-Object { $_ })) {
|
||||
if ((Split-Path -Leaf "$envPath") -ne $EnvName) {
|
||||
continue
|
||||
}
|
||||
|
||||
$pythonPath = Join-Path -Path "$envPath" -ChildPath "python.exe"
|
||||
if (Test-Path -LiteralPath $pythonPath) {
|
||||
$resolved = Resolve-Path -LiteralPath $pythonPath -ErrorAction SilentlyContinue
|
||||
if ($resolved) {
|
||||
return $resolved.Path
|
||||
}
|
||||
return $pythonPath
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
return $null
|
||||
}
|
||||
|
||||
return $null
|
||||
}
|
||||
|
||||
function Convert-WindowsPathToWsl {
|
||||
param([string]$WindowsPath)
|
||||
|
||||
$resolved = Resolve-Path -LiteralPath $WindowsPath -ErrorAction SilentlyContinue
|
||||
$path = if ($resolved) { $resolved.Path } else { [System.IO.Path]::GetFullPath($WindowsPath) }
|
||||
|
||||
if ($path -notmatch '^[A-Za-z]:\\') {
|
||||
throw "Path cannot be converted to a WSL mount path: $path"
|
||||
}
|
||||
|
||||
$drive = $path.Substring(0, 1).ToLowerInvariant()
|
||||
$rest = $path.Substring(2).Replace('\', '/')
|
||||
return "/mnt/$drive$rest"
|
||||
}
|
||||
|
||||
function Get-WslBootstrapPrefix {
|
||||
return @(
|
||||
"if [ -f ~/.bashrc ]; then source ~/.bashrc >/dev/null 2>&1; fi",
|
||||
"if ! command -v conda >/dev/null 2>&1 && [ -f ~/miniconda3/etc/profile.d/conda.sh ]; then . ~/miniconda3/etc/profile.d/conda.sh; fi",
|
||||
"if ! command -v conda >/dev/null 2>&1 && [ -f ~/anaconda3/etc/profile.d/conda.sh ]; then . ~/anaconda3/etc/profile.d/conda.sh; fi",
|
||||
"command -v conda >/dev/null 2>&1 || { echo 'conda not found inside WSL'; exit 127; }"
|
||||
) -join "; "
|
||||
}
|
||||
|
||||
function Invoke-WslCommand {
|
||||
param(
|
||||
[string]$Distro,
|
||||
[string]$Command,
|
||||
[switch]$AllowFailure
|
||||
)
|
||||
|
||||
& "$WslExe" -d "$Distro" bash -lc "$Command"
|
||||
$exitCode = $LASTEXITCODE
|
||||
if (-not $AllowFailure -and $exitCode -ne 0) {
|
||||
throw "WSL command failed for distro '$Distro' with exit code $exitCode."
|
||||
}
|
||||
return $exitCode
|
||||
}
|
||||
|
||||
Write-Section "Clone bootstrap"
|
||||
Write-Info "Project root: $ProjectRoot"
|
||||
|
||||
if (-not (Test-Path -LiteralPath $EnvPath)) {
|
||||
if (-not (Test-Path -LiteralPath $EnvExamplePath)) {
|
||||
throw "Missing .env.example at $EnvExamplePath"
|
||||
}
|
||||
|
||||
Copy-Item -LiteralPath $EnvExamplePath -Destination $EnvPath
|
||||
Write-Info "Created .env from .env.example. Review it before starting the system."
|
||||
} else {
|
||||
Write-Info ".env already exists. Keeping the current file."
|
||||
}
|
||||
|
||||
$EnvMap = Read-EnvFile -Path $EnvPath
|
||||
$CondaExe = Resolve-CommandPath -Candidates @($EnvMap["CONDA_EXE"], "conda", "conda.exe")
|
||||
$NpmExe = Resolve-CommandPath -Candidates @("npm.cmd", "npm")
|
||||
$PythonExe = Resolve-CommandPath -Candidates @($EnvMap["PYTHON_PATH"], "python", "py")
|
||||
$WslExe = Resolve-CommandPath -Candidates @("wsl.exe", "wsl")
|
||||
|
||||
$WindowsCondaEnvName = Get-FirstNonEmptyValue -Values @(
|
||||
$EnvMap["CONDA_ENV_NAME"],
|
||||
(Get-CondaEnvNameFromSpec -SpecPath $WindowsCondaSpec -FallbackName "InSAR")
|
||||
)
|
||||
|
||||
$WslDistro = Get-FirstNonEmptyValue -Values @(
|
||||
$EnvMap["WSL_DISTRO"],
|
||||
$EnvMap["TIMESERIES_WSL_DISTRO"],
|
||||
$EnvMap["ISCE2_WSL_DISTRO"],
|
||||
$EnvMap["PYINT_WSL_DISTRO"]
|
||||
) -Fallback "Ubuntu-24.04"
|
||||
|
||||
$WslCondaEnvName = Get-FirstNonEmptyValue -Values @(
|
||||
$EnvMap["WSL_SHARED_CONDA_ENV"],
|
||||
$EnvMap["TIMESERIES_ENV_NAME"],
|
||||
(Get-CondaEnvNameFromSpec -SpecPath $WslCondaSpec -FallbackName "insar_wsl_v1")
|
||||
) -Fallback "insar_wsl_v1"
|
||||
|
||||
if (-not $InitWindowsConda -and -not $InitFrontend -and -not $BuildFrontend -and -not $InitWslConda) {
|
||||
Write-Info "No optional install switches were requested. Use -All or individual switches to bootstrap runtimes."
|
||||
}
|
||||
|
||||
if ($InitWindowsConda) {
|
||||
Write-Section "Windows Conda runtime"
|
||||
|
||||
if (-not $CondaExe) {
|
||||
throw "Conda was not found. Set CONDA_EXE in .env or add conda to PATH."
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $WindowsCondaSpec)) {
|
||||
throw "Missing Windows conda spec: $WindowsCondaSpec"
|
||||
}
|
||||
|
||||
if (Get-WindowsCondaEnvExists -CondaExe $CondaExe -EnvName $WindowsCondaEnvName) {
|
||||
Write-Info "Updating Windows conda env '$WindowsCondaEnvName' from environment.yml"
|
||||
& "$CondaExe" env update --name "$WindowsCondaEnvName" --file "$WindowsCondaSpec" --prune
|
||||
} else {
|
||||
Write-Info "Creating Windows conda env '$WindowsCondaEnvName' from environment.yml"
|
||||
& "$CondaExe" env create --name "$WindowsCondaEnvName" --file "$WindowsCondaSpec"
|
||||
}
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Windows conda bootstrap failed."
|
||||
}
|
||||
|
||||
$ResolvedWindowsPython = Resolve-CondaEnvPythonPath -CondaExe $CondaExe -EnvName $WindowsCondaEnvName
|
||||
if ($ResolvedWindowsPython) {
|
||||
$PythonExe = $ResolvedWindowsPython
|
||||
Write-Info "Suggested .env values:"
|
||||
Write-Host " CONDA_EXE=$CondaExe"
|
||||
Write-Host " CONDA_ENV_NAME=$WindowsCondaEnvName"
|
||||
Write-Host " PYTHON_PATH=$ResolvedWindowsPython"
|
||||
} else {
|
||||
Write-WarnMessage "The conda environment was created or updated, but python.exe could not be resolved automatically."
|
||||
}
|
||||
}
|
||||
|
||||
if ($InitFrontend) {
|
||||
Write-Section "Frontend dependencies"
|
||||
|
||||
if (-not $NpmExe) {
|
||||
throw "npm was not found. Install Node.js and make sure npm is in PATH."
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $FrontendDir)) {
|
||||
throw "Missing frontend directory: $FrontendDir"
|
||||
}
|
||||
|
||||
Push-Location $FrontendDir
|
||||
try {
|
||||
Write-Info "Running npm ci in frontend/"
|
||||
& "$NpmExe" ci
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "npm ci failed."
|
||||
}
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
}
|
||||
|
||||
if ($BuildFrontend) {
|
||||
Write-Section "Frontend build"
|
||||
|
||||
if (-not $NpmExe) {
|
||||
throw "npm was not found. Install Node.js and make sure npm is in PATH."
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $FrontendDir)) {
|
||||
throw "Missing frontend directory: $FrontendDir"
|
||||
}
|
||||
|
||||
Push-Location $FrontendDir
|
||||
try {
|
||||
Write-Info "Running npm run build in frontend/"
|
||||
& "$NpmExe" run build
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "npm run build failed."
|
||||
}
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
}
|
||||
|
||||
if ($InitWslConda) {
|
||||
Write-Section "WSL shared runtime"
|
||||
|
||||
if (-not $WslExe) {
|
||||
throw "wsl.exe was not found."
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $WslCondaSpec)) {
|
||||
throw "Missing WSL conda spec: $WslCondaSpec"
|
||||
}
|
||||
|
||||
$WslSpecPath = Convert-WindowsPathToWsl -WindowsPath $WslCondaSpec
|
||||
$Prefix = Get-WslBootstrapPrefix
|
||||
$ExistsCommand = "$Prefix; conda env list | grep -E ""^$WslCondaEnvName[[:space:]]"" >/dev/null"
|
||||
$ExistsExitCode = Invoke-WslCommand -Distro $WslDistro -Command $ExistsCommand -AllowFailure
|
||||
|
||||
if ($ExistsExitCode -eq 0) {
|
||||
Write-Info "Updating WSL conda env '$WslCondaEnvName' in distro '$WslDistro'"
|
||||
$BootstrapCommand = "$Prefix; conda env update -n ""$WslCondaEnvName"" -f ""$WslSpecPath"" --prune"
|
||||
} else {
|
||||
Write-Info "Creating WSL conda env '$WslCondaEnvName' in distro '$WslDistro'"
|
||||
$BootstrapCommand = "$Prefix; conda env create -n ""$WslCondaEnvName"" -f ""$WslSpecPath"""
|
||||
}
|
||||
|
||||
[void](Invoke-WslCommand -Distro $WslDistro -Command $BootstrapCommand)
|
||||
|
||||
Write-Info "Suggested .env values:"
|
||||
Write-Host " WSL_DISTRO=$WslDistro"
|
||||
Write-Host " WSL_SHARED_CONDA_ENV=$WslCondaEnvName"
|
||||
Write-Host " TIMESERIES_ENV_NAME=$WslCondaEnvName"
|
||||
Write-WarnMessage "Verify WSL_SHARED_PYTHON, ISCE2_PYTHON, TIMESERIES_PYTHON, and PYINT_WSL_PYTHON against the real conda install root inside WSL."
|
||||
}
|
||||
|
||||
if (-not $SkipChecks) {
|
||||
Write-Section "Deployment validation"
|
||||
|
||||
if (-not $PythonExe -and $CondaExe -and $WindowsCondaEnvName) {
|
||||
$PythonExe = Resolve-CondaEnvPythonPath -CondaExe $CondaExe -EnvName $WindowsCondaEnvName
|
||||
}
|
||||
|
||||
if (-not $PythonExe) {
|
||||
Write-WarnMessage "No Windows Python interpreter was resolved. Skipping scripts/check_runtime_config.py."
|
||||
} elseif (-not (Test-Path -LiteralPath $CheckRuntimeScript)) {
|
||||
Write-WarnMessage "Missing validation script: $CheckRuntimeScript"
|
||||
} else {
|
||||
Write-Info "Using Python for validation: $PythonExe"
|
||||
& "$PythonExe" "$CheckRuntimeScript"
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Deployment configuration validation failed. Fix .env and rerun the bootstrap."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Write-Section "Next steps"
|
||||
Write-Host "1. Review .env and align local paths, database settings, and WSL python paths."
|
||||
Write-Host "2. Run start_system.bat when scripts/check_runtime_config.py passes."
|
||||
Write-Host "3. Use GET /api/health after startup to confirm database, catalog, product_packages, and wsl_runtime are healthy."
|
||||
Reference in New Issue
Block a user