Skip to content

Commit 4555ee9

Browse files
committed
feat(build): support Linux aarch64 builds
The architecture-specific build logic keyed on PAIMON_CPU_FLAG and PAIMON_ARMV8_ARCH, neither of which was ever defined, so aarch64 got no -march= at all. TargetArchitecture.cmake now resolves the target architecture once, and PAIMON_AARCH64_MARCH selects the -march= value, default armv8-a. Building and running the suite there then exposed target-dependent behavior, and a gap in how a failure is reported. One of the fixes changes results on x86-64 as well: an out of range float to integer conversion now follows Java rather than whatever the hardware did. - The SSE4.2 CRC32C kernel is removed: it computed Castagnoli, not the persisted zlib CRC-32, and PAIMON_SIMD_LEVEL never selected it, so no on-disk value changes. - output/paimon-cpp.tar.gz becomes paimon-cpp-linux-x86_64.tar.gz, and Lumina, prebuilt for linux-x86_64 only, now fails configuration elsewhere with an explicit error. - CI runs the same matrix on both architectures: every build-and-test job now names the architecture it targets, and each x86_64 entry has an aarch64 counterpart, sanitizers included, with clang-tidy the one exception. A script-tests job is added as well, and all of them are required checks. - docs: a supported platform matrix, and rules on char signedness and on float to integer conversion. - BinaryString::NumBytesForFirstByte classified a UTF-8 leading byte by its sign, and the AArch64 Linux ABI makes plain char unsigned, so NumChars, Substring and IndexOf counted bytes, not characters. It classifies the byte as uint8_t now. - The variant a row value is read into keeps TINYINT in a plain char, and min and max aggregation compared those variants directly, so where char is unsigned they ordered a negative value above a positive one and min(-20, 10) returned 10. They compare the signed value now, as does VariantValueToString, which printed 236 for -20. - Converting an out of range or non-finite float to an integer was a plain static_cast, in the cast executor and in the Arrow kernel it called, so it was undefined behavior the two targets answered differently. JavaFloatingToIntegerCast applies the rule Java does instead: NaN to 0, and out of range saturating at the int32 or int64 bounds before narrowing to width. Both the literal and the array path use it, because a file is skipped by comparing stats converted through one against data converted through the other. - Scaling a decimal multiplied its 128-bit value as a signed integer. UBSan instruments that, and Clang implements the check on aarch64 by calling __muloti4, a compiler-rt builtin libgcc does not provide, so the sanitizer build did not link there. The overflow check preceding the multiplication already proves the product fits, so it goes through the unsigned type now, where the arithmetic is defined. - asan_symbolize.py, which every test binary pipes its output through, decoded stdin and encoded stdout with different settings, so a byte that is not valid UTF-8 broke the pipe and truncated the failure report. Both streams are pinned to UTF-8 with surrogateescape now, and ci/scripts/test_asan_symbolize.sh covers it.
1 parent 653cf18 commit 4555ee9

34 files changed

Lines changed: 1335 additions & 223 deletions

.asf.yaml

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -59,17 +59,31 @@ github:
5959
app_slug: -1
6060
- name: "rat-license-check"
6161
app_slug: -1
62-
- name: "asan-ubsan"
62+
- name: "script-tests"
6363
app_slug: -1
64-
- name: "tsan"
64+
- name: "asan-ubsan-x86_64"
6565
app_slug: -1
66-
- name: "clang-debug"
66+
- name: "tsan-x86_64"
6767
app_slug: -1
68-
- name: "clang-release"
68+
- name: "clang-debug-x86_64"
6969
app_slug: -1
70-
- name: "gcc-debug"
70+
- name: "clang-release-x86_64"
7171
app_slug: -1
72-
- name: "gcc-release"
72+
- name: "gcc-debug-x86_64"
73+
app_slug: -1
74+
- name: "gcc-release-x86_64"
75+
app_slug: -1
76+
- name: "gcc-debug-aarch64"
77+
app_slug: -1
78+
- name: "gcc-release-aarch64"
79+
app_slug: -1
80+
- name: "clang-debug-aarch64"
81+
app_slug: -1
82+
- name: "asan-ubsan-aarch64"
83+
app_slug: -1
84+
- name: "clang-release-aarch64"
85+
app_slug: -1
86+
- name: "tsan-aarch64"
7387
app_slug: -1
7488
- name: "gcc8-test"
7589
app_slug: -1

.github/workflows/build_and_test.yaml

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,31 +33,69 @@ permissions:
3333
contents: read
3434

