- 桌面应用
- 开发工具
【免费下载链接】MooTool
Handy tool set for developers
环境变量工具(F08)是 MooTool 三款桌面产品线共有的核心系统工具,本文以next-compose/docs/fixtures/electron-next-envTools-vitest.md这份对照登记表为主干,系统梳理MooTool Next Compose如何把 Electron 版的「导出分区、previewDiff、环境文件更新、权限拒绝保持原文件、会话恢复」逐项搬进 Compose 桌面产品,并以单测(EnvEngineTest/EnvSessionMetadataTest/VariablesSessionRestoreTest)锁定行为。读完本文,你可以完整掌握 Compose 版环境变量引擎的存储路径、shell 钩子、原子写入、提权策略、导出格式与会话 wire 的来龙去脉,也能直接看懂这份对照表的每一行对应仓库中的哪段实现与测试。
一、这份对照表在讲什么
electron-next-envTools-vitest.md是next-compose/docs/fixtures/目录下的一张「Electron ↔ Compose 验证对照登记表」。它的作用不是复述 Electron 代码,而是给出一个可审计的契约:每一类行为,在 Electron 源文件里是什么、在 Compose 里由哪个测试证明等价。表内四条已落地登记 + 一条会话登记如下:
| caseId | sourceProduct | sourceFile | Compose 验证 |
|---|---|---|---|
| export-sections | MooTool Next Electron | next/src/features/variables/VariablesTool.tsxformatEnvironment | EnvEngineTest.formatExportUsesElectronSectionHeaders(EnvExportSections,运行时区为 Compose 产品名) |
| preview-diff | 同上编辑对话框 | variables.diff+ 保存前摘要 | EnvEngineTest.previewDiffDescribesScopeFileAndDelete |
| env-file-update | 同上 | next/src/shared/systemService.test.ts | EnvEngineTest.updatesEnvironmentFileLikeElectronSystemServiceFixture(见 electron-next-systemService-vitest.md) |
| permission-deny | Compose 行为 | — | EnvEngineTest.systemWriteWithoutElevationKeepsOriginalFile |
| session-restore | Compose 会话 | tab/scope/query wire | EnvSessionMetadataTest/VariablesSessionRestoreTest(DIFF-535) |
末尾还登记了两个 diff 切片:DIFF-534(导出分区 + previewDiff 回归)与 DIFF-535(会话 metadata + 恢复)。也就是说,这张表就是 F08 在「Electron 参照 → Compose 实现 → 单测锁定」这条链路中的索引页。
二、先看 Electron 参照端:导出格式与系统服务
对照表前三行的 sourceFile 都指向 Electron 侧,理解它们才能看懂 Compose 对齐了什么。
2.1formatEnvironment的四段导出分区
Electron 的变量工具页在 VariablesTool.tsx 中定义formatEnvironment(snapshot):把快照按「持久用户 → 持久系统 → 当前进程 → Electron runtime」四个分区拼成一个文本,每个分区用一行------------标题分隔,条目格式为key=value:
function formatEnvironment(snapshot: EnvironmentSnapshot): string { return [ '------------Persistent user environment---------------', ...snapshot.user.map((entry) => `${entry.key}=${entry.value}`), '', '------------Persistent system environment---------------', ...snapshot.system.map((entry) => `${entry.key}=${entry.value}`), '', '------------Current process environment---------------', ...snapshot.environment.map((entry) => `${entry.key}=${entry.value}`), '', '------------Electron runtime---------------', ...snapshot.runtime.map((entry) => `${entry.key}=${entry.value}`) ].join('\n') }exportValues()在 VariablesTool.tsx 通过window.mootool.saveTextFile把这段内容落盘,默认文件名为mootool-environment.txt。这就是 Compose 端EnvExportSections要对齐的「分区标题」。
2.2systemService.test.ts的环境文件更新链
对照表的env-file-update行指向 systemService.test.ts。该用例给定一段含注释、export JAVA_HOME='/old jdk'、OTHER="value"的文本,验证三件事:
updateEnvironmentContent(source, 'JAVA_HOME', "/opt/jdk's", true)把单引号值安全转义为'/opt/jdk'\''s',注释与其他行原样保留;parseEnvironmentContent能解析回JAVA_HOME=/opt/jdk's与OTHER=value;- 传入
value === undefined时删除该键,得到# keep this comment\nOTHER="value"\n。
这些纯函数位于 systemService.ts,配套的 IPC 边界校验normalizeEnvironmentVariableInput/normalizeDeleteEnvironmentVariableInput(同样在该测试文件中)负责在进入主进程前拒绝非法键名(如BAD-NAME)与含换行的非法值——两个产品线在这一层保持了同一套安全约束。
三、Compose 侧核心实现:EnvEngine 与 EnvExportSections
Compose 版把这些行为收敛进一个可单测的纯 Kotlin 引擎 EnvEngine.kt,配套类型EnvTab/EnvDisplayScope/EnvPersistScope/EnvErrorCode、快照与写入结果均在同一文件。
3.1 产品独立的路径与 shell 钩子
EnvStoreConfig.production(directories)(EnvEngine.kt)决定各平台路径:
| 平台 | 用户文件 | 系统文件 | shell profile |
|---|---|---|---|
| macOS | 数据目录data/environment | /etc/zshenv | ~/.zshenv |
| Linux | 数据目录data/environment | /etc/environment | ~/.profile |
| Windows | 数据目录data/environment | 空(走注册表) | ~/.unused-compose-env-profile |
数据目录来自AppDirectories:macOS 为~/Library/Application Support/com.rememberber.mootool.next.compose/data,Linux 走 XDG 数据目录,Windows 为%APPDATA%/MooToolNextCompose/data(见 AppPaths.kt)。这一点与 Electron 有本质区别:Electron 写~/.MooTool/environment并在 profile 里插入# >>> MooTool environment >>>钩子;Compose 明确不读写~/.MooTool,其 shell 钩子常量是隔离标记:
const val SHELL_HOOK_BEGIN = "# >>> MooTool Next Compose environment >>>" const val SHELL_HOOK_END = "# <<< MooTool Next Compose environment <<<"ensureShellHook(EnvEngine.kt)在 profile 里追加:
# >>> MooTool Next Compose environment >>> [ -f '<userFile>' ] && . '<userFile>' # <<< MooTool Next Compose environment <<<正因为标记不同,两个产品线可以同时安装、互不覆盖。这一设计决策的完整理由见 DIFF-016:产品线独立要求自有路径与标记,规格禁止用进程 Map 冒充系统设置。
3.2 解析、序列化与安全校验
EnvEngine.parseContent用正则^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$匹配赋值行,unquote还原单双引号('\''转义、\"与\\反转义),未知行一律跳过但不删除。updateContent逐行扫描,把目标键的所有赋值替换为一条新的序列化行,其他行(含注释、非赋值文本)原样保留;若键不存在则追加,最后统一以\n结尾(EnvEngine.kt)。
序列化分两种模式(EnvEngine.kt):
// shellExport = true(用户作用域,或 macOS 的系统作用域) "export $key='${value.replace("'", "'\\''")}'" // 否则 "$key=\"${value.replace("\\", "\\\\").replace("\"", "\\\"")}\""校验规则与 Electron 完全一致:键名必须匹配^[A-Za-z_][A-Za-z0-9_]*$;值长度上限 65535,且不得包含\0、\r、\n(validateName/validateValue,EnvEngine.kt)。非法输入抛EnvException并携带EnvErrorCode(INVALID_NAME/INVALID_VALUE等)。
3.3 导出分区与运行时信息
EnvExportSections(EnvEngine.kt)把四个分区标题抽成常量,其中前三个标题与 ElectronformatEnvironment逐字相同,第四个刻意不同:
object EnvExportSections { const val USER = "------------Persistent user environment---------------" const val SYSTEM = "------------Persistent system environment---------------" const val PROCESS = "------------Current process environment---------------" const val RUNTIME_COMPOSE = "------------Compose runtime---------------" }formatExport(EnvEngine.kt)按 user → system → process → runtime 顺序输出。runtime 分区由runtimeEntries(EnvEngine.kt)生成:先是mootool.product(ProductIdentity.DISPLAY_NAME= "MooTool Next Compose",见 ProductIdentity.kt)、mootool.version、mootool.id,再是 JVM 与 OS 属性(java.version、os.name、user.home、file.encoding等)。Electron 的 runtime 区则显示process.versions.electron/chrome/node与process.cwd(见 systemService.ts)。Compose 没有 Electron 运行时字段,所以用产品名/JVM/OS 代替——这正是 DIFF-016 强调的「Electron 运行时字段在 JVM 桌面产品中没有对应物」。
3.4 写入流程:diff → 备份 → 原子写入 → 提权
EnvEngine.set/delete收敛到私有write(EnvEngine.kt),完整链路是:
- 校验键名与值;
snapshot(config)取写入前快照,previewDiff生成「scope + 目标文件 + 旧值 → 新值/删除」摘要;- 备份:若目标文件已存在,先复制到
data/backups/environment/environment-<scope>-<UTC 时间戳>.bak(backupIfPresent); - 更新内容后以
tmp-<pid>临时文件 +ATOMIC_MOVE原子落盘;用户文件写完后把权限收窄为仅 owner 可读写(atomicWrite); - 用户作用域:追加/校验 shell 钩子;macOS 额外尝试
launchctl setenv/unsetenv(失败静默忽略); - 系统作用域:直接写,若抛权限类 IOException 且允许提权,则走
writeElevated——macOS 用osascript do shell script ... with administrator privileges,Linux 用pkexec复制并chmod 644(EnvEngine.kt);若allowElevation = false,则抛出EnvErrorCode.PERMISSION; - 返回写入后快照 + 备份路径 + diff。
注意第 4 步的关键差异:Compose 绝不改写当前 JVM 的System.getenv()。Electron 在写完后会通过syncCurrentProcessEnvironment把process.env[key]原地更新(systemService.ts),Compose 则明确拒绝这种行为,理由见 DIFF-016:「规格禁止用进程 Map 冒充系统设置」。用户保存的用户/系统变量只在重新登录、新终端或已加载 Compose shell 钩子的会话中生效。
Windows 分支单独走 PowerShell:用户作用域[Environment]::SetEnvironmentVariable(...,[EnvironmentVariableTarget]::User),系统作用域通过Start-Process -Verb RunAs提权;读取则用GetEnvironmentVariables+ Sort + UTF-8 输出解析(readWindows/writeWindows,EnvEngine.kt)。该分支在本机(Intel macOS)未实测,属于已知限制。
四、四条对照登记逐一拆解
下面把对照表每一行映射到 Compose 单测 EnvEngineTest.kt 的具体断言,证明「登记 ≠ 口头承诺,测试确实锁住了行为」。
4.1 export-sections:formatExportUsesElectronSectionHeaders
EnvEngineTest.kt 构造一个含 user/system/process/runtime 四项的快照,断言EnvEngine.formatExport输出依次包含EnvExportSections.USER、SYSTEM、PROCESS、RUNTIME_COMPOSE,条目呈现为U=1、PATH=/bin,并且不包含"Electron runtime"。这条测试锁死两件事:分区标题与 Electron 对齐(前三段逐字一致),以及运行时区必须使用 Compose 产品名而非 Electron 字样。
4.2 preview-diff:previewDiffDescribesScopeFileAndDelete
EnvEngineTest.kt 验证EnvEngine.previewDiff的输出结构:更新时包含作用域名(user)、目标文件(/data/environment)、旧值(FOO: bar)与箭头(→ baz);删除一个不存在的键时输出MISSING: (absent)与→ (delete)。该摘要正是VariablesScreen编辑对话框里variables.diff区域展示的保存前预览(见 VariablesScreen.kt),对应 Electron 编辑对话框中同样的variables.diff+ 保存前摘要。
4.3 env-file-update:updatesEnvironmentFileLikeElectronSystemServiceFixture
EnvEngineTest.kt 使用与 ElectronsystemService.test.ts完全相同的输入文本(# keep this comment+export JAVA_HOME='/old jdk'+OTHER="value"),验证:
- 更新含单引号的值时,输出
export JAVA_HOME='/opt/jdk'\''s',注释与其他行原样保留; EnvEngine.parseContent可解析回两个条目;- 以
value = null调用时删除该键,保留注释。
updatesEnvironmentFileLikeElectronSystemServiceFixture这个名字直接表明它是「对齐 Electron fixture」的回归测试;对照表的env-file-update行同时挂到 electron-next-systemService-vitest.md,后者把env-update一行也登记为同一个测试。配套用例parsesAndUpdatesWhileKeepingUnknownLines(EnvEngineTest.kt)还覆盖了重复赋值取最后一个、非赋值文本保留、单引号值往返、非法键名1ABC抛INVALID_NAME、含换行的值抛INVALID_VALUE等边界。
userFileRoundTripCreatesBackupAndDoesNotMutateProcessMap(EnvEngineTest.kt)是这条链路的完整闭环:真实写临时文件 → 断言备份文件存在 → 断言.zshenv里含SHELL_HOOK_BEGIN且不含 Electron 标记# >>> MooTool environment >>>→ 断言System.getenv()前后逐项相等(进程 Map 未被改写)→ 断言 runtime 含java.version与mootool.product=…Compose…→ 删除后导出文本含"Compose runtime"且不含"Electron runtime"。
4.4 permission-deny:systemWriteWithoutElevationKeepsOriginalFile
这是表格中sourceProduct标为「Compose 行为」的一行,代表 Compose 自己的新增约束,没有 Electron 参照。测试 EnvEngineTest.kt 构造一个setWritable(false)的锁目录,以allowElevation = false写入系统作用域,断言:抛出EnvException且code == EnvErrorCode.PERMISSION、目标文件不存在(没有创建半成品)、随后snapshot(config).system仍为空(原文件保持原状)。这印证了 DIFF-016 中「系统作用域在无权限时按需提权;拒绝时保持原文件」的实现承诺。
五、会话恢复:session-restore 登记的 tab/scope/query wire
对照表最后一行session-restore不指向 Electron,而是 Compose 自己的会话子系统,登记于 DIFF-535:F08 缺与 F11/F15 同级的可单测 metadata/restore 模块,本轮补上。
5.1 EnvSessionMetadata:wire 规范化
EnvSessionMetadata.kt 定义了持久化到会话 JSON 的 wire 值常量:environment/runtime(tab),user/system/process(scope)。normalizeTab把任何未知输入回退为environment;normalizeScope把未知输入回退为process。测试 EnvSessionMetadataTest.kt 覆盖大小写归一("Runtime"→runtime、"User"→user)与空串回退(""→process)。
5.2 EnvSessionRestore + VariablesSession:重载即恢复
VariablesSession(SessionManager.kt)持有tab/scope/query及 transient 状态(snapshot、loading、saving、error、notice、editor 字段、lastBackup、lastDiff)。EnvSessionRestore.apply(EnvSessionRestore.kt)把 wire 字符串映射回枚举,并重置全部 transient 状态:snapshot = null、loading = false、saving = false、error = ""、notice = ""、editorOpen = false、deleteKey = ""、lastBackup = ""、lastDiff = ""。
会话的读写由SessionManager.variablesSession()/persistVariables()承担:加载时从 store 反序列化VariablesSessionSnapshot(tab/scope/query三个字段,均带默认值),保存时序列化当前状态(SessionManager.kt)。VariablesScreen在切 Tab、切 scope、输入搜索词、增删改时都会调用persist(),确保 UI 状态持续落盘。
测试 VariablesSessionRestoreTest.kt 把 session 置为 Runtime/System/PATH/已载入快照/loading/error,然后restore(VariablesSessionSnapshot("environment", "user", "JAVA")),断言恢复为EnvTab.Environment、EnvDisplayScope.User、query = "JAVA"、snapshot == null、loading == false、error == ""。
5.3 会话链路与安全边界
DIFF-535明确说明 F08 是「无通用历史」工具:不把变量值写入会话 JSON 或通用历史,VariablesSessionSnapshot只含 tab/scope/query,避免密钥类环境变量进入 SQLite 或历史库(见 DIFF-016「受影响范围」与 feature-parity.md 中「variables 不声明通用历史」的注册说明)。相比之下,EnvSnapshot这类临时快照只在内存中刷新(VariablesScreen.refresh走EnvWiringPresentation.runSnapshot,见 EnvWiringPresentation.kt)。
六、从表格到运行:如何查看、构建与验证
本产品是独立的 Compose Multiplatform Desktop 工程(见 README.md),与 Electron 版互不依赖,需 JDK 21。开发运行与测试:
cd next-compose export JAVA_HOME="$(/usr/libexec/java_home -v 21)" # macOS;Linux 请指向已安装的 JDK 21 ./scripts/check-toolchain.sh ./scripts/check-core.sh MOOTOOL_COMPOSE_DATA_DIR="$PWD/local.properties.d/dev-profile" ./scripts/run.sh跑 F08 相关单测(EnvEngineTest、EnvSessionMetadataTest、VariablesSessionRestoreTest):
./gradlew :composeApp:desktopTest --offline测试结果在composeApp/build/reports/tests/desktopTest/index.html可查看。三个 diff 文档各自记录过完整运行结论:F08 验收当轮desktopTest100/100(见 2026-09-09-f08/results.md),DIFF-534 记为841/841,DIFF-535 记为851/851(均为 JDK 21、offline 运行,另有默认跳过的公网 GET/multipart smoke)。这些是开发切片快照而非发布保证,具体以你本地运行结果为准。
界面侧:运行后在工具列表(⌘/Ctrl+K)中进入「环境变量」,顶部 Tab 为 Environment/Runtime,Environment 下有 user/system/process 作用域切换,编辑对话框中展示variables.diff保存前摘要。Compose 导出默认文件名为mootool-next-compose-environment.txt(见 VariablesScreen.kt),区别于 Electron 的mootool-environment.txt。
七、局限与后续切片
从 DIFF-016「受影响范围」与 results.md「未测」可以明确列出本功能的已知边界:
- 新终端不会自动看到用户变量,除非已加载 Compose shell 钩子;macOS 会额外尝试
launchctl setenv(失败忽略); - 系统文件在 macOS 为
/etc/zshenv、Linux 为/etc/environment,但并非所有发行版的唯一全局环境来源; - Windows 用户/系统走 PowerShell 注册表目标,在 Intel macOS 本机未实测;
- 不把变量值写入会话 JSON 或通用历史(防密钥进 SQLite);
- 未做项还包括窗口截图、系统提权对话框的界面验收,以及新终端实际加载钩子的手工验证。
这些边界在后续切片(如 P7 三平台安装验收)中才会逐步收敛,阅读对照表与 DIFF 记录时应一并留意。
八、小结:一份可审计的 parity 登记
electron-next-envTools-vitest.md这类 fixture 的价值在于:它把「Electron 参照、Compose 实现、单测锁定」三件事压进同一张表。EnvEngineTest用与 Electron 相同的 fixture 文本验证解析/序列化兼容,用formatExportUsesElectronSectionHeaders锁定分区标题逐字对齐,用systemWriteWithoutElevationKeepsOriginalFile记录 Compose 自己的权限语义;EnvSessionMetadataTest/VariablesSessionRestoreTest则覆盖会话 wire 与恢复。如果你要为本产品新增功能或回归检查 F08,正确姿势是:先读这张对照表定位 caseId,再打开EnvEngineTest对应的测试方法,最后回看 DIFF-534、DIFF-535 与 DIFF-016 的行为描述。这条从「文档登记 → 测试断言 → diff 决策」的证据链,正是整个 next-compose 切片工程可复现、可审计的研发方式的缩影。
- 桌面应用
- 开发工具
【免费下载链接】MooTool
Handy tool set for developers
相关推荐
MooTool next-compose Cron 工具测试对齐:从 Electron Vitest 到 Compose Kotlin 的 Quartz 语义迁移
MooTool next compose Cron 工具测试对齐:从 Electron Vitest 到 Compose Kotlin 的 Quartz 语义迁
桌面应用开发工具MooTool Compose 版跨产品草稿迁移:Java `t_func_content` 字段对齐与工具会话恢复实战
MooTool Compose 版跨产品草稿迁移:Java t_func_content 字段对齐与工具会话恢复实战 本篇基于 DIFF 153 对齐文档 ht
桌面应用开发工具MooTool Vault Git 面板文案对齐实战:Compose `git.*` 与 Electron `json.git.*` 全量对照解析
MooTool Vault Git 面板文案对齐实战:Compose git. 与 Electron json.git. 全量对照解析 导读 在 MooTool
桌面应用开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考