Repository navigation
Expand file tree
/
Copy pathcreateMotorDriverModule.py
More file actions
executable file
·677 lines (544 loc) · 25.1 KB
/
Copy pathcreateMotorDriverModule.py
File metadata and controls
executable file
·677 lines (544 loc) · 25.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
#!/usr/bin/env python3
"""
createMotorDriverModule.py
Clones the motorExample EPICS motor driver template and replaces all
occurrences of "example"/"Example"/"EXAMPLE" with user-specified names
to create a new motor driver module.
Usage:
python createMotorDriverModule.py <ModuleName> [--driver <DriverName>] [--class <ClassName>] [--url <git_url>]
Examples:
# Simple case (module and driver name are the same):
python createMotorDriverModule.py Vendor
# Two-name case (module name differs from driver/class name):
python createMotorDriverModule.py AcsMotion --driver SPiiPlus
# Three-name case (module, driver, and class names all differ):
python createMotorDriverModule.py VMC --driver vmc --class VirtualMotor
# Custom clone URL:
python createMotorDriverModule.py AcsMotion --driver SPiiPlus --url https://github.com/myorg/motorExample.git
"""
import argparse
import os
import shutil
import subprocess
import sys
import tempfile
# Default clone URL for the motorExample template
DEFAULT_URL = "https://github.com/epics-motor/motorExample.git"
# Files where driver/class-scoped replacements are applied (paths relative to
# the cloned module root, using the ORIGINAL template names before renaming).
#
# In three-level mode these are split: SUPPORT_SCOPED_FILES get driver-level
# replacements for library/dbd/Makefile names, while IOC_SCOPED_FILES use
# module-level replacements throughout. In two-level mode the combined
# DRIVER_SCOPED_FILES set is used (backward compatible).
SUPPORT_SCOPED_FILES = {
"exampleApp/src/ExampleDriver.h",
"exampleApp/src/ExampleDriver.cpp",
"exampleApp/src/Example.dbd",
"exampleApp/src/Makefile",
"exampleApp/iocsh/motorExample.iocsh",
"exampleApp/iocsh/Makefile",
"exampleApp/iocsh/EXAMPLE_motorExample.substitutions",
}
IOC_SCOPED_FILES = {
"iocs/exampleIOC/iocBoot/iocExample/example.iocsh",
"iocs/exampleIOC/iocBoot/iocExample/st.cmd",
"iocs/exampleIOC/exampleApp/src/Makefile",
}
DRIVER_SCOPED_FILES = SUPPORT_SCOPED_FILES | IOC_SCOPED_FILES
# Placeholder that won't appear in any EPICS source file.
# Must not contain "EXAMPLE" or any replacement target to avoid
# being corrupted by subsequent replacements.
_PLACEHOLDER = "\x00XMPL_PREFIX_GUARD\x00"
def camel_lower(name):
"""Lowercase a title-case or CamelCase name.
If the name is all uppercase (e.g., VMC), uses simple .lower().
Otherwise, only the first character is lowered to preserve CamelCase.
Examples:
AcsMotion -> acsMotion
Vendor -> vendor
VMC -> vmc
"""
if not name:
return name
if name.isupper():
return name.lower()
return name[0].lower() + name[1:]
def derive_names(title_case):
"""Derive the three case variants from a title-case module name.
Uses CamelCase-aware lowering (only the first character is lowered).
Returns:
(lowercase, title_case, uppercase)
"""
return camel_lower(title_case), title_case, title_case.upper()
def derive_driver_names(title_case):
"""Derive the three case variants from a title-case driver name.
Uses simple .lower() for the lowercase form.
Returns:
(lowercase, title_case, uppercase)
"""
return title_case.lower(), title_case, title_case.upper()
def is_text_file(filepath):
"""Heuristic check for whether a file is a text file."""
try:
with open(filepath, "rb") as f:
chunk = f.read(8192)
# Check for null bytes (strong indicator of binary)
if b"\x00" in chunk:
return False
return True
except (IOError, OSError):
return False
def replace_in_content(content, replacements):
"""Apply a list of (old, new) replacements to a string, in order.
Replacements are applied sequentially. Order matters to avoid
double-replacement (longer/more-specific patterns first).
"""
for old, new in replacements:
content = content.replace(old, new)
return content
# ---------------------------------------------------------------------------
# Two-level mode replacement builders (when --class is NOT specified)
# ---------------------------------------------------------------------------
def build_driver_replacements(driver_title):
"""Build the ordered list of driver-scoped (old, new) replacement pairs.
These replace ExampleMotor (as a compound) with the driver name,
effectively removing the 'Motor' from class names while handling
all the ExampleMotor-compound identifiers uniformly.
Applied BEFORE module-scoped replacements in driver-scoped files.
"""
driver_lower, driver_tc, driver_upper = derive_driver_names(driver_title)
return [
# Filename/include references: ExampleDriver -> <DriverTitle>Driver
("ExampleDriver", f"{driver_tc}Driver"),
# Compound uppercase in #defines: EXAMPLE_MOTOR -> DRIVER_UPPER
("EXAMPLE_MOTOR", driver_upper),
# Compound title-case: ExampleMotor -> DriverTitle
# This handles classes, functions, variables, registrar, comments
("ExampleMotor", driver_tc),
# Lowercase compound in error messages: "example motor" -> "driverLower"
("example motor", f"{driver_lower}"),
]
def build_module_replacements(module_title):
"""Build the ordered list of module-scoped (old, new) replacement pairs.
Applied to ALL files (after driver-scoped replacements in driver files).
Order: longest/most-specific first to prevent partial matches.
The EXAMPLE_ filename prefix (an EPICS convention for example config files)
is preserved by temporarily protecting it with a placeholder before the
general EXAMPLE replacement, then restoring it afterward.
"""
module_lower, module_tc, module_upper = derive_names(module_title)
return [
# Protect the EXAMPLE_ filename prefix from the general EXAMPLE replacement
("EXAMPLE_", _PLACEHOLDER),
# Compound uppercase (LICENSE): MOTOREXAMPLE -> MOTOR<UPPER>
("MOTOREXAMPLE", f"MOTOR{module_upper}"),
# Compound mixed-case: motorExample -> motor<Title>
("motorExample", f"motor{module_tc}"),
# RELEASE macro style: MOTOR_EXAMPLE -> MOTOR_<UPPER>
("MOTOR_EXAMPLE", f"MOTOR_{module_upper}"),
# General uppercase: EXAMPLE -> <UPPER>
("EXAMPLE", module_upper),
# Restore the protected EXAMPLE_ prefix
(_PLACEHOLDER, "EXAMPLE_"),
# General title-case: Example -> <Title>
("Example", module_tc),
# General lowercase: example -> <lower>
("example", module_lower),
]
# ---------------------------------------------------------------------------
# Three-level mode replacement builders (when --class IS specified)
# ---------------------------------------------------------------------------
def build_class_replacements(driver_name, class_name):
"""Build replacement pairs for C++ class names and driver filenames.
Used in three-level mode. Applied first in driver-scoped files.
- ExampleMotor compound -> class_name (classes, functions, registrar, defines)
- ExampleDriver -> driver_name + Driver (source filenames / includes)
"""
class_lower = class_name.lower()
class_upper = class_name.upper()
return [
# Filename/include references: ExampleDriver -> <driverName>Driver
("ExampleDriver", f"{driver_name}Driver"),
# Compound uppercase in #defines: EXAMPLE_MOTOR -> CLASS_UPPER
("EXAMPLE_MOTOR", class_upper),
# Compound title-case: ExampleMotor -> ClassName
("ExampleMotor", class_name),
# Lowercase compound in error messages: "example motor" -> "class_lower"
("example motor", f"{class_lower}"),
]
def build_module_compound_replacements(module_title):
"""Build replacement pairs for module-level compound patterns.
Used in three-level mode. Applied to ALL files (after class replacements
in driver-scoped files).
Handles motorExample, MOTOR_EXAMPLE, MOTOREXAMPLE, and standalone EXAMPLE
(uppercase). Protects the EXAMPLE_ filename prefix with a placeholder.
"""
module_lower, module_tc, module_upper = derive_names(module_title)
return [
# Protect the EXAMPLE_ filename prefix
("EXAMPLE_", _PLACEHOLDER),
# Compound uppercase (LICENSE): MOTOREXAMPLE -> MOTOR<UPPER>
("MOTOREXAMPLE", f"MOTOR{module_upper}"),
# Compound mixed-case: motorExample -> motor<Title>
("motorExample", f"motor{module_tc}"),
# RELEASE macro style: MOTOR_EXAMPLE -> MOTOR_<UPPER>
("MOTOR_EXAMPLE", f"MOTOR_{module_upper}"),
# General uppercase: EXAMPLE -> <UPPER>
("EXAMPLE", module_upper),
# Restore the protected EXAMPLE_ prefix
(_PLACEHOLDER, "EXAMPLE_"),
]
def build_support_general_replacements(driver_name, module_title):
"""Build replacement pairs for support-scoped files in three-level mode.
Applied after class and module compound replacements, only in
SUPPORT_SCOPED_FILES.
Handles the mixed context where source file references (Example.dbd)
use the driver name, while build artifact names (LIBRARY_IOC, support
dbd, Makefile prefixes) use the module name.
The compound 'Example.dbd' is replaced first so it isn't consumed by
the general 'Example' -> module_tc replacement.
"""
module_lower, module_tc, _ = derive_names(module_title)
return [
# Source dbd filename compound: Example.dbd -> <driver>.dbd
# Must come BEFORE general Example -> module_tc
("Example.dbd", f"{driver_name}.dbd"),
# General title-case: Example -> module_tc (library, support dbd,
# Makefile variable prefixes)
("Example", module_tc),
# General lowercase: example -> module_lower
("example", module_lower),
]
def build_module_general_replacements(module_title):
"""Build replacement pairs for standalone Example/example.
Used in three-level mode. Applied after module compound replacements
in files that are NOT support-scoped (including IOC-scoped files and
all other files). Maps Example/example to the module name for
directory references, IOC binary names, etc.
"""
module_lower, module_tc, _ = derive_names(module_title)
return [
("Example", module_tc),
("example", module_lower),
]
# ---------------------------------------------------------------------------
# File processing
# ---------------------------------------------------------------------------
def process_file_contents(root_dir, driver_name, class_name, module_title,
three_level_mode):
"""Walk all files under root_dir and apply text replacements.
In two-level mode (--class not specified):
Driver-scoped files get ExampleMotor/ExampleDriver compound
replacements first, then all files get module-scoped replacements.
In three-level mode (--class specified):
Support-scoped files get class replacements, then module compound
replacements, then support-general replacements (driver name for
source files, module name for library/dbd/Makefile artifacts).
All other files (including IOC-scoped) get module compound
replacements, then module-general replacements.
"""
if three_level_mode:
class_reps = build_class_replacements(driver_name, class_name)
mod_compound_reps = build_module_compound_replacements(module_title)
support_general_reps = build_support_general_replacements(
driver_name, module_title)
mod_general_reps = build_module_general_replacements(module_title)
else:
driver_reps = build_driver_replacements(driver_name)
module_reps = build_module_replacements(module_title)
for dirpath, dirnames, filenames in os.walk(root_dir):
# Skip .git directory
if ".git" in dirnames:
dirnames.remove(".git")
for filename in filenames:
filepath = os.path.join(dirpath, filename)
if not is_text_file(filepath):
continue
# Compute the relative path from the module root for scoping
relpath = os.path.relpath(filepath, root_dir)
try:
with open(filepath, "r") as f:
content = f.read()
except (IOError, UnicodeDecodeError):
continue
original = content
if three_level_mode:
if relpath in SUPPORT_SCOPED_FILES:
content = replace_in_content(content, class_reps)
content = replace_in_content(content, mod_compound_reps)
content = replace_in_content(content, support_general_reps)
else:
# IOC-scoped files and all other files use module names
content = replace_in_content(content, mod_compound_reps)
content = replace_in_content(content, mod_general_reps)
else:
# Two-level mode: existing behavior
if relpath in DRIVER_SCOPED_FILES:
content = replace_in_content(content, driver_reps)
content = replace_in_content(content, module_reps)
if content != original:
with open(filepath, "w") as f:
f.write(content)
def rename_files(root_dir, driver_name, module_title, three_level_mode):
"""Rename files whose names contain example/Example/EXAMPLE variants.
Walk bottom-up so child renames happen before parent directory renames.
Special handling:
- Files starting with EXAMPLE_ keep that prefix (EPICS convention for
example configuration files). Only the portion after EXAMPLE_ is renamed.
- ExampleDriver.* files are renamed using the driver name.
In two-level mode, standalone Example/example use the module name.
In three-level mode, Example.dbd uses the driver name (compound match),
while other standalone Example/example use the module name.
"""
module_lower, module_tc, module_upper = derive_names(module_title)
# Walk bottom-up to handle nested renames correctly
for dirpath, dirnames, filenames in os.walk(root_dir, topdown=False):
# Skip .git directory
if ".git" in os.path.relpath(dirpath, root_dir).split(os.sep):
continue
for filename in filenames:
new_name = filename
# Check if file starts with EXAMPLE_ prefix
if new_name.startswith("EXAMPLE_"):
# Preserve the EXAMPLE_ prefix, only rename the rest
prefix = "EXAMPLE_"
rest = new_name[len(prefix):]
# Driver filename rename: ExampleDriver -> <driverName>Driver
rest = rest.replace("ExampleDriver", f"{driver_name}Driver")
# Module compound: motorExample -> motor<Module>
rest = rest.replace("motorExample", f"motor{module_tc}")
if three_level_mode:
# Source dbd compound: Example.dbd -> <driver>.dbd
rest = rest.replace("Example.dbd", f"{driver_name}.dbd")
# General: Example/example -> module name
rest = rest.replace("Example", module_tc)
rest = rest.replace("example", module_lower)
new_name = prefix + rest
else:
# Driver filename rename: ExampleDriver -> <driverName>Driver
new_name = new_name.replace("ExampleDriver", f"{driver_name}Driver")
# Module compound: motorExample -> motor<Module>
new_name = new_name.replace("motorExample", f"motor{module_tc}")
if three_level_mode:
# Source dbd compound: Example.dbd -> <driver>.dbd
new_name = new_name.replace("Example.dbd", f"{driver_name}.dbd")
# General: Example/example -> module name
new_name = new_name.replace("Example", module_tc)
new_name = new_name.replace("example", module_lower)
if new_name != filename:
old_path = os.path.join(dirpath, filename)
new_path = os.path.join(dirpath, new_name)
os.rename(old_path, new_path)
def rename_directories(root_dir, module_title):
"""Rename directories whose names contain example/Example/EXAMPLE variants.
Walk bottom-up so child renames happen before parent directory renames.
Directory names always use the module name.
"""
module_lower, module_tc, module_upper = derive_names(module_title)
for dirpath, dirnames, filenames in os.walk(root_dir, topdown=False):
# Skip .git directory
if ".git" in os.path.relpath(dirpath, root_dir).split(os.sep):
continue
for dirname in dirnames:
if dirname == ".git":
continue
new_name = dirname
# Module compound: motorExample -> motor<Module>
new_name = new_name.replace("motorExample", f"motor{module_tc}")
# General: Example/example -> module name
new_name = new_name.replace("Example", module_tc)
new_name = new_name.replace("example", module_lower)
if new_name != dirname:
old_path = os.path.join(dirpath, dirname)
new_path = os.path.join(dirpath, new_name)
os.rename(old_path, new_path)
# ---------------------------------------------------------------------------
# Git and utility functions
# ---------------------------------------------------------------------------
def clone_template(url, dest_dir):
"""Clone the motorExample template repository."""
print(f"Cloning {url} ...")
try:
subprocess.run(
["git", "clone", url, dest_dir],
check=True,
capture_output=True,
text=True,
)
except subprocess.CalledProcessError as e:
print(f"Error cloning repository: {e.stderr}", file=sys.stderr)
sys.exit(1)
print("Clone complete.")
def remove_git_dir(module_dir):
"""Remove the .git directory from the cloned module."""
git_dir = os.path.join(module_dir, ".git")
if os.path.exists(git_dir):
shutil.rmtree(git_dir)
def git_init(module_dir):
"""Initialize a fresh git repository in the module directory."""
try:
subprocess.run(
["git", "init"],
cwd=module_dir,
check=True,
capture_output=True,
text=True,
)
except subprocess.CalledProcessError as e:
print(f"Warning: git init failed: {e.stderr}", file=sys.stderr)
def print_summary(module_title, driver_name, class_name, output_dir,
three_level_mode):
"""Print a summary of what was created."""
module_lower, module_tc, module_upper = derive_names(module_title)
print()
print("=" * 60)
print(f" Created motor driver module: motor{module_tc}")
print("=" * 60)
print()
print(f" Location: {output_dir}")
print()
if three_level_mode:
print(" Module name replacements:")
print(f" motorExample -> motor{module_tc}")
print(f" MOTOR_EXAMPLE -> MOTOR_{module_upper}")
print(f" EXAMPLE -> {module_upper}")
print(f" Example -> {module_tc}")
print(f" example -> {module_lower}")
print()
print(" Driver name replacements (source filenames):")
print(f" ExampleDriver -> {driver_name}Driver")
print(f" Example.dbd -> {driver_name}.dbd")
print()
print(" Class name replacements (C++ classes, iocsh functions, defines):")
print(f" ExampleMotor -> {class_name}")
print(f" EXAMPLE_MOTOR -> {class_name.upper()}")
print()
else:
print(" Module name replacements:")
print(f" example -> {module_lower}")
print(f" Example -> {module_tc}")
print(f" EXAMPLE -> {module_upper}")
print()
if driver_name != module_title:
driver_lower, driver_tc, driver_upper = derive_driver_names(driver_name)
print(" Driver name replacements (in driver files):")
print(f" ExampleMotor -> {driver_tc}")
print(f" EXAMPLE_MOTOR -> {driver_upper}")
print(f" ExampleDriver -> {driver_tc}Driver")
print()
# Determine the dbd filename
if three_level_mode:
dbd_name = f"{driver_name}.dbd"
else:
dbd_name = f"{module_tc}.dbd"
print(" Key files:")
print(f" Driver source: {module_lower}App/src/{driver_name}Driver.cpp")
print(f" Driver header: {module_lower}App/src/{driver_name}Driver.h")
print(f" DBD file: {module_lower}App/src/{dbd_name}")
print(f" IOC shell: {module_lower}App/iocsh/motor{module_tc}.iocsh")
print(f" Substitutions: {module_lower}App/iocsh/EXAMPLE_motor{module_tc}.substitutions")
print()
print(" A fresh git repository has been initialized.")
print(" Next steps:")
print(f" cd {os.path.basename(output_dir)}")
print(" # Edit the driver source to implement your motor controller protocol")
print(" # Configure RELEASE.local with paths to your EPICS base and dependencies")
print(" make")
print()
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main():
parser = argparse.ArgumentParser(
description="Create a new EPICS motor driver module from the motorExample template.",
epilog=(
"Examples:\n"
" %(prog)s Vendor\n"
" %(prog)s AcsMotion --driver SPiiPlus\n"
" %(prog)s VMC --driver vmc --class VirtualMotor\n"
" %(prog)s AcsMotion --driver SPiiPlus --url https://github.com/myorg/motorExample.git\n"
),
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument(
"module_name",
metavar="ModuleName",
help="Title-case module name (e.g., Vendor, AcsMotion, VMC). "
"The top-level directory will be named motor<ModuleName>.",
)
parser.add_argument(
"--driver",
metavar="DriverName",
default=None,
help="Driver name used for source filenames (.cpp, .h, .dbd). "
"When --class is not specified, also controls C++ class names "
"and iocsh functions. If omitted, defaults to ModuleName.",
)
parser.add_argument(
"--class",
metavar="ClassName",
dest="class_name",
default=None,
help="C++ class name prefix, used as-is for Controller/Axis classes, "
"iocsh registration functions, and #define constants "
"(e.g., VirtualMotor). When specified, --driver controls only "
"filenames and build artifacts while --class controls the C++ API. "
"If omitted, defaults to the --driver value.",
)
parser.add_argument(
"--url",
metavar="URL",
default=DEFAULT_URL,
help=f"Git clone URL for the motorExample template. Default: {DEFAULT_URL}",
)
args = parser.parse_args()
module_title = args.module_name
driver_name = args.driver if args.driver else module_title
# Three-level mode activates when --class is explicitly provided.
# This changes the meaning of --driver (filenames/library only) and
# adds --class for C++ class names.
three_level_mode = args.class_name is not None
class_name = args.class_name if args.class_name else driver_name
clone_url = args.url
# Derive the output directory name
_, module_tc, _ = derive_names(module_title)
output_dir_name = f"motor{module_tc}"
output_dir = os.path.join(os.getcwd(), output_dir_name)
# Check that the output directory doesn't already exist
if os.path.exists(output_dir):
print(
f"Error: Directory '{output_dir_name}' already exists in the current directory.",
file=sys.stderr,
)
sys.exit(1)
# Clone into a temporary directory first
with tempfile.TemporaryDirectory() as tmpdir:
clone_dir = os.path.join(tmpdir, "motorExample")
clone_template(clone_url, clone_dir)
# Remove the cloned .git directory
print("Removing template git history...")
remove_git_dir(clone_dir)
# Apply content replacements
print("Applying content replacements...")
process_file_contents(clone_dir, driver_name, class_name,
module_title, three_level_mode)
# Rename files
print("Renaming files...")
rename_files(clone_dir, driver_name, module_title, three_level_mode)
# Rename directories
print("Renaming directories...")
rename_directories(clone_dir, module_title)
# Rename the top-level directory
final_dir = os.path.join(tmpdir, output_dir_name)
os.rename(clone_dir, final_dir)
# Move to the output location
shutil.move(final_dir, output_dir)
# Initialize a fresh git repo
print("Initializing git repository...")
git_init(output_dir)
# Print summary
print_summary(module_title, driver_name, class_name, output_dir,
three_level_mode)
if __name__ == "__main__":
main()