3535
jobs:
36+
script-tests:
37+
name: script-tests
38+
runs-on: ubuntu-24.04
39+
timeout-minutes: 10
40+
steps:
41+
- name: Checkout paimon-cpp
42+
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
43+
- name: Run CMake module tests
44+
shell: bash
45+
run: ci/scripts/test_cmake_modules.sh
46+
- name: Run packaging argument tests
47+
shell: bash
48+
run: ci/scripts/test_packaging_args.sh
49+
- name: Run asan_symbolize tests
50+
shell: bash
51+
run: ci/scripts/test_asan_symbolize.sh
52+
3653
build-and-test:
3754
name: ${{ matrix.name }}
38-
runs-on: ubuntu-24.04
55+
runs-on: ${{ matrix.runner || 'ubuntu-24.04' }}
3956
timeout-minutes: 120
4057
strategy:
4158
fail-fast: false
4259
matrix:
4360
include:
44-
- name: gcc-release
61+
- name: gcc-release-x86_64
4562
cc: gcc-14
4663
cxx: g++-14
4764
build_args: --build_type Release
48-
- name: clang-release
65+
- name: clang-release-x86_64
4966
build_args: --build_type Release
50-
- name: gcc-debug
67+
- name: gcc-debug-x86_64
5168
cc: gcc-14
5269
cxx: g++-14
53-
- name: clang-debug
70+
- name: clang-debug-x86_64
5471
fetch_depth: '0' # fetch the PR target branch history for clang-tidy
5572
build_args: >-
5673
--check_clang_tidy
5774
--lint_git_target_commit "origin/${{ github.base_ref || github.event.repository.default_branch }}"
58-
- name: asan-ubsan
75+
- name: asan-ubsan-x86_64
5976
build_args: --enable_asan --enable_ubsan
60-
- name: tsan
77+
- name: tsan-x86_64
78+
skip_rust: true
79+
build_args: --enable_tsan
80+
- name: gcc-debug-aarch64
81+
runner: ubuntu-24.04-arm
82+
cc: gcc-14
83+
cxx: g++-14
84+
- name: gcc-release-aarch64
85+
runner: ubuntu-24.04-arm
86+
cc: gcc-14
87+
cxx: g++-14
88+
build_args: --build_type Release
89+
- name: clang-debug-aarch64
90+
runner: ubuntu-24.04-arm
91+
- name: asan-ubsan-aarch64
92+
runner: ubuntu-24.04-arm
93+
build_args: --enable_asan --enable_ubsan
94+
- name: clang-release-aarch64
95+
runner: ubuntu-24.04-arm
96+
build_args: --build_type Release
97+
- name: tsan-aarch64
98+
runner: ubuntu-24.04-arm
6199
skip_rust: true
62100
build_args: --enable_tsan
63101
steps:

.github/workflows/release_candidate.yaml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,21 +99,26 @@ jobs:
9999
name: Verify source archive (${{ matrix.compiler }})
100100
if: github.ref_type == 'tag'
101101
needs: archive
102-
runs-on: ubuntu-24.04
102+
runs-on: ${{ matrix.runner || 'ubuntu-24.04' }}
103103
timeout-minutes: 180
104104
strategy:
105105
fail-fast: false
106106
matrix:
107107
compiler:
108108
- gcc-14
109109
- clang
110+
- gcc-14-arm64
110111
include:
111112
- compiler: gcc-14
112113
cc: gcc-14
113114
cxx: g++-14
114115
- compiler: clang
115116
cc: clang
116117
cxx: clang++
118+
- compiler: gcc-14-arm64
119+
cc: gcc-14
120+
cxx: g++-14
121+
runner: ubuntu-24.04-arm
117122
steps:
118123
- name: Checkout source
119124
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ Paimon C++ currently provides:
4949
> Paimon C++ therefore currently treats the `bitmap` global index type as unsupported. The legacy
5050
> implementation remains in the codebase pending migration to the Java-compatible format.
5151
52-
Note: Only Linux x86_64 builds are currently supported and verified.
52+
Note: Linux `x86_64` and `aarch64` builds are supported and verified in CI. See the supported
53+
platform matrix in [docs/source/building.rst](docs/source/building.rst) for other platforms.
5354

5455
## Building
5556

build_and_package.sh

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ MAKE_CLEAN=false
2626
PACKAGE=false
2727
CMAKE_OPTIONS=()
2828
JOBS=""
29+
PACKAGE_PLATFORM=""
30+
PRINT_NAME=false
2931

