desktop-and-build-footguns
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseDesktop JVM and build footguns
Desktop JVM 与构建陷阱
24 traps mined from a production Kotlin and Compose Multiplatform app, one short file each
under . Every file gives the working pattern, the Traps (the specific ways it
fails in practice, and why), and a Verifying it section with commands to run against the
user's own tree.
references/How to use this index. Match the code about to be written, or the symptom being chased,
against the entries below: each names its topic and the symptom that should send you to it. Read
every file that plausibly applies with the Read tool before proposing code or a fix; each is under
150 lines. Where a Verifying command names the source project's paths, substitute the equivalent
paths in the user's tree.
Cross-references. A backticked trap name inside a file that is not listed here lives in a
sibling area skill: glob to open it.
../*/references/<name>.md从生产级 Kotlin 和 Compose Multiplatform 应用中提炼出的 24 个陷阱, 下每个陷阱对应一个简短文件。每个文件都会给出可行模式、陷阱(实际失败的具体方式及原因),以及 验证方法 小节,其中包含可针对用户项目目录运行的命令。
references/如何使用此索引。 将即将编写的代码或正在排查的症状与下方条目进行匹配:每个条目都注明主题,以及应据此查阅该条目的症状。在提出代码或修复方案前,使用 Read 工具阅读所有可能适用的文件;每个文件均少于 150 行。如果验证命令提到源项目中的路径,请替换为用户项目目录中的等效路径。
交叉引用。 文件中以反引号标出的陷阱名称如果未在此列出,则位于同级领域的 skill 中:使用 glob 打开它。
../*/references/<name>.mdNative code on the desktop JVM
Desktop JVM 上的原生代码
- — Hand-writing a JVM binding for a C library with JNA (Java Native Access) — the open-flags option that means something else on Windows, structs read by raw offset, callbacks the binding holds weakly, search paths registered too late, and proving which file was actually opened. Reach for it when a binding works on every developer machine and fails on a clean one, or when the very first symbol lookup fails with "the specified module could not be found" while the library is sitting right there.
jna-native-binding-traps - — Producing a native shared library a JVM app can actually load and ship — why a prebuilt portable bundle usually is not a loadable library at all, building on the oldest base system you support, run-path choice, which libraries to deliberately leave out of the bundle, and gating the build on a load-and-initialize smoke test. Reach for it when the bundled native works on every developer machine and fails on a clean one, or when you discover the app has been quietly using a system-wide copy instead of yours.
native-artifact-for-embedding - — A native library you bundle with a desktop app drags its own copy of a general-purpose base library along, that copy claims the shared-object name for the whole process the moment your native loads, and an unrelated platform API then fails with a missing-symbol message naming a third library. Use when a feature that opens links or system dialogs works on your machine but silently does nothing on users' machines, when a platform API reports itself unsupported at runtime, when deciding what a native bundle may contain, or when a merged fix for exactly this bug does not seem to have changed anything for users.
bundled-native-soname-conflict - — Strip the
macos-codesign-sidecar-stripcompanion files that macOS archiving writes beside files carrying extended attributes, after unpacking anything into a macOS app bundle and before the bundle is signed, because the signer seals those companions as ordinary bundle members and the OS deletes them the first time the file manager touches the app. Use when a signed macOS app launches fine on the build machine but users are told it is damaged and can't be opened, or when signature verification reports a sealed resource missing.._* - — Judge and reduce a desktop JVM application's memory honestly — read the heap-to-footprint ratio rather than the resident figure, run the one experiment that separates a leak from an allocator holding idle pages, and understand why per-thread allocator arenas make the footprint depend on the user's core count. Use when a desktop app settles near a gigabyte or keeps climbing over a long session, when users on big machines report far worse memory than you can reproduce, or before tuning any allocator or garbage-collector flag.
jvm-desktop-memory-footprint - — Embedding a native C media engine in a JVM desktop app — one handle per media item, a dedicated event-pump thread, pinning the output driver and creating the render context in the right order, confining every property write to the thread that also releases handles, feature-detecting optional engine options, and formatting numbers the way the engine parses them. Reach for it when the app stops while setting a property, when the engine opens a window of its own, or when a value the app clearly sets is silently ignored.
embed-media-engine-desktop - — Playing a separate audio-only stream and video-only stream as ONE source on JVM desktop through the media engine's edit-list URL form — the desktop counterpart of a merging media source — including length-prefixed quoting of stream URLs and why a merged two-URL item must not be prepared in the middle of a crossfade. Reach for it when desktop video plays back completely silent with nothing in the log, when a stream URL is truncated at the first semicolon, or when fading into a video track cuts the outgoing song short.
merge-split-av-streams-desktop - — Rendering video frames from a native media engine in Compose Desktop without embedding a heavyweight AWT panel — publish finished frames as immutable snapshots on a StateFlow and draw them with a plain Image, convert off the UI thread, match the engine's pixel byte order, and let the engine decide the fit. Reach for it when embedded video sits on top of everything regardless of z-order, lags a frame behind while scrolling, goes black on one screen the moment a second screen shows the player, comes out with red and blue swapped, or shows black bars that no content-scale can remove.
compose-desktop-video-no-swingpanel - — Wire a desktop app into each OS's system now-playing and transport surface behind one facade, so a failed native initialisation disables the integration and never takes playback down. Covers initialising on the platform's main thread inside a packaged app, reaching an OS media framework through the JVM's native-access layer, holding strong references to callbacks handed to the OS, and confining the integration to its own thread. Use when media keys or the system now-playing panel work under a plain Gradle run but not in the packaged app, when only the app name renders instead of the track title, or when transport callbacks stop arriving after a while.
desktop-system-media-integration - — The shape a workaround should take when the bug is in a library you cannot patch — trace the mechanism as far as you actually got, name the tradeoffs you accepted, leave an escape hatch so a failure degrades instead of going silent, write the explicit condition for removing it, and record why your usage pattern exposes a bug the library's own users never hit. Use when you are about to pin a setting, avoid a code path or add a defensive branch because of someone else's bug, or when reviewing a workaround whose comment does not say when it may be deleted.
upstream-lib-bug-workaround-template - — Declaring a Launch Services environment dictionary (
macos-lsenvironment-path-pin) in a packaged macOS app's property list pins the processLSEnvironmentto the four bare system directories, so every external process the app spawns loses everything installed elsewhere. Use before adding any environment variable to a macOS app bundle, when a helper the app shells out to reports "command not found" only for installed users, or when a feature that works from a terminal launch silently does nothing from the Dock.PATH
- —— 使用 JNA(Java Native Access)为 C 库手写 JVM 绑定——在 Windows 上含义不同的 open-flags 选项、按原始偏移读取的 struct、绑定仅弱引用的 callback,以及注册过晚的搜索路径,还要证明实际打开的是哪个文件。当某个绑定在每台开发机上都能工作,却在干净环境失败时,或者库明明就在旁边但第一次查找 symbol 就报“the specified module could not be found”时,查阅本条。
jna-native-binding-traps - —— 生成 JVM 应用真正能够加载和发布的原生共享库——说明为何预构建的 portable bundle 通常并不是可加载的库,如何在支持的最旧基础系统上构建,如何选择 run-path,哪些库应刻意不放入 bundle,以及如何用 load-and-initialize smoke test 阻断不合格构建。当捆绑的 native component 在每台开发机上都能工作,却在干净环境失败时,或者发现应用一直悄悄使用 system-wide copy 而非你提供的版本时,查阅本条。
native-artifact-for-embedding - —— 随 desktop app 捆绑的 native library 会连带引入其自带的通用基础库副本;该副本在 native library 加载后会立即为整个进程占用 shared-object 名称,随后无关的 platform API 会因缺少 symbol 而失败,错误信息却指向第三个库。当打开链接或系统对话框的功能在你的机器上正常,却在用户机器上静默无效;platform API 在 runtime 报告自身不受支持;需要决定 native bundle 可以包含哪些内容;或针对该 bug 的合并修复似乎没有为用户带来任何变化时,使用本条。
bundled-native-soname-conflict - —— 在将任何内容解包到 macOS app bundle 之后、bundle 签名之前,移除 macOS 归档在带扩展属性的文件旁写入的
macos-codesign-sidecar-stripcompanion file,因为签名工具会将这些 companion file 封装为普通 bundle member,而操作系统在文件管理器首次访问应用时就会将其删除。当签名后的 macOS app 在构建机上可正常启动,但用户被提示应用已损坏且无法打开;或签名验证报告缺少 sealed resource 时,使用本条。._* - —— 客观评估并降低桌面 JVM 应用的内存占用——查看 heap-to-footprint ratio,而非仅看 resident 数值;执行能够区分 memory leak 与 allocator 持有空闲页面的实验;理解 per-thread allocator arena 为何使 footprint 取决于用户的 CPU core 数量。当桌面应用稳定在约 1 GB 附近,或在长时间会话中持续增长;高配置机器用户报告的内存占用远差于本地复现;或准备调整 allocator 或 garbage-collector flag 之前,使用本条。
jvm-desktop-memory-footprint - —— 在 JVM desktop app 中嵌入原生 C media engine——每个 media item 使用一个 handle、专用 event-pump thread、正确顺序下 pin output driver 并创建 render context、将所有 property 写入限制在同时负责释放 handle 的线程中、对可选 engine option 进行 feature detection,并按 engine 解析数字的方式格式化数值。当 app 在设置 property 时停止;engine 自行打开窗口;或 app 明确设置的 value 被静默忽略时,查阅本条。
embed-media-engine-desktop - —— 在 JVM desktop 上通过 media engine 的 edit-list URL 形式,将独立的 audio-only stream 和 video-only stream 作为 ONE source 播放——这是 merging media source 的 desktop 对应方案——其中包括对 stream URL 进行 length-prefixed quoting,以及为何不能在 crossfade 中间准备一个合并后的双 URL item。当 desktop video 播放时完全静音且日志中没有任何信息;stream URL 在第一个分号处被截断;或淡入 video track 时导致正在退出的歌曲被过早截断时,查阅本条。
merge-split-av-streams-desktop - —— 在 Compose Desktop 中渲染来自原生 media engine 的 video frame,而不嵌入 heavyweight AWT panel——将完成的 frame 作为 StateFlow 上的 immutable snapshot 发布,并通过普通 Image 绘制;在 UI thread 之外执行转换;匹配 engine 的 pixel byte order;由 engine 决定 fit。当 embedded video 无论 z-order 如何都覆盖所有内容;滚动时落后一帧;第二块屏幕显示 player 的瞬间,第一块屏幕变黑;red 和 blue 互换;或出现任何 content-scale 都无法去除的 black bar 时,查阅本条。
compose-desktop-video-no-swingpanel - —— 通过统一 facade 将 desktop app 接入各 OS 的 system now-playing 和 transport surface,确保 native initialization 失败时只禁用 integration,绝不导致 playback 中断。内容包括:在 packaged app 内于 platform 主线程初始化;通过 JVM 的 native-access layer 访问 OS media framework;对交给 OS 的 callback 保持 strong reference;将 integration 限制在专属 thread 中。当 media key 或 system now-playing panel 在普通 Gradle run 下正常,但在 packaged app 中无效;只显示 app name 而不显示 track title;或 transport callback 运行一段时间后停止到达时,使用本条。
desktop-system-media-integration - —— 当 bug 位于无法修补的 library 中时,workaround 应采用的结构——尽可能追踪实际触及的 mechanism;明确列出接受的 trade-off;保留 escape hatch,使失败时降级而非静默;写明可移除 workaround 的明确条件;记录为何你的 usage pattern 会暴露 library 自身用户从未遇到的 bug。当因他人的 bug 而准备 pin setting、避开 code path 或增加 defensive branch;或审查未在注释中说明何时可以删除的 workaround 时,使用本条。
upstream-lib-bug-workaround-template - —— 在 packaged macOS app 的 property list 中声明 Launch Services environment dictionary(
macos-lsenvironment-path-pin),会把进程LSEnvironment固定到四个基础 system directory,导致 app 启动的每个 external process 都失去安装在其他位置的命令。向 macOS app bundle 添加任何 environment variable 之前;app shell out 调用的 helper 仅对已安装用户报告“command not found”;或某功能从 terminal launch 启动时正常,但从 Dock 启动后静默无效时,使用本条。PATH
Packaging, signing, CI and build
打包、签名、CI 与构建
- — Packaging a JVM desktop app with a config-driven packager whose HOCON config silently ignores unknown keys — which keys bind at the app level versus a per-OS section versus a nested group, command-line key overrides, pinning the packaging JDK, and the environment block that quietly pins PATH. Reach for it when a key you wrote is having no effect on the built installer and nothing in the build log complains.
conveyor-desktop-packaging - — Verifying a config-driven feature against the generated artifact instead of against the config, for formats that fail open and ignore unknown keys — the two-build A/B diff, the artifact fingerprint to grep for, and the CI assertion that keeps it from regressing. Reach for it when a config key looks correct, the build is green, and the feature it configures has simply never been observed working.
config-fails-open-verify-artifact - — Wiring a custom URL scheme end to end on a JVM desktop app — per-OS registration, the argument filter at startup, single-instance forwarding, and delivering a callback's token to app state. Reach for it when clicking a link or returning from a browser redirect merely brings the app to the front and the flow it was supposed to complete just sits there.
desktop-deep-link-plumbing - — Running a bytecode shrinker over a JVM desktop app — which optimization families must stay off and why, why obfuscation breaks the rendering and reflection layers, the keep-rule families a native-binding plus coroutines plus HTTP-client app needs, and how to feed the shrunk jars to the packager. Reach for it when the release build starts with a verification error, renders a see-through or blank surface, or fails only in the packaged installer while the development run is fine.
r8-proguard-desktop-survival - — Diagnosing Gradle failures of the form "cannot mutate a configuration after its child configuration was resolved" — why the message names the configuration you touched rather than the plugin that resolved it, how to bisect plugins against a minimal working template, and which fixes are documented dead ends. Reach for it when adding a perfectly ordinary dependency line makes the build refuse to configure, and rewriting that line every possible way changes nothing.
gradle-config-resolved-too-early - — Handling a transitive dependency whose strict version constraint overrides the version you chose, dragging a shared lower-level library up or down for the whole build — how to find who pinned what, when to force a version back versus align everything with the pin, and how to document a pin so nobody upgrades it back into the breakage. Reach for it when adding one unrelated library produces a missing-method failure at runtime, inside a rendering pass, in a component you did not touch.
transitive-version-pinning - — Ship prebuilt native libraries to a multiplatform desktop build by splitting bundling into two Gradle tasks with different homes — a dev-machine task that builds the slices, packs tarballs and prints their digests, and a CI task that only downloads and verifies against digests pinned in the build file; reach for it when CI needs a native toolchain it should not have, when a native bump quietly ships the previous binaries, or when the packaged installer launches with the native missing entirely.
reproducible-native-bundling-two-tasks - — Detect that a desktop app is running inside a virtual machine on Windows after the classic command-line management query tool was removed in Windows 11 — query the management layer through PowerShell, probe both the manufacturer and the model field, and pick the fail direction deliberately; reach for it when a transparent or undecorated window renders nothing on a VM while the process keeps running, or when a detection probe that worked for years suddenly reports empty on every modern host.
windows-vm-detection-post-wmic - — Ship a self-signed MSIX (the modern Windows application package format) that end users can actually install without a hosted update site — bundle an install script plus the signing certificate beside the package, script the trust-then-sideload steps, and keep the signing key stable across releases; reach for it when double-clicking the packager's output fails, when its wrapper installer dies fetching a URL that returns 404, or when a new build refuses to install over the previous one.
windows-msix-offline-installer - — Audit every native dependency for a slice on a CPU architecture before promising that target in a multiplatform desktop build — one missing native takes the whole target down at first use rather than at build time, so make the audit a repeatable command over the resolved artifacts and re-run it on every dependency bump; reach for it when deciding whether to add an ARM64 target, or when a build that packaged and installed cleanly dies the first time it touches the database, the renderer or the media layer.
arm64-native-gap-audit - — Find and fix CI steps that only ever passed by timing luck — an asynchronous detach of a same-name mounted volume colliding with the next iteration's mount, and a downloader that quietly saves an error page as the artifact; reach for it when a step that ran green for months starts failing after a runner image update, or when a job succeeds and something minutes later fails on a corrupt or empty file it was handed.
ci-flaky-timing-luck - — Structure a GitHub Actions release pipeline for a multiplatform desktop app so one Linux runner cross-builds every platform's artifacts and a second, tiny macOS job does only the one step that genuinely requires macOS — with artifact handoff between them and no compilation on the costly runner; reach for it when your release workflow runs three OS jobs that each rebuild the world, or when macOS users hit a hard block dialog on an app the pipeline signed correctly.
github-actions-multiplatform-release - — Wire build-time configuration into a Kotlin Multiplatform app with BuildKonfig — secrets read from an untracked local properties file and injected as generated constants, with the no-secrets branch getting empty strings so the feature disables itself instead of failing the build, plus the task-dependency wiring newer Gradle demands for generated sources; reach for it when common code needs a compile-time constant, when an open-source build must not carry credentials, or when a build fails on an implicit dependency between a generated-source task and a consumer.
buildkonfig-secrets-flavors
- —— 使用由 config 驱动且 HOCON config 会静默忽略未知 key 的 packager 对 JVM desktop app 进行打包——哪些 key 绑定在 app level、per-OS section 或 nested group;command-line key override;固定 packaging JDK;以及会悄悄固定 PATH 的 environment block。当你编写的 key 对生成的 installer 没有作用,且 build log 没有任何报错时,查阅本条。
conveyor-desktop-packaging - —— 针对 fail open 且忽略未知 key 的格式,应对照生成的 artifact 而非 config 来验证 config-driven feature——使用两次构建的 A/B diff、用于 grep 的 artifact fingerprint,以及防止回归的 CI assertion。当 config key 看起来正确、build 为 green,但它配置的 feature 从未被观察到实际工作时,查阅本条。
config-fails-open-verify-artifact - —— 在 JVM desktop app 中端到端接入 custom URL scheme——per-OS registration、startup 时的 argument filter、single-instance forwarding,以及将 callback 的 token 交付给 app state。当点击 link 或从 browser redirect 返回时仅将 app 切到前台,而本应完成的 flow 一直停滞时,查阅本条。
desktop-deep-link-plumbing - —— 在 JVM desktop app 上运行 bytecode shrinker——哪些 optimization family 必须关闭及其原因;obfuscation 为何会破坏 rendering 和 reflection layer;包含 native binding、coroutines 和 HTTP client 的 app 需要哪些 keep-rule family;以及如何将 shrink 后的 jar 交给 packager。当 release build 启动时出现 verification error;渲染出透明或空白 surface;或仅在 packaged installer 中失败,而 development run 正常时,查阅本条。
r8-proguard-desktop-survival - —— 诊断形式为“cannot mutate a configuration after its child configuration was resolved”的 Gradle failure——为什么错误信息指向你修改的 configuration,而不是实际触发 resolve 的 plugin;如何以最小可用 template 对 plugin 进行 bisect;哪些修复已被记录为 dead end。当添加一条再普通不过的 dependency line 却让 build 拒绝 configure,而且无论怎样重写该行都毫无变化时,查阅本条。
gradle-config-resolved-too-early - —— 处理某个 transitive dependency:它的 strict version constraint 会覆盖你选择的 version,并把共享的 lower-level library 为整个 build 向上或向下拉動——包括如何找出是谁 pin 了什么、何时应 force version 回去而不是让所有依赖与 pin 对齐,以及如何记录 pin,避免有人再次升级并重新引入故障。当添加一个看似无关的 library 后,runtime 在你未改动的 component 中、某次 rendering pass 里出现 missing-method failure 时,查阅本条。
transitive-version-pinning - —— 将预构建 native library 交付给 multiplatform desktop build:把 bundling 拆成两个 homes 不同的 Gradle task——dev-machine task 负责构建 slice、打包 tarball 并输出 digest;CI task 只负责下载,并对照 build file 中固定的 digest 进行验证。当 CI 实际并不需要却要求安装 native toolchain;native 升级后仍悄悄发布旧 binary;或 packaged installer 启动时完全缺少 native component 时,查阅本条。
reproducible-native-bundling-two-tasks - —— 在 Windows 11 移除经典 command-line management query tool 后,检测 desktop app 是否运行在 virtual machine 中——通过 PowerShell 查询 management layer,同时探测 manufacturer 和 model field,并明确选择 fail direction。当 VM 上 transparent 或 undecorated window 什么都不渲染但 process 持续运行;或多年正常的 detection probe 在所有 modern host 上突然都返回空值时,查阅本条。
windows-vm-detection-post-wmic - —— 发布 self-signed MSIX(现代 Windows application package format),让 end user 无需 hosted update site 也能安装——在 package 旁附带 install script 和 signing certificate,用脚本执行 trust-then-sideload 流程,并在各 release 之间保持 signing key 稳定。当双击 packager 输出失败;wrapper installer 获取返回 404 的 URL 时崩溃;或新 build 无法覆盖安装旧版本时,查阅本条。
windows-msix-offline-installer - —— 在承诺支持某个 CPU architecture target 之前,审计 multiplatform desktop build 中每个 native dependency 是否有对应 slice——一个缺失的 native component 往往不会在 build time 导致失败,而是在首次使用时拖垮整个 target;因此应对 resolved artifact 执行可重复的 audit command,并在每次 dependency bump 后重新运行。当决定是否增加 ARM64 target;或 build 已成功打包和安装,却在首次访问 database、renderer 或 media layer 时崩溃时,查阅本条。
arm64-native-gap-audit - —— 查找并修复那些只是靠 timing luck 才通过的 CI step——例如异步 detach 同名 mounted volume 时与下一轮 mount 冲突,以及 downloader 悄悄把 error page 当作 artifact 保存。当某个连续数月 green 的 step 在 runner image 更新后开始失败;或 job 显示成功,但几分钟后下游因收到 corrupt 或 empty file 而失败时,查阅本条。
ci-flaky-timing-luck - —— 为 multiplatform desktop app 设计 GitHub Actions release pipeline:让一个 Linux runner cross-build 所有 platform 的 artifact,再用一个精简的 macOS job 只执行真正必须依赖 macOS 的步骤;两者之间通过 artifact handoff,并且不在昂贵的 runner 上执行 compilation。当 release workflow 运行三个 OS job,且每个都重新构建整个项目;或 macOS 用户打开 pipeline 已正确签名的 app 时出现 hard block dialog 时,查阅本条。
github-actions-multiplatform-release - —— 使用 BuildKonfig 将 build-time configuration 接入 Kotlin Multiplatform app——secret 从未纳入版本控制的 local properties file 读取,并作为 generated constant 注入;无 secret 分支获得空字符串,使功能自行禁用而不是让 build 失败;此外还包括新版 Gradle 对 generated source 要求的 task-dependency wiring。当 common code 需要 compile-time constant;open-source build 不得携带 credential;或 build 因 generated-source task 与 consumer 之间的 implicit dependency 而失败时,查阅本条。
buildkonfig-secrets-flavors