|
| 1 | +#!/usr/bin/env python3 |
| 2 | +""" |
| 3 | +crate_recreator.py |
| 4 | +
|
| 5 | +This script generates a self-contained Python script that, when executed, recreates |
| 6 | +the directory structure and file contents of a Rust crate. The process is as follows: |
| 7 | +
|
| 8 | +1. Starting from a specified source folder (or using --src-only to restrict to its "src" subfolder), |
| 9 | + the script searches upward for a Cargo.toml file. When found, it uses the folder containing |
| 10 | + Cargo.toml as the crate root. |
| 11 | +2. It extracts the crate name from the Cargo.toml’s [package] section. If not found, it falls back |
| 12 | + to using the basename of the originally provided folder. |
| 13 | +3. The script then gathers all files recursively from the determined root (or root/src if --src-only), |
| 14 | + ignoring directories like ".git", "target", ".aipack", ".github" and files such as ".gitignore", |
| 15 | + "Cargo.lock", or any file whose name starts with "LICENSE" or "NOTICE", as well as binary files |
| 16 | + (e.g., .webp, .jpg, .jpeg, .png). |
| 17 | +4. It generates a self-contained Python script that will recreate the crate’s structure and embedded file contents. |
| 18 | + In the generated script each embedded file is annotated with a comment showing its relative path. |
| 19 | +5. The generated file is named in the format: <crate_name>_recreate_YYMMDD_HHMM.py. |
| 20 | +6. The generated script is set as executable and its content is copied to the clipboard. |
| 21 | + (This copied content is the generated recreate script—not this crate recreator.) |
| 22 | +
|
| 23 | +Usage: |
| 24 | + python crate_recreator.py <source_folder> [--src-only] |
| 25 | +""" |
| 26 | + |
| 27 | +import os |
| 28 | +import sys |
| 29 | +import argparse |
| 30 | +import subprocess |
| 31 | +import stat |
| 32 | +from datetime import datetime |
| 33 | +import re |
| 34 | + |
| 35 | +def gather_files(source_folder): |
| 36 | + """ |
| 37 | + Walk the source folder recursively and return a dictionary mapping |
| 38 | + relative file paths to their contents. |
| 39 | +
|
| 40 | + Excludes directories named "target" (case-insensitive), ".git", ".aipack", ".github", |
| 41 | + and files such as ".gitignore", "Cargo.lock", or any file whose name starts with |
| 42 | + "LICENSE" or "NOTICE". Also ignores binary files with extensions like .webp, .jpg, .jpeg, .png. |
| 43 | +
|
| 44 | + Provides detailed tracing for debugging. |
| 45 | + """ |
| 46 | + ignore_dirs = {".git", ".aipack", ".github"} |
| 47 | + ignore_files = {".gitignore", "Cargo.lock"} |
| 48 | + binary_extensions = (".webp", ".jpg", ".jpeg", ".png") |
| 49 | + files_dict = {} |
| 50 | + print(f"[TRACE] Starting to traverse source folder: {source_folder}") |
| 51 | + for root, dirs, files in os.walk(source_folder): |
| 52 | + # Exclude specified directories. |
| 53 | + original_dirs = dirs.copy() |
| 54 | + dirs[:] = [d for d in dirs if d.lower() != "target" and d not in ignore_dirs] |
| 55 | + for excluded in set(original_dirs) - set(dirs): |
| 56 | + print(f"[TRACE] Excluding directory: {excluded}") |
| 57 | + # Process each file in the current directory. |
| 58 | + for file in files: |
| 59 | + # Check ignore conditions for file names. |
| 60 | + if file in ignore_files or file.endswith(('.bak', '~')): |
| 61 | + print(f"[TRACE] Ignoring file: {file}") |
| 62 | + continue |
| 63 | + if file.startswith("LICENSE") or file.startswith("NOTICE"): |
| 64 | + print(f"[TRACE] Ignoring file (starts with LICENSE or NOTICE): {file}") |
| 65 | + continue |
| 66 | + lower_file = file.lower() |
| 67 | + if lower_file.endswith(binary_extensions): |
| 68 | + print(f"[TRACE] Ignoring binary file: {file}") |
| 69 | + continue |
| 70 | + |
| 71 | + full_path = os.path.join(root, file) |
| 72 | + # Compute relative path based on the source folder. |
| 73 | + rel_path = os.path.relpath(full_path, source_folder) |
| 74 | + print(f"[TRACE] Processing file: {full_path} as {rel_path}") |
| 75 | + try: |
| 76 | + with open(full_path, "r", encoding="utf-8") as f: |
| 77 | + content = f.read() |
| 78 | + except Exception as e: |
| 79 | + print(f"[WARNING] Skipping file {full_path} due to read error: {e}") |
| 80 | + continue |
| 81 | + files_dict[rel_path] = content |
| 82 | + print(f"[TRACE] Completed traversing. Total files gathered: {len(files_dict)}") |
| 83 | + return files_dict |
| 84 | + |
| 85 | +def generate_script(files_dict, crate_name): |
| 86 | + """ |
| 87 | + Generate a self-contained Python script as a string. |
| 88 | + When run, the generated script creates a folder (named after the crate) |
| 89 | + and reconstructs all files with their original contents. |
| 90 | +
|
| 91 | + This generated script also includes: |
| 92 | + - A function to copy its own source code to the clipboard. |
| 93 | + - Detailed tracing messages to follow its execution. |
| 94 | +
|
| 95 | + Each embedded file is annotated with a trailing comment indicating its relative path. |
| 96 | + |
| 97 | + Returns: |
| 98 | + A string containing the full source code of the generated script. |
| 99 | + """ |
| 100 | + lines = [] |
| 101 | + lines.append("#!/usr/bin/env python3") |
| 102 | + lines.append("import os") |
| 103 | + lines.append("import sys") |
| 104 | + lines.append("import subprocess") |
| 105 | + lines.append("") |
| 106 | + lines.append("def copy_to_clipboard(text):") |
| 107 | + lines.append(" \"\"\"") |
| 108 | + lines.append(" Copies the given text to the system clipboard.") |
| 109 | + lines.append(" Uses 'clip' on Windows and 'pbcopy' on macOS.") |
| 110 | + lines.append(" \"\"\"") |
| 111 | + lines.append(" try:") |
| 112 | + lines.append(" if sys.platform.startswith('win'):") |
| 113 | + lines.append(" proc = subprocess.Popen(['clip'], stdin=subprocess.PIPE, close_fds=True)") |
| 114 | + lines.append(" proc.communicate(input=text.encode('utf-8'))") |
| 115 | + lines.append(" elif sys.platform == 'darwin':") |
| 116 | + lines.append(" proc = subprocess.Popen(['pbcopy'], stdin=subprocess.PIPE, close_fds=True)") |
| 117 | + lines.append(" proc.communicate(input=text.encode('utf-8'))") |
| 118 | + lines.append(" else:") |
| 119 | + lines.append(" print('[TRACE] Clipboard copy not supported on this platform.')") |
| 120 | + lines.append(" except Exception as e:") |
| 121 | + lines.append(" print(f'[ERROR] Failed to copy to clipboard: {e}')") |
| 122 | + lines.append("") |
| 123 | + lines.append("def copy_self_to_clipboard():") |
| 124 | + lines.append(" \"\"\"") |
| 125 | + lines.append(" Reads its own source file and copies the content to the clipboard.") |
| 126 | + lines.append(" Provides detailed tracing for debugging.") |
| 127 | + lines.append(" \"\"\"") |
| 128 | + lines.append(" try:") |
| 129 | + lines.append(" with open(__file__, 'r', encoding='utf-8') as f:") |
| 130 | + lines.append(" content = f.read()") |
| 131 | + lines.append(" copy_to_clipboard(content)") |
| 132 | + lines.append(" print('[TRACE] The script has been copied to the clipboard.')") |
| 133 | + lines.append(" except Exception as e:") |
| 134 | + lines.append(" print(f'[ERROR] Failed to copy self to clipboard: {e}')") |
| 135 | + lines.append("") |
| 136 | + lines.append("def create_crate():") |
| 137 | + lines.append(" \"\"\"") |
| 138 | + lines.append(" Recreates the directory structure and files for the crate.") |
| 139 | + lines.append(" Provides detailed tracing for each step.") |
| 140 | + lines.append(" \"\"\"") |
| 141 | + lines.append(f" base_folder = os.path.join(os.getcwd(), '{crate_name}')") |
| 142 | + lines.append(" print(f'[TRACE] Creating base folder: {base_folder}')") |
| 143 | + lines.append(" os.makedirs(base_folder, exist_ok=True)") |
| 144 | + lines.append(" files = {") |
| 145 | + # Embed each file's content along with a comment indicating its relative path. |
| 146 | + for path, content in files_dict.items(): |
| 147 | + lines.append(f" {repr(path)}: {repr(content)}, # File: {path}") |
| 148 | + lines.append(" }") |
| 149 | + lines.append("") |
| 150 | + lines.append(" for relative_path, content in files.items():") |
| 151 | + lines.append(" full_path = os.path.join(base_folder, relative_path)") |
| 152 | + lines.append(" directory = os.path.dirname(full_path)") |
| 153 | + lines.append(" if not os.path.exists(directory):") |
| 154 | + lines.append(" os.makedirs(directory, exist_ok=True)") |
| 155 | + lines.append(" print(f'[TRACE] Created directory: {directory}')") |
| 156 | + lines.append(" with open(full_path, 'w', encoding='utf-8') as f:") |
| 157 | + lines.append(" f.write(content)") |
| 158 | + lines.append(" print(f'[TRACE] Created file: {full_path}')") |
| 159 | + lines.append("") |
| 160 | + lines.append("if __name__ == '__main__':") |
| 161 | + lines.append(" create_crate()") |
| 162 | + lines.append(" # Uncomment the next line to enable self-copy functionality.") |
| 163 | + lines.append(" # copy_self_to_clipboard()") |
| 164 | + lines.append(" print('[TRACE] Crate creation complete.')") |
| 165 | + |
| 166 | + return "\n".join(lines) |
| 167 | + |
| 168 | +def copy_to_clipboard(text): |
| 169 | + """ |
| 170 | + Copies the given text to the system clipboard. |
| 171 | + Uses 'clip' on Windows and 'pbcopy' on macOS. |
| 172 | + Provides tracing information. |
| 173 | + """ |
| 174 | + try: |
| 175 | + if sys.platform.startswith("win"): |
| 176 | + print("[TRACE] Using 'clip' for clipboard copy on Windows.") |
| 177 | + proc = subprocess.Popen(["clip"], stdin=subprocess.PIPE, close_fds=True) |
| 178 | + proc.communicate(input=text.encode("utf-8")) |
| 179 | + elif sys.platform == "darwin": |
| 180 | + print("[TRACE] Using 'pbcopy' for clipboard copy on macOS.") |
| 181 | + proc = subprocess.Popen(["pbcopy"], stdin=subprocess.PIPE, close_fds=True) |
| 182 | + proc.communicate(input=text.encode("utf-8")) |
| 183 | + else: |
| 184 | + print("[TRACE] Clipboard copy not supported on this platform.") |
| 185 | + except Exception as e: |
| 186 | + print(f"[ERROR] Failed to copy to clipboard: {e}") |
| 187 | + |
| 188 | +def find_cargo_toml(start_dir): |
| 189 | + """ |
| 190 | + Starting from start_dir, search upward for a Cargo.toml file. |
| 191 | + Returns the path to Cargo.toml if found, otherwise None. |
| 192 | + """ |
| 193 | + current_dir = start_dir |
| 194 | + while True: |
| 195 | + candidate = os.path.join(current_dir, "Cargo.toml") |
| 196 | + if os.path.exists(candidate): |
| 197 | + print(f"[TRACE] Found Cargo.toml at: {candidate}") |
| 198 | + return candidate |
| 199 | + parent = os.path.dirname(current_dir) |
| 200 | + if parent == current_dir: # Reached root directory |
| 201 | + break |
| 202 | + current_dir = parent |
| 203 | + print("[TRACE] No Cargo.toml found in the directory hierarchy.") |
| 204 | + return None |
| 205 | + |
| 206 | +def get_crate_name_from_cargo_toml(cargo_toml_path): |
| 207 | + """ |
| 208 | + Parse the Cargo.toml file to extract the crate name from the [package] section. |
| 209 | + Returns the crate name if found, otherwise None. |
| 210 | + """ |
| 211 | + in_package = False |
| 212 | + try: |
| 213 | + with open(cargo_toml_path, "r", encoding="utf-8") as f: |
| 214 | + for line in f: |
| 215 | + stripped = line.strip() |
| 216 | + if stripped.startswith("[package]"): |
| 217 | + in_package = True |
| 218 | + elif stripped.startswith("[") and in_package: |
| 219 | + # Exiting the [package] section. |
| 220 | + break |
| 221 | + elif in_package and stripped.startswith("name"): |
| 222 | + match = re.search(r'name\s*=\s*["\'](.+?)["\']', stripped) |
| 223 | + if match: |
| 224 | + crate_name = match.group(1) |
| 225 | + print(f"[TRACE] Crate name found in Cargo.toml: {crate_name}") |
| 226 | + return crate_name |
| 227 | + except Exception as e: |
| 228 | + print(f"[ERROR] Failed to parse Cargo.toml: {e}") |
| 229 | + return None |
| 230 | + |
| 231 | +def main(): |
| 232 | + parser = argparse.ArgumentParser( |
| 233 | + description="Generate a self-contained Python script that recreates a Rust crate with enhanced features." |
| 234 | + ) |
| 235 | + parser.add_argument("source_folder", help="Path to the source folder (should be within or at the crate root).") |
| 236 | + parser.add_argument("--src-only", action="store_true", help="Process only the 'src' folder inside the crate root.") |
| 237 | + args = parser.parse_args() |
| 238 | + |
| 239 | + # Resolve the absolute path of the provided source folder. |
| 240 | + orig_source_folder = os.path.abspath(args.source_folder) |
| 241 | + print(f"[TRACE] Source folder resolved to: {orig_source_folder}") |
| 242 | + |
| 243 | + # Search upward for Cargo.toml from the provided folder. |
| 244 | + cargo_toml_path = find_cargo_toml(orig_source_folder) |
| 245 | + if cargo_toml_path: |
| 246 | + # Use the directory containing Cargo.toml as the crate root. |
| 247 | + crate_root = os.path.dirname(cargo_toml_path) |
| 248 | + crate_name_from_toml = get_crate_name_from_cargo_toml(cargo_toml_path) |
| 249 | + if crate_name_from_toml: |
| 250 | + crate_name = crate_name_from_toml |
| 251 | + print(f"[TRACE] Using crate name from Cargo.toml: {crate_name}") |
| 252 | + else: |
| 253 | + crate_name = os.path.basename(orig_source_folder.rstrip(os.sep)) |
| 254 | + print("[TRACE] Cargo.toml found but crate name could not be extracted; using fallback name.") |
| 255 | + else: |
| 256 | + crate_root = orig_source_folder |
| 257 | + crate_name = os.path.basename(orig_source_folder.rstrip(os.sep)) |
| 258 | + print("[TRACE] No Cargo.toml found; using fallback crate name.") |
| 259 | + |
| 260 | + # Determine the folder to gather files from. |
| 261 | + if args.src_only: |
| 262 | + print("[TRACE] --src-only flag is set; processing only the 'src' subfolder.") |
| 263 | + source_folder = os.path.join(crate_root, "src") |
| 264 | + else: |
| 265 | + source_folder = crate_root |
| 266 | + |
| 267 | + if not os.path.exists(source_folder): |
| 268 | + print(f"[ERROR] Source folder '{source_folder}' does not exist.") |
| 269 | + sys.exit(1) |
| 270 | + |
| 271 | + # Gather files from the determined source folder. |
| 272 | + files_dict = gather_files(source_folder) |
| 273 | + |
| 274 | + # Generate the script content (this is the recreate script that will be copied to clipboard). |
| 275 | + generated_script = generate_script(files_dict, crate_name) |
| 276 | + |
| 277 | + # Create a timestamped output file name in the format: <crate_name>_recreate_YYMMDD_HHMM.py |
| 278 | + timestamp = datetime.now().strftime("%y%m%d_%H%M") |
| 279 | + output_file = f"{crate_name}_recreate_{timestamp}.py" |
| 280 | + print(f"[TRACE] Writing generated script to: {output_file}") |
| 281 | + with open(output_file, "w", encoding="utf-8") as f: |
| 282 | + f.write(generated_script) |
| 283 | + |
| 284 | + # Set the generated script to be executable. |
| 285 | + try: |
| 286 | + st = os.stat(output_file) |
| 287 | + os.chmod(output_file, st.st_mode | stat.S_IEXEC) |
| 288 | + print(f"[TRACE] Set executable permission for {output_file}.") |
| 289 | + except Exception as e: |
| 290 | + print(f"[ERROR] Failed to set executable permission for {output_file}: {e}") |
| 291 | + |
| 292 | + print(f"[TRACE] Generated script saved to {output_file}.") |
| 293 | + |
| 294 | + # Copy the generated recreate script's content to the clipboard. |
| 295 | + # (This is the script that, when run, will recreate the crate.) |
| 296 | + copy_to_clipboard(generated_script) |
| 297 | + print("[TRACE] Generated script copied to clipboard.") |
| 298 | + |
| 299 | +if __name__ == "__main__": |
| 300 | + main() |
| 301 | + |
0 commit comments