| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505 |
- #!/bin/bash
- #
- ##===----------------------------------------------------------------------===##
- ##
- ## This source file is part of the OpenSwiftUI open source project
- ##
- ## Copyright (c) 2025 the OpenSwiftUI project authors
- ## Licensed under Apache License v2.0
- ##
- ## See LICENSE.txt for license information
- ##
- ## SPDX-License-Identifier: Apache-2.0
- ##
- ##===----------------------------------------------------------------------===##
- set -euo pipefail
- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
- REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
- DOCS_DIR="$REPO_ROOT/.docs"
- BUILD_DIR="$DOCS_DIR/build"
- SYMBOL_GRAPH_DIR="$BUILD_DIR/symbol-graphs"
- DOCC_OUTPUT_DIR="$BUILD_DIR/docc-output"
- # Default configuration
- PREVIEW_MODE=false
- MINIMUM_ACCESS_LEVEL="public"
- TARGET_NAME="OpenSwiftUI"
- HOSTING_BASE_PATH=""
- CLEAN_BUILD=false
- PREVIEW_PORT=8000
- SOURCE_SERVICE="github"
- SOURCE_SERVICE_BASE_URL="https://github.com/OpenSwiftUIProject/OpenSwiftUI/blob/main"
- # Colors for output
- RED='\033[0;31m'
- GREEN='\033[0;32m'
- YELLOW='\033[1;33m'
- NC='\033[0m' # No Color
- usage() {
- cat << EOF
- Usage: $(basename "$0") [OPTIONS]
- Build Swift documentation using DocC with optional local preview.
- OPTIONS:
- --preview Preview documentation locally with HTTP server
- --minimum-access-level LEVEL Set minimum access level (public, package, internal, private, fileprivate)
- Default: public
- --target TARGET Target to document (default: OpenSwiftUI)
- --hosting-base-path PATH Base path for hosting (e.g., /OpenSwiftUI)
- --port PORT Port for preview server (default: 8000)
- --source-service SERVICE Source service (github, gitlab, bitbucket)
- --source-service-base-url URL Base URL for source service
- (e.g., https://github.com/user/repo/blob/main)
- --clean Clean build artifacts and force rebuild
- -h, --help Show this help message
- ENVIRONMENT:
- DOCC_HTML_DIR Use an existing swift-docc-render dist directory.
- When unset, DocC uses its bundled renderer.
- EXAMPLES:
- # Build and preview documentation (source links enabled by default)
- $(basename "$0") --preview
- # Preview with internal symbols on port 8080
- $(basename "$0") --preview --minimum-access-level internal --port 8080
- # Clean rebuild
- $(basename "$0") --preview --clean
- # Build for specific target
- $(basename "$0") --target OpenSwiftUICore --preview
- # Build with custom source service (e.g., for a fork)
- $(basename "$0") --preview \\
- --source-service github \\
- --source-service-base-url https://github.com/yourname/OpenSwiftUI/blob/custom-branch
- EOF
- exit 0
- }
- log_info() {
- echo -e "${GREEN}[INFO]${NC} $1"
- }
- log_warning() {
- echo -e "${YELLOW}[WARNING]${NC} $1"
- }
- log_error() {
- echo -e "${RED}[ERROR]${NC} $1"
- }
- # Function to run docc command
- rundocc() {
- if command -v xcrun >/dev/null 2>&1; then
- xcrun docc "$@"
- else
- docc "$@"
- fi
- }
- configure_docc_renderer() {
- if [[ -n "${DOCC_HTML_DIR:-}" ]]; then
- [[ -d "$DOCC_HTML_DIR" ]] || {
- log_error "DOCC_HTML_DIR does not exist: $DOCC_HTML_DIR"
- exit 1
- }
- export DOCC_HTML_DIR
- log_info "Using DocC renderer: $DOCC_HTML_DIR"
- return
- fi
- log_info "DocC Renderer: bundled default"
- }
- # Parse command line arguments
- while [[ $# -gt 0 ]]; do
- case $1 in
- --preview)
- PREVIEW_MODE=true
- shift
- ;;
- --minimum-access-level)
- MINIMUM_ACCESS_LEVEL="$2"
- shift 2
- ;;
- --target)
- TARGET_NAME="$2"
- shift 2
- ;;
- --hosting-base-path)
- HOSTING_BASE_PATH="$2"
- shift 2
- ;;
- --port)
- PREVIEW_PORT="$2"
- shift 2
- ;;
- --source-service)
- SOURCE_SERVICE="$2"
- shift 2
- ;;
- --source-service-base-url)
- SOURCE_SERVICE_BASE_URL="$2"
- shift 2
- ;;
- --clean)
- CLEAN_BUILD=true
- shift
- ;;
- -h|--help)
- usage
- ;;
- *)
- log_error "Unknown option: $1"
- usage
- ;;
- esac
- done
- # Validate minimum access level
- case "$MINIMUM_ACCESS_LEVEL" in
- public|package|internal|private|fileprivate)
- ;;
- *)
- log_error "Invalid minimum access level: $MINIMUM_ACCESS_LEVEL"
- log_error "Valid values: public, package, internal, private, fileprivate"
- exit 1
- ;;
- esac
- log_info "Configuration:"
- log_info " Target: $TARGET_NAME"
- log_info " Minimum Access Level: $MINIMUM_ACCESS_LEVEL"
- log_info " Preview Mode: $PREVIEW_MODE"
- if [[ "$PREVIEW_MODE" == true ]]; then
- log_info " Preview Port: $PREVIEW_PORT"
- fi
- if [[ -n "$HOSTING_BASE_PATH" && "$PREVIEW_MODE" == true ]]; then
- log_warning "--hosting-base-path is ignored in preview mode; it is intended for static hosting."
- elif [[ -n "$HOSTING_BASE_PATH" ]]; then
- log_info " Hosting Base Path: $HOSTING_BASE_PATH"
- fi
- if [[ -n "$SOURCE_SERVICE" ]]; then
- log_info " Source Service: $SOURCE_SERVICE"
- log_info " Source Service Base URL: $SOURCE_SERVICE_BASE_URL"
- fi
- if [[ -n "${DOCC_HTML_DIR:-}" ]]; then
- log_info " DocC Renderer: $DOCC_HTML_DIR"
- else
- log_info " DocC Renderer: bundled default"
- fi
- # Validate source service configuration
- if [[ -n "$SOURCE_SERVICE" ]] && [[ -z "$SOURCE_SERVICE_BASE_URL" ]]; then
- log_error "--source-service requires --source-service-base-url"
- exit 1
- fi
- if [[ -z "$SOURCE_SERVICE" ]] && [[ -n "$SOURCE_SERVICE_BASE_URL" ]]; then
- log_error "--source-service-base-url requires --source-service"
- exit 1
- fi
- # Check for required tools
- command -v swift >/dev/null 2>&1 || {
- log_error "swift is required but not installed. Aborting."
- exit 1
- }
- # Check for docc
- if ! command -v docc >/dev/null 2>&1 && ! command -v xcrun >/dev/null 2>&1; then
- log_error "docc is required but not found. Please install Swift-DocC."
- exit 1
- fi
- # Clean build if requested
- if [[ "$CLEAN_BUILD" == true ]]; then
- log_info "Cleaning build artifacts..."
- swift package clean
- rm -rf "$DOCS_DIR"
- fi
- # Create build directories
- log_info "Preparing build directories..."
- mkdir -p "$SYMBOL_GRAPH_DIR"
- mkdir -p "$DOCC_OUTPUT_DIR"
- configure_docc_renderer
- # Step 1: Generate symbol graphs
- cd "$REPO_ROOT"
- # Use default .build directory for symbol graphs (reuses existing build)
- SWIFT_BUILD_DIR=".build"
- DEFAULT_SYMBOL_GRAPH_DIR="$SWIFT_BUILD_DIR/symbol-graphs"
- # Check if symbol graphs already exist for the target
- REBUILD_NEEDED=false
- if [[ ! -f "$DEFAULT_SYMBOL_GRAPH_DIR/${TARGET_NAME}.symbols.json" ]]; then
- REBUILD_NEEDED=true
- log_info "No existing symbol graphs found for $TARGET_NAME, will perform clean build..."
- else
- log_info "Found existing symbol graphs for $TARGET_NAME, reusing them (use --clean to rebuild)"
- fi
- if [[ "$REBUILD_NEEDED" == true ]] || [[ "$CLEAN_BUILD" == true ]]; then
- # Clean build to ensure symbol graphs are generated
- if [[ "$REBUILD_NEEDED" == true ]]; then
- log_info "Cleaning build to ensure symbol graph generation..."
- swift package clean
- fi
- log_info "Generating symbol graphs..."
- swift build \
- --target "$TARGET_NAME" \
- -Xswiftc -emit-symbol-graph \
- -Xswiftc -emit-symbol-graph-dir \
- -Xswiftc "$DEFAULT_SYMBOL_GRAPH_DIR" \
- -Xswiftc -symbol-graph-minimum-access-level \
- -Xswiftc "$MINIMUM_ACCESS_LEVEL"
- if [[ ! -d "$DEFAULT_SYMBOL_GRAPH_DIR" ]] || [[ -z "$(ls -A "$DEFAULT_SYMBOL_GRAPH_DIR")" ]]; then
- log_error "Symbol graph generation failed or produced no output"
- exit 1
- fi
- fi
- # Filter symbol graphs for the target module
- # Only include the target itself (which already includes re-exported OpenSwiftUICore symbols)
- log_info "Filtering symbol graphs for $TARGET_NAME..."
- if ls "$DEFAULT_SYMBOL_GRAPH_DIR/${TARGET_NAME}.symbols.json" >/dev/null 2>&1; then
- # Copy only the main target symbol graphs
- # OpenSwiftUI already includes OpenSwiftUICore symbols via @_exported import
- # Use explicit patterns to avoid matching OpenSwiftUICore*.symbols.json
- cp "$DEFAULT_SYMBOL_GRAPH_DIR/${TARGET_NAME}.symbols.json" "$SYMBOL_GRAPH_DIR/" 2>/dev/null || true
- cp "$DEFAULT_SYMBOL_GRAPH_DIR/${TARGET_NAME}@"*.symbols.json "$SYMBOL_GRAPH_DIR/" 2>/dev/null || true
- log_info "Symbol graphs for $TARGET_NAME copied successfully"
- # Filter out symbols from unwanted modules (CoreFoundation, CoreGraphics, etc.)
- log_info "Removing re-exported system framework symbols..."
- python3 << EOF
- import json
- import sys
- import os
- def filter_symbol_graph(file_path, allowed_modules):
- """Filter symbol graph to only include symbols from allowed modules."""
- try:
- with open(file_path, 'r') as f:
- data = json.load(f)
- if 'symbols' in data and isinstance(data['symbols'], list):
- original_count = len(data['symbols'])
- # Filter symbols by extracting module from precise identifier
- filtered_symbols = []
- for symbol in data['symbols']:
- precise = symbol.get('identifier', {}).get('precise', '')
- module = None
- # Extract module from mangled Swift names
- if precise.startswith('s:'):
- rest = precise[2:]
- if rest and rest[0].isdigit():
- i = 0
- while i < len(rest) and rest[i].isdigit():
- i += 1
- if i > 0:
- mod_len = int(rest[:i])
- module = rest[i:i+mod_len]
- # Keep if it's from an allowed module
- if module and module in allowed_modules:
- filtered_symbols.append(symbol)
- data['symbols'] = filtered_symbols
- filtered_count = len(filtered_symbols)
- # Write back
- with open(file_path, 'w') as f:
- json.dump(data, f)
- print(f" {os.path.basename(file_path)}: {original_count} -> {filtered_count} symbols", file=sys.stderr)
- return True
- return False
- except Exception as e:
- print(f" Error filtering {file_path}: {e}", file=sys.stderr)
- return False
- # List of allowed modules (OpenSwiftUI and OpenSwiftUICore only)
- allowed_modules = {'OpenSwiftUI', 'OpenSwiftUICore', '$TARGET_NAME'}
- # Filter the main OpenSwiftUI symbol graph
- symbol_file = '$SYMBOL_GRAPH_DIR/OpenSwiftUI.symbols.json'
- if os.path.exists(symbol_file):
- filter_symbol_graph(symbol_file, allowed_modules)
- EOF
- else
- log_error "No symbol graphs found for $TARGET_NAME"
- log_error "Available symbol graphs:"
- ls "$DEFAULT_SYMBOL_GRAPH_DIR"/*.symbols.json 2>/dev/null || echo " (none)"
- exit 1
- fi
- # Step 2: Find or create documentation catalog
- DOCC_CATALOG=""
- if [[ -d "Sources/$TARGET_NAME/${TARGET_NAME}.docc" ]]; then
- DOCC_CATALOG="Sources/$TARGET_NAME/${TARGET_NAME}.docc"
- log_info "Using documentation catalog: $DOCC_CATALOG"
- else
- log_warning "No .docc catalog found for $TARGET_NAME"
- log_info "DocC will generate documentation from symbol graphs only"
- fi
- # Step 3: Build documentation
- log_info "Building documentation archive..."
- if [[ -n "$DOCC_CATALOG" ]]; then
- DOCC_ARGS=(
- "$DOCC_CATALOG"
- --emit-digest
- --transform-for-static-hosting
- --output-path "$DOCC_OUTPUT_DIR"
- )
- if [[ "$PREVIEW_MODE" == true ]]; then
- DOCC_ARGS+=(--port "$PREVIEW_PORT")
- fi
- if [[ -n "$HOSTING_BASE_PATH" && "$PREVIEW_MODE" == false ]]; then
- DOCC_ARGS+=(--hosting-base-path "$HOSTING_BASE_PATH")
- fi
- if [[ -n "$SOURCE_SERVICE" ]]; then
- DOCC_ARGS+=(--source-service "$SOURCE_SERVICE")
- DOCC_ARGS+=(--source-service-base-url "$SOURCE_SERVICE_BASE_URL")
- DOCC_ARGS+=(--checkout-path "$REPO_ROOT")
- fi
- if [[ -d "$SYMBOL_GRAPH_DIR" ]] && [[ -n "$(ls -A "$SYMBOL_GRAPH_DIR")" ]]; then
- DOCC_ARGS+=(--additional-symbol-graph-dir "$SYMBOL_GRAPH_DIR")
- fi
- # Enabled experimental features:
- DOCC_ARGS+=(--enable-experimental-overloaded-symbol-presentation)
- if [[ "$PREVIEW_MODE" == true ]]; then
- # Check if port is already in use
- if lsof -Pi :"$PREVIEW_PORT" -sTCP:LISTEN -t >/dev/null 2>&1; then
- log_warning "Port $PREVIEW_PORT is already in use"
- # Find the process using the port
- PORT_PID=$(lsof -Pi :"$PREVIEW_PORT" -sTCP:LISTEN -t)
- PORT_PROCESS=$(ps -p "$PORT_PID" -o command= 2>/dev/null || echo "Unknown process")
- log_info "Process using port $PREVIEW_PORT (PID $PORT_PID): $PORT_PROCESS"
- # Ask user if they want to kill it
- read -p "Do you want to kill this process and start the preview server? (y/N): " -n 1 -r
- echo
- if [[ $REPLY =~ ^[Yy]$ ]]; then
- log_info "Killing process $PORT_PID..."
- kill "$PORT_PID"
- sleep 1
- # Verify it's killed
- if lsof -Pi :"$PREVIEW_PORT" -sTCP:LISTEN -t >/dev/null 2>&1; then
- log_error "Failed to kill process on port $PREVIEW_PORT"
- exit 1
- fi
- log_info "Process killed successfully"
- else
- log_error "Cannot start preview server. Please free port $PREVIEW_PORT or use --port option"
- exit 1
- fi
- fi
- log_info "Starting documentation preview server on port $PREVIEW_PORT..."
- log_info "Press Ctrl+C to stop the server"
- rundocc preview "${DOCC_ARGS[@]}"
- else
- rundocc convert "${DOCC_ARGS[@]}"
- fi
- else
- TEMP_DOCC_CATALOG="$BUILD_DIR/${TARGET_NAME}.docc"
- mkdir -p "$TEMP_DOCC_CATALOG"
- if [[ -d "$SYMBOL_GRAPH_DIR" ]] && [[ -n "$(ls -A "$SYMBOL_GRAPH_DIR")" ]]; then
- cp "$SYMBOL_GRAPH_DIR"/*.symbols.json "$TEMP_DOCC_CATALOG/"
- fi
- DOCC_ARGS=(
- "$TEMP_DOCC_CATALOG"
- --emit-digest
- --transform-for-static-hosting
- --output-path "$DOCC_OUTPUT_DIR"
- )
- if [[ "$PREVIEW_MODE" == true ]]; then
- DOCC_ARGS+=(--port "$PREVIEW_PORT")
- fi
- if [[ -n "$HOSTING_BASE_PATH" && "$PREVIEW_MODE" == false ]]; then
- DOCC_ARGS+=(--hosting-base-path "$HOSTING_BASE_PATH")
- fi
- if [[ -n "$SOURCE_SERVICE" ]]; then
- DOCC_ARGS+=(--source-service "$SOURCE_SERVICE")
- DOCC_ARGS+=(--source-service-base-url "$SOURCE_SERVICE_BASE_URL")
- DOCC_ARGS+=(--checkout-path "$REPO_ROOT")
- fi
- if [[ "$PREVIEW_MODE" == true ]]; then
- # Check if port is already in use
- if lsof -Pi :"$PREVIEW_PORT" -sTCP:LISTEN -t >/dev/null 2>&1; then
- log_warning "Port $PREVIEW_PORT is already in use"
- # Find the process using the port
- PORT_PID=$(lsof -Pi :"$PREVIEW_PORT" -sTCP:LISTEN -t)
- PORT_PROCESS=$(ps -p "$PORT_PID" -o command= 2>/dev/null || echo "Unknown process")
- log_info "Process using port $PREVIEW_PORT (PID $PORT_PID): $PORT_PROCESS"
- # Ask user if they want to kill it
- read -p "Do you want to kill this process and start the preview server? (y/N): " -n 1 -r
- echo
- if [[ $REPLY =~ ^[Yy]$ ]]; then
- log_info "Killing process $PORT_PID..."
- kill "$PORT_PID"
- sleep 1
- # Verify it's killed
- if lsof -Pi :"$PREVIEW_PORT" -sTCP:LISTEN -t >/dev/null 2>&1; then
- log_error "Failed to kill process on port $PREVIEW_PORT"
- exit 1
- fi
- log_info "Process killed successfully"
- else
- log_error "Cannot start preview server. Please free port $PREVIEW_PORT or use --port option"
- exit 1
- fi
- fi
- log_info "Starting documentation preview server on port $PREVIEW_PORT..."
- log_info "Press Ctrl+C to stop the server"
- rundocc preview "${DOCC_ARGS[@]}"
- else
- rundocc convert "${DOCC_ARGS[@]}"
- fi
- fi
- if [[ "$PREVIEW_MODE" == false ]]; then
- log_info "Documentation built successfully"
- log_info "Documentation output: $DOCC_OUTPUT_DIR"
- fi
- log_info "Done!"
|