build-documentation.sh 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505
  1. #!/bin/bash
  2. #
  3. ##===----------------------------------------------------------------------===##
  4. ##
  5. ## This source file is part of the OpenSwiftUI open source project
  6. ##
  7. ## Copyright (c) 2025 the OpenSwiftUI project authors
  8. ## Licensed under Apache License v2.0
  9. ##
  10. ## See LICENSE.txt for license information
  11. ##
  12. ## SPDX-License-Identifier: Apache-2.0
  13. ##
  14. ##===----------------------------------------------------------------------===##
  15. set -euo pipefail
  16. SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  17. REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
  18. DOCS_DIR="$REPO_ROOT/.docs"
  19. BUILD_DIR="$DOCS_DIR/build"
  20. SYMBOL_GRAPH_DIR="$BUILD_DIR/symbol-graphs"
  21. DOCC_OUTPUT_DIR="$BUILD_DIR/docc-output"
  22. # Default configuration
  23. PREVIEW_MODE=false
  24. MINIMUM_ACCESS_LEVEL="public"
  25. TARGET_NAME="OpenSwiftUI"
  26. HOSTING_BASE_PATH=""
  27. CLEAN_BUILD=false
  28. PREVIEW_PORT=8000
  29. SOURCE_SERVICE="github"
  30. SOURCE_SERVICE_BASE_URL="https://github.com/OpenSwiftUIProject/OpenSwiftUI/blob/main"
  31. # Colors for output
  32. RED='\033[0;31m'
  33. GREEN='\033[0;32m'
  34. YELLOW='\033[1;33m'
  35. NC='\033[0m' # No Color
  36. usage() {
  37. cat << EOF
  38. Usage: $(basename "$0") [OPTIONS]
  39. Build Swift documentation using DocC with optional local preview.
  40. OPTIONS:
  41. --preview Preview documentation locally with HTTP server
  42. --minimum-access-level LEVEL Set minimum access level (public, package, internal, private, fileprivate)
  43. Default: public
  44. --target TARGET Target to document (default: OpenSwiftUI)
  45. --hosting-base-path PATH Base path for hosting (e.g., /OpenSwiftUI)
  46. --port PORT Port for preview server (default: 8000)
  47. --source-service SERVICE Source service (github, gitlab, bitbucket)
  48. --source-service-base-url URL Base URL for source service
  49. (e.g., https://github.com/user/repo/blob/main)
  50. --clean Clean build artifacts and force rebuild
  51. -h, --help Show this help message
  52. ENVIRONMENT:
  53. DOCC_HTML_DIR Use an existing swift-docc-render dist directory.
  54. When unset, DocC uses its bundled renderer.
  55. EXAMPLES:
  56. # Build and preview documentation (source links enabled by default)
  57. $(basename "$0") --preview
  58. # Preview with internal symbols on port 8080
  59. $(basename "$0") --preview --minimum-access-level internal --port 8080
  60. # Clean rebuild
  61. $(basename "$0") --preview --clean
  62. # Build for specific target
  63. $(basename "$0") --target OpenSwiftUICore --preview
  64. # Build with custom source service (e.g., for a fork)
  65. $(basename "$0") --preview \\
  66. --source-service github \\
  67. --source-service-base-url https://github.com/yourname/OpenSwiftUI/blob/custom-branch
  68. EOF
  69. exit 0
  70. }
  71. log_info() {
  72. echo -e "${GREEN}[INFO]${NC} $1"
  73. }
  74. log_warning() {
  75. echo -e "${YELLOW}[WARNING]${NC} $1"
  76. }
  77. log_error() {
  78. echo -e "${RED}[ERROR]${NC} $1"
  79. }
  80. # Function to run docc command
  81. rundocc() {
  82. if command -v xcrun >/dev/null 2>&1; then
  83. xcrun docc "$@"
  84. else
  85. docc "$@"
  86. fi
  87. }
  88. configure_docc_renderer() {
  89. if [[ -n "${DOCC_HTML_DIR:-}" ]]; then
  90. [[ -d "$DOCC_HTML_DIR" ]] || {
  91. log_error "DOCC_HTML_DIR does not exist: $DOCC_HTML_DIR"
  92. exit 1
  93. }
  94. export DOCC_HTML_DIR
  95. log_info "Using DocC renderer: $DOCC_HTML_DIR"
  96. return
  97. fi
  98. log_info "DocC Renderer: bundled default"
  99. }
  100. # Parse command line arguments
  101. while [[ $# -gt 0 ]]; do
  102. case $1 in
  103. --preview)
  104. PREVIEW_MODE=true
  105. shift
  106. ;;
  107. --minimum-access-level)
  108. MINIMUM_ACCESS_LEVEL="$2"
  109. shift 2
  110. ;;
  111. --target)
  112. TARGET_NAME="$2"
  113. shift 2
  114. ;;
  115. --hosting-base-path)
  116. HOSTING_BASE_PATH="$2"
  117. shift 2
  118. ;;
  119. --port)
  120. PREVIEW_PORT="$2"
  121. shift 2
  122. ;;
  123. --source-service)
  124. SOURCE_SERVICE="$2"
  125. shift 2
  126. ;;
  127. --source-service-base-url)
  128. SOURCE_SERVICE_BASE_URL="$2"
  129. shift 2
  130. ;;
  131. --clean)
  132. CLEAN_BUILD=true
  133. shift
  134. ;;
  135. -h|--help)
  136. usage
  137. ;;
  138. *)
  139. log_error "Unknown option: $1"
  140. usage
  141. ;;
  142. esac
  143. done
  144. # Validate minimum access level
  145. case "$MINIMUM_ACCESS_LEVEL" in
  146. public|package|internal|private|fileprivate)
  147. ;;
  148. *)
  149. log_error "Invalid minimum access level: $MINIMUM_ACCESS_LEVEL"
  150. log_error "Valid values: public, package, internal, private, fileprivate"
  151. exit 1
  152. ;;
  153. esac
  154. log_info "Configuration:"
  155. log_info " Target: $TARGET_NAME"
  156. log_info " Minimum Access Level: $MINIMUM_ACCESS_LEVEL"
  157. log_info " Preview Mode: $PREVIEW_MODE"
  158. if [[ "$PREVIEW_MODE" == true ]]; then
  159. log_info " Preview Port: $PREVIEW_PORT"
  160. fi
  161. if [[ -n "$HOSTING_BASE_PATH" && "$PREVIEW_MODE" == true ]]; then
  162. log_warning "--hosting-base-path is ignored in preview mode; it is intended for static hosting."
  163. elif [[ -n "$HOSTING_BASE_PATH" ]]; then
  164. log_info " Hosting Base Path: $HOSTING_BASE_PATH"
  165. fi
  166. if [[ -n "$SOURCE_SERVICE" ]]; then
  167. log_info " Source Service: $SOURCE_SERVICE"
  168. log_info " Source Service Base URL: $SOURCE_SERVICE_BASE_URL"
  169. fi
  170. if [[ -n "${DOCC_HTML_DIR:-}" ]]; then
  171. log_info " DocC Renderer: $DOCC_HTML_DIR"
  172. else
  173. log_info " DocC Renderer: bundled default"
  174. fi
  175. # Validate source service configuration
  176. if [[ -n "$SOURCE_SERVICE" ]] && [[ -z "$SOURCE_SERVICE_BASE_URL" ]]; then
  177. log_error "--source-service requires --source-service-base-url"
  178. exit 1
  179. fi
  180. if [[ -z "$SOURCE_SERVICE" ]] && [[ -n "$SOURCE_SERVICE_BASE_URL" ]]; then
  181. log_error "--source-service-base-url requires --source-service"
  182. exit 1
  183. fi
  184. # Check for required tools
  185. command -v swift >/dev/null 2>&1 || {
  186. log_error "swift is required but not installed. Aborting."
  187. exit 1
  188. }
  189. # Check for docc
  190. if ! command -v docc >/dev/null 2>&1 && ! command -v xcrun >/dev/null 2>&1; then
  191. log_error "docc is required but not found. Please install Swift-DocC."
  192. exit 1
  193. fi
  194. # Clean build if requested
  195. if [[ "$CLEAN_BUILD" == true ]]; then
  196. log_info "Cleaning build artifacts..."
  197. swift package clean
  198. rm -rf "$DOCS_DIR"
  199. fi
  200. # Create build directories
  201. log_info "Preparing build directories..."
  202. mkdir -p "$SYMBOL_GRAPH_DIR"
  203. mkdir -p "$DOCC_OUTPUT_DIR"
  204. configure_docc_renderer
  205. # Step 1: Generate symbol graphs
  206. cd "$REPO_ROOT"
  207. # Use default .build directory for symbol graphs (reuses existing build)
  208. SWIFT_BUILD_DIR=".build"
  209. DEFAULT_SYMBOL_GRAPH_DIR="$SWIFT_BUILD_DIR/symbol-graphs"
  210. # Check if symbol graphs already exist for the target
  211. REBUILD_NEEDED=false
  212. if [[ ! -f "$DEFAULT_SYMBOL_GRAPH_DIR/${TARGET_NAME}.symbols.json" ]]; then
  213. REBUILD_NEEDED=true
  214. log_info "No existing symbol graphs found for $TARGET_NAME, will perform clean build..."
  215. else
  216. log_info "Found existing symbol graphs for $TARGET_NAME, reusing them (use --clean to rebuild)"
  217. fi
  218. if [[ "$REBUILD_NEEDED" == true ]] || [[ "$CLEAN_BUILD" == true ]]; then
  219. # Clean build to ensure symbol graphs are generated
  220. if [[ "$REBUILD_NEEDED" == true ]]; then
  221. log_info "Cleaning build to ensure symbol graph generation..."
  222. swift package clean
  223. fi
  224. log_info "Generating symbol graphs..."
  225. swift build \
  226. --target "$TARGET_NAME" \
  227. -Xswiftc -emit-symbol-graph \
  228. -Xswiftc -emit-symbol-graph-dir \
  229. -Xswiftc "$DEFAULT_SYMBOL_GRAPH_DIR" \
  230. -Xswiftc -symbol-graph-minimum-access-level \
  231. -Xswiftc "$MINIMUM_ACCESS_LEVEL"
  232. if [[ ! -d "$DEFAULT_SYMBOL_GRAPH_DIR" ]] || [[ -z "$(ls -A "$DEFAULT_SYMBOL_GRAPH_DIR")" ]]; then
  233. log_error "Symbol graph generation failed or produced no output"
  234. exit 1
  235. fi
  236. fi
  237. # Filter symbol graphs for the target module
  238. # Only include the target itself (which already includes re-exported OpenSwiftUICore symbols)
  239. log_info "Filtering symbol graphs for $TARGET_NAME..."
  240. if ls "$DEFAULT_SYMBOL_GRAPH_DIR/${TARGET_NAME}.symbols.json" >/dev/null 2>&1; then
  241. # Copy only the main target symbol graphs
  242. # OpenSwiftUI already includes OpenSwiftUICore symbols via @_exported import
  243. # Use explicit patterns to avoid matching OpenSwiftUICore*.symbols.json
  244. cp "$DEFAULT_SYMBOL_GRAPH_DIR/${TARGET_NAME}.symbols.json" "$SYMBOL_GRAPH_DIR/" 2>/dev/null || true
  245. cp "$DEFAULT_SYMBOL_GRAPH_DIR/${TARGET_NAME}@"*.symbols.json "$SYMBOL_GRAPH_DIR/" 2>/dev/null || true
  246. log_info "Symbol graphs for $TARGET_NAME copied successfully"
  247. # Filter out symbols from unwanted modules (CoreFoundation, CoreGraphics, etc.)
  248. log_info "Removing re-exported system framework symbols..."
  249. python3 << EOF
  250. import json
  251. import sys
  252. import os
  253. def filter_symbol_graph(file_path, allowed_modules):
  254. """Filter symbol graph to only include symbols from allowed modules."""
  255. try:
  256. with open(file_path, 'r') as f:
  257. data = json.load(f)
  258. if 'symbols' in data and isinstance(data['symbols'], list):
  259. original_count = len(data['symbols'])
  260. # Filter symbols by extracting module from precise identifier
  261. filtered_symbols = []
  262. for symbol in data['symbols']:
  263. precise = symbol.get('identifier', {}).get('precise', '')
  264. module = None
  265. # Extract module from mangled Swift names
  266. if precise.startswith('s:'):
  267. rest = precise[2:]
  268. if rest and rest[0].isdigit():
  269. i = 0
  270. while i < len(rest) and rest[i].isdigit():
  271. i += 1
  272. if i > 0:
  273. mod_len = int(rest[:i])
  274. module = rest[i:i+mod_len]
  275. # Keep if it's from an allowed module
  276. if module and module in allowed_modules:
  277. filtered_symbols.append(symbol)
  278. data['symbols'] = filtered_symbols
  279. filtered_count = len(filtered_symbols)
  280. # Write back
  281. with open(file_path, 'w') as f:
  282. json.dump(data, f)
  283. print(f" {os.path.basename(file_path)}: {original_count} -> {filtered_count} symbols", file=sys.stderr)
  284. return True
  285. return False
  286. except Exception as e:
  287. print(f" Error filtering {file_path}: {e}", file=sys.stderr)
  288. return False
  289. # List of allowed modules (OpenSwiftUI and OpenSwiftUICore only)
  290. allowed_modules = {'OpenSwiftUI', 'OpenSwiftUICore', '$TARGET_NAME'}
  291. # Filter the main OpenSwiftUI symbol graph
  292. symbol_file = '$SYMBOL_GRAPH_DIR/OpenSwiftUI.symbols.json'
  293. if os.path.exists(symbol_file):
  294. filter_symbol_graph(symbol_file, allowed_modules)
  295. EOF
  296. else
  297. log_error "No symbol graphs found for $TARGET_NAME"
  298. log_error "Available symbol graphs:"
  299. ls "$DEFAULT_SYMBOL_GRAPH_DIR"/*.symbols.json 2>/dev/null || echo " (none)"
  300. exit 1
  301. fi
  302. # Step 2: Find or create documentation catalog
  303. DOCC_CATALOG=""
  304. if [[ -d "Sources/$TARGET_NAME/${TARGET_NAME}.docc" ]]; then
  305. DOCC_CATALOG="Sources/$TARGET_NAME/${TARGET_NAME}.docc"
  306. log_info "Using documentation catalog: $DOCC_CATALOG"
  307. else
  308. log_warning "No .docc catalog found for $TARGET_NAME"
  309. log_info "DocC will generate documentation from symbol graphs only"
  310. fi
  311. # Step 3: Build documentation
  312. log_info "Building documentation archive..."
  313. if [[ -n "$DOCC_CATALOG" ]]; then
  314. DOCC_ARGS=(
  315. "$DOCC_CATALOG"
  316. --emit-digest
  317. --transform-for-static-hosting
  318. --output-path "$DOCC_OUTPUT_DIR"
  319. )
  320. if [[ "$PREVIEW_MODE" == true ]]; then
  321. DOCC_ARGS+=(--port "$PREVIEW_PORT")
  322. fi
  323. if [[ -n "$HOSTING_BASE_PATH" && "$PREVIEW_MODE" == false ]]; then
  324. DOCC_ARGS+=(--hosting-base-path "$HOSTING_BASE_PATH")
  325. fi
  326. if [[ -n "$SOURCE_SERVICE" ]]; then
  327. DOCC_ARGS+=(--source-service "$SOURCE_SERVICE")
  328. DOCC_ARGS+=(--source-service-base-url "$SOURCE_SERVICE_BASE_URL")
  329. DOCC_ARGS+=(--checkout-path "$REPO_ROOT")
  330. fi
  331. if [[ -d "$SYMBOL_GRAPH_DIR" ]] && [[ -n "$(ls -A "$SYMBOL_GRAPH_DIR")" ]]; then
  332. DOCC_ARGS+=(--additional-symbol-graph-dir "$SYMBOL_GRAPH_DIR")
  333. fi
  334. # Enabled experimental features:
  335. DOCC_ARGS+=(--enable-experimental-overloaded-symbol-presentation)
  336. if [[ "$PREVIEW_MODE" == true ]]; then
  337. # Check if port is already in use
  338. if lsof -Pi :"$PREVIEW_PORT" -sTCP:LISTEN -t >/dev/null 2>&1; then
  339. log_warning "Port $PREVIEW_PORT is already in use"
  340. # Find the process using the port
  341. PORT_PID=$(lsof -Pi :"$PREVIEW_PORT" -sTCP:LISTEN -t)
  342. PORT_PROCESS=$(ps -p "$PORT_PID" -o command= 2>/dev/null || echo "Unknown process")
  343. log_info "Process using port $PREVIEW_PORT (PID $PORT_PID): $PORT_PROCESS"
  344. # Ask user if they want to kill it
  345. read -p "Do you want to kill this process and start the preview server? (y/N): " -n 1 -r
  346. echo
  347. if [[ $REPLY =~ ^[Yy]$ ]]; then
  348. log_info "Killing process $PORT_PID..."
  349. kill "$PORT_PID"
  350. sleep 1
  351. # Verify it's killed
  352. if lsof -Pi :"$PREVIEW_PORT" -sTCP:LISTEN -t >/dev/null 2>&1; then
  353. log_error "Failed to kill process on port $PREVIEW_PORT"
  354. exit 1
  355. fi
  356. log_info "Process killed successfully"
  357. else
  358. log_error "Cannot start preview server. Please free port $PREVIEW_PORT or use --port option"
  359. exit 1
  360. fi
  361. fi
  362. log_info "Starting documentation preview server on port $PREVIEW_PORT..."
  363. log_info "Press Ctrl+C to stop the server"
  364. rundocc preview "${DOCC_ARGS[@]}"
  365. else
  366. rundocc convert "${DOCC_ARGS[@]}"
  367. fi
  368. else
  369. TEMP_DOCC_CATALOG="$BUILD_DIR/${TARGET_NAME}.docc"
  370. mkdir -p "$TEMP_DOCC_CATALOG"
  371. if [[ -d "$SYMBOL_GRAPH_DIR" ]] && [[ -n "$(ls -A "$SYMBOL_GRAPH_DIR")" ]]; then
  372. cp "$SYMBOL_GRAPH_DIR"/*.symbols.json "$TEMP_DOCC_CATALOG/"
  373. fi
  374. DOCC_ARGS=(
  375. "$TEMP_DOCC_CATALOG"
  376. --emit-digest
  377. --transform-for-static-hosting
  378. --output-path "$DOCC_OUTPUT_DIR"
  379. )
  380. if [[ "$PREVIEW_MODE" == true ]]; then
  381. DOCC_ARGS+=(--port "$PREVIEW_PORT")
  382. fi
  383. if [[ -n "$HOSTING_BASE_PATH" && "$PREVIEW_MODE" == false ]]; then
  384. DOCC_ARGS+=(--hosting-base-path "$HOSTING_BASE_PATH")
  385. fi
  386. if [[ -n "$SOURCE_SERVICE" ]]; then
  387. DOCC_ARGS+=(--source-service "$SOURCE_SERVICE")
  388. DOCC_ARGS+=(--source-service-base-url "$SOURCE_SERVICE_BASE_URL")
  389. DOCC_ARGS+=(--checkout-path "$REPO_ROOT")
  390. fi
  391. if [[ "$PREVIEW_MODE" == true ]]; then
  392. # Check if port is already in use
  393. if lsof -Pi :"$PREVIEW_PORT" -sTCP:LISTEN -t >/dev/null 2>&1; then
  394. log_warning "Port $PREVIEW_PORT is already in use"
  395. # Find the process using the port
  396. PORT_PID=$(lsof -Pi :"$PREVIEW_PORT" -sTCP:LISTEN -t)
  397. PORT_PROCESS=$(ps -p "$PORT_PID" -o command= 2>/dev/null || echo "Unknown process")
  398. log_info "Process using port $PREVIEW_PORT (PID $PORT_PID): $PORT_PROCESS"
  399. # Ask user if they want to kill it
  400. read -p "Do you want to kill this process and start the preview server? (y/N): " -n 1 -r
  401. echo
  402. if [[ $REPLY =~ ^[Yy]$ ]]; then
  403. log_info "Killing process $PORT_PID..."
  404. kill "$PORT_PID"
  405. sleep 1
  406. # Verify it's killed
  407. if lsof -Pi :"$PREVIEW_PORT" -sTCP:LISTEN -t >/dev/null 2>&1; then
  408. log_error "Failed to kill process on port $PREVIEW_PORT"
  409. exit 1
  410. fi
  411. log_info "Process killed successfully"
  412. else
  413. log_error "Cannot start preview server. Please free port $PREVIEW_PORT or use --port option"
  414. exit 1
  415. fi
  416. fi
  417. log_info "Starting documentation preview server on port $PREVIEW_PORT..."
  418. log_info "Press Ctrl+C to stop the server"
  419. rundocc preview "${DOCC_ARGS[@]}"
  420. else
  421. rundocc convert "${DOCC_ARGS[@]}"
  422. fi
  423. fi
  424. if [[ "$PREVIEW_MODE" == false ]]; then
  425. log_info "Documentation built successfully"
  426. log_info "Documentation output: $DOCC_OUTPUT_DIR"
  427. fi
  428. log_info "Done!"