-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathuntar_and_make.py
More file actions
254 lines (234 loc) · 11.1 KB
/
Copy pathuntar_and_make.py
File metadata and controls
254 lines (234 loc) · 11.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
import os
import tarfile
import zipfile
import shutil
import argparse
import subprocess
from loguru import logger
def simplify_extracted_directory(target_directory):
"""
Simplify the extracted directory structure by flattening out unnecessary single-directory wrappers.
If target_directory contains exactly one subdirectory and no files, move that subdirectory's contents up
and remove the redundant folder. Repeat until no single subdirectory remains.
"""
while True:
items = os.listdir(target_directory)
if len(items) == 1:
single_item = os.path.join(target_directory, items[0])
if os.path.isdir(single_item):
for sub_item in os.listdir(single_item):
src = os.path.join(single_item, sub_item)
dst = os.path.join(target_directory, sub_item)
shutil.move(src, dst)
os.rmdir(single_item)
logger.info("Simplified directory structure by removing redundant folder: {}", single_item)
continue
break
def remove_whitespace_from_paths(directory):
"""
Recursively renames all files and directories in the given directory by removing whitespace characters from their names.
"""
for root, dirs, files in os.walk(directory, topdown=False):
for file in files:
new_file = file.replace(" ", "_")
if new_file != file:
old_path = os.path.join(root, file)
new_path = os.path.join(root, new_file)
os.rename(old_path, new_path)
logger.info("Renamed file {} to {}", old_path, new_path)
for d in dirs:
new_d = d.replace(" ", "_")
if new_d != d:
old_path = os.path.join(root, d)
new_path = os.path.join(root, new_d)
os.rename(old_path, new_path)
logger.info("Renamed directory {} to {}", old_path, new_path)
def remove_assignsubmission_folder(directory):
"""
Checks for any subdirectory whose name contains '_assignsubmission_file'.
If found, moves its contents up to the given directory and removes the folder.
"""
for item in os.listdir(directory):
item_path = os.path.join(directory, item)
if os.path.isdir(item_path) and "_assignsubmission_file" in item:
for sub_item in os.listdir(item_path):
src = os.path.join(item_path, sub_item)
dst = os.path.join(directory, sub_item)
shutil.move(src, dst)
os.rmdir(item_path)
logger.info("Removed assignsubmission folder: {}", item_path)
def filter_specific_warning(text):
"""
Filters out the specific CMake deprecation warning regarding compatibility with CMake < 3.5.
Only removes the block of text associated with that warning, leaving other warnings intact.
"""
lines = text.splitlines(keepends=True)
filtered_lines = []
skip = False
for line in lines:
if not skip and "CMake Deprecation Warning" in line and "cmake_minimum_required" in line:
skip = True
continue
if skip:
if line.startswith(" ") or line.strip() == "":
continue
else:
skip = False
filtered_lines.append(line)
return "".join(filtered_lines)
def run_command(command, working_directory):
"""
Runs a shell command in the specified working directory.
Captures stdout and stderr, filters out the specific CMake deprecation warning for cmake commands,
logs the results, and returns a tuple (return_code, stdout, stderr).
"""
logger.info("Running command '{}' in directory '{}'", command, working_directory)
process = subprocess.Popen(
command,
shell=True,
cwd=working_directory,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
stdout, stderr = process.communicate()
# If this is a cmake command, filter out only the specific deprecation warning.
if "cmake" in command:
stdout = filter_specific_warning(stdout)
stderr = filter_specific_warning(stderr)
if stdout:
logger.info("Command output:\n{}", stdout)
if stderr:
logger.error("Command errors:\n{}", stderr)
return process.returncode, stdout, stderr
def extract_archive(archive_path, target_directory):
"""
Extracts the archive (tar.gz, .tgz, or .zip) into the target directory.
Then simplifies the directory structure, removes whitespace characters from paths, and
removes any extra '_assignsubmission_file' folder layer.
"""
if archive_path.endswith((".tar.gz", ".tgz")):
try:
os.makedirs(target_directory, exist_ok=True)
with tarfile.open(archive_path, "r:*") as tar:
tar.extractall(path=target_directory)
logger.info("Extracted {} into {}", archive_path, target_directory)
except Exception as e:
logger.error("Error extracting {}: {}", archive_path, e)
elif archive_path.endswith(".zip"):
try:
os.makedirs(target_directory, exist_ok=True)
with zipfile.ZipFile(archive_path, "r") as zip_ref:
zip_ref.extractall(target_directory)
logger.info("Extracted {} into {}", archive_path, target_directory)
except Exception as e:
logger.error("Error extracting {}: {}", archive_path, e)
else:
logger.warning("Unsupported file format: {}", archive_path)
simplify_extracted_directory(target_directory)
remove_whitespace_from_paths(target_directory)
remove_assignsubmission_folder(target_directory)
def find_main_cpp_directory(directory):
"""
Recursively searches for the directory containing 'Main.cpp' within the given directory.
Returns the path to that directory if found, otherwise returns None.
"""
for root, dirs, files in os.walk(directory):
if "Main.cpp" in files:
return root
return None
def build_project(project_directory):
"""
Searches for the folder containing 'Main.cpp' within the project_directory.
If found, deletes any existing 'build' folder, creates a new one,
runs cmake and make in it, and saves the output of these commands to a build.log file.
"""
main_cpp_dir = find_main_cpp_directory(project_directory)
if main_cpp_dir is None:
logger.warning("Main.cpp not found in project {}. Skipping build.", project_directory)
return
logger.info("Found Main.cpp in directory: {}", main_cpp_dir)
build_dir = os.path.join(main_cpp_dir, "build")
if os.path.exists(build_dir):
try:
shutil.rmtree(build_dir)
logger.info("Deleted existing build directory: {}", build_dir)
except Exception as e:
logger.error("Error deleting build directory {}: {}", build_dir, e)
os.makedirs(build_dir, exist_ok=True)
build_log_path = os.path.join(build_dir, "build.log")
try:
with open(build_log_path, "w") as log_file:
log_file.write("=== Running cmake .. ===\n")
cmake_command = "cmake .."
ret, out, err = run_command(cmake_command, build_dir)
log_file.write(out)
log_file.write(err)
if ret != 0:
logger.error("cmake failed with return code {}", ret)
log_file.write("cmake failed with return code {}\n".format(ret))
return
log_file.write("\n=== Running make ===\n")
ret, out, err = run_command("make", build_dir)
log_file.write(out)
log_file.write(err)
if ret != 0:
logger.error("make failed with return code {}", ret)
log_file.write("make failed with return code {}\n".format(ret))
else:
logger.info("Build completed successfully in {}", build_dir)
log_file.write("Build completed successfully in {}\n".format(build_dir))
except Exception as e:
logger.exception("Error during build process in {}: {}", build_dir, e)
def main(input_dir, output_dir):
"""
Recursively searches the input_dir for archives.
For each archive:
- Creates a corresponding directory in output_dir preserving the subdirectory structure,
- When the student's folder (from the relative path) contains '_assignsubmission_file',
it is removed from the final folder name.
- If such a folder is detected, the archive's own subfolder is omitted to avoid extra nesting.
- Extracts the archive into the target directory, simplifies its path, and builds the project.
"""
if not os.path.isdir(input_dir):
logger.error("Input directory does not exist: {}", input_dir)
return
for root, dirs, files in os.walk(input_dir):
for file in files:
if file.endswith((".tar.gz", ".tgz", ".zip")):
archive_path = os.path.join(root, file)
# Compute the relative path with respect to input_dir.
rel_path = os.path.relpath(root, input_dir)
# Clean the relative path: remove whitespaces and remove the substring "_assignsubmission_file"
clean_rel_path = os.path.sep.join(part.replace(" ", "_") for part in rel_path.split(os.path.sep))
if "_assignsubmission_file" in clean_rel_path:
clean_rel_path = clean_rel_path.replace("_assignsubmission_file", "")
# When the student's folder already has the assignsubmission substring, do not add an extra project folder.
target_directory = os.path.join(output_dir, clean_rel_path)
else:
# Determine the project name (archive name without extension), also remove spaces.
if file.endswith(".tar.gz"):
project_name = file[:-7]
elif file.endswith(".tgz"):
project_name = file[:-4]
elif file.endswith(".zip"):
project_name = file[:-4]
else:
project_name = file
project_name = project_name.replace(" ", "_")
target_directory = os.path.join(output_dir, clean_rel_path, project_name)
# Ensure the directory for the clean relative path exists.
os.makedirs(os.path.join(output_dir, clean_rel_path), exist_ok=True)
logger.info("Processing archive: {}", archive_path)
extract_archive(archive_path, target_directory)
build_project(target_directory)
else:
logger.info("Skipping unsupported file: {}", os.path.join(root, file))
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Recursive extraction, cleaning build directories, and project build with logging"
)
parser.add_argument("input_dir", help="Path to the directory containing student archives (e.g. /path/to/archives)")
parser.add_argument("output_dir", help="Path to the directory where extracted projects will be placed")
args = parser.parse_args()
main(args.input_dir, args.output_dir)