mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-05 19:28:25 +00:00
Merge pull request #4249 from vitorpamplona/claude/jolly-galileo-1jnyq4
Add Tor connection splash screen with escape hatch for desktop
This commit is contained in:
+5
-1
@@ -190,6 +190,10 @@ Where:
|
||||
| `<arch>` | `x64`, `arm64` |
|
||||
| `<ext>` | `dmg`, `msi`, `zip`, `deb`, `rpm`, `AppImage`, `flatpak`, `tar.gz` |
|
||||
|
||||
The AppImage is the exception: `amethyst-desktop-<version>-<x86_64|aarch64>.AppImage`.
|
||||
AppImageHub flags `linux` in an AppImage name (every AppImage is for Linux) and
|
||||
expects the AppImage arch names.
|
||||
|
||||
Single source of truth: [`scripts/asset-name.sh`](scripts/asset-name.sh).
|
||||
Package manager manifests (Homebrew cask, Winget) depend on this exact scheme —
|
||||
any change is a breaking contract.
|
||||
@@ -199,7 +203,7 @@ Examples:
|
||||
- `amethyst-desktop-1.12.1-macos-x64.dmg`
|
||||
- `amethyst-desktop-1.12.1-macos-arm64.dmg`
|
||||
- `amethyst-desktop-1.12.1-windows-x64.msi`
|
||||
- `amethyst-desktop-1.12.1-linux-x64.AppImage`
|
||||
- `amethyst-desktop-1.12.1-x86_64.AppImage`
|
||||
- `amethyst-desktop-1.12.1-linux-x64.flatpak`
|
||||
|
||||
---
|
||||
|
||||
@@ -1863,6 +1863,9 @@
|
||||
<string name="tor_connection_failed_body">Amethyst couldn't finish bootstrapping Tor. Use a regular (non-Tor) connection so the feed can load? We'll remember this choice for the next hour and re-try Tor after that.</string>
|
||||
<string name="tor_continue_without_for_session">Use regular connection</string>
|
||||
<string name="tor_keep_waiting">Keep waiting</string>
|
||||
<string name="tor_splash_connecting">Connecting to Tor…</string>
|
||||
<string name="tor_splash_error">Tor error: %1$s</string>
|
||||
<string name="tor_splash_explainer">Amethyst uses Tor to hide your IP address from relays. Starting Tor can take a minute and needs internet access.</string>
|
||||
<string name="notification_settings_categories_explainer">Tap a category to open Android notification settings for it — sound, importance, badges and Do Not Disturb live there.</string>
|
||||
<string name="notification_channel_status_on">On</string>
|
||||
<string name="notification_channel_status_silent">Silent</string>
|
||||
|
||||
@@ -95,6 +95,8 @@ import com.vitorpamplona.amethyst.commons.relayClient.nip17Dm.unwrapAndUnsealOrN
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.user.LocalUserFinder
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.user.LocalUserFinderAccount
|
||||
import com.vitorpamplona.amethyst.commons.scheduledposts.ScheduledPostStatus
|
||||
import com.vitorpamplona.amethyst.commons.tor.TorServiceStatus
|
||||
import com.vitorpamplona.amethyst.commons.tor.TorType
|
||||
import com.vitorpamplona.amethyst.commons.wot.LocalWoTReady
|
||||
import com.vitorpamplona.amethyst.commons.wot.LocalWoTService
|
||||
import com.vitorpamplona.amethyst.desktop.account.AccountManager
|
||||
@@ -122,6 +124,7 @@ import com.vitorpamplona.amethyst.desktop.service.scheduledposts.LocalScheduledP
|
||||
import com.vitorpamplona.amethyst.desktop.service.scheduledposts.OsScheduler
|
||||
import com.vitorpamplona.amethyst.desktop.service.scheduledposts.runHeadlessPublish
|
||||
import com.vitorpamplona.amethyst.desktop.subscriptions.DesktopRelaySubscriptionsCoordinator
|
||||
import com.vitorpamplona.amethyst.desktop.tor.DesktopTorPreferences
|
||||
import com.vitorpamplona.amethyst.desktop.ui.ComposeNoteDialog
|
||||
import com.vitorpamplona.amethyst.desktop.ui.ConnectingRelaysScreen
|
||||
import com.vitorpamplona.amethyst.desktop.ui.ImportFollowListDialog
|
||||
@@ -160,6 +163,8 @@ import com.vitorpamplona.amethyst.desktop.ui.settings.SettingsAccordionCard
|
||||
import com.vitorpamplona.amethyst.desktop.ui.settings.SettingsEntry
|
||||
import com.vitorpamplona.amethyst.desktop.ui.settings.SettingsMeta
|
||||
import com.vitorpamplona.amethyst.desktop.ui.settings.WalletConnectSettingsSection
|
||||
import com.vitorpamplona.amethyst.desktop.ui.tor.TorConnectingSplash
|
||||
import com.vitorpamplona.amethyst.desktop.ui.tor.TorSettingsDialog
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.SubscriptionListener
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
@@ -342,14 +347,11 @@ fun main(args: Array<String>) {
|
||||
val feedSearchActiveState = remember { mutableStateOf(false) }
|
||||
|
||||
// Tor state at Window level — survives key() app rebuild
|
||||
var torSettings by remember {
|
||||
mutableStateOf(
|
||||
com.vitorpamplona.amethyst.desktop.tor.DesktopTorPreferences
|
||||
.load(),
|
||||
)
|
||||
}
|
||||
val torSettings = remember { DesktopTorPreferences.load() }
|
||||
val torTypeFlow = remember { kotlinx.coroutines.flow.MutableStateFlow(torSettings.torType) }
|
||||
val externalPortFlow = remember { kotlinx.coroutines.flow.MutableStateFlow(torSettings.externalSocksPort) }
|
||||
// "Use regular connection" on the Tor splash: Tor off for this run only, never saved.
|
||||
val torSessionBypassFlow = remember { kotlinx.coroutines.flow.MutableStateFlow(false) }
|
||||
val windowScope = rememberCoroutineScope()
|
||||
val torManager =
|
||||
remember {
|
||||
@@ -734,6 +736,7 @@ fun main(args: Array<String>) {
|
||||
torManager = torManager,
|
||||
torTypeFlow = torTypeFlow,
|
||||
externalPortFlow = externalPortFlow,
|
||||
torSessionBypassFlow = torSessionBypassFlow,
|
||||
initialTorSettings = torSettings,
|
||||
onNavigateToScreen = { navigateToScreen = it },
|
||||
)
|
||||
@@ -770,6 +773,7 @@ fun App(
|
||||
torManager: com.vitorpamplona.amethyst.commons.tor.ITorManager,
|
||||
torTypeFlow: kotlinx.coroutines.flow.MutableStateFlow<com.vitorpamplona.amethyst.commons.tor.TorType>,
|
||||
externalPortFlow: kotlinx.coroutines.flow.MutableStateFlow<Int>,
|
||||
torSessionBypassFlow: kotlinx.coroutines.flow.MutableStateFlow<Boolean>,
|
||||
initialTorSettings: com.vitorpamplona.amethyst.commons.tor.TorSettings,
|
||||
onNavigateToScreen: ((DeckColumnType) -> Unit) -> Unit = {},
|
||||
testOverrides: LaunchTestOverrides? = null,
|
||||
@@ -826,6 +830,7 @@ fun App(
|
||||
torManager = torManager,
|
||||
torTypeFlow = torTypeFlow,
|
||||
externalPortFlow = externalPortFlow,
|
||||
torSessionBypassFlow = torSessionBypassFlow,
|
||||
initialTorSettings = initialTorSettings,
|
||||
onNavigateToScreen = onNavigateToScreen,
|
||||
testOverrides = testOverrides,
|
||||
@@ -861,6 +866,7 @@ private fun AppInner(
|
||||
torManager: com.vitorpamplona.amethyst.commons.tor.ITorManager,
|
||||
torTypeFlow: kotlinx.coroutines.flow.MutableStateFlow<com.vitorpamplona.amethyst.commons.tor.TorType>,
|
||||
externalPortFlow: kotlinx.coroutines.flow.MutableStateFlow<Int>,
|
||||
torSessionBypassFlow: kotlinx.coroutines.flow.MutableStateFlow<Boolean>,
|
||||
initialTorSettings: com.vitorpamplona.amethyst.commons.tor.TorSettings,
|
||||
onNavigateToScreen: ((DeckColumnType) -> Unit) -> Unit,
|
||||
testOverrides: LaunchTestOverrides?,
|
||||
@@ -875,56 +881,42 @@ private fun AppInner(
|
||||
// Always reload from prefs — after key() rebuild, prefs have the latest saved settings.
|
||||
// Tests can short-circuit the prefs read via `testOverrides.torSettingsOverride` so the
|
||||
// Tor splash gate (below) does not block them behind a real kmp-tor runtime.
|
||||
// torSettings is the user's saved choice (what the settings UI shows and saves);
|
||||
// effectiveTorSettings is what this session actually routes by.
|
||||
var torSettings by remember {
|
||||
mutableStateOf(
|
||||
testOverrides?.torSettingsOverride
|
||||
?: com.vitorpamplona.amethyst.desktop.tor.DesktopTorPreferences
|
||||
.load(),
|
||||
)
|
||||
mutableStateOf(testOverrides?.torSettingsOverride ?: DesktopTorPreferences.load())
|
||||
}
|
||||
val torSessionBypass by torSessionBypassFlow.collectAsState()
|
||||
val effectiveTorSettings = if (torSessionBypass) torSettings.copy(torType = TorType.OFF) else torSettings
|
||||
|
||||
// Gate: block EVERYTHING until Tor proxy is ready (when Tor expected)
|
||||
// This must be before any OkHttpClient/Coil/relay creation
|
||||
val torStatus by torManager.status.collectAsState()
|
||||
val isTorExpected = torSettings.torType != com.vitorpamplona.amethyst.commons.tor.TorType.OFF
|
||||
if (isTorExpected && torStatus !is com.vitorpamplona.amethyst.commons.tor.TorServiceStatus.Active) {
|
||||
val splashIcon = com.vitorpamplona.amethyst.desktop.platform.IconResources.rawBitmapPainter
|
||||
androidx.compose.foundation.layout.Box(
|
||||
modifier =
|
||||
androidx.compose.ui.Modifier
|
||||
.fillMaxSize(),
|
||||
contentAlignment = androidx.compose.ui.Alignment.Center,
|
||||
) {
|
||||
androidx.compose.foundation.layout.Column(
|
||||
horizontalAlignment = androidx.compose.ui.Alignment.CenterHorizontally,
|
||||
) {
|
||||
androidx.compose.material3.CircularProgressIndicator()
|
||||
androidx.compose.foundation.layout.Spacer(
|
||||
modifier =
|
||||
androidx.compose.ui.Modifier
|
||||
.height(16.dp),
|
||||
)
|
||||
if (torStatus is com.vitorpamplona.amethyst.commons.tor.TorServiceStatus.Error) {
|
||||
androidx.compose.material3.Text(
|
||||
"Tor error: ${(torStatus as com.vitorpamplona.amethyst.commons.tor.TorServiceStatus.Error).message}",
|
||||
)
|
||||
} else {
|
||||
androidx.compose.material3.Text("Connecting to Tor...")
|
||||
}
|
||||
androidx.compose.foundation.layout.Spacer(
|
||||
modifier =
|
||||
androidx.compose.ui.Modifier
|
||||
.height(24.dp),
|
||||
)
|
||||
androidx.compose.material3.Icon(
|
||||
painter = splashIcon,
|
||||
contentDescription = "Amethyst",
|
||||
modifier =
|
||||
androidx.compose.ui.Modifier
|
||||
.size(96.dp),
|
||||
tint = androidx.compose.material3.MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
val isTorExpected = effectiveTorSettings.torType != TorType.OFF
|
||||
if (isTorExpected && torStatus !is TorServiceStatus.Active) {
|
||||
var showTorSettings by remember { mutableStateOf(false) }
|
||||
TorConnectingSplash(
|
||||
status = torStatus,
|
||||
onContinueWithoutTor = {
|
||||
torSessionBypassFlow.value = true
|
||||
torTypeFlow.value = TorType.OFF
|
||||
},
|
||||
onOpenTorSettings = { showTorSettings = true },
|
||||
)
|
||||
if (showTorSettings) {
|
||||
TorSettingsDialog(
|
||||
currentSettings = torSettings,
|
||||
torStatus = torStatus,
|
||||
onSettingsChanged = { newSettings ->
|
||||
// No onRestartApp(): nothing below the gate has been built yet, and a
|
||||
// rebuild would only restart the splash (and its escape timer).
|
||||
torSettings = newSettings
|
||||
DesktopTorPreferences.save(newSettings)
|
||||
torTypeFlow.value = newSettings.torType
|
||||
externalPortFlow.value = newSettings.externalSocksPort
|
||||
},
|
||||
onDismiss = { showTorSettings = false },
|
||||
)
|
||||
}
|
||||
return // Nothing below runs until Tor is Active
|
||||
}
|
||||
@@ -985,15 +977,15 @@ private fun AppInner(
|
||||
|
||||
// Build TorRelayEvaluation for per-relay routing
|
||||
val torRelayEvaluation =
|
||||
remember(torSettings) {
|
||||
remember(effectiveTorSettings) {
|
||||
com.vitorpamplona.amethyst.commons.tor.TorRelayEvaluation(
|
||||
torSettings =
|
||||
com.vitorpamplona.amethyst.commons.tor.TorRelaySettings(
|
||||
torType = torSettings.torType,
|
||||
onionRelaysViaTor = torSettings.onionRelaysViaTor,
|
||||
dmRelaysViaTor = torSettings.dmRelaysViaTor,
|
||||
newRelaysViaTor = torSettings.newRelaysViaTor,
|
||||
trustedRelaysViaTor = torSettings.trustedRelaysViaTor,
|
||||
torType = effectiveTorSettings.torType,
|
||||
onionRelaysViaTor = effectiveTorSettings.onionRelaysViaTor,
|
||||
dmRelaysViaTor = effectiveTorSettings.dmRelaysViaTor,
|
||||
newRelaysViaTor = effectiveTorSettings.newRelaysViaTor,
|
||||
trustedRelaysViaTor = effectiveTorSettings.trustedRelaysViaTor,
|
||||
),
|
||||
// TODO: populate from account relay lists
|
||||
classification =
|
||||
@@ -1499,9 +1491,11 @@ private fun AppInner(
|
||||
status = currentTorStatus,
|
||||
settings = torSettings,
|
||||
onSettingsChanged = { newSettings ->
|
||||
// Saving re-applies the saved choice, ending a
|
||||
// session bypass from the splash.
|
||||
torSessionBypassFlow.value = false
|
||||
torSettings = newSettings
|
||||
com.vitorpamplona.amethyst.desktop.tor.DesktopTorPreferences
|
||||
.save(newSettings)
|
||||
DesktopTorPreferences.save(newSettings)
|
||||
torTypeFlow.value = newSettings.torType
|
||||
externalPortFlow.value = newSettings.externalSocksPort
|
||||
// Rebuild app to apply Tor changes
|
||||
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.desktop.ui.tor
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_logo
|
||||
import com.vitorpamplona.amethyst.commons.resources.connect_via_tor2
|
||||
import com.vitorpamplona.amethyst.commons.resources.tor_continue_without_for_session
|
||||
import com.vitorpamplona.amethyst.commons.resources.tor_splash_connecting
|
||||
import com.vitorpamplona.amethyst.commons.resources.tor_splash_error
|
||||
import com.vitorpamplona.amethyst.commons.resources.tor_splash_explainer
|
||||
import com.vitorpamplona.amethyst.commons.tor.TorServiceStatus
|
||||
import com.vitorpamplona.amethyst.desktop.platform.IconResources
|
||||
import kotlinx.coroutines.delay
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
/**
|
||||
* How long the splash waits before offering a way past Tor. Long enough that a normal
|
||||
* bootstrap finishes without the user ever seeing the choice, short enough that someone
|
||||
* offline (or on a network that blocks Tor) is not left staring at a spinner.
|
||||
*/
|
||||
private const val ESCAPE_DELAY_MS = 10_000L
|
||||
|
||||
/**
|
||||
* Shown instead of the app while the embedded Tor is expected but not yet routable.
|
||||
*
|
||||
* Tor never finishes bootstrapping without internet access, so this screen must not be a dead
|
||||
* end: after [ESCAPE_DELAY_MS] (or at once, on an error) it offers to continue over a regular
|
||||
* connection for this session, or to open the Tor settings (e.g. to point at an external Tor).
|
||||
*/
|
||||
@Composable
|
||||
fun TorConnectingSplash(
|
||||
status: TorServiceStatus,
|
||||
onContinueWithoutTor: () -> Unit,
|
||||
onOpenTorSettings: () -> Unit,
|
||||
) {
|
||||
var showEscape by remember { mutableStateOf(false) }
|
||||
LaunchedEffect(Unit) {
|
||||
delay(ESCAPE_DELAY_MS)
|
||||
showEscape = true
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize().padding(24.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.widthIn(max = 420.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
Spacer(Modifier.height(16.dp))
|
||||
Text(
|
||||
text =
|
||||
if (status is TorServiceStatus.Error) {
|
||||
stringResource(Res.string.tor_splash_error, status.message)
|
||||
} else {
|
||||
stringResource(Res.string.tor_splash_connecting)
|
||||
},
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Text(
|
||||
text = stringResource(Res.string.tor_splash_explainer),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
Spacer(Modifier.height(24.dp))
|
||||
Icon(
|
||||
painter = IconResources.rawBitmapPainter,
|
||||
contentDescription = stringResource(Res.string.app_logo),
|
||||
modifier = Modifier.size(96.dp),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
if (showEscape || status is TorServiceStatus.Error) {
|
||||
Spacer(Modifier.height(24.dp))
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
OutlinedButton(onClick = onOpenTorSettings) {
|
||||
Text(stringResource(Res.string.connect_via_tor2))
|
||||
}
|
||||
Button(onClick = onContinueWithoutTor) {
|
||||
Text(stringResource(Res.string.tor_continue_without_for_session))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
@@ -129,6 +129,7 @@ class AppStateMachineTest {
|
||||
torManager = torManager,
|
||||
torTypeFlow = MutableStateFlow(TorType.OFF),
|
||||
externalPortFlow = MutableStateFlow(9050),
|
||||
torSessionBypassFlow = MutableStateFlow(false),
|
||||
initialTorSettings = OFF_TOR_SETTINGS,
|
||||
testOverrides =
|
||||
LaunchTestOverrides(
|
||||
@@ -190,6 +191,7 @@ class AppStateMachineTest {
|
||||
torManager = torManager,
|
||||
torTypeFlow = MutableStateFlow(TorType.OFF),
|
||||
externalPortFlow = MutableStateFlow(9050),
|
||||
torSessionBypassFlow = MutableStateFlow(false),
|
||||
initialTorSettings = OFF_TOR_SETTINGS,
|
||||
testOverrides =
|
||||
LaunchTestOverrides(
|
||||
@@ -266,6 +268,7 @@ class AppStateMachineTest {
|
||||
torManager = torManager,
|
||||
torTypeFlow = MutableStateFlow(TorType.OFF),
|
||||
externalPortFlow = MutableStateFlow(9050),
|
||||
torSessionBypassFlow = MutableStateFlow(false),
|
||||
initialTorSettings = OFF_TOR_SETTINGS,
|
||||
testOverrides =
|
||||
LaunchTestOverrides(
|
||||
@@ -342,6 +345,7 @@ class AppStateMachineTest {
|
||||
torManager = torManager,
|
||||
torTypeFlow = MutableStateFlow(TorType.OFF),
|
||||
externalPortFlow = MutableStateFlow(9050),
|
||||
torSessionBypassFlow = MutableStateFlow(false),
|
||||
initialTorSettings = OFF_TOR_SETTINGS,
|
||||
testOverrides =
|
||||
LaunchTestOverrides(
|
||||
|
||||
+26
-4
@@ -28,12 +28,12 @@
|
||||
# amethyst-desktop-1.08.0-windows-arm64.zip
|
||||
# amethyst-desktop-1.08.0-linux-x64.deb
|
||||
# amethyst-desktop-1.08.0-linux-x64.rpm
|
||||
# amethyst-desktop-1.08.0-linux-x64.AppImage
|
||||
# amethyst-desktop-1.08.0-x86_64.AppImage
|
||||
# amethyst-desktop-1.08.0-linux-x64.flatpak
|
||||
# amethyst-desktop-1.08.0-linux-x64.tar.gz
|
||||
# amethyst-desktop-1.08.0-linux-arm64.deb
|
||||
# amethyst-desktop-1.08.0-linux-arm64.rpm
|
||||
# amethyst-desktop-1.08.0-linux-arm64.AppImage
|
||||
# amethyst-desktop-1.08.0-aarch64.AppImage
|
||||
# amethyst-desktop-1.08.0-linux-arm64.flatpak
|
||||
# amethyst-desktop-1.08.0-linux-arm64.tar.gz
|
||||
# amy-1.08.0-macos-arm64.tar.gz
|
||||
@@ -56,7 +56,12 @@
|
||||
# geode-1.08.0-windows-x64.zip
|
||||
# geode-1.08.0-windows-arm64.zip
|
||||
#
|
||||
# Two assets break the family/arch shape on purpose: the no-JRE jar bundles for
|
||||
# The AppImage breaks the family/arch shape on purpose: AppImageHub
|
||||
# (appimage.github.io) flags "linux" in an AppImage name, since every AppImage
|
||||
# is for Linux, and expects the AppImage arch names, so it is
|
||||
# amethyst-desktop-<version>-<x86_64|aarch64>.AppImage
|
||||
#
|
||||
# Two more assets break the family/arch shape on purpose: the no-JRE jar bundles for
|
||||
# Homebrew-core are pure JVM bytecode (no bundled runtime), so a single
|
||||
# platform-independent artifact serves every OS:
|
||||
# amy-1.08.0-jvm.tar.gz
|
||||
@@ -87,6 +92,19 @@ geode_asset_name() {
|
||||
printf 'geode-%s-%s-%s.%s' "$version" "$family" "$arch" "$ext"
|
||||
}
|
||||
|
||||
# AppImage variant: no <family> (all AppImages are for Linux) and the AppImage
|
||||
# arch names (x86_64 / aarch64) instead of x64 / arm64.
|
||||
# Usage: appimage_asset_name <arch> <version>
|
||||
appimage_asset_name() {
|
||||
local arch="$1" version="$2" appimage_arch
|
||||
case "$arch" in
|
||||
x64) appimage_arch="x86_64" ;;
|
||||
arm64) appimage_arch="aarch64" ;;
|
||||
*) echo "appimage_asset_name: unsupported arch '$arch'" >&2; return 1 ;;
|
||||
esac
|
||||
printf 'amethyst-desktop-%s-%s.AppImage' "$version" "$appimage_arch"
|
||||
}
|
||||
|
||||
# Copy + rename build outputs into <dest_dir> using the canonical naming scheme.
|
||||
# Usage: collect_assets <family> <arch> <version> <dest_dir>
|
||||
# Expects build outputs under desktopApp/build/... (Compose binaries + custom tasks + portable archives).
|
||||
@@ -115,7 +133,11 @@ collect_assets() {
|
||||
*.tar.gz) ext="tar.gz" ;;
|
||||
*) ext="${base##*.}" ;;
|
||||
esac
|
||||
dst="$dest/$(asset_name "$family" "$arch" "$version" "$ext")"
|
||||
if [[ "$ext" == "AppImage" ]]; then
|
||||
dst="$dest/$(appimage_asset_name "$arch" "$version")"
|
||||
else
|
||||
dst="$dest/$(asset_name "$family" "$arch" "$version" "$ext")"
|
||||
fi
|
||||
cp "$src" "$dst"
|
||||
echo "Collected: $dst"
|
||||
done
|
||||
|
||||
Reference in New Issue
Block a user