3032
show_help() {
3133
cat << EOF
@@ -37,6 +39,12 @@ Options:
3739
-c, --clean Clean build directory before building
3840
-p, --package Package creation
3941
-j, --jobs <num> Number of parallel jobs for building (default: auto-detect)
42+
--platform <label>
43+
Platform label used in the package name (default: the host,
44+
e.g. linux-aarch64). This only labels the artifact; it does
45+
not configure a cross build. Must match
46+
[A-Za-z0-9][A-Za-z0-9._-]*, i.e. a single path component.
47+
--print-name Print the resolved package name and exit
4048
-h, --help Show this help message
4149
4250
CMake Options:
@@ -46,6 +54,7 @@ CMake Options:
4654
Examples:
4755
$0 -r -p -j 8 -DPAIMON_BUILD_SHARED=ON -DPAIMON_BUILD_STATIC=OFF
4856
$0 --debug --clean --package --jobs 4
57+
$0 -r -p --platform linux-aarch64
4958
5059
EOF
5160
}
@@ -82,6 +91,24 @@ while [[ $# -gt 0 ]]; do
8291
exit 1
8392
fi
8493
;;
94+
--platform)
95+
shift
96+
# The label becomes a path component of both the install prefix and
97+
# the tarball name, so it must stay a single safe component: the
98+
# pattern admits no separator and no shell metacharacter. Requiring a
99+
# leading letter or digit additionally stops a following option from
100+
# being swallowed as the value.
101+
if [[ $# -eq 0 || ! $1 =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]]; then
102+
echo "Error: --platform requires a label matching [A-Za-z0-9][A-Za-z0-9._-]*" >&2
103+
exit 1
104+
fi
105+
PACKAGE_PLATFORM="$1"
106+
shift
107+
;;
108+
--print-name)
109+
PRINT_NAME=true
110+
shift
111+
;;
85112
-h|--help)
86113
show_help
87114
exit 0
@@ -94,8 +121,27 @@ while [[ $# -gt 0 ]]; do
94121
esac
95122
done
96123

124+
# Defaults to the host. Note this only names the artifact: the bundled third-party
125+
# builds receive just CMAKE_C_COMPILER / CMAKE_CXX_COMPILER (see
126+
# EP_COMMON_TOOLCHAIN in cmake_modules/ThirdpartyToolchain.cmake) and no toolchain
127+
# file, sysroot or find-root, so a cross build is not wired up end to end.
128+
if [ -z "$PACKAGE_PLATFORM" ]; then
129+
HOST_OS=$(uname -s | tr '[:upper:]' '[:lower:]')
130+
if [ "$HOST_OS" = "darwin" ]; then
131+
HOST_OS="macos"
132+
fi
133+
PACKAGE_PLATFORM="$HOST_OS-$(uname -m)"
134+
fi
135+
BUILD_NAME="$BUILD_NAME-$PACKAGE_PLATFORM"
136+
137+
if [ "$PRINT_NAME" = true ]; then
138+
echo "$BUILD_NAME"
139+
exit 0
140+
fi
141+
97142
echo "========== Build Configuration =========="
98143
echo "Build Type: $BUILD_TYPE"
144+
echo "Package Platform: $PACKAGE_PLATFORM"
99145
echo "Package Name: $BUILD_NAME"
100146
echo "Clean Build: $MAKE_CLEAN"
101147
echo "Package: $PACKAGE"

build_support/asan_symbolize.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#
99
#===------------------------------------------------------------------------===#
1010
import bisect
11+
import io
1112
import os
1213
import re
1314
import subprocess
@@ -333,8 +334,17 @@ def process_stdin(self):
333334
if sys.version_info[0] == 2:
334335
sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', 0)
335336
else:
336-
# Unbuffered output is not supported in Python 3
337-
sys.stdout = os.fdopen(sys.stdout.fileno(), 'w')
337+
# Test output is not guaranteed to be valid UTF-8, so pin both streams to the same
338+
# encoding and round such bytes through surrogateescape, which passes them out
339+
# unchanged instead of raising and truncating the rest of the test log. Both the
340+
# encoding and the error handler have to be set on both streams: taking either from
341+
# the locale or from PYTHONIOENCODING would let them disagree and re-encode the bytes.
342+
# The buffers are rewrapped rather than reconfigured, which needs Python 3.7, and
343+
# line buffering stands in for the unbuffered output Python 3 does not support.
344+
sys.stdin = io.TextIOWrapper(sys.stdin.buffer, encoding='utf-8',
345+
errors='surrogateescape')
346+
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8',
347+
errors='surrogateescape', line_buffering=True)
338348

339349
while True:
340350
line = sys.stdin.readline()

ci/scripts/build_paimon.sh

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,12 @@ fi
136136
if [[ "${enable_tsan}" == "true" ]]; then
137137
ENABLE_TANTIVY="OFF" # Tantivy's Rust library is not TSAN-instrumented.
138138
fi
139+
# CI always builds natively, so the host architecture is the target architecture.
140+
host_arch=$(uname -m)
141+
if [[ "${host_arch}" != "x86_64" ]]; then
142+
ENABLE_LUMINA="OFF"
143+
echo "=== Lumina disabled: no prebuilt artifacts for ${host_arch} ==="
144+
fi
139145

140146
CMAKE_ARGS=(
141147
"-G Ninja"

ci/scripts/test_asan_symbolize.sh

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
#!/usr/bin/env bash
2+
# Licensed to the Apache Software Foundation (ASF) under one
3+
# or more contributor license agreements. See the NOTICE file
4+
# distributed with this work for additional information
5+
# regarding copyright ownership. The ASF licenses this file
6+
# to you under the Apache License, Version 2.0 (the
7+
# "License"); you may not use this file except in compliance
8+
# with the License. You may obtain a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing, software
13+
# distributed under the License is distributed on an "AS IS" BASIS,
14+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
# See the License for the specific language governing permissions and
16+
# limitations under the License.
17+
18+
# Tests that build_support/asan_symbolize.py, which every test binary pipes its
19+
# output through, passes bytes that are not valid UTF-8 straight through. A test
20+
# that fails while printing such a byte used to kill the symbolizer, and with it
21+
# the rest of the test log.
22+
23+
set -euo pipefail
24+
25+
source_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)
26+
symbolizer="${source_dir}/build_support/asan_symbolize.py"
27+
python=${PYTHON:-python3}
28+
# The vendored script emits SyntaxWarnings for its own regexes; they are unrelated noise here.
29+
export PYTHONWARNINGS=ignore
30+
31+
status=0
32+
33+
# Returns non-zero on failure as well as recording it, so that a caller running it in a
34+
# subshell, where the recorded status would not propagate, can still see the result.
35+
check() {
36+
local name=$1 input=$2 expected=$3 actual_hex expected_hex result=0
37+
echo "=== ${name} ==="
38+
# Compare hex dumps so that a mismatch is readable and the shell does not
39+
# mangle the bytes on the way. A failing symbolizer is a failed check, not a
40+
# reason to abort the script, so the remaining checks still run.
41+
if ! actual_hex=$(printf '%b' "${input}" | "${python}" "${symbolizer}" | od -An -tx1 |
42+
tr -d ' \n'); then
43+
echo "symbolizer exited non-zero"
44+
status=1
45+
result=1
46+
fi
47+
expected_hex=$(printf '%b' "${expected}" | od -An -tx1 | tr -d ' \n')
48+
if [[ "${actual_hex}" != "${expected_hex}" ]]; then
49+
echo "expected: ${expected_hex}"
50+
echo "actual: ${actual_hex}"
51+
status=1
52+
result=1
53+
fi
54+
return "${result}"
55+
}
56+
57+
# A lone 0xff is what a failing TINYINT literal assertion prints. Every byte,
58+
# valid UTF-8 or not, has to come out unchanged.
59+
check "invalid utf-8 round trips" 'ok\n\xff\xfe binary\nplain\n' 'ok\n\xff\xfe binary\nplain\n'
60+
61+
# Valid multi byte UTF-8 must not be damaged either: test names contain it.
62+
check "utf-8 round trips" '\xe4\xb8\xad\xe6\x96\x87\n' '\xe4\xb8\xad\xe6\x96\x87\n'
63+
64+
# The bytes have to survive whatever the environment asks Python to use: an encoding taken
65+
# from PYTHONIOENCODING on one stream and from the locale on the other would re-encode them.
66+
(
67+
export PYTHONIOENCODING=latin-1
68+
check "invalid utf-8 round trips under PYTHONIOENCODING" 'ok\n\xff\xfe\n' 'ok\n\xff\xfe\n'
69+
# status is set in this subshell and does not reach the caller, so report it as the exit
70+
# code, which stays correct if another check is added here.
71+
exit "${status}"
72+
) || status=1
73+
74+
# An invalid byte must not stop the lines that follow it from being processed:
75+
# the stack frame below is still rewritten by the symbolizer. Its stderr is dropped
76+
# because addr2line reports the fake binary path there even when the check passes.
77+
echo "=== keeps processing after an invalid byte ==="
78+
if ! output=$(printf '%b' '\xff\n #0 0x7f6e35cf2e45 (/blah/foo.so+0x11fe45)\ntail\n' |
79+
"${python}" "${symbolizer}" 2>/dev/null); then
80+
echo "symbolizer exited non-zero"
81+
status=1
82+
fi
83+
# "#0" and "tail" alone would also match unprocessed input, so assert that the frame was
84+
# actually rewritten: the symbolized form gains " in" and loses the raw binary path.
85+
if [[ "${output}" != *"#0"* || "${output}" != *"tail"* || "${output}" != *" in"* ||
86+
"${output}" == *"/blah/foo.so"* ]]; then
87+
echo "frame was not rewritten after the invalid byte, got:"
88+
echo "${output}"
89+
status=1
90+
fi
91+
92+
exit "${status}"

0 commit comments

Comments
 (0)