|
1 | 1 | #!/usr/bin/env python3
|
2 | 2 | """
|
3 |
| -xrobot_add_mod.py - 添加模块仓库或追加模块实例配置 |
| 3 | +xrobot_add_mod.py - Add a module repository to modules.yaml or append a module instance config. |
4 | 4 |
|
5 |
| -Usage 示例: |
6 |
| - # 添加模块仓库(默认写入 Modules/modules.yaml) |
7 |
| - python xrobot_add_mod.py https://github.com/yourorg/BlinkLED.git --version main |
8 |
| -
|
9 |
| - # 追加模块实例(默认写入 User/xrobot.yaml) |
| 5 | +Usage examples: |
| 6 | + python xrobot_add_mod.py xrobot-org/BlinkLED@main |
10 | 7 | python xrobot_add_mod.py BlinkLED
|
11 | 8 | """
|
12 | 9 |
|
13 | 10 | import argparse
|
14 | 11 | import yaml
|
15 | 12 | from pathlib import Path
|
16 |
| -from urllib.parse import urlparse |
17 |
| -from collections import OrderedDict |
18 | 13 |
|
| 14 | +# Default config file paths |
19 | 15 | DEFAULT_REPO_CONFIG = Path("Modules/modules.yaml")
|
20 | 16 | DEFAULT_INSTANCE_CONFIG = Path("User/xrobot.yaml")
|
21 | 17 | MODULES_DIR = Path("Modules")
|
22 | 18 |
|
23 |
| -def is_repo_url(s: str) -> bool: |
24 |
| - return s.startswith("http://") or s.startswith("https://") |
25 |
| - |
26 |
| -def extract_name_from_repo(repo: str) -> str: |
27 |
| - return Path(urlparse(repo).path).stem |
28 |
| - |
29 |
| -def load_yaml(path: Path) -> dict: |
30 |
| - return yaml.safe_load(path.read_text(encoding="utf-8")) if path.exists() else {} |
31 |
| - |
32 |
| -def save_yaml(path: Path, data: dict): |
33 |
| - path.parent.mkdir(parents=True, exist_ok=True) |
34 |
| - path.write_text(yaml.dump(data, sort_keys=False, allow_unicode=True), encoding="utf-8") |
35 |
| - |
36 |
| -def parse_manifest_from_header(header_path: Path) -> dict: |
37 |
| - content = header_path.read_text(encoding="utf-8") |
38 |
| - lines = content.splitlines() |
39 |
| - block = [] |
40 |
| - inside = False |
41 |
| - |
42 |
| - for line in lines: |
43 |
| - stripped = line.strip() |
44 |
| - if stripped == "/* === MODULE MANIFEST ===": |
45 |
| - inside = True |
46 |
| - continue |
47 |
| - if stripped == "=== END MANIFEST === */": |
48 |
| - break |
49 |
| - if inside: |
50 |
| - block.append(line.replace("/*", " ").replace("*/", " ").rstrip()) |
51 |
| - |
52 |
| - try: |
53 |
| - return yaml.safe_load("\n".join(block)) or {} |
54 |
| - except yaml.YAMLError as e: |
55 |
| - print(f"[ERROR] Failed to parse manifest: {e}") |
56 |
| - return {} |
57 |
| - |
58 |
| -def append_module_instance(module_name: str, config_path: Path): |
59 |
| - header_path = MODULES_DIR / module_name / f"{module_name}.hpp" |
60 |
| - if not header_path.exists(): |
61 |
| - print(f"[ERROR] Module header not found: {header_path}") |
| 19 | +# Import utility functions from local module |
| 20 | +from xrobot.ModuleParser import load_single_module, parse_constructor_args |
| 21 | + |
| 22 | +def is_repo_id(s: str) -> bool: |
| 23 | + """ |
| 24 | + Determine if string is a repo ID (e.g., 'namespace/ModuleName' or 'namespace/ModuleName@version'). |
| 25 | + """ |
| 26 | + return "/" in s |
| 27 | + |
| 28 | +def get_next_instance_id(modules: list, base_name: str) -> str: |
| 29 | + """ |
| 30 | + Return a unique instance id for the given module name, such as 'BlinkLED_0', 'BlinkLED_1', etc. |
| 31 | + """ |
| 32 | + existing = [m.get("id", "") for m in modules if m.get("name") == base_name] |
| 33 | + nums = [] |
| 34 | + for eid in existing: |
| 35 | + if eid and eid.startswith(base_name + "_"): |
| 36 | + try: |
| 37 | + nums.append(int(eid[len(base_name) + 1:])) |
| 38 | + except Exception: |
| 39 | + pass |
| 40 | + next_num = (max(nums) + 1) if nums else 0 |
| 41 | + return f"{base_name}_{next_num}" |
| 42 | + |
| 43 | +def append_module_instance(module_name: str, config_path: Path, instance_id: str = None): |
| 44 | + """ |
| 45 | + Append a module instance (with id and constructor args) to the user config YAML. |
| 46 | + """ |
| 47 | + mod_dir = MODULES_DIR / module_name |
| 48 | + manifest = load_single_module(mod_dir) |
| 49 | + if not manifest: |
| 50 | + print(f"[ERROR] Module manifest not found in: {mod_dir}") |
62 | 51 | return
|
63 | 52 |
|
64 |
| - manifest = parse_manifest_from_header(header_path) |
65 |
| - args = manifest.get("constructor_args", {}) |
66 |
| - args_ordered = OrderedDict() |
67 |
| - if isinstance(args, dict): |
68 |
| - args_ordered.update(args) |
69 |
| - elif isinstance(args, list): |
70 |
| - for item in args: |
71 |
| - if isinstance(item, dict): |
72 |
| - args_ordered.update(item) |
73 |
| - |
74 |
| - data = load_yaml(config_path) |
| 53 | + # Parse constructor and template args in order |
| 54 | + args_ordered = parse_constructor_args(manifest.constructor_args) |
| 55 | + template_args_ordered = parse_constructor_args(manifest.template_args) |
| 56 | + |
| 57 | + # Load or create config file |
| 58 | + if config_path.exists(): |
| 59 | + data = yaml.safe_load(config_path.read_text(encoding="utf-8")) or {} |
| 60 | + else: |
| 61 | + data = {} |
75 | 62 | if "modules" not in data or not isinstance(data["modules"], list):
|
76 | 63 | data["modules"] = []
|
77 | 64 |
|
78 |
| - data["modules"].append({ |
| 65 | + # Auto-generate unique instance id if not given |
| 66 | + inst_id = instance_id or get_next_instance_id(data["modules"], module_name) |
| 67 | + |
| 68 | + mod_entry = { |
| 69 | + "id": inst_id, |
79 | 70 | "name": module_name,
|
80 | 71 | "constructor_args": dict(args_ordered)
|
81 |
| - }) |
82 |
| - |
83 |
| - save_yaml(config_path, data) |
84 |
| - print(f"[SUCCESS] Appended module instance '{module_name}' to {config_path}") |
85 |
| - |
86 |
| -def add_repo_entry(repo: str, version: str, config_path: Path): |
87 |
| - name = extract_name_from_repo(repo) |
88 |
| - data = load_yaml(config_path) |
89 |
| - modules = data.get("modules", []) |
| 72 | + } |
| 73 | + if template_args_ordered: |
| 74 | + mod_entry["template_args"] = dict(template_args_ordered) |
| 75 | + |
| 76 | + data["modules"].append(mod_entry) |
| 77 | + |
| 78 | + config_path.parent.mkdir(parents=True, exist_ok=True) |
| 79 | + config_path.write_text(yaml.dump(data, sort_keys=False, allow_unicode=True), encoding="utf-8") |
| 80 | + print(f"[SUCCESS] Appended module instance '{module_name}' as id '{inst_id}' to {config_path}") |
| 81 | + |
| 82 | +def add_repo_entry(repo_id: str, config_path: Path): |
| 83 | + """ |
| 84 | + Add a new repo module (as a plain string, e.g., 'namespace/ModuleName[@version]') to modules.yaml. |
| 85 | + """ |
| 86 | + if config_path.exists(): |
| 87 | + data = yaml.safe_load(config_path.read_text(encoding="utf-8")) or {} |
| 88 | + else: |
| 89 | + data = {} |
| 90 | + if "modules" not in data or not isinstance(data["modules"], list): |
| 91 | + data["modules"] = [] |
90 | 92 |
|
91 |
| - if any(m.get("name") == name for m in modules): |
92 |
| - print(f"[WARN] Module '{name}' already exists in {config_path}. Skipping.") |
| 93 | + if repo_id in data["modules"]: |
| 94 | + print(f"[WARN] Module '{repo_id}' already exists in {config_path}. Skipping.") |
93 | 95 | return
|
94 | 96 |
|
95 |
| - entry = {"name": name, "repo": repo} |
96 |
| - if version: |
97 |
| - entry["version"] = version |
98 |
| - modules.append(entry) |
99 |
| - |
100 |
| - save_yaml(config_path, {"modules": modules}) |
101 |
| - print(f"[SUCCESS] Added repo module '{name}' to {config_path}") |
| 97 | + data["modules"].append(repo_id) |
| 98 | + config_path.parent.mkdir(parents=True, exist_ok=True) |
| 99 | + config_path.write_text(yaml.dump(data, sort_keys=False, allow_unicode=True), encoding="utf-8") |
| 100 | + print(f"[SUCCESS] Added repo module '{repo_id}' to {config_path}") |
102 | 101 |
|
103 | 102 | def main():
|
104 |
| - parser = argparse.ArgumentParser(description="Add module repo or instance config to YAML") |
105 |
| - parser.add_argument("target", help="Repo URL or local module name") |
106 |
| - parser.add_argument("--version", "-v", help="Branch or tag (repo only)") |
107 |
| - parser.add_argument("--config", "-c", help="YAML file path to write (auto default)") |
| 103 | + parser = argparse.ArgumentParser(description="Add a module repo or instance config to a YAML file") |
| 104 | + parser.add_argument("target", help="namespace/ModuleName[@version] or local module name") |
| 105 | + parser.add_argument("--config", "-c", help="Path to YAML config file (optional, auto default)") |
| 106 | + parser.add_argument("--instance-id", help="Optional: manually set the module instance id") |
108 | 107 |
|
109 | 108 | args = parser.parse_args()
|
110 | 109 |
|
111 |
| - # 判断类型并选择默认 config 路径 |
112 |
| - if is_repo_url(args.target): |
| 110 | + if is_repo_id(args.target): |
113 | 111 | config_path = Path(args.config) if args.config else DEFAULT_REPO_CONFIG
|
114 |
| - add_repo_entry(args.target, args.version, config_path) |
| 112 | + add_repo_entry(args.target, config_path) |
115 | 113 | else:
|
116 | 114 | config_path = Path(args.config) if args.config else DEFAULT_INSTANCE_CONFIG
|
117 |
| - append_module_instance(args.target, config_path) |
| 115 | + append_module_instance(args.target, config_path, args.instance_id) |
118 | 116 |
|
119 | 117 | if __name__ == "__main__":
|
120 | 118 | main()
|
0 commit comments