diff --git a/.github/workflows/cpp-check.yml b/.github/workflows/cpp-check.yml index 817a4f0ad..ccaabcbf8 100644 --- a/.github/workflows/cpp-check.yml +++ b/.github/workflows/cpp-check.yml @@ -5,45 +5,382 @@ on: branches: - main - release/* + - develop types: - opened - synchronize - reopened + - labeled workflow_dispatch: permissions: contents: read + packages: read + +env: + GHCR_IMAGE: ghcr.io/${{ github.repository_owner }}/cfdesktop-build-env jobs: - linux-build-test: - name: Linux build and tests + check-trigger: + name: Check CI trigger + runs-on: ubuntu-latest + outputs: + should_run: ${{ steps.check.outputs.should_run }} + run_clang: ${{ steps.check.outputs.run_clang }} + run_msvc: ${{ steps.check.outputs.run_msvc }} + steps: + - name: Determine CI scope + id: check + run: | + BASE="${{ github.base_ref }}" + LABELS="${{ join(github.event.pull_request.labels.*.name, ',') }}" + + echo "should_run=true" >> "$GITHUB_OUTPUT" + + # Clang: build-clang label or main/release (always full) + if echo "$LABELS" | grep -qw "build-clang" || [[ "$BASE" != "develop" ]]; then + echo "run_clang=true" >> "$GITHUB_OUTPUT" + else + echo "run_clang=false" >> "$GITHUB_OUTPUT" + fi + + # MSVC: build-msvc label or main/release (always full) + if echo "$LABELS" | grep -qw "build-msvc" || [[ "$BASE" != "develop" ]]; then + echo "run_msvc=true" >> "$GITHUB_OUTPUT" + else + echo "run_msvc=false" >> "$GITHUB_OUTPUT" + fi + + echo "--- CI Scope ---" + echo "Base branch: $BASE" + echo "Labels: $LABELS" + echo "GCC: always" + echo "Clang: $(echo "$LABELS" | grep -qE 'build-(clang|all-platform)' && echo 'yes' || { [[ "$BASE" != "develop" ]] && echo 'yes' || echo 'no'; })" + echo "MSVC: $(echo "$LABELS" | grep -qE 'build-(msvc|all-platform)' && echo 'yes' || { [[ "$BASE" != "develop" ]] && echo 'yes' || echo 'no'; })" + + linux-gcc: + name: Linux GCC + needs: check-trigger + if: needs.check-trigger.outputs.should_run == 'true' runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - - name: Build Docker image + - name: Normalize Docker image name + run: echo "GHCR_IMAGE=$(echo '${{ env.GHCR_IMAGE }}' | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_ENV" + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Pull pre-built Docker image + run: | + if docker pull ${{ env.GHCR_IMAGE }}:latest; then + echo "GHCR_PULL_FAILED=false" >> "$GITHUB_ENV" + else + echo "::warning::GHCR image pull failed, falling back to local Docker build." + echo "GHCR_PULL_FAILED=true" >> "$GITHUB_ENV" + fi + + - name: Build Docker image (fallback) + if: env.GHCR_PULL_FAILED == 'true' run: | + echo "::warning::GHCR image not available, building locally..." docker build \ + --progress=plain \ --platform linux/amd64 \ --build-arg QT_ARCH=linux_gcc_64 \ + --build-arg CI=true \ -f scripts/docker/Dockerfile.build \ - -t cfdesktop-build \ + -t ${{ env.GHCR_IMAGE }}:latest \ . - - name: Configure, build, and test + - name: Cache ccache + uses: actions/cache@v4 + with: + path: .ccache/gcc + key: ccache-linux-gcc-${{ github.sha }} + restore-keys: | + ccache-linux-gcc- + + - name: Restore GCC build directory + uses: actions/cache/restore@v4 + with: + path: out/build_ci + key: build-linux-gcc-${{ github.sha }} + restore-keys: | + build-linux-gcc- + + - name: Configure and build + run: | + docker run --rm \ + --platform linux/amd64 \ + -e QT_QPA_PLATFORM=offscreen \ + -e CCACHE_DIR=/project/.ccache/gcc \ + -e CCACHE_BASEDIR=/project \ + -e CCACHE_COMPILERCHECK=content \ + -e CCACHE_NOHASHDIR=true \ + -v "$PWD:/project" \ + -w /project \ + ${{ env.GHCR_IMAGE }}:latest \ + bash -lc 'ccache --max-size=5G; ccache --set-config=compression=true; ccache --set-config=compression_level=1; ccache -z; bash scripts/build_helpers/linux_fast_develop_build.sh ci -c build_ci_config.ini' + + - name: Save GCC build cache + if: success() + uses: actions/cache/save@v4 + with: + path: out/build_ci + key: build-linux-gcc-${{ github.sha }} + + - name: Run tests run: | docker run --rm \ --platform linux/amd64 \ -e QT_QPA_PLATFORM=offscreen \ -v "$PWD:/project" \ -w /project \ - cfdesktop-build \ - bash -lc "bash scripts/build_helpers/linux_develop_build.sh ci -c build_ci_config.ini && bash scripts/build_helpers/linux_run_tests.sh ci -c build_ci_config.ini" + ${{ env.GHCR_IMAGE }}:latest \ + bash -lc 'bash scripts/build_helpers/linux_run_tests.sh ci -c build_ci_config.ini' + + - name: Show ccache stats + if: always() + run: | + docker run --rm \ + --platform linux/amd64 \ + -e CCACHE_DIR=/project/.ccache/gcc \ + -v "$PWD:/project" \ + -w /project \ + ${{ env.GHCR_IMAGE }}:latest \ + bash -lc 'ccache -s || true' || true + + linux-clang: + name: Linux Clang + needs: check-trigger + if: needs.check-trigger.outputs.run_clang == 'true' + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Normalize Docker image name + run: echo "GHCR_IMAGE=$(echo '${{ env.GHCR_IMAGE }}' | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_ENV" + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Pull pre-built Docker image + run: | + if docker pull ${{ env.GHCR_IMAGE }}:latest; then + echo "GHCR_PULL_FAILED=false" >> "$GITHUB_ENV" + else + echo "::warning::GHCR image pull failed, falling back to local Docker build." + echo "GHCR_PULL_FAILED=true" >> "$GITHUB_ENV" + fi + + - name: Build Docker image (fallback) + if: env.GHCR_PULL_FAILED == 'true' + run: | + echo "::warning::GHCR image not available, building locally..." + docker build \ + --progress=plain \ + --platform linux/amd64 \ + --build-arg QT_ARCH=linux_gcc_64 \ + --build-arg CI=true \ + -f scripts/docker/Dockerfile.build \ + -t ${{ env.GHCR_IMAGE }}:latest \ + . + + - name: Cache ccache + uses: actions/cache@v4 + with: + path: .ccache/clang + key: ccache-linux-clang-${{ github.sha }} + restore-keys: | + ccache-linux-clang- + + - name: Restore Clang build directory + uses: actions/cache/restore@v4 + with: + path: out/build_ci_clang + key: build-linux-clang-${{ github.sha }} + restore-keys: | + build-linux-clang- + + - name: Configure and build + run: | + docker run --rm \ + --platform linux/amd64 \ + -e QT_QPA_PLATFORM=offscreen \ + -e CCACHE_DIR=/project/.ccache/clang \ + -e CCACHE_BASEDIR=/project \ + -e CCACHE_COMPILERCHECK=content \ + -e CCACHE_NOHASHDIR=true \ + -v "$PWD:/project" \ + -w /project \ + ${{ env.GHCR_IMAGE }}:latest \ + bash -lc 'ccache --max-size=5G; ccache --set-config=compression=true; ccache --set-config=compression_level=1; ccache -z; bash scripts/build_helpers/linux_fast_develop_build.sh ci -c build_ci_clang_config.ini' + + - name: Save Clang build cache + if: success() + uses: actions/cache/save@v4 + with: + path: out/build_ci_clang + key: build-linux-clang-${{ github.sha }} + + - name: Run tests + run: | + docker run --rm \ + --platform linux/amd64 \ + -e QT_QPA_PLATFORM=offscreen \ + -v "$PWD:/project" \ + -w /project \ + ${{ env.GHCR_IMAGE }}:latest \ + bash -lc 'bash scripts/build_helpers/linux_run_tests.sh ci -c build_ci_clang_config.ini' + + - name: Show ccache stats + if: always() + run: | + docker run --rm \ + --platform linux/amd64 \ + -e CCACHE_DIR=/project/.ccache/clang \ + -v "$PWD:/project" \ + -w /project \ + ${{ env.GHCR_IMAGE }}:latest \ + bash -lc 'ccache -s || true' || true + + windows-msvc: + name: Windows MSVC + needs: check-trigger + if: needs.check-trigger.outputs.run_msvc == 'true' + runs-on: windows-latest + + env: + QT_VERSION: 6.8.1 + QT_ARCH: win64_msvc2022_64 + QT_DIR: C:\Qt\6.8.1\msvc2022_64 + CCACHE_DIR: ${{ github.workspace }}\.ccache\msvc + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup MSVC developer environment + uses: ilammy/msvc-dev-cmd@v1 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.x' + + - name: Install ccache + shell: pwsh + run: | + if (-not (Get-Command ccache -ErrorAction SilentlyContinue)) { + choco install ccache -y --no-progress + } + ccache --version + + - name: Cache ccache + uses: actions/cache@v4 + with: + path: .ccache\msvc + key: ccache-windows-msvc-${{ github.sha }} + restore-keys: | + ccache-windows-msvc- + + - name: Configure ccache + shell: pwsh + run: | + New-Item -ItemType Directory -Force -Path $env:CCACHE_DIR | Out-Null + ccache --set-config="cache_dir=$env:CCACHE_DIR" + ccache --set-config=max_size=5G + ccache --set-config=compiler_check=content + ccache --set-config=compression=true + ccache --set-config=compression_level=1 + ccache -z + ccache -s + + - name: Cache Qt installation + id: cache-qt + uses: actions/cache@v4 + with: + path: ${{ env.QT_DIR }} + key: qt-${{ env.QT_VERSION }}-${{ env.QT_ARCH }}-v1 + + - name: Install aqtinstall + if: steps.cache-qt.outputs.cache-hit != 'true' + run: pip install aqtinstall + + - name: Install Qt + if: steps.cache-qt.outputs.cache-hit != 'true' + run: | + python -m aqt install-qt windows desktop ${{ env.QT_VERSION }} ${{ env.QT_ARCH }} ` + -O C:\Qt ` + -m qt5compat qtserialport qtimageformats + + - name: Save Qt cache immediately + if: steps.cache-qt.outputs.cache-hit != 'true' + uses: actions/cache/save@v4 + with: + path: ${{ env.QT_DIR }} + key: qt-${{ env.QT_VERSION }}-${{ env.QT_ARCH }}-v1 + + - name: Restore MSVC build directory + uses: actions/cache/restore@v4 + with: + path: out\build_ci_windows + key: build-windows-msvc-${{ github.sha }} + restore-keys: | + build-windows-msvc- + + - name: Configure and build + shell: pwsh + env: + Qt6_DIR: ${{ env.QT_DIR }}\lib\cmake\Qt6 + CCACHE_BASEDIR: ${{ github.workspace }} + CCACHE_COMPILERCHECK: content + CCACHE_NOHASHDIR: true + run: | + ccache -z + pwsh -NoLogo -NoProfile -ExecutionPolicy Bypass -File scripts/build_helpers/windows_fast_develop_build.ps1 -Config ci + + - name: Save MSVC build cache + if: success() + uses: actions/cache/save@v4 + with: + path: out\build_ci_windows + key: build-windows-msvc-${{ github.sha }} + + - name: Run tests + shell: pwsh + env: + Qt6_DIR: ${{ env.QT_DIR }}\lib\cmake\Qt6 + QT_QPA_PLATFORM: offscreen + run: | + pwsh -NoLogo -NoProfile -ExecutionPolicy Bypass -File scripts/build_helpers/windows_run_tests.ps1 -Config ci + + - name: Show ccache stats + if: always() + shell: pwsh + run: | + if (Get-Command ccache -ErrorAction SilentlyContinue) { + ccache -s + } docs-build: name: VitePress docs build + needs: check-trigger + if: needs.check-trigger.outputs.should_run == 'true' runs-on: ubuntu-latest steps: diff --git a/.github/workflows/docker-build-env.yml b/.github/workflows/docker-build-env.yml new file mode 100644 index 000000000..44675b470 --- /dev/null +++ b/.github/workflows/docker-build-env.yml @@ -0,0 +1,71 @@ +name: Build Docker Environment + +on: + push: + branches: + - main + - develop + paths: + - .github/workflows/docker-build-env.yml + - scripts/docker/Dockerfile.build + - scripts/dependency/install_build_dependencies.sh + workflow_dispatch: + schedule: + - cron: '0 3 * * 1' + +permissions: + contents: read + packages: write + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository_owner }}/cfdesktop-build-env + +jobs: + build-and-push: + name: Build and push Docker image + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Normalize image name + id: image + run: | + IMAGE="${REGISTRY}/${IMAGE_NAME}" + echo "image=${IMAGE,,}" >> "$GITHUB_OUTPUT" + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ steps.image.outputs.image }} + tags: | + type=raw,value=latest + type=sha,prefix= + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + file: scripts/docker/Dockerfile.build + platforms: linux/amd64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + build-args: | + QT_ARCH=linux_gcc_64 + CI=true diff --git a/.gitignore b/.gitignore index adb742025..e8c058458 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # ignores .cache +.ccache/ .claude CLAUDE.md MEMORY.md diff --git a/base/CMakeLists.txt b/base/CMakeLists.txt index ba64b3f6b..0a0f55588 100644 --- a/base/CMakeLists.txt +++ b/base/CMakeLists.txt @@ -21,20 +21,33 @@ add_subdirectory(device) # Create the unified shared library add_library(cfbase SHARED) -# Link static libraries from sub-modules with --whole-archive to ensure -# all symbols (including dllexport) are included in cfbase.dll. -# Without this, the linker skips static lib objects since cfbase has no own sources. -# Force all objects into cfbase.dll (PRIVATE: only affects cfbase's link step) -target_link_libraries(cfbase PRIVATE - -Wl,--whole-archive +set(CFBASE_MODULE_TARGETS cfbase_cpu cfbase_memory cfbase_network cfbase_gpu cfbase_console - -Wl,--no-whole-archive ) +# Link static libraries from sub-modules with --whole-archive to ensure +# all symbols (including dllexport) are included in cfbase.dll. +# Without this, the linker skips static lib objects since cfbase has no own sources. +# Force all objects into cfbase.dll (PRIVATE: only affects cfbase's link step). +if(MSVC) + target_link_libraries(cfbase PRIVATE ${CFBASE_MODULE_TARGETS}) + foreach(_cfbase_module IN LISTS CFBASE_MODULE_TARGETS) + target_link_options(cfbase PRIVATE "/WHOLEARCHIVE:$") + endforeach() +elseif(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang" AND NOT APPLE) + target_link_libraries(cfbase PRIVATE + -Wl,--whole-archive + ${CFBASE_MODULE_TARGETS} + -Wl,--no-whole-archive + ) +else() + target_link_libraries(cfbase PRIVATE ${CFBASE_MODULE_TARGETS}) +endif() + # Set include directories # Expose both include/ (for public headers) and . (for sub-module headers like device/console) target_include_directories(cfbase PUBLIC diff --git a/base/include/base/lockfree/mpsc_queue.hpp b/base/include/base/lockfree/mpsc_queue.hpp index 1bd0a90a2..354c2a415 100644 --- a/base/include/base/lockfree/mpsc_queue.hpp +++ b/base/include/base/lockfree/mpsc_queue.hpp @@ -19,11 +19,41 @@ #include #include +#if defined(_MSC_VER) && (defined(_M_X64) || defined(_M_IX86)) +# include +#endif + #include "base/span/span.h" namespace cf { namespace lockfree { +namespace detail { + +/** + * @brief Issues a short processor-friendly spin-wait hint. + * + * Uses the platform pause intrinsic on x86 targets and falls back to a compiler + * signal fence on other architectures. + * + * @throws None. + * @note Intended for tight lock-free retry loops. + * @warning Does not yield the current thread or provide scheduling fairness. + * @since 0.1 + * @ingroup base_lockfree + */ +inline void cpu_relax() noexcept { +#if defined(_MSC_VER) && (defined(_M_X64) || defined(_M_IX86)) + _mm_pause(); +#elif (defined(__x86_64__) || defined(__i386__)) && (defined(__GNUC__) || defined(__clang__)) + __builtin_ia32_pause(); +#else + std::atomic_signal_fence(std::memory_order_seq_cst); +#endif +} + +} // namespace detail + /** * @brief Multi-Producer Single-Consumer lock-free queue. * @@ -124,13 +154,7 @@ template class MpscQueue { // If seq > pos, it means consumer hasn't caught up (queue full) // Uses a simple approach: just wait without timeout for MPSC while (seq != pos) { -#if defined(__x86_64__) || defined(_M_X64) || defined(__i386__) || defined(_M_IX86) - __builtin_ia32_pause(); // x86 pause instruction -#else - // Generic fallback - volatile int dummy = 0; - (void)dummy; -#endif + detail::cpu_relax(); seq = cell->sequence.load(std::memory_order_acquire); } @@ -207,13 +231,7 @@ template class MpscQueue { size_type seq = cell->sequence.load(std::memory_order_acquire); // Wait for this slot to become available (like tryPush) while (seq != pos) { -#if defined(__x86_64__) || defined(_M_X64) || defined(__i386__) || defined(_M_IX86) - __builtin_ia32_pause(); // x86 pause instruction -#else - // Generic fallback - volatile int dummy = 0; - (void)dummy; -#endif + detail::cpu_relax(); seq = cell->sequence.load(std::memory_order_acquire); } diff --git a/base/include/base/policy_chain/policy_chain.hpp b/base/include/base/policy_chain/policy_chain.hpp index 25b8491c9..85b0dd50e 100644 --- a/base/include/base/policy_chain/policy_chain.hpp +++ b/base/include/base/policy_chain/policy_chain.hpp @@ -15,13 +15,35 @@ * @ingroup base_policy_chain */ +#include #include #include +#include #include +#include #include namespace cf { +namespace policy_chain_detail { + +#if defined(__clang__) && (__clang_major__ < 19) +# define CF_POLICY_CHAIN_INVOKE_BARRIER __attribute__((noinline, optnone)) +#else +# define CF_POLICY_CHAIN_INVOKE_BARRIER +#endif + +// Clang 18 can miscompile inlined std::optional-returning fallback chains under -O3. +template [[nodiscard]] CF_POLICY_CHAIN_INVOKE_BARRIER auto +invoke_policy(Policy const& policy, CallArgs&&... args) + -> decltype(policy(std::forward(args)...)) { + return policy(std::forward(args)...); +} + +#undef CF_POLICY_CHAIN_INVOKE_BARRIER + +} // namespace policy_chain_detail + /** * @brief PolicyChain with fallback mechanism * @@ -60,19 +82,27 @@ template class PolicyChain { public: using PolicyType = std::function(Args...)>; using ValueType = Ret; - using SizeType = size_t; + using SizeType = std::size_t; PolicyChain() = default; + PolicyChain(const PolicyChain&) = default; + PolicyChain& operator=(const PolicyChain&) = default; + PolicyChain(PolicyChain&&) noexcept = default; + PolicyChain& operator=(PolicyChain&&) noexcept = default; /** * @brief Add a policy to the front of the chain (highest priority) */ - void add_front(PolicyType policy) { policies_.push_front(std::move(policy)); } + template void add_front(Policy&& policy) { + policies_.emplace_front(std::forward(policy)); + } /** * @brief Add a policy to the back of the chain (lowest priority) */ - void add_back(PolicyType policy) { policies_.push_back(std::move(policy)); } + template void add_back(Policy&& policy) { + policies_.emplace_back(std::forward(policy)); + } /** * @brief Execute policies in chain order until one succeeds @@ -80,8 +110,9 @@ template class PolicyChain { */ [[nodiscard]] std::optional execute(Args... args) const { for (const auto& policy : policies_) { - if (auto result = policy(args...); result.has_value()) { - return result; + auto result = policy_chain_detail::invoke_policy(policy, args...); + if (result.has_value()) { + return std::move(result); } } return std::nullopt; @@ -118,7 +149,67 @@ template class PolicyChain { [[nodiscard]] auto end() const { return policies_.end(); } private: - std::list policies_; + struct PolicyConcept { + virtual ~PolicyConcept() = default; + [[nodiscard]] virtual std::optional invoke(Args... args) const = 0; + [[nodiscard]] virtual std::unique_ptr clone() const = 0; + }; + + template struct PolicyModel final : PolicyConcept { + explicit PolicyModel(Policy policy) : policy_(std::move(policy)) {} + + [[nodiscard]] std::optional invoke(Args... args) const override { + auto result = std::invoke(policy_, args...); +#if defined(__clang__) && (__clang_major__ < 19) + // Clang 18 can mispack disengaged small std::optional returns under -O3. + if (!result.has_value()) { + return std::nullopt; + } +#endif + return result; + } + + [[nodiscard]] std::unique_ptr clone() const override { + return std::make_unique(policy_); + } + + mutable Policy policy_; + }; + + class PolicyEntry { + public: + template explicit PolicyEntry(Policy&& policy) + : policy_(make_policy_model(std::forward(policy))) {} + + PolicyEntry(const PolicyEntry& other) : policy_(other.policy_->clone()) {} + PolicyEntry(PolicyEntry&&) noexcept = default; + + PolicyEntry& operator=(const PolicyEntry& other) { + if (this != &other) { + policy_ = other.policy_->clone(); + } + return *this; + } + + PolicyEntry& operator=(PolicyEntry&&) noexcept = default; + + [[nodiscard]] std::optional operator()(Args... args) const { + return policy_->invoke(args...); + } + + private: + template + [[nodiscard]] static std::unique_ptr make_policy_model(Policy&& policy) { + using StoredPolicy = std::decay_t; + static_assert(std::is_copy_constructible_v, + "PolicyChain policies must be copy constructible"); + return std::make_unique>(std::forward(policy)); + } + + std::unique_ptr policy_; + }; + + std::list policies_; }; // ============================================================================ @@ -199,8 +290,8 @@ template class PolicyChainBuilder { * @since 0.13.0 * @ingroup base_policy_chain */ - PolicyChainBuilder& then(PolicyType policy) { - chain_.add_back(std::move(policy)); + template PolicyChainBuilder& then(Policy&& policy) { + chain_.add_back(std::forward(policy)); return *this; } diff --git a/base/system/cpu/private/win_impl/cpu_features.cpp b/base/system/cpu/private/win_impl/cpu_features.cpp index d2f123cf7..c08c3c6f4 100644 --- a/base/system/cpu/private/win_impl/cpu_features.cpp +++ b/base/system/cpu/private/win_impl/cpu_features.cpp @@ -12,9 +12,16 @@ #include namespace { +#if defined(_MSC_VER) && !defined(__clang__) +static unsigned long long read_xcr0() noexcept { + return static_cast(_xgetbv(0)); +} +#else __attribute__((target("xsave"))) static unsigned long long read_xcr0() { return _xgetbv(0); } +#endif + void addFeatureIfSupported(bool condition, const char* name, std::vector& feats) { if (condition) { feats.emplace_back(name); @@ -53,4 +60,4 @@ void query_cpu_features(std::vector& feats) { addFeatureIfSupported(cpuInfo[1] & (1 << 5), "avx2", feats); addFeatureIfSupported(cpuInfo[1] & (1 << 16), "avx512", feats); addFeatureIfSupported(cpuInfo[1] & (1 << 29), "sha2", feats); -} \ No newline at end of file +} diff --git a/cmake/build_type_config.cmake b/cmake/build_type_config.cmake index e504f9eb8..7ae67668e 100644 --- a/cmake/build_type_config.cmake +++ b/cmake/build_type_config.cmake @@ -8,17 +8,29 @@ endif() set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release" "RelWithDebInfo") # Set compiler flags for each build type (use CACHE FORCE to override CMake defaults) -# Debug: No optimization (-O0), full debug info (-g) -set(CMAKE_CXX_FLAGS_DEBUG "-O0 -g -D_CFDESKTOPDEBUG" CACHE STRING "Flags used by the C++ compiler during Debug builds" FORCE) +if(MSVC) + set(_CF_DEBUG_CXX_FLAGS "/Od /Zi /D_CFDESKTOPDEBUG") + set(_CF_RELEASE_CXX_FLAGS "/O2 /DNDEBUG") + set(_CF_RELWITHDEBINFO_CXX_FLAGS "/O2 /Zi /DNDEBUG") + set(_CF_RELEASE_LINKER_FLAGS "") +else() + set(_CF_DEBUG_CXX_FLAGS "-O0 -g -D_CFDESKTOPDEBUG") + set(_CF_RELEASE_CXX_FLAGS "-O3 -DNDEBUG") + set(_CF_RELWITHDEBINFO_CXX_FLAGS "-O2 -g -DNDEBUG") + set(_CF_RELEASE_LINKER_FLAGS "-s") +endif() + +# Debug: No optimization, full debug info +set(CMAKE_CXX_FLAGS_DEBUG "${_CF_DEBUG_CXX_FLAGS}" CACHE STRING "Flags used by the C++ compiler during Debug builds" FORCE) set(CMAKE_EXE_LINKER_FLAGS_DEBUG "" CACHE STRING "Linker flags used during Debug builds" FORCE) # For Static Library Relocatable set(CMAKE_POSITION_INDEPENDENT_CODE ON) -# Release: Maximum optimization (-O3), no debug info -set(CMAKE_CXX_FLAGS_RELEASE "-O3 -DNDEBUG" CACHE STRING "Flags used by the C++ compiler during Release builds" FORCE) -set(CMAKE_EXE_LINKER_FLAGS_RELEASE "-s" CACHE STRING "Linker flags used during Release builds" FORCE) +# Release: Maximum optimization, no debug info +set(CMAKE_CXX_FLAGS_RELEASE "${_CF_RELEASE_CXX_FLAGS}" CACHE STRING "Flags used by the C++ compiler during Release builds" FORCE) +set(CMAKE_EXE_LINKER_FLAGS_RELEASE "${_CF_RELEASE_LINKER_FLAGS}" CACHE STRING "Linker flags used during Release builds" FORCE) # RelWithDebInfo: Optimization (-O2) + full debug info -set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "-O2 -g -DNDEBUG" CACHE STRING "Flags used by the C++ compiler during RelWithDebInfo builds" FORCE) +set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "${_CF_RELWITHDEBINFO_CXX_FLAGS}" CACHE STRING "Flags used by the C++ compiler during RelWithDebInfo builds" FORCE) set(CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO "" CACHE STRING "Linker flags used during RelWithDebInfo builds" FORCE) string(TOUPPER "${CMAKE_BUILD_TYPE}" _build_type_upper) diff --git a/cmake/check_pre_configure.cmake b/cmake/check_pre_configure.cmake index 8aa3c31c2..6779d24e3 100644 --- a/cmake/check_pre_configure.cmake +++ b/cmake/check_pre_configure.cmake @@ -62,7 +62,7 @@ endif() include(cmake/generate_develop_helpers.cmake) # Skip VSCode config generation in CI environment (docker builds) -if(NOT USE_TOOLCHAIN STREQUAL "linux/ci") +if(NOT USE_TOOLCHAIN MATCHES "^linux/ci") log_info("DevHelpers" "Will Generate VSCode clangd configuration") generate_vscode_clangd() diff --git a/cmake/cmake_toolchain/linux/.gitignore b/cmake/cmake_toolchain/linux/.gitignore index e7a8bc690..42c8fc4be 100644 --- a/cmake/cmake_toolchain/linux/.gitignore +++ b/cmake/cmake_toolchain/linux/.gitignore @@ -1,7 +1,8 @@ -# Reason Why? -# Every man owns his configurations, please specified the -# cmake prefix path -*.cmake -!ci-aarch64-toolchain.cmake -!ci-armhf-toolchain.cmake -!ci-x86_64-toolchain.cmake \ No newline at end of file +# Reason Why? +# Every man owns his configurations, please specified the +# cmake prefix path +*.cmake +!ci-aarch64-toolchain.cmake +!ci-armhf-toolchain.cmake +!ci-clang-x86_64-toolchain.cmake +!ci-x86_64-toolchain.cmake diff --git a/cmake/cmake_toolchain/linux/ci-clang-x86_64-toolchain.cmake b/cmake/cmake_toolchain/linux/ci-clang-x86_64-toolchain.cmake new file mode 100644 index 000000000..fcb50c43f --- /dev/null +++ b/cmake/cmake_toolchain/linux/ci-clang-x86_64-toolchain.cmake @@ -0,0 +1,54 @@ +# ============================================================================= +# CI Build Toolchain Configuration (Clang x86_64/AMD64) +# ============================================================================= +# Clang toolchain for CI Docker x86_64 builds +# +# Qt installation path: /opt/Qt/6.8.1/gcc_64 (same as GCC, compatible) +# +# Usage: +# cmake -DUSE_TOOLCHAIN=linux/ci-clang-x86_64 -S . -B build +# ============================================================================= + +set(CMAKE_SYSTEM_NAME Linux) + +# ----------------------------------------------------------------------------- +# Compiler configuration +# Ubuntu 24.04 ships Clang 18 with full C++23 support +# ----------------------------------------------------------------------------- +find_program(CF_CLANG_C_COMPILER NAMES clang) +find_program(CF_CLANG_CXX_COMPILER NAMES clang++) + +if(NOT CF_CLANG_C_COMPILER OR NOT CF_CLANG_CXX_COMPILER) + message(FATAL_ERROR "Clang CI toolchain requires clang and clang++ in PATH") +endif() + +set(CMAKE_C_COMPILER "${CF_CLANG_C_COMPILER}" CACHE FILEPATH "C compiler") +set(CMAKE_CXX_COMPILER "${CF_CLANG_CXX_COMPILER}" CACHE FILEPATH "C++ compiler") + +# ----------------------------------------------------------------------------- +# Qt6 search path (same as GCC CI toolchain) +# Qt pre-built binaries from aqtinstall are compiler-agnostic on Linux +# ----------------------------------------------------------------------------- +set(QT6_BASE_DIR "/opt/Qt/6.8.1/gcc_64") +set(QT6_CMAKE_DIR "${QT6_BASE_DIR}/lib/cmake/Qt6") + +if(DEFINED ENV{Qt6_DIR}) + set(QT6_CMAKE_DIR "$ENV{Qt6_DIR}") + get_filename_component(QT6_BASE_DIR "${QT6_CMAKE_DIR}/../.." ABSOLUTE) +elseif(DEFINED ENV{QT6_DIR}) + set(QT6_BASE_DIR "$ENV{QT6_DIR}") + set(QT6_CMAKE_DIR "${QT6_BASE_DIR}/lib/cmake/Qt6") +endif() + +set(Qt6_DIR "${QT6_CMAKE_DIR}" CACHE PATH "Qt6 installation directory") +set(CMAKE_PREFIX_PATH "${QT6_BASE_DIR}") +list(APPEND CMAKE_PREFIX_PATH "${QT6_BASE_DIR}") + +# ----------------------------------------------------------------------------- +# ccache acceleration (available in Docker container) +# ----------------------------------------------------------------------------- +find_program(CCACHE_PROGRAM ccache CACHE) +if(CCACHE_PROGRAM) + set(CMAKE_C_COMPILER_LAUNCHER "${CCACHE_PROGRAM}" CACHE FILEPATH "C compiler launcher") + set(CMAKE_CXX_COMPILER_LAUNCHER "${CCACHE_PROGRAM}" CACHE FILEPATH "CXX compiler launcher") +endif() diff --git a/cmake/cmake_toolchain/windows/.gitignore b/cmake/cmake_toolchain/windows/.gitignore index 319d6c149..9fb34d475 100644 --- a/cmake/cmake_toolchain/windows/.gitignore +++ b/cmake/cmake_toolchain/windows/.gitignore @@ -1,4 +1,5 @@ # Reason Why? # Every man owns his configurations, please specified the # cmake prefix path -*.cmake \ No newline at end of file +*.cmake +!ci-msvc-toolchain.cmake \ No newline at end of file diff --git a/cmake/cmake_toolchain/windows/ci-msvc-toolchain.cmake b/cmake/cmake_toolchain/windows/ci-msvc-toolchain.cmake new file mode 100644 index 000000000..bc84ccc88 --- /dev/null +++ b/cmake/cmake_toolchain/windows/ci-msvc-toolchain.cmake @@ -0,0 +1,42 @@ +# ============================================================================= +# CI Build Toolchain Configuration (Windows MSVC) +# ============================================================================= +# MSVC toolchain for Windows CI on github-actions runners +# +# Qt installation path: C:/Qt/6.8.1/msvc2022_64 +# Compiler: auto-detected by Visual Studio generator (MSVC v143) +# +# Usage: +# cmake -DUSE_TOOLCHAIN=windows/ci-msvc -S . -B build +# ============================================================================= + +set(CMAKE_SYSTEM_NAME Windows) + +# ----------------------------------------------------------------------------- +# Qt6 search path (Windows CI runner with aqtinstall) +# ----------------------------------------------------------------------------- +set(QT6_BASE_DIR "C:/Qt/6.8.1/msvc2022_64") +set(QT6_CMAKE_DIR "${QT6_BASE_DIR}/lib/cmake/Qt6") + +# Allow environment variable overrides for local testing +if(DEFINED ENV{Qt6_DIR}) + set(QT6_CMAKE_DIR "$ENV{Qt6_DIR}") + get_filename_component(QT6_BASE_DIR "${QT6_CMAKE_DIR}/../.." ABSOLUTE) +elseif(DEFINED ENV{QT6_DIR}) + set(QT6_BASE_DIR "$ENV{QT6_DIR}") + set(QT6_CMAKE_DIR "${QT6_BASE_DIR}/lib/cmake/Qt6") +endif() + +set(Qt6_DIR "${QT6_CMAKE_DIR}" CACHE PATH "Qt6 installation directory") +set(CMAKE_PREFIX_PATH "${QT6_BASE_DIR}") +list(APPEND CMAKE_PREFIX_PATH "${QT6_BASE_DIR}") + +# ----------------------------------------------------------------------------- +# ccache acceleration +# GitHub Actions installs ccache before configuring this toolchain. +# ----------------------------------------------------------------------------- +find_program(CCACHE_PROGRAM ccache CACHE) +if(CCACHE_PROGRAM) + set(CMAKE_C_COMPILER_LAUNCHER "${CCACHE_PROGRAM}" CACHE FILEPATH "C compiler launcher") + set(CMAKE_CXX_COMPILER_LAUNCHER "${CCACHE_PROGRAM}" CACHE FILEPATH "CXX compiler launcher") +endif() diff --git a/desktop/CMakeLists.txt b/desktop/CMakeLists.txt index 50a2ebe08..0b0f6fcee 100644 --- a/desktop/CMakeLists.txt +++ b/desktop/CMakeLists.txt @@ -49,17 +49,14 @@ target_sources(CFDesktop_shared PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/desktop_run_session.cpp ) -# Link all static libraries using --whole-archive -# This ensures all symbols are included even if not directly referenced -# Supported on both Unix and MinGW/Windows -if(UNIX AND NOT APPLE) - target_link_libraries(CFDesktop_shared PRIVATE - "-Wl,--whole-archive" - ${CFDESKTOP_STATIC_LIBS} - "-Wl,--no-whole-archive" - ) -elseif(WIN32) - # MinGW supports --whole-archive on Windows too - required for DLL symbol export +# Link all static libraries using whole-archive semantics. +# This ensures all symbols are included even if not directly referenced. +if(MSVC) + target_link_libraries(CFDesktop_shared PRIVATE ${CFDESKTOP_STATIC_LIBS}) + foreach(_cfdesktop_module IN LISTS CFDESKTOP_STATIC_LIBS) + target_link_options(CFDesktop_shared PRIVATE "/WHOLEARCHIVE:$") + endforeach() +elseif(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang" AND NOT APPLE) target_link_libraries(CFDesktop_shared PRIVATE "-Wl,--whole-archive" ${CFDESKTOP_STATIC_LIBS} diff --git a/desktop/main/early_session/impl/early_welcome_impl.h b/desktop/main/early_session/impl/early_welcome_impl.h index e18017dd5..02c8130cb 100644 --- a/desktop/main/early_session/impl/early_welcome_impl.h +++ b/desktop/main/early_session/impl/early_welcome_impl.h @@ -11,6 +11,8 @@ #pragma once #include "../init_chain/init_stage.h" +#include + namespace cf::desktop::early_stage { /** diff --git a/document/notes/06-PolicyChain-Clang18-Miscompilation-Debug.md b/document/notes/06-PolicyChain-Clang18-Miscompilation-Debug.md new file mode 100644 index 000000000..8c132fabf --- /dev/null +++ b/document/notes/06-PolicyChain-Clang18-Miscompilation-Debug.md @@ -0,0 +1,157 @@ +# PolicyChain Clang 18 Miscompilation — Debug Progress + +## 问题描述 + +CI 中 Clang 18 (Ubuntu 24.04 Docker, Release/-O3) 有 4 个 `policy_chain_test` 测试失败, +GCC 和 Clang 22 均通过。 + +**失败的测试:** +- `PolicyChainTest.Fallback_SecondPolicy` — `execute(-3)` 返回 `-6`(期望 `3`) +- `PolicyChainTest.Fallback_MultiplePolicies` — `execute(-5)` 返回 `-10`(期望 `0`) +- `PolicyChainTest.MakePolicyChain_Basic` — `execute(-3)` 返回 `-6`(期望 `3`) +- `PolicyChainTest.Builder_Basic` — `execute(-3)` 返回 `-6`(期望 `3`) + +**共同模式:** 第一个 policy 的 `if (x > 0)` 条件被 Clang 18 优化掉, +`x * 2` 被无条件执行,`std::nullopt` 返回路径被消除。 + +## 复现命令(Docker) + +```bash +# 构建 +docker run --rm -v "$PWD":/project -w /project \ + ghcr.io/awesome-embedded-learning-studio/cfdesktop-build-env:latest \ + bash -lc 'ccache -C; bash scripts/build_helpers/linux_fast_develop_build.sh ci -c build_ci_clang_config.ini' + +# 跑单个失败测试 +docker run --rm -v "$PWD":/project -w /project \ + ghcr.io/awesome-embedded-learning-studio/cfdesktop-build-env:latest \ + bash -lc './out/build_ci_clang/test/bin/policy_chain_test --gtest_filter="PolicyChainTest.Fallback_SecondPolicy"' + +# 跑全部 policy_chain 测试 +docker run --rm -v "$PWD":/project -w /project \ + ghcr.io/awesome-embedded-learning-studio/cfdesktop-build-env:latest \ + bash -lc 'ctest --test-dir out/build_ci_clang/test -R policy_chain --output-on-failure' +``` + +## 关键文件 + +- **实现:** `base/include/base/policy_chain/policy_chain.hpp` +- **测试:** `test/base/policy_chain/policy_chain_test.cpp` + +## 已有的 Workaround(不够) + +文件中已有 `invoke_policy` barrier(第 30-41 行): + +```cpp +#if defined(__clang__) && (__clang_major__ < 19) +# define CF_POLICY_CHAIN_INVOKE_BARRIER __attribute__((noinline, optnone)) +#else +# define CF_POLICY_CHAIN_INVOKE_BARRIER +#endif + +template +[[nodiscard]] CF_POLICY_CHAIN_INVOKE_BARRIER auto +invoke_policy(Policy const& policy, CallArgs&&... args) + -> decltype(policy(std::forward(args)...)) { + return policy(std::forward(args)...); +} +``` + +这个 barrier 无效——问题不在 `execute()` 的 `has_value()` 检查,而在更深层。 + +## 调试发现(通过 fprintf 日志定位) + +### 实验 1:在 `execute()` 和 `PolicyModel::invoke()` 都加 fprintf → PASS +### 实验 2:只在 `execute()` 加 fprintf,`PolicyModel::invoke()` 不加 → FAIL + +**关键日志(失败时):** +``` +execute(-3): + [DEBUG] invoke_policy returned, has_value=1 ← 只调了一次! + [DEBUG] returning value: -6 +``` + +**正确行为(加 fprintf 后):** +``` +execute(-3): + [DEBUG] PolicyModel::invoke(), has_value=0, val=0 ← 第一个 policy 返回 nullopt + [DEBUG] PolicyModel::invoke(), has_value=1, val=3 ← 第二个 policy 返回 3 + [DEBUG] returning value: 3 +``` + +**结论:** 第一个 policy 的 lambda 被错误编译——`if (x > 0)` 条件被消除。 +fprintf 在 `PolicyModel::invoke()` 中作为副作用阻止了该错误优化。 + +## 已尝试但无效的修复 + +1. `__attribute__((noinline, optnone))` on `PolicyModel::invoke()` — 编译错误(与 `[[nodiscard]]` 冲突) +2. `[[clang::noinline]]` on `PolicyModel::invoke()` — 编译通过但测试仍失败 +3. `[[clang::optnone]]` on `PolicyModel::invoke()` — 编译通过但测试仍失败 +4. `[[gnu::noinline, gnu::optnone]]` — Clang 不认识 `[[gnu::optnone]]`,被忽略 + +## 调用链分析 + +``` +PolicyChain::execute(args) + → policy_chain_detail::invoke_policy(policy, args) [noinline, optnone] ← 无效 + → PolicyEntry::operator()(args) + → PolicyConcept::invoke(args) [virtual dispatch] + → PolicyModel::invoke(args) ← lambda 在这里被调用 + → std::invoke(policy_, args...) ← lambda body 被错误优化 +``` + +**根因:** Clang 18 `-O3` 在编译 `PolicyModel::invoke()` 时, +将 lambda body 内联后错误优化掉了条件分支。`invoke_policy` 的 barrier 在调用链外层, +无法保护 `invoke()` 内部的 lambda 编译。 + +## 根因分析(2026-05-23 反汇编确认) + +反汇编显示,lambda 的条件分支并不是简单被删除,而是 Clang 18 在 `-O3` +下把 `std::optional` 的小对象返回值打包进寄存器时**污染了 engaged byte**。 + +失败用例的第一条 policy 生成了类似逻辑: + +```asm +xor %eax,%eax +test %edi,%edi +setg %al +shl $0x20,%rax ; has_value 放到 bit 32 +mov %edi,%ecx +lea (%rax,%rcx,2),%rax +``` + +当 `x == -3` 时,`x * 2` 的 64-bit `lea` 结果为 `0x00000001fffffffa`, +bit 32 被算术进位置 1,导致 disengaged optional 被调用方看成 +`has_value == true`,值为 `-6`。 + +### 试过但无效 + +- 把 lambda 调用包进新的 `noinline, optnone invoke_stored_policy()`:无效,因为 + lambda `operator()` 仍会作为单独 O3 函数生成坏的返回打包。 +- `std::function(int)>` 最小复现:同样失败。 +- `asm volatile("" : "+m"(result) : : "memory")`:无效,只是把已经污染的 + packed register 存回内存。 + +## 有效修复 + +在 `PolicyModel::invoke()` 中先接住 `std::invoke()` 的结果,再针对 Clang < 19 +显式重建 nullopt 路径: + +```cpp +auto result = std::invoke(policy_, args...); +#if defined(__clang__) && (__clang_major__ < 19) +if (!result.has_value()) { + return std::nullopt; +} +#endif +return result; +``` + +这会让 Clang 18 保留 disengaged 分支,避免直接复用被污染的 packed register。 + +## 验证 + +- Docker/CI Clang 18:`ctest --test-dir out/build_ci_clang/test -R policy_chain --output-on-failure` + 全部通过,1/1 test passed。 +- 本地 develop 构建:`./out/build_develop/test/bin/policy_chain_test` + 全部通过,29/29 tests passed。 diff --git a/document/notes/index.md b/document/notes/index.md index 970577ff3..0254c0f99 100644 --- a/document/notes/index.md +++ b/document/notes/index.md @@ -70,6 +70,25 @@ description: 本文档系列详细介绍了桌面应用程序中窗口行为建 --- +### [05 - Logger 单例链接架构](05-Logger-Singleton-Link-Architecture.md) + +讲解日志子系统的单例与链接层架构设计。 + +--- + +### [06 - PolicyChain Clang 18 Miscompilation 调试记录](06-PolicyChain-Clang18-Miscompilation-Debug.md) + +记录 Clang 18 `-O3` 下 `policy_chain` 测试失败的完整调试过程,包括: + +- 复现命令与失败模式分析 +- fprintf 实验定位问题层 +- 反汇编确认根因:`std::optional` 返回值寄存器打包时 engaged byte 被污染 +- 无效修复尝试记录与最终 workaround + +**适用场景**:遇到编译器优化导致的 `std::optional` / type-erasure 返回值异常时参考。 + +--- + ## 核心概念 ### DesktopBehaviors diff --git a/example/base/common/example_policy_chain.cpp b/example/base/common/example_policy_chain.cpp index 551b77c2b..ae0ed1c4d 100644 --- a/example/base/common/example_policy_chain.cpp +++ b/example/base/common/example_policy_chain.cpp @@ -1,6 +1,7 @@ #include "base/policy_chain/policy_chain.hpp" #include #include +#include void example_basic_usage() { std::cout << "=== Basic Usage ===" << std::endl; diff --git a/scripts/build_helpers/.gitignore b/scripts/build_helpers/.gitignore index 142c5ef2a..ea55ab1cf 100644 --- a/scripts/build_helpers/.gitignore +++ b/scripts/build_helpers/.gitignore @@ -2,4 +2,6 @@ *.ini !build_ci_config.ini !build_ci_aarch64_config.ini -!build_ci_armhf_config.ini \ No newline at end of file +!build_ci_armhf_config.ini +!build_ci_clang_config.ini +!build_ci_windows_config.ini \ No newline at end of file diff --git a/scripts/build_helpers/build_ci_clang_config.ini b/scripts/build_helpers/build_ci_clang_config.ini new file mode 100644 index 000000000..cc9471d8e --- /dev/null +++ b/scripts/build_helpers/build_ci_clang_config.ini @@ -0,0 +1,23 @@ +# Build Configuration for CFDesktop (Docker CI - Clang) +# Clang x86_64 build configuration for CI + +[cmake] +# CMake generator for Linux containers +generator=Unix Makefiles + +# Toolchain for CI Clang builds in Docker (x86_64) +toolchain=linux/ci-clang-x86_64 + +# Build type: Release for CI/production +build_type=Release + +[paths] +# Source directory (relative to project root, use "." for root) +source=. + +# Build output directory (relative to project root) +build_dir=out/build_ci_clang + +[options] +# Parallel jobs for compilation +jobs=16 diff --git a/scripts/build_helpers/build_ci_windows_config.ini b/scripts/build_helpers/build_ci_windows_config.ini new file mode 100644 index 000000000..4db30c8ea --- /dev/null +++ b/scripts/build_helpers/build_ci_windows_config.ini @@ -0,0 +1,23 @@ +# Build Configuration for CFDesktop (Windows CI - MSVC) +# MSVC x86_64 build configuration for GitHub Actions CI + +[cmake] +# CMake generator for CI: Ninja is faster and more reliable than VS generator +generator=Ninja + +# Toolchain for CI MSVC builds +toolchain=windows/ci-msvc + +# Build type: Release for CI/production +build_type=Release + +[paths] +# Source directory (relative to project root, use "." for root) +source=. + +# Build output directory (relative to project root) +build_dir=out/build_ci_windows + +[options] +# Parallel jobs for compilation +jobs=16 diff --git a/scripts/build_helpers/windows_configure.ps1 b/scripts/build_helpers/windows_configure.ps1 index ae4307aee..2b2857e23 100644 --- a/scripts/build_helpers/windows_configure.ps1 +++ b/scripts/build_helpers/windows_configure.ps1 @@ -3,7 +3,6 @@ # Usage: .\windows_configure.ps1 [-Config ] param( [Parameter(Position=0)] - [ValidateSet("develop", "deploy")] [string]$Config = "develop" ) @@ -34,12 +33,15 @@ Write-LogInfo "Changing to project directory" Set-Location $ProjectRoot # Determine which config file to use -if ($Config -eq "deploy") { - $ConfigFile = Join-Path $ScriptDir "build_deploy_config.ini" -} -else { - $ConfigFile = Join-Path $ScriptDir "build_develop_config.ini" +$ConfigFileName = switch ($Config) { + "develop" { "build_develop_config.ini" } + "deploy" { "build_deploy_config.ini" } + "ci" { "build_ci_windows_config.ini" } + default { + if ($Config -like "*.ini") { $Config } else { "$Config.ini" } + } } +$ConfigFile = Join-Path $ScriptDir $ConfigFileName Write-LogInfo "Loading configuration from: $ConfigFile" diff --git a/scripts/build_helpers/windows_develop_build.ps1 b/scripts/build_helpers/windows_develop_build.ps1 index 47bcd326c..1cab65747 100644 --- a/scripts/build_helpers/windows_develop_build.ps1 +++ b/scripts/build_helpers/windows_develop_build.ps1 @@ -1,5 +1,10 @@ # This script cleans the build directory then calls the fast version to build +param( + [Parameter(Mandatory = $false)] + [string]$Config = "develop" +) + # 导入库模块 $LibDir = Join-Path (Split-Path -Parent $PSScriptRoot) "lib\powershell" Import-Module (Join-Path $LibDir "LibCommon.psm1") -Force @@ -28,7 +33,16 @@ Write-LogInfo "Project root: $ProjectRoot" Set-Location $ProjectRoot # Load configuration from INI file -$ConfigFile = Join-Path $ScriptDir "build_develop_config.ini" +$ConfigFileName = switch ($Config) { + "develop" { "build_develop_config.ini" } + "deploy" { "build_deploy_config.ini" } + "ci" { "build_ci_windows_config.ini" } + default { + # Treat as direct filename for custom configs + if ($Config -like "*.ini") { $Config } else { "$Config.ini" } + } +} +$ConfigFile = Join-Path $ScriptDir $ConfigFileName Write-LogInfo "Loading configuration from: $ConfigFile" # Safety check: config file must exist @@ -43,7 +57,7 @@ if (!(Test-Path $ConfigFile)) { } try { - $Config = Get-IniConfig -FilePath $ConfigFile + $ConfigData = Get-IniConfig -FilePath $ConfigFile Write-LogSuccess "Configuration loaded successfully!" } catch { @@ -52,7 +66,7 @@ catch { } # Extract configuration values -$BuildDir = $Config["paths"]["build_dir"] +$BuildDir = $ConfigData["paths"]["build_dir"] # Safety check: BUILD_DIR must not be empty if ([string]::IsNullOrWhiteSpace($BuildDir)) { @@ -91,10 +105,10 @@ Write-LogInfo "Step 2: Calling fast build script" Write-LogSeparator $FastBuildScript = Join-Path $ScriptDir "windows_fast_develop_build.ps1" -Write-LogInfo "Executing: $FastBuildScript" +Write-LogInfo "Executing: $FastBuildScript -Config $Config" try { - & $FastBuildScript + & $FastBuildScript -Config $Config if ($LASTEXITCODE -eq 0) { Write-LogSeparator Write-LogSuccess "Build process completed successfully!" @@ -116,10 +130,10 @@ Write-LogInfo "Step 3: Running tests" Write-LogSeparator $TestScript = Join-Path $ScriptDir "windows_run_tests.ps1" -Write-LogInfo "Executing: $TestScript -Config develop" +Write-LogInfo "Executing: $TestScript -Config $Config" try { - & $TestScript -Config "develop" + & $TestScript -Config $Config if ($LASTEXITCODE -eq 0) { Write-LogSeparator Write-LogSuccess "All tests passed successfully!" diff --git a/scripts/build_helpers/windows_fast_develop_build.ps1 b/scripts/build_helpers/windows_fast_develop_build.ps1 index 0f27a6ffe..ca597620b 100644 --- a/scripts/build_helpers/windows_fast_develop_build.ps1 +++ b/scripts/build_helpers/windows_fast_develop_build.ps1 @@ -1,6 +1,11 @@ # This script configures and builds the project (FAST version - no cleaning) # It calls the configure script first, then builds +param( + [Parameter(Mandatory = $false)] + [string]$Config = "develop" +) + # 导入库模块 $LibDir = Join-Path (Split-Path -Parent $PSScriptRoot) "lib\powershell" Import-Module (Join-Path $LibDir "LibCommon.psm1") -Force @@ -34,10 +39,10 @@ Write-LogInfo "Step 1: Configuring with CMake" Write-LogSeparator $ConfigureScript = Join-Path $ScriptDir "windows_configure.ps1" -Write-LogInfo "Executing: $ConfigureScript -Config develop" +Write-LogInfo "Executing: $ConfigureScript -Config $Config" try { - & $ConfigureScript -Config "develop" + & $ConfigureScript -Config $Config if ($LASTEXITCODE -ne 0) { Write-LogError "Configure script failed with exit code: $LASTEXITCODE" Stop-BuildTimer @@ -51,7 +56,15 @@ catch { } # Step 2: Load config for build -$ConfigFile = Join-Path $ScriptDir "build_develop_config.ini" +$ConfigFileName = switch ($Config) { + "develop" { "build_develop_config.ini" } + "deploy" { "build_deploy_config.ini" } + "ci" { "build_ci_windows_config.ini" } + default { + if ($Config -like "*.ini") { $Config } else { "$Config.ini" } + } +} +$ConfigFile = Join-Path $ScriptDir $ConfigFileName # Safety check: config file must exist if (!(Test-Path $ConfigFile)) { @@ -64,17 +77,36 @@ if (!(Test-Path $ConfigFile)) { exit 1 } -$Config = Get-IniConfig -FilePath $ConfigFile -$BuildDir = $Config["paths"]["build_dir"] +try { + $ConfigData = Get-IniConfig -FilePath $ConfigFile +} +catch { + Write-LogError "Failed to load configuration: $_" + Stop-BuildTimer + exit 1 +} + +if (-not $ConfigData -or -not $ConfigData.ContainsKey("paths")) { + Write-LogError "Configuration error: [paths] section is missing in config file" + Write-LogError "Please check: $ConfigFile" + Stop-BuildTimer + exit 1 +} + +$BuildDir = $ConfigData["paths"]["build_dir"] # Safety check: BUILD_DIR must not be empty if ([string]::IsNullOrWhiteSpace($BuildDir)) { Write-LogError "Configuration error: build_dir is not set in config file" Write-LogError "Please check the [paths] section in: $ConfigFile" + Stop-BuildTimer exit 1 } -$Jobs = if ($Config["options"] -and $Config["options"]["jobs"]) { $Config["options"]["jobs"] } else { "" } +$Jobs = "" +if ($ConfigData.ContainsKey("options") -and $ConfigData["options"].ContainsKey("jobs")) { + $Jobs = $ConfigData["options"]["jobs"] +} # Step 3: Build with CMake Write-LogSeparator diff --git a/scripts/build_helpers/windows_run_tests.ps1 b/scripts/build_helpers/windows_run_tests.ps1 index 048f569d9..c865f24aa 100644 --- a/scripts/build_helpers/windows_run_tests.ps1 +++ b/scripts/build_helpers/windows_run_tests.ps1 @@ -33,9 +33,12 @@ Set-Location $ProjectRoot $ConfigFile = switch ($Config) { "develop" { "build_develop_config.ini" } "deploy" { "build_deploy_config.ini" } + "ci" { "build_ci_windows_config.ini" } default { - Write-LogError "Unknown config: $Config. Valid options are: develop, deploy" - exit 1 + if ($Config -like "*.ini") { $Config } else { + Write-LogError "Unknown config: $Config. Valid options are: develop, deploy, ci" + exit 1 + } } } diff --git a/scripts/dependency/install_build_dependencies.sh b/scripts/dependency/install_build_dependencies.sh index 16a5a87ca..5c83c7f58 100755 --- a/scripts/dependency/install_build_dependencies.sh +++ b/scripts/dependency/install_build_dependencies.sh @@ -102,81 +102,93 @@ main() { # 标记是否已成功配置镜像(避免重复 apt update) _mirror_configured=false - # 检查 ca-certificates 是否已安装 - if ! dpkg -s ca-certificates &>/dev/null; then - # 未安装 CA 证书,先用 HTTP 镜像绕过 - log_warn "ca-certificates missing, trying HTTP bootstrap..." - - _write_http_mirror() { - local mirror="$1" - local sources_file="/etc/apt/sources.list.d/ubuntu.sources" - [ -f "$sources_file" ] || return 1 - cp "$sources_file" "${sources_file}.bak" - local ports_mirror="$mirror" - [[ "$mirror" != *"/ubuntu-ports" ]] && ports_mirror="${mirror%/ubuntu}/ubuntu-ports" - sed -i \ - -e "s|https\?://archive.ubuntu.com/ubuntu|$mirror|g" \ - -e "s|https\?://security.ubuntu.com/ubuntu|$mirror|g" \ - -e "s|https\?://ports.ubuntu.com/ubuntu-ports|$ports_mirror|g" \ - "$sources_file" - } - - local arch="$(uname -m)" - local http_mirror="http://mirrors.aliyun.com/ubuntu" - [[ "$arch" == "aarch64" ]] && http_mirror="http://mirrors.tuna.tsinghua.edu.cn/ubuntu-ports" - HTTP_MIRROR="${QT_MIRROR_APT:-$http_mirror}" - - if _write_http_mirror "$HTTP_MIRROR" && timeout 60 apt update 2>/dev/null; then - log_success "HTTP mirror active: $HTTP_MIRROR" - rm -f /etc/apt/sources.list.d/ubuntu.sources.bak 2>/dev/null || true - _mirror_configured=true - else - log_warn "HTTP mirror failed, using default sources" - cp /etc/apt/sources.list.d/ubuntu.sources.bak /etc/apt/sources.list.d/ubuntu.sources 2>/dev/null || true - fi + # CI 环境(GitHub Actions 等)直接使用默认 Ubuntu 源,跳过镜像配置 + if [[ "${CI:-}" == "true" ]]; then + log_info "CI environment detected, using default Ubuntu sources" + apt update + _mirror_configured=true fi - # 如果还没配置成功镜像,配置 HTTPS 镜像 + # 非 CI 环境配置国内镜像加速(仅本地 / 开发 Docker 使用) if [[ "$_mirror_configured" == "false" ]]; then - local arch="$(uname -m)" - local mirrors=() - if [[ "$arch" == "aarch64" ]]; then - mirrors=( - "https://mirrors.tuna.tsinghua.edu.cn/ubuntu-ports" - "https://mirrors.ustc.edu.cn/ubuntu-ports" - ) - else - mirrors=( - "https://mirrors.aliyun.com/ubuntu" - "https://mirrors.tuna.tsinghua.edu.cn/ubuntu" - ) - fi - - local sources_file="/etc/apt/sources.list.d/ubuntu.sources" - if [ -f "$sources_file" ]; then - cp "$sources_file" "${sources_file}.bak" - for mirror in "${mirrors[@]}"; do - log_info "Trying APT mirror: $mirror" + # 检查 ca-certificates 是否已安装 + if ! dpkg -s ca-certificates &>/dev/null; then + # 未安装 CA 证书,先用 HTTP 镜像绕过 + log_warn "ca-certificates missing, trying HTTP bootstrap..." + + _write_http_mirror() { + local mirror="$1" + local sources_file="/etc/apt/sources.list.d/ubuntu.sources" + [ -f "$sources_file" ] || return 1 + cp "$sources_file" "${sources_file}.bak" local ports_mirror="$mirror" [[ "$mirror" != *"/ubuntu-ports" ]] && ports_mirror="${mirror%/ubuntu}/ubuntu-ports" sed -i \ - -e "s|http://archive.ubuntu.com/ubuntu|$mirror|g" \ - -e "s|https://archive.ubuntu.com/ubuntu|$mirror|g" \ - -e "s|http://security.ubuntu.com/ubuntu|$mirror|g" \ - -e "s|http://ports.ubuntu.com/ubuntu-ports|$ports_mirror|g" \ - -e "s|https://ports.ubuntu.com/ubuntu-ports|$ports_mirror|g" \ + -e "s|https\?://archive.ubuntu.com/ubuntu|$mirror|g" \ + -e "s|https\?://security.ubuntu.com/ubuntu|$mirror|g" \ + -e "s|https\?://ports.ubuntu.com/ubuntu-ports|$ports_mirror|g" \ "$sources_file" + } + + local arch="$(uname -m)" + local http_mirror="http://mirrors.aliyun.com/ubuntu" + [[ "$arch" == "aarch64" ]] && http_mirror="http://mirrors.tuna.tsinghua.edu.cn/ubuntu-ports" + HTTP_MIRROR="${QT_MIRROR_APT:-$http_mirror}" + + if _write_http_mirror "$HTTP_MIRROR" && timeout 300 apt update 2>/dev/null; then + log_success "HTTP mirror active: $HTTP_MIRROR" + rm -f /etc/apt/sources.list.d/ubuntu.sources.bak 2>/dev/null || true + _mirror_configured=true + apt install -y --no-install-recommends ca-certificates 2>/dev/null && \ + log_success "ca-certificates installed via HTTP bootstrap" + else + log_warn "HTTP mirror failed, using default sources" + cp /etc/apt/sources.list.d/ubuntu.sources.bak /etc/apt/sources.list.d/ubuntu.sources 2>/dev/null || true + fi + fi - if timeout 60 apt update 2>/dev/null; then - log_success "APT mirror active: $mirror" - rm -f "${sources_file}.bak" 2>/dev/null || true - _mirror_configured=true - break - fi - log_warn "Mirror failed: $mirror, trying next..." - cp "${sources_file}.bak" "$sources_file" - done - rm -f "${sources_file}.bak" 2>/dev/null || true + # 如果还没配置成功镜像,配置 HTTPS 镜像(需要 ca-certificates) + if [[ "$_mirror_configured" == "false" ]] && dpkg -s ca-certificates &>/dev/null; then + local arch="$(uname -m)" + local mirrors=() + if [[ "$arch" == "aarch64" ]]; then + mirrors=( + "https://mirrors.tuna.tsinghua.edu.cn/ubuntu-ports" + "https://mirrors.ustc.edu.cn/ubuntu-ports" + ) + else + mirrors=( + "https://mirrors.aliyun.com/ubuntu" + "https://mirrors.tuna.tsinghua.edu.cn/ubuntu" + ) + fi + + local sources_file="/etc/apt/sources.list.d/ubuntu.sources" + if [ -f "$sources_file" ]; then + cp "$sources_file" "${sources_file}.bak" + for mirror in "${mirrors[@]}"; do + log_info "Trying APT mirror: $mirror" + local ports_mirror="$mirror" + [[ "$mirror" != *"/ubuntu-ports" ]] && ports_mirror="${mirror%/ubuntu}/ubuntu-ports" + sed -i \ + -e "s|http://archive.ubuntu.com/ubuntu|$mirror|g" \ + -e "s|https://archive.ubuntu.com/ubuntu|$mirror|g" \ + -e "s|http://security.ubuntu.com/ubuntu|$mirror|g" \ + -e "s|http://ports.ubuntu.com/ubuntu-ports|$ports_mirror|g" \ + -e "s|https://ports.ubuntu.com/ubuntu-ports|$ports_mirror|g" \ + "$sources_file" + + if timeout 60 apt update 2>/dev/null; then + log_success "APT mirror active: $mirror" + rm -f "${sources_file}.bak" 2>/dev/null || true + _mirror_configured=true + break + fi + log_warn "Mirror failed: $mirror, trying next..." + cp "${sources_file}.bak" "$sources_file" + done + rm -f "${sources_file}.bak" 2>/dev/null || true + fi fi fi @@ -187,7 +199,7 @@ main() { # 注意:ca-certificates 包含在此列表,用于 HTTP bootstrap 后的补装 apt install -y --no-install-recommends \ `# === 编译工具链 ===` \ - build-essential cmake ninja-build ccache git wget curl \ + build-essential clang cmake ninja-build ccache git wget curl \ `# === Python ===` \ python3 python3-pip \ `# === 证书(Bootstrap 时可能未安装)===` \ diff --git a/scripts/docker/Dockerfile.build b/scripts/docker/Dockerfile.build index 666164da0..f1164183c 100644 --- a/scripts/docker/Dockerfile.build +++ b/scripts/docker/Dockerfile.build @@ -48,6 +48,9 @@ ENV DEBIAN_FRONTEND=noninteractive # Set INSTALL_DEPS=0 to skip automatic dependency installation # Useful for interactive mode where you want to install manually # ============================================================================= +ARG CI=false +ENV CI=${CI} + ARG INSTALL_DEPS=1 COPY scripts/dependency/install_build_dependencies.sh /tmp/install_deps.sh diff --git a/ui/CMakeLists.txt b/ui/CMakeLists.txt index 2e3ddf4a1..d70b0baee 100644 --- a/ui/CMakeLists.txt +++ b/ui/CMakeLists.txt @@ -28,11 +28,7 @@ add_subdirectory(models) add_library(cfui SHARED) -# Force all objects into cfui.dll (PRIVATE: only affects cfui's own link step). -# cfui has no own sources, so the linker would otherwise skip static lib objects. -# Note: cf_ui_widget is an INTERFACE library — use its actual static lib deps instead. -target_link_libraries(cfui PRIVATE - -Wl,--whole-archive +set(CFUI_STATIC_LIBS cf_ui_base cf_ui_core cf_ui_core_material @@ -40,9 +36,26 @@ target_link_libraries(cfui PRIVATE cf_ui_application_support cf_ui_components_material cf_ui_components - -Wl,--no-whole-archive ) +# Force all objects into cfui.dll (PRIVATE: only affects cfui's own link step). +# cfui has no own sources, so the linker would otherwise skip static lib objects. +# Note: cf_ui_widget is an INTERFACE library — use its actual static lib deps instead. +if(MSVC) + target_link_libraries(cfui PRIVATE ${CFUI_STATIC_LIBS}) + foreach(_cfui_module IN LISTS CFUI_STATIC_LIBS) + target_link_options(cfui PRIVATE "/WHOLEARCHIVE:$") + endforeach() +elseif(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang" AND NOT APPLE) + target_link_libraries(cfui PRIVATE + -Wl,--whole-archive + ${CFUI_STATIC_LIBS} + -Wl,--no-whole-archive + ) +else() + target_link_libraries(cfui PRIVATE ${CFUI_STATIC_LIBS}) +endif() + # Set include directories target_include_directories(cfui PUBLIC $ diff --git a/ui/widget/material/widget/button/button.cpp b/ui/widget/material/widget/button/button.cpp index 6ca7633af..bba9f39aa 100644 --- a/ui/widget/material/widget/button/button.cpp +++ b/ui/widget/material/widget/button/button.cpp @@ -65,11 +65,12 @@ Button::Button(ButtonVariant variant, QWidget* parent) : QPushButton(parent), va m_ripple->setMode(RippleHelper::Mode::Bounded); // Connect repaint signals - connect(m_ripple, &RippleHelper::repaintNeeded, this, QOverload<>::of(&Button::update)); + connect(m_ripple, &RippleHelper::repaintNeeded, this, + static_cast(&QWidget::update)); connect(m_stateMachine, &StateMachine::stateLayerOpacityChanged, this, - QOverload<>::of(&Button::update)); + static_cast(&QWidget::update)); connect(m_elevation, &MdElevationController::pressOffsetChanged, this, - QOverload<>::of(&Button::update)); + static_cast(&QWidget::update)); // Set default font setFont(labelFont()); diff --git a/ui/widget/material/widget/checkbox/checkbox.cpp b/ui/widget/material/widget/checkbox/checkbox.cpp index 5388a74a2..21e47f8ca 100644 --- a/ui/widget/material/widget/checkbox/checkbox.cpp +++ b/ui/widget/material/widget/checkbox/checkbox.cpp @@ -62,9 +62,10 @@ CheckBox::CheckBox(QWidget* parent) : QCheckBox(parent) { m_ripple->setMode(RippleHelper::Mode::Bounded); // Connect repaint signals - connect(m_ripple, &RippleHelper::repaintNeeded, this, QOverload<>::of(&CheckBox::update)); + connect(m_ripple, &RippleHelper::repaintNeeded, this, + static_cast(&QWidget::update)); connect(m_stateMachine, &StateMachine::stateLayerOpacityChanged, this, - QOverload<>::of(&CheckBox::update)); + static_cast(&QWidget::update)); // Initialize check animation based on current state // Both PartiallyChecked and Checked use 1.0 for full mark visibility diff --git a/ui/widget/material/widget/comboBox/combobox.cpp b/ui/widget/material/widget/comboBox/combobox.cpp index d919dde9d..af70e4ca3 100644 --- a/ui/widget/material/widget/comboBox/combobox.cpp +++ b/ui/widget/material/widget/comboBox/combobox.cpp @@ -88,9 +88,10 @@ ComboBox::ComboBox(QWidget* parent) : QComboBox(parent), variant_(ComboBoxVarian m_ripple->setMode(RippleHelper::Mode::Bounded); // Connect repaint signals - connect(m_ripple, &RippleHelper::repaintNeeded, this, QOverload<>::of(&ComboBox::update)); + connect(m_ripple, &RippleHelper::repaintNeeded, this, + static_cast(&QWidget::update)); connect(m_stateMachine, &StateMachine::stateLayerOpacityChanged, this, - QOverload<>::of(&ComboBox::update)); + static_cast(&QWidget::update)); // Initialize popup animation m_popupAnimation = new QPropertyAnimation(this); diff --git a/ui/widget/material/widget/doublespinbox/doublespinbox.cpp b/ui/widget/material/widget/doublespinbox/doublespinbox.cpp index 8b282c32c..f054c0eb9 100644 --- a/ui/widget/material/widget/doublespinbox/doublespinbox.cpp +++ b/ui/widget/material/widget/doublespinbox/doublespinbox.cpp @@ -102,9 +102,10 @@ DoubleSpinBox::DoubleSpinBox(QWidget* parent) m_ripple->setMode(RippleHelper::Mode::Bounded); // Connect repaint signals - connect(m_ripple, &RippleHelper::repaintNeeded, this, QOverload<>::of(&DoubleSpinBox::update)); + connect(m_ripple, &RippleHelper::repaintNeeded, this, + static_cast(&QWidget::update)); connect(m_stateMachine, &StateMachine::stateLayerOpacityChanged, this, - QOverload<>::of(&DoubleSpinBox::update)); + static_cast(&QWidget::update)); // Set default font setFont(textFont()); diff --git a/ui/widget/material/widget/listview/listview.cpp b/ui/widget/material/widget/listview/listview.cpp index fc08c15b1..1340d2189 100644 --- a/ui/widget/material/widget/listview/listview.cpp +++ b/ui/widget/material/widget/listview/listview.cpp @@ -106,9 +106,10 @@ ListView::ListView(QWidget* parent) viewport()->setMouseTracking(true); // Connect repaint signals - connect(m_ripple, &RippleHelper::repaintNeeded, viewport(), QOverload<>::of(&QWidget::update)); + connect(m_ripple, &RippleHelper::repaintNeeded, viewport(), + static_cast(&QWidget::update)); connect(m_stateMachine, &StateMachine::stateLayerOpacityChanged, this, - QOverload<>::of(&ListView::update)); + static_cast(&QWidget::update)); // Set default selection mode setSelectionMode(QAbstractItemView::SingleSelection); diff --git a/ui/widget/material/widget/progressbar/progressbar.cpp b/ui/widget/material/widget/progressbar/progressbar.cpp index 0adf641ad..50d3c515c 100644 --- a/ui/widget/material/widget/progressbar/progressbar.cpp +++ b/ui/widget/material/widget/progressbar/progressbar.cpp @@ -70,7 +70,7 @@ ProgressBar::ProgressBar(QWidget* parent) : QProgressBar(parent) { // Connect repaint signals connect(m_stateMachine, &StateMachine::stateLayerOpacityChanged, this, - QOverload<>::of(&ProgressBar::update)); + static_cast(&QWidget::update)); // Start indeterminate animation if in indeterminate mode if (minimum() == 0 && maximum() == 0) { diff --git a/ui/widget/material/widget/radiobutton/radiobutton.cpp b/ui/widget/material/widget/radiobutton/radiobutton.cpp index e19b79d6d..125e798dd 100644 --- a/ui/widget/material/widget/radiobutton/radiobutton.cpp +++ b/ui/widget/material/widget/radiobutton/radiobutton.cpp @@ -91,9 +91,10 @@ RadioButton::RadioButton(QWidget* parent) : QRadioButton(parent) { m_ripple->setMode(RippleHelper::Mode::Bounded); // Connect repaint signals - connect(m_ripple, &RippleHelper::repaintNeeded, this, QOverload<>::of(&RadioButton::update)); + connect(m_ripple, &RippleHelper::repaintNeeded, this, + static_cast(&QWidget::update)); connect(m_stateMachine, &StateMachine::stateLayerOpacityChanged, this, - QOverload<>::of(&RadioButton::update)); + static_cast(&QWidget::update)); // Set default font setFont(labelFont()); diff --git a/ui/widget/material/widget/slider/slider.cpp b/ui/widget/material/widget/slider/slider.cpp index 45d279ccf..75de40059 100644 --- a/ui/widget/material/widget/slider/slider.cpp +++ b/ui/widget/material/widget/slider/slider.cpp @@ -84,11 +84,12 @@ Slider::Slider(QWidget* parent) : QSlider(Qt::Horizontal, parent) { m_elevation->setElevation(1); // Connect repaint signals - connect(m_ripple, &RippleHelper::repaintNeeded, this, QOverload<>::of(&Slider::update)); + connect(m_ripple, &RippleHelper::repaintNeeded, this, + static_cast(&QWidget::update)); connect(m_stateMachine, &StateMachine::stateLayerOpacityChanged, this, - QOverload<>::of(&Slider::update)); + static_cast(&QWidget::update)); connect(m_elevation, &MdElevationController::pressOffsetChanged, this, - QOverload<>::of(&Slider::update)); + static_cast(&QWidget::update)); // Set default cursor setCursor(Qt::PointingHandCursor); @@ -119,11 +120,12 @@ Slider::Slider(Qt::Orientation orientation, QWidget* parent) : QSlider(orientati m_elevation->setElevation(1); // Connect repaint signals - connect(m_ripple, &RippleHelper::repaintNeeded, this, QOverload<>::of(&Slider::update)); + connect(m_ripple, &RippleHelper::repaintNeeded, this, + static_cast(&QWidget::update)); connect(m_stateMachine, &StateMachine::stateLayerOpacityChanged, this, - QOverload<>::of(&Slider::update)); + static_cast(&QWidget::update)); connect(m_elevation, &MdElevationController::pressOffsetChanged, this, - QOverload<>::of(&Slider::update)); + static_cast(&QWidget::update)); // Set default cursor setCursor(Qt::PointingHandCursor); diff --git a/ui/widget/material/widget/spinbox/spinbox.cpp b/ui/widget/material/widget/spinbox/spinbox.cpp index bde0ce00e..cf5774795 100644 --- a/ui/widget/material/widget/spinbox/spinbox.cpp +++ b/ui/widget/material/widget/spinbox/spinbox.cpp @@ -97,9 +97,10 @@ SpinBox::SpinBox(QWidget* parent) m_ripple->setMode(RippleHelper::Mode::Bounded); // Connect repaint signals - connect(m_ripple, &RippleHelper::repaintNeeded, this, QOverload<>::of(&SpinBox::update)); + connect(m_ripple, &RippleHelper::repaintNeeded, this, + static_cast(&QWidget::update)); connect(m_stateMachine, &StateMachine::stateLayerOpacityChanged, this, - QOverload<>::of(&SpinBox::update)); + static_cast(&QWidget::update)); // Set default font setFont(textFont()); diff --git a/ui/widget/material/widget/switch/switch.cpp b/ui/widget/material/widget/switch/switch.cpp index cc180799c..7bda094af 100644 --- a/ui/widget/material/widget/switch/switch.cpp +++ b/ui/widget/material/widget/switch/switch.cpp @@ -80,11 +80,12 @@ Switch::Switch(QWidget* parent) : QCheckBox(parent) { m_elevation->setElevation(1); // Connect repaint signals - connect(m_ripple, &RippleHelper::repaintNeeded, this, QOverload<>::of(&Switch::update)); + connect(m_ripple, &RippleHelper::repaintNeeded, this, + static_cast(&QWidget::update)); connect(m_stateMachine, &StateMachine::stateLayerOpacityChanged, this, - QOverload<>::of(&Switch::update)); + static_cast(&QWidget::update)); connect(m_elevation, &MdElevationController::pressOffsetChanged, this, - QOverload<>::of(&Switch::update)); + static_cast(&QWidget::update)); // Initialize thumb position based on current state m_thumbPosition = isChecked() ? 1.0f : 0.0f; diff --git a/ui/widget/material/widget/tableview/tableview.cpp b/ui/widget/material/widget/tableview/tableview.cpp index ec0cc2e48..de4fd18f6 100644 --- a/ui/widget/material/widget/tableview/tableview.cpp +++ b/ui/widget/material/widget/tableview/tableview.cpp @@ -83,9 +83,10 @@ TableView::TableView(QWidget* parent) m_ripple->setMode(RippleHelper::Mode::Bounded); // Connect repaint signals - connect(m_ripple, &RippleHelper::repaintNeeded, this, QOverload<>::of(&TableView::update)); + connect(m_ripple, &RippleHelper::repaintNeeded, this, + static_cast(&QWidget::update)); connect(m_stateMachine, &StateMachine::stateLayerOpacityChanged, this, - QOverload<>::of(&TableView::update)); + static_cast(&QWidget::update)); // Configure default QTableView properties for Material rendering setAttribute(Qt::WA_Hover, true); diff --git a/ui/widget/material/widget/tabview/private/materialtabbar.cpp b/ui/widget/material/widget/tabview/private/materialtabbar.cpp index e6370ceec..95e233bae 100644 --- a/ui/widget/material/widget/tabview/private/materialtabbar.cpp +++ b/ui/widget/material/widget/tabview/private/materialtabbar.cpp @@ -74,7 +74,7 @@ MaterialTabBar::MaterialTabBar(TabView* parent) m_focusIndicator = new MdFocusIndicator(m_animationFactory, this); connect(m_stateMachine, &StateMachine::stateLayerOpacityChanged, this, - QOverload<>::of(&MaterialTabBar::update)); + static_cast(&QWidget::update)); connect(this, &QTabBar::currentChanged, this, &MaterialTabBar::animateIndicatorTo); } diff --git a/ui/widget/material/widget/textarea/textarea.cpp b/ui/widget/material/widget/textarea/textarea.cpp index d8886127f..cb4026e24 100644 --- a/ui/widget/material/widget/textarea/textarea.cpp +++ b/ui/widget/material/widget/textarea/textarea.cpp @@ -99,9 +99,10 @@ TextArea::TextArea(TextAreaVariant variant, QWidget* parent) m_ripple->setMode(RippleHelper::Mode::Bounded); // Connect repaint signals - connect(m_ripple, &RippleHelper::repaintNeeded, this, QOverload<>::of(&TextArea::update)); + connect(m_ripple, &RippleHelper::repaintNeeded, this, + static_cast(&QWidget::update)); connect(m_stateMachine, &StateMachine::stateLayerOpacityChanged, this, - QOverload<>::of(&TextArea::update)); + static_cast(&QWidget::update)); // Connect text change signal connect(this, &QTextEdit::textChanged, this, &TextArea::textChanged); diff --git a/ui/widget/material/widget/textfield/textfield.cpp b/ui/widget/material/widget/textfield/textfield.cpp index 421c67d86..740852f8b 100644 --- a/ui/widget/material/widget/textfield/textfield.cpp +++ b/ui/widget/material/widget/textfield/textfield.cpp @@ -96,9 +96,10 @@ TextField::TextField(TextFieldVariant variant, QWidget* parent) m_ripple->setMode(RippleHelper::Mode::Bounded); // Connect repaint signals - connect(m_ripple, &RippleHelper::repaintNeeded, this, QOverload<>::of(&TextField::update)); + connect(m_ripple, &RippleHelper::repaintNeeded, this, + static_cast(&QWidget::update)); connect(m_stateMachine, &StateMachine::stateLayerOpacityChanged, this, - QOverload<>::of(&TextField::update)); + static_cast(&QWidget::update)); // Connect text change signal connect(this, &QLineEdit::textChanged, this, &TextField::textChanged); diff --git a/ui/widget/material/widget/treeview/treeview.cpp b/ui/widget/material/widget/treeview/treeview.cpp index d30b21b4a..eb025a0e4 100644 --- a/ui/widget/material/widget/treeview/treeview.cpp +++ b/ui/widget/material/widget/treeview/treeview.cpp @@ -81,9 +81,10 @@ TreeView::TreeView(QWidget* parent) // Connect repaint signals // QTreeView paints on viewport, so connect to viewport() for ripple updates - connect(m_ripple, &RippleHelper::repaintNeeded, viewport(), QOverload<>::of(&QWidget::update)); + connect(m_ripple, &RippleHelper::repaintNeeded, viewport(), + static_cast(&QWidget::update)); connect(m_stateMachine, &StateMachine::stateLayerOpacityChanged, this, - QOverload<>::of(&TreeView::update)); + static_cast(&QWidget::update)); // Configure tree view for Material appearance setHeaderHidden(true);