From 229bcdb9c21101e5a6f3e7b3d164091b8e6fa901 Mon Sep 17 00:00:00 2001 From: Brian Ernst Date: Mon, 10 Nov 2025 13:39:58 -0800 Subject: [PATCH 1/3] Add Unreal cleanup script. --- source/project/unreal_clean.py | 148 +++++++++++++++++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 source/project/unreal_clean.py diff --git a/source/project/unreal_clean.py b/source/project/unreal_clean.py new file mode 100644 index 0000000..e527c9f --- /dev/null +++ b/source/project/unreal_clean.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python + +""" +Drop this script into a folder you want to clean, and run it. +Eventually I'll configure this as runnable from a scripts directory without having to drop it at a root directory; that relies on a config. If no config found, it will run in current directory. +""" + +__license__ = "MPL 2.0" +__version__ = "0.1" +__credits__ = ["Brian Ernst (leetNightshade)"] + +__maintainer__ = "leetNightshade" +__status__ = "Prototype" + +import argparse +import os +import pathlib +import shutil +import stat +import sys +import time + +ignore_folders = { + '.git' +} + +dirs_to_delete = { + ".idea", + ".vs", + "Binaries", + "DerivedDataCache", + "Intermediate", + "Saved", +} +files_to_delete = { + ".vsconfig", + "*.sln", +} + +def get_file_directory(): + return pathlib.Path(__file__).parent + +def del_rw(action, name, exc): + os.chmod(name, stat.S_IWRITE) + os.remove(name) + +def clean_unreal_temps(dry_run = False, verbose = False, quiet = False): + + target_path = get_file_directory() + + count_dirs_deleted = 0 + count_files_deleted = 0 + + if not quiet: + print(f"Iterating {target_path} for directories and files to cleanup{', doing a dry-run ' if dry_run else ''}...") + + # TODO: If not doing a dry run could give people a 5+ second countdown or something to cancel the script. + + start = time.time() + + counted_iterated = 0 + + for root, dirs, files, in os.walk(target_path): + counted_iterated += 1 + + # Don't waste time processing an empty directory. + if len(dirs) == 0 and len(files) == 0: + continue + + root_path = pathlib.Path(root) + + for dir in reversed(dirs): + if dir in ignore_folders: + dirs.remove(dir) + continue + + if dir not in dirs_to_delete: + # We do want os.walk to continue to look into this directory. + continue + + # We're processing a directory we're going to delete, no need to leave + # os.walk to iterate over the files in this directory. Helpful during + # a dry-run to run similarly during non-dry-run. But also if we were + # multi-threaded, this wouldn't try to iterate files we're already + # trying to delete. + dirs.remove(dir) + + dir_path = root_path / dir + + if not dry_run: + shutil.rmtree(dir_path, onerror=del_rw) + + count_dirs_deleted += 1 + + if verbose and not quiet: + if dry_run: + print(f" Pretend Deleted: {dir_path}") + else: + print(f" Deleted: {dir_path}") + + for file in files: + if not any(pathlib.PurePath(file).match(pattern) for pattern in files_to_delete): + continue + + file_path = root_path / file + + if not dry_run: + file_path.unlink() + + count_files_deleted += 1 + + if verbose and not quiet: + if dry_run: + print(f" Pretend Deleted: {dir_path}") + else: + print(f" Deleted: {dir_path}") + + # Done, summarize everything. + if not quiet: + prefix = None + if dry_run: + prefix = "Dry-run, would have deleted" + else: + prefix = "Deleted" + + + print(f" {prefix} {count_dirs_deleted} director{'ies' if count_dirs_deleted != 1 else 'y'}, {count_files_deleted} file{'s' if count_files_deleted != 1 else ''}. Iterated {counted_iterated} paths.") + + if count_dirs_deleted > 0: + print(f" (NOTE: Number of {'potentially ' if dry_run else ''}deleted files does not currently include a count of files from a deleted directory.)") + + print(f" Done. Finished in {str(time.time() - start)}s") + +if __name__ == "__main__": + + parser = argparse.ArgumentParser(description="Script to clean temp Unreal files/folders") + + parser.add_argument('--dry-run', '-n', action='store_true', help='Dry run without any modifications.') + parser.add_argument('--unattended', '-u', action='store_true', help='Run without waiting.') + parser.add_argument('--quiet', '-q', action='store_true', help='Quiet output.') + parser.add_argument('--verbose', '-v', action='store_true', help='Verbose output.') + + args = parser.parse_args() + + clean_unreal_temps(dry_run = args.dry_run, verbose = args.verbose) + + if not args.unattended: + input("Waiting for input...") From 9e741096041a64e8c7e18b2b185818bef9381b20 Mon Sep 17 00:00:00 2001 From: Brian Ernst Date: Mon, 10 Nov 2025 13:56:12 -0800 Subject: [PATCH 2/3] Fixed up Unreal cleanup script. Included folders and files that might be included with source control, removed some configured paths. Tidied up script. --- source/project/unreal_clean.py | 126 ++++++++++++++++++--------------- 1 file changed, 68 insertions(+), 58 deletions(-) diff --git a/source/project/unreal_clean.py b/source/project/unreal_clean.py index e527c9f..6c837f7 100644 --- a/source/project/unreal_clean.py +++ b/source/project/unreal_clean.py @@ -24,17 +24,18 @@ ignore_folders = { '.git' } +# NOTE: These are recursively deleted. dirs_to_delete = { - ".idea", ".vs", - "Binaries", "DerivedDataCache", "Intermediate", "Saved", } + +# NOTE: These are recursively deleted. +# Be careful not to mark for deletion files that are included with your version +# control. files_to_delete = { - ".vsconfig", - "*.sln", } def get_file_directory(): @@ -54,82 +55,91 @@ def clean_unreal_temps(dry_run = False, verbose = False, quiet = False): if not quiet: print(f"Iterating {target_path} for directories and files to cleanup{', doing a dry-run ' if dry_run else ''}...") - # TODO: If not doing a dry run could give people a 5+ second countdown or something to cancel the script. + # TODO: If not doing a dry run could give people a 5+ second countdown or + # something to cancel the script. start = time.time() counted_iterated = 0 - for root, dirs, files, in os.walk(target_path): - counted_iterated += 1 + if len(dirs_to_delete) or len(files_to_delete): + for root, dirs, files, in os.walk(target_path): + counted_iterated += 1 - # Don't waste time processing an empty directory. - if len(dirs) == 0 and len(files) == 0: - continue - - root_path = pathlib.Path(root) - - for dir in reversed(dirs): - if dir in ignore_folders: - dirs.remove(dir) + # Don't waste time processing an empty directory. + if len(dirs) == 0 and len(files) == 0: continue - if dir not in dirs_to_delete: - # We do want os.walk to continue to look into this directory. - continue + root_path = pathlib.Path(root) - # We're processing a directory we're going to delete, no need to leave - # os.walk to iterate over the files in this directory. Helpful during - # a dry-run to run similarly during non-dry-run. But also if we were - # multi-threaded, this wouldn't try to iterate files we're already - # trying to delete. - dirs.remove(dir) + if len(dirs_to_delete) or len(ignore_folders): + for dir in reversed(dirs): + if dir in ignore_folders: + dirs.remove(dir) + continue - dir_path = root_path / dir - - if not dry_run: - shutil.rmtree(dir_path, onerror=del_rw) + if dir not in dirs_to_delete: + # We do want os.walk to continue to look into this directory. + continue - count_dirs_deleted += 1 + # We're processing a directory we're going to delete, no need to leave + # os.walk to iterate over the files in this directory. Helpful during + # a dry-run to run similarly during non-dry-run. But also if we were + # multi-threaded, this wouldn't try to iterate files we're already + # trying to delete. + dirs.remove(dir) - if verbose and not quiet: - if dry_run: - print(f" Pretend Deleted: {dir_path}") - else: - print(f" Deleted: {dir_path}") + dir_path = root_path / dir + + if not dry_run: + shutil.rmtree(dir_path, onerror=del_rw) - for file in files: - if not any(pathlib.PurePath(file).match(pattern) for pattern in files_to_delete): - continue + count_dirs_deleted += 1 - file_path = root_path / file + if verbose and not quiet: + if dry_run: + print(f" Pretend Deleted: {dir_path}") + else: + print(f" Deleted: {dir_path}") - if not dry_run: - file_path.unlink() + if len(files_to_delete): + for file in files: + if not any(pathlib.PurePath(file).match(pattern) for pattern in files_to_delete): + continue - count_files_deleted += 1 + file_path = root_path / file - if verbose and not quiet: - if dry_run: - print(f" Pretend Deleted: {dir_path}") - else: - print(f" Deleted: {dir_path}") + if not dry_run: + file_path.unlink() - # Done, summarize everything. - if not quiet: - prefix = None - if dry_run: - prefix = "Dry-run, would have deleted" - else: - prefix = "Deleted" + count_files_deleted += 1 + + if verbose and not quiet: + if dry_run: + print(f" Pretend Deleted: {file_path}") + else: + print(f" Deleted: {file_path}") + + # Done, summarize everything. + if not quiet: + prefix = None + if dry_run: + prefix = "Dry-run, would have deleted" + else: + prefix = "Deleted" - print(f" {prefix} {count_dirs_deleted} director{'ies' if count_dirs_deleted != 1 else 'y'}, {count_files_deleted} file{'s' if count_files_deleted != 1 else ''}. Iterated {counted_iterated} paths.") + print(f" {prefix} {count_dirs_deleted} director{'ies' if count_dirs_deleted != 1 else 'y'}, {count_files_deleted} file{'s' if count_files_deleted != 1 else ''}. Iterated {counted_iterated} paths.") - if count_dirs_deleted > 0: - print(f" (NOTE: Number of {'potentially ' if dry_run else ''}deleted files does not currently include a count of files from a deleted directory.)") + # TODO: Add up the file size deleted (including from deleted folders) + # in a verbose run, or have that as an extra param. - print(f" Done. Finished in {str(time.time() - start)}s") + if count_dirs_deleted > 0: + print(f" (NOTE: Number of {'potentially ' if dry_run else ''}deleted files does not include files from a deleted directory.)") + + print(f" Done. Finished in {str(time.time() - start)}s") + else: + print(" Skipped. Nothing configured to be deleted.") if __name__ == "__main__": From b9c7f7519c1ab7e04e203967ebee69e53ae3994b Mon Sep 17 00:00:00 2001 From: Brian Ernst Date: Mon, 10 Nov 2025 14:09:48 -0800 Subject: [PATCH 3/3] Added note about why Binaries folder isn't included. --- source/project/unreal_clean.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/source/project/unreal_clean.py b/source/project/unreal_clean.py index 6c837f7..298e622 100644 --- a/source/project/unreal_clean.py +++ b/source/project/unreal_clean.py @@ -27,6 +27,11 @@ ignore_folders = { # NOTE: These are recursively deleted. dirs_to_delete = { ".vs", + # The Binaries folder commonly contains checked-in files. This script + # isn't designed to check against version control, it's supposed to + # be a quick simple script to quickly get back disk space. Uncomment + # this folder at your own peril. + #"Binaries", "DerivedDataCache", "Intermediate", "Saved",