#!/usr/bin/python3 from zipfile import ZipFile from glob import glob from os.path import join, split, splitext, exists, basename, isdir, dirname import sys import argparse from pathlib import Path from json import load import re def dpPrint(text: str, toFile: str = ''): print(text, flush=True) if toFile: try: with open(toFile, 'a', encoding='utf-8') as f: f.write(text + '\n') except Exception as error: print(f"ERROR, failed to write to {toFile}: {error}", file=sys.stderr, flush=True) return def check_name_variation(varName, dependenciesList): parts = varName.split('.') if parts[-1].isdigit() or parts[-1] == 'latest': baseName = '.'.join(parts[:-1]) else: baseName = varName return varName in dependenciesList or f'{baseName}.latest' in dependenciesList def getDependencies(varFile: str) -> tuple[Exception|None, set]: try: with ZipFile(varFile) as myzip: with myzip.open('meta.json') as metaJson: data = load(metaJson) myzip.close() if 'dependencies' not in data: return None, set() return None, set(data['dependencies'].keys()) except Exception as error: return error, set() # Get all dependencies listed in presets def getPresetDependencies(customPath: str) -> dict: allDependencies = {} presets = sorted(glob(join(customPath, 'Custom', '**/*.vap'), recursive=True)) for vap in presets: try: with open(vap, 'r', encoding='utf-8') as f: pattern = r'"([^"]+):/[^"]*"' occurences = set(re.findall(pattern, f.read())) except Exception as error: dpPrint(f"ERROR, failed to read: {vap}: {error}", args.output) continue parts = Path(vap).parts presetName = '/'.join(parts[parts.index("Custom")+1:]) dependenciesDict = {key: {presetName} for key in occurences} for key, val in dependenciesDict.items(): if key in allDependencies: allDependencies[key].update(val) else: allDependencies[key] = val return allDependencies # Collect all vars, a complete list of all dependencies, and which vars uses which dependency def getAllVars(directoryPath: str) -> tuple[set, dict]: allDependencies = {} vars = glob(join(directoryPath, 'AddonPackages', '**/*.var'), recursive=True) allVarList = {basename(var) for var in vars} for varFilename in vars: if isdir(varFilename): error, dependencies = getMetaDependencies(varFilename) else: error, dependencies = getDependencies(varFilename) if (isinstance(error, Exception)): dpPrint(f"ERROR: {varFilename}: {error}", args.output) continue if (len(dependencies) == 0): continue dependenciesDict = {key: {split(varFilename)[1]} for key in dependencies} for key, val in dependenciesDict.items(): if key in allDependencies: allDependencies[key].update(val) else: allDependencies[key] = val return allVarList, allDependencies # Get all dependencies listed in meta.json stored directly in folders def getMetaDependencies(folder: str) -> tuple[Exception|None, set]: filename = join(folder, 'meta.json') try: with open(filename, 'r', encoding='utf-8') as metaJson: data = load(metaJson) if 'dependencies' not in data: return None, set() return None, set(data['dependencies'].keys()) except Exception as error: return error, set() argParser = argparse.ArgumentParser(prog='DependencyScanner.py', description='Searches for dependencies inside all vars in a folder and if a name is given, lists whether there a vars that depend on it. If no name is given, it will instead list all vars in the folder which isn\'t a dependency of any other var.') argParser.add_argument("-p", "--path", type=Path, default='.', help="Path your VaM folder") argParser.add_argument("-n", "--name", type=str, default='', help="Name of a var file. Can be a partial name") argParser.add_argument("-o", "--output", type=Path, nargs='?', default=None, const='.\\SearchVarOutput.txt', help="File to save the results in (default: '.\\SearchVarOutput.txt')") args = argParser.parse_args() # Check the given arguments are valid if not exists(args.path): print(f"The given path doesn't exist: {args.path}") exit(1) if args.output: if isdir(args.output): print(f"The given output is a folder, not a file: {args.output}") exit(1) if not isdir(dirname(args.output)) and not basename(args.output) != '': print(f"The given output file folder path doesn't exist: {args.output}") exit(1) open(args.output, 'w', encoding='utf-8').close() parts = args.name.split('.') if parts[-1] == 'var': args.name = '.'.join(parts[:-1]) # Get a list of all vars and a dict of all dependencies from vars allVarList, allVarDependencies = getAllVars(args.path) allDepVars = allVarDependencies.keys() # Get a dict of all dependencies from presets allPresetDependencies = getPresetDependencies(args.path) allDepVarsPresets = allPresetDependencies.keys() # Combine all dependencies # Incorrect! Must keep separate dicts till later # allDependencies = allVarDependencies | allPresetDependencies # allDependencies = dict(list(allVarDependencies.items()) + list(allPresetDependencies.items())) # Get a list of which vars in the folder that are not used as a dependency noDependencyVar = [var for var in allVarList if not check_name_variation(var, allDepVars)] noDependencyPreset = [var for var in allVarList if not check_name_variation(var, allDepVarsPresets)] noDependency = sorted(list(set(noDependencyVar) & set(noDependencyPreset)), key=str.lower) # If no name is given, list all vars that aren't used as a dependency if args.name == '': dpPrint(f"{len(noDependency)} vars are not used as a dependency:", args.output) for var in noDependency: dpPrint("\t" + var, args.output) exit(0) # List all vars with the given name that aren't used as a dependency foundVars = [var for var in noDependency if args.name.lower() in var.lower()] if len(foundVars) > 0: dpPrint(f"The following {str(len(foundVars)) + ' vars were' if len(foundVars) > 1 else 'var was'} checked for dependency in other vars:", args.output) for var in foundVars: dpPrint("\t" + var, args.output) print() # List all vars with the given name that depend on other vars # Keep separate lists for Var dependencies and Preset dependencies foundDepAVars = sorted([var for var in allVarDependencies.keys() if args.name.lower() in var.lower()], key=str.lower) foundDepPVars = sorted([var for var in allPresetDependencies.keys() if args.name.lower() in var.lower()], key=str.lower) #foundDepVars = foundDepAVars + foundDepPVars # Loop over both lists to print results if len(foundDepAVars) > 0: dpPrint(f"\nThe following {len(foundDepAVars)} iteration{'s' if len(foundDepAVars) > 1 else ''} of '" + args.name + "' has other vars that depend on it:", args.output) # print all dependencies where the name is a key, or the name is part of the key for key in foundDepAVars: if args.name.lower() in key.lower(): if args.name != key: dpPrint(f"{key} ->", args.output) for var in sorted(allVarDependencies[key], key=str.lower): dpPrint("\t" + var, args.output) if len(foundDepPVars) > 0: dpPrint(f"\nThe following {len(foundDepPVars)} iteration{'s' if len(foundDepPVars) > 1 else ''} of '" + args.name + "' has presets that depend on it:", args.output) # print all dependencies where the name is a key, or the name is part of the key for key in foundDepPVars: if args.name.lower() in key.lower(): if args.name != key: dpPrint(f"{key} ->", args.output) for var in sorted(allPresetDependencies[key], key=str.lower): dpPrint("\t" + var, args.output) if args.output: print(f"\nResults saved to: {args.output}")