chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:47:42 +08:00
commit be3ef883e1
1214 changed files with 431743 additions and 0 deletions
View File
+395
View File
@@ -0,0 +1,395 @@
#!/bin/bash
set -e
#######################################################################
# build_android.sh — cross-compile zvec for Android arm64-v8a,
# then run all C/C++ unit tests inside an emulator.
#
# Usage:
# ./scripts/build_android.sh [api_level] [build_type]
#
# Examples:
# ./scripts/build_android.sh # defaults: API 36, Release
# ./scripts/build_android.sh 36 Debug
#######################################################################
CURRENT_DIR=$(pwd)
ABI="arm64-v8a"
API_LEVEL=${1:-35}
BUILD_TYPE=${2:-"Release"}
CORE_COUNT=$(sysctl -n hw.ncpu 2>/dev/null || nproc 2>/dev/null || echo 4)
# ── Android SDK paths (set later, after host protoc build) ───────────
ANDROID_SDK_ROOT=${ANDROID_SDK_ROOT:-$HOME/Library/Android/sdk}
ANDROID_NDK_HOME=${ANDROID_NDK_HOME:-$(ls -d "$ANDROID_SDK_ROOT/ndk/"* 2>/dev/null | sort -V | tail -1)}
echo "============================================================"
echo " Android Cross Build & Test"
echo "============================================================"
echo " ABI : $ABI"
echo " API Level : $API_LEVEL"
echo " Build Type : $BUILD_TYPE"
echo " NDK : $ANDROID_NDK_HOME"
echo " CPU cores : $CORE_COUNT"
echo "============================================================"
if [ ! -d "$ANDROID_NDK_HOME" ]; then
echo "ERROR: ANDROID_NDK_HOME not found at $ANDROID_NDK_HOME"
echo "Please install the NDK via Android Studio or sdkmanager."
exit 1
fi
# ── Step 1: build host protoc (using HOST compiler, NOT NDK) ─────────
echo ""
echo ">>> Step 1: Building protoc for host..."
HOST_BUILD_DIR="build_host"
git submodule foreach --recursive 'git stash --include-untracked' 2>/dev/null || true
if [ ! -f "$CURRENT_DIR/$HOST_BUILD_DIR/bin/protoc" ]; then
# Explicitly avoid NDK toolchain for host build
cmake -S . -B "$HOST_BUILD_DIR" \
-DCMAKE_BUILD_TYPE="$BUILD_TYPE" \
-DCMAKE_TOOLCHAIN_FILE="" \
-G Ninja
cmake --build "$HOST_BUILD_DIR" --target protoc -j"$CORE_COUNT"
else
echo " (cached — skipping)"
fi
PROTOC_EXECUTABLE=$CURRENT_DIR/$HOST_BUILD_DIR/bin/protoc
echo ">>> Step 1: Done (protoc=$PROTOC_EXECUTABLE)"
# ── Now export Android env vars for cross-compilation ────────────────
export ANDROID_SDK_ROOT
export ANDROID_HOME=$ANDROID_SDK_ROOT
export ANDROID_NDK_HOME
export CMAKE_TOOLCHAIN_FILE=$ANDROID_NDK_HOME/build/cmake/android.toolchain.cmake
export PATH=$PATH:$ANDROID_SDK_ROOT/cmdline-tools/latest/bin
export PATH=$PATH:$ANDROID_SDK_ROOT/platform-tools
export PATH=$PATH:$ANDROID_SDK_ROOT/emulator
export PATH=$PATH:$ANDROID_NDK_HOME
# ── Step 2: cross-compile zvec + tests for Android ───────────────────
echo ""
echo ">>> Step 2: Cross-compiling zvec for Android ($ABI, API $API_LEVEL)..."
# reset thirdparty so the cross toolchain can patch cleanly
git submodule foreach --recursive 'git stash --include-untracked' 2>/dev/null || true
BUILD_DIR="build_android_${ABI}"
# Force CMake reconfigure to pick up any bazel.cmake changes
rm -f "$BUILD_DIR/CMakeCache.txt"
cmake -S . -B "$BUILD_DIR" -G Ninja \
-DANDROID_NDK="$ANDROID_NDK_HOME" \
-DCMAKE_TOOLCHAIN_FILE="$CMAKE_TOOLCHAIN_FILE" \
-DANDROID_ABI="$ABI" \
-DANDROID_NATIVE_API_LEVEL="$API_LEVEL" \
-DANDROID_STL="c++_static" \
-DCMAKE_BUILD_TYPE="$BUILD_TYPE" \
-DBUILD_PYTHON_BINDINGS=OFF \
-DBUILD_TOOLS=OFF \
-DENABLE_NATIVE=OFF \
-DAUTO_DETECT_ARCH=OFF \
-DCMAKE_INSTALL_PREFIX="$BUILD_DIR/install" \
-DGLOBAL_CC_PROTOBUF_PROTOC="$PROTOC_EXECUTABLE"
echo " Building library..."
cmake --build "$BUILD_DIR" -j"$CORE_COUNT"
# Discover test target names from ctest metadata (before building)
echo " Discovering test targets from ctest..."
TEST_NAMES=()
while IFS= read -r line; do
name=$(echo "$line" | sed -n 's/.*Test[[:space:]]*#[0-9]*:[[:space:]]*//p')
if [ -n "$name" ]; then
TEST_NAMES+=("$name")
fi
done < <(cd "$BUILD_DIR" && ctest --show-only 2>/dev/null || true)
# Fallback: find *_test targets from CMake cache
if [ ${#TEST_NAMES[@]} -eq 0 ]; then
echo " ctest listing unavailable, scanning ninja targets..."
while IFS= read -r line; do
name=$(echo "$line" | sed -n 's/^\([a-zA-Z0-9_]*_test\): .*/\1/p')
if [ -n "$name" ]; then
TEST_NAMES+=("$name")
fi
done < <(ninja -C "$BUILD_DIR" -t targets all 2>/dev/null || true)
fi
echo " Found ${#TEST_NAMES[@]} test targets."
if [ ${#TEST_NAMES[@]} -eq 0 ]; then
echo "WARNING: No test targets found. Skipping emulator step."
exit 0
fi
# Build all test executables at once (without running ctest)
echo " Building ${#TEST_NAMES[@]} test executables..."
ninja -C "$BUILD_DIR" -j"$CORE_COUNT" "${TEST_NAMES[@]}"
echo ">>> Step 2: Done"
# ── Step 3: collect test binaries ────────────────────────────────────
echo ""
echo ">>> Step 3: Collecting test binaries..."
TEST_BINS=()
for name in "${TEST_NAMES[@]}"; do
bin_path=$(find "$BUILD_DIR" -name "$name" -type f -perm +111 2>/dev/null | head -1)
if [ -n "$bin_path" ]; then
TEST_BINS+=("$bin_path")
else
echo " WARNING: binary not found for target '$name'"
fi
done
echo " Found ${#TEST_BINS[@]} test binaries."
if [ ${#TEST_BINS[@]} -eq 0 ]; then
echo "WARNING: No test binaries found. Skipping emulator step."
exit 0
fi
# ── Step 4: start emulator ───────────────────────────────────────────
echo ""
echo ">>> Step 4: Starting Android emulator..."
EMULATOR_BIN="$ANDROID_SDK_ROOT/emulator/emulator"
ADB_BIN="$ANDROID_SDK_ROOT/platform-tools/adb"
AVD_NAME="zvec_test_avd"
# Find a usable system image
SYS_IMG=""
for candidate in \
"$ANDROID_SDK_ROOT/system-images/android-${API_LEVEL}/default/arm64-v8a" \
"$ANDROID_SDK_ROOT/system-images/android-${API_LEVEL}/google_apis/arm64-v8a" \
"$ANDROID_SDK_ROOT/system-images/android-${API_LEVEL}/google_apis_playstore/arm64-v8a"; do
if [ -d "$candidate" ]; then
SYS_IMG="$candidate"
break
fi
done
# If the exact API level isn't found, try any available arm64-v8a image
if [ -z "$SYS_IMG" ]; then
echo " No system image for API $API_LEVEL, searching for any arm64-v8a image..."
SYS_IMG=$(find "$ANDROID_SDK_ROOT/system-images" -type d -name "arm64-v8a" 2>/dev/null | head -1)
fi
if [ -z "$SYS_IMG" ]; then
echo "ERROR: No arm64-v8a system image found."
echo "Install one via: sdkmanager 'system-images;android-${API_LEVEL};default;arm64-v8a'"
exit 1
fi
echo " Using system image: $SYS_IMG"
# Create AVD without avdmanager (write INI files directly)
AVD_DIR="$HOME/.android/avd/${AVD_NAME}.avd"
AVD_INI="$HOME/.android/avd/${AVD_NAME}.ini"
cleanup_emulator() {
echo ""
echo ">>> Cleaning up emulator..."
$ADB_BIN emu kill 2>/dev/null || true
sleep 2
# Kill any lingering emulator processes for our AVD
pkill -f "emulator.*${AVD_NAME}" 2>/dev/null || true
}
trap cleanup_emulator EXIT
mkdir -p "$AVD_DIR"
# Write the top-level .ini
cat > "$AVD_INI" << EOF
avd.ini.encoding=UTF-8
path=${AVD_DIR}
path.rel=avd/${AVD_NAME}.avd
target=android-${API_LEVEL}
EOF
# Write the AVD config
cat > "$AVD_DIR/config.ini" << EOF
AvdId=${AVD_NAME}
PlayStore.enabled=false
abi.type=arm64-v8a
avd.ini.displayname=${AVD_NAME}
avd.ini.encoding=UTF-8
disk.dataPartition.size=8G
hw.accelerator.isConfigured=true
hw.cpu.arch=arm64
hw.cpu.ncore=4
hw.lcd.density=420
hw.lcd.height=1920
hw.lcd.width=1080
hw.ramSize=4096
image.sysdir.1=${SYS_IMG}/
tag.display=Default
tag.id=default
EOF
echo " Created AVD: $AVD_NAME"
# Kill any existing emulator
$ADB_BIN emu kill 2>/dev/null || true
sleep 1
# Start emulator
echo " Launching emulator..."
$EMULATOR_BIN -avd "$AVD_NAME" \
-no-window -no-audio -no-boot-anim \
-gpu swiftshader_indirect \
-netdelay none -netspeed full \
-memory 4096 \
-no-snapshot \
-wipe-data \
2>&1 | sed 's/^/ [emulator] /' &
EMULATOR_PID=$!
# Wait for the device to become reachable
echo " Waiting for device..."
$ADB_BIN wait-for-device
# Wait for boot_completed
echo " Waiting for boot to complete..."
BOOT_TIMEOUT=300
BOOT_ELAPSED=0
while true; do
BOOTED=$($ADB_BIN shell getprop sys.boot_completed 2>/dev/null | tr -d '\r\n ' || true)
if [ "$BOOTED" = "1" ]; then
echo " Emulator is ready! (took ${BOOT_ELAPSED}s)"
break
fi
if [ $BOOT_ELAPSED -ge $BOOT_TIMEOUT ]; then
echo "ERROR: Emulator failed to boot within ${BOOT_TIMEOUT}s"
exit 1
fi
sleep 3
BOOT_ELAPSED=$((BOOT_ELAPSED + 3))
done
# Print device info
echo ""
echo " Device ABI : $($ADB_BIN shell getprop ro.product.cpu.abi | tr -d '\r')"
echo " ABI list : $($ADB_BIN shell getprop ro.product.cpu.abilist | tr -d '\r')"
echo " CPU info :"
$ADB_BIN shell 'cat /proc/cpuinfo | grep -E "^(Features|flags|processor)"' 2>/dev/null | head -4 | sed 's/^/ /'
# ── Step 5: run tests on emulator ────────────────────────────────────
echo ""
echo ">>> Step 5: Running ${#TEST_BINS[@]} unit tests on emulator..."
DEVICE_TEST_DIR="/data/local/tmp/zvec_tests"
DEVICE_LIB_DIR="$DEVICE_TEST_DIR/lib"
$ADB_BIN shell "mkdir -p $DEVICE_TEST_DIR $DEVICE_LIB_DIR" 2>/dev/null
# Push all shared libraries that tests may depend on
echo " Pushing shared libraries..."
SO_COUNT=0
while IFS= read -r so_file; do
$ADB_BIN push "$so_file" "$DEVICE_LIB_DIR/$(basename "$so_file")" > /dev/null 2>&1
SO_COUNT=$((SO_COUNT + 1))
done < <(find "$BUILD_DIR/lib" -name "*.so" -type f 2>/dev/null)
echo " Pushed $SO_COUNT shared libraries."
# Push non-test helper binaries (e.g. data_generator, collection_optimizer)
# These are needed by crash_recovery tests which fork+exec them at runtime.
echo " Pushing helper binaries..."
HELPER_COUNT=0
for helper_name in data_generator collection_optimizer; do
helper_path=$(find "$BUILD_DIR" -name "$helper_name" -type f -perm +111 ! -name "*_test" 2>/dev/null | head -1)
if [ -n "$helper_path" ]; then
$ADB_BIN push "$helper_path" "$DEVICE_TEST_DIR/$helper_name" > /dev/null 2>&1
$ADB_BIN shell "chmod 755 $DEVICE_TEST_DIR/$helper_name" 2>/dev/null
HELPER_COUNT=$((HELPER_COUNT + 1))
fi
done
echo " Pushed $HELPER_COUNT helper binaries."
TOTAL=${#TEST_BINS[@]}
PASSED=0
FAILED=0
FAILED_NAMES=()
IDX=0
for test_bin in "${TEST_BINS[@]}"; do
IDX=$((IDX + 1))
test_name=$(basename "$test_bin")
device_path="$DEVICE_TEST_DIR/$test_name"
# Give each test its own working directory to avoid name collisions
# (e.g. flat_builder_test binary vs flat_builder_test/ directory)
WORK_DIR="$DEVICE_TEST_DIR/workdir_${test_name}"
echo ""
echo "────────────────────────────────────────"
echo " [$IDX/$TOTAL] $test_name"
echo "────────────────────────────────────────"
set +e
# Create isolated working directory
$ADB_BIN shell "mkdir -p $WORK_DIR" 2>/dev/null
# Copy helper binaries into working directory so crash_recovery tests
# (which fork+exec data_generator / collection_optimizer) can find them.
$ADB_BIN shell "for h in $DEVICE_TEST_DIR/data_generator $DEVICE_TEST_DIR/collection_optimizer; do [ -f \$h ] && cp \$h $WORK_DIR/; done" 2>/dev/null
# Push binary
$ADB_BIN push "$test_bin" "$device_path" > /dev/null 2>&1
$ADB_BIN shell "chmod 755 $device_path" 2>/dev/null
# Run test from its own working directory with LD_LIBRARY_PATH
OUTPUT=$($ADB_BIN shell "cd $WORK_DIR && LD_LIBRARY_PATH=$DEVICE_LIB_DIR $device_path 2>&1; echo EXIT_CODE=\$?" 2>&1)
# Extract exit code from the output
EXIT_CODE=$(echo "$OUTPUT" | grep -o 'EXIT_CODE=[0-9]*' | tail -1 | cut -d= -f2)
set -e
# Print test output (without the EXIT_CODE line)
echo "$OUTPUT" | grep -v 'EXIT_CODE=' | sed 's/^/ /'
if [ "$EXIT_CODE" = "0" ]; then
echo " >>> PASSED"
PASSED=$((PASSED + 1))
else
# Detect "segfault-on-exit" pattern: all gtest assertions passed but
# process crashed during teardown (common with cross-compiled gmock tests).
GTEST_PASSED_LINE=$(echo "$OUTPUT" | grep '\[ PASSED \]' | tail -1)
GTEST_FAILED_LINE=$(echo "$OUTPUT" | grep '\[ FAILED \]' | head -1)
if [ -n "$GTEST_PASSED_LINE" ] && [ -z "$GTEST_FAILED_LINE" ] && \
{ [ "$EXIT_CODE" = "139" ] || [ "$EXIT_CODE" = "134" ] || [ "$EXIT_CODE" = "135" ]; }; then
echo " >>> PASSED (with crash-on-exit, exit code: $EXIT_CODE — ignored)"
PASSED=$((PASSED + 1))
else
echo " >>> FAILED (exit code: $EXIT_CODE)"
FAILED=$((FAILED + 1))
FAILED_NAMES+=("$test_name")
fi
fi
# Clean up binary and working directory to reclaim disk space
$ADB_BIN shell "rm -rf $device_path $WORK_DIR" 2>/dev/null || true
done
# ── Summary ──────────────────────────────────────────────────────────
echo ""
echo "============================================================"
echo " Test Summary"
echo "============================================================"
echo " Total : $TOTAL"
echo " Passed : $PASSED"
echo " Failed : $FAILED"
if [ $FAILED -gt 0 ]; then
echo ""
echo " Failed tests:"
for name in "${FAILED_NAMES[@]}"; do
echo " - $name"
done
fi
echo "============================================================"
if [ $FAILED -gt 0 ]; then
exit 1
fi
echo "All tests passed!"
+178
View File
@@ -0,0 +1,178 @@
#!/bin/bash
set -e
CURRENT_DIR=$(pwd)
# Platform options: OS (arm64 device), SIMULATOR64 (x86_64 sim), SIMULATORARM64 (arm64 sim)
PLATFORM=${1:-"OS"}
BUILD_TYPE=${2:-"Release"}
IOS_DEPLOYMENT_TARGET="13.0"
# Determine architecture based on platform
case "$PLATFORM" in
"OS")
ARCH="arm64"
;;
"SIMULATOR64")
ARCH="x86_64"
;;
"SIMULATORARM64")
ARCH="arm64"
;;
*)
echo "error: Unknown platform '$PLATFORM'"
echo "Usage: $0 [OS|SIMULATOR64|SIMULATORARM64] [Release|Debug]"
echo " OS - Build for iOS device (arm64)"
echo " SIMULATOR64 - Build for iOS Simulator (x86_64)"
echo " SIMULATORARM64- Build for iOS Simulator (arm64, Apple Silicon)"
exit 1
;;
esac
echo "Building zvec for iOS"
echo " Platform: $PLATFORM"
echo " Architecture: $ARCH"
echo " Build Type: $BUILD_TYPE"
echo " iOS Deployment Target: $IOS_DEPLOYMENT_TARGET"
# step1: use host env to compile protoc
echo "step1: building protoc for host..."
git submodule foreach --recursive 'git stash --include-untracked'
HOST_BUILD_DIR="build_host"
mkdir -p $HOST_BUILD_DIR
cd $HOST_BUILD_DIR
cmake -DCMAKE_BUILD_TYPE="$BUILD_TYPE" ..
make -j protoc
PROTOC_EXECUTABLE=$CURRENT_DIR/$HOST_BUILD_DIR/bin/protoc
cd $CURRENT_DIR
echo "step1: Done!!!"
# step2: cross build zvec for iOS
echo "step2: building zvec for iOS..."
# reset thirdparty directory
git submodule foreach --recursive 'git stash --include-untracked'
BUILD_DIR="build_ios_${PLATFORM}"
mkdir -p $BUILD_DIR
cd $BUILD_DIR
# Determine SDK and additional flags based on platform
if [ "$PLATFORM" = "OS" ]; then
SDK_NAME="iphoneos"
else
SDK_NAME="iphonesimulator"
fi
SDK_PATH=$(xcrun --sdk $SDK_NAME --show-sdk-path)
echo "configure CMake..."
cmake \
-DCMAKE_SYSTEM_NAME=iOS \
-DCMAKE_OSX_DEPLOYMENT_TARGET="$IOS_DEPLOYMENT_TARGET" \
-DCMAKE_OSX_ARCHITECTURES="$ARCH" \
-DCMAKE_OSX_SYSROOT="$SDK_PATH" \
-DCMAKE_BUILD_TYPE="$BUILD_TYPE" \
-DBUILD_PYTHON_BINDINGS=OFF \
-DBUILD_TOOLS=OFF \
-DCMAKE_INSTALL_PREFIX="./install" \
-DGLOBAL_CC_PROTOBUF_PROTOC=$PROTOC_EXECUTABLE \
-DIOS=ON \
../
echo "building..."
CORE_COUNT=$(sysctl -n hw.ncpu)
make -j$CORE_COUNT
echo "step2: Done!!!"
# step3: build and run all unit tests on simulator
if [ "$PLATFORM" != "OS" ]; then
echo "step3: building and running unit tests on simulator..."
make -j$CORE_COUNT unittest
# Boot simulator
DEVICE_ID=$(xcrun simctl list devices available -j \
| python3 -c "
import json, sys
data = json.load(sys.stdin)
for runtime, devices in data['devices'].items():
if 'iOS' in runtime:
for d in devices:
if 'iPhone' in d['name'] and d['isAvailable']:
print(d['udid'])
sys.exit(0)
sys.exit(1)
")
xcrun simctl boot "$DEVICE_ID"
echo "Booted simulator: $DEVICE_ID"
# Run all test .app bundles
FAILED_TESTS=""
PASSED=0
TOTAL=0
for APP in bin/*_test.app; do
[ -d "$APP" ] || continue
TEST_NAME=$(basename "$APP" .app)
BUNDLE_ID="com.zvec.${TEST_NAME}"
TOTAL=$((TOTAL + 1))
echo "--- Running ${TEST_NAME} ---"
xcrun simctl install "$DEVICE_ID" "$APP"
set +e
for attempt in 1 2 3; do
xcrun simctl launch --console "$DEVICE_ID" "$BUNDLE_ID" 2>&1 | tee /tmp/${TEST_NAME}.log
LAUNCH_EXIT=${PIPESTATUS[0]}
if ! grep -q "unknown to FrontBoard" /tmp/${TEST_NAME}.log; then
break
fi
echo "Attempt ${attempt}/3: FrontBoard has not registered ${TEST_NAME} yet, retrying in 3s..."
sleep 3
done
set -e
if grep -q '\[ FAILED \]' /tmp/${TEST_NAME}.log; then
echo "FAIL: ${TEST_NAME}"
FAILED_TESTS="${FAILED_TESTS} ${TEST_NAME}"
elif grep -q '\[ PASSED \]' /tmp/${TEST_NAME}.log; then
PASSED=$((PASSED + 1))
elif grep -qE 'Failed: 0$' /tmp/${TEST_NAME}.log; then
# c_api_test uses a custom test framework (not GTest)
PASSED=$((PASSED + 1))
elif [ "$LAUNCH_EXIT" -eq 0 ]; then
echo "WARN: ${TEST_NAME} exited 0 but produced no recognisable test summary"
PASSED=$((PASSED + 1))
else
echo "FAIL: ${TEST_NAME} exited ${LAUNCH_EXIT} with no test summary"
FAILED_TESTS="${FAILED_TESTS} ${TEST_NAME}"
fi
done
# Shutdown simulator
xcrun simctl shutdown "$DEVICE_ID" || true
echo ""
echo "Test summary: ${PASSED}/${TOTAL} passed"
if [ -n "$FAILED_TESTS" ]; then
echo "Failed tests:${FAILED_TESTS}"
exit 1
fi
echo "step3: Done!!!"
else
echo "Skipping tests (device build cannot run on simulator)"
fi
echo ""
echo "Build completed successfully!"
echo "Output directory: $CURRENT_DIR/$BUILD_DIR"
# Test On MacOS15
# 1: xcrun simctl boot "iPhone 16"
# 2: cd $BUILD_DIR
# 3: xcrun simctl launch --console booted com.zvec.collection_test
+41
View File
@@ -0,0 +1,41 @@
#!/bin/bash
project_name=proxima-zvec
gcov_tool=gcov
zip_html=false
output_name=html
keep_info=false
script_dir=$(cd "$(dirname "$0")"; pwd)
source_base=$(dirname "$script_dir")
filter_list="'*/tests/*' '*/thirdparty/*' '*/deps/*' '*/proto/*' '*/external/*' '*/sqlengine/antlr/gen/*'"
while getopts t:p:o:zk option; do
case "$option" in
t)
gcov_tool=$OPTARG;;
p)
project_name=$OPTARG;;
o)
output_name=$OPTARG;;
z)
zip_html=true;;
k)
keep_info=true;;
esac
done
# Process sources
lcov -c -b "$source_base" -d . -o $project_name.lcov.info --gcov-tool=$gcov_tool --no-external --keep-going --ignore-errors unused,mismatch || exit 1
eval $(echo lcov -r $project_name.lcov.info -o $project_name-filtered.lcov.info $filter_list --ignore-errors unused,mismatch) || exit 1
# Gather HTML files
genhtml -t "$project_name" -o $output_name $project_name-filtered.lcov.info || exit 1
if [ "$keep_info" = false ]; then
rm -rf *.lcov.info
fi
# Zip HTML files
if $zip_html ; then
zip -r $output_name.zip $output_name/
fi
+177
View File
@@ -0,0 +1,177 @@
#!/usr/bin/env python3
#
# Generate Unicode lookup tables for the standard tokenizer.
#
# Usage:
# scripts/generate_standard_tokenizer_unicode.py \
# --ucd-dir /path/to/Public/17.0.0/ucd \
# --out src/db/index/column/fts_column/tokenizer/standard_tokenizer_unicode.inc
from __future__ import annotations
import argparse
import pathlib
WORD_BREAK_CLASSES = {
"ALetter": "ALetter",
"CR": "CR",
"Double_Quote": "DoubleQuote",
"Extend": "Extend",
"ExtendNumLet": "ExtendNumLet",
"Format": "Format",
"Hebrew_Letter": "HebrewLetter",
"Katakana": "Katakana",
"LF": "LF",
"MidLetter": "MidLetter",
"MidNum": "MidNum",
"MidNumLet": "MidNumLet",
"Newline": "Newline",
"Numeric": "Numeric",
"Regional_Indicator": "RegionalIndicator",
"Single_Quote": "SingleQuote",
"WSegSpace": "WSegSpace",
"ZWJ": "ZWJ",
}
SCRIPT_CLASSES = {
"Han": "Ideographic",
"Hangul": "Hangul",
"Hiragana": "Hiragana",
}
LINE_BREAK_COMPLEX_CONTEXT = {
"Complex_Context",
"SA",
}
def parse_codepoint_range(field):
if ".." in field:
start, end = field.split("..", 1)
return int(start, 16), int(end, 16)
cp = int(field, 16)
return cp, cp
def parse_property_file(path, accepted_properties):
ranges = []
with path.open("r", encoding="utf-8") as fin:
for raw_line in fin:
line = raw_line.split("#", 1)[0].strip()
if not line:
continue
fields = [field.strip() for field in line.split(";")]
if len(fields) < 2:
continue
prop = fields[1]
if prop not in accepted_properties:
continue
start, end = parse_codepoint_range(fields[0])
ranges.append((start, end, accepted_properties[prop]))
return merge_class_ranges(ranges)
def parse_range_properties(path, accepted_properties):
ranges = []
with path.open("r", encoding="utf-8") as fin:
for raw_line in fin:
line = raw_line.split("#", 1)[0].strip()
if not line:
continue
fields = [field.strip() for field in line.split(";")]
if len(fields) < 2:
continue
if fields[1] not in accepted_properties:
continue
ranges.append(parse_codepoint_range(fields[0]))
return merge_ranges(ranges)
def merge_class_ranges(ranges):
merged = []
for start, end, cls in sorted(ranges):
if merged and merged[-1][2] == cls and merged[-1][1] + 1 == start:
merged[-1] = (merged[-1][0], end, cls)
else:
merged.append((start, end, cls))
return merged
def merge_ranges(ranges):
merged = []
for start, end in sorted(ranges):
if merged and merged[-1][1] + 1 >= start:
merged[-1] = (merged[-1][0], max(merged[-1][1], end))
else:
merged.append((start, end))
return merged
def parse_binary_property(path, property_name):
return parse_range_properties(path, {property_name})
def write_class_table(out, name, ranges):
out.write(f"constexpr UnicodeClassRange {name}[] = {{\n")
for start, end, cls in ranges:
out.write(f" {{0x{start:04X}, 0x{end:04X}, WordBreakClass::{cls}}},\n")
out.write("};\n\n")
def write_range_table(out, name, ranges):
out.write(f"constexpr UnicodeRange {name}[] = {{\n")
for start, end in ranges:
out.write(f" {{0x{start:04X}, 0x{end:04X}}},\n")
out.write("};\n\n")
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--ucd-dir", required=True, type=pathlib.Path)
parser.add_argument("--out", required=True, type=pathlib.Path)
args = parser.parse_args()
word_break_path = args.ucd_dir / "auxiliary" / "WordBreakProperty.txt"
emoji_data_path = args.ucd_dir / "emoji" / "emoji-data.txt"
line_break_path = args.ucd_dir / "LineBreak.txt"
scripts_path = args.ucd_dir / "Scripts.txt"
word_break_ranges = parse_property_file(word_break_path, WORD_BREAK_CLASSES)
script_ranges = parse_property_file(scripts_path, SCRIPT_CLASSES)
extended_pictographic_ranges = parse_binary_property(
emoji_data_path, "Extended_Pictographic"
)
emoji_modifier_base_ranges = parse_binary_property(
emoji_data_path, "Emoji_Modifier_Base"
)
emoji_modifier_ranges = parse_binary_property(emoji_data_path, "Emoji_Modifier")
line_break_complex_context_ranges = parse_range_properties(
line_break_path, LINE_BREAK_COMPLEX_CONTEXT
)
args.out.parent.mkdir(parents=True, exist_ok=True)
with args.out.open("w", encoding="utf-8", newline="\n") as fout:
fout.write("// Generated by scripts/generate_standard_tokenizer_unicode.py\n")
fout.write(
"// Source: Unicode 17.0.0 WordBreakProperty, emoji-data, LineBreak, Scripts.\n"
)
fout.write(
"// Derived from Unicode data files licensed under Unicode License V3; see NOTICE.\n"
)
fout.write("// Do not edit by hand.\n\n")
fout.write("// clang-format off\n\n")
write_class_table(fout, "kWordBreakRanges", word_break_ranges)
write_class_table(fout, "kScriptClassRanges", script_ranges)
write_range_table(
fout, "kExtendedPictographicRanges", extended_pictographic_ranges
)
write_range_table(fout, "kEmojiModifierBaseRanges", emoji_modifier_base_ranges)
write_range_table(fout, "kEmojiModifierRanges", emoji_modifier_ranges)
write_range_table(
fout, "kLineBreakComplexContextRanges", line_break_complex_context_ranges
)
fout.write("// clang-format on\n")
if __name__ == "__main__":
main()