1
0

build-documentation.sh 16 KB

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