| #!/usr/bin/env 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. |
| # fast-compile-fe.sh — Incrementally compile FE and update doris-fe.jar |
| # |
| # Usage: |
| # ./fast-compile.sh # Auto-detect changed main sources and compile |
| # ./fast-compile.sh Foo.java Bar.java # Compile specified main sources |
| # ./fast-compile.sh --test # Auto-detect changed test sources and compile |
| # ./fast-compile.sh --test FooTest.java # Compile specified test sources |
| # |
| # Dependencies: javac, jar (JDK 8+), mvn (needed for initial classpath generation) |
| |
| set -euo pipefail |
| |
| DORIS_HOME="$(cd "$(dirname "$0")/.." && pwd)" |
| FE_CORE="$DORIS_HOME/fe/fe-core" |
| SRC_ROOT="$FE_CORE/src/main/java" |
| TEST_SRC_ROOT="$FE_CORE/src/test/java" |
| TARGET_CLASSES="$FE_CORE/target/classes" |
| TARGET_TEST_CLASSES="$FE_CORE/target/test-classes" |
| TARGET_LIB="$FE_CORE/target/lib" |
| OUTPUT_JAR="$DORIS_HOME/output/fe/lib/doris-fe.jar" |
| TARGET_JAR="$FE_CORE/target/doris-fe.jar" |
| CP_CACHE="$FE_CORE/target/fast-compile-cp.txt" |
| TEST_CP_CACHE="$FE_CORE/target/fast-compile-test-cp.txt" |
| |
| FE_COMMON="$DORIS_HOME/fe/fe-common" |
| VERSION_JAVA="$FE_COMMON/target/generated-sources/build/org/apache/doris/common/Version.java" |
| VERSION_CLASS_DIR="$FE_COMMON/target/classes" |
| FE_COMMON_TARGET_JAR="$FE_COMMON/target/doris-fe-common.jar" |
| |
| # Extract Java release level from fe-core pom.xml (maven-compiler-plugin <release>N</release>) |
| JAVA_RELEASE="$(sed -n 's/.*<release>\([0-9]*\)<\/release>.*/\1/p' "$FE_CORE/pom.xml" | head -1)" |
| JAVA_RELEASE="${JAVA_RELEASE:-8}" |
| |
| # Generated source directories — auto-scanned so new directories are picked up automatically |
| GEN_SOURCES=() |
| for _d in "$FE_CORE/target/generated-sources"/*/; do |
| [[ -d "$_d" ]] && GEN_SOURCES+=("${_d%/}") |
| done |
| unset _d |
| |
| # ─── Color Output ──────────────────────────────────────────────────────────────── |
| RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m' |
| info() { echo -e "${GREEN}[INFO]${NC} $*"; } |
| warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } |
| error() { echo -e "${RED}[ERROR]${NC} $*" >&2; } |
| |
| # ─── Update Version.java if git commit changed ─────────────────────────────── |
| # Version.java is generated by gensrc/script/gen_build_version.sh (see how-to-get-version.txt). |
| # It bakes the current git short hash into DORIS_BUILD_SHORT_HASH, which FE reports |
| # via heartbeat and `show frontends` shows as the Version column. A full mvn build |
| # regenerates it, but fast-compile never triggers that, so the hash goes stale. |
| # Here we compare the on-disk Version.java's hash against `git log -1 %h` and, when |
| # they differ, delete Version.java (gen_build_version.sh early-exits if it exists), |
| # re-run the generator, recompile Version.class, and refresh the fe-common jars so |
| # the running FE picks up the new commit. |
| update_version_if_commit_changed() { |
| [[ -d "$DORIS_HOME/.git" ]] || return 0 |
| local current_hash |
| current_hash="$(cd "$DORIS_HOME" && git log -1 --pretty=format:"%h" 2>/dev/null || true)" |
| [[ -n "$current_hash" ]] || return 0 |
| |
| local baked_hash="" |
| if [[ -f "$VERSION_JAVA" ]]; then |
| baked_hash="$(sed -n 's/.*DORIS_BUILD_SHORT_HASH[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/p' "$VERSION_JAVA" | head -1)" |
| fi |
| |
| if [[ "$baked_hash" == "$current_hash" ]]; then |
| return 0 |
| fi |
| |
| info "Git commit changed: ${baked_hash:-<missing>} → $current_hash, regenerating Version.java" |
| |
| # gen_build_version.sh exits early when all three generated files exist, so |
| # force regeneration by removing Version.java first. |
| rm -f "$VERSION_JAVA" |
| (cd "$DORIS_HOME" && bash gensrc/script/gen_build_version.sh) \ |
| || { error "gen_build_version.sh failed"; return 1; } |
| |
| if [[ ! -f "$VERSION_JAVA" ]]; then |
| error "Version.java was not generated at $VERSION_JAVA" |
| return 1 |
| fi |
| |
| mkdir -p "$VERSION_CLASS_DIR/org/apache/doris/common" |
| javac --release "$JAVA_RELEASE" -encoding UTF-8 \ |
| -d "$VERSION_CLASS_DIR" "$VERSION_JAVA" \ |
| || { error "Failed to compile Version.java"; return 1; } |
| |
| local version_class_rel="org/apache/doris/common/Version.class" |
| local version_class="$VERSION_CLASS_DIR/$version_class_rel" |
| [[ -f "$version_class" ]] || { error "Version.class missing after compile"; return 1; } |
| |
| pushd "$VERSION_CLASS_DIR" > /dev/null |
| if [[ -f "$FE_COMMON_TARGET_JAR" ]]; then |
| jar uf "$FE_COMMON_TARGET_JAR" "$version_class_rel" |
| info "Updated $FE_COMMON_TARGET_JAR" |
| fi |
| # output/fe/lib uses a versioned name like fe-common-1.2-SNAPSHOT.jar |
| local output_common |
| for output_common in "$DORIS_HOME/output/fe/lib"/fe-common-*.jar; do |
| [[ -f "$output_common" ]] || continue |
| jar uf "$output_common" "$version_class_rel" |
| info "Updated $output_common" |
| done |
| popd > /dev/null |
| } |
| |
| # ─── Environment Check ─────────────────────────────────────────────────────────── |
| check_env() { |
| if [[ ! -d "$TARGET_CLASSES" ]]; then |
| error "target/classes does not exist, please run a full build first: cd $DORIS_HOME && mvn package -pl fe/fe-core -DskipTests -T4" |
| exit 1 |
| fi |
| if [[ ! -f "$OUTPUT_JAR" && ! -f "$TARGET_JAR" ]]; then |
| error "doris-fe.jar does not exist, please run a full build first" |
| exit 1 |
| fi |
| # Sanity check: a complete build should have thousands of class files. |
| # If only a few hundred exist, the IDE (e.g. VS Code Java extension) likely |
| # cleaned and partially rebuilt target/classes. |
| local class_count |
| class_count=$(find "$TARGET_CLASSES" -name "*.class" | wc -l | tr -d ' ') |
| if [[ "$class_count" -lt 1000 ]]; then |
| error "target/classes appears incomplete (only $class_count .class files found)." |
| error "This usually means the VS Code Java extension has overwritten the Maven build output." |
| error "Please run a full build: cd $DORIS_HOME && mvn package -pl fe/fe-core -DskipTests -T4" |
| exit 1 |
| fi |
| } |
| |
| # ─── Find a jar in local .m2 repository (for 'provided' scope deps absent from target/lib) ── |
| # Usage: find_m2_jar <groupId_path> <artifactId> [version_property_name] |
| # Example: find_m2_jar org/projectlombok lombok lombok.version |
| find_m2_jar() { |
| local group_path="$1" artifact="$2" version_prop="${3:-}" |
| local m2_dir="$HOME/.m2/repository/$group_path/$artifact" |
| [[ -d "$m2_dir" ]] || return 0 |
| |
| if [[ -n "$version_prop" ]]; then |
| local version |
| version=$(grep "<${version_prop}>" "$DORIS_HOME/fe/pom.xml" 2>/dev/null \ |
| | sed 's/.*>\(.*\)<.*/\1/' | head -1) |
| if [[ -n "$version" ]]; then |
| local jar="$m2_dir/${version}/${artifact}-${version}.jar" |
| [[ -f "$jar" ]] && echo "$jar" && return |
| fi |
| fi |
| # Fallback: pick the latest version found in .m2 |
| find "$m2_dir" -name "${artifact}-*.jar" ! -name "*sources*" ! -name "*tests*" \ |
| 2>/dev/null | sort -V | tail -1 |
| } |
| |
| # ─── Get classpath (with cache) ──────────────────────────────────────────────── |
| get_classpath() { |
| # Build the classpath from output/fe/lib/*.jar — always present after a full build. |
| # This avoids the 'mvn dependency:build-classpath' approach which fails when the |
| # installed POMs in .m2 contain unresolved ${revision} CI-friendly placeholders. |
| local output_lib="$DORIS_HOME/output/fe/lib" |
| if [[ ! -d "$output_lib" ]]; then |
| error "output/fe/lib does not exist, please run a full build first" |
| exit 1 |
| fi |
| |
| if [[ ! -f "$CP_CACHE" || ! -s "$CP_CACHE" || "$FE_CORE/pom.xml" -nt "$CP_CACHE" ]]; then |
| info "Building classpath from output/fe/lib/*.jar ..." |
| local cp="" |
| for jar in "$output_lib"/*.jar; do |
| [[ -f "$jar" ]] || continue |
| cp="${cp:+$cp:}$jar" |
| done |
| if [[ -z "$cp" ]]; then |
| error "No jars found in $output_lib" |
| exit 1 |
| fi |
| echo "$cp" > "$CP_CACHE" |
| info "Classpath cache saved to $CP_CACHE" |
| fi |
| |
| # 'provided' scope jars are excluded by the build output; locate from .m2 and append. |
| # lombok: annotation processor for @Getter/@Setter/@Data etc., used widely across the codebase |
| local lombok_jar |
| lombok_jar="$(find_m2_jar org/projectlombok lombok lombok.version)" |
| [[ -z "$lombok_jar" ]] && warn "lombok jar not found; files with Lombok annotations may fail to compile" |
| |
| # lakesoul-io-java: directly imported by LakeSoul catalog source files |
| local lakesoul_jar |
| lakesoul_jar="$(find_m2_jar com/dmetasoul lakesoul-io-java)" |
| |
| # slf4j-api: logging facade, 'provided' scope so absent from compile classpath |
| local slf4j_jar |
| slf4j_jar="$(find_m2_jar org/slf4j slf4j-api slf4j.version)" |
| [[ -z "$slf4j_jar" ]] && warn "slf4j-api jar not found; files using SLF4J may fail to compile" |
| |
| # hudi-common: contains FileIOUtils imported by vendored DiskMap.java |
| local hudi_jar |
| hudi_jar="$(find_m2_jar org/apache/hudi hudi-common hudi.version)" |
| [[ -z "$hudi_jar" ]] && warn "hudi-common jar not found; Hudi source files may fail to compile" |
| |
| # immutables/value: @Value.Immutable annotation used by FlightAuthResult.java |
| local immutables_jar |
| immutables_jar="$(find_m2_jar org/immutables value immutables.version)" |
| [[ -z "$immutables_jar" ]] && warn "immutables value jar not found; files using @Value.Immutable may fail to compile" |
| |
| local provided_cp="" |
| for jar in "$lombok_jar" "$lakesoul_jar" "$slf4j_jar" "$hudi_jar" "$immutables_jar"; do |
| [[ -n "$jar" ]] && provided_cp="$provided_cp:$jar" |
| done |
| |
| # Internal sibling modules (fe-common, fe-foundation, fe-catalog, fe-type, etc.) that fe-core |
| # depends on are resolved by dependency:build-classpath from .m2, which requires 'mvn install'. |
| # If only 'mvn package' was run, those jars won't be in .m2. Add their target/classes |
| # directories here so incremental compilation always works regardless of install state. |
| local sibling_cp="" |
| local fe_dir="$DORIS_HOME/fe" |
| for module_dir in "$fe_dir"/fe-common "$fe_dir"/fe-foundation "$fe_dir"/fe-catalog \ |
| "$fe_dir"/fe-type "$fe_dir"/fe-authentication "$fe_dir"/fe-extension-spi; do |
| local mc="$module_dir/target/classes" |
| [[ -d "$mc" ]] && sibling_cp="$sibling_cp:$mc" |
| done |
| # classpath = output jars + provided jars + target/classes |
| echo "$(cat "$CP_CACHE")${provided_cp}:$TARGET_CLASSES" |
| } |
| |
| # ─── Get test classpath (with cache) ────────────────────────────────────────── |
| get_test_classpath() { |
| # Build test classpath from output/fe/lib/*.jar plus test-scope jars from .m2 |
| local output_lib="$DORIS_HOME/output/fe/lib" |
| if [[ ! -f "$TEST_CP_CACHE" || ! -s "$TEST_CP_CACHE" || "$FE_CORE/pom.xml" -nt "$TEST_CP_CACHE" ]]; then |
| info "Building test classpath from output/fe/lib/*.jar ..." |
| local cp="" |
| for jar in "$output_lib"/*.jar; do |
| [[ -f "$jar" ]] || continue |
| cp="${cp:+$cp:}$jar" |
| done |
| |
| # Add test-scope jars from .m2 (junit, mockito, etc.) |
| local test_jars=() |
| local m2="$HOME/.m2/repository" |
| for pattern in \ |
| "org/junit/jupiter" "org/junit/platform" "org/junit/vintage" \ |
| "org/mockito" "junit/junit" "org/hamcrest" \ |
| "org/opentest4j" "org/apiguardian"; do |
| while IFS= read -r jar; do |
| test_jars+=("$jar") |
| done < <(find "$m2/$pattern" -name "*.jar" ! -name "*sources*" ! -name "*javadoc*" 2>/dev/null) |
| done |
| for jar in "${test_jars[@]}"; do |
| cp="$cp:$jar" |
| done |
| |
| echo "$cp" > "$TEST_CP_CACHE" |
| info "Test classpath cache saved to $TEST_CP_CACHE" |
| fi |
| # test classpath = output jars + test jars + main classes + test classes |
| echo "$(cat "$TEST_CP_CACHE"):$TARGET_CLASSES:$TARGET_TEST_CLASSES" |
| } |
| |
| # ─── Find stale test java files ──────────────────────────────────────────────── |
| find_stale_test_java_files() { |
| local stale_files=() |
| |
| while IFS= read -r java_file; do |
| local rel_path="${java_file#$TEST_SRC_ROOT/}" |
| local class_path="$TARGET_TEST_CLASSES/${rel_path%.java}.class" |
| |
| if [[ ! -f "$class_path" ]]; then |
| stale_files+=("$java_file") |
| elif [[ "$java_file" -nt "$class_path" ]]; then |
| stale_files+=("$java_file") |
| fi |
| done < <(find "$TEST_SRC_ROOT" -name "*.java") |
| |
| printf '%s\n' "${stale_files[@]}" |
| } |
| |
| # ─── Compile test java files ────────────────────────────────────────────────── |
| compile_test_files() { |
| local classpath="$1" |
| shift |
| local java_files=("$@") |
| |
| mkdir -p "$TARGET_TEST_CLASSES" |
| |
| info "Compiling ${#java_files[@]} test files..." |
| for f in "${java_files[@]}"; do |
| echo " → ${f#$DORIS_HOME/}" |
| done |
| |
| javac \ |
| --release "$JAVA_RELEASE" \ |
| -encoding UTF-8 \ |
| -cp "$classpath" \ |
| -d "$TARGET_TEST_CLASSES" \ |
| "${java_files[@]}" 2>&1 |
| |
| info "Test compilation finished" |
| } |
| |
| # ─── Find stale java files ───────────────────────────────────────────────────── |
| find_stale_java_files() { |
| local stale_files=() |
| |
| while IFS= read -r java_file; do |
| # java_file: /path/to/src/main/java/org/apache/doris/Foo.java |
| # Skip the pattern generator package: it is compiled by Maven with <proc>only> |
| # (annotation processing only, no class output) and then excluded from |
| # default-compile, so no .class file is ever produced for it intentionally. |
| [[ "$java_file" == */nereids/pattern/generator/* ]] && continue |
| |
| # Convert to class file path |
| local rel_path="${java_file#$SRC_ROOT/}" # org/apache/doris/Foo.java |
| local class_path="$TARGET_CLASSES/${rel_path%.java}.class" |
| |
| if [[ ! -f "$class_path" ]]; then |
| # class file does not exist, must compile |
| stale_files+=("$java_file") |
| elif [[ "$java_file" -nt "$class_path" ]]; then |
| # java file is newer than main class file |
| stale_files+=("$java_file") |
| fi |
| done < <(find "$SRC_ROOT" -name "*.java") |
| |
| printf '%s\n' "${stale_files[@]}" |
| } |
| |
| # ─── Compile java files ─────────────────────────────────────────────────────── |
| compile_files() { |
| local classpath="$1" |
| shift |
| local java_files=("$@") |
| |
| info "Compiling ${#java_files[@]} files..." |
| for f in "${java_files[@]}"; do |
| echo " → ${f#$DORIS_HOME/}" |
| done |
| |
| # Compile with javac. |
| # NOTE: We intentionally omit -sourcepath here. With -sourcepath, javac would |
| # transitively recompile every referenced source file, pulling in files that |
| # depend on 'provided'-scope JARs (e.g. com.sleepycat.je, guava, log4j) not |
| # present in the incremental classpath. Since a full build has already |
| # populated target/classes, internal project dependencies are resolved from |
| # there via -cp, which is sufficient for incremental compilation. |
| javac \ |
| --release "$JAVA_RELEASE" \ |
| -encoding UTF-8 \ |
| -cp "$classpath" \ |
| -d "$TARGET_CLASSES" \ |
| "${java_files[@]}" 2>&1 |
| |
| info "Compilation finished" |
| } |
| |
| # ─── Collect class files to update jar ───────────────────────────────────────── |
| collect_updated_classes() { |
| local java_files=("$@") |
| local class_files=() |
| |
| for java_file in "${java_files[@]}"; do |
| local rel_path="${java_file#$SRC_ROOT/}" |
| local class_prefix="$TARGET_CLASSES/${rel_path%.java}" |
| local dir |
| dir="$(dirname "$class_prefix")" |
| local base |
| base="$(basename "$class_prefix")" |
| |
| # Main class file |
| [[ -f "$class_prefix.class" ]] && class_files+=("$class_prefix.class") |
| |
| # Inner classes and anonymous classes: Foo$Bar.class, Foo$1.class, etc. |
| while IFS= read -r inner; do |
| class_files+=("$inner") |
| done < <(find "$dir" -maxdepth 1 -name "${base}\$*.class" 2>/dev/null) |
| done |
| |
| printf '%s\n' "${class_files[@]}" |
| } |
| |
| # ─── Update jar ─────────────────────────────────────────────────────────────── |
| update_jar() { |
| local class_files=("$@") |
| |
| info "Updating jar (total ${#class_files[@]} class files)..." |
| |
| # Convert class file paths to relative paths for jar command |
| local tmpfile |
| tmpfile="$(mktemp)" |
| trap "rm -f $tmpfile" EXIT |
| |
| for cf in "${class_files[@]}"; do |
| echo "${cf#$TARGET_CLASSES/}" >> "$tmpfile" |
| done |
| |
| # Run jar uf in TARGET_CLASSES directory to ensure correct jar internal paths |
| pushd "$TARGET_CLASSES" > /dev/null |
| |
| # Update target/doris-fe.jar |
| if [[ -f "$TARGET_JAR" ]]; then |
| xargs jar uf "$TARGET_JAR" < "$tmpfile" |
| info "Updated $TARGET_JAR" |
| fi |
| |
| # Update output/fe/lib/doris-fe.jar |
| if [[ -f "$OUTPUT_JAR" ]]; then |
| xargs jar uf "$OUTPUT_JAR" < "$tmpfile" |
| info "Updated $OUTPUT_JAR" |
| fi |
| |
| popd > /dev/null |
| } |
| |
| # ─── Main workflow ──────────────────────────────────────────────────────────── |
| main() { |
| check_env |
| update_version_if_commit_changed |
| |
| # ── Test mode: --test [files...] ────────────────────────────────────────── |
| if [[ "${1:-}" == "--test" ]]; then |
| shift |
| local java_files=() |
| |
| if [[ $# -gt 0 ]]; then |
| for arg in "$@"; do |
| local abs_path |
| if [[ "$arg" = /* ]]; then |
| abs_path="$arg" |
| else |
| abs_path="$(pwd)/$arg" |
| fi |
| if [[ ! -f "$abs_path" ]]; then |
| local found |
| found="$(find "$TEST_SRC_ROOT" -name "$(basename "$arg")" | head -1)" |
| if [[ -z "$found" ]]; then |
| error "File does not exist: $arg" |
| exit 1 |
| fi |
| abs_path="$found" |
| fi |
| java_files+=("$abs_path") |
| done |
| info "Manually specified ${#java_files[@]} test files" |
| else |
| info "Scanning for changed test Java files..." |
| while IFS= read -r f; do |
| [[ -n "$f" ]] && java_files+=("$f") |
| done < <(find_stale_test_java_files) |
| |
| if [[ ${#java_files[@]} -eq 0 ]]; then |
| info "No test files need to be compiled, everything is up to date" |
| exit 0 |
| fi |
| info "Found ${#java_files[@]} test files need to be recompiled" |
| fi |
| |
| local start_time |
| start_time=$(date +%s) |
| |
| local test_classpath |
| test_classpath="$(get_test_classpath)" |
| |
| compile_test_files "$test_classpath" "${java_files[@]}" |
| |
| local end_time |
| end_time=$(date +%s) |
| info "Done! Time elapsed: $((end_time - start_time)) seconds" |
| return |
| fi |
| |
| # ── Main mode ───────────────────────────────────────────────────────────── |
| local java_files=() |
| |
| if [[ $# -gt 0 ]]; then |
| # User directly specifies files |
| for arg in "$@"; do |
| # Support relative and absolute paths |
| local abs_path |
| if [[ "$arg" = /* ]]; then |
| abs_path="$arg" |
| else |
| abs_path="$(pwd)/$arg" |
| fi |
| if [[ ! -f "$abs_path" ]]; then |
| # Try searching under SRC_ROOT |
| local found |
| found="$(find "$SRC_ROOT" -name "$(basename "$arg")" | head -1)" |
| if [[ -z "$found" ]]; then |
| error "File does not exist: $arg" |
| exit 1 |
| fi |
| abs_path="$found" |
| fi |
| java_files+=("$abs_path") |
| done |
| info "Manually specified ${#java_files[@]} files" |
| else |
| # Automatically detect changes |
| info "Scanning for changed Java files..." |
| while IFS= read -r f; do |
| [[ -n "$f" ]] && java_files+=("$f") |
| done < <(find_stale_java_files) |
| |
| if [[ ${#java_files[@]} -eq 0 ]]; then |
| info "No files need to be compiled, everything is up to date" |
| exit 0 |
| fi |
| info "Found ${#java_files[@]} files need to be recompiled" |
| fi |
| |
| local start_time |
| start_time=$(date +%s) |
| |
| local classpath |
| classpath="$(get_classpath)" |
| |
| compile_files "$classpath" "${java_files[@]}" |
| |
| # Collect class files (including inner classes) |
| local class_files=() |
| while IFS= read -r cf; do |
| [[ -n "$cf" ]] && class_files+=("$cf") |
| done < <(collect_updated_classes "${java_files[@]}") |
| |
| if [[ ${#class_files[@]} -eq 0 ]]; then |
| warn "No compiled artifacts found, skipping jar update" |
| exit 0 |
| fi |
| |
| # Update jar |
| update_jar "${class_files[@]}" |
| |
| local end_time |
| end_time=$(date +%s) |
| info "Done! Time elapsed: $((end_time - start_time)) seconds" |
| } |
| |
| main "$@" |