404 lines
18 KiB
PowerShell
404 lines
18 KiB
PowerShell
<#
|
||
.SYNOPSIS
|
||
Baut, signiert und veröffentlicht eine neue VitosUpdater-Version auf Gitea.
|
||
|
||
.DESCRIPTION
|
||
1. Prüft Git-Zustand, cargo check, cargo clippy, cargo test und svelte-check
|
||
2. Erhöht die Version in tauri.conf.json, Cargo.toml und package.json
|
||
3. Baut den NSIS-Installer samt signiertem Updater-Artefakt (.sig)
|
||
4. Erzeugt latest.json
|
||
5. Committet die Version, setzt den Tag vX.Y.Z und pusht beides
|
||
6. Erstellt das Gitea-Release vX.Y.Z und lädt Installer, .sig und latest.json hoch
|
||
7. Aktualisiert das rollende Release "latest"
|
||
8. Prüft die öffentliche latest.json und die Installer-URL
|
||
|
||
Voraussetzungen: Umgebungsvariable GITEA_TOKEN (oder Datei .env.local mit GITEA_TOKEN=...),
|
||
Signierschlüssel unter %USERPROFILE%\.vitosupdater\updater.key (oder TAURI_SIGNING_PRIVATE_KEY[_PATH]).
|
||
|
||
.PARAMETER Bump
|
||
Art der Versionserhöhung: patch (Standard), minor oder major.
|
||
|
||
.PARAMETER Version
|
||
Explizite Version x.y.z (überschreibt -Bump).
|
||
|
||
.PARAMETER NoPublish
|
||
Nur bauen und latest.json erzeugen. Es wird nichts committet, getaggt, gepusht oder hochgeladen;
|
||
die Versionsänderung wird danach zurückgenommen. Die Ergebnisse liegen in release-out\.
|
||
|
||
.PARAMETER SkipChecks
|
||
Überspringt cargo check/clippy/test und svelte-check (nur für schnelle Wiederholungen).
|
||
#>
|
||
[CmdletBinding()]
|
||
param(
|
||
[ValidateSet('patch', 'minor', 'major')]
|
||
[string]$Bump = 'patch',
|
||
[string]$Version,
|
||
[switch]$NoPublish,
|
||
[switch]$SkipChecks
|
||
)
|
||
|
||
Set-StrictMode -Version Latest
|
||
$ErrorActionPreference = 'Stop'
|
||
[Console]::OutputEncoding = [Text.Encoding]::UTF8
|
||
$OutputEncoding = [Text.Encoding]::UTF8
|
||
try { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 } catch { }
|
||
|
||
$GiteaBase = 'https://gitea.davito.synology.me'
|
||
$Owner = 'Davito'
|
||
$Repo = 'VitosUpdater'
|
||
$Branch = 'main'
|
||
$Root = Split-Path -Parent $PSScriptRoot
|
||
$OutDir = Join-Path $Root 'release-out'
|
||
$Utf8NoBom = New-Object Text.UTF8Encoding($false)
|
||
$VersionFiles = @('src-tauri/tauri.conf.json', 'src-tauri/Cargo.toml', 'src-tauri/Cargo.lock', 'package.json', 'package-lock.json')
|
||
$script:Token = $null
|
||
$script:Committed = $false
|
||
$script:Bumped = $false
|
||
|
||
function Write-Step([string]$Text) { Write-Host ''; Write-Host "==> $Text" -ForegroundColor Cyan }
|
||
function Write-Ok([string]$Text) { Write-Host " [OK] $Text" -ForegroundColor Green }
|
||
function Write-Info([string]$Text) { Write-Host " $Text" -ForegroundColor Gray }
|
||
|
||
function Invoke-Native([string]$Description, [scriptblock]$Command) {
|
||
& $Command
|
||
if ($LASTEXITCODE -ne 0) { throw "$Description ist fehlgeschlagen (Exit-Code $LASTEXITCODE)." }
|
||
}
|
||
|
||
function Get-GitOutput([string[]]$GitArgs) {
|
||
$result = & git @GitArgs
|
||
if ($LASTEXITCODE -ne 0) { throw "git $($GitArgs -join ' ') ist fehlgeschlagen (Exit-Code $LASTEXITCODE)." }
|
||
return $result
|
||
}
|
||
|
||
function Read-Token {
|
||
if ($env:GITEA_TOKEN) { return $env:GITEA_TOKEN.Trim() }
|
||
$file = Join-Path $Root '.env.local'
|
||
if (Test-Path $file) {
|
||
foreach ($line in Get-Content $file -Encoding UTF8) {
|
||
if ($line -match '^\s*GITEA_TOKEN\s*=\s*(.+?)\s*$') { return $Matches[1].Trim('"', "'") }
|
||
}
|
||
}
|
||
return $null
|
||
}
|
||
|
||
function Show-TokenHelp {
|
||
Write-Host ''
|
||
Write-Host 'Es wurde kein Gitea-Zugriffstoken gefunden.' -ForegroundColor Yellow
|
||
Write-Host ' 1. In Gitea anmelden: Einstellungen -> Anwendungen -> Zugriffstoken'
|
||
Write-Host ' 2. Neues Token erzeugen, Berechtigung: Repository = Lesen und Schreiben (write:repository)'
|
||
Write-Host ' 3. Token hinterlegen, entweder'
|
||
Write-Host ' dauerhaft: setx GITEA_TOKEN "<token>" (danach neues Terminal öffnen)'
|
||
Write-Host " oder lokal: Datei .env.local im Projektordner mit der Zeile GITEA_TOKEN=<token>"
|
||
Write-Host ' (.env.local ist in .gitignore eingetragen und wird nie eingecheckt.)'
|
||
}
|
||
|
||
function Get-CurrentVersion {
|
||
$conf = Get-Content (Join-Path $Root 'src-tauri/tauri.conf.json') -Raw -Encoding UTF8 | ConvertFrom-Json
|
||
return [string]$conf.version
|
||
}
|
||
|
||
function Get-NextVersion([string]$Current) {
|
||
if ($Version) {
|
||
if ($Version -notmatch '^\d+\.\d+\.\d+$') { throw "Ungültige Version '$Version'. Erwartet wird x.y.z." }
|
||
return $Version
|
||
}
|
||
if ($Current -notmatch '^(\d+)\.(\d+)\.(\d+)$') { throw "Aktuelle Version '$Current' ist keine einfache SemVer-Version." }
|
||
$major = [int]$Matches[1]; $minor = [int]$Matches[2]; $patch = [int]$Matches[3]
|
||
switch ($Bump) {
|
||
'major' { return '{0}.0.0' -f ($major + 1) }
|
||
'minor' { return '{0}.{1}.0' -f $major, ($minor + 1) }
|
||
default { return '{0}.{1}.{2}' -f $major, $minor, ($patch + 1) }
|
||
}
|
||
}
|
||
|
||
function Set-FirstMatch([string]$Path, [string]$Pattern, [string]$Replacement) {
|
||
$full = Join-Path $Root $Path
|
||
$text = [IO.File]::ReadAllText($full, $Utf8NoBom)
|
||
$regex = New-Object Text.RegularExpressions.Regex($Pattern, [Text.RegularExpressions.RegexOptions]::Multiline)
|
||
if (-not $regex.IsMatch($text)) { throw "Versionszeile in $Path nicht gefunden." }
|
||
[IO.File]::WriteAllText($full, $regex.Replace($text, $Replacement, 1), $Utf8NoBom)
|
||
}
|
||
|
||
function Set-ProjectVersion([string]$New) {
|
||
Set-FirstMatch 'src-tauri/tauri.conf.json' '^( "version": ")[^"]+(")' ('${1}' + $New + '${2}')
|
||
Set-FirstMatch 'src-tauri/Cargo.toml' '^(version = ")[^"]+(")' ('${1}' + $New + '${2}')
|
||
Invoke-Native 'npm version' { npm version $New --no-git-tag-version --allow-same-version | Out-Null }
|
||
}
|
||
|
||
function Undo-VersionChange {
|
||
if ($script:Bumped -and -not $script:Committed) {
|
||
Write-Info 'Versionsänderung wird zurückgenommen ...'
|
||
& git checkout -- @VersionFiles 2>$null | Out-Null
|
||
$script:Bumped = $false
|
||
}
|
||
}
|
||
|
||
function Get-ReleaseNotes([string]$NewVersion) {
|
||
$lastTag = $null
|
||
$found = & git describe --tags --abbrev=0 --match 'v*' 2>$null
|
||
if ($LASTEXITCODE -eq 0 -and $found) { $lastTag = [string]$found }
|
||
$range = 'HEAD'
|
||
if ($lastTag) { $range = "$lastTag..HEAD" }
|
||
$lines = & git -c core.quotepath=off log $range --no-merges --pretty=format:'- %s (%h)' --grep='^Release v' --invert-grep
|
||
if ($LASTEXITCODE -ne 0) { throw 'git log ist fehlgeschlagen.' }
|
||
$text = ($lines | Where-Object { $_ }) -join "`n"
|
||
if (-not $text) { $text = '- Interne Verbesserungen und Fehlerbehebungen' }
|
||
return $text
|
||
}
|
||
|
||
# ---------- Gitea-API ----------
|
||
function Invoke-Gitea([string]$Method, [string]$Path, $Body = $null) {
|
||
$uri = "$GiteaBase/api/v1/repos/$Owner/$Repo$Path"
|
||
$headers = @{ Authorization = "token $script:Token"; Accept = 'application/json' }
|
||
if ($null -ne $Body) {
|
||
$bytes = [Text.Encoding]::UTF8.GetBytes(($Body | ConvertTo-Json -Depth 6))
|
||
return Invoke-RestMethod -Method $Method -Uri $uri -Headers $headers -ContentType 'application/json; charset=utf-8' -Body $bytes
|
||
}
|
||
return Invoke-RestMethod -Method $Method -Uri $uri -Headers $headers
|
||
}
|
||
|
||
function Get-ReleaseByTag([string]$Tag) {
|
||
try {
|
||
return Invoke-Gitea 'GET' "/releases/tags/$Tag"
|
||
} catch {
|
||
$status = $null
|
||
if ($_.Exception.PSObject.Properties.Name -contains 'Response' -and $_.Exception.Response) { $status = [int]$_.Exception.Response.StatusCode }
|
||
if ($status -eq 404) { return $null }
|
||
throw
|
||
}
|
||
}
|
||
|
||
function Send-Asset([int]$ReleaseId, [string]$File, [string]$Name) {
|
||
$url = "$GiteaBase/api/v1/repos/$Owner/$Repo/releases/$ReleaseId/assets?name=" + [uri]::EscapeDataString($Name)
|
||
# Das Token wird über stdin an curl übergeben, damit es nicht in der Prozessliste erscheint.
|
||
$config = 'header = "Authorization: token ' + $script:Token + '"'
|
||
$response = $config | & curl.exe --silent --show-error --fail --config - -X POST -F "attachment=@$File" $url
|
||
if ($LASTEXITCODE -ne 0) { throw "Upload von '$Name' ist fehlgeschlagen (curl-Exit-Code $LASTEXITCODE)." }
|
||
Write-Ok "Hochgeladen: $Name ($([math]::Round((Get-Item $File).Length / 1MB, 1)) MB)"
|
||
}
|
||
|
||
function Clear-ReleaseAssets([int]$ReleaseId) {
|
||
$release = Invoke-Gitea 'GET' "/releases/$ReleaseId"
|
||
foreach ($asset in @($release.assets)) {
|
||
Invoke-Gitea 'DELETE' "/releases/$ReleaseId/assets/$($asset.id)" | Out-Null
|
||
Write-Info "Altes Asset entfernt: $($asset.name)"
|
||
}
|
||
}
|
||
|
||
function Get-HttpStatus([string]$Url) {
|
||
$code = & curl.exe --silent --location --head --output NUL --write-out '%{http_code}' $Url
|
||
if ($code -eq '405' -or $code -eq '000') {
|
||
$code = & curl.exe --silent --location --range 0-0 --output NUL --write-out '%{http_code}' $Url
|
||
}
|
||
return [string]$code
|
||
}
|
||
|
||
# ---------- Ablauf ----------
|
||
Set-Location $Root
|
||
$started = Get-Date
|
||
try {
|
||
Write-Host 'VitosUpdater – Release-Skript' -ForegroundColor White
|
||
|
||
# --- 1. Voraussetzungen und Prüfungen ---
|
||
Write-Step '1/8 Voraussetzungen und Qualitätsprüfungen'
|
||
if (-not $NoPublish) {
|
||
$script:Token = Read-Token
|
||
if (-not $script:Token) { Show-TokenHelp; throw 'GITEA_TOKEN fehlt.' }
|
||
Write-Ok 'Gitea-Token gefunden (wird nicht angezeigt)'
|
||
}
|
||
|
||
$keyPath = Join-Path $env:USERPROFILE '.vitosupdater\updater.key'
|
||
if (-not $env:TAURI_SIGNING_PRIVATE_KEY -and -not $env:TAURI_SIGNING_PRIVATE_KEY_PATH) {
|
||
if (-not (Test-Path $keyPath)) {
|
||
throw "Signierschlüssel nicht gefunden: $keyPath. Erzeugen mit: cargo tauri signer generate -w `"$keyPath`" (öffentlichen Schlüssel danach in tauri.conf.json eintragen)."
|
||
}
|
||
$env:TAURI_SIGNING_PRIVATE_KEY_PATH = $keyPath
|
||
}
|
||
Write-Ok 'Signierschlüssel gefunden'
|
||
|
||
$dirty = & git status --porcelain
|
||
if ($LASTEXITCODE -ne 0) { throw 'git status ist fehlgeschlagen. Ist dies ein Git-Repository?' }
|
||
if ($dirty) {
|
||
Write-Host ($dirty -join "`n") -ForegroundColor Yellow
|
||
throw 'Das Arbeitsverzeichnis ist nicht sauber. Bitte zuerst committen oder verwerfen.'
|
||
}
|
||
$branch = (Get-GitOutput @('rev-parse', '--abbrev-ref', 'HEAD') | Select-Object -First 1).Trim()
|
||
if ($branch -ne $Branch) { throw "Es muss der Branch '$Branch' ausgecheckt sein (aktuell: '$branch')." }
|
||
if (-not $NoPublish) {
|
||
Invoke-Native 'git fetch' { git fetch origin --tags --force --quiet }
|
||
$remoteMain = & git rev-parse --verify --quiet "refs/remotes/origin/$Branch"
|
||
if ($LASTEXITCODE -eq 0 -and $remoteMain) {
|
||
$behind = [int](Get-GitOutput @('rev-list', '--count', "HEAD..origin/$Branch") | Select-Object -First 1)
|
||
if ($behind -gt 0) { throw "Der lokale Branch liegt $behind Commit(s) hinter origin/$Branch. Bitte zuerst git pull ausführen." }
|
||
}
|
||
}
|
||
Write-Ok 'Git-Zustand ist sauber'
|
||
|
||
if ($SkipChecks) {
|
||
Write-Info 'Prüfungen werden übersprungen (-SkipChecks).'
|
||
} else {
|
||
Push-Location (Join-Path $Root 'src-tauri')
|
||
try {
|
||
Invoke-Native 'cargo check' { cargo check --all-targets }
|
||
Write-Ok 'cargo check'
|
||
Invoke-Native 'cargo clippy' { cargo clippy --all-targets -- -D warnings }
|
||
Write-Ok 'cargo clippy (ohne Warnungen)'
|
||
Invoke-Native 'cargo test' { cargo test }
|
||
Write-Ok 'cargo test'
|
||
} finally { Pop-Location }
|
||
Invoke-Native 'svelte-check' { npm run check }
|
||
Write-Ok 'svelte-check'
|
||
}
|
||
|
||
# --- 2. Version erhöhen ---
|
||
Write-Step '2/8 Version erhöhen'
|
||
$current = Get-CurrentVersion
|
||
$new = Get-NextVersion $current
|
||
if (-not $NoPublish) {
|
||
$existingTag = & git tag --list "v$new"
|
||
if ($existingTag) { throw "Der Tag v$new existiert bereits. Bitte eine höhere Version wählen." }
|
||
if ([version]$new -le [version]$current) { throw "Die neue Version $new ist nicht größer als die aktuelle Version $current." }
|
||
}
|
||
$script:Bumped = $true
|
||
Set-ProjectVersion $new
|
||
Write-Ok "Version $current -> $new (tauri.conf.json, Cargo.toml, package.json)"
|
||
|
||
# --- 3. Bauen ---
|
||
Write-Step '3/8 Installer bauen und signieren (cargo tauri build)'
|
||
$env:CI = 'true'
|
||
Invoke-Native 'cargo tauri build' { cargo tauri build }
|
||
$nsisDir = Join-Path $Root 'src-tauri/target/release/bundle/nsis'
|
||
$installer = Get-ChildItem $nsisDir -Filter "*_${new}_*-setup.exe" | Select-Object -First 1
|
||
if (-not $installer) { throw "Installer für Version $new wurde nicht gefunden in $nsisDir." }
|
||
$sigFile = "$($installer.FullName).sig"
|
||
if (-not (Test-Path $sigFile)) { throw "Signaturdatei fehlt: $sigFile. Wurde createUpdaterArtifacts aktiviert und der Schlüssel gefunden?" }
|
||
Write-Ok "Installer: $($installer.Name) ($([math]::Round($installer.Length / 1MB, 1)) MB)"
|
||
Write-Ok 'Signatur (.sig) erzeugt'
|
||
|
||
# --- 4. latest.json ---
|
||
Write-Step '4/8 latest.json erzeugen'
|
||
$notes = Get-ReleaseNotes $new
|
||
$assetUrl = "$GiteaBase/$Owner/$Repo/releases/download/v$new/$($installer.Name)"
|
||
$manifest = [ordered]@{
|
||
version = $new
|
||
notes = $notes
|
||
pub_date = (Get-Date).ToUniversalTime().ToString("yyyy-MM-dd'T'HH:mm:ss'Z'")
|
||
platforms = [ordered]@{
|
||
'windows-x86_64' = [ordered]@{
|
||
signature = (Get-Content $sigFile -Raw -Encoding UTF8).Trim()
|
||
url = $assetUrl
|
||
size = [int64]$installer.Length
|
||
}
|
||
}
|
||
}
|
||
if (Test-Path $OutDir) { Remove-Item $OutDir -Recurse -Force }
|
||
New-Item -ItemType Directory -Path $OutDir | Out-Null
|
||
$latestFile = Join-Path $OutDir 'latest.json'
|
||
[IO.File]::WriteAllText($latestFile, ($manifest | ConvertTo-Json -Depth 6), $Utf8NoBom)
|
||
Copy-Item $installer.FullName $OutDir
|
||
Copy-Item $sigFile $OutDir
|
||
$outInstaller = Join-Path $OutDir $installer.Name
|
||
$outSig = Join-Path $OutDir "$($installer.Name).sig"
|
||
Write-Ok "latest.json -> $latestFile"
|
||
|
||
if ($NoPublish) {
|
||
Undo-VersionChange
|
||
Write-Step 'Fertig (-NoPublish)'
|
||
Write-Ok "Version $new gebaut. Dateien liegen in: $OutDir"
|
||
return
|
||
}
|
||
|
||
# --- 5. Commit, Tag, Push ---
|
||
Write-Step "5/8 Commit, Tag v$new und Push"
|
||
Invoke-Native 'git add' { git add -- @VersionFiles }
|
||
Invoke-Native 'git commit' { git commit --quiet -m "Release v$new" }
|
||
$script:Committed = $true
|
||
Invoke-Native 'git tag' { git tag -a "v$new" -m "Release v$new" }
|
||
Invoke-Native 'git push (Branch)' { git push origin $Branch }
|
||
Invoke-Native 'git push (Tag)' { git push origin "v$new" }
|
||
Write-Ok "Commit und Tag v$new gepusht"
|
||
|
||
# --- 6. Gitea-Release ---
|
||
Write-Step "6/8 Gitea-Release v$new erstellen"
|
||
$release = Get-ReleaseByTag "v$new"
|
||
if ($release) {
|
||
Write-Info 'Release existiert bereits, Assets werden ersetzt.'
|
||
Clear-ReleaseAssets $release.id
|
||
} else {
|
||
$release = Invoke-Gitea 'POST' '/releases' @{
|
||
tag_name = "v$new"
|
||
target_commitish = $Branch
|
||
name = "VitosUpdater $new"
|
||
body = $notes
|
||
draft = $false
|
||
prerelease = $false
|
||
}
|
||
}
|
||
Write-Ok "Release angelegt: $($release.html_url)"
|
||
Send-Asset $release.id $outInstaller $installer.Name
|
||
Send-Asset $release.id $outSig "$($installer.Name).sig"
|
||
Send-Asset $release.id $latestFile 'latest.json'
|
||
|
||
# --- 7. Rollendes Release "latest" ---
|
||
Write-Step '7/8 Rollendes Release "latest" aktualisieren'
|
||
Invoke-Native 'git tag latest' { git tag -f latest HEAD | Out-Null }
|
||
Invoke-Native 'git push latest' { git push --force origin refs/tags/latest }
|
||
$rolling = Get-ReleaseByTag 'latest'
|
||
$rollingBody = "Aktuelle Version: **$new**`n`n$notes"
|
||
if ($rolling) {
|
||
Invoke-Gitea 'PATCH' "/releases/$($rolling.id)" @{ name = "Aktuellste Version ($new)"; body = $rollingBody } | Out-Null
|
||
Clear-ReleaseAssets $rolling.id
|
||
} else {
|
||
$rolling = Invoke-Gitea 'POST' '/releases' @{
|
||
tag_name = 'latest'
|
||
target_commitish = $Branch
|
||
name = "Aktuellste Version ($new)"
|
||
body = $rollingBody
|
||
draft = $false
|
||
prerelease = $false
|
||
}
|
||
}
|
||
Send-Asset $rolling.id $outInstaller $installer.Name
|
||
Send-Asset $rolling.id $outSig "$($installer.Name).sig"
|
||
Send-Asset $rolling.id $latestFile 'latest.json'
|
||
Write-Ok 'Rollendes Release aktualisiert'
|
||
|
||
# --- 8. Prüfung ---
|
||
Write-Step '8/8 Veröffentlichung prüfen'
|
||
$latestUrl = "$GiteaBase/$Owner/$Repo/releases/download/latest/latest.json"
|
||
$raw = Invoke-WebRequest -Uri $latestUrl -UseBasicParsing
|
||
$content = $raw.Content
|
||
if ($content -is [byte[]]) { $content = [Text.Encoding]::UTF8.GetString($content) }
|
||
$remote = $content | ConvertFrom-Json
|
||
if ($remote.version -ne $new) { throw "Öffentliche latest.json enthält Version $($remote.version), erwartet wurde $new." }
|
||
Write-Ok "latest.json öffentlich erreichbar, Version $($remote.version)"
|
||
$platform = $remote.platforms.'windows-x86_64'
|
||
$status = Get-HttpStatus $platform.url
|
||
if ($status -ne '200' -and $status -ne '206') { throw "Installer-URL antwortet mit HTTP ${status}: $($platform.url)" }
|
||
Write-Ok "Installer-URL antwortet mit HTTP $status"
|
||
$rollingStatus = Get-HttpStatus "$GiteaBase/$Owner/$Repo/releases/download/latest/$($installer.Name)"
|
||
if ($rollingStatus -ne '200' -and $rollingStatus -ne '206') { throw "Installer im Release 'latest' antwortet mit HTTP $rollingStatus." }
|
||
Write-Ok "Installer im Release 'latest' antwortet mit HTTP $rollingStatus"
|
||
|
||
$elapsed = [math]::Round(((Get-Date) - $started).TotalMinutes, 1)
|
||
Write-Host ''
|
||
Write-Host '==================== Zusammenfassung ====================' -ForegroundColor Green
|
||
Write-Host " Version: $new (vorher $current)"
|
||
Write-Host " Release: $($release.html_url)"
|
||
Write-Host " Rollendes Release: $GiteaBase/$Owner/$Repo/releases/tag/latest"
|
||
Write-Host " Update-Endpunkt: $latestUrl"
|
||
Write-Host " Installer: $($platform.url)"
|
||
Write-Host " Dauer: $elapsed Minuten"
|
||
Write-Host '==========================================================' -ForegroundColor Green
|
||
}
|
||
catch {
|
||
Write-Host ''
|
||
Write-Host "FEHLER: $($_.Exception.Message)" -ForegroundColor Red
|
||
if ($script:Committed) {
|
||
Write-Host 'Der Versions-Commit wurde bereits lokal erstellt. Nach der Fehlerbehebung zurücknehmen mit: git tag -d v<Version>; git reset --hard HEAD~1 (bereits gepushte Teile ggf. auf Gitea prüfen), danach das Skript erneut starten.' -ForegroundColor Yellow
|
||
} else {
|
||
Undo-VersionChange
|
||
}
|
||
exit 1
|
||
}
|