blob: faaafffe63380d477e0ecfd3548fbf3f13c965a7 [file]
#!/bin/bash
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
#
# Orchestration script for the TinkerPop documentation build.
#
# Full build (--fullRun, default):
# Validates console/server distributions, installs plugins, starts
# Gremlin Server and Gephi mock, then invokes Maven with the
# AsciidoctorJ extension to render docs with live code execution.
#
# Dry-run (--dryRun):
# Invokes Maven with -Dgremlin-docs-dryrun=true. No server, console,
# or plugins required.
set -e
cd "$(dirname "$0")/.."
TP_HOME="$(pwd)"
# The docs build renders every book to both HTML and Markdown in a single Maven JVM (AsciidoctorJ
# runs in-process), so the heap must hold the parsed AST + JRuby runtime for ~two dozen executions.
# Raise the heap unless the caller already set one, to avoid OutOfMemoryError during rendering.
if [[ "${MAVEN_OPTS:-}" != *-Xmx* ]]; then
export MAVEN_OPTS="${MAVEN_OPTS:-} -Xmx8g"
fi
# ---------------------------------------------------------------------------
# Argument parsing
# ---------------------------------------------------------------------------
MODE="full"
NOCLEAN=""
while [[ $# -gt 0 ]]; do
case "$1" in
--fullRun|-f) MODE="full"; shift ;;
--dryRun|-d) MODE="dry"; shift ;;
--noClean|-n) NOCLEAN="1"; shift ;;
*) echo "Unknown option: $1"; exit 1 ;;
esac
done
# ---------------------------------------------------------------------------
# Cleanup trap
# ---------------------------------------------------------------------------
GREMLIN_SERVER_PID=""
GEPHI_MOCK_PID=""
# The console subprocesses spawned by the docs extension drive JLine, which puts the shared
# controlling terminal into raw mode (echo off) even though its own stdin/stdout are pipes. A
# console that is killed rather than shut down cleanly never restores those settings, leaving
# the invoking shell unable to echo typed characters. Snapshot the settings here and restore
# them on exit.
STTY_SAVED=""
if [ -t 0 ]; then
STTY_SAVED=$(stty -g 2>/dev/null || true)
fi
cleanup() {
set +e
[ -n "${GREMLIN_SERVER_PID}" ] && kill "${GREMLIN_SERVER_PID}" 2>/dev/null
[ -n "${GEPHI_MOCK_PID}" ] && kill "${GEPHI_MOCK_PID}" 2>/dev/null
[ -n "${STTY_SAVED}" ] && stty "${STTY_SAVED}" 2>/dev/null
}
trap cleanup EXIT
trap 'cleanup; exit 130' INT
trap 'cleanup; exit 143' TERM
# ---------------------------------------------------------------------------
# Port readiness probe
# ---------------------------------------------------------------------------
# Returns 0 if a TCP port on the given host is accepting connections. Prefer nc when it is on the
# PATH, but fall back to bash's /dev/tcp so the build still works on hosts without netcat (some
# minimal container images and sandboxes ship no nc). Both branches are silent.
port_open() {
local host="$1" port="$2"
if command -v nc >/dev/null 2>&1; then
nc -z "${host}" "${port}" 2>/dev/null
else
(exec 3<>"/dev/tcp/${host}/${port}") 2>/dev/null && exec 3>&- 2>/dev/null
fi
}
# Resolve version from pom.xml. Needed by both the dry-run and full-build paths (the Markdown split
# stamps it into each page's llms.txt pointer), so it is resolved before either runs.
TP_VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout 2>/dev/null || \
grep -A1 '<artifactId>tinkerpop</artifactId>' pom.xml | grep -o 'version>[^<]*' | grep -o '>.*' | cut -d '>' -f2 | head -n1)
# ---------------------------------------------------------------------------
# Markdown split (agentdocsspec.com)
# ---------------------------------------------------------------------------
# After the Markdown mirror is rendered under target/docs/markdown/, split each rendered book into
# pages driven by llms-summary markers and rewrite intra-book links. Runs MarkdownSplitter from the
# tinkeradoc-extension classes (built by the asciidoc profile). The split is summary-driven: a
# section is a page iff it has an llms-summary. --strict fails the build on any page that exceeds
# the size budget and is not marked allow-oversize, so uncurated/oversized content is surfaced.
split_markdown() {
local md_root="target/docs/markdown"
[ -d "${md_root}" ] || return 0
local ext_classes="docs/tinkeradoc-extension/target/classes"
if [ ! -d "${ext_classes}" ]; then
echo "WARNING: ${ext_classes} not found; skipping Markdown split."
return 0
fi
# The splitter is pure Java, so the extension classes alone suffice on the classpath.
local books
books=$(find "${md_root}" -name index.md 2>/dev/null)
[ -z "${books}" ] && return 0
echo "Splitting Markdown books into agent-sized pages (summary-driven)..."
# shellcheck disable=SC2086
java -cp "${ext_classes}" org.apache.tinkerpop.tinkeradoc.MarkdownSplitter \
--strict --version "${TP_VERSION}" ${books}
# Generate the llms.txt discovery index over the split pages (agentdocsspec.com).
echo "Generating llms.txt discovery index..."
java -cp "${ext_classes}" org.apache.tinkerpop.tinkeradoc.LlmsTxtGenerator "${md_root}"
}
# ---------------------------------------------------------------------------
# Dry-run mode
# ---------------------------------------------------------------------------
if [ "${MODE}" == "dry" ]; then
if [ -z "${NOCLEAN}" ]; then
rm -rf target/postprocess-asciidoc target/doc-source target/docs 2>/dev/null || true
fi
echo "Copying docs sources to target/postprocess-asciidoc/..."
mkdir -p target/postprocess-asciidoc
cp -r docs/{static,stylesheets} target/postprocess-asciidoc/
cp -r docs/src/* target/postprocess-asciidoc/
mvn process-resources -pl . -Dasciidoc -Dgremlin.docs.dryrun=true
ec=$?
[ ${ec} -eq 0 ] && split_markdown
exit ${ec}
fi
# ---------------------------------------------------------------------------
# Full build mode
# ---------------------------------------------------------------------------
# 1. Validate console distribution
CONSOLE_DIR=$(ls -d gremlin-console/target/apache-tinkerpop-gremlin-console-*-standalone 2>/dev/null | head -n1)
if [ -z "${CONSOLE_DIR}" ] || [ ! -d "${CONSOLE_DIR}" ]; then
echo "ERROR: Gremlin Console distribution not found."
echo "Build it first: mvn clean install -pl :gremlin-console -am -DskipTests"
exit 1
fi
CONSOLE_HOME="$(cd "${CONSOLE_DIR}" && pwd)"
# 2. Validate server distribution
SERVER_DIR=$(ls -d gremlin-server/target/apache-tinkerpop-gremlin-server-*-standalone 2>/dev/null | head -n1)
if [ -z "${SERVER_DIR}" ] || [ ! -d "${SERVER_DIR}" ]; then
echo "ERROR: Gremlin Server distribution not found."
echo "Build it first: mvn clean install -pl :gremlin-server -am -DskipTests"
exit 1
fi
SERVER_HOME="$(cd "${SERVER_DIR}" && pwd)"
# Clear any plugin directories parked aside by a previous (possibly interrupted) run. The
# extension moves ext/<plugin> to ext-disabled/<plugin> when excluding a plugin per-book; a
# leftover ext-disabled/ from a crashed build would otherwise shadow the freshly-installed
# plugins and break the next run's console restarts.
rm -rf "${CONSOLE_HOME}/ext-disabled"
# 3. Install plugins into console
echo "Installing plugins into console..."
# Copy a plugin's dependency jars onto the console classpath via ext/<plugin>/plugin/ (which
# bin/gremlin.sh globs) rather than the shared lib/. This keeps each plugin's transitive deps
# isolatable so the docs extension can exclude conflicting plugins per-book (e.g. Neo4j's
# Scala 2.11 vs Spark's 2.12) by moving the plugin directory off the classpath. Jars already
# present in lib/ (core gremlin deps) and slf4j/logback-classic are skipped to avoid duplicate
# classpath entries and logger bindings -- mirroring the console's own :install (DependencyGrabber).
copy_deps_to_plugin() {
local src_dir="$1" plugin="$2"
local plugin_dir="${CONSOLE_HOME}/ext/${plugin}/plugin"
mkdir -p "${plugin_dir}"
local jar base
for jar in "${src_dir}"/*.jar; do
[ -e "${jar}" ] || continue
base=$(basename "${jar}")
case "${base}" in slf4j-*|logback-classic-*) continue ;; esac
[ -e "${CONSOLE_HOME}/lib/${base}" ] && continue
cp "${jar}" "${plugin_dir}/" 2>/dev/null
done
}
PLUGINS="hadoop-gremlin spark-gremlin neo4j-gremlin sparql-gremlin"
for plugin in ${PLUGINS}; do
PLUGIN_DIR="${plugin}/target/${plugin}-${TP_VERSION}-standalone"
if [ -d "${PLUGIN_DIR}" ]; then
echo " * installing ${plugin} (standalone)"
cp -r "${PLUGIN_DIR}" "${CONSOLE_HOME}/ext/${plugin}"
mkdir -p "${CONSOLE_HOME}/ext/${plugin}/plugin"
cp "${plugin}/target/${plugin}-${TP_VERSION}.jar" "${CONSOLE_HOME}/ext/${plugin}/plugin/" 2>/dev/null
copy_deps_to_plugin "${CONSOLE_HOME}/ext/${plugin}/lib" "${plugin}"
elif [ -f "${plugin}/target/${plugin}-${TP_VERSION}.jar" ]; then
echo " * installing ${plugin} (jar + dependencies)"
mkdir -p "${CONSOLE_HOME}/ext/${plugin}/lib"
mkdir -p "${CONSOLE_HOME}/ext/${plugin}/plugin"
cp "${plugin}/target/${plugin}-${TP_VERSION}.jar" "${CONSOLE_HOME}/ext/${plugin}/lib/"
cp "${plugin}/target/${plugin}-${TP_VERSION}.jar" "${CONSOLE_HOME}/ext/${plugin}/plugin/"
cp "${plugin}"/target/dependency/*.jar "${CONSOLE_HOME}/ext/${plugin}/lib/" 2>/dev/null || \
mvn dependency:copy-dependencies -pl "${plugin}" -DoutputDirectory="${CONSOLE_HOME}/ext/${plugin}/lib" -q
copy_deps_to_plugin "${CONSOLE_HOME}/ext/${plugin}/lib" "${plugin}"
else
echo " * WARNING: ${plugin} not found"
fi
done
# 3b. Resolve the Neo4j database implementation onto the console classpath.
# neo4j-gremlin declares neo4j-tinkerpop-api-impl as a Gremlin-Plugin-Dependencies manifest
# entry (test-scoped / gated behind the includeNeo4j profile), so it is not bundled by the
# local jar copy above. The old console ':install' flow fetched it via DependencyGrabber;
# here we resolve it (and its transitive deps) from Maven so Neo4jGraph blocks can execute.
NEO4J_IMPL_VERSION="0.9-3.4.0"
NEO4J_PLUGIN_LIB="${CONSOLE_HOME}/ext/neo4j-gremlin/lib"
if [ -d "${NEO4J_PLUGIN_LIB}" ] && ! ls "${NEO4J_PLUGIN_LIB}"/neo4j-tinkerpop-api-impl-*.jar >/dev/null 2>&1; then
echo " * resolving Neo4j implementation (neo4j-tinkerpop-api-impl:${NEO4J_IMPL_VERSION})"
NEO4J_POM="${TP_HOME}/target/neo4j-impl-pom.xml"
cat > "${NEO4J_POM}" <<POM
<project xmlns="http://maven.apache.org/POM/4.0.0"><modelVersion>4.0.0</modelVersion>
<groupId>org.apache.tinkerpop.docs</groupId><artifactId>neo4j-impl-resolver</artifactId><version>1</version>
<dependencies><dependency><groupId>org.neo4j</groupId><artifactId>neo4j-tinkerpop-api-impl</artifactId><version>${NEO4J_IMPL_VERSION}</version></dependency></dependencies></project>
POM
mvn -q -f "${NEO4J_POM}" dependency:copy-dependencies -DoutputDirectory="${NEO4J_PLUGIN_LIB}"
# Drop ONLY the conflicting io.netty 4.x jar that Neo4j pulls in (netty-all-4.1.24): it
# contains an older io.netty.handler.codec.http.websocketx.WebSocketClientHandshaker13 that
# shadows the console driver's 4.1.136 class and breaks ':remote' server connections with a
# NoSuchMethodError. Keep netty-3.9.x (org.jboss.netty package) -- it does NOT conflict and
# is required by Neo4j 3.4's IO layer.
rm -f "${NEO4J_PLUGIN_LIB}"/netty-all-4.*.jar
copy_deps_to_plugin "${NEO4J_PLUGIN_LIB}" "neo4j-gremlin"
fi
# 4. Register plugins in console
echo "Registering plugins..."
# Write plugins.txt deterministically rather than appending to whatever state a prior run left:
# the console rewrites this file to the set of successfully-activated plugins on shutdown, so a
# previous (possibly failed) run can leave it missing TinkerGraph/Credentials, which would fail
# the first doc block with "No such property: TinkerFactory". Lightweight built-in plugins are
# listed before the heavy graph plugins so activation order is stable.
cat > "${CONSOLE_HOME}/ext/plugins.txt" <<'EOF'
org.apache.tinkerpop.gremlin.console.jsr223.DriverGremlinPlugin
org.apache.tinkerpop.gremlin.console.jsr223.UtilitiesGremlinPlugin
org.apache.tinkerpop.gremlin.tinkergraph.jsr223.TinkerGraphGremlinPlugin
org.apache.tinkerpop.gremlin.groovy.jsr223.dsl.credential.CredentialGraphGremlinPlugin
org.apache.tinkerpop.gremlin.hadoop.jsr223.HadoopGremlinPlugin
org.apache.tinkerpop.gremlin.spark.jsr223.SparkGremlinPlugin
org.apache.tinkerpop.gremlin.neo4j.jsr223.Neo4jGremlinPlugin
org.apache.tinkerpop.gremlin.sparql.jsr223.SparqlGremlinPlugin
EOF
# 5. Copy hadoop config to console classpath
HADOOP_CONF_SRC="docs/tinkeradoc-extension/src/main/resources/hadoop-conf"
cp "${HADOOP_CONF_SRC}/core-site.xml" "${CONSOLE_HOME}/conf/"
cp "${HADOOP_CONF_SRC}/hadoop-docs.properties" "${CONSOLE_HOME}/conf/"
# 5. Start Gremlin Server
echo "Starting Gremlin Server..."
mkdir -p target
# Fail fast if port 8182 is already in use. The docs ':remote connect' blocks
# target localhost:8182, so any other service on that port (a stale Gremlin
# Server, or an unrelated process that happens to claim 8182) will cause our
# server to fail binding while the readiness check still passes -- the console
# then connects to the wrong service and WebSocket handshakes fail, dumping
# large stacktraces into the rendered docs.
if port_open 127.0.0.1 8182; then
echo "ERROR: Port 8182 is already in use by another process."
echo " Gremlin Server needs this port for the docs ':remote' examples."
echo " Identify the process with 'lsof -nP -iTCP:8182' and stop it,"
echo " then re-run the docs build."
exit 1
fi
pushd "${SERVER_HOME}" > /dev/null
bin/gremlin-server.sh conf/gremlin-server-modern.yaml > "${TP_HOME}/target/gremlin-server-docs.log" 2>&1 &
GREMLIN_SERVER_PID=$!
popd > /dev/null
# Wait for server to be ready (port 8182)
echo -n "Waiting for Gremlin Server on port 8182..."
for i in $(seq 1 30); do
# Detect early server failure (e.g. bind error) so we don't wait the full timeout
if ! kill -0 "${GREMLIN_SERVER_PID}" 2>/dev/null; then
echo " FAILED"
echo "ERROR: Gremlin Server process exited during startup. See target/gremlin-server-docs.log"
exit 1
fi
if port_open 127.0.0.1 8182; then
echo " ready."
break
fi
if [ $i -eq 30 ]; then
echo " TIMEOUT"
echo "ERROR: Gremlin Server failed to start within 30 seconds."
exit 1
fi
sleep 1
echo -n "."
done
# 6. Start Gephi mock (port 8080)
if ! port_open 127.0.0.1 8080; then
echo "Starting Gephi mock on port 8080..."
bin/gephi-mock.py > /dev/null 2>&1 &
GEPHI_MOCK_PID=$!
fi
# 7. Resolve HADOOP_GREMLIN_LIBS path
HADOOP_GREMLIN_LIBS="${CONSOLE_HOME}/ext/hadoop-gremlin/lib"
# 8. Copy source docs to staging area (replaces old preprocessor copy)
echo "Copying docs sources to target/postprocess-asciidoc/..."
mkdir -p target/postprocess-asciidoc
cp -r docs/{static,stylesheets} target/postprocess-asciidoc/
cp -r docs/src/* target/postprocess-asciidoc/
# 9. Invoke Maven with AsciidoctorJ extension attributes
echo "Running documentation build..."
if [ -z "${NOCLEAN}" ]; then
rm -r target/doc-source target/docs 2>/dev/null || true
fi
set +e
mvn process-resources -pl . -Dasciidoc \
-Dgremlin.docs.console.home="${CONSOLE_HOME}" \
-Dgremlin.docs.hadoop.libs="${HADOOP_GREMLIN_LIBS}"
ec=$?
set -e
if [ ${ec} -eq 0 ]; then
split_markdown
echo "Documentation build complete. Output: target/docs/htmlsingle/ and target/docs/markdown/"
else
echo "ERROR: Documentation build failed."
fi
exit ${ec}