302 lines
11 KiB
Python
Executable File
302 lines
11 KiB
Python
Executable File
#!/usr/bin/env python3
|
||
"""
|
||
Package validation script for Text Texture Generator addon.
|
||
Validates ZIP packages to ensure they meet Blender addon requirements.
|
||
"""
|
||
|
||
import os
|
||
import sys
|
||
import json
|
||
import zipfile
|
||
import tempfile
|
||
from pathlib import Path
|
||
from typing import Dict, List, Tuple, Optional
|
||
import argparse
|
||
|
||
def get_project_root() -> Path:
|
||
"""Get the project root directory."""
|
||
return Path(__file__).parent.parent
|
||
|
||
def load_config() -> Dict:
|
||
"""Load build configuration."""
|
||
config_path = get_project_root() / "build" / "config.json"
|
||
with open(config_path, 'r') as f:
|
||
return json.load(f)
|
||
|
||
class ValidationResult:
|
||
"""Holds validation results."""
|
||
def __init__(self):
|
||
self.errors = []
|
||
self.warnings = []
|
||
self.info = []
|
||
self.is_valid = True
|
||
|
||
def add_error(self, message: str):
|
||
"""Add an error message."""
|
||
self.errors.append(message)
|
||
self.is_valid = False
|
||
|
||
def add_warning(self, message: str):
|
||
"""Add a warning message."""
|
||
self.warnings.append(message)
|
||
|
||
def add_info(self, message: str):
|
||
"""Add an info message."""
|
||
self.info.append(message)
|
||
|
||
def print_results(self):
|
||
"""Print validation results."""
|
||
if self.info:
|
||
print("\n📋 Information:")
|
||
for msg in self.info:
|
||
print(f" ℹ️ {msg}")
|
||
|
||
if self.warnings:
|
||
print("\n⚠️ Warnings:")
|
||
for msg in self.warnings:
|
||
print(f" ⚠️ {msg}")
|
||
|
||
if self.errors:
|
||
print("\n❌ Errors:")
|
||
for msg in self.errors:
|
||
print(f" ❌ {msg}")
|
||
else:
|
||
print("\n✅ No errors found!")
|
||
|
||
print(f"\nValidation Result: {'✅ PASS' if self.is_valid else '❌ FAIL'}")
|
||
|
||
def validate_zip_structure(zip_path: Path, config: Dict) -> ValidationResult:
|
||
"""Validate the ZIP file structure."""
|
||
result = ValidationResult()
|
||
addon_id = config['project']['id']
|
||
|
||
try:
|
||
with zipfile.ZipFile(zip_path, 'r') as zip_file:
|
||
file_list = zip_file.namelist()
|
||
|
||
# Check if all files are within the addon directory
|
||
addon_root = f"{addon_id}/"
|
||
files_in_root = [f for f in file_list if f.startswith(addon_root)]
|
||
|
||
if len(files_in_root) != len(file_list):
|
||
result.add_error(f"Some files are not in the addon root directory '{addon_root}'")
|
||
|
||
result.add_info(f"ZIP contains {len(file_list)} files")
|
||
|
||
# Required files
|
||
required_files = [
|
||
f"{addon_id}/__init__.py",
|
||
f"{addon_id}/blender_manifest.toml"
|
||
]
|
||
|
||
for required_file in required_files:
|
||
if required_file not in file_list:
|
||
result.add_error(f"Missing required file: {required_file}")
|
||
else:
|
||
result.add_info(f"Found required file: {required_file}")
|
||
|
||
# Check for excluded files
|
||
exclude_patterns = config['build']['exclude_patterns']
|
||
for file_path in file_list:
|
||
for pattern in exclude_patterns:
|
||
if pattern in file_path:
|
||
result.add_warning(f"Excluded pattern found in package: {file_path}")
|
||
|
||
except zipfile.BadZipFile:
|
||
result.add_error("Invalid ZIP file")
|
||
except Exception as e:
|
||
result.add_error(f"Error reading ZIP file: {e}")
|
||
|
||
return result
|
||
|
||
def validate_manifest(zip_path: Path, config: Dict, version_type: str) -> ValidationResult:
|
||
"""Validate the blender_manifest.toml file."""
|
||
result = ValidationResult()
|
||
addon_id = config['project']['id']
|
||
manifest_path = f"{addon_id}/blender_manifest.toml"
|
||
|
||
try:
|
||
with zipfile.ZipFile(zip_path, 'r') as zip_file:
|
||
if manifest_path not in zip_file.namelist():
|
||
result.add_error("blender_manifest.toml not found in package")
|
||
return result
|
||
|
||
# Read and parse manifest
|
||
manifest_content = zip_file.read(manifest_path).decode('utf-8')
|
||
result.add_info("Successfully read blender_manifest.toml")
|
||
|
||
# Check for required fields
|
||
required_fields = ['id', 'version', 'name', 'blender_version_min']
|
||
for field in required_fields:
|
||
if f'{field} = ' not in manifest_content:
|
||
result.add_error(f"Missing required field in manifest: {field}")
|
||
|
||
# Validate specific values
|
||
if f'id = "{addon_id}"' not in manifest_content:
|
||
result.add_error(f"Incorrect addon ID in manifest (expected: {addon_id})")
|
||
|
||
# Check version format
|
||
import re
|
||
version_pattern = r'version = "(\d+\.\d+\.\d+)"'
|
||
version_match = re.search(version_pattern, manifest_content)
|
||
if version_match:
|
||
version = version_match.group(1)
|
||
result.add_info(f"Package version: {version}")
|
||
else:
|
||
result.add_error("Invalid or missing version format in manifest")
|
||
|
||
except Exception as e:
|
||
result.add_error(f"Error validating manifest: {e}")
|
||
|
||
return result
|
||
|
||
def validate_constants(zip_path: Path, config: Dict, version_type: str) -> ValidationResult:
|
||
"""Validate the constants.py file for feature flags."""
|
||
result = ValidationResult()
|
||
addon_id = config['project']['id']
|
||
constants_path = f"{addon_id}/utils/constants.py"
|
||
|
||
try:
|
||
with zipfile.ZipFile(zip_path, 'r') as zip_file:
|
||
if constants_path not in zip_file.namelist():
|
||
result.add_error("utils/constants.py not found in package")
|
||
return result
|
||
|
||
# Read constants file
|
||
constants_content = zip_file.read(constants_path).decode('utf-8')
|
||
result.add_info("Successfully read utils/constants.py")
|
||
|
||
# Check for feature flags
|
||
if 'ENABLED_FEATURES' not in constants_content:
|
||
result.add_error("ENABLED_FEATURES not found in constants.py")
|
||
|
||
if 'VERSION_TYPE' not in constants_content:
|
||
result.add_error("VERSION_TYPE not found in constants.py")
|
||
else:
|
||
# Check if version type matches expected
|
||
if f'VERSION_TYPE = "{version_type}"' not in constants_content:
|
||
result.add_warning(f"VERSION_TYPE might not match expected type: {version_type}")
|
||
|
||
# Check for version limits functions
|
||
if 'get_version_limits' not in constants_content:
|
||
result.add_error("get_version_limits function not found in constants.py")
|
||
|
||
except Exception as e:
|
||
result.add_error(f"Error validating constants: {e}")
|
||
|
||
return result
|
||
|
||
def validate_feature_flags(zip_path: Path, config: Dict) -> ValidationResult:
|
||
"""Validate the feature flags system."""
|
||
result = ValidationResult()
|
||
addon_id = config['project']['id']
|
||
feature_flags_path = f"{addon_id}/utils/feature_flags.py"
|
||
|
||
try:
|
||
with zipfile.ZipFile(zip_path, 'r') as zip_file:
|
||
if feature_flags_path not in zip_file.namelist():
|
||
result.add_warning("utils/feature_flags.py not found in package")
|
||
return result
|
||
|
||
# Read feature flags file
|
||
feature_content = zip_file.read(feature_flags_path).decode('utf-8')
|
||
result.add_info("Successfully read utils/feature_flags.py")
|
||
|
||
# Check for essential classes and functions
|
||
required_items = [
|
||
'FeatureManager',
|
||
'is_feature_enabled',
|
||
'require_feature',
|
||
'feature_required',
|
||
'FeatureNotAvailableError'
|
||
]
|
||
|
||
for item in required_items:
|
||
if item not in feature_content:
|
||
result.add_warning(f"Missing feature flag component: {item}")
|
||
else:
|
||
result.add_info(f"Found feature flag component: {item}")
|
||
|
||
except Exception as e:
|
||
result.add_error(f"Error validating feature flags: {e}")
|
||
|
||
return result
|
||
|
||
def detect_version_type(zip_path: Path) -> Optional[str]:
|
||
"""Detect version type from ZIP filename or contents."""
|
||
filename = zip_path.name.lower()
|
||
if '_free' in filename:
|
||
return 'free'
|
||
elif 'free' in filename:
|
||
return 'free'
|
||
else:
|
||
return 'full' # Default assumption
|
||
|
||
def validate_package(zip_path: Path, version_type: Optional[str] = None) -> bool:
|
||
"""Validate a complete addon package."""
|
||
if not zip_path.exists():
|
||
print(f"❌ Package file not found: {zip_path}")
|
||
return False
|
||
|
||
if version_type is None:
|
||
version_type = detect_version_type(zip_path)
|
||
|
||
print(f"🔍 Validating package: {zip_path.name}")
|
||
print(f"📋 Detected version type: {version_type}")
|
||
print("=" * 60)
|
||
|
||
config = load_config()
|
||
overall_valid = True
|
||
|
||
# Structure validation
|
||
print("Validating ZIP structure...")
|
||
structure_result = validate_zip_structure(zip_path, config)
|
||
structure_result.print_results()
|
||
overall_valid &= structure_result.is_valid
|
||
|
||
print("\n" + "─" * 60)
|
||
|
||
# Manifest validation
|
||
print("Validating blender_manifest.toml...")
|
||
manifest_result = validate_manifest(zip_path, config, version_type)
|
||
manifest_result.print_results()
|
||
overall_valid &= manifest_result.is_valid
|
||
|
||
print("\n" + "─" * 60)
|
||
|
||
# Constants validation
|
||
print("Validating utils/constants.py...")
|
||
constants_result = validate_constants(zip_path, config, version_type)
|
||
constants_result.print_results()
|
||
overall_valid &= constants_result.is_valid
|
||
|
||
print("\n" + "─" * 60)
|
||
|
||
# Feature flags validation
|
||
print("Validating feature flags system...")
|
||
features_result = validate_feature_flags(zip_path, config)
|
||
features_result.print_results()
|
||
overall_valid &= features_result.is_valid
|
||
|
||
print("\n" + "=" * 60)
|
||
print(f"Overall Validation: {'✅ PASS' if overall_valid else '❌ FAIL'}")
|
||
|
||
return overall_valid
|
||
|
||
def main():
|
||
"""Main entry point."""
|
||
parser = argparse.ArgumentParser(description="Validate Text Texture Generator addon package")
|
||
parser.add_argument("package", type=Path, help="Path to ZIP package to validate")
|
||
parser.add_argument("--type", choices=["free", "full"], help="Expected version type")
|
||
|
||
args = parser.parse_args()
|
||
|
||
try:
|
||
is_valid = validate_package(args.package, args.type)
|
||
sys.exit(0 if is_valid else 1)
|
||
except Exception as e:
|
||
print(f"❌ Validation failed: {e}")
|
||
sys.exit(1)
|
||
|
||
if __name__ == "__main__":
|
||
main() |