diff --git a/.gitignore b/.gitignore index 5b388ddcd..bea14c6d6 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,4 @@ tools/ndocs-project/Data/* ArmaScriptCompiler.exe *.sqfc +sqfvm.exe diff --git a/tools/.vscode/tasks.json b/tools/.vscode/tasks.json index ac1a73618..d37b471bd 100644 --- a/tools/.vscode/tasks.json +++ b/tools/.vscode/tasks.json @@ -3,7 +3,7 @@ "tasks": [ { "label": "Validate SQF", - "command": "${config:python.pythonPath}", + "command": "python", "options": { "cwd": "${workspaceFolder}/tools" }, @@ -13,7 +13,7 @@ }, { "label": "Validate Config", - "command": "${config:python.pythonPath}", + "command": "python", "options": { "cwd": "${workspaceFolder}/tools" }, @@ -23,7 +23,7 @@ }, { "label": "Check Strings", - "command": "${config:python.pythonPath}", + "command": "python", "options": { "cwd": "${workspaceFolder}/tools" }, @@ -31,12 +31,24 @@ "check_strings.py" ] }, + { + "label": "SQFVM Checker", + "command": "${config:python.pythonPath}", + "options": { + "cwd": "${workspaceFolder}/tools" + }, + "args": [ + "sqfvmChecker.py" + ], + "problemMatcher": [] + }, { "label": "Test All", "dependsOn": [ "Validate SQF", "Validate Config", - "Check Strings" + "Check Strings", + "SQFVM Checker" ], "group": { "kind": "test", @@ -45,12 +57,13 @@ }, { "label": "Build: make.py (pboProject)", - "command": "${config:python.pythonPath}", + "command": "python", "options": { "cwd": "${workspaceFolder}/tools" }, "args": [ - "make.py", "ci" + "make.py", + "ci" ], "group": { "kind": "build", @@ -58,13 +71,14 @@ } }, { - "label": "Build: Hemtt Release", + "label": "Build: Hemtt", "command": "hemtt.exe", "options": { "cwd": "${workspaceFolder}" }, "args": [ - "build", "--release", "--ci" + "build", + "-v" ], "group": "build" } diff --git a/tools/sqfvmChecker.py b/tools/sqfvmChecker.py new file mode 100644 index 000000000..b8c652ed2 --- /dev/null +++ b/tools/sqfvmChecker.py @@ -0,0 +1,107 @@ +import os +import sys +import subprocess +import concurrent.futures + +addon_base_path = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) + +files_to_ignore_lower = [ + x.lower() for x in ["initSettings.sqf", "initKeybinds.sqf", "XEH_PREP.sqf", "backwards_comp.sqf", "gui_createCategory.sqf"] +] +sqfvm_exe = os.path.join(addon_base_path, "sqfvm.exe") +virtual_paths = [ + # would need to add more even more to /include to use it + "P:/a3|/a3", # "{}|/a3".format(os.path.join(addon_base_path, "include", "a3")), + "P:/a3|/A3", + f"{addon_base_path}|/x/cba" +] + + +def get_files_to_process(basePath): + arma_files = [] + for root, _dirs, files in os.walk(os.path.join(addon_base_path, "addons")): + for file in files: + if file.endswith(".sqf") or file == "config.cpp": + if file.lower() in files_to_ignore_lower: + continue + skipPreprocessing = False + addonTomlPath = os.path.join(root, "addon.toml") + if os.path.isfile(addonTomlPath): + with open(addonTomlPath, "r") as f: + tomlFile = f.read() + if "preprocess = false" in tomlFile: + print("'preprocess = false' not supported") + raise + skipPreprocessing = "[preprocess]\nenabled = false" in tomlFile + addonTomlPath = os.path.join(os.path.dirname(root), "addon.toml") + if os.path.isfile(addonTomlPath): + with open(addonTomlPath, "r") as f: + tomlFile = f.read() + if "preprocess = false" in tomlFile: + print("'preprocess = false' not supported") + raise + skipPreprocessing = "[preprocess]\nenabled = false" in tomlFile + if file == "config.cpp" and skipPreprocessing: + continue # ignore configs with __has_include + filePath = os.path.join(root, file) + arma_files.append(filePath) + return arma_files + + +def process_file(filePath, skipA3Warnings=True): + with open(filePath, "r", encoding="utf-8", errors="ignore") as file: + content = file.read() + if content.startswith("//pragma SKIP_COMPILE"): + return False + cmd = [sqfvm_exe, "--input", filePath, "--parse-only", "--automated"] + for v in virtual_paths: + cmd.append("-v") + cmd.append(v) + # cmd.append("-V") + proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, universal_newlines=True) + try: + ret = proc.wait(12) # max wait - seconds + except Exception as _e: + print("sqfvm timed out: {}".format(filePath)) + return True + # print("{} = {}".format(filePath, ret)) + + fileHasError = False + keepReadingLines = True + while keepReadingLines: + line = proc.stdout.readline() + if not line: + keepReadingLines = False + else: + line = line.rstrip() + if line.startswith("[ERR]"): + fileHasError = True + if not ( + skipA3Warnings + and line.startswith("[WRN]") + and ("a3/" in line) + and (("Unexpected IFDEF" in line) or ("defined twice" in line)) + ): + print(" {}".format(line)) + return fileHasError + + +def main(): + if not os.path.isfile(sqfvm_exe): + print("Error: sqfvm.exe not found in base folder [{}]".format(sqfvm_exe)) + return 1 + + error_count = 0 + arma_files = get_files_to_process(addon_base_path) + print("Checking {} files".format(len(arma_files))) + with concurrent.futures.ThreadPoolExecutor(max_workers=12) as executor: + for fileError in executor.map(process_file, arma_files): + if fileError: + error_count += 1 + + print("Errors: {}".format(error_count)) + return error_count + + +if __name__ == "__main__": + sys.exit(main())