deployment
This commit is contained in:
263
build/build_addon.py
Executable file
263
build/build_addon.py
Executable file
@@ -0,0 +1,263 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Build script for Text Texture Generator addon.
|
||||
Creates ZIP packages for both full and free versions.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import shutil
|
||||
import zipfile
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional
|
||||
import tempfile
|
||||
import argparse
|
||||
import fnmatch
|
||||
|
||||
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)
|
||||
|
||||
def load_exclusions_config() -> Dict:
|
||||
"""Load file exclusions configuration."""
|
||||
config_path = get_project_root() / "build" / "file_exclusions.json"
|
||||
with open(config_path, 'r') as f:
|
||||
return json.load(f)
|
||||
|
||||
def clean_directory(path: Path):
|
||||
"""Clean a directory by removing all contents."""
|
||||
if path.exists():
|
||||
shutil.rmtree(path)
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def should_exclude_file(file_path: Path, source_dir: Path, exclude_patterns: List[str], exclude_files: List[str]) -> bool:
|
||||
"""Check if a file should be excluded based on patterns and specific files."""
|
||||
file_str = str(file_path)
|
||||
name = file_path.name
|
||||
|
||||
# Get relative path from source directory for exclusion checking
|
||||
try:
|
||||
rel_path = file_path.relative_to(source_dir)
|
||||
rel_path_str = str(rel_path)
|
||||
except ValueError:
|
||||
rel_path_str = str(file_path)
|
||||
|
||||
# Check specific file exclusions
|
||||
for exclude_file in exclude_files:
|
||||
# Handle directory exclusions (ending with /)
|
||||
if exclude_file.endswith('/'):
|
||||
exclude_dir = exclude_file.rstrip('/')
|
||||
if rel_path_str.startswith(exclude_dir + '/') or rel_path_str == exclude_dir:
|
||||
return True
|
||||
# Handle exact file matches
|
||||
elif rel_path_str == exclude_file:
|
||||
return True
|
||||
|
||||
# Check general patterns (for cache files, etc.)
|
||||
for pattern in exclude_patterns:
|
||||
if pattern in file_str or pattern in name:
|
||||
return True
|
||||
|
||||
# Handle wildcards
|
||||
if pattern.startswith('*.'):
|
||||
extension = pattern[1:]
|
||||
if file_str.endswith(extension):
|
||||
return True
|
||||
|
||||
# Handle glob patterns
|
||||
if fnmatch.fnmatch(name, pattern) or fnmatch.fnmatch(rel_path_str, pattern):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def copy_source_files(source_dir: Path, dest_dir: Path, exclude_patterns: List[str], exclude_files: List[str] = None):
|
||||
"""Copy source files to destination, excluding specified patterns and files."""
|
||||
if exclude_files is None:
|
||||
exclude_files = []
|
||||
|
||||
print(f"Copying files from {source_dir} to {dest_dir}")
|
||||
print(f"Excluding files: {exclude_files}")
|
||||
print(f"Excluding patterns: {exclude_patterns}")
|
||||
|
||||
copied_count = 0
|
||||
excluded_count = 0
|
||||
|
||||
for item in source_dir.rglob('*'):
|
||||
if item.is_file():
|
||||
if should_exclude_file(item, source_dir, exclude_patterns, exclude_files):
|
||||
rel_path = item.relative_to(source_dir)
|
||||
print(f" Excluded: {rel_path}")
|
||||
excluded_count += 1
|
||||
else:
|
||||
# Calculate relative path
|
||||
rel_path = item.relative_to(source_dir)
|
||||
dest_file = dest_dir / rel_path
|
||||
|
||||
# Create parent directories
|
||||
dest_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Copy file
|
||||
shutil.copy2(item, dest_file)
|
||||
print(f" Copied: {rel_path}")
|
||||
copied_count += 1
|
||||
|
||||
print(f"Files copied: {copied_count}, excluded: {excluded_count}")
|
||||
|
||||
def create_zip_package(source_dir: Path, output_file: Path, addon_name: str):
|
||||
"""Create a ZIP package from source directory."""
|
||||
print(f"Creating ZIP package: {output_file}")
|
||||
|
||||
with zipfile.ZipFile(output_file, 'w', zipfile.ZIP_DEFLATED) as zip_file:
|
||||
for file_path in source_dir.rglob('*'):
|
||||
if file_path.is_file():
|
||||
# Calculate archive path (include addon name as root folder)
|
||||
rel_path = file_path.relative_to(source_dir)
|
||||
archive_path = Path(addon_name) / rel_path
|
||||
|
||||
zip_file.write(file_path, archive_path)
|
||||
print(f" Added to ZIP: {archive_path}")
|
||||
|
||||
print(f"ZIP package created: {output_file}")
|
||||
|
||||
def get_version_from_git() -> Optional[str]:
|
||||
"""Get version from git tags."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['python3', 'build/sync_version.py', '--version'],
|
||||
capture_output=True, text=True, cwd=get_project_root()
|
||||
)
|
||||
if result.returncode == 0:
|
||||
# Extract version from output
|
||||
lines = result.stdout.strip().split('\n')
|
||||
for line in lines:
|
||||
if 'Synchronizing version:' in line:
|
||||
return line.split(':')[1].strip().split()[0]
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not get version from git: {e}")
|
||||
|
||||
return None
|
||||
|
||||
def build_addon(version_type: str, version: Optional[str] = None, output_dir: Optional[Path] = None):
|
||||
"""Build addon for specified version type."""
|
||||
config = load_config()
|
||||
exclusions_config = load_exclusions_config()
|
||||
project_root = get_project_root()
|
||||
|
||||
# Determine version
|
||||
if version is None:
|
||||
version = get_version_from_git() or "1.0.0"
|
||||
|
||||
# Set up paths
|
||||
source_dir = project_root / config['build']['source_dir']
|
||||
if output_dir is None:
|
||||
output_dir = project_root / config['build']['dist_dir']
|
||||
temp_dir = project_root / config['build']['temp_dir']
|
||||
|
||||
# Clean and create directories
|
||||
clean_directory(temp_dir)
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Sync version files for the specific version type (simplified - no feature flags)
|
||||
print(f"Synchronizing version files for {version_type} version {version}")
|
||||
sync_result = subprocess.run([
|
||||
'python3', 'build/sync_version.py',
|
||||
'--version', version,
|
||||
'--type', version_type
|
||||
], cwd=project_root)
|
||||
|
||||
if sync_result.returncode != 0:
|
||||
raise RuntimeError(f"Version synchronization failed")
|
||||
|
||||
# Get version-specific exclusions
|
||||
version_config = exclusions_config['version_configs'][version_type]
|
||||
exclude_files = version_config.get('exclude_files', [])
|
||||
exclude_patterns = config['build']['exclude_patterns'] + version_config.get('exclude_patterns', [])
|
||||
|
||||
print(f"\n📋 Build configuration for {version_type} version:")
|
||||
print(f"Max texture size: {version_config.get('max_texture_size', 'unlimited')}")
|
||||
print(f"Max concurrent operations: {version_config.get('max_concurrent_operations', 'unlimited')}")
|
||||
if exclude_files:
|
||||
print(f"Files to exclude: {exclude_files}")
|
||||
if version_config.get('exclude_patterns'):
|
||||
print(f"Additional patterns to exclude: {version_config.get('exclude_patterns')}")
|
||||
|
||||
# Copy source files to temp directory with version-specific exclusions
|
||||
build_temp_dir = temp_dir / f"{config['project']['id']}_{version_type}"
|
||||
copy_source_files(source_dir, build_temp_dir, exclude_patterns, exclude_files)
|
||||
|
||||
# Create output filename
|
||||
version_suffix = f"_{version_type}" if version_type == "free" else ""
|
||||
output_filename = f"{config['project']['id']}_v{version}{version_suffix}.zip"
|
||||
output_file = output_dir / output_filename
|
||||
|
||||
# Create ZIP package
|
||||
create_zip_package(build_temp_dir, output_file, config['project']['id'])
|
||||
|
||||
# Clean up temp directory
|
||||
shutil.rmtree(temp_dir)
|
||||
|
||||
print(f"\n✅ Build complete!")
|
||||
print(f"Package: {output_file}")
|
||||
print(f"Version: {version} ({version_type})")
|
||||
|
||||
return output_file
|
||||
|
||||
def build_all_versions(version: Optional[str] = None):
|
||||
"""Build both full and free versions."""
|
||||
print("Building all versions...")
|
||||
|
||||
built_packages = []
|
||||
|
||||
# Build full version
|
||||
print("\n" + "="*50)
|
||||
print("BUILDING FULL VERSION")
|
||||
print("="*50)
|
||||
full_package = build_addon("full", version)
|
||||
built_packages.append(("full", full_package))
|
||||
|
||||
# Build free version
|
||||
print("\n" + "="*50)
|
||||
print("BUILDING FREE VERSION")
|
||||
print("="*50)
|
||||
free_package = build_addon("free", version)
|
||||
built_packages.append(("free", free_package))
|
||||
|
||||
print("\n" + "="*50)
|
||||
print("BUILD SUMMARY")
|
||||
print("="*50)
|
||||
for version_type, package_path in built_packages:
|
||||
file_size = package_path.stat().st_size / 1024 # KB
|
||||
print(f"{version_type.upper():>4}: {package_path.name} ({file_size:.1f} KB)")
|
||||
|
||||
return built_packages
|
||||
|
||||
def main():
|
||||
"""Main entry point."""
|
||||
parser = argparse.ArgumentParser(description="Build Text Texture Generator addon")
|
||||
parser.add_argument("--version", help="Version to build (defaults to git tag)")
|
||||
parser.add_argument("--type", choices=["free", "full", "all"], default="all",
|
||||
help="Version type to build")
|
||||
parser.add_argument("--output", type=Path, help="Output directory")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
if args.type == "all":
|
||||
build_all_versions(args.version)
|
||||
else:
|
||||
build_addon(args.type, args.version, args.output)
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Build failed: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
36
build/config.json
Normal file
36
build/config.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"project": {
|
||||
"name": "Text Texture Generator",
|
||||
"id": "text_texture_generator",
|
||||
"author": "Marc Mintel <marc@mintel.me>",
|
||||
"description": "Generate image textures from text with accurate dimensions and instant live preview",
|
||||
"category": "Material",
|
||||
"location": "3D View > Sidebar (N) > Tool Tab | Shader Editor > Sidebar > Tool Tab",
|
||||
"blender_version_min": "4.0.0",
|
||||
"license": ["GPL-3.0-or-later"],
|
||||
"support": "COMMUNITY",
|
||||
"tags": ["Material", "Shader", "Texture", "Text", "Image"]
|
||||
},
|
||||
"build": {
|
||||
"source_dir": "src",
|
||||
"dist_dir": "dist",
|
||||
"temp_dir": "build/temp",
|
||||
"exclude_patterns": [
|
||||
"__pycache__",
|
||||
"*.pyc",
|
||||
"*.pyo",
|
||||
".DS_Store",
|
||||
"Thumbs.db"
|
||||
]
|
||||
},
|
||||
"versions": {
|
||||
"template_files": ["src/blender_manifest.toml", "src/utils/constants.py"],
|
||||
"git_tag_pattern": "^v?([0-9]+\\.[0-9]+\\.[0-9]+)$"
|
||||
},
|
||||
"file_exclusions": {
|
||||
"config_file": "build/file_exclusions.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"pillow": ">=10.0.0"
|
||||
}
|
||||
}
|
||||
67
build/features.json.backup
Normal file
67
build/features.json.backup
Normal file
@@ -0,0 +1,67 @@
|
||||
{
|
||||
"features": {
|
||||
"basic": {
|
||||
"name": "Basic Text Generation",
|
||||
"description": "Core text texture generation functionality",
|
||||
"enabled_by_default": true,
|
||||
"components": [
|
||||
"core.text_processor",
|
||||
"core.generation_engine",
|
||||
"ui.panels.basic"
|
||||
]
|
||||
},
|
||||
"preview": {
|
||||
"name": "Live Preview",
|
||||
"description": "Real-time preview of text textures",
|
||||
"enabled_by_default": true,
|
||||
"components": ["ui.preview", "core.text_fitting"]
|
||||
},
|
||||
"advanced_styling": {
|
||||
"name": "Advanced Styling Options",
|
||||
"description": "Extended styling and customization options",
|
||||
"enabled_by_default": false,
|
||||
"components": ["ui.panels.advanced", "core.advanced_styling"]
|
||||
},
|
||||
"normal_maps": {
|
||||
"name": "Normal Map Generation",
|
||||
"description": "Generate normal maps from text",
|
||||
"enabled_by_default": false,
|
||||
"components": ["core.normal_maps", "ui.panels.normal_maps"]
|
||||
},
|
||||
"presets": {
|
||||
"name": "Preset Management",
|
||||
"description": "Save and load text style presets",
|
||||
"enabled_by_default": false,
|
||||
"components": [
|
||||
"presets.storage_system",
|
||||
"operators.preset_ops",
|
||||
"ui.panels.presets"
|
||||
]
|
||||
},
|
||||
"batch_processing": {
|
||||
"name": "Batch Processing",
|
||||
"description": "Process multiple text textures at once",
|
||||
"enabled_by_default": false,
|
||||
"components": ["operators.batch_ops", "ui.panels.batch"]
|
||||
}
|
||||
},
|
||||
"version_configs": {
|
||||
"free": {
|
||||
"enabled_features": ["basic", "preview"],
|
||||
"max_texture_size": 1024,
|
||||
"max_concurrent_operations": 1
|
||||
},
|
||||
"full": {
|
||||
"enabled_features": [
|
||||
"basic",
|
||||
"preview",
|
||||
"advanced_styling",
|
||||
"normal_maps",
|
||||
"presets",
|
||||
"batch_processing"
|
||||
],
|
||||
"max_texture_size": 4096,
|
||||
"max_concurrent_operations": 4
|
||||
}
|
||||
}
|
||||
}
|
||||
30
build/file_exclusions.json
Normal file
30
build/file_exclusions.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"comment": "File exclusion configuration for building different addon versions",
|
||||
"description": "Maps features to specific files/directories that should be excluded from free builds",
|
||||
"version_configs": {
|
||||
"free": {
|
||||
"description": "Free version - core text texture generation only",
|
||||
"exclude_files": [
|
||||
"core/normal_maps.py",
|
||||
"operators/preset_ops.py",
|
||||
"operators/utility_ops.py",
|
||||
"presets/"
|
||||
],
|
||||
"exclude_patterns": ["*batch*", "*advanced*"],
|
||||
"max_texture_size": 1024,
|
||||
"max_concurrent_operations": 1
|
||||
},
|
||||
"full": {
|
||||
"description": "Full version - all features included",
|
||||
"exclude_files": [],
|
||||
"exclude_patterns": [],
|
||||
"max_texture_size": 4096,
|
||||
"max_concurrent_operations": 4
|
||||
}
|
||||
},
|
||||
"feature_to_files_mapping": {
|
||||
"normal_maps": ["core/normal_maps.py"],
|
||||
"presets": ["presets/", "operators/preset_ops.py"],
|
||||
"batch_processing": ["operators/utility_ops.py"]
|
||||
}
|
||||
}
|
||||
296
build/generate_changelog.py
Executable file
296
build/generate_changelog.py
Executable file
@@ -0,0 +1,296 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Changelog generation script for Text Texture Generator.
|
||||
Generates changelog from git commits between tags.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import subprocess
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
from typing import List, Dict, Optional, Tuple
|
||||
from datetime import datetime
|
||||
import re
|
||||
|
||||
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)
|
||||
|
||||
def run_git_command(command: List[str]) -> Optional[str]:
|
||||
"""Run a git command and return output."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['git'] + command,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
cwd=get_project_root()
|
||||
)
|
||||
return result.stdout.strip()
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Git command failed: {' '.join(['git'] + command)}")
|
||||
print(f"Error: {e.stderr}")
|
||||
return None
|
||||
|
||||
def get_git_tags() -> List[str]:
|
||||
"""Get all git tags sorted by version."""
|
||||
output = run_git_command(['tag', '-l', '--sort=-version:refname'])
|
||||
if output:
|
||||
return [tag for tag in output.split('\n') if tag.strip()]
|
||||
return []
|
||||
|
||||
def get_commits_between(from_ref: Optional[str], to_ref: str) -> List[Dict]:
|
||||
"""Get commits between two references."""
|
||||
if from_ref:
|
||||
commit_range = f"{from_ref}..{to_ref}"
|
||||
else:
|
||||
commit_range = to_ref
|
||||
|
||||
# Get commit info in a structured format
|
||||
format_str = "%H|%an|%ae|%ad|%s"
|
||||
output = run_git_command([
|
||||
'log',
|
||||
'--pretty=format:' + format_str,
|
||||
'--date=iso',
|
||||
commit_range
|
||||
])
|
||||
|
||||
if not output:
|
||||
return []
|
||||
|
||||
commits = []
|
||||
for line in output.split('\n'):
|
||||
if '|' in line:
|
||||
parts = line.split('|', 4)
|
||||
if len(parts) == 5:
|
||||
commits.append({
|
||||
'hash': parts[0],
|
||||
'author': parts[1],
|
||||
'email': parts[2],
|
||||
'date': parts[3],
|
||||
'message': parts[4]
|
||||
})
|
||||
|
||||
return commits
|
||||
|
||||
def categorize_commit(commit_message: str) -> Tuple[str, str]:
|
||||
"""Categorize a commit based on its message."""
|
||||
message_lower = commit_message.lower()
|
||||
|
||||
# Define categories and their patterns
|
||||
categories = {
|
||||
'Features': [
|
||||
r'^feat(\(.+\))?:', r'^add:', r'^implement:',
|
||||
r'\bfeature\b', r'\badd\b.*\bfeature\b', r'\bnew\b.*\bfeature\b'
|
||||
],
|
||||
'Bug Fixes': [
|
||||
r'^fix(\(.+\))?:', r'^bug:', r'^hotfix:',
|
||||
r'\bfix\b', r'\bbug\b.*\bfix\b', r'\bresolve\b.*\bissue\b'
|
||||
],
|
||||
'Improvements': [
|
||||
r'^improve(\(.+\))?:', r'^enhance(\(.+\))?:', r'^refactor(\(.+\))?:',
|
||||
r'\bimprove\b', r'\benhance\b', r'\boptimize\b', r'\brefactor\b'
|
||||
],
|
||||
'Documentation': [
|
||||
r'^docs?(\(.+\))?:', r'^documentation:',
|
||||
r'\bdocs?\b', r'\bdocumentation\b', r'\breadme\b'
|
||||
],
|
||||
'Build System': [
|
||||
r'^build(\(.+\))?:', r'^ci(\(.+\))?:', r'^chore(\(.+\))?:',
|
||||
r'\bbuild\b', r'\bci\b', r'\bchore\b', r'\bscript\b'
|
||||
],
|
||||
'Tests': [
|
||||
r'^test(\(.+\))?:', r'\btest\b', r'\btesting\b'
|
||||
]
|
||||
}
|
||||
|
||||
# Check each category
|
||||
for category, patterns in categories.items():
|
||||
for pattern in patterns:
|
||||
if re.search(pattern, message_lower):
|
||||
# Clean up the message
|
||||
cleaned_message = commit_message
|
||||
# Remove conventional commit prefixes
|
||||
cleaned_message = re.sub(r'^(feat|fix|docs?|style|refactor|test|chore|build|ci|perf|improve|enhance|add|implement|bug|hotfix)(\(.+\))?:\s*', '', cleaned_message, flags=re.IGNORECASE)
|
||||
return category, cleaned_message
|
||||
|
||||
# Default category
|
||||
return 'Other Changes', commit_message
|
||||
|
||||
def generate_changelog_content(version: str, commits: List[Dict], previous_version: Optional[str] = None) -> str:
|
||||
"""Generate changelog content for a version."""
|
||||
lines = []
|
||||
|
||||
# Header
|
||||
lines.append(f"## [{version}] - {datetime.now().strftime('%Y-%m-%d')}")
|
||||
lines.append("")
|
||||
|
||||
if not commits:
|
||||
lines.append("No changes recorded.")
|
||||
lines.append("")
|
||||
return "\n".join(lines)
|
||||
|
||||
# Categorize commits
|
||||
categorized = {}
|
||||
for commit in commits:
|
||||
category, message = categorize_commit(commit['message'])
|
||||
if category not in categorized:
|
||||
categorized[category] = []
|
||||
categorized[category].append({
|
||||
'message': message,
|
||||
'hash': commit['hash'][:8],
|
||||
'author': commit['author']
|
||||
})
|
||||
|
||||
# Order categories by importance
|
||||
category_order = [
|
||||
'Features',
|
||||
'Bug Fixes',
|
||||
'Improvements',
|
||||
'Documentation',
|
||||
'Build System',
|
||||
'Tests',
|
||||
'Other Changes'
|
||||
]
|
||||
|
||||
# Generate sections
|
||||
for category in category_order:
|
||||
if category in categorized:
|
||||
lines.append(f"### {category}")
|
||||
lines.append("")
|
||||
for item in categorized[category]:
|
||||
# Format: - Message (hash)
|
||||
lines.append(f"- {item['message']} ({item['hash']})")
|
||||
lines.append("")
|
||||
|
||||
# Add comparison link if we have a previous version
|
||||
if previous_version:
|
||||
repo_url = "https://github.com/marcmintel/text-texture-generator" # Adjust as needed
|
||||
lines.append(f"**Full Changelog**: {repo_url}/compare/{previous_version}...{version}")
|
||||
lines.append("")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
def generate_full_changelog() -> str:
|
||||
"""Generate complete changelog from all tags."""
|
||||
lines = []
|
||||
|
||||
# Header
|
||||
lines.append("# Changelog")
|
||||
lines.append("")
|
||||
lines.append("All notable changes to this project will be documented in this file.")
|
||||
lines.append("")
|
||||
lines.append("The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),")
|
||||
lines.append("and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).")
|
||||
lines.append("")
|
||||
|
||||
# Get tags
|
||||
tags = get_git_tags()
|
||||
|
||||
if not tags:
|
||||
lines.append("## [Unreleased]")
|
||||
lines.append("")
|
||||
# Get all commits
|
||||
commits = get_commits_between(None, "HEAD")
|
||||
version_content = generate_changelog_content("Unreleased", commits)
|
||||
lines.append(version_content)
|
||||
else:
|
||||
# Process each version
|
||||
for i, tag in enumerate(tags):
|
||||
# Clean version (remove 'v' prefix if present)
|
||||
clean_version = tag.lstrip('v')
|
||||
|
||||
# Get previous tag
|
||||
previous_tag = tags[i + 1] if i + 1 < len(tags) else None
|
||||
|
||||
# Get commits for this version
|
||||
commits = get_commits_between(previous_tag, tag)
|
||||
|
||||
# Generate content
|
||||
version_content = generate_changelog_content(clean_version, commits, previous_tag)
|
||||
lines.append(version_content)
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
def generate_changelog_for_version(version: str, output_file: Optional[Path] = None) -> str:
|
||||
"""Generate changelog for a specific version."""
|
||||
tags = get_git_tags()
|
||||
|
||||
# Find the version tag
|
||||
version_tag = None
|
||||
previous_tag = None
|
||||
|
||||
for i, tag in enumerate(tags):
|
||||
if tag == version or tag == f"v{version}":
|
||||
version_tag = tag
|
||||
previous_tag = tags[i + 1] if i + 1 < len(tags) else None
|
||||
break
|
||||
|
||||
if not version_tag:
|
||||
print(f"Warning: Version tag '{version}' not found. Using current HEAD.")
|
||||
version_tag = "HEAD"
|
||||
previous_tag = tags[0] if tags else None
|
||||
|
||||
# Get commits
|
||||
commits = get_commits_between(previous_tag, version_tag)
|
||||
|
||||
# Generate content
|
||||
content = generate_changelog_content(version, commits, previous_tag)
|
||||
|
||||
# Write to file if specified
|
||||
if output_file:
|
||||
with open(output_file, 'w') as f:
|
||||
f.write(content)
|
||||
print(f"Changelog written to: {output_file}")
|
||||
|
||||
return content
|
||||
|
||||
def main():
|
||||
"""Main entry point."""
|
||||
parser = argparse.ArgumentParser(description="Generate changelog from git commits")
|
||||
parser.add_argument("--version", help="Generate changelog for specific version")
|
||||
parser.add_argument("--output", type=Path, help="Output file path")
|
||||
parser.add_argument("--full", action="store_true", help="Generate full changelog")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
if args.full:
|
||||
content = generate_full_changelog()
|
||||
output_file = args.output or get_project_root() / "CHANGELOG.md"
|
||||
elif args.version:
|
||||
content = generate_changelog_for_version(args.version, args.output)
|
||||
output_file = args.output
|
||||
else:
|
||||
# Default: generate for latest version
|
||||
tags = get_git_tags()
|
||||
if tags:
|
||||
latest_version = tags[0].lstrip('v')
|
||||
content = generate_changelog_for_version(latest_version, args.output)
|
||||
output_file = args.output
|
||||
else:
|
||||
print("No git tags found. Use --full to generate unreleased changelog.")
|
||||
sys.exit(1)
|
||||
|
||||
# Write to file or print
|
||||
if output_file and not args.version:
|
||||
with open(output_file, 'w') as f:
|
||||
f.write(content)
|
||||
print(f"Changelog written to: {output_file}")
|
||||
elif not args.version:
|
||||
print(content)
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Changelog generation failed: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
184
build/sync_version.py
Executable file
184
build/sync_version.py
Executable file
@@ -0,0 +1,184 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Version synchronization script for Text Texture Generator.
|
||||
Updates version information from git tags across all template files.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import Dict, Tuple, Optional
|
||||
|
||||
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)
|
||||
|
||||
def get_latest_git_tag() -> Optional[str]:
|
||||
"""Get the latest git tag that matches the version pattern."""
|
||||
try:
|
||||
# Get all tags sorted by version
|
||||
result = subprocess.run(['git', 'tag', '-l', '--sort=-version:refname'],
|
||||
capture_output=True, text=True, check=True)
|
||||
tags = result.stdout.strip().split('\n')
|
||||
|
||||
config = load_config()
|
||||
pattern = config['versions']['git_tag_pattern']
|
||||
version_regex = re.compile(pattern)
|
||||
|
||||
for tag in tags:
|
||||
if version_regex.match(tag):
|
||||
# Extract version number, removing 'v' prefix if present
|
||||
match = version_regex.match(tag)
|
||||
return match.group(1) if match else None
|
||||
|
||||
return None
|
||||
except subprocess.CalledProcessError:
|
||||
print("Warning: Could not get git tags. Using fallback version.")
|
||||
return None
|
||||
|
||||
def get_version_info(version_string: str) -> Tuple[str, int, int, int]:
|
||||
"""Parse version string into components."""
|
||||
parts = version_string.split('.')
|
||||
if len(parts) != 3:
|
||||
raise ValueError(f"Invalid version format: {version_string}")
|
||||
|
||||
major, minor, patch = map(int, parts)
|
||||
return version_string, major, minor, patch
|
||||
|
||||
def create_template_variables(config: Dict, version: str, version_type: str = "full") -> Dict[str, str]:
|
||||
"""Create template variables for file generation."""
|
||||
version_str, major, minor, patch = get_version_info(version)
|
||||
|
||||
# Load exclusions configuration for version limits
|
||||
exclusions_path = get_project_root() / "build" / "file_exclusions.json"
|
||||
with open(exclusions_path, 'r') as f:
|
||||
exclusions_config = json.load(f)
|
||||
|
||||
# Get version-specific configuration
|
||||
version_config = exclusions_config['version_configs'][version_type]
|
||||
|
||||
# Parse Blender version
|
||||
blender_version = config['project']['blender_version_min']
|
||||
blender_parts = blender_version.split('.')
|
||||
blender_major, blender_minor = int(blender_parts[0]), int(blender_parts[1])
|
||||
blender_patch = int(blender_parts[2]) if len(blender_parts) > 2 else 0
|
||||
|
||||
# Format dependencies for TOML
|
||||
deps = []
|
||||
for dep, version_spec in config['dependencies'].items():
|
||||
deps.append(f'{dep} = "{version_spec}"')
|
||||
dependencies_str = '\n'.join(deps)
|
||||
|
||||
variables = {
|
||||
'PROJECT_ID': config['project']['id'],
|
||||
'PROJECT_NAME': config['project']['name'],
|
||||
'PROJECT_AUTHOR': config['project']['author'],
|
||||
'PROJECT_DESCRIPTION': config['project']['description'],
|
||||
'PROJECT_CATEGORY': config['project']['category'],
|
||||
'PROJECT_LOCATION': config['project']['location'],
|
||||
'PROJECT_SUPPORT': config['project']['support'],
|
||||
'PROJECT_TAGS': json.dumps(config['project']['tags']),
|
||||
'PROJECT_LICENSE': json.dumps(config['project']['license']),
|
||||
'VERSION': version_str,
|
||||
'VERSION_MAJOR': str(major),
|
||||
'VERSION_MINOR': str(minor),
|
||||
'VERSION_PATCH': str(patch),
|
||||
'VERSION_TYPE': version_type,
|
||||
'BLENDER_VERSION_MIN': blender_version,
|
||||
'BLENDER_VERSION_MAJOR': str(blender_major),
|
||||
'BLENDER_VERSION_MINOR': str(blender_minor),
|
||||
'BLENDER_VERSION_PATCH': str(blender_patch),
|
||||
'DEPENDENCIES': dependencies_str,
|
||||
'MAX_TEXTURE_SIZE': str(version_config['max_texture_size']),
|
||||
'MAX_CONCURRENT_OPERATIONS': str(version_config['max_concurrent_operations'])
|
||||
}
|
||||
|
||||
return variables
|
||||
|
||||
def process_template(template_path: Path, output_path: Path, variables: Dict[str, str]):
|
||||
"""Process a template file and write the output."""
|
||||
print(f"Processing template: {template_path} -> {output_path}")
|
||||
|
||||
# Read template
|
||||
with open(template_path, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Replace variables
|
||||
for key, value in variables.items():
|
||||
content = content.replace(f'{{{{{key}}}}}', value)
|
||||
|
||||
# Ensure output directory exists
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Write output
|
||||
with open(output_path, 'w') as f:
|
||||
f.write(content)
|
||||
|
||||
print(f"Generated: {output_path}")
|
||||
|
||||
def sync_version(version: Optional[str] = None, version_type: str = "full"):
|
||||
"""Synchronize version across all template files."""
|
||||
config = load_config()
|
||||
|
||||
# Get version from git tag if not provided
|
||||
if version is None:
|
||||
version = get_latest_git_tag()
|
||||
if version is None:
|
||||
print("Warning: No valid git tag found. Using version from current blender_manifest.toml")
|
||||
# Fallback to current version
|
||||
manifest_path = get_project_root() / "src" / "blender_manifest.toml"
|
||||
if manifest_path.exists():
|
||||
with open(manifest_path, 'r') as f:
|
||||
for line in f:
|
||||
if line.startswith('version = '):
|
||||
version = line.split('=')[1].strip().strip('"')
|
||||
break
|
||||
if version is None:
|
||||
version = "1.0.0" # Ultimate fallback
|
||||
|
||||
print(f"Synchronizing version: {version} (type: {version_type})")
|
||||
|
||||
# Create template variables
|
||||
variables = create_template_variables(config, version, version_type)
|
||||
|
||||
# Process each template file
|
||||
templates_dir = get_project_root() / "build" / "templates"
|
||||
for template_file in templates_dir.glob("*.template"):
|
||||
# Determine output path
|
||||
relative_name = template_file.stem # Remove .template extension
|
||||
if relative_name == "blender_manifest.toml":
|
||||
output_path = get_project_root() / "src" / relative_name
|
||||
elif relative_name == "constants.py":
|
||||
output_path = get_project_root() / "src" / "utils" / relative_name
|
||||
else:
|
||||
# For other templates, maintain directory structure
|
||||
output_path = get_project_root() / "src" / relative_name
|
||||
|
||||
process_template(template_file, output_path, variables)
|
||||
|
||||
print(f"Version synchronization complete: {version}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description="Synchronize version information")
|
||||
parser.add_argument("--version", help="Version to set (defaults to latest git tag)")
|
||||
parser.add_argument("--type", choices=["free", "full"], default="full",
|
||||
help="Version type (free or full)")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
sync_version(args.version, args.type)
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
sys.exit(1)
|
||||
19
build/templates/blender_manifest.toml.template
Normal file
19
build/templates/blender_manifest.toml.template
Normal file
@@ -0,0 +1,19 @@
|
||||
schema_version = "1.0.0"
|
||||
|
||||
id = "{{PROJECT_ID}}"
|
||||
version = "{{VERSION}}"
|
||||
name = "{{PROJECT_NAME}}"
|
||||
tagline = "{{PROJECT_DESCRIPTION}}"
|
||||
maintainer = "{{PROJECT_AUTHOR}}"
|
||||
type = "add-on"
|
||||
support = "{{PROJECT_SUPPORT}}"
|
||||
|
||||
tags = {{PROJECT_TAGS}}
|
||||
|
||||
blender_version_min = "{{BLENDER_VERSION_MIN}}"
|
||||
|
||||
license = {{PROJECT_LICENSE}}
|
||||
|
||||
# Dependencies
|
||||
[dependencies]
|
||||
{{DEPENDENCIES}}
|
||||
32
build/templates/constants.py.template
Normal file
32
build/templates/constants.py.template
Normal file
@@ -0,0 +1,32 @@
|
||||
"""
|
||||
Constants and metadata for the Text Texture Generator addon.
|
||||
"""
|
||||
|
||||
bl_info = {
|
||||
"name": "{{PROJECT_NAME}}",
|
||||
"author": "{{PROJECT_AUTHOR}}",
|
||||
"version": ({{VERSION_MAJOR}}, {{VERSION_MINOR}}, {{VERSION_PATCH}}),
|
||||
"blender": ({{BLENDER_VERSION_MAJOR}}, {{BLENDER_VERSION_MINOR}}, {{BLENDER_VERSION_PATCH}}),
|
||||
"location": "{{PROJECT_LOCATION}}",
|
||||
"description": "{{PROJECT_DESCRIPTION}}",
|
||||
"category": "{{PROJECT_CATEGORY}}",
|
||||
}
|
||||
|
||||
# Version information
|
||||
VERSION_TYPE = "{{VERSION_TYPE}}" # "free" or "full"
|
||||
|
||||
# Version-specific limits (applied at build time through file exclusions)
|
||||
def get_version_limits():
|
||||
"""Get version-specific limits."""
|
||||
return {
|
||||
"max_texture_size": {{MAX_TEXTURE_SIZE}},
|
||||
"max_concurrent_operations": {{MAX_CONCURRENT_OPERATIONS}}
|
||||
}
|
||||
|
||||
def is_free_version():
|
||||
"""Check if this is the free version."""
|
||||
return VERSION_TYPE == "free"
|
||||
|
||||
def is_full_version():
|
||||
"""Check if this is the full version."""
|
||||
return VERSION_TYPE == "full"
|
||||
302
build/validate_package.py
Executable file
302
build/validate_package.py
Executable file
@@ -0,0 +1,302 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user