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.
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
+ )
constants.py ADDED
@@ -0,0 +1,6 @@
1
+ """xt-cli 核心常量。"""
2
+
3
+ CONFIG_VERSION = 1
4
+ DEFAULT_TARGET = "windows/simulator"
5
+ PRIORITY_DEFAULT = 100 # hook task 默认优先级
6
+ PRIORITY_INDEPENDENT = 100 # independent task 优先级