diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..7eea563 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,20 @@ +** + +!.dockerignore +!docker/ +!docker/linux-build/ +!docker/linux-build/** + +# Dependency manifests and local xmake package recipes are the only project +# files needed while pre-warming the build image. +!xmake.lua +!xmake/ +!xmake/** +!thirdparty/ +!thirdparty/** +!submodules/ +!submodules/xmake.lua +!submodules/minirtc/ +!submodules/minirtc/xmake.lua +!submodules/minirtc/thirdparty/ +!submodules/minirtc/thirdparty/** diff --git a/.github/workflows/build-linux-image.yml b/.github/workflows/build-linux-image.yml new file mode 100644 index 0000000..4245cd2 --- /dev/null +++ b/.github/workflows/build-linux-image.yml @@ -0,0 +1,155 @@ +name: Build Linux Build Image + +on: + push: + branches: + - ci/linux-build-image + paths: + - .dockerignore + - .gitmodules + - .github/workflows/build-linux-image.yml + - docker/linux-build/** + - xmake.lua + - xmake/** + - thirdparty/** + - submodules/xmake.lua + - submodules/minirtc + - submodules/minirtc/xmake.lua + - submodules/minirtc/thirdparty/** + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: linux-build-image-${{ github.ref }} + cancel-in-progress: true + +env: + IMAGE_NAME: crossdesk/ubuntu22.04 + STACK_TAG: buildenv-cuda12.6.3-xmake3.0.9-rust1.92.0 + XMAKE_VERSION: 3.0.9 + RUST_VERSION: 1.92.0 + RUSTUP_VERSION: 1.28.2 + +jobs: + build: + name: Build candidate (${{ matrix.arch }}) + runs-on: ${{ matrix.runner }} + timeout-minutes: 180 + strategy: + fail-fast: false + matrix: + include: + - arch: amd64 + platform: linux/amd64 + runner: ubuntu-24.04 + base_image: nvidia/cuda:12.6.3-devel-ubuntu22.04 + - arch: arm64 + platform: linux/arm64 + runner: ubuntu-24.04-arm + base_image: ubuntu:22.04 + steps: + - name: Set candidate tag + id: image + run: | + CANDIDATE_TAG="${IMAGE_NAME}:${STACK_TAG}-${GITHUB_SHA}-${{ matrix.arch }}" + echo "CANDIDATE_TAG=${CANDIDATE_TAG}" >> "${GITHUB_ENV}" + echo "candidate=${CANDIDATE_TAG}" >> "${GITHUB_OUTPUT}" + + - name: Checkout source + uses: actions/checkout@v5 + with: + submodules: recursive + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Log in to Docker Hub + uses: docker/login-action@v4 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and push candidate + uses: docker/build-push-action@v7 + with: + context: . + file: docker/linux-build/Dockerfile + platforms: ${{ matrix.platform }} + push: true + pull: true + tags: ${{ steps.image.outputs.candidate }} + cache-from: type=registry,ref=${{ env.IMAGE_NAME }}:${{ env.STACK_TAG }} + cache-to: type=inline + provenance: false + sbom: false + build-args: | + BASE_IMAGE=${{ matrix.base_image }} + XMAKE_VERSION=${{ env.XMAKE_VERSION }} + RUST_VERSION=${{ env.RUST_VERSION }} + RUSTUP_VERSION=${{ env.RUSTUP_VERSION }} + + verify: + name: Verify candidate (${{ matrix.arch }}) + needs: build + runs-on: ${{ matrix.runner }} + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + include: + - arch: amd64 + runner: ubuntu-24.04 + - arch: arm64 + runner: ubuntu-24.04-arm + steps: + - name: Set candidate tag + run: | + CANDIDATE_TAG="${IMAGE_NAME}:${STACK_TAG}-${GITHUB_SHA}-${{ matrix.arch }}" + echo "CANDIDATE_TAG=${CANDIDATE_TAG}" >> "${GITHUB_ENV}" + + - name: Log in to Docker Hub + uses: docker/login-action@v4 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Verify tools, architecture, and Ubuntu baseline + run: | + docker pull "${CANDIDATE_TAG}" + docker run --rm \ + --env EXPECTED_ARCH="${{ matrix.arch }}" \ + "${CANDIDATE_TAG}" \ + verify-crossdesk-build-image + + publish: + name: Publish multi-arch image + needs: verify + runs-on: ubuntu-24.04 + timeout-minutes: 20 + steps: + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Log in to Docker Hub + uses: docker/login-action@v4 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Promote verified candidates + run: | + AMD64_IMAGE="${IMAGE_NAME}:${STACK_TAG}-${GITHUB_SHA}-amd64" + ARM64_IMAGE="${IMAGE_NAME}:${STACK_TAG}-${GITHUB_SHA}-arm64" + + docker buildx imagetools create \ + --tag "${IMAGE_NAME}:${STACK_TAG}" \ + --tag "${IMAGE_NAME}:sha-${GITHUB_SHA}" \ + --tag "${IMAGE_NAME}:latest" \ + "${AMD64_IMAGE}" \ + "${ARM64_IMAGE}" + + - name: Inspect published manifest + run: | + docker buildx imagetools inspect "${IMAGE_NAME}:${STACK_TAG}" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cbcb0e6..bb3eb49 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,6 +4,7 @@ on: push: branches: - "**" + - "!ci/linux-build-image" tags: - "*" workflow_dispatch: @@ -27,13 +28,13 @@ jobs: matrix: include: - arch: amd64 - runner: ubuntu-22.04 - image: crossdesk/ubuntu20.04:latest + runner: ubuntu-24.04 + image: crossdesk/ubuntu22.04:buildenv-cuda12.6.3-xmake3.0.9-rust1.92.0 package_script: ./scripts/linux/pkg_amd64.sh - arch: arm64 - runner: ubuntu-22.04-arm - image: crossdesk/ubuntu20.04-arm64v8:latest + runner: ubuntu-24.04-arm + image: crossdesk/ubuntu22.04:buildenv-cuda12.6.3-xmake3.0.9-rust1.92.0 package_script: ./scripts/linux/pkg_arm64.sh container: @@ -99,12 +100,14 @@ jobs: with: submodules: recursive + - name: Verify Linux build image + run: verify-crossdesk-build-image + - name: Build CrossDesk env: CUDA_PATH: /usr/local/cuda XMAKE_GLOBALDIR: /data run: | - apt install -y libxft-dev xmake f --CROSSDESK_VERSION=${LEGAL_VERSION} --USE_CUDA=true --root -y xmake b -vy --root crossdesk @@ -113,6 +116,14 @@ jobs: chmod +x ${{ matrix.package_script }} ${{ matrix.package_script }} ${LEGAL_VERSION} + - name: Verify packaged Ubuntu 22.04 glibc baseline + run: | + PACKAGE_FILE="${GITHUB_WORKSPACE}/crossdesk-linux-${{ matrix.arch }}-${LEGAL_VERSION}.deb" + VERIFY_DIR="${RUNNER_TEMP}/crossdesk-glibc-${{ matrix.arch }}" + install -d "${VERIFY_DIR}" + dpkg-deb --extract "${PACKAGE_FILE}" "${VERIFY_DIR}" + verify-crossdesk-glibc-baseline "${VERIFY_DIR}" 2.35 + - name: Upload artifact uses: actions/upload-artifact@v6 with: @@ -123,17 +134,21 @@ jobs: build-macos: name: Build on macOS runs-on: ${{ matrix.runner }} + env: + MACOSX_DEPLOYMENT_TARGET: "14.0" strategy: matrix: include: - arch: x64 + binary_arch: x86_64 runner: macos-15-intel - cache-key: intel + cache-key: intel-macos14 out-dir: ./build/macosx/x86_64/release/crossdesk package_script: ./scripts/macosx/pkg_x64.sh - arch: arm64 + binary_arch: arm64 runner: macos-14 - cache-key: arm + cache-key: arm-macos14 out-dir: ./build/macosx/arm64/release/crossdesk package_script: ./scripts/macosx/pkg_arm64.sh @@ -189,6 +204,9 @@ jobs: - name: Install xmake run: brew install xmake + - name: Update xmake repositories + run: xmake repo -u + - name: Checkout code uses: actions/checkout@v5 @@ -197,7 +215,7 @@ jobs: - name: Build CrossDesk run: | - xmake f --CROSSDESK_VERSION=${VERSION_NUM} --USE_CUDA=true -y + xmake f --target_minver=${MACOSX_DEPLOYMENT_TARGET} --CROSSDESK_VERSION=${VERSION_NUM} --USE_CUDA=true -y xmake b -vy crossdesk - name: Package CrossDesk app @@ -205,6 +223,22 @@ jobs: chmod +x ${{ matrix.package_script }} ${{ matrix.package_script }} ${VERSION_NUM} + - name: Verify packaged app + shell: bash + run: | + PACKAGE_FILE="${GITHUB_WORKSPACE}/crossdesk-macos-${{ matrix.arch }}-${VERSION_NUM}.pkg" + VERIFY_DIR="$(mktemp -d "${RUNNER_TEMP}/crossdesk-macos-pkg.XXXXXX")" + trap 'rm -rf "${VERIFY_DIR}"' EXIT + + pkgutil --expand-full "${PACKAGE_FILE}" "${VERIFY_DIR}/expanded" + APP_PATH="$(find "${VERIFY_DIR}/expanded" -type d -name 'CrossDesk.app' -print -quit)" + if [[ -z "${APP_PATH}" ]]; then + echo "CrossDesk.app is missing from ${PACKAGE_FILE}" >&2 + exit 1 + fi + + ./scripts/macosx/verify_app.sh "${APP_PATH}" "${{ matrix.binary_arch }}" + - name: Upload build artifacts uses: actions/upload-artifact@v6 with: @@ -275,6 +309,7 @@ jobs: run: | Invoke-Expression (Invoke-Webrequest 'https://raw.githubusercontent.com/tboox/xmake/master/scripts/get.ps1' -UseBasicParsing).Content echo "C:\Users\runneradmin\xmake" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + xmake repo -u xmake create cuda Set-Location cuda xmake g --theme=plain diff --git a/docker/linux-build/Dockerfile b/docker/linux-build/Dockerfile new file mode 100644 index 0000000..e10c342 --- /dev/null +++ b/docker/linux-build/Dockerfile @@ -0,0 +1,153 @@ +# syntax=docker/dockerfile:1.7 + +ARG BASE_IMAGE=nvidia/cuda:12.6.3-devel-ubuntu22.04 +FROM ${BASE_IMAGE} + +ARG TARGETARCH +ARG XMAKE_VERSION=3.0.9 +ARG RUST_VERSION=1.92.0 +ARG RUSTUP_VERSION=1.28.2 + +LABEL org.opencontainers.image.title="CrossDesk Linux build environment" \ + org.opencontainers.image.description="Ubuntu 22.04 build environment for CrossDesk" \ + org.opencontainers.image.source="https://github.com/kunkundi/crossdesk" \ + org.opencontainers.image.licenses="GPL-3.0-only" + +ENV DEBIAN_FRONTEND=noninteractive \ + TZ=Etc/UTC \ + LANG=C.UTF-8 \ + LC_ALL=C.UTF-8 \ + RUSTUP_HOME=/opt/rustup \ + CARGO_HOME=/opt/cargo \ + XMAKE_GLOBALDIR=/data \ + XMAKE_ROOT=y \ + CUDA_PATH=/usr/local/cuda \ + PATH=/root/.local/bin:/opt/cargo/bin:/usr/local/bin:${PATH} + +SHELL ["/bin/bash", "-o", "pipefail", "-c"] + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + autoconf \ + automake \ + binutils \ + bison \ + build-essential \ + ca-certificates \ + ccache \ + cmake \ + curl \ + dbus \ + dpkg-dev \ + file \ + flex \ + gettext \ + git \ + libasound2-dev \ + libdbus-1-dev \ + libdrm-dev \ + libegl1-mesa-dev \ + libffi-dev \ + libfontconfig1-dev \ + libfreetype6-dev \ + libgl1-mesa-dev \ + libmount-dev \ + libpulse-dev \ + libreadline-dev \ + libsndio-dev \ + libssl-dev \ + libtool \ + libx11-dev \ + libxcb-xfixes0-dev \ + libxcb-randr0-dev \ + libxcb-shape0-dev \ + libxcb-shm0-dev \ + libxcb-xinerama0-dev \ + libxcb-xkb-dev \ + libxcb-xtest0-dev \ + libxcursor-dev \ + libxext-dev \ + libxfixes-dev \ + libxft-dev \ + libxi-dev \ + libxinerama-dev \ + libxkbcommon-dev \ + libxrandr-dev \ + libxrender-dev \ + libxtst-dev \ + libxv-dev \ + make \ + nasm \ + ninja-build \ + patch \ + patchelf \ + perl \ + pkg-config \ + python3 \ + python3-pip \ + unzip \ + xutils-dev \ + xz-utils \ + yasm \ + zip \ + zlib1g-dev \ + && rm -rf /var/lib/apt/lists/* + +RUN export GITHUB_ACTIONS=true \ + && curl -fsSL https://xmake.io/shget.text \ + | bash -s -- "v${XMAKE_VERSION}" \ + && source ~/.xmake/profile \ + && xmake --version | grep -F "v${XMAKE_VERSION}" + +RUN case "${TARGETARCH}" in \ + amd64) \ + rustup_arch="x86_64-unknown-linux-gnu"; \ + rustup_sha256="20a06e644b0d9bd2fbdbfd52d42540bdde820ea7df86e92e533c073da0cdd43c" \ + ;; \ + arm64) \ + rustup_arch="aarch64-unknown-linux-gnu"; \ + rustup_sha256="e3853c5a252fca15252d07cb23a1bdd9377a8c6f3efa01531109281ae47f841c" \ + ;; \ + *) echo "Unsupported architecture: ${TARGETARCH}" >&2; exit 1 ;; \ + esac \ + && curl --proto '=https' --tlsv1.2 --fail --show-error --location \ + "https://static.rust-lang.org/rustup/archive/${RUSTUP_VERSION}/${rustup_arch}/rustup-init" \ + -o /tmp/rustup-init \ + && echo "${rustup_sha256} /tmp/rustup-init" | sha256sum --check - \ + && chmod 0755 /tmp/rustup-init \ + && /tmp/rustup-init -y --no-modify-path --profile minimal \ + --default-toolchain "${RUST_VERSION}" \ + && rm -f /tmp/rustup-init \ + && rustc --version \ + && cargo --version + +WORKDIR /tmp/crossdesk-dependencies + +COPY xmake.lua ./xmake.lua +COPY xmake ./xmake +COPY thirdparty ./thirdparty +COPY submodules/xmake.lua ./submodules/xmake.lua +COPY submodules/minirtc/xmake.lua ./submodules/minirtc/xmake.lua +COPY submodules/minirtc/thirdparty ./submodules/minirtc/thirdparty + +# Configure the real project manifests so the image cache stays aligned with +# CrossDesk's exact dependency graph. Source files are intentionally excluded +# from the Docker context; source-only changes therefore reuse this layer. +RUN xmake repo -u --root \ + && xmake f -m release --USE_CUDA=true --root -y \ + && rm -rf /data/.xmake/cache /tmp/crossdesk-dependencies \ + && mkdir -p /workspace + +WORKDIR /workspace + +COPY docker/linux-build/verify-build-image.sh \ + /usr/local/bin/verify-crossdesk-build-image +COPY docker/linux-build/verify-glibc-baseline.sh \ + /usr/local/bin/verify-crossdesk-glibc-baseline + +RUN chmod 0755 \ + /usr/local/bin/verify-crossdesk-build-image \ + /usr/local/bin/verify-crossdesk-glibc-baseline \ + && verify-crossdesk-build-image + +CMD ["bash"] diff --git a/docker/linux-build/README.md b/docker/linux-build/README.md new file mode 100644 index 0000000..a7d5a0d --- /dev/null +++ b/docker/linux-build/README.md @@ -0,0 +1,41 @@ +# CrossDesk Linux 构建镜像 + +这个目录维护 CrossDesk 专用的 Ubuntu 22.04 构建环境。镜像包含 xmake、 +Rust、Linux 开发库以及根据项目 `xmake.lua` 提前编译好的依赖。 + +## 发布方式 + +推送影响镜像的文件到 `ci/linux-build-image` 分支时, +`build-linux-image.yml` 会在 GitHub 原生 amd64/arm64 runner 上分别构建和验证, +全部通过后再合并为同一个 multi-arch 镜像: + +```text +crossdesk/ubuntu22.04:buildenv-cuda12.6.3-xmake3.0.9-rust1.92.0 +``` + +Docker 会根据运行机器自动选择正确架构。amd64 变体以 +`nvidia/cuda:12.6.3-devel-ubuntu22.04` 为基础;arm64 变体不包含当前构建 +用不到的 CUDA SDK。两个变体都使用 Ubuntu 22.04/glibc 2.35。 +因此该镜像生成的 Linux 安装包最低支持 Ubuntu 22.04,不再保证兼容 +Ubuntu 20.04。 + +每次成功发布还会生成 `sha-<完整提交 SHA>`,用于精确回滚;`latest` 和上面的 +工具链版本 tag 只会在两个架构都验证成功后更新。 + +## GitHub 配置 + +在仓库的 Actions secrets 中配置: + +- `DOCKERHUB_USERNAME`:有权推送 `crossdesk/ubuntu22.04` 的 Docker Hub 用户名 +- `DOCKERHUB_TOKEN`:该用户的 Docker Hub access token + +工作流文件进入默认分支后,也可以从 Actions 页面手动运行。为了避免首次发布 +新 tag 时应用构建先于镜像完成,常规 `build.yml` 不响应 +`ci/linux-build-image` 分支的 push。第一次启用时,应先推送该分支并等待镜像 +发布成功,再把应用构建工作流的修改合并到 `main`。 + +## 更新依赖 + +修改 Dockerfile、xmake 清单、本地包配方或子模块中的包配方后,推送该分支即可。 +升级工具链时应同时修改 Dockerfile 的版本参数、工作流中的 `STACK_TAG`,以及 +常规构建工作流引用的镜像 tag。 diff --git a/docker/linux-build/verify-build-image.sh b/docker/linux-build/verify-build-image.sh new file mode 100644 index 0000000..c5a20b2 --- /dev/null +++ b/docker/linux-build/verify-build-image.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +set -euo pipefail + +EXPECTED_ARCH="${EXPECTED_ARCH:-}" +EXPECTED_XMAKE_VERSION="${EXPECTED_XMAKE_VERSION:-3.0.9}" +EXPECTED_RUST_VERSION="${EXPECTED_RUST_VERSION:-1.92.0}" + +# shellcheck disable=SC1091 +source /etc/os-release +if [[ "${ID}" != "ubuntu" || "${VERSION_ID}" != "22.04" ]]; then + echo "Expected Ubuntu 22.04, found ${ID:-unknown} ${VERSION_ID:-unknown}" >&2 + exit 1 +fi + +GLIBC_VERSION="$(getconf GNU_LIBC_VERSION | awk '{print $2}')" +if [[ "${GLIBC_VERSION}" != "2.35" ]]; then + echo "Expected glibc 2.35, found ${GLIBC_VERSION}" >&2 + exit 1 +fi + +ACTUAL_ARCH="$(dpkg --print-architecture)" +if [[ -n "${EXPECTED_ARCH}" && "${ACTUAL_ARCH}" != "${EXPECTED_ARCH}" ]]; then + echo "Expected architecture ${EXPECTED_ARCH}, found ${ACTUAL_ARCH}" >&2 + exit 1 +fi + +XMAKE_VERSION_OUTPUT="$(xmake --version 2>&1)" +grep -F "v${EXPECTED_XMAKE_VERSION}" <<<"${XMAKE_VERSION_OUTPUT}" >/dev/null +rustc --version | grep -F "rustc ${EXPECTED_RUST_VERSION}" >/dev/null +cargo --version >/dev/null +cmake --version >/dev/null +g++ --version >/dev/null +pkg-config --exists xft + +for package_path in \ + "s/slint" \ + "l/libdatachannel" \ + "o/openssl3" \ + "s/spdlog"; do + if [[ ! -d "${XMAKE_GLOBALDIR}/.xmake/packages/${package_path}" ]]; then + echo "Prebuilt xmake package is missing: ${package_path}" >&2 + exit 1 + fi +done + +if ! find "${XMAKE_GLOBALDIR}/.xmake/packages/s/slint" \ + -type f -name 'slint-compiler*' -print -quit | grep -q .; then + echo "The prebuilt Slint compiler is missing" >&2 + exit 1 +fi + +if [[ "${ACTUAL_ARCH}" == "amd64" && ! -f "${CUDA_PATH}/include/cuda.h" ]]; then + echo "The amd64 image is missing CUDA headers under ${CUDA_PATH}" >&2 + exit 1 +fi + +echo "Verified CrossDesk build image: Ubuntu ${VERSION_ID}, ${ACTUAL_ARCH}, glibc ${GLIBC_VERSION}" diff --git a/docker/linux-build/verify-glibc-baseline.sh b/docker/linux-build/verify-glibc-baseline.sh new file mode 100644 index 0000000..17ffd64 --- /dev/null +++ b/docker/linux-build/verify-glibc-baseline.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [[ $# -lt 1 || $# -gt 2 ]]; then + echo "Usage: $0 [maximum-glibc-version]" >&2 + exit 2 +fi + +INPUT_PATH="$1" +MAXIMUM_VERSION="${2:-2.35}" + +verify_elf() { + local elf_file="$1" + local version="" + local highest_version="none" + local -a required_versions=() + + mapfile -t required_versions < <( + readelf --version-info "${elf_file}" \ + | grep -oE 'GLIBC_[0-9]+(\.[0-9]+)+' \ + | sed 's/^GLIBC_//' \ + | sort -Vu \ + || true + ) + + for version in "${required_versions[@]}"; do + if dpkg --compare-versions "${version}" gt "${MAXIMUM_VERSION}"; then + echo "${elf_file} requires GLIBC_${version}; maximum allowed is GLIBC_${MAXIMUM_VERSION}" >&2 + return 1 + fi + done + + if [[ ${#required_versions[@]} -gt 0 ]]; then + highest_version="${required_versions[-1]}" + fi + echo "Verified ${elf_file}: highest required glibc version is ${highest_version}" +} + +if [[ -f "${INPUT_PATH}" ]]; then + if ! readelf -h "${INPUT_PATH}" >/dev/null 2>&1; then + echo "Not an ELF file: ${INPUT_PATH}" >&2 + exit 1 + fi + verify_elf "${INPUT_PATH}" + exit 0 +fi + +if [[ ! -d "${INPUT_PATH}" ]]; then + echo "File or directory does not exist: ${INPUT_PATH}" >&2 + exit 1 +fi + +ELF_COUNT=0 +while IFS= read -r -d '' candidate; do + if readelf -h "${candidate}" >/dev/null 2>&1; then + verify_elf "${candidate}" + ELF_COUNT=$((ELF_COUNT + 1)) + fi +done < <(find "${INPUT_PATH}" -type f -print0) + +if [[ ${ELF_COUNT} -eq 0 ]]; then + echo "No ELF files found under ${INPUT_PATH}" >&2 + exit 1 +fi diff --git a/scripts/linux/package_deb.sh b/scripts/linux/package_deb.sh index c067281..9b804cd 100755 --- a/scripts/linux/package_deb.sh +++ b/scripts/linux/package_deb.sh @@ -147,7 +147,7 @@ Version: $DEB_VERSION Architecture: $DEBIAN_ARCH Maintainer: $MAINTAINER Description: $DESCRIPTION -Depends: libc6 (>= 2.29), libstdc++6 (>= 9), libx11-6, libxext6, +Depends: libc6 (>= 2.35), libstdc++6 (>= 9), libx11-6, libxext6, libxrender1, libxft2, libxrandr2, libxfixes3, libxcb1, libxcb-randr0, libxcb-xtest0, libxcb-xinerama0, libxcb-shape0, libxcb-xkb1, libxcb-xfixes0, libxv1, libxtst6, $ALSA_RUNTIME_DEP, libsndio7.0, diff --git a/scripts/macosx/bundle_app.sh b/scripts/macosx/bundle_app.sh new file mode 100755 index 0000000..d0cdee9 --- /dev/null +++ b/scripts/macosx/bundle_app.sh @@ -0,0 +1,131 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [[ $# -ne 2 ]]; then + echo "Usage: $0 " >&2 + exit 2 +fi + +APP_BUNDLE="$1" +EXPECTED_ARCH="$2" +APP_EXECUTABLE="$APP_BUNDLE/Contents/MacOS/CrossDesk" +FRAMEWORKS_DIR="$APP_BUNDLE/Contents/Frameworks" + +for command_name in codesign install_name_tool lipo otool; do + if ! command -v "$command_name" >/dev/null 2>&1; then + echo "Required bundling command is missing: $command_name" >&2 + exit 1 + fi +done + +if [[ ! -x "$APP_EXECUTABLE" ]]; then + echo "Missing app executable: $APP_EXECUTABLE" >&2 + exit 1 +fi + +if ! lipo "$APP_EXECUTABLE" -verify_arch "$EXPECTED_ARCH" >/dev/null 2>&1; then + echo "App executable does not contain the expected architecture: $EXPECTED_ARCH" >&2 + lipo -archs "$APP_EXECUTABLE" >&2 || true + exit 1 +fi + +SLINT_REFERENCE="$(otool -L "$APP_EXECUTABLE" | awk \ + '$1 ~ /(^|\/)libslint_cpp[^\/[:space:]]*\.dylib$/ { print $1; exit }')" +if [[ -z "$SLINT_REFERENCE" ]]; then + echo "The app executable does not reference the Slint runtime" >&2 + exit 1 +fi + +SLINT_NAME="${SLINT_REFERENCE##*/}" +SLINT_CURRENT_VERSION="$(otool -L "$APP_EXECUTABLE" | awk -v dependency="$SLINT_REFERENCE" ' + $1 == dependency { + for (i = 1; i <= NF; ++i) { + if ($i == "current") { + version = $(i + 2) + gsub(/[),]/, "", version) + print version + exit + } + } + }')" + +slint_runtime_version() { + local library="$1" + otool -L "$library" | awk -v name="$SLINT_NAME" ' + index($1, "/" name) || $1 == "@rpath/" name { + for (i = 1; i <= NF; ++i) { + if ($i == "current") { + version = $(i + 2) + gsub(/[),]/, "", version) + print version + exit + } + } + }' +} + +find_slint_runtime() { + local candidate="" + local candidate_version="" + local package_root="" + local package_roots=() + + if [[ "$SLINT_REFERENCE" == /* && -f "$SLINT_REFERENCE" ]]; then + if lipo "$SLINT_REFERENCE" -verify_arch "$EXPECTED_ARCH" >/dev/null 2>&1; then + echo "$SLINT_REFERENCE" + return 0 + fi + fi + + if [[ -n "${XMAKE_GLOBALDIR:-}" ]]; then + package_roots+=("$XMAKE_GLOBALDIR/.xmake/packages") + fi + package_roots+=("$HOME/.xmake/packages" "/data/.xmake/packages") + + for package_root in "${package_roots[@]}"; do + [[ -d "$package_root/s/slint" ]] || continue + while IFS= read -r candidate; do + if ! lipo "$candidate" -verify_arch "$EXPECTED_ARCH" >/dev/null 2>&1; then + continue + fi + + candidate_version="$(slint_runtime_version "$candidate")" + if [[ -n "$SLINT_CURRENT_VERSION" && "$candidate_version" != "$SLINT_CURRENT_VERSION" ]]; then + continue + fi + + echo "$candidate" + return 0 + done < <(find "$package_root/s/slint" -type f -path "*/lib/$SLINT_NAME" -print 2>/dev/null) + done + + return 1 +} + +SLINT_LIBRARY="$(find_slint_runtime || true)" +if [[ -z "$SLINT_LIBRARY" ]]; then + echo "Unable to locate $SLINT_NAME for $EXPECTED_ARCH (current version $SLINT_CURRENT_VERSION)" >&2 + exit 1 +fi + +install -d "$FRAMEWORKS_DIR" +install -m 0755 "$SLINT_LIBRARY" "$FRAMEWORKS_DIR/$SLINT_NAME" + +install_name_tool -id "@rpath/$SLINT_NAME" "$FRAMEWORKS_DIR/$SLINT_NAME" +if [[ "$SLINT_REFERENCE" != "@rpath/$SLINT_NAME" ]]; then + install_name_tool -change "$SLINT_REFERENCE" "@rpath/$SLINT_NAME" "$APP_EXECUTABLE" +fi + +if ! otool -l "$APP_EXECUTABLE" | awk ' + $1 == "cmd" { in_rpath = ($2 == "LC_RPATH"); next } + in_rpath && $1 == "path" && $2 == "@executable_path/../Frameworks" { found = 1 } + END { exit(found ? 0 : 1) }'; then + install_name_tool -add_rpath "@executable_path/../Frameworks" "$APP_EXECUTABLE" +fi + +# install_name_tool invalidates the linker's ad-hoc signature. Sign nested code +# first, then seal the complete app bundle so local packages remain launchable. +codesign --force --sign - "$FRAMEWORKS_DIR/$SLINT_NAME" +codesign --force --deep --sign - "$APP_BUNDLE" + +echo "Bundled Slint runtime: $SLINT_LIBRARY" diff --git a/scripts/macosx/pkg_arm64.sh b/scripts/macosx/pkg_arm64.sh index edf3eeb..debaf24 100755 --- a/scripts/macosx/pkg_arm64.sh +++ b/scripts/macosx/pkg_arm64.sh @@ -1,14 +1,16 @@ -#!/bin/bash -set -e +#!/usr/bin/env bash +set -euo pipefail APP_NAME="crossdesk" APP_NAME_UPPER="CrossDesk" EXECUTABLE_PATH="./build/macosx/arm64/release/crossdesk" PLATFORM="macos" ARCH="arm64" +BINARY_ARCH="arm64" IDENTIFIER="cn.crossdesk.app" ICON_PATH="icons/macos/crossdesk.icns" -MACOS_MIN_VERSION="10.12" +MACOS_MIN_VERSION="14.0" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" normalize_app_version() { local input="$1" @@ -95,6 +97,9 @@ EOF xattr -cr "${APP_BUNDLE}" 2>/dev/null || true find "${APP_BUNDLE}" -name '._*' -delete +"${SCRIPT_DIR}/bundle_app.sh" "${APP_BUNDLE}" "${BINARY_ARCH}" +"${SCRIPT_DIR}/verify_app.sh" "${APP_BUNDLE}" "${BINARY_ARCH}" + echo ".app created successfully." mkdir -p build_pkg_scripts diff --git a/scripts/macosx/pkg_x64.sh b/scripts/macosx/pkg_x64.sh index 0d4babc..79fd4a5 100755 --- a/scripts/macosx/pkg_x64.sh +++ b/scripts/macosx/pkg_x64.sh @@ -1,14 +1,16 @@ -#!/bin/bash -set -e +#!/usr/bin/env bash +set -euo pipefail APP_NAME="crossdesk" APP_NAME_UPPER="CrossDesk" EXECUTABLE_PATH="build/macosx/x86_64/release/crossdesk" PLATFORM="macos" ARCH="x64" +BINARY_ARCH="x86_64" IDENTIFIER="cn.crossdesk.app" ICON_PATH="icons/macos/crossdesk.icns" -MACOS_MIN_VERSION="10.12" +MACOS_MIN_VERSION="14.0" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" normalize_app_version() { local input="$1" @@ -95,6 +97,9 @@ EOF xattr -cr "${APP_BUNDLE}" 2>/dev/null || true find "${APP_BUNDLE}" -name '._*' -delete +"${SCRIPT_DIR}/bundle_app.sh" "${APP_BUNDLE}" "${BINARY_ARCH}" +"${SCRIPT_DIR}/verify_app.sh" "${APP_BUNDLE}" "${BINARY_ARCH}" + echo ".app created successfully." mkdir -p build_pkg_scripts diff --git a/scripts/macosx/verify_app.sh b/scripts/macosx/verify_app.sh new file mode 100755 index 0000000..c8c39fe --- /dev/null +++ b/scripts/macosx/verify_app.sh @@ -0,0 +1,99 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [[ $# -ne 2 ]]; then + echo "Usage: $0 " >&2 + exit 2 +fi + +APP_BUNDLE="$1" +EXPECTED_ARCH="$2" +APP_EXECUTABLE="$APP_BUNDLE/Contents/MacOS/CrossDesk" +FRAMEWORKS_DIR="$APP_BUNDLE/Contents/Frameworks" +INFO_PLIST="$APP_BUNDLE/Contents/Info.plist" + +if [[ ! -x "$APP_EXECUTABLE" ]]; then + echo "Missing app executable: $APP_EXECUTABLE" >&2 + exit 1 +fi + +if ! lipo "$APP_EXECUTABLE" -verify_arch "$EXPECTED_ARCH" >/dev/null 2>&1; then + echo "Unexpected app architecture; expected $EXPECTED_ARCH" >&2 + lipo -archs "$APP_EXECUTABLE" >&2 || true + exit 1 +fi + +if ! otool -l "$APP_EXECUTABLE" | awk ' + $1 == "cmd" { in_rpath = ($2 == "LC_RPATH"); next } + in_rpath && $1 == "path" && $2 == "@executable_path/../Frameworks" { found = 1 } + END { exit(found ? 0 : 1) }'; then + echo "Missing app rpath: @executable_path/../Frameworks" >&2 + exit 1 +fi + +verify_dependencies() { + local macho="$1" + local dependency="" + local relative_path="" + + while IFS= read -r dependency; do + case "$dependency" in + /System/Library/*|/usr/lib/*) + ;; + @rpath/*) + relative_path="${dependency#@rpath/}" + if [[ ! -f "$FRAMEWORKS_DIR/$relative_path" ]]; then + echo "Unbundled dependency in $macho: $dependency" >&2 + return 1 + fi + ;; + @executable_path/../Frameworks/*) + relative_path="${dependency#@executable_path/../Frameworks/}" + if [[ ! -f "$FRAMEWORKS_DIR/$relative_path" ]]; then + echo "Unbundled dependency in $macho: $dependency" >&2 + return 1 + fi + ;; + @loader_path/*) + relative_path="${dependency#@loader_path/}" + if [[ ! -f "$(dirname "$macho")/$relative_path" ]]; then + echo "Unbundled dependency in $macho: $dependency" >&2 + return 1 + fi + ;; + *) + echo "External non-system dependency in $macho: $dependency" >&2 + return 1 + ;; + esac + done < <(otool -L "$macho" | tail -n +2 | awk '{ print $1 }') +} + +verify_dependencies "$APP_EXECUTABLE" + +SLINT_LIBRARY="$(find "$FRAMEWORKS_DIR" -maxdepth 1 -type f -name 'libslint_cpp*.dylib' -print -quit 2>/dev/null || true)" +if [[ -z "$SLINT_LIBRARY" ]]; then + echo "The packaged app does not contain libslint_cpp.dylib" >&2 + exit 1 +fi + +if ! lipo "$SLINT_LIBRARY" -verify_arch "$EXPECTED_ARCH" >/dev/null 2>&1; then + echo "Unexpected Slint runtime architecture; expected $EXPECTED_ARCH" >&2 + lipo -archs "$SLINT_LIBRARY" >&2 || true + exit 1 +fi +verify_dependencies "$SLINT_LIBRARY" + +PLIST_MIN_VERSION="$(/usr/libexec/PlistBuddy -c 'Print :LSMinimumSystemVersion' "$INFO_PLIST")" +BINARY_MIN_VERSION="$(otool -l "$APP_EXECUTABLE" | awk ' + $1 == "cmd" { build_version = ($2 == "LC_BUILD_VERSION"); version_min = ($2 == "LC_VERSION_MIN_MACOSX"); next } + build_version && $1 == "minos" { print $2; exit } + version_min && $1 == "version" { print $2; exit }')" +if [[ -z "$BINARY_MIN_VERSION" || "$PLIST_MIN_VERSION" != "$BINARY_MIN_VERSION" ]]; then + echo "Minimum macOS version mismatch: plist=$PLIST_MIN_VERSION binary=${BINARY_MIN_VERSION:-unknown}" >&2 + exit 1 +fi + +codesign --verify --deep --strict --verbose=2 "$APP_BUNDLE" + +echo "Verified $APP_BUNDLE ($EXPECTED_ARCH, macOS $BINARY_MIN_VERSION+)" diff --git a/src/device_controller/keyboard/linux/keyboard_capturer.cpp b/src/device_controller/keyboard/linux/keyboard_capturer.cpp index a5a7a74..95df0c7 100644 --- a/src/device_controller/keyboard/linux/keyboard_capturer.cpp +++ b/src/device_controller/keyboard/linux/keyboard_capturer.cpp @@ -1,5 +1,9 @@ #include "keyboard_capturer.h" +#include +#include +#include + #include #include diff --git a/src/device_controller/keyboard/linux/keyboard_capturer.h b/src/device_controller/keyboard/linux/keyboard_capturer.h index 2ef2048..80f44ca 100644 --- a/src/device_controller/keyboard/linux/keyboard_capturer.h +++ b/src/device_controller/keyboard/linux/keyboard_capturer.h @@ -7,10 +7,6 @@ #ifndef _KEYBOARD_CAPTURER_H_ #define _KEYBOARD_CAPTURER_H_ -#include -#include -#include - #include #include #include @@ -21,6 +17,7 @@ struct DBusConnection; struct DBusMessageIter; +struct _XDisplay; namespace crossdesk { @@ -48,8 +45,8 @@ class KeyboardCapturer : public DeviceController { append_args); private: - Display* display_; - Window root_; + _XDisplay* display_; + unsigned long root_; std::atomic running_; std::thread event_thread_; bool use_wayland_portal_ = false; diff --git a/src/device_controller/mouse/linux/mouse_controller.cpp b/src/device_controller/mouse/linux/mouse_controller.cpp index ca7af51..1105e17 100644 --- a/src/device_controller/mouse/linux/mouse_controller.cpp +++ b/src/device_controller/mouse/linux/mouse_controller.cpp @@ -1,6 +1,9 @@ #include "mouse_controller.h" +#include +#include #include +#include #include "platform.h" #include "rd_log.h" diff --git a/src/device_controller/mouse/linux/mouse_controller.h b/src/device_controller/mouse/linux/mouse_controller.h index 4d78fa0..a2fa312 100644 --- a/src/device_controller/mouse/linux/mouse_controller.h +++ b/src/device_controller/mouse/linux/mouse_controller.h @@ -7,10 +7,6 @@ #ifndef _MOUSE_CONTROLLER_H_ #define _MOUSE_CONTROLLER_H_ -#include -#include -#include - #include #include #include @@ -20,6 +16,7 @@ struct DBusConnection; struct DBusMessageIter; +struct _XDisplay; namespace crossdesk { @@ -53,8 +50,8 @@ class MouseController : public DeviceController { enum class WaylandAbsoluteMode { kUnknown, kPixels, kNormalized, kDisabled }; - Display* display_ = nullptr; - Window root_ = 0; + _XDisplay* display_ = nullptr; + unsigned long root_ = 0; std::vector display_info_list_; int screen_width_ = 0; int screen_height_ = 0; diff --git a/src/thumbnail/thumbnail.cpp b/src/thumbnail/thumbnail.cpp index 19e0ccc..992af47 100644 --- a/src/thumbnail/thumbnail.cpp +++ b/src/thumbnail/thumbnail.cpp @@ -57,7 +57,7 @@ void ScaleNv12ToABGR(char* src, int src_w, int src_h, int dst_w, int dst_h, v_fit.data(), (fit_w + 1) / 2, abgr.data(), fit_w * 4, fit_w, fit_h); - memset(dst_rgba, 0, dst_w * dst_h * 4); + std::memset(dst_rgba, 0, dst_w * dst_h * 4); for (int i = 0; i < dst_w * dst_h; ++i) { dst_rgba[i * 4 + 3] = static_cast(0xFF); } @@ -65,7 +65,8 @@ void ScaleNv12ToABGR(char* src, int src_w, int src_h, int dst_w, int dst_h, for (int row = 0; row < fit_h; ++row) { int dst_offset = ((row + (dst_h - fit_h) / 2) * dst_w + (dst_w - fit_w) / 2) * 4; - memcpy(dst_rgba + dst_offset, abgr.data() + row * fit_w * 4, fit_w * 4); + std::memcpy(dst_rgba + dst_offset, abgr.data() + row * fit_w * 4, + fit_w * 4); } } @@ -85,8 +86,8 @@ Thumbnail::Thumbnail(std::string save_path, unsigned char* aes128_key, save_path_ = save_path; } - memcpy(aes128_key_, aes128_key, sizeof(aes128_key_)); - memcpy(aes128_iv_, aes128_iv, sizeof(aes128_iv_)); + std::memcpy(aes128_key_, aes128_key, sizeof(aes128_key_)); + std::memcpy(aes128_iv_, aes128_iv, sizeof(aes128_iv_)); std::filesystem::create_directories(save_path_); } @@ -116,7 +117,7 @@ int Thumbnail::SaveToThumbnail(const char* yuv420p, int width, int height, thumbnail_height_, rgba_buffer_); } else { // If yuv420p is null, fill the buffer with black pixels - memset(rgba_buffer_, 0x00, thumbnail_width_ * thumbnail_height_ * 4); + std::memset(rgba_buffer_, 0x00, thumbnail_width_ * thumbnail_height_ * 4); for (int i = 0; i < thumbnail_width_ * thumbnail_height_; ++i) { // Set alpha channel to opaque rgba_buffer_[i * 4 + 3] = static_cast(0xFF); diff --git a/src/thumbnail/thumbnail.h b/src/thumbnail/thumbnail.h index ba6b324..5336f2d 100644 --- a/src/thumbnail/thumbnail.h +++ b/src/thumbnail/thumbnail.h @@ -7,6 +7,7 @@ #ifndef _THUMBNAIL_H_ #define _THUMBNAIL_H_ +#include #include #include #include @@ -52,18 +53,18 @@ class Thumbnail { int DeleteAllFilesInDirectory(); int GetKey(unsigned char* aes128_key) { - memcpy(aes128_key, aes128_key_, sizeof(aes128_key_)); + std::memcpy(aes128_key, aes128_key_, sizeof(aes128_key_)); return sizeof(aes128_key_); } int GetIv(unsigned char* aes128_iv) { - memcpy(aes128_iv, aes128_iv_, sizeof(aes128_iv_)); + std::memcpy(aes128_iv, aes128_iv_, sizeof(aes128_iv_)); return sizeof(aes128_iv_); } int GetKeyAndIv(unsigned char* aes128_key, unsigned char* aes128_iv) { - memcpy(aes128_key, aes128_key_, sizeof(aes128_key_)); - memcpy(aes128_iv, aes128_iv_, sizeof(aes128_iv_)); + std::memcpy(aes128_key, aes128_key_, sizeof(aes128_key_)); + std::memcpy(aes128_iv, aes128_iv_, sizeof(aes128_iv_)); return 0; } diff --git a/xmake/repository/packages/s/slint/xmake.lua b/thirdparty/slint/xmake.lua similarity index 65% rename from xmake/repository/packages/s/slint/xmake.lua rename to thirdparty/slint/xmake.lua index 29d6acc..a2c5063 100644 --- a/xmake/repository/packages/s/slint/xmake.lua +++ b/thirdparty/slint/xmake.lua @@ -13,14 +13,17 @@ package("slint") on_load(function(package) package:add("includedirs", "include/slint") - package:add("links", "slint_cpp") + if package:is_plat("windows") then + package:add("links", "slint_cpp.dll") + else + package:add("links", "slint_cpp") + end end) on_install("windows", "linux", "macosx", function(package) local configs = { "-DSLINT_BUILD_TESTING=OFF", "-DSLINT_BUILD_EXAMPLES=OFF", - "-DSLINT_COMPILER=download", "-DSLINT_FEATURE_INTERPRETER=OFF", "-DSLINT_FEATURE_LIVE_PREVIEW=OFF", "-DSLINT_FEATURE_TESTING=OFF", @@ -36,23 +39,6 @@ package("slint") "-DBUILD_SHARED_LIBS=ON" } import("package.tools.cmake").install(package, configs) - - -- SLINT_COMPILER=download only configures downstream CMake projects. - -- xmake's native Slint rule needs the matching host executable here. - import("net.http") - import("utils.archive") - local host_system = is_host("windows") and "windows" or (is_host("macosx") and "Darwin" or "Linux") - local host_arch = os.arch() - if host_arch == "x64" then - host_arch = "x86_64" - end - local suffix = host_system .. "-" .. host_arch - local archive_name = "slint-compiler-" .. suffix .. ".tar.gz" - local archive_file = path.join(package:builddir(), archive_name) - local download_url = "https://github.com/slint-ui/slint/releases/download/v" .. package:version_str() .. "/" .. archive_name - http.download(download_url, archive_file) - os.mkdir(path.join(package:installdir(), "bin")) - archive.extract(archive_file, package:installdir()) end) on_test(function(package) diff --git a/thirdparty/xmake.lua b/thirdparty/xmake.lua index c8bf5d8..e0a8563 100644 --- a/thirdparty/xmake.lua +++ b/thirdparty/xmake.lua @@ -1 +1 @@ -includes("cpp-httplib") \ No newline at end of file +includes("cpp-httplib", "slint") diff --git a/xmake.lua b/xmake.lua index 42f8e53..2190487 100644 --- a/xmake.lua +++ b/xmake.lua @@ -1,8 +1,6 @@ set_project("crossdesk") set_license("GPL-3.0-only") -add_repositories("crossdesk-packages " .. path.join(os.scriptdir(), "xmake", "repository")) - includes("xmake/options.lua") includes("xmake/platform.lua") includes("xmake/rules/slint.lua") diff --git a/xmake/platform.lua b/xmake/platform.lua index f454963..7280391 100644 --- a/xmake/platform.lua +++ b/xmake/platform.lua @@ -26,6 +26,7 @@ end function setup_platform_settings() if is_os("windows") then add_requires("libyuv", "miniaudio 0.11.21") + add_defines("NOMINMAX") add_links("Shell32", "dwmapi", "User32", "kernel32", "gdi32", "winmm", "setupapi", "version", "Imm32", "iphlpapi", "d3d11", "dxgi") diff --git a/xmake/targets.lua b/xmake/targets.lua index 0a0ffed..ff2884f 100644 --- a/xmake/targets.lua +++ b/xmake/targets.lua @@ -240,6 +240,7 @@ function setup_targets() "src/gui/features/settings/*.cpp", "src/gui/ui/crossdesk_ui.slint") add_includedirs("src/gui", {public = true}) if is_os("windows") then + add_cxxflags("/bigobj") add_files("src/gui/platform/tray/win_tray.cpp") add_includedirs("src/service/windows", {public = true}) elseif is_os("macosx") then