xt-cli 0.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- __init__.py +5 -0
- __main__.py +7 -0
- cli.py +342 -0
- commands/build_cmd.py +67 -0
- commands/clean_cmd.py +31 -0
- commands/config_cmd.py +202 -0
- commands/deps_cmd.py +128 -0
- commands/fullclean_cmd.py +32 -0
- config.py +356 -0
- constants.py +6 -0
- context.py +238 -0
- dependencies.py +1109 -0
- errors.py +29 -0
- hooks.py +317 -0
- models.py +107 -0
- output.py +16 -0
- paths.py +61 -0
- project.py +28 -0
- xmake.py +92 -0
- xt_cli-0.2.1.dist-info/METADATA +125 -0
- xt_cli-0.2.1.dist-info/RECORD +26 -0
- xt_cli-0.2.1.dist-info/WHEEL +5 -0
- xt_cli-0.2.1.dist-info/entry_points.txt +2 -0
- xt_cli-0.2.1.dist-info/licenses/LICENSE +202 -0
- xt_cli-0.2.1.dist-info/top_level.txt +16 -0
- xt_cli.py +10 -0
commands/deps_cmd.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""deps 命令:管理工程版本依赖。"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
|
|
6
|
+
from cli import register_task
|
|
7
|
+
from constants import PRIORITY_INDEPENDENT
|
|
8
|
+
from models import BuildContext
|
|
9
|
+
from dependencies import (
|
|
10
|
+
_get_entry_version, _load_jsonc, _print_error, _print_info,
|
|
11
|
+
_resolve_repo_path, _save_jsonc, hash_dir,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def add_args_deps(parser: argparse.ArgumentParser) -> None:
|
|
16
|
+
parser.add_argument("--update-deps-require", nargs="?", const="__ALL__", default=None,
|
|
17
|
+
metavar="<name>",
|
|
18
|
+
help="Update require to current HEAD sha")
|
|
19
|
+
parser.add_argument("--hash", metavar="<path>",
|
|
20
|
+
help="Compute SHA-256 hash of a directory")
|
|
21
|
+
parser.epilog = (
|
|
22
|
+
"Examples:\n"
|
|
23
|
+
" xt deps --update-deps-require Update all entries\n"
|
|
24
|
+
" xt deps --update-deps-require=lm620 Update single platform\n"
|
|
25
|
+
" xt deps --update-deps-require=xt-sdk,lm620 Update multiple (comma-separated)\n"
|
|
26
|
+
" xt deps --hash components/my_utils Compute directory hash"
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def handle_deps_command(
|
|
31
|
+
context: BuildContext,
|
|
32
|
+
global_args: argparse.Namespace,
|
|
33
|
+
shared_args: argparse.Namespace,
|
|
34
|
+
self_args: argparse.Namespace,
|
|
35
|
+
unknown_args: list[str],
|
|
36
|
+
) -> int:
|
|
37
|
+
if self_args.update_deps_require is not None:
|
|
38
|
+
if context is None:
|
|
39
|
+
from context import build_context_from_options
|
|
40
|
+
context = build_context_from_options(
|
|
41
|
+
cwd=global_args.project,
|
|
42
|
+
project_dir=global_args.project,
|
|
43
|
+
sdk_dir=global_args.sdk,
|
|
44
|
+
target=global_args.target,
|
|
45
|
+
toolchain_path=global_args.toolchain_path,
|
|
46
|
+
board=global_args.board,
|
|
47
|
+
)
|
|
48
|
+
return _handle_update_require(context, self_args.update_deps_require)
|
|
49
|
+
if self_args.hash is not None:
|
|
50
|
+
print(hash_dir(self_args.hash))
|
|
51
|
+
return 0
|
|
52
|
+
print("Usage: xt deps --update-deps-require[=<name>[,<name>...]]")
|
|
53
|
+
print(" xt deps --hash <path>")
|
|
54
|
+
return 0
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _handle_update_require(context, raw_value: str) -> int:
|
|
58
|
+
deps_path = context.project_dir / "xt_deps.jsonc"
|
|
59
|
+
xt_deps = _load_jsonc(deps_path)
|
|
60
|
+
if xt_deps is None:
|
|
61
|
+
_print_error("xt_deps.jsonc not found")
|
|
62
|
+
return 1
|
|
63
|
+
|
|
64
|
+
if raw_value == "__ALL__":
|
|
65
|
+
names = _collect_entry_names(xt_deps)
|
|
66
|
+
else:
|
|
67
|
+
names = [s.strip() for s in raw_value.split(",") if s.strip()]
|
|
68
|
+
|
|
69
|
+
for name in names:
|
|
70
|
+
entry, repo_path = _resolve_entry_and_path(name, xt_deps, context)
|
|
71
|
+
if entry is None:
|
|
72
|
+
_print_error(f"{name}: not found in xt_deps.jsonc")
|
|
73
|
+
return 1
|
|
74
|
+
if not (repo_path / ".git").exists():
|
|
75
|
+
h = hash_dir(repo_path)
|
|
76
|
+
entry["require"] = {"type": "hash", "val": h}
|
|
77
|
+
_print_info(f"{name}: require → hash {h}")
|
|
78
|
+
else:
|
|
79
|
+
version = _get_entry_version(repo_path)
|
|
80
|
+
entry["require"] = version.split("-")[0]
|
|
81
|
+
_print_info(f"{name}: require → {entry['require']}")
|
|
82
|
+
|
|
83
|
+
_save_jsonc(deps_path, xt_deps)
|
|
84
|
+
return 0
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _collect_entry_names(xt_deps: dict) -> list[str]:
|
|
88
|
+
names: list[str] = []
|
|
89
|
+
if "xt-sdk" in xt_deps:
|
|
90
|
+
names.append("xt-sdk")
|
|
91
|
+
platforms = xt_deps.get("platforms", {})
|
|
92
|
+
if isinstance(platforms, dict):
|
|
93
|
+
names.extend(platforms.keys())
|
|
94
|
+
components = xt_deps.get("components", {})
|
|
95
|
+
if isinstance(components, dict):
|
|
96
|
+
names.extend(components.keys())
|
|
97
|
+
return names
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _resolve_entry_and_path(name: str, xt_deps: dict, context):
|
|
101
|
+
"""解析条目和仓库路径。"""
|
|
102
|
+
if name == "xt-sdk":
|
|
103
|
+
entry = xt_deps.get("xt-sdk")
|
|
104
|
+
if isinstance(entry, dict):
|
|
105
|
+
return entry, context.sdk_dir
|
|
106
|
+
return None, None
|
|
107
|
+
|
|
108
|
+
platforms = xt_deps.get("platforms", {})
|
|
109
|
+
if isinstance(platforms, dict):
|
|
110
|
+
entry = platforms.get(name)
|
|
111
|
+
if isinstance(entry, dict):
|
|
112
|
+
repo_path = _resolve_repo_path(name, entry, context)
|
|
113
|
+
return entry, repo_path
|
|
114
|
+
|
|
115
|
+
components = xt_deps.get("components", {})
|
|
116
|
+
if isinstance(components, dict):
|
|
117
|
+
entry = components.get(name)
|
|
118
|
+
if isinstance(entry, dict):
|
|
119
|
+
from dependencies import _resolve_component_path
|
|
120
|
+
repo_path = _resolve_component_path(name, entry, context)
|
|
121
|
+
return entry, repo_path
|
|
122
|
+
|
|
123
|
+
return None, None
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
register_task("deps", handle_deps_command, independent=True,
|
|
127
|
+
add_parser=add_args_deps, help="Manage project dependencies",
|
|
128
|
+
priority=PRIORITY_INDEPENDENT)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import shutil
|
|
5
|
+
|
|
6
|
+
from cli import register_task
|
|
7
|
+
from hooks import dispatch_hook
|
|
8
|
+
from models import BuildContext
|
|
9
|
+
from output import print_step
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def handle_fullclean_command(context: BuildContext, global_args: argparse.Namespace, shared_args: argparse.Namespace, self_args: argparse.Namespace, unknown_args: list[str]) -> int:
|
|
13
|
+
"""处理 fullclean 命令——直接删除 .xmake 和 .build 目录。"""
|
|
14
|
+
hook_result = dispatch_hook(context, "fullclean", unknown_args, global_args=global_args, shared_args=shared_args, self_args=self_args)
|
|
15
|
+
if hook_result.handled:
|
|
16
|
+
return hook_result.returncode
|
|
17
|
+
|
|
18
|
+
from hooks import run_lifecycle_hook
|
|
19
|
+
|
|
20
|
+
before_ret = run_lifecycle_hook(context, "before_fullclean", unknown_args, global_args, shared_args, self_args)
|
|
21
|
+
if before_ret != 0:
|
|
22
|
+
return before_ret
|
|
23
|
+
|
|
24
|
+
print_step("fullclean")
|
|
25
|
+
shutil.rmtree(context.project_dir / ".xmake", ignore_errors=True)
|
|
26
|
+
shutil.rmtree(context.project_dir / ".build", ignore_errors=True)
|
|
27
|
+
|
|
28
|
+
run_lifecycle_hook(context, "after_fullclean", unknown_args, global_args, shared_args, self_args)
|
|
29
|
+
return 0
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
register_task("fullclean", handle_fullclean_command, independent=False, help="Delete build output directories", priority=10)
|
config.py
ADDED
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import json5
|
|
8
|
+
|
|
9
|
+
from constants import CONFIG_VERSION, DEFAULT_TARGET
|
|
10
|
+
from errors import ConfigError
|
|
11
|
+
from paths import build_global_config_path, normalize_path
|
|
12
|
+
|
|
13
|
+
_LOCAL_CONFIG_FILENAME = "xt_conf.jsonc"
|
|
14
|
+
_TOOLCHAINS_KEY = "toolchains"
|
|
15
|
+
_TOOLCHAIN_PATH_KEY = "toolchain-path"
|
|
16
|
+
_CONFIG_VERSION_KEY = "config_version"
|
|
17
|
+
_SDK_MARKER_RELATIVE_PATH = Path(".xt-sdk-version")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _normalize_config_key(key: str) -> str:
|
|
21
|
+
"""规范化配置键。deps-on-* → deps.on_*,兼容 CLI 标志格式。"""
|
|
22
|
+
if key.startswith("deps-"):
|
|
23
|
+
rest = key[5:]
|
|
24
|
+
return "deps." + rest.replace("-", "_")
|
|
25
|
+
return key
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ConfigStore:
|
|
29
|
+
"""负责配置文件的读写。"""
|
|
30
|
+
|
|
31
|
+
def __init__(self, path: str | Path) -> None:
|
|
32
|
+
self._path = normalize_path(path)
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def path(self) -> Path:
|
|
36
|
+
"""返回配置文件路径。"""
|
|
37
|
+
return self._path
|
|
38
|
+
|
|
39
|
+
def load(self) -> dict[str, Any]:
|
|
40
|
+
"""加载配置文件内容。"""
|
|
41
|
+
raw_text = self._path.read_text(encoding="utf-8")
|
|
42
|
+
data = json5.loads(raw_text)
|
|
43
|
+
if not isinstance(data, dict):
|
|
44
|
+
raise ConfigError("Config data must be a JSON object")
|
|
45
|
+
normalized_data = _ensure_config_version(data)
|
|
46
|
+
if normalized_data != data:
|
|
47
|
+
self.save(normalized_data)
|
|
48
|
+
return normalized_data
|
|
49
|
+
|
|
50
|
+
def save(self, data: dict[str, Any]) -> None:
|
|
51
|
+
"""保存配置文件内容。"""
|
|
52
|
+
self._path.parent.mkdir(parents=True, exist_ok=True)
|
|
53
|
+
normalized_data = _ensure_config_version(data)
|
|
54
|
+
self._path.write_text(
|
|
55
|
+
json.dumps(normalized_data, ensure_ascii=False, indent=2) + "\n",
|
|
56
|
+
encoding="utf-8",
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
def load_or_default(self) -> dict[str, Any]:
|
|
60
|
+
"""加载配置文件,不存在时返回空对象。"""
|
|
61
|
+
if not self._path.exists():
|
|
62
|
+
return {}
|
|
63
|
+
return self.load()
|
|
64
|
+
|
|
65
|
+
def upsert_key(self, key: str, value: Any) -> None:
|
|
66
|
+
"""设置顶层配置项。"""
|
|
67
|
+
data = self.load_or_default()
|
|
68
|
+
data[key] = value
|
|
69
|
+
self.save(data)
|
|
70
|
+
|
|
71
|
+
def upsert_nested_key(self, parent_key: str, child_key: str, value: Any) -> None:
|
|
72
|
+
"""设置嵌套配置项。"""
|
|
73
|
+
data = self.load_or_default()
|
|
74
|
+
child_data = data.get(parent_key)
|
|
75
|
+
if not isinstance(child_data, dict):
|
|
76
|
+
child_data = {}
|
|
77
|
+
child_data[child_key] = value
|
|
78
|
+
data[parent_key] = child_data
|
|
79
|
+
self.save(data)
|
|
80
|
+
|
|
81
|
+
def reset_key(self, key: str) -> bool:
|
|
82
|
+
"""重置指定配置项。"""
|
|
83
|
+
if not self._path.exists():
|
|
84
|
+
return False
|
|
85
|
+
|
|
86
|
+
data = self.load()
|
|
87
|
+
if key not in data:
|
|
88
|
+
return False
|
|
89
|
+
|
|
90
|
+
del data[key]
|
|
91
|
+
if _has_user_config(data):
|
|
92
|
+
self.save(data)
|
|
93
|
+
else:
|
|
94
|
+
self._path.unlink()
|
|
95
|
+
return True
|
|
96
|
+
|
|
97
|
+
def reset_nested_key(self, parent_key: str, child_key: str) -> bool:
|
|
98
|
+
"""重置嵌套配置项。"""
|
|
99
|
+
if not self._path.exists():
|
|
100
|
+
return False
|
|
101
|
+
|
|
102
|
+
data = self.load()
|
|
103
|
+
child_data = data.get(parent_key)
|
|
104
|
+
if not isinstance(child_data, dict) or child_key not in child_data:
|
|
105
|
+
return False
|
|
106
|
+
|
|
107
|
+
del child_data[child_key]
|
|
108
|
+
if child_data:
|
|
109
|
+
data[parent_key] = child_data
|
|
110
|
+
else:
|
|
111
|
+
del data[parent_key]
|
|
112
|
+
|
|
113
|
+
if _has_user_config(data):
|
|
114
|
+
self.save(data)
|
|
115
|
+
else:
|
|
116
|
+
self._path.unlink()
|
|
117
|
+
return True
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def set_config_value(
|
|
121
|
+
key: str,
|
|
122
|
+
value: str,
|
|
123
|
+
platform: str | None,
|
|
124
|
+
use_local: bool,
|
|
125
|
+
cwd: str | Path,
|
|
126
|
+
home: str | Path | None = None,
|
|
127
|
+
) -> str:
|
|
128
|
+
"""设置指定配置项并返回来源。"""
|
|
129
|
+
config_store = _resolve_config_store(use_local=use_local, cwd=cwd, home=home)
|
|
130
|
+
normalized_value = _normalize_config_value_for_store(
|
|
131
|
+
key=key,
|
|
132
|
+
value=value,
|
|
133
|
+
cwd=Path(cwd),
|
|
134
|
+
)
|
|
135
|
+
if key == _TOOLCHAIN_PATH_KEY:
|
|
136
|
+
normalized_platform = _require_platform(platform, key)
|
|
137
|
+
config_store.upsert_nested_key(_TOOLCHAINS_KEY, normalized_platform, normalized_value)
|
|
138
|
+
return _describe_source(use_local)
|
|
139
|
+
|
|
140
|
+
if key.startswith("deps."):
|
|
141
|
+
sub_key = key[5:]
|
|
142
|
+
config_store.upsert_nested_key("deps_configs", sub_key, normalized_value)
|
|
143
|
+
return _describe_source(use_local)
|
|
144
|
+
|
|
145
|
+
config_store.upsert_key(key, normalized_value)
|
|
146
|
+
return _describe_source(use_local)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def get_config_value(
|
|
150
|
+
key: str,
|
|
151
|
+
platform: str | None,
|
|
152
|
+
use_local: bool,
|
|
153
|
+
cwd: str | Path,
|
|
154
|
+
home: str | Path | None = None,
|
|
155
|
+
) -> tuple[str | int, str]:
|
|
156
|
+
"""获取指定配置项的值与来源。"""
|
|
157
|
+
key = _normalize_config_key(key)
|
|
158
|
+
if key == "config-version":
|
|
159
|
+
return CONFIG_VERSION, "cli"
|
|
160
|
+
|
|
161
|
+
if use_local:
|
|
162
|
+
local_store = _resolve_config_store(use_local=True, cwd=cwd, home=home)
|
|
163
|
+
local_data = local_store.load_or_default()
|
|
164
|
+
local_value = _extract_config_value(local_data, key, platform)
|
|
165
|
+
if local_value is not None:
|
|
166
|
+
return local_value, _describe_source(True)
|
|
167
|
+
if key == "target":
|
|
168
|
+
return DEFAULT_TARGET, "default"
|
|
169
|
+
if key == "debug":
|
|
170
|
+
return 0, "default"
|
|
171
|
+
raise ConfigError(f"Unknown config key: {key}")
|
|
172
|
+
|
|
173
|
+
local_store = _resolve_config_store(use_local=True, cwd=cwd, home=home)
|
|
174
|
+
local_data = local_store.load_or_default()
|
|
175
|
+
local_value = _extract_config_value(local_data, key, platform)
|
|
176
|
+
if local_value is not None:
|
|
177
|
+
return local_value, _describe_source(True)
|
|
178
|
+
|
|
179
|
+
global_store = _resolve_config_store(use_local=False, cwd=cwd, home=home)
|
|
180
|
+
global_data = global_store.load_or_default()
|
|
181
|
+
global_value = _extract_config_value(global_data, key, platform)
|
|
182
|
+
if global_value is not None:
|
|
183
|
+
return global_value, _describe_source(False)
|
|
184
|
+
|
|
185
|
+
if key == "target":
|
|
186
|
+
return DEFAULT_TARGET, "default"
|
|
187
|
+
if key == "board":
|
|
188
|
+
return "", "default"
|
|
189
|
+
if key == "debug":
|
|
190
|
+
return 0, "default"
|
|
191
|
+
if key == "deps.on_version_fail":
|
|
192
|
+
return "warn", "default"
|
|
193
|
+
if key == "deps.on_target_fail":
|
|
194
|
+
return "warn", "default"
|
|
195
|
+
if key == "deps.track":
|
|
196
|
+
return False, "default"
|
|
197
|
+
|
|
198
|
+
raise KeyError(key)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def reset_config_key(
|
|
202
|
+
key: str,
|
|
203
|
+
platform: str | None,
|
|
204
|
+
use_local: bool,
|
|
205
|
+
cwd: str | Path,
|
|
206
|
+
home: str | Path | None = None,
|
|
207
|
+
) -> str:
|
|
208
|
+
"""重置指定配置项并返回来源。"""
|
|
209
|
+
key = _normalize_config_key(key)
|
|
210
|
+
config_store = _resolve_config_store(use_local=use_local, cwd=cwd, home=home)
|
|
211
|
+
if key == _TOOLCHAIN_PATH_KEY:
|
|
212
|
+
normalized_platform = _require_platform(platform, key)
|
|
213
|
+
config_store.reset_nested_key(_TOOLCHAINS_KEY, normalized_platform)
|
|
214
|
+
return _describe_source(use_local)
|
|
215
|
+
|
|
216
|
+
if key.startswith("deps."):
|
|
217
|
+
sub_key = key[5:]
|
|
218
|
+
config_store.reset_nested_key("deps_configs", sub_key)
|
|
219
|
+
return _describe_source(use_local)
|
|
220
|
+
|
|
221
|
+
config_store.reset_key(key)
|
|
222
|
+
return _describe_source(use_local)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _describe_source(use_local: bool) -> str:
|
|
226
|
+
"""返回配置来源描述。"""
|
|
227
|
+
return "local config" if use_local else "global config"
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _maybe_migrate_config(new_path: Path) -> None:
|
|
231
|
+
"""如果旧配置文件 .xt_conf.jsonc 存在且新文件不存在,自动重命名。"""
|
|
232
|
+
old_path = new_path.parent / ".xt_conf.jsonc"
|
|
233
|
+
if not new_path.exists() and old_path.exists():
|
|
234
|
+
old_path.rename(new_path)
|
|
235
|
+
print(f"[xt cli] Migrated config: .xt_conf.jsonc → xt_conf.jsonc")
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _resolve_config_store(
|
|
239
|
+
*,
|
|
240
|
+
use_local: bool,
|
|
241
|
+
cwd: str | Path,
|
|
242
|
+
home: str | Path | None,
|
|
243
|
+
) -> ConfigStore:
|
|
244
|
+
"""按作用域返回配置存储。"""
|
|
245
|
+
if use_local:
|
|
246
|
+
new_path = Path(cwd) / _LOCAL_CONFIG_FILENAME
|
|
247
|
+
else:
|
|
248
|
+
new_path = build_global_config_path(home)
|
|
249
|
+
_maybe_migrate_config(new_path)
|
|
250
|
+
return ConfigStore(new_path)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _extract_config_value(data: dict[str, Any], key: str, platform: str | None) -> str | int | None:
|
|
254
|
+
"""从配置对象中提取指定值。"""
|
|
255
|
+
if key == _TOOLCHAIN_PATH_KEY:
|
|
256
|
+
normalized_platform = _require_platform(platform, key)
|
|
257
|
+
toolchains = data.get(_TOOLCHAINS_KEY)
|
|
258
|
+
if not isinstance(toolchains, dict):
|
|
259
|
+
return None
|
|
260
|
+
value = toolchains.get(normalized_platform)
|
|
261
|
+
return value if isinstance(value, (str, int)) else None
|
|
262
|
+
|
|
263
|
+
if key.startswith("deps."):
|
|
264
|
+
sub_key = key[5:]
|
|
265
|
+
deps = data.get("deps_configs")
|
|
266
|
+
if isinstance(deps, dict):
|
|
267
|
+
value = deps.get(sub_key)
|
|
268
|
+
if sub_key == "track":
|
|
269
|
+
if isinstance(value, bool):
|
|
270
|
+
return value
|
|
271
|
+
if isinstance(value, str):
|
|
272
|
+
return value.lower() in ("true", "1")
|
|
273
|
+
return None
|
|
274
|
+
return value if isinstance(value, str) else None
|
|
275
|
+
return None
|
|
276
|
+
|
|
277
|
+
value = data.get(key)
|
|
278
|
+
if key == "sdk" and isinstance(value, str):
|
|
279
|
+
return str(Path(value))
|
|
280
|
+
if key == "debug" and isinstance(value, int):
|
|
281
|
+
return value
|
|
282
|
+
return value if isinstance(value, (str, int)) else None
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _normalize_config_value_for_store(key: str, value: str, cwd: Path) -> str | int | bool:
|
|
286
|
+
"""规范化需要落盘的配置值。"""
|
|
287
|
+
if key == "deps.track":
|
|
288
|
+
if isinstance(value, bool):
|
|
289
|
+
return value
|
|
290
|
+
if value.lower() in ("true", "1"):
|
|
291
|
+
return True
|
|
292
|
+
if value.lower() in ("false", "0"):
|
|
293
|
+
return False
|
|
294
|
+
raise ConfigError("deps.track must be true or false")
|
|
295
|
+
|
|
296
|
+
if key.startswith("deps."):
|
|
297
|
+
if value not in ("warn", "error", "ignore"):
|
|
298
|
+
raise ConfigError("deps on_fail value must be warn, error, or ignore")
|
|
299
|
+
return value
|
|
300
|
+
|
|
301
|
+
if key == "debug":
|
|
302
|
+
return _parse_debug_value(value)
|
|
303
|
+
if key != "sdk":
|
|
304
|
+
return value
|
|
305
|
+
|
|
306
|
+
normalized_value = Path(value).expanduser()
|
|
307
|
+
if not normalized_value.is_absolute():
|
|
308
|
+
normalized_value = (cwd / normalized_value).resolve()
|
|
309
|
+
_validate_sdk_dir(normalized_value)
|
|
310
|
+
return normalized_value.as_posix()
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _require_platform(platform: str | None, key: str) -> str:
|
|
314
|
+
"""校验需要平台参数的配置项。"""
|
|
315
|
+
if key == _TOOLCHAIN_PATH_KEY and not platform:
|
|
316
|
+
raise ConfigError("Platform is required for toolchain-path")
|
|
317
|
+
return "" if platform is None else platform
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _describe_source(use_local: bool) -> str:
|
|
321
|
+
"""返回配置来源描述。"""
|
|
322
|
+
return "local config" if use_local else "global config"
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def _ensure_config_version(data: dict[str, Any]) -> dict[str, Any]:
|
|
326
|
+
"""确保配置版本号始终位于首行。"""
|
|
327
|
+
normalized_data = dict(data)
|
|
328
|
+
config_version = normalized_data.pop(_CONFIG_VERSION_KEY, None)
|
|
329
|
+
if config_version is None:
|
|
330
|
+
config_version = CONFIG_VERSION
|
|
331
|
+
return {
|
|
332
|
+
_CONFIG_VERSION_KEY: config_version,
|
|
333
|
+
**normalized_data,
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def _has_user_config(data: dict[str, Any]) -> bool:
|
|
338
|
+
"""判断是否还存在用户配置项。"""
|
|
339
|
+
return any(key != _CONFIG_VERSION_KEY for key in data)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def _parse_debug_value(value: str) -> int:
|
|
343
|
+
"""解析调试开关配置值。"""
|
|
344
|
+
if value not in {"0", "1"}:
|
|
345
|
+
raise ConfigError("Debug value must be 0 or 1")
|
|
346
|
+
return int(value)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _validate_sdk_dir(sdk_dir: Path) -> None:
|
|
350
|
+
"""校验 sdk 目录结构。"""
|
|
351
|
+
sdk_marker = sdk_dir / _SDK_MARKER_RELATIVE_PATH
|
|
352
|
+
if sdk_marker.is_file():
|
|
353
|
+
return
|
|
354
|
+
raise ConfigError(
|
|
355
|
+
f"Invalid sdk directory: {sdk_dir}. Missing required file: .xt-sdk-version"
|
|
356
|
+
)
|