diff --git a/.gitignore b/.gitignore index b18037c69..43e4312c6 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,5 @@ __Snapshots__ Example/ReferenceImages/**/*.png .claude +.docs +gh-pages diff --git a/Docs/Documentation.md b/Docs/Documentation.md new file mode 100644 index 000000000..011530cc2 --- /dev/null +++ b/Docs/Documentation.md @@ -0,0 +1,280 @@ +# Documentation + +This document explains how OpenSwiftUI's documentation is built and hosted. + +## Overview + +OpenSwiftUI uses Swift-DocC to generate API documentation, which is hosted on GitHub Pages at: + +**https://openswiftuiproject.github.io/OpenSwiftUI/documentation/openswiftui/** + +The documentation is built from the `OpenSwiftUI` target and includes symbols from `OpenSwiftUICore` via `@_exported import`. System framework symbols (CoreFoundation, CoreGraphics, etc.) are filtered out to keep the documentation focused on OpenSwiftUI's own APIs. + +## Documentation Scripts + +Two scripts are provided for documentation management: + +### `Scripts/build-documentation.sh` + +Builds documentation locally with optional preview server. + +**Usage:** +```bash +# Build and preview documentation locally +./Scripts/build-documentation.sh --preview + +# Build with internal symbols visible +./Scripts/build-documentation.sh --minimum-access-level internal + +# Preview on a different port +./Scripts/build-documentation.sh --preview --port 8080 + +# Clean build (force regenerate symbol graphs) +./Scripts/build-documentation.sh --clean +``` + +**Options:** +- `--preview` - Start a local HTTP server to preview documentation +- `--port PORT` - Port for preview server (default: 8000) +- `--minimum-access-level LEVEL` - Symbol visibility: public, internal, private, fileprivate (default: public) +- `--target TARGET` - Documentation target (default: OpenSwiftUI) +- `--hosting-base-path PATH` - Base path for hosting (e.g., /OpenSwiftUI) +- `--source-service SERVICE` - Source service (github, gitlab, bitbucket) +- `--source-service-base-url URL` - Base URL for source links +- `--clean` - Clean build artifacts and force rebuild + +**Example workflow:** +```bash +# Preview documentation locally before deploying +./Scripts/build-documentation.sh --preview + +# Open http://localhost:8000/documentation/openswiftui in your browser +# Make changes to documentation comments in source code +# Re-run the script to see updates (uses incremental builds) +``` + +### `Scripts/update-gh-pages-documentation.sh` + +Builds and deploys documentation to GitHub Pages. + +**Usage:** +```bash +# Build and deploy documentation to GitHub Pages +./Scripts/update-gh-pages-documentation.sh --hosting-base-path /OpenSwiftUI + +# Test deployment without pushing (dry-run) +./Scripts/update-gh-pages-documentation.sh --hosting-base-path /OpenSwiftUI --echo-without-push + +# Deploy using existing build (skip building) +./Scripts/update-gh-pages-documentation.sh --no-build --hosting-base-path /OpenSwiftUI + +# Deploy with internal symbols +./Scripts/update-gh-pages-documentation.sh --minimum-access-level internal --hosting-base-path /OpenSwiftUI +``` + +**Important:** Always use `--hosting-base-path /OpenSwiftUI` when deploying to GitHub Pages, as the site is served from a subdirectory. Without this flag, CSS/JS resources will fail to load. + +**Options:** +- `--hosting-base-path PATH` - **Required** for GitHub Pages deployment (use `/OpenSwiftUI`) +- `--no-build` - Skip building, use existing documentation output +- `--echo-without-push` - Show push command without executing (for testing) +- `--minimum-access-level LEVEL` - Symbol visibility (default: public) +- `--clean` - Clean build artifacts and force rebuild +- `--no-force` - Don't force push (preserves gh-pages history, increases repo size) + +**How it works:** +1. Builds documentation using `build-documentation.sh` +2. Creates or updates the `gh-pages` branch as a git worktree at `gh-pages/` +3. Copies documentation files to `gh-pages/docs/` +4. Commits and force-pushes to `origin/gh-pages` +5. Cleans up the worktree + +**Note:** Force push is used by default to keep the repository size small by avoiding accumulation of large binary files (CSS, JS, images) in git history. Each deployment completely replaces the previous one. + +## GitHub Pages Configuration + +The documentation is deployed to the `gh-pages` branch with the following structure: + +``` +gh-pages/ +├── .nojekyll # Prevents Jekyll processing +└── docs/ # Documentation root (configured in GitHub Pages settings) + ├── index.html + ├── css/ + ├── js/ + ├── data/ + ├── documentation/ + └── ... +``` + +**GitHub Pages settings:** +- **Source:** Deploy from branch +- **Branch:** `gh-pages` +- **Folder:** `/docs` + +## Why Self-Hosted Instead of Swift Package Index? + +We initially used [Swift Package Index (SPI)](https://swiftpackageindex.com) for documentation hosting, which worked well and provided excellent features like multi-version documentation picker. However, we encountered several limitations that led us to switch to self-hosted GitHub Pages: + +### Limitations of SPI Documentation + +SPI's documentation system is built on `swift-docc-plugin` and SwiftPM, which currently has some constraints: + +1. **Binary Target Limitations** - SwiftPM has issues with binary targets in documentation builds ([swiftlang/swift-package-manager#7580](https://github.com/swiftlang/swift-package-manager/issues/7580)). We had to add `isSPIDocGenerationBuild` workarounds in `Package.swift` to exclude certain dependencies during SPI builds. + +2. **Exported Symbol Handling** - SwiftPM's symbol graph generation doesn't properly handle `@_exported import` declarations ([swiftlang/swift-package-manager#9101](https://github.com/swiftlang/swift-package-manager/issues/9101)), which is essential for OpenSwiftUI's re-export architecture where `OpenSwiftUI` re-exports `OpenSwiftUICore`. + +3. **Limited Customization** - The plugin-based approach doesn't provide fine-grained control over symbol filtering, documentation generation parameters, or output customization that we need for a complex project like OpenSwiftUI. + +### Benefits of Self-Hosted Documentation + +By self-hosting, we gain: + +- **Full Control** - Direct access to Swift-DocC compiler flags and symbol graph filtering +- **Flexible Deployment** - Custom scripts tailored to OpenSwiftUI's specific needs +- **Faster Iteration** - No dependency on external service processing times +- **Symbol Filtering** - Ability to filter out re-exported system framework symbols (CoreFoundation, CoreGraphics) that would otherwise clutter the documentation +- **Custom Build Workflow** - Support for local preview, incremental builds, and testing before deployment + +We appreciate the Swift Package Index team's efforts in providing documentation hosting for the Swift community. The decision to self-host is purely technical, driven by OpenSwiftUI's specific requirements and architectural constraints rather than any shortcomings of SPI itself. + +## Updating Documentation + +### For regular updates: + +```bash +# 1. Make changes to documentation comments in source code + +# 2. Preview locally +./Scripts/build-documentation.sh --preview + +# 3. Verify changes at http://localhost:8000/documentation/openswiftui + +# 4. Deploy to GitHub Pages +./Scripts/update-gh-pages-documentation.sh --hosting-base-path /OpenSwiftUI + +# 5. Wait 1-2 minutes for GitHub Pages to rebuild +# 6. Verify at https://openswiftuiproject.github.io/OpenSwiftUI/documentation/openswiftui/ +``` + +### For major API changes: + +If you've added new public APIs or significantly changed the module structure, use `--clean` to force regeneration of symbol graphs: + +```bash +./Scripts/update-gh-pages-documentation.sh --clean --hosting-base-path /OpenSwiftUI +``` + +## Troubleshooting + +### CSS/JS files not loading (404 errors) + +**Symptom:** Documentation page loads but appears unstyled, browser console shows 404 errors for CSS/JS files. + +**Cause:** Documentation was built without `--hosting-base-path /OpenSwiftUI` flag. + +**Solution:** Rebuild and redeploy with the correct flag: +```bash +./Scripts/update-gh-pages-documentation.sh --hosting-base-path /OpenSwiftUI +``` + +### Symbol graphs are stale + +**Symptom:** New APIs don't appear in documentation. + +**Cause:** Symbol graphs weren't regenerated. + +**Solution:** Use `--clean` flag to force regeneration: +```bash +./Scripts/build-documentation.sh --clean +``` + +### Documentation includes system framework symbols + +**Symptom:** Documentation shows CoreFoundation, CoreGraphics symbols. + +**Cause:** Symbol filtering failed or was disabled. + +**Solution:** The filtering is automatic. If you see system symbols, check the build output for filtering errors and ensure the Python JSON filtering step completed successfully. + +### Port already in use (preview) + +**Symptom:** `Address already in use` error when running preview. + +**Solution:** The script will detect this and prompt you to kill the existing process, or use a different port: +```bash +./Scripts/build-documentation.sh --preview --port 8001 +``` + +## Advanced Usage + +### Building documentation for OpenSwiftUICore + +```bash +./Scripts/build-documentation.sh --target OpenSwiftUICore --preview +``` + +### Internal documentation for contributors + +```bash +./Scripts/build-documentation.sh \ + --minimum-access-level internal \ + --preview +``` + +### Testing deployment without pushing + +```bash +./Scripts/update-gh-pages-documentation.sh \ + --hosting-base-path /OpenSwiftUI \ + --echo-without-push +``` + +This creates the gh-pages branch locally and shows what would be pushed without actually pushing to the remote. + +## Technical Details + +### Symbol Graph Filtering + +The build process filters symbol graphs to remove re-exported system framework symbols: + +1. Swift compiler generates symbol graphs with `-emit-symbol-graph` +2. Symbol graphs include all accessible symbols, including re-exports from CoreFoundation, CoreGraphics, etc. +3. Python script parses Swift mangled identifiers (format: `s:MODULE_LEN+MODULE_NAME...`) to extract module names +4. Only symbols from `OpenSwiftUI` and `OpenSwiftUICore` modules are kept +5. Typical reduction: ~10,000 → ~4,300 symbols + +### Git Worktree Approach + +The deployment script uses git worktree to manage the gh-pages branch: + +- Main working tree: feature branch with source code +- Worktree at `gh-pages/`: checked out to gh-pages branch +- This allows deploying documentation without switching branches in the main working tree +- Worktree is automatically cleaned up after deployment + +### Force Push Strategy + +By default, deployments use `git push --force` to prevent repository size growth: + +- Documentation includes large binary files (CSS, JS, images, data) +- Preserving history would accumulate these files with each deployment +- Force push keeps only the latest version in git history +- Trade-off: No documentation version history in git (use git tags on main branch for versioning instead) + +To preserve history: `./Scripts/update-gh-pages-documentation.sh --no-force --hosting-base-path /OpenSwiftUI` + +## Future Improvements + +The following improvements are planned for the documentation system: + +- [ ] **Remove default implementation** - Currently, the documentation includes default implementations from protocol extensions. These can clutter the documentation and make it harder to find the primary API declarations. Future work will add filtering to hide default implementations while keeping protocol requirements visible. + +- [ ] **Migrate to GitHub Actions** - The current deployment process uses a local `gh-pages` branch and manual script execution. Migrating to GitHub Actions would provide: + - Automatic documentation updates on every push to main + - Consistent build environment (no local machine dependencies) + - Build artifacts and logs available in GitHub UI + - Easier collaboration (no need to run deployment scripts locally) + - Integration with PR previews (optional) + + This would replace the manual `./Scripts/update-gh-pages-documentation.sh` workflow with an automated CI/CD pipeline. diff --git a/README.md b/README.md index 46b78dca7..7d74628ba 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,9 @@ And the API design and documentation is to stay as compatible with the original Currently, this project is in early development. -You can find the API [documentation](https://swiftpackageindex.com/OpenSwiftUIProject/OpenSwiftUI/main/documentation/openswiftui) here. +The full API [documentation](https://openswiftuiproject.github.io/OpenSwiftUI/documentation/openswiftui/) is hosted on GitHub Pages. + +A legacy version is available on [SwiftPackageIndex](https://swiftpackageindex.com/OpenSwiftUIProject/OpenSwiftUI/main/documentation/openswiftui), but it does not include OpenSwiftUICore APIs. ## Notes diff --git a/Scripts/build-documentation.sh b/Scripts/build-documentation.sh new file mode 100755 index 000000000..1a9021231 --- /dev/null +++ b/Scripts/build-documentation.sh @@ -0,0 +1,448 @@ +#!/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, internal, private) + 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 + +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 +} + +# 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|internal|private|fileprivate) + ;; + *) + log_error "Invalid minimum access level: $MINIMUM_ACCESS_LEVEL" + log_error "Valid values: public, 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" ]]; 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 + +# 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" + +# 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 + # We have a .docc catalog + DOCC_ARGS=( + "$DOCC_CATALOG" + --output-path "$DOCC_OUTPUT_DIR" + --emit-digest + --transform-for-static-hosting + ) + + if [[ -n "$HOSTING_BASE_PATH" ]]; then + DOCC_ARGS+=(--hosting-base-path "$HOSTING_BASE_PATH") + fi + + # Add source service configuration + 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 + + # Add symbol graph directory if we have symbol graphs + if [[ -d "$SYMBOL_GRAPH_DIR" ]] && [[ -n "$(ls -A "$SYMBOL_GRAPH_DIR")" ]]; then + DOCC_ARGS+=(--additional-symbol-graph-dir "$SYMBOL_GRAPH_DIR") + fi + + rundocc convert "${DOCC_ARGS[@]}" +else + # No .docc catalog, create one from symbol graphs + TEMP_DOCC_CATALOG="$BUILD_DIR/${TARGET_NAME}.docc" + mkdir -p "$TEMP_DOCC_CATALOG" + + # Copy symbol graphs into the 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" + --output-path "$DOCC_OUTPUT_DIR" + --emit-digest + --transform-for-static-hosting + ) + + if [[ -n "$HOSTING_BASE_PATH" ]]; then + DOCC_ARGS+=(--hosting-base-path "$HOSTING_BASE_PATH") + fi + + # Add source service configuration + 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 + + rundocc convert "${DOCC_ARGS[@]}" +fi + +log_info "Documentation built successfully" +log_info "Documentation output: $DOCC_OUTPUT_DIR" + +# Step 4: Preview if requested +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 + + # Detect the actual module name from the generated documentation + ACTUAL_MODULE="" + if [[ -d "$DOCC_OUTPUT_DIR/data/documentation" ]]; then + # Find the first .json file in data/documentation directory + ACTUAL_MODULE=$(ls "$DOCC_OUTPUT_DIR/data/documentation"/*.json 2>/dev/null | head -1 | xargs basename -s .json) + fi + + log_info "Starting preview server on port $PREVIEW_PORT..." + if [[ -n "$ACTUAL_MODULE" ]]; then + log_info "Documentation will be available at: http://localhost:$PREVIEW_PORT/documentation/$ACTUAL_MODULE" + else + log_info "Documentation will be available at: http://localhost:$PREVIEW_PORT/" + fi + log_info "Press Ctrl+C to stop the server" + + cd "$DOCC_OUTPUT_DIR" + python3 -m http.server $PREVIEW_PORT +fi + +log_info "Done!" diff --git a/Scripts/update-gh-pages-documentation.sh b/Scripts/update-gh-pages-documentation.sh new file mode 100755 index 000000000..a76906c46 --- /dev/null +++ b/Scripts/update-gh-pages-documentation.sh @@ -0,0 +1,281 @@ +#!/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" +DOCC_OUTPUT_DIR="$BUILD_DIR/docc-output" + +# Default configuration +BUILD_DOCS=true +MINIMUM_ACCESS_LEVEL="public" +TARGET_NAME="OpenSwiftUI" +HOSTING_BASE_PATH="" +CLEAN_BUILD=false +SOURCE_SERVICE="github" +SOURCE_SERVICE_BASE_URL="https://github.com/OpenSwiftUIProject/OpenSwiftUI/blob/main" +FORCE_PUSH=true # Force push to save git repo size (avoids accumulating large binary files) +ECHO_WITHOUT_PUSH=false # Echo push command without actually pushing + +# 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] + +Publish Swift documentation to GitHub Pages using DocC. + +This script will build documentation (or use existing build) and deploy it +to the gh-pages branch for GitHub Pages hosting. + +OPTIONS: + --no-build Skip building documentation (use existing output) + --minimum-access-level LEVEL Set minimum access level (public, internal, private) + Default: public + --target TARGET Target to document (default: OpenSwiftUI) + --hosting-base-path PATH Base path for hosting (e.g., /OpenSwiftUI) + --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) + --no-force Don't force push (preserves gh-pages history) + Default: force push to save repo size + --clean Clean build artifacts and force rebuild + --echo-without-push Echo push command without actually pushing (for testing) + -h, --help Show this help message + +EXAMPLES: + # Build and publish to GitHub Pages (source links enabled by default) + $(basename "$0") + + # Publish using existing documentation build + $(basename "$0") --no-build + + # Build with internal symbols and publish + $(basename "$0") --minimum-access-level internal + + # Document a specific target + $(basename "$0") --target OpenSwiftUICore + + # Build and publish with custom source service (e.g., for a fork) + $(basename "$0") \\ + --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" +} + +git_push() { + if [[ "$ECHO_WITHOUT_PUSH" == true ]]; then + echo "[echo without push]: git push $*" + else + git push "$@" + fi +} + +# Parse command line arguments +while [[ $# -gt 0 ]]; do + case $1 in + --no-build) + BUILD_DOCS=false + 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 + ;; + --source-service) + SOURCE_SERVICE="$2" + shift 2 + ;; + --source-service-base-url) + SOURCE_SERVICE_BASE_URL="$2" + shift 2 + ;; + --no-force) + FORCE_PUSH=false + shift + ;; + --clean) + CLEAN_BUILD=true + shift + ;; + --echo-without-push) + ECHO_WITHOUT_PUSH=true + shift + ;; + -h|--help) + usage + ;; + *) + log_error "Unknown option: $1" + usage + ;; + esac +done + +log_info "Configuration:" +log_info " Target: $TARGET_NAME" +log_info " Build Documentation: $BUILD_DOCS" +log_info " Force Push: $FORCE_PUSH" +if [[ -n "$HOSTING_BASE_PATH" ]]; then + log_info " Hosting Base Path: $HOSTING_BASE_PATH" +fi + +# Check for required tools +command -v git >/dev/null 2>&1 || { + log_error "git is required but not installed. Aborting." + exit 1 +} + +# Build documentation if requested +if [[ "$BUILD_DOCS" == true ]]; then + log_info "Building documentation..." + + BUILD_SCRIPT="$SCRIPT_DIR/build-documentation.sh" + if [[ ! -f "$BUILD_SCRIPT" ]]; then + log_error "Build script not found: $BUILD_SCRIPT" + exit 1 + fi + + BUILD_ARGS=() + BUILD_ARGS+=(--target "$TARGET_NAME") + BUILD_ARGS+=(--minimum-access-level "$MINIMUM_ACCESS_LEVEL") + + if [[ -n "$HOSTING_BASE_PATH" ]]; then + BUILD_ARGS+=(--hosting-base-path "$HOSTING_BASE_PATH") + fi + + if [[ -n "$SOURCE_SERVICE" ]]; then + BUILD_ARGS+=(--source-service "$SOURCE_SERVICE") + BUILD_ARGS+=(--source-service-base-url "$SOURCE_SERVICE_BASE_URL") + fi + + if [[ "$CLEAN_BUILD" == true ]]; then + BUILD_ARGS+=(--clean) + fi + + "$BUILD_SCRIPT" "${BUILD_ARGS[@]}" +fi + +# Verify documentation output exists +if [[ ! -d "$DOCC_OUTPUT_DIR" ]] || [[ -z "$(ls -A "$DOCC_OUTPUT_DIR" 2>/dev/null)" ]]; then + log_error "Documentation output not found at: $DOCC_OUTPUT_DIR" + log_error "Please build documentation first or run without --no-build" + exit 1 +fi + +log_info "Using documentation from: $DOCC_OUTPUT_DIR" + +# Deploy to GitHub Pages +GH_PAGES_DIR="$REPO_ROOT/gh-pages" + +log_info "Preparing GitHub Pages deployment..." + +# Check if we're in a git repository +if ! git rev-parse --git-dir > /dev/null 2>&1; then + log_error "Not in a git repository. Cannot deploy to GitHub Pages." + exit 1 +fi + +# Store current branch +CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD) + +# Check if gh-pages branch exists +if git show-ref --verify --quiet refs/heads/gh-pages; then + log_info "Using existing gh-pages branch" + git fetch origin gh-pages 2>/dev/null || true + git worktree add "$GH_PAGES_DIR" gh-pages || { + log_warning "Worktree already exists, removing and recreating..." + git worktree remove "$GH_PAGES_DIR" --force + git worktree add "$GH_PAGES_DIR" gh-pages + } +else + log_info "Creating new gh-pages branch" + # Create an empty orphan branch first + git checkout --orphan gh-pages + git rm -rf . 2>/dev/null || true + git commit --allow-empty -m "Initialize gh-pages branch" + git checkout "$CURRENT_BRANCH" + + # Now create worktree from the new branch + git worktree add "$GH_PAGES_DIR" gh-pages +fi + +# Copy documentation to gh-pages worktree docs folder +log_info "Copying documentation files..." +mkdir -p "$GH_PAGES_DIR/docs" +rsync -av --delete "$DOCC_OUTPUT_DIR/" "$GH_PAGES_DIR/docs/" + +# Add .nojekyll to prevent GitHub Pages from processing with Jekyll +touch "$GH_PAGES_DIR/.nojekyll" + +# Commit and push +cd "$GH_PAGES_DIR" +git add -Af . + +if git diff --cached --quiet; then + log_info "No changes to documentation" +else + log_info "Committing documentation changes..." + git commit -m "Update documentation (generated from $CURRENT_BRANCH@$(git -C "$REPO_ROOT" rev-parse --short HEAD))" + + log_info "Pushing to gh-pages branch..." + if [[ "$FORCE_PUSH" == true ]]; then + log_info "Using --force to save git repo size (avoids accumulating large binary files)" + git_push --force origin gh-pages + else + git_push origin gh-pages + fi + + log_info "${GREEN}✓${NC} Documentation published successfully!" + log_info "GitHub Pages will be updated shortly at your repository's GitHub Pages URL" +fi + +# Cleanup +cd "$REPO_ROOT" +git worktree remove "$GH_PAGES_DIR" + +log_info "Cleaning up build artifacts..." +rm -rf "$DOCS_DIR" + +log_info "Done!"