This commit is contained in:
2026-02-19 00:33:08 -08:00
parent e37f3dd7b1
commit 70dd0779f2
143 changed files with 31888 additions and 0 deletions

View File

@@ -0,0 +1,310 @@
#!/usr/bin/env python3
"""
Build and Test Automation for Xcode Projects
Ultra token-efficient build automation with progressive disclosure via xcresult bundles.
Features:
- Minimal default output (5-10 tokens)
- Progressive disclosure for error/warning/log details
- Native xcresult bundle support
- Clean modular architecture
Usage Examples:
# Build (minimal output)
python scripts/build_and_test.py --project MyApp.xcodeproj
# Output: Build: SUCCESS (0 errors, 3 warnings) [xcresult-20251018-143052]
# Get error details
python scripts/build_and_test.py --get-errors xcresult-20251018-143052
# Get warnings
python scripts/build_and_test.py --get-warnings xcresult-20251018-143052
# Get build log
python scripts/build_and_test.py --get-log xcresult-20251018-143052
# Get everything as JSON
python scripts/build_and_test.py --get-all xcresult-20251018-143052 --json
# List recent builds
python scripts/build_and_test.py --list-xcresults
# Verbose mode (for debugging)
python scripts/build_and_test.py --project MyApp.xcodeproj --verbose
"""
import argparse
import sys
from pathlib import Path
# Import our modular components
from xcode import BuildRunner, OutputFormatter, XCResultCache, XCResultParser
def main():
"""Main entry point."""
parser = argparse.ArgumentParser(
description="Build and test Xcode projects with progressive disclosure",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Build project (minimal output)
python scripts/build_and_test.py --project MyApp.xcodeproj
# Run tests
python scripts/build_and_test.py --project MyApp.xcodeproj --test
# Get error details from previous build
python scripts/build_and_test.py --get-errors xcresult-20251018-143052
# Get all details as JSON
python scripts/build_and_test.py --get-all xcresult-20251018-143052 --json
# List recent builds
python scripts/build_and_test.py --list-xcresults
""",
)
# Build/test mode arguments
build_group = parser.add_argument_group("Build/Test Options")
project_group = build_group.add_mutually_exclusive_group()
project_group.add_argument("--project", help="Path to .xcodeproj file")
project_group.add_argument("--workspace", help="Path to .xcworkspace file")
build_group.add_argument("--scheme", help="Build scheme (auto-detected if not specified)")
build_group.add_argument(
"--configuration",
default="Debug",
choices=["Debug", "Release"],
help="Build configuration (default: Debug)",
)
build_group.add_argument("--simulator", help="Simulator name (default: iPhone 15)")
build_group.add_argument("--clean", action="store_true", help="Clean before building")
build_group.add_argument("--test", action="store_true", help="Run tests")
build_group.add_argument("--suite", help="Specific test suite to run")
# Progressive disclosure arguments
disclosure_group = parser.add_argument_group("Progressive Disclosure Options")
disclosure_group.add_argument(
"--get-errors", metavar="XCRESULT_ID", help="Get error details from xcresult"
)
disclosure_group.add_argument(
"--get-warnings", metavar="XCRESULT_ID", help="Get warning details from xcresult"
)
disclosure_group.add_argument(
"--get-log", metavar="XCRESULT_ID", help="Get build log from xcresult"
)
disclosure_group.add_argument(
"--get-all", metavar="XCRESULT_ID", help="Get all details from xcresult"
)
disclosure_group.add_argument(
"--list-xcresults", action="store_true", help="List recent xcresult bundles"
)
# Output options
output_group = parser.add_argument_group("Output Options")
output_group.add_argument("--verbose", action="store_true", help="Show detailed output")
output_group.add_argument("--json", action="store_true", help="Output as JSON")
args = parser.parse_args()
# Initialize cache
cache = XCResultCache()
# Handle list mode
if args.list_xcresults:
xcresults = cache.list()
if args.json:
import json
print(json.dumps(xcresults, indent=2))
elif not xcresults:
print("No xcresult bundles found")
else:
print(f"Recent XCResult bundles ({len(xcresults)}):")
print()
for xc in xcresults:
print(f" {xc['id']}")
print(f" Created: {xc['created']}")
print(f" Size: {xc['size_mb']} MB")
print()
return 0
# Handle retrieval modes
xcresult_id = args.get_errors or args.get_warnings or args.get_log or args.get_all
if xcresult_id:
xcresult_path = cache.get_path(xcresult_id)
if not xcresult_path or not xcresult_path.exists():
print(f"Error: XCResult bundle not found: {xcresult_id}", file=sys.stderr)
print("Use --list-xcresults to see available bundles", file=sys.stderr)
return 1
# Load cached stderr for progressive disclosure
cached_stderr = cache.get_stderr(xcresult_id)
parser = XCResultParser(xcresult_path, stderr=cached_stderr)
# Get errors
if args.get_errors:
errors = parser.get_errors()
if args.json:
import json
print(json.dumps(errors, indent=2))
else:
print(OutputFormatter.format_errors(errors))
return 0
# Get warnings
if args.get_warnings:
warnings = parser.get_warnings()
if args.json:
import json
print(json.dumps(warnings, indent=2))
else:
print(OutputFormatter.format_warnings(warnings))
return 0
# Get log
if args.get_log:
log = parser.get_build_log()
if log:
print(OutputFormatter.format_log(log))
else:
print("No build log available", file=sys.stderr)
return 1
return 0
# Get all
if args.get_all:
error_count, warning_count = parser.count_issues()
errors = parser.get_errors()
warnings = parser.get_warnings()
build_log = parser.get_build_log()
if args.json:
import json
data = {
"xcresult_id": xcresult_id,
"error_count": error_count,
"warning_count": warning_count,
"errors": errors,
"warnings": warnings,
"log_preview": build_log[:1000] if build_log else None,
}
print(json.dumps(data, indent=2))
else:
print(f"XCResult: {xcresult_id}")
print(f"Errors: {error_count}, Warnings: {warning_count}")
print()
if errors:
print(OutputFormatter.format_errors(errors, limit=10))
print()
if warnings:
print(OutputFormatter.format_warnings(warnings, limit=10))
print()
if build_log:
print("Build Log (last 30 lines):")
print(OutputFormatter.format_log(build_log, lines=30))
return 0
# Build/test mode
if not args.project and not args.workspace:
# Try to auto-detect in current directory
cwd = Path.cwd()
projects = list(cwd.glob("*.xcodeproj"))
workspaces = list(cwd.glob("*.xcworkspace"))
if workspaces:
args.workspace = str(workspaces[0])
elif projects:
args.project = str(projects[0])
else:
parser.error("No project or workspace specified and none found in current directory")
# Initialize builder
builder = BuildRunner(
project_path=args.project,
workspace_path=args.workspace,
scheme=args.scheme,
configuration=args.configuration,
simulator=args.simulator,
cache=cache,
)
# Execute build or test
if args.test:
success, xcresult_id, stderr = builder.test(test_suite=args.suite)
else:
success, xcresult_id, stderr = builder.build(clean=args.clean)
if not xcresult_id and not stderr:
print("Error: Build/test failed without creating xcresult or error output", file=sys.stderr)
return 1
# Save stderr to cache for progressive disclosure
if xcresult_id and stderr:
cache.save_stderr(xcresult_id, stderr)
# Parse results
xcresult_path = cache.get_path(xcresult_id) if xcresult_id else None
parser = XCResultParser(xcresult_path, stderr=stderr)
error_count, warning_count = parser.count_issues()
# Format output
status = "SUCCESS" if success else "FAILED"
# Generate hints for failed builds
hints = None
if not success:
errors = parser.get_errors()
hints = OutputFormatter.generate_hints(errors)
if args.verbose:
# Verbose mode with error/warning details
errors = parser.get_errors() if error_count > 0 else None
warnings = parser.get_warnings() if warning_count > 0 else None
output = OutputFormatter.format_verbose(
status=status,
error_count=error_count,
warning_count=warning_count,
xcresult_id=xcresult_id or "N/A",
errors=errors,
warnings=warnings,
)
print(output)
elif args.json:
# JSON mode
data = {
"success": success,
"xcresult_id": xcresult_id or None,
"error_count": error_count,
"warning_count": warning_count,
}
if hints:
data["hints"] = hints
import json
print(json.dumps(data, indent=2))
else:
# Minimal mode (default)
output = OutputFormatter.format_minimal(
status=status,
error_count=error_count,
warning_count=warning_count,
xcresult_id=xcresult_id or "N/A",
hints=hints,
)
print(output)
# Exit with appropriate code
return 0 if success else 1
if __name__ == "__main__":
sys.exit(main())