From: snickersun Date: Sat, 11 Apr 2026 07:47:45 +0000 (+0800) Subject: 修复win11上编译问题 X-Git-Url: http://git.snicker.com.cn/?a=commitdiff_plain;h=945f8eb5a0039a1a2e2a89157099332cc93727d4;p=mirrors%2FASR1603_G5.git 修复win11上编译问题 --- diff --git a/2.sdk/Helios_SDK/Makefile b/2.sdk/Helios_SDK/Makefile index f1b50291..2a34b4f9 100644 --- a/2.sdk/Helios_SDK/Makefile +++ b/2.sdk/Helios_SDK/Makefile @@ -467,7 +467,7 @@ common_clean: clean: private_clean common_clean - +COMSPEC:=$(shell echo %COMSPEC%) ################################################################# # # @@ -486,13 +486,8 @@ endef # Else, in BASH environment else - -# Get helios version -ifeq ($(shell type -P helios),) - HELIOS_VERSION := -else - HELIOS_VERSION := $(shell helios --version | grep -Po 'Helios \(released by Quectel\) \K[^\s]+') -endif +# Get helios version (reuse the existing Windows batch for consistent parsing) +HELIOS_VERSION := $(shell cmd /c get_helios_version.bat) # Define a macro to check if a string represents a number define is_number $(shell echo "$1" | grep -q '^[0-9]\+$' && echo "true" || echo "false") @@ -506,19 +501,10 @@ ifeq ($(HELIOS_VERSION),) else ifeq ($(HELIOS_VERSION),1.4.1) IS_NEWER_THAN_1_4_1 := true else - # Split the version into parts - HELIOS_VERSION_PARTS := $(subst ., ,$(HELIOS_VERSION)) - # Check if major, minor, and patch are numbers - IS_MAJOR_NUMBER := $(call is_number,$(word 1,$(HELIOS_VERSION_PARTS))) - IS_MINOR_NUMBER := $(call is_number,$(word 2,$(HELIOS_VERSION_PARTS))) - IS_PATCH_NUMBER := $(call is_number,$(word 3,$(HELIOS_VERSION_PARTS))) - - # If any part is not a number, report an error - ifneq ($(IS_MAJOR_NUMBER)$(IS_MINOR_NUMBER)$(IS_PATCH_NUMBER),truetruetrue) + # Parse and compare semantic version in one step (avoid shell quoting issues). + IS_NEWER_THAN_1_4_1 := $(shell $(PY) -c "import sys; v=sys.argv[1].strip(); parts=v.split('.'); ok=(len(parts)==3 and all(p.isdigit() for p in parts)); print('invalid' if not ok else ('true' if tuple(map(int,parts)) >= (1,4,1) else 'false'))" "$(HELIOS_VERSION)") + ifeq ($(IS_NEWER_THAN_1_4_1),invalid) $(error "Unexpected version format: $(HELIOS_VERSION)") - else - # Compare the version with 1.4.1 - IS_NEWER_THAN_1_4_1 := $(shell echo "$(HELIOS_VERSION)" | awk -F. '{if ($$1 > 1 || ($$1 == 1 && $$2 >= 4 && $$3 >= 1)) print "true"; else print "false"}') endif endif diff --git a/2.sdk/Helios_SDK/compile.py b/2.sdk/Helios_SDK/compile.py index 5fcf025c..06d3a358 100644 --- a/2.sdk/Helios_SDK/compile.py +++ b/2.sdk/Helios_SDK/compile.py @@ -12,9 +12,40 @@ # See the License for the specific language governing permissions and # limitations under the License. -import json, os, re, sys +import json, os, re, sys, shutil, subprocess dep_dict = {} +SYSTEMROOT = os.environ.get('SystemRoot', r'C:\Windows') +# 不信任 COMSPEC/ComSpec(批处理/工具链可能把它设成相对路径 cmd.exe),直接固定用系统 cmd.exe 绝对路径 +CMD_EXE = os.path.join(SYSTEMROOT, 'System32', 'cmd.exe') + +def _inject_make_abs_path(cmd: str) -> str: + """把命令开头的 make 替换成绝对路径,避免 PATH/编码导致找不到可执行文件。""" + make_exe = shutil.which('make') + if make_exe and cmd.lstrip().startswith('make '): + # 只替换开头的 'make ',保留后续参数原样 + prefix_len = len(cmd) - len(cmd.lstrip()) + return cmd[:prefix_len] + make_exe + cmd[prefix_len + 4:] + return cmd + +def run_cmd_capture(cmd: str) -> bytes: + """ + 显式用 cmd.exe 执行(不要依赖 COMSPEC/默认 shell),并捕获 stdout+stderr。 + """ + cmd = _inject_make_abs_path(cmd) + p = subprocess.Popen( + [CMD_EXE, '/d', '/s', '/c', cmd], + shell=False, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) + out, _ = p.communicate() + return out or b'' + +def system_cmd(cmd: str) -> int: + """用 subprocess.call 显式 cmd.exe 执行。""" + cmd = _inject_make_abs_path(cmd) + return subprocess.call([CMD_EXE, '/d', '/s', '/c', cmd], shell=False) def not_empty(s): if not s == '': @@ -43,9 +74,7 @@ def search_for_deps(comp_path): comp_mk = comp_path + '/' + comp_path.split('/')[-1] + '.mk' cmd = 'make --no-print-directory -f comp_info_tell.mk' + ' COMP_MK=' + comp_mk - res = os.popen(cmd) - ret = res.buffer.read().decode(encoding='utf-8') - res.close() + ret = run_cmd_capture(cmd).decode(encoding='utf-8', errors='ignore') if not ret: raise RuntimeError('cmd [%s] execution error' % cmd) regex = re.compile(r'NAME: (.*)\nGLOBAL_INCS: (.*)\nGLOBAL_DEFINE: (.*)\nLOCAL_CFLAGS: (.*)\nLOCAL_ARCHIVES: (.*)\nLOCAL_SRCS: (.*)\nLOCAL_SRCS_2: (.*)\nLOCAL_SRCS_3: (.*)\nLOCAL_INCS: (.*)\nLOCAL_DEFINE: (.*)\nCOMP_DEPS: (.*)\n') @@ -193,20 +222,25 @@ def compile_proc(): if 'DEF_COLLECT' in v and v['DEF_COLLECT']: comp_dflags = ' '.join(v['DEF_COLLECT']) os.environ['COMP_DFLAGS'] = comp_dflags - if os.system(cmd): + if system_cmd(cmd): raise RuntimeError('cmd [%s] execution error' % cmd) if 'LOCAL_ARCHIVES' in v and v['LOCAL_ARCHIVES']: user_archive_list.extend(list(map(lambda x : k + '/' + x, v['LOCAL_ARCHIVES']))) target_bin = os.environ['TARGET_BIN'] - cmd = 'make --no-print-directory -f link.mk ' + target_bin + ' COMP_ARCHIVES="' + ' '.join(comp_archive_list) + '"' + ' USER_ARCHIVES="' + ' '.join(user_archive_list) + '"' - if os.system(cmd): - raise RuntimeError('cmd [%s] execution error' % cmd) + make_exe = shutil.which('make') or 'make' + env = os.environ.copy() + env['COMP_ARCHIVES'] = ' '.join(comp_archive_list) + env['USER_ARCHIVES'] = ' '.join(user_archive_list) + if subprocess.call([make_exe, '--no-print-directory', '-f', 'link.mk', target_bin], env=env): + raise RuntimeError('cmd [make -f link.mk] execution error') if os.environ['PLAT'] != 'RDA' and os.environ['PLAT'] != 'Qualcomm': target_hex = os.environ['TARGET_HEX'] - cmd = 'make --no-print-directory -f link.mk ' + target_hex + ' COMP_ARCHIVES="' + ' '.join(comp_archive_list) + '"' + ' USER_ARCHIVES="' + ' '.join(user_archive_list) + '"' - if os.system(cmd): - raise RuntimeError('cmd [%s] execution error' % cmd) + env = os.environ.copy() + env['COMP_ARCHIVES'] = ' '.join(comp_archive_list) + env['USER_ARCHIVES'] = ' '.join(user_archive_list) + if subprocess.call([make_exe, '--no-print-directory', '-f', 'link.mk', target_hex], env=env): + raise RuntimeError('cmd [make -f link.mk] execution error') def build_proc(app): print('\n[searching for all components ...] ', end = '', flush=True) diff --git a/2.sdk/Helios_SDK/link.mk b/2.sdk/Helios_SDK/link.mk index 9ad2bcf7..854c0b3d 100644 --- a/2.sdk/Helios_SDK/link.mk +++ b/2.sdk/Helios_SDK/link.mk @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +USE_WHOLE_ARCHIVE ?= y + $(TARGET_BIN): $(TARGET_ELF) ifeq ($(strip $(PLAT)),Qualcomm) $(Q)$(OBJCOPY_PY) --bin $^ --output $@ @@ -36,12 +38,29 @@ $(OUTPUT_LD_FILE): $(LD_FILE) $(TARGET_ELF): $(OUTPUT_LD_FILE) $(COMP_ARCHIVES) $(USER_ARCHIVES) $(STD_LIBS) @echo ifeq ($(strip $(PLAT)),ECR6600) +ifeq ($(strip $(USE_WHOLE_ARCHIVE)),n) + # ROM overflow 时,保留 microPython 与 USER_ARCHIVES 的 whole-archive, + # 其余 COMP_ARCHIVES 走普通按需拉取,避免输出空壳。 + $(Q)$(CC) -Wl,--whole-archive \ + $(filter %/services/microPython/libmicroPython.a,$(COMP_ARCHIVES)) $(USER_ARCHIVES) \ + -Wl,--no-whole-archive $(filter-out %/services/microPython/libmicroPython.a,$(COMP_ARCHIVES)) $(STD_LIBS) \ + -o $@ $(LDFLAGS) +else $(Q)$(CC) -Wl,--whole-archive \ - $(COMP_ARCHIVES) $(USER_ARCHIVES) -Wl,--no-whole-archive $(STD_LIBS) \ - -o $@ $(LDFLAGS) + $(COMP_ARCHIVES) $(USER_ARCHIVES) -Wl,--no-whole-archive $(STD_LIBS) \ + -o $@ $(LDFLAGS) +endif else +ifeq ($(strip $(USE_WHOLE_ARCHIVE)),n) + # 同上:只对 microPython 与 USER_ARCHIVES 使用 whole-archive $(LD) --whole-archive \ - $(COMP_ARCHIVES) $(USER_ARCHIVES) --no-whole-archive $(STD_LIBS) \ - -o $@ $(LDFLAGS) + $(filter %/services/microPython/libmicroPython.a,$(COMP_ARCHIVES)) $(USER_ARCHIVES) \ + --no-whole-archive $(filter-out %/services/microPython/libmicroPython.a,$(COMP_ARCHIVES)) $(STD_LIBS) \ + -o $@ $(LDFLAGS) +else + $(LD) --whole-archive \ + $(COMP_ARCHIVES) $(USER_ARCHIVES) --no-whole-archive $(STD_LIBS) \ + -o $@ $(LDFLAGS) +endif endif @echo ----- $@ updated diff --git a/2.sdk/build.bat b/2.sdk/build.bat index ef3c24d9..f0aa9ee8 100644 --- a/2.sdk/build.bat +++ b/2.sdk/build.bat @@ -1,4 +1,7 @@ @echo off +set PATH=D:\helios\bin;%PATH% +@REM set COMSPEC=cmd.exe +set "COMSPEC=%SystemRoot%\System32\cmd.exe" cd Helios_SDK helios make services/microPython @EG810MCN_GA_ALIPAY EG810MCN_GA_ALIPAY cd .. diff --git a/2.sdk/clean.bat b/2.sdk/clean.bat index eb57536c..ab4aa081 100644 --- a/2.sdk/clean.bat +++ b/2.sdk/clean.bat @@ -1,5 +1,7 @@ @echo off - +set PATH=D:\helios\bin;%PATH% +@REM set COMSPEC=cmd.exe +set "COMSPEC=%SystemRoot%\System32\cmd.exe" cd Helios_SDK helios clean cd .. diff --git a/2.sdk/new_clean.bat b/2.sdk/new_clean.bat index 4c55cdbb..f2e19e6a 100644 --- a/2.sdk/new_clean.bat +++ b/2.sdk/new_clean.bat @@ -1,5 +1,6 @@ @echo off - +set PATH=D:\helios\bin;%PATH% +set "COMSPEC=%SystemRoot%\System32\cmd.exe" cd Helios_SDK helios clean cd .. diff --git a/2.sdk/project/7PIN/new.bat b/2.sdk/project/7PIN/new.bat index bd2d9eaa..853a796a 100644 --- a/2.sdk/project/7PIN/new.bat +++ b/2.sdk/project/7PIN/new.bat @@ -5,31 +5,31 @@ del images /q @GOTO :STEP1 ..\..\Helios_SDK\tools\ASR_1606\win32\mklfs.exe -c ext -b 4096 -r 4096 -p 4096 -s 1048576 -i .\images\ext_fs.bin if errorlevel 1 ( - echo "ext_fs.binÉú³Éʧ°Ü !!!!!!!!!!!!!!! " + echo "ext_fs.bin����ʧ�� !!!!!!!!!!!!!!! " goto exit ) if errorlevel 0 ( - echo "ext_fs.binÉú³É³É¹¦ !!!!!!!!!!!!!!! " + echo "ext_fs.bin���ɳɹ� !!!!!!!!!!!!!!! " ) :STEP1 ..\..\Helios_SDK\tools\ASR_1606\win32\mklfs.exe -c usr -b 4096 -r 4096 -p 4096 -s 1376256 -i .\images\customer_fs.bin if errorlevel 1 ( - echo "customer_fs.binÉú³Éʧ°Ü Çë¼ì²éusrĿ¼ÊÇ·ñ´óÓÚ316K!!!!!!!!!!!!!!!" + echo "customer_fs.bin����ʧ�� ����usrĿ¼�Ƿ����316K!!!!!!!!!!!!!!!" goto exit ) if errorlevel 0 ( - echo "customer_fs.binÉú³É³É¹¦ !!!!!!!!!!!!!!! " + echo "customer_fs.bin���ɳɹ� !!!!!!!!!!!!!!! " ) %copy font\ext_font.bin images\ext_font.bin% ..\..\Helios_SDK\tools\common\win32\7z\7z a sdk\system\platform\ASR_1606\boards\EG810MCN_GA_VOLTE\images\firmware_base_release_pack.zip .\images\ if errorlevel 0 ( - echo imagesѹËõ³É¹¦!!!!!!!!!!!!!! + echo imagesѹ���ɹ�!!!!!!!!!!!!!! ) if errorlevel 1 ( - echo imagesѹËõʧ°Ü!!!!!!!!!!!!!!!!!! + echo imagesѹ��ʧ��!!!!!!!!!!!!!!!!!! goto exit ) xcopy sdk\ ..\..\Helios_SDK\ /e/h/y diff --git a/2.sdk/project/7PIN_1_5/new.bat b/2.sdk/project/7PIN_1_5/new.bat index 65596b90..396c014a 100644 --- a/2.sdk/project/7PIN_1_5/new.bat +++ b/2.sdk/project/7PIN_1_5/new.bat @@ -1,39 +1,38 @@ @echo off - md images del images /q @GOTO :STEP1 ..\..\Helios_SDK\tools\ASR_1606\win32\mklfs.exe -c ext -b 4096 -r 4096 -p 4096 -s 1048576 -i .\images\ext_fs.bin if errorlevel 1 ( - echo "ext_fs.binÉú³Éʧ°Ü !!!!!!!!!!!!!!! " + echo "ext_fs.bin生成失败 !!!!!!!!!!!!!!! " goto exit ) if errorlevel 0 ( - echo "ext_fs.binÉú³É³É¹¦ !!!!!!!!!!!!!!! " + echo "ext_fs.bin生成成功 !!!!!!!!!!!!!!! " ) :STEP1 ..\..\Helios_SDK\tools\ASR_1606\win32\mklfs.exe -c usr -b 4096 -r 4096 -p 4096 -s 1376256 -i .\images\customer_fs.bin if errorlevel 1 ( - echo "customer_fs.binÉú³Éʧ°Ü Çë¼ì²éusrĿ¼ÊÇ·ñ´óÓÚ316K!!!!!!!!!!!!!!!" + echo "customer_fs.bin生成失败 请检查usr目录是否大于316K!!!!!!!!!!!!!!!" goto exit ) if errorlevel 0 ( - echo "customer_fs.binÉú³É³É¹¦ !!!!!!!!!!!!!!! " + echo "customer_fs.bin生成成功 !!!!!!!!!!!!!!! " ) %copy font\ext_font.bin images\ext_font.bin% ..\..\Helios_SDK\tools\common\win32\7z\7z a sdk\system\platform\ASR_1606\boards\EG810MCN_GA_VOLTE\images\firmware_base_release_pack.zip .\images\ if errorlevel 0 ( - echo imagesѹËõ³É¹¦!!!!!!!!!!!!!! + echo images压缩成功!!!!!!!!!!!!!! ) if errorlevel 1 ( - echo imagesѹËõʧ°Ü!!!!!!!!!!!!!!!!!! + echo images压缩失败!!!!!!!!!!!!!!!!!! goto exit ) xcopy sdk\ ..\..\Helios_SDK\ /e/h/y cd ..\..\Helios_SDK -helios make services/microPython @EG810MCN_GA_VOLTE EG810MCN_GA_VOLTE +helios.exe make services/microPython @EG810MCN_GA_VOLTE EG810MCN_GA_VOLTE cd .. :exit \ No newline at end of file diff --git a/gitdiff.py b/gitdiff.py new file mode 100644 index 00000000..e580d4f7 --- /dev/null +++ b/gitdiff.py @@ -0,0 +1,234 @@ +import os +import subprocess +import datetime +import sys +from pathlib import Path + +def run_git_command(command): + try: + # 设置环境变量以正确处理中文文件名 + env = os.environ.copy() + env['LC_ALL'] = 'C.UTF-8' + env['LANG'] = 'C.UTF-8' + + result = subprocess.run( + command, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding='utf-8', + errors='replace', + check=True, + env=env + ) + return result.stdout.strip() + except subprocess.CalledProcessError as e: + print(f"Error executing git command: {e.stderr}") + # 不要立即退出,让调用者处理错误 + raise + except UnicodeDecodeError as e: + print(f"Encoding error: {e}") + raise + +def main(): + if len(sys.argv) > 3: + print("Usage: python git_changes_extractor.py [commit1 [commit2]]") + print(" If no arguments are provided, uncommitted changes will be extracted") + print(" If only commit1 is provided, it defaults to comparing with HEAD") + sys.exit(1) + + # 处理不同参数情况 + if len(sys.argv) == 1: + commit1 = None + commit2 = None + mode = "uncommitted" + elif len(sys.argv) == 2: + commit1 = sys.argv[1] + commit2 = "HEAD" + mode = "commits" + else: + commit1 = sys.argv[1] + commit2 = sys.argv[2] + mode = "commits" + + # 获取当前仓库路径和名称 + repo_path = Path(os.getcwd()) + repo_name = repo_path.name + + # 创建输出目录 (格式: 仓库名_时间戳) + timestamp = datetime.datetime.now().strftime("%y%m%d%H%M%S") + output_dir_name = f"{repo_name}_{timestamp}" + patches_dir = repo_path.parent / "PATCHES" + output_dir = patches_dir / output_dir_name + output_dir.mkdir(parents=True, exist_ok=True) + + # 创建diff子目录 + diff_dir = output_dir / "diff" + diff_dir.mkdir(exist_ok=True) + + # 获取变更文件列表 + if mode == "uncommitted": + # 获取暂存区变更 + staged_changes = run_git_command( + ["git", "diff", "--name-status", "--cached", "--diff-filter=ACDMRTUXB"] + ).splitlines() + + # 获取工作区变更 + unstaged_changes = run_git_command( + ["git", "diff", "--name-status", "--diff-filter=ACDMRTUXB"] + ).splitlines() + + # 获取未跟踪文件 (新增但未git add的文件) + untracked_files = run_git_command( + ["git", "ls-files", "--others", "--exclude-standard"] + ).splitlines() + untracked_changes = [f"A\t{file}" for file in untracked_files] + + # 合并变更 (暂存区优先,然后工作区,最后未跟踪) + change_list = staged_changes.copy() + + # 添加工作区变更中不在暂存区的文件 + staged_files = {line.split('\t', 1)[1] for line in staged_changes} + for line in unstaged_changes: + file_path = line.split('\t', 1)[1] + if file_path not in staged_files: + change_list.append(line) + + # 添加未跟踪文件 + all_existing_files = {line.split('\t', 1)[1] for line in change_list} + for line in untracked_changes: + file_path = line.split('\t', 1)[1] + if file_path not in all_existing_files: + change_list.append(line) + + # 生成完整的未提交变更diff + full_diff = run_git_command( + ["git", "diff", "--patch", "--cached"] + ) + with open(diff_dir / "uncommitted_changes.diff", "w", encoding='utf-8') as f: + f.write(full_diff) + + # 生成工作区与暂存区差异 + unstaged_diff = run_git_command( + ["git", "diff", "--patch"] + ) + with open(diff_dir / "unstaged_changes.diff", "w", encoding='utf-8') as f: + f.write(unstaged_diff) + else: + # 获取两个提交之间的变更 + change_list = run_git_command( + ["git", "diff", "--name-status", commit1, commit2] + ).splitlines() + + # 生成每个commit的详细diff + commit_hashes = run_git_command( + ["git", "rev-list", "--reverse", f"{commit1}..{commit2}"] + ).splitlines() + + if not commit_hashes: + commit_diff = run_git_command( + ["git", "diff", "--patch", commit1, commit2] + ) + diff_file = diff_dir / f"{commit1[:8]}_{commit2[:8]}.diff" + with open(diff_file, "w", encoding='utf-8') as f: + f.write(commit_diff) + else: + all_commits = [commit1] + commit_hashes + for i in range(len(all_commits) - 1): + parent = all_commits[i] + child = all_commits[i + 1] + + commit_diff = run_git_command( + ["git", "diff", "--patch", parent, child] + ) + diff_file = diff_dir / f"{parent[:8]}_{child[:8]}.diff" + with open(diff_file, "w", encoding='utf-8') as f: + f.write(commit_diff) + + # 分类变更文件 + added_files = [] + modified_files = [] + deleted_files = [] + + for line in change_list: + parts = line.split('\t') + if len(parts) < 2: + continue + status, file_path = parts[0], '\t'.join(parts[1:]) + + if status == 'A': + added_files.append(file_path) + elif status == 'M': + modified_files.append(file_path) + elif status == 'D': + deleted_files.append(file_path) + + # 写入变更列表文件 + with open(output_dir / "gitchangelist.txt", "w", encoding='utf-8') as f: + f.write("Added Files:\n") + for file in added_files: + f.write(f" {file}\n") + + f.write("\nModified Files:\n") + for file in modified_files: + f.write(f" {file}\n") + + f.write("\nDeleted Files:\n") + for file in deleted_files: + f.write(f" {file}\n") + + # 写入git log(如果有提交范围) + if mode == "commits": + git_log = run_git_command( + ["git", "log", "--name-status", f"{commit1}..{commit2}"] + ) + with open(output_dir / "gitlog.txt", "w", encoding='utf-8') as f: + f.write(git_log) + else: + # 未提交变更的简短信息 + with open(output_dir / "gitlog.txt", "w", encoding='utf-8') as f: + f.write("Summary of uncommitted changes\n") + f.write("================================\n\n") + f.write("Changes are relative to HEAD:\n") + f.write(run_git_command(["git", "status", "--short"])) + + # 复制变更文件(不包括已删除的) + for file_path in added_files + modified_files: + source_file = repo_path / file_path + target_file = output_dir / file_path + + target_file.parent.mkdir(parents=True, exist_ok=True) + + try: + # 直接从工作区复制,因为未跟踪文件在工作区中存在 + if os.path.exists(source_file): + with open(source_file, "rb") as src, open(target_file, "wb") as dst: + dst.write(src.read()) + else: + # 从暂存区提取文件内容 + # 对于包含特殊字符的文件名,使用更安全的方式 + try: + blob_content = run_git_command( + ["git", "show", f":{file_path}"] + ) + except: + # 如果pathspec失败,尝试使用git cat-file + try: + # 获取文件的blob hash + blob_hash = run_git_command( + ["git", "ls-files", "--stage", file_path] + ).split()[1] + blob_content = run_git_command( + ["git", "cat-file", "blob", blob_hash] + ) + except: + print(f"Warning: Could not extract staged content for {file_path}") + continue + with open(target_file, "w", encoding='utf-8') as dst: + dst.write(blob_content) + except Exception as e: + print(f"Error copying file {file_path}: {e}") + + print(f"Changes saved to: {output_dir}") + +if __name__ == "__main__": + main() \ No newline at end of file