microchip-devtools 0.1.0__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.
@@ -0,0 +1,343 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ microchip_devtools.mcc.mcc_refresh — Force full MCC regeneration of driver files.
4
+
5
+ GUI-assisted: MPLAB X is launched automatically; the operator opens MCC and
6
+ clicks "Generate Code", then confirms by pressing Enter here.
7
+
8
+ Workflow (default)
9
+ ------------------
10
+ 1. Preflight – verify MCC project structure and MPLAB X installation.
11
+ 2. Backup – copy generated dir to build/backups/{timestamp}/ for rollback.
12
+ 3. Clean – delete generated output tree + MCC hash-tracking flags.
13
+ 4. Launch – open project in MPLAB X (unless --skip-launch).
14
+ 5. Wait – prompt operator to confirm MCC generation is complete.
15
+ 6. Merge – launch meld to review old vs new files (unless --skip-merge).
16
+ 7. Validate – run mchp-check-peripheral (Bug 1 + Bug 3 guards).
17
+ 8. Report – print git diff stat for the generated directory.
18
+
19
+ Usage
20
+ -----
21
+ mchp-mcc-refresh [--root PATH] [--project-name NAME] [options]
22
+
23
+ --root PATH Project root (default: $VOLTU_PROJECT_ROOT or cwd)
24
+ --project-name NAME Project name (default: $VOLTU_PROJECT_NAME or folder name)
25
+ --dry-run Preview only
26
+ --force Skip backup + skip merge tool
27
+ --skip-launch IDE already open
28
+ --skip-merge Backup but no merge review
29
+ """
30
+
31
+ from __future__ import annotations
32
+
33
+ import argparse
34
+ import os
35
+ import shutil
36
+ import subprocess
37
+ import sys
38
+ import time
39
+ from pathlib import Path
40
+ from typing import Optional
41
+
42
+ from microchip_devtools._project import project_name as _env_project_name
43
+ from microchip_devtools._project import project_root as _env_project_root
44
+
45
+ _DEFAULT_MPLAB = "/opt/microchip/mplabx/v6.25/mplab_platform/bin/mplab_ide"
46
+ _DEFAULT_MERGE_TOOL = "meld"
47
+
48
+
49
+ def log(msg: str) -> None:
50
+ print(f"[mcc-refresh] {msg}", flush=True)
51
+
52
+
53
+ def _count_files(path: Path) -> int:
54
+ return sum(1 for _ in path.rglob("*") if _.is_file()) if path.exists() else 0
55
+
56
+
57
+ def _build_paths(root: Path, name: str) -> dict[str, Path]:
58
+ return {
59
+ "generated_dir": root / "firmware/src/config/default",
60
+ "flags_dir": root / f"firmware/{name}.X/.generated_files/flags/default",
61
+ "mcc_project": root / f"firmware/{name}.X",
62
+ "mcc_config": root / f"firmware/{name}.X/{name}_default/mcc-config.mc4",
63
+ "success_manifest": root
64
+ / "firmware/src/config/default/harmony-manifest-success.yml",
65
+ "log_dir": root / "build/logs",
66
+ "backup_dir": root / "build/backups",
67
+ }
68
+
69
+
70
+ def preflight(args: argparse.Namespace, paths: dict[str, Path], root: Path) -> None:
71
+ mplab_ide = Path(os.environ.get("MPLAB_IDE", _DEFAULT_MPLAB))
72
+ merge_tool = os.environ.get("MCC_MERGE_TOOL", _DEFAULT_MERGE_TOOL)
73
+ errors: list[str] = []
74
+
75
+ if not paths["generated_dir"].is_dir():
76
+ errors.append(
77
+ f"Generated output directory not found: {paths['generated_dir'].relative_to(root)}"
78
+ )
79
+
80
+ if not paths["mcc_config"].exists():
81
+ errors.append(f"MCC config not found: {paths['mcc_config'].relative_to(root)}")
82
+
83
+ if not args.skip_launch and not mplab_ide.exists():
84
+ errors.append(
85
+ f"MPLAB X IDE not found: {mplab_ide}\n"
86
+ " Set MPLAB_IDE env var or use --skip-launch if IDE is already open."
87
+ )
88
+
89
+ if not args.force and not args.skip_merge and shutil.which(merge_tool) is None:
90
+ errors.append(
91
+ f"Merge tool not found: {merge_tool}\n"
92
+ f" Install {merge_tool} or use --skip-merge.\n"
93
+ f" Set MCC_MERGE_TOOL env var to use a different tool."
94
+ )
95
+
96
+ if errors:
97
+ for e in errors:
98
+ log(f"ERROR: {e}")
99
+ sys.exit(1)
100
+
101
+ log("Preflight OK")
102
+
103
+
104
+ def backup(dry_run: bool, paths: dict[str, Path], root: Path) -> Optional[Path]:
105
+ if dry_run:
106
+ log("[dry-run] Would backup generated files to build/backups/{timestamp}/")
107
+ return None
108
+
109
+ paths["backup_dir"].mkdir(parents=True, exist_ok=True)
110
+ ts = time.strftime("%Y%m%d_%H%M%S")
111
+ backup_path = paths["backup_dir"] / f"generated_{ts}"
112
+
113
+ if paths["generated_dir"].exists():
114
+ log(f"Backing up generated files to: {backup_path.relative_to(root)}")
115
+ shutil.copytree(paths["generated_dir"], backup_path, dirs_exist_ok=False)
116
+ log(f"Backup complete: {_count_files(backup_path)} files")
117
+ return backup_path
118
+
119
+ log("Generated directory does not exist; skipping backup")
120
+ return None
121
+
122
+
123
+ def clean(dry_run: bool, paths: dict[str, Path], root: Path) -> None:
124
+ tasks = [
125
+ (paths["generated_dir"], "generated source tree"),
126
+ (paths["flags_dir"], "MCC hash-tracking flags"),
127
+ ]
128
+
129
+ for path, label in tasks:
130
+ count = _count_files(path)
131
+ if not path.exists():
132
+ log(f"Skip (already absent): {label}")
133
+ continue
134
+ if dry_run:
135
+ log(
136
+ f"[dry-run] Would delete {count:>4} files — {label}: {path.relative_to(root)}"
137
+ )
138
+ else:
139
+ log(f"Deleting {count:>4} files — {label}: {path.relative_to(root)}")
140
+ shutil.rmtree(path)
141
+
142
+ if not dry_run:
143
+ log("Clean complete")
144
+
145
+
146
+ def launch_mplab(dry_run: bool, paths: dict[str, Path], root: Path) -> Optional[subprocess.Popen]: # type: ignore[type-arg]
147
+ mplab_ide = Path(os.environ.get("MPLAB_IDE", _DEFAULT_MPLAB))
148
+ cmd = [str(mplab_ide), "--open", str(paths["mcc_project"]), "--nosplash"]
149
+
150
+ if dry_run:
151
+ log(f"[dry-run] Would launch: {' '.join(cmd)}")
152
+ return None
153
+
154
+ log(f"Launching MPLAB X (project: {paths['mcc_project'].relative_to(root)})")
155
+ proc = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
156
+ log(f"MPLAB X launched (PID {proc.pid})")
157
+ return proc
158
+
159
+
160
+ def wait_for_user(dry_run: bool, paths: dict[str, Path], root: Path) -> None:
161
+ if dry_run:
162
+ log("[dry-run] Would wait for operator to click Generate Code in MCC")
163
+ return
164
+
165
+ print()
166
+ print("=" * 62)
167
+ print(" ACTION REQUIRED IN MPLAB X")
168
+ print()
169
+ print(" 1. Wait for MPLAB X to finish loading the project.")
170
+ print(" 2. In the toolbar, click Tools → MCC (or press Ctrl+Shift+M).")
171
+ print(" 3. In the MCC panel, click Generate (the blue Generate button).")
172
+ print(" 4. Wait until the progress bar disappears and no errors appear.")
173
+ print()
174
+ print(" Then press Enter here to continue.")
175
+ print("=" * 62)
176
+ print()
177
+
178
+ try:
179
+ input(" Press Enter after MCC generation completes > ")
180
+ except KeyboardInterrupt:
181
+ print()
182
+ log("Aborted by user")
183
+ sys.exit(1)
184
+
185
+ c_files = (
186
+ list(paths["generated_dir"].rglob("*.c"))
187
+ if paths["generated_dir"].exists()
188
+ else []
189
+ )
190
+ if not c_files:
191
+ log("ERROR: Generated directory is empty or missing after generation.")
192
+ log(f" Expected sources at: {paths['generated_dir'].relative_to(root)}")
193
+ sys.exit(1)
194
+
195
+ if not paths["success_manifest"].exists():
196
+ log(
197
+ f"WARNING: Success manifest not found: {paths['success_manifest'].relative_to(root)}"
198
+ )
199
+ log(" Generation may have completed partially. Continuing to validation.")
200
+
201
+ total = _count_files(paths["generated_dir"])
202
+ log(f"Generated directory restored — {total} files")
203
+
204
+
205
+ def merge_review(
206
+ backup_path: Optional[Path], dry_run: bool, paths: dict[str, Path]
207
+ ) -> None:
208
+ merge_tool = os.environ.get("MCC_MERGE_TOOL", _DEFAULT_MERGE_TOOL)
209
+
210
+ if dry_run or backup_path is None:
211
+ if dry_run:
212
+ log(
213
+ f"[dry-run] Would launch {merge_tool} to compare old vs new generated files"
214
+ )
215
+ return
216
+
217
+ if not backup_path.exists() or not paths["generated_dir"].exists():
218
+ log("Cannot review: backup or generated directory missing")
219
+ return
220
+
221
+ log(f"Launching {merge_tool} to review changes...")
222
+ print()
223
+ print("=" * 62)
224
+ print(f" Opening {merge_tool} for visual diff/merge review")
225
+ print(f" Left: {backup_path.name} (old)")
226
+ print(f" Right: current generated files")
227
+ print("=" * 62)
228
+ print()
229
+
230
+ try:
231
+ subprocess.run(
232
+ [merge_tool, str(backup_path), str(paths["generated_dir"])], check=False
233
+ )
234
+ except (FileNotFoundError, Exception) as e:
235
+ log(f"WARNING: Failed to launch merge tool: {e}")
236
+
237
+ log("Merge review complete")
238
+
239
+
240
+ def validate(dry_run: bool, root: Path, name: str) -> None:
241
+ if dry_run:
242
+ log("[dry-run] Would run: mchp-check-peripheral")
243
+ return
244
+
245
+ log("Running peripheral config validation...")
246
+ result = subprocess.run(
247
+ ["mchp-check-peripheral", "--root", str(root), "--project-name", name],
248
+ cwd=root,
249
+ )
250
+ if result.returncode != 0:
251
+ log("VALIDATION FAILED — generated files have configuration mismatches.")
252
+ sys.exit(1)
253
+
254
+ log("Validation passed")
255
+
256
+
257
+ def report_diff(dry_run: bool, paths: dict[str, Path], root: Path) -> None:
258
+ if dry_run:
259
+ return
260
+
261
+ result = subprocess.run(
262
+ ["git", "diff", "--stat", str(paths["generated_dir"].relative_to(root))],
263
+ cwd=root,
264
+ capture_output=True,
265
+ text=True,
266
+ )
267
+ if result.stdout.strip():
268
+ log("Git diff summary for generated files:")
269
+ print(result.stdout)
270
+ else:
271
+ log("No unstaged changes in generated files (git diff clean)")
272
+
273
+
274
+ def write_log(dry_run: bool, paths: dict[str, Path], success: bool) -> None:
275
+ if dry_run:
276
+ return
277
+
278
+ paths["log_dir"].mkdir(parents=True, exist_ok=True)
279
+ ts = time.strftime("%Y%m%d_%H%M%S")
280
+ log_path = paths["log_dir"] / f"mcc_refresh_{ts}.log"
281
+ status = "SUCCESS" if success else "FAILED"
282
+ log_path.write_text(f"mcc_refresh {ts} {status}\n")
283
+ log(f"Run log: {log_path.relative_to(paths['log_dir'].parent.parent)}")
284
+
285
+
286
+ def main() -> None:
287
+ parser = argparse.ArgumentParser(
288
+ description="Force full MCC regeneration of driver files.",
289
+ formatter_class=argparse.RawDescriptionHelpFormatter,
290
+ epilog=__doc__,
291
+ )
292
+ parser.add_argument(
293
+ "--root",
294
+ type=Path,
295
+ default=None,
296
+ help="Project root (default: $VOLTU_PROJECT_ROOT or cwd)",
297
+ )
298
+ parser.add_argument(
299
+ "--project-name",
300
+ default=None,
301
+ help="Project name (default: $VOLTU_PROJECT_NAME or folder name)",
302
+ )
303
+ parser.add_argument("--dry-run", action="store_true")
304
+ parser.add_argument("--skip-launch", action="store_true")
305
+ parser.add_argument("--skip-merge", action="store_true")
306
+ parser.add_argument("--force", action="store_true")
307
+ args = parser.parse_args()
308
+
309
+ root = args.root or _env_project_root()
310
+ name = args.project_name or _env_project_name()
311
+ paths = _build_paths(root, name)
312
+
313
+ if args.dry_run:
314
+ log("DRY RUN — no files will be modified")
315
+
316
+ preflight(args, paths, root)
317
+
318
+ if args.force:
319
+ log("Force mode: skipping backup and merge review")
320
+ backup_path = None
321
+ else:
322
+ backup_path = backup(args.dry_run, paths, root)
323
+
324
+ clean(args.dry_run, paths, root)
325
+
326
+ if not args.skip_launch:
327
+ launch_mplab(args.dry_run, paths, root)
328
+ else:
329
+ log("Skipping MPLAB X launch (--skip-launch)")
330
+
331
+ wait_for_user(args.dry_run, paths, root)
332
+
333
+ if not args.force and not args.skip_merge:
334
+ merge_review(backup_path, args.dry_run, paths)
335
+
336
+ validate(args.dry_run, root, name)
337
+ report_diff(args.dry_run, paths, root)
338
+ write_log(args.dry_run, paths, success=True)
339
+ log("Done.")
340
+
341
+
342
+ if __name__ == "__main__":
343
+ main()