Skip to content

Commit 76a087b

Browse files
authored
fix(windows): selfContained detection false positive from subos shim path (#301)
On Windows, shims in subos/current/bin/ are hardlinks or copies (not symlinks like Unix). get_executable_path() returns the shim path, and the parent-of-parent subos directory has both .xlings.json and bin/xlings.exe — falsely matching the selfContained detection pattern. This caused homeDir to be set to the subos directory instead of ~/.xlings, breaking all subsequent operations (versions DB, workspace, shim creation paths). Fix: use `if constexpr (platform::OS_NAME == "windows")` to add a compile-time guarded content check — verify that the candidate .xlings.json contains both "version" and "activeSubos" fields (written by `self install`). Subos .xlings.json only has {"workspace":{}}, so the dual-field check reliably distinguishes the two cases. Linux/macOS pay zero overhead since shim symlinks resolve correctly. Also replaces the #ifdef _WIN32 for binary name detection with a constexpr ternary on platform::OS_NAME, converging platform branching into the platform module. Bump version to 0.4.39.
1 parent 37b2cdc commit 76a087b

2 files changed

Lines changed: 153 additions & 7 deletions

File tree

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
# Windows selfContained 检测误判 Bug 分析与修复
2+
3+
**日期**: 2026-05-22
4+
**状态**: Fix in progress
5+
**关联代码**: `src/core/config.cppm:400-418`
6+
**影响版本**: 0.4.38 及之前所有版本
7+
**影响平台**: Windows(Linux/macOS 不受影响)
8+
9+
---
10+
11+
## 1. 问题描述
12+
13+
在 Windows 上,当 xlings 从 `~/.xlings/subos/current/bin/xlings.exe`(PATH 中的标准位置)运行时,selfContained 检测误触发,导致 `homeDir` 被错误地设置为 `~/.xlings/subos/current/` 而不是 `~/.xlings/`
14+
15+
**表现**`xlings install <pkg> -y -g` 成功完成,但安装的包无法通过命令行找到(`The term 'xxx' is not recognized`)。
16+
17+
**发现场景**:mcpp 项目 CI(GitHub Actions Windows runner)中 `xlings install mcpp -y -g``mcpp --version` 报 command not found。
18+
19+
## 2. 根因分析
20+
21+
### 2.1 selfContained 检测逻辑
22+
23+
`config.cppm:404-418` 的 selfContained 检测用于支持"免安装/便携版"场景——解压发布包后直接运行,不需要 `self install`
24+
25+
```cpp
26+
auto exePath = platform::get_executable_path();
27+
auto exeParent = exePath.parent_path(); // bin/
28+
auto candidate = exeParent.parent_path(); // 发布包根目录
29+
auto hasRootConfig = fs::exists(candidate / ".xlings.json");
30+
auto hasRootBin = fs::exists(candidate / "bin" / "xlings.exe");
31+
if (hasRootConfig && hasRootBin) {
32+
paths_.homeDir = candidate; // ← 把发布包根目录当 home
33+
paths_.selfContained = true;
34+
}
35+
```
36+
37+
### 2.2 正常安装后的目录结构
38+
39+
```
40+
~/.xlings/ ← 真正的 homeDir
41+
├── bin/xlings.exe ← 主 binary
42+
├── .xlings.json ← home 配置(含 version, mirror, xim 等)
43+
├── subos/
44+
│ ├── default/
45+
│ │ ├── bin/xlings.exe ← shim (hardlink/copy on Windows)
46+
│ │ └── .xlings.json ← subos 配置 {"workspace":{}}
47+
│ └── current → default ← junction (mklink /J)
48+
```
49+
50+
### 2.3 误判原因
51+
52+
`~/.xlings/subos/current/bin/xlings.exe` 运行时:
53+
54+
| 检测步骤 || 结果 |
55+
|----------|-----|------|
56+
| `exePath` | `~/.xlings/subos/current/bin/xlings.exe` | |
57+
| `candidate` | `~/.xlings/subos/current/` | |
58+
| `candidate/.xlings.json` | `{"workspace":{}}` (subos 配置) | ✅ 存在 |
59+
| `candidate/bin/xlings.exe` | shim 自己 | ✅ 存在 |
60+
| **判定** | **selfContained = true** | **❌ 误判** |
61+
62+
`homeDir` = `~/.xlings/subos/current/`(错误)
63+
64+
### 2.4 为什么只影响 Windows
65+
66+
- **Linux/macOS**: shim 通过 **symlink** 创建。`get_executable_path()` 解析 symlink 返回真实路径 `~/.xlings/bin/xlings`,candidate 为 `~/.xlings`(正确的 home)
67+
- **Windows**: 不支持 symlink(需管理员权限),shim 通过 **hardlink 或 copy** 创建。`get_executable_path()` 返回 shim 自身路径,candidate 为 `~/.xlings/subos/current/`(subos 目录)
68+
69+
### 2.5 影响链
70+
71+
homeDir 错误导致一系列连锁问题:
72+
73+
1. `dataDir` = `subos/current/data`(不是 `~/.xlings/data`
74+
2. 读取的配置是 subos 的 `{"workspace":{}}`,丢失所有全局配置
75+
3. versions/workspace 写入位置错误
76+
4. shim 创建路径错误——新安装的包在 PATH 中找不到
77+
78+
## 3. 修复方案
79+
80+
### 方案 A(采用):`if constexpr` 编译期分支 + `.xlings.json` 内容双字段检测
81+
82+
Linux/macOS 上 shim 是 symlink,不存在此问题,额外的 JSON 解析是不必要的开销。
83+
使用 `if constexpr (platform::OS_NAME == "windows")` 做编译期分支,仅在
84+
Windows 上执行内容检查。同时消除了 `#ifdef _WIN32` 宏,将平台差异收敛到
85+
`platform` 模块的 `OS_NAME` 常量。
86+
87+
真正的 home `.xlings.json` 同时包含 `version``activeSubos` 字段(由
88+
`self install` 写入),而 subos 的 `.xlings.json` 只有 `{"workspace":{}}`
89+
双字段检查避免了未来 subos config 增加单个字段导致再次误判。
90+
91+
```cpp
92+
constexpr auto kBinName = (platform::OS_NAME == "windows")
93+
? "bin/xlings.exe" : "bin/xlings";
94+
auto hasRootBin = !exePath.empty() && fs::exists(candidate / kBinName);
95+
96+
bool isSelfContained = hasRootConfig && hasRootBin;
97+
if constexpr (platform::OS_NAME == "windows") {
98+
if (isSelfContained) {
99+
try {
100+
auto cfg = platform::read_file_to_string(
101+
(candidate / ".xlings.json").string());
102+
auto j = nlohmann::json::parse(cfg, nullptr, false);
103+
isSelfContained = !j.is_discarded()
104+
&& j.contains("version")
105+
&& j.contains("activeSubos");
106+
} catch (...) { isSelfContained = false; }
107+
}
108+
}
109+
```
110+
111+
**优点**
112+
- Linux/macOS 零开销(编译期排除)
113+
- 双字段检查更健壮,不易被未来 schema 变更击穿
114+
- 消除 `#ifdef` 宏,平台差异由 `platform::OS_NAME` 统一管理
115+
116+
### 备选方案(未采用)
117+
118+
- **方案 B**:排除路径中包含 `subos/` 的 candidate — 依赖路径命名约定,脆弱
119+
- **方案 C**:用标记文件 `.xlings-selfcontained` — 需要修改发布包构建流程,改动大
120+
- **方案 D**:Windows shim 中嵌入原始路径 — 改动 shim 机制,影响面大
121+
122+
## 4. 验证
123+
124+
修复后需验证:
125+
1. Windows CI:`xlings install <pkg> -y -g` 后包可正常运行
126+
2. selfContained 模式:从解压目录直接运行仍然正常
127+
3. Linux/macOS:行为无变化

src/core/config.cppm

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import xlings.core.xvm.db;
1313
namespace xlings {
1414

1515
export struct Info {
16-
static constexpr std::string_view VERSION = "0.4.38";
16+
static constexpr std::string_view VERSION = "0.4.39";
1717
static constexpr std::string_view REPO = "https://github.com/openxlings/xlings";
1818
};
1919

@@ -405,12 +405,31 @@ private:
405405
auto exeParent = exePath.parent_path();
406406
auto candidate = exeParent.parent_path();
407407
auto hasRootConfig = !exePath.empty() && fs::exists(candidate / ".xlings.json");
408-
#ifdef _WIN32
409-
auto hasRootBin = !exePath.empty() && fs::exists(candidate / "bin" / "xlings.exe");
410-
#else
411-
auto hasRootBin = !exePath.empty() && fs::exists(candidate / "bin" / "xlings");
412-
#endif
413-
if (hasRootConfig && hasRootBin) {
408+
constexpr auto kBinName = (platform::OS_NAME == "windows")
409+
? "bin/xlings.exe" : "bin/xlings";
410+
auto hasRootBin = !exePath.empty() && fs::exists(candidate / kBinName);
411+
412+
bool isSelfContained = hasRootConfig && hasRootBin;
413+
if constexpr (platform::OS_NAME == "windows") {
414+
// Windows: shims are hardlinks/copies (not symlinks), so
415+
// get_executable_path() returns the shim path inside a subos
416+
// dir (e.g. ~/.xlings/subos/current/bin/xlings.exe). That
417+
// directory has both .xlings.json and bin/xlings.exe, falsely
418+
// matching the selfContained pattern. Disambiguate by checking
419+
// json content: real home/selfContained config always has both
420+
// "version" and "activeSubos"; subos config only has workspace.
421+
if (isSelfContained) {
422+
try {
423+
auto cfg = platform::read_file_to_string(
424+
(candidate / ".xlings.json").string());
425+
auto j = nlohmann::json::parse(cfg, nullptr, false);
426+
isSelfContained = !j.is_discarded()
427+
&& j.contains("version")
428+
&& j.contains("activeSubos");
429+
} catch (...) { isSelfContained = false; }
430+
}
431+
}
432+
if (isSelfContained) {
414433
paths_.homeDir = candidate;
415434
paths_.selfContained = true;
416435
} else {

0 commit comments

Comments
 (0)