diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 000000000..059c0a65f --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[target.'cfg(all(target_arch = "x86_64", not(target_env = "musl")))'] +rustflags = ["-C", "target-cpu=x86-64-v2"] diff --git a/.dockerignore b/.dockerignore index 27780830e..898199581 100644 --- a/.dockerignore +++ b/.dockerignore @@ -6,5 +6,6 @@ !tests !resources +!.cargo !Cargo.lock !Cargo.toml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ab2691f7c..80ffd1ef5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -115,39 +115,48 @@ jobs: matrix: include: - target: x86_64-unknown-linux-gnu + runner: ubuntu-latest platform: linux/amd64 suffix: "" build_env: "" - target: x86_64-unknown-linux-musl + runner: ubuntu-latest platform: linux/amd64 suffix: "-alpine" build_env: "" - target: aarch64-unknown-linux-gnu + runner: ubuntu-24.04-arm platform: linux/arm64 suffix: "" build_env: "JEMALLOC_SYS_WITH_LG_PAGE=16 " - target: aarch64-unknown-linux-musl + runner: ubuntu-24.04-arm platform: linux/arm64 suffix: "-alpine" build_env: "JEMALLOC_SYS_WITH_LG_PAGE=16 " - target: armv7-unknown-linux-gnueabihf + runner: ubuntu-latest platform: linux/arm/v7 suffix: "" build_env: "JEMALLOC_SYS_WITH_LG_PAGE=16 " - target: armv7-unknown-linux-musleabihf + runner: ubuntu-latest platform: linux/arm/v7 suffix: "-alpine" build_env: "JEMALLOC_SYS_WITH_LG_PAGE=16 " - target: arm-unknown-linux-gnueabihf + runner: ubuntu-latest platform: linux/arm/v6 suffix: "" build_env: "" - target: arm-unknown-linux-musleabihf + runner: ubuntu-latest platform: linux/arm/v6 suffix: "-alpine" build_env: "" name: Build / ${{matrix.target}} - runs-on: ubuntu-latest + runs-on: ${{matrix.runner}} + timeout-minutes: 180 steps: - name: Checkout uses: actions/checkout@v7 @@ -163,23 +172,31 @@ jobs: - name: Add swap (heavy ARM targets) if: contains(matrix.target, 'arm') || contains(matrix.target, 'aarch64') run: | - mnt_avail=$(df --output=avail -k /mnt | tail -1) - if [ "$mnt_avail" -lt 18874368 ]; then - echo "Insufficient space on /mnt (${mnt_avail}K available), aborting swap setup" + swap_dir="" + for dir in /mnt /; do + if [ -d "$dir" ] && [ "$(df --output=avail -k "$dir" | tail -1)" -ge 18874368 ]; then + swap_dir="$dir" + break + fi + done + if [ -z "$swap_dir" ]; then + echo "Insufficient space for swap on /mnt or /, aborting swap setup" + df -h exit 1 fi - sudo fallocate -l 16G /mnt/swapfile - sudo chmod 600 /mnt/swapfile - sudo mkswap /mnt/swapfile - sudo swapon /mnt/swapfile + sudo fallocate -l 16G "${swap_dir%/}/swapfile" + sudo chmod 600 "${swap_dir%/}/swapfile" + sudo mkswap "${swap_dir%/}/swapfile" + sudo swapon "${swap_dir%/}/swapfile" sudo sysctl vm.swappiness=80 free -h swapon --show - name: Set up QEMU + if: startsWith(matrix.platform, 'linux/arm/') uses: docker/setup-qemu-action@v4 with: - platforms: "arm64,arm" + platforms: "arm" - name: Set up Docker Buildx uses: docker/setup-buildx-action@v4 @@ -198,6 +215,7 @@ jobs: password: ${{github.token}} - name: Log In to DockerHub + if: github.event_name == 'push' || inputs.Docker uses: docker/login-action@v4 with: username: ${{secrets.DOCKERHUB_USERNAME}} @@ -215,7 +233,7 @@ jobs: path: | var-cache-apt var-lib-apt - key: apt-cache-${{ hashFiles('Dockerfile.build') }} + key: apt-cache-${{ runner.arch }}-${{ hashFiles('Dockerfile.build') }} - name: Cache Cargo uses: actions/cache@v6.1.0 @@ -258,6 +276,7 @@ jobs: uses: docker/bake-action@v7 env: DOCKER_BUILD_RECORD_UPLOAD: false + GITHUB_TOKEN: ${{github.token}} TARGET: ${{matrix.target}} GHCR_REPO: ghcr.io/${{github.repository}} BUILD_ENV: ${{matrix.build_env}} diff --git a/.gitignore b/.gitignore index d6d2a48a9..1a302cb3f 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,5 @@ run.sh !.gitignore !.gitattributes !.github +!.cargo CLAUDE.md diff --git a/Cargo.lock b/Cargo.lock index 438d2bb69..966fef598 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8196,6 +8196,7 @@ dependencies = [ "http_proto", "imap", "jmap", + "libc", "managesieve", "migration", "pop3", diff --git a/Dockerfile.build b/Dockerfile.build index 0635ce703..a8a25d21a 100644 --- a/Dockerfile.build +++ b/Dockerfile.build @@ -4,7 +4,7 @@ # ***************** # Base image for planner & builder # ***************** -FROM --platform=$BUILDPLATFORM rust:1.98.1-slim-trixie AS base +FROM --platform=$BUILDPLATFORM rust:slim-trixie AS base ENV DEBIAN_FRONTEND="noninteractive" \ BINSTALL_DISABLE_TELEMETRY=true \ @@ -24,25 +24,24 @@ RUN \ echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' >/etc/apt/apt.conf.d/keep-cache && \ apt-get update && \ apt-get install -yq --no-install-recommends ca-certificates curl git jq xz-utils make libclang-19-dev -# Zig is pinned to 0.13.0. Zig 0.14+ changed how its default target ABI is -# resolved for `x86_64-linux-gnu`-style triples, and on Stalwart's dep tree -# (rocksdb, jemalloc, aws-lc-sys, libfdb_c.so, rust-std 1.87+) this surfaces -# as ld.lld undefined references to pthread_*/stat/pow@GLIBC_2.33+. An -# explicit glibc suffix (`.2.17`) is NOT a workaround — C deps compiled via -# zig cc against Zig 0.16 headers still emit @2.33+ symbols even when the -# linker target is 2.17. Zig 0.13 produces binaries with a max glibc ref of -# ~2.28 which stays compatible with Debian 11 / RHEL 8. Revisit this pin if -# Zig upstream stabilizes a backwards-compatible default or if Stalwart drops -# its hard C deps. RUN \ - ZIG_VERSION=0.13.0 && \ - curl --retry 5 -fsSL "https://ziglang.org/download/${ZIG_VERSION}/zig-linux-$(uname -m)-${ZIG_VERSION}.tar.xz" | tar -J -x -C /usr/local && \ - ln -s "/usr/local/zig-linux-$(uname -m)-${ZIG_VERSION}/zig" /usr/local/bin/zig && \ + ZIG_VERSION=0.17.0 && \ + case "$(uname -m)" in \ + x86_64) ZIG_SHA256=1cbe9df9f27e6b78d14ccbca43b6703a404ef79ef1c463de901d7f088d4e2026 ;; \ + aarch64) ZIG_SHA256=9e8d11661d4ae3bd57702a3832781e23ad151dde5798e16a5ccd503f65234ff8 ;; \ + *) exit 1 ;; \ + esac && \ + curl --retry 5 -fsSLo /tmp/zig.tar.xz "https://ziglang.org/download/${ZIG_VERSION}/zig-$(uname -m)-linux-${ZIG_VERSION}.tar.xz" && \ + echo "${ZIG_SHA256} /tmp/zig.tar.xz" | sha256sum -c - && \ + tar -J -x -C /usr/local -f /tmp/zig.tar.xz && \ + rm /tmp/zig.tar.xz && \ + ln -s "/usr/local/zig-$(uname -m)-linux-${ZIG_VERSION}/zig" /usr/local/bin/zig && \ zig version # Install cargo-binstall RUN curl --retry 5 -fL --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash # Install cargo-chef & sccache & cargo-zigbuild -RUN cargo binstall --no-confirm cargo-chef@0.1.78 sccache@0.17.0 cargo-zigbuild@0.23.2 +RUN cargo binstall --no-confirm cargo-chef@0.1.78 sccache@0.17.0 +RUN cargo install --locked --git https://github.com/rust-cross/cargo-zigbuild --rev d9f7fd32fbac13cc26d23847835f7f23a5ed3a25 cargo-zigbuild # ***************** # Planner @@ -61,6 +60,7 @@ WORKDIR /app COPY --from=planner /app/recipe.json recipe.json ARG TARGET ARG BUILD_ENV +ARG GLIBC_VERSION=2.28 SHELL ["/bin/bash", "-o", "pipefail", "-c"] # Install toolchain and specify some env variables RUN \ @@ -68,6 +68,13 @@ RUN \ rustup target add ${TARGET} && \ mkdir -p artifact && \ touch /env-cargo && \ + if [[ "${TARGET}" == *-gnu* ]]; then \ + echo "export BUILD_TARGET=${TARGET}.${GLIBC_VERSION}" >> /env-cargo; \ + mkdir -p target && \ + ln -s "${TARGET}" "target/${TARGET}.${GLIBC_VERSION}"; \ + else \ + echo "export BUILD_TARGET=${TARGET}" >> /env-cargo; \ + fi && \ if [ ! -z "${BUILD_ENV}" ]; then \ echo "export ${BUILD_ENV}" >> /env-cargo; \ echo "Setting up ${BUILD_ENV}"; \ @@ -85,15 +92,22 @@ RUN \ exit 1; \ ;; \ esac; \ + target_var="${TARGET^^}"; \ + echo "export FDB_RUSTFLAGS_VAR=CARGO_TARGET_${target_var//-/_}_RUSTFLAGS" >> /env-cargo; \ fi # Install FoundationDB (pinned to latest 7.4.x; Apple publishes 7.4 as # prereleases on GitHub, so the release list is fetched without the # prerelease filter and narrowed by tag name). ARG FDB_VERSION_RANGE="7.4" RUN \ + --mount=type=secret,id=GITHUB_TOKEN,env=GITHUB_TOKEN \ source /env-cargo && \ if [ ! -z "${FDB_ARCH}" ]; then \ - curl --retry 5 -fLso fdb-client.deb "$(curl --retry 5 -fLs 'https://api.github.com/repos/apple/foundationdb/releases?per_page=100' | jq --arg FDB_ARCH "$FDB_ARCH" --arg RANGE "${FDB_VERSION_RANGE}" -r '[.[] | select(.tag_name | startswith($RANGE + "."))] | sort_by(.tag_name | split(".") | map(tonumber)) | reverse | .[0].assets[] | select(.name | test("foundationdb-clients.*" + $FDB_ARCH + ".deb$")) | .browser_download_url')" && \ + auth=() && \ + if [ -n "${GITHUB_TOKEN}" ]; then auth=(-H "Authorization: Bearer ${GITHUB_TOKEN}"); fi && \ + fdb_url="$(curl --retry 5 -fLs "${auth[@]}" 'https://api.github.com/repos/apple/foundationdb/releases?per_page=100' | jq --arg FDB_ARCH "$FDB_ARCH" --arg RANGE "${FDB_VERSION_RANGE}" -r '[.[] | select(.tag_name | startswith($RANGE + "."))] | sort_by(.tag_name | split(".") | map(tonumber)) | reverse | .[0].assets[] | select(.name | test("foundationdb-clients.*" + $FDB_ARCH + ".deb$")) | .browser_download_url')" && \ + [ -n "${fdb_url}" ] && \ + curl --retry 5 -fLso fdb-client.deb "${fdb_url}" && \ mkdir -p /fdb && \ dpkg -x fdb-client.deb /fdb && \ mv /fdb/usr/include/foundationdb /usr/include && \ @@ -108,7 +122,7 @@ RUN \ --mount=type=cache,target=/usr/local/cargo/git \ source /env-cargo && \ if [ ! -z "${FDB_ARCH}" ]; then \ - RUSTFLAGS="-L /usr/lib" cargo chef cook --recipe-path recipe.json --zigbuild --release --target ${TARGET} -p stalwart --no-default-features --features "foundationdb s3 redis nats enterprise"; \ + env "${FDB_RUSTFLAGS_VAR}=-L /usr/lib" cargo chef cook --recipe-path recipe.json --zigbuild --release --target ${BUILD_TARGET} -p stalwart --no-default-features --features "foundationdb s3 redis nats enterprise"; \ fi RUN \ --mount=type=secret,id=ACTIONS_RESULTS_URL,env=ACTIONS_RESULTS_URL \ @@ -116,7 +130,7 @@ RUN \ --mount=type=cache,target=/usr/local/cargo/registry \ --mount=type=cache,target=/usr/local/cargo/git \ source /env-cargo && \ - cargo chef cook --recipe-path recipe.json --zigbuild --release --target ${TARGET} -p stalwart --no-default-features --features "sqlite postgres mysql rocks s3 redis azure nats enterprise" + cargo chef cook --recipe-path recipe.json --zigbuild --release --target ${BUILD_TARGET} -p stalwart --no-default-features --features "sqlite postgres mysql rocks s3 redis azure nats enterprise" # Copy the source code COPY . . ENV RUSTC_WRAPPER="sccache" \ @@ -129,7 +143,7 @@ RUN \ --mount=type=cache,target=/usr/local/cargo/git \ source /env-cargo && \ if [ ! -z "${FDB_ARCH}" ]; then \ - RUSTFLAGS="-L /usr/lib" cargo zigbuild --release --target ${TARGET} -p stalwart --no-default-features --features "foundationdb s3 redis nats enterprise" && \ + env "${FDB_RUSTFLAGS_VAR}=-L /usr/lib" cargo zigbuild --release --target ${BUILD_TARGET} -p stalwart --no-default-features --features "foundationdb s3 redis nats enterprise" && \ mv /app/target/${TARGET}/release/stalwart /app/artifact/stalwart-foundationdb; \ fi # Build generic version @@ -139,7 +153,7 @@ RUN \ --mount=type=cache,target=/usr/local/cargo/registry \ --mount=type=cache,target=/usr/local/cargo/git \ source /env-cargo && \ - cargo zigbuild --release --target ${TARGET} -p stalwart --no-default-features --features "sqlite postgres mysql rocks s3 redis azure nats enterprise" && \ + cargo zigbuild --release --target ${BUILD_TARGET} -p stalwart --no-default-features --features "sqlite postgres mysql rocks s3 redis azure nats enterprise" && \ mv /app/target/${TARGET}/release/stalwart /app/artifact/stalwart # ***************** diff --git a/crates/main/Cargo.toml b/crates/main/Cargo.toml index 8add2f252..b22656105 100644 --- a/crates/main/Cargo.toml +++ b/crates/main/Cargo.toml @@ -41,6 +41,9 @@ migration = { path = "../migration" } tokio = { version = "1.53", features = ["full"] } rustls = { version = "0.23.43", default-features = false, features = ["std", "aws_lc_rs", "tls12"] } +[target.'cfg(all(target_arch = "x86_64", any(target_os = "linux", target_os = "freebsd")))'.dependencies] +libc = "0.2.189" + [target.'cfg(not(any(target_env = "msvc", target_os = "freebsd")))'.dependencies] tikv-jemallocator = "0.7" diff --git a/crates/main/src/cpu.rs b/crates/main/src/cpu.rs new file mode 100644 index 000000000..3c3ec5f80 --- /dev/null +++ b/crates/main/src/cpu.rs @@ -0,0 +1,51 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use std::arch::x86_64::__cpuid; + +const LEAF_FEATURES: u32 = 0x0000_0001; +const LEAF_EXTENDED_MAX: u32 = 0x8000_0000; +const LEAF_EXTENDED_FEATURES: u32 = 0x8000_0001; + +const SSE3: u32 = 1 << 0; +const SSSE3: u32 = 1 << 9; +const CMPXCHG16B: u32 = 1 << 13; +const SSE4_1: u32 = 1 << 19; +const SSE4_2: u32 = 1 << 20; +const POPCNT: u32 = 1 << 23; +const LAHF_SAHF: u32 = 1 << 0; + +const FEATURES_X86_64_V2: u32 = SSE3 | SSSE3 | CMPXCHG16B | SSE4_1 | SSE4_2 | POPCNT; +const EXTENDED_FEATURES_X86_64_V2: u32 = LAHF_SAHF; + +const UNSUPPORTED_CPU: &[u8] = b"This build of Stalwart requires an x86-64-v2 compatible CPU (SSE3, SSSE3, SSE4.1, SSE4.2, POPCNT, CMPXCHG16B and LAHF/SAHF). If Stalwart runs in a virtual machine, set its CPU type to 'host' or 'x86-64-v2'.\n"; +const EXIT_UNSUPPORTED_CPU: i32 = 1; + +#[used] +#[unsafe(link_section = ".init_array.00001")] +static ASSERT_X86_64_V2: extern "C" fn() = assert_x86_64_v2; + +extern "C" fn assert_x86_64_v2() { + let features = __cpuid(LEAF_FEATURES).ecx; + let extended_features = if __cpuid(LEAF_EXTENDED_MAX).eax >= LEAF_EXTENDED_FEATURES { + __cpuid(LEAF_EXTENDED_FEATURES).ecx + } else { + 0 + }; + + if features & FEATURES_X86_64_V2 != FEATURES_X86_64_V2 + || extended_features & EXTENDED_FEATURES_X86_64_V2 != EXTENDED_FEATURES_X86_64_V2 + { + unsafe { + libc::write( + libc::STDERR_FILENO, + UNSUPPORTED_CPU.as_ptr().cast(), + UNSUPPORTED_CPU.len(), + ); + libc::_exit(EXIT_UNSUPPORTED_CPU); + } + } +} diff --git a/crates/main/src/main.rs b/crates/main/src/main.rs index db0a4c049..a3afbc5bc 100644 --- a/crates/main/src/main.rs +++ b/crates/main/src/main.rs @@ -20,6 +20,12 @@ use std::time::Duration; use trc::Collector; use utils::wait_for_shutdown; +#[cfg(all( + target_arch = "x86_64", + target_feature = "sse4.2", + any(target_os = "linux", target_os = "freebsd") +))] +mod cpu; #[cfg(feature = "dev_mode")] pub mod test_data; diff --git a/docker-bake.hcl b/docker-bake.hcl index ec490a35b..2cc42fe52 100644 --- a/docker-bake.hcl +++ b/docker-bake.hcl @@ -17,7 +17,8 @@ target "docker-metadata-action" {} target "build" { secret = [ "type=env,id=ACTIONS_RESULTS_URL", - "type=env,id=ACTIONS_RUNTIME_TOKEN" + "type=env,id=ACTIONS_RUNTIME_TOKEN", + "type=env,id=GITHUB_TOKEN" ] args = { TARGET = "${TARGET}"