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.
- microchip_devtools/__init__.py +0 -0
- microchip_devtools/_project.py +14 -0
- microchip_devtools/format/__init__.py +0 -0
- microchip_devtools/format/uncrustify.py +119 -0
- microchip_devtools/list_cmds.py +31 -0
- microchip_devtools/mcc/__init__.py +0 -0
- microchip_devtools/mcc/check_peripheral.py +207 -0
- microchip_devtools/mcc/mcc_refresh.py +343 -0
- microchip_devtools/mcc/parse_hardware.py +374 -0
- microchip_devtools/setup_env/__init__.py +0 -0
- microchip_devtools/setup_env/_ui.py +63 -0
- microchip_devtools/setup_env/checks.py +178 -0
- microchip_devtools/setup_env/defaults.py +33 -0
- microchip_devtools/setup_env/runner.py +334 -0
- microchip_devtools/xc32/__init__.py +0 -0
- microchip_devtools/xc32/merge_hex.py +238 -0
- microchip_devtools/xc32/validate_fmt3.py +230 -0
- microchip_devtools-0.1.0.dist-info/METADATA +16 -0
- microchip_devtools-0.1.0.dist-info/RECORD +21 -0
- microchip_devtools-0.1.0.dist-info/WHEEL +4 -0
- microchip_devtools-0.1.0.dist-info/entry_points.txt +10 -0
|
@@ -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()
|