mirror of
https://github.com/fromchat-messenger/app.git
synced 2026-11-15 18:34:54 +03:00
Fix macOS launch, Windows ARM64 install, and notification translation
macOS: the app died before the first frame with "MacOS does not support
OPENGL rendering API" from SkiaLayer's constructor. Compose's platform DSL
(windows { } / macOS { }) has no jvmArgs of its own, so the unqualified
jvmArgs(...) in `windows { }` resolved to JvmApplication.jvmArgs — the one
application-level list the plugin writes into the launcher config of *every*
platform it packages. The published macOS bundle therefore shipped
-Dskiko.renderApi=OPENGL and -Dsun.java2d.d3d=true. GPU args now come from
hostGpuJvmArgs(), added per host: OPENGL on Linux and Windows x64, ANGLE on
Windows ARM64, nothing on macOS (Skiko defaults to Metal there). Verified by
repackaging locally: the macOS FromChat.cfg no longer carries those flags and
the packaged app starts, connects and renders.
Windows ARM64: the installer aborted with "installer has no payload for this
CPU (x86_64)". payload.rs picked the payload with std::env::consts::ARCH,
which is the *installer's* compile-time arch — and the stub is built once on
windows-latest (x64). On Windows ARM64 it runs emulated, reports x86_64 and
looks for an x64 payload the ARM-only bundle never embeds. host_arch() now
prefers PROCESSOR_ARCHITEW6432, which carries the machine's real architecture
for WOW64/emulated processes. payload_zstd() is only called by the runtime
binaries, never by the packer, so this cannot affect what gets packed.
Notifications: a Russian UI produced notifications titled "Main chat" (the
English default) because getString() outside a composition resolves against
the default resource. The chats screen already resolves the title the way the
UI renders it, so it now publishes that through LocalizedChatTitles and the
sink reuses it — same string the user sees, no second resolution path.
This commit is contained in:
@@ -127,12 +127,29 @@ private fun syncWindowsArm64SkikoNatives(appDir: File, classpath: Iterable<File>
|
||||
}
|
||||
}
|
||||
|
||||
private fun windowsSkikoJvmArgs(): List<String> =
|
||||
/**
|
||||
* GPU-path JVM args for the *packaging host*.
|
||||
*
|
||||
* Compose's platform DSL (`windows { }`, `macOS { }`, …) has no `jvmArgs` of its own: an
|
||||
* unqualified `jvmArgs(...)` inside one resolves to [JvmApplication.jvmArgs], the single
|
||||
* application-level list, which the plugin writes into the launcher config of **every** platform
|
||||
* it packages. So these args have to be added here, per host, instead of inside `windows { }`.
|
||||
*
|
||||
* OPENGL is right on Linux and Windows x64, ANGLE (D3D11) is required on Windows ARM64, and
|
||||
* macOS must get nothing: Skiko defaults to Metal there, and asking for OPENGL throws
|
||||
* "MacOS does not support OPENGL rendering API" in SkiaLayer's constructor, killing the app
|
||||
* before the first frame.
|
||||
*/
|
||||
private fun hostGpuJvmArgs(): List<String> =
|
||||
if (isWindowsArm64Host()) {
|
||||
// Windows ARM64: OpenGL is unsupported; ANGLE (D3D11) is the supported GPU path in Skiko.
|
||||
listOf("-Dskiko.rendering.angle.enabled=true")
|
||||
} else {
|
||||
} else if (System.getProperty("os.name").orEmpty().lowercase().contains("win")) {
|
||||
listOf("-Dskiko.renderApi=OPENGL", "-Dsun.java2d.d3d=true")
|
||||
} else if (System.getProperty("os.name").orEmpty().lowercase().contains("mac")) {
|
||||
emptyList()
|
||||
} else {
|
||||
listOf("-Dskiko.renderApi=OPENGL")
|
||||
}
|
||||
|
||||
private fun jdkReportsArch(javaExe: File): String? {
|
||||
@@ -407,6 +424,10 @@ compose.desktop {
|
||||
application {
|
||||
mainClass = "ru.fromchat.desktop.MainKt"
|
||||
javaHome = resolvePackagingJdkHome()
|
||||
// Application-level, and per host: an unqualified jvmArgs() inside
|
||||
// nativeDistributions.<platform> { } lands here anyway and then leaks into every platform's
|
||||
// launcher config. See hostGpuJvmArgs().
|
||||
jvmArgs(*hostGpuJvmArgs().toTypedArray())
|
||||
|
||||
buildTypes.release.proguard {
|
||||
version = proguardVersion
|
||||
@@ -461,7 +482,6 @@ compose.desktop {
|
||||
iconFile.set(desktopWindowIconIco)
|
||||
packageVersion = nativePackageVersion
|
||||
exePackageVersion = nativePackageVersion
|
||||
jvmArgs(*windowsSkikoJvmArgs().toTypedArray())
|
||||
}
|
||||
macOS {
|
||||
bundleID = "ru.fromchat.desktop"
|
||||
@@ -597,8 +617,8 @@ fun JavaExec.configureFromChatDesktopJvm() {
|
||||
}
|
||||
jvmArgs(
|
||||
dockIconArgs + buildList {
|
||||
if (System.getProperty("os.name").orEmpty().lowercase().contains("win")) {
|
||||
addAll(windowsSkikoJvmArgs())
|
||||
if (!System.getProperty("os.name").orEmpty().lowercase().contains("mac")) {
|
||||
addAll(hostGpuJvmArgs())
|
||||
}
|
||||
addAll(
|
||||
listOf(
|
||||
|
||||
@@ -48,12 +48,34 @@ impl EmbeddedBundle {
|
||||
if let Some(payload) = host_payload(self) {
|
||||
return Ok(payload);
|
||||
}
|
||||
bail!("installer has no payload for this CPU ({})", std::env::consts::ARCH)
|
||||
bail!(
|
||||
"installer has no payload for this CPU ({})",
|
||||
host_arch()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// The architecture to install for — the *machine's*, not the installer's.
|
||||
///
|
||||
/// `std::env::consts::ARCH` is fixed when the binary is compiled, and the installer stub is built
|
||||
/// once on windows-latest (x64). On Windows ARM64 that stub runs emulated, so it reports
|
||||
/// "x86_64" and looks for an x86_64 payload — which the ARM64-only installer does not embed, so it
|
||||
/// dies with "installer has no payload for this CPU (x86_64)" before touching the disk.
|
||||
/// `PROCESSOR_ARCHITEW6432` carries the real machine architecture for WOW64/emulated processes.
|
||||
fn host_arch() -> &'static str {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
if let Ok(native) = std::env::var("PROCESSOR_ARCHITEW6432") {
|
||||
if native.eq_ignore_ascii_case("ARM64") {
|
||||
return "aarch64";
|
||||
}
|
||||
}
|
||||
}
|
||||
std::env::consts::ARCH
|
||||
}
|
||||
|
||||
fn host_payload(bundle: &EmbeddedBundle) -> Option<&[u8]> {
|
||||
match std::env::consts::ARCH {
|
||||
match host_arch() {
|
||||
"x86_64" => bundle
|
||||
.payload_x64
|
||||
.as_deref()
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package ru.fromchat.notifications
|
||||
|
||||
/**
|
||||
* Chat titles resolved by the UI, for callers that run outside a composition.
|
||||
*
|
||||
* The desktop notification sinks need a chat title but cannot use `stringResource`, and
|
||||
* `org.jetbrains.compose.resources.getString` called outside a composition falls back to the
|
||||
* default (English) resource rather than the locale the UI is actually rendering in — so a Russian
|
||||
* UI produced notifications titled "Main chat". The chats screen already resolves the title exactly
|
||||
* the way the UI renders it (server profile title, else the localized `public_chat` string), so it
|
||||
* publishes the result here and the sinks reuse it verbatim.
|
||||
*/
|
||||
internal object LocalizedChatTitles {
|
||||
@Volatile
|
||||
var fallback: String = ""
|
||||
|
||||
@Volatile
|
||||
var publicChatProfileTitle: String = ""
|
||||
|
||||
fun remember(publicChatProfileTitle: String, fallback: String) {
|
||||
if (publicChatProfileTitle.isNotBlank()) this.publicChatProfileTitle = publicChatProfileTitle
|
||||
if (fallback.isNotBlank()) this.fallback = fallback
|
||||
}
|
||||
|
||||
fun publicChatOrNull(): String? =
|
||||
publicChatProfileTitle.takeIf { it.isNotBlank() } ?: fallback.takeIf { it.isNotBlank() }
|
||||
}
|
||||
@@ -136,6 +136,7 @@ import ru.fromchat.chat_preview_image
|
||||
import ru.fromchat.chat_preview_image_emoji
|
||||
import ru.fromchat.chats_selected_count
|
||||
import ru.fromchat.config.ServerConfig
|
||||
import ru.fromchat.notifications.LocalizedChatTitles
|
||||
import ru.fromchat.public_chat
|
||||
import ru.fromchat.search_title
|
||||
import ru.fromchat.status_connecting
|
||||
@@ -822,6 +823,14 @@ fun ChatsTab(
|
||||
val publicChatFallbackTitle = stringResource(Res.string.public_chat)
|
||||
val publicChatTitle = publicChatProfile?.title?.takeIf { it.isNotBlank() }
|
||||
?: publicChatFallbackTitle.takeIf { activeInstanceId.isNotBlank() }
|
||||
// Hand the resolved titles to the desktop notification sinks, which resolve strings outside a
|
||||
// composition and would otherwise fall back to the default (English) resource.
|
||||
SideEffect {
|
||||
LocalizedChatTitles.remember(
|
||||
publicChatProfileTitle = publicChatProfile?.title.orEmpty(),
|
||||
fallback = publicChatFallbackTitle,
|
||||
)
|
||||
}
|
||||
val publicChatLink = publicChatProfile?.let { "https://fromchat.ru/chats/${it.id}" }
|
||||
val deleteConfirmTitle = stringResource(Res.string.chat_delete_confirm_title)
|
||||
val deleteConfirmBody = stringResource(Res.string.chat_delete_confirm_body)
|
||||
|
||||
+4
-1
@@ -36,7 +36,10 @@ internal actual object MessageNotificationSink {
|
||||
title = notification.senderName.ifBlank { "FromChat" }
|
||||
subtitle = ""
|
||||
} else {
|
||||
title = getString(Res.string.public_chat)
|
||||
// getString() outside a composition resolves against the default resource, which is why
|
||||
// this used to show English "Main chat" under a localized UI. Prefer the titles the
|
||||
// chats screen already resolved for the UI.
|
||||
title = LocalizedChatTitles.publicChatOrNull() ?: getString(Res.string.public_chat)
|
||||
subtitle = notification.senderName
|
||||
}
|
||||
DesktopNotifier.rememberLaunch(notification.identifier, notification.launchTarget)
|
||||
|
||||
Reference in New Issue
Block a user