Notes of Effective CMake

目录

1. The Philosophy of Modern CMake

Why "Effective CMake"?

Just like with C++, the way you write CMake code significantly impacts your project's maintainability, ease of use for others, and scalability. Adopting modern practices is key.

CMake is Code

Treat your CMakeLists.txt files with the same care as your source code. Apply principles like Don't Repeat Yourself (DRY), keep it clean, and write comments where necessary.

2. The CMake Language: A Quick Tour

Organization

CMake code can be organized in three ways:

  • Directories (CMakeLists.txt): The entry point for a project or sub-project. add_subdirectory() adds another directory (which must contain a CMakeLists.txt) to the build.
  • Scripts (<name>.cmake): Executed with cmake -P <script>.cmake. They are useful for automation but cannot define build targets like executables or libraries. In other words, not all commands are supported.
  • Modules (<name>.cmake): Reusable code included in your projects via include(). They are located in the CMAKE_MODULE_PATH.

Commands

CMake commands are case-insensitive, but their arguments (including variable names) are case-sensitive.

# command_name(ARGUMENT1 ARGUMENT2 ...)
project(MyProject VERSION 1.0)

Variables

Variables are the backbone of CMake scripting.

# Set a variable
set(MY_VARIABLE "Hello") # Reference a variable (dereference)
message(STATUS "My variable is: ${MY_VARIABLE}") # Unset a variable
unset(MY_VARIABLE)

IMPORTANT

  • In CMake, everything is a string. Lists are just strings separated by semicolons ; (e.g., "item1;item2;item3").
  • An unset or undefined variable expands to an empty string. This can be a common source of bugs! Use if(DEFINED VAR_NAME) to check if a variable is set.

Comments

# This is a single-line comment.

#[=[
This is a multi-line comment.
It can contain other symbols and even # characters.
#]=]

Generator Expressions: The $<...> Syntax

Generator expressions, often called "genex," are a powerful CMake feature that uses the $<...> syntax. They are not evaluated when CMake first reads your CMakeLists.txt. Instead, they are written into the native build files (like Makefiles or Visual Studio projects) and are evaluated during the build process.

This delayed evaluation is crucial because it allows you to create build configurations that are aware of things that are only known at build time, such as the specific build type (Debug, Release), the compiler being used, or the language of a source file.

IMPORTANT

Think of generator expressions as placeholders that the final build tool (like Make, Ninja, or MSBuild) will fill in with the correct value at the right time. This is much more flexible than using if() statements in CMake, which are only evaluated once when you run cmake.

Common Use Cases and Examples

  1. Conditional Compilation Definitions ($<CONFIG:...>)

This is the most common use case. You want to define a preprocessor macro differently for Debug and Release builds.

# In Debug mode, VERBOSITY will be 2. In all other modes (e.g., Release), it will be 0.
target_compile_definitions(my_app PRIVATE
"VERBOSITY=$<IF:$<CONFIG:Debug>,2,0>"
)

The $<IF:condition,true_value,false_value> expression is evaluated at build time. If the configuration is Debug, it resolves to VERBOSITY=2; otherwise, it becomes VERBOSITY=0.

  1. Conditional Include Directories ($<BUILD_INTERFACE:...> and $<INSTALL_INTERFACE:...>)

A library often has different include paths when being built inside a project versus when it's installed on a system.

target_include_directories(my_lib PUBLIC
# When my_lib is built as part of this project, use the source directory.
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include> # When an external project uses an installed version of my_lib, the path is just 'include'.
$<INSTALL_INTERFACE:include>
)

This ensures your library is usable both during development and after deployment without any changes.

  1. Compiler-Specific Flags ($<CXX_COMPILER_ID:...>)

You can pass specific flags depending on the compiler being used (e.g., GCC, Clang, MSVC).

# Enable strong warnings, but use the correct flag for each compiler.
set(GCC_CLANG_WARNINGS "-Wall -Wextra -Wpedantic")
set(MSVC_WARNINGS "/W4") target_compile_options(my_app PRIVATE
"$<IF:$<CXX_COMPILER_ID:MSVC>,${MSVC_WARNINGS},${GCC_CLANG_WARNINGS}>"
)
  1. Language-Specific Standards ($<COMPILE_LANGUAGE:...>)

If your target mixes C and C++ code, you can set standards for each language.

target_compile_features(my_app PRIVATE
# Set C++ standard to 17 for all C++ files.
$<COMPILE_LANGUAGE:CXX>:cxx_std_17 # Set C standard to 11 for all C files.
$<COMPILE_LANGUAGE:C>:c_std_11
)

Generator expressions are a cornerstone of modern, robust, and portable CMake scripts. Mastering them allows you to write cleaner and more powerful CMakeLists.txt files.

Custom Commands: function() vs. macro()

You can create your own commands to reduce code duplication.

  • function(): Creates a new variable scope. To pass results back to the caller, you must use set(... PARENT_SCOPE).
  • macro(): Does not create a new scope. It performs simple text replacement, much like a C preprocessor macro.

TIP

Rule of Thumb:

  • Use function() by default to avoid polluting the caller's scope with side effects.
  • Use macro() only when you need to wrap a command that has an output parameter or when you explicitly want side effects in the caller's scope.

3. The Core of Modern CMake: Targets and Properties

Modern CMake revolves around targets and their properties. A target can be an executable, a library, or a custom target.

WARNING

Avoid directory-level commands like include_directories(), link_libraries(), and add_compile_options(). They use global state and make dependencies hard to reason about. Always prefer the target_* equivalents.

Thinking in Targets

Imagine targets as objects in an OOP language.

  • Constructor: add_executable(), add_library()
  • Member Functions: target_sources(), target_include_directories(), target_link_libraries(), etc.
  • Member Variables: Properties like VERSION, SOURCES, INTERFACE_INCLUDE_DIRECTORIES.
# Create a library target
add_library(my_lib STATIC my_lib.cpp my_lib.h) # Add properties to the target
target_include_directories(my_lib PUBLIC include)
target_compile_features(my_lib PUBLIC cxx_std_17)

Build Specifications vs. Usage Requirements

  • Non-INTERFACE_ properties define the build specification of a terget
  • INTERFACE_ properties define the usage requirements of a target.

This is the most critical concept in modern CMake. When you link a library, the consumer needs to know its include directories, compile definitions, etc.

  • PRIVATE: The property is only for building this target. It is not passed on to consumers (Non-INTERFACE_).
  • INTERFACE: The property is only for consumers of this target. The target itself doesn't use it for its own build. This is perfect for header-only libraries (INTERFACE_).
  • PUBLIC: The property is for both the target's build (PRIVATE) and for its consumers (INTERFACE).

Example

# A logging library that uses spdlog internally
add_library(my_logger my_logger.cpp) # my_logger needs the spdlog headers to compile.
# Anyone who uses my_logger does NOT need spdlog headers directly.
target_include_directories(my_logger
PRIVATE
${spdlog_SOURCE_DIR}/include
) # Anyone who uses my_logger needs my_logger's own headers.
target_include_directories(my_logger
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include> # Different path after installation
)

Header-Only Libraries

For libraries that are headers-only, create an INTERFACE library. It has no sources and only defines usage requirements.

add_library(my_header_lib INTERFACE)

target_include_directories(my_header_lib INTERFACE
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
)

WARNING

The library interface may change during installation. Use the BUILD_INTERFACE and INSTALL_INTERFACE generator expressions as filters

4. Working with Dependencies

Finding Packages with find_package()

This is the standard way to find and use external libraries that have been installed on the system or are part of the project.

# Find the Boost library, version 1.71 or newer.
# It is REQUIRED; CMake will fail if it's not found.
find_package(Boost 1.71 REQUIRED COMPONENTS system thread) # If found, use the imported target provided by Boost
if(Boost_FOUND)
target_link_libraries(my_app PRIVATE Boost::system Boost::thread)
endif()

Note

Regardless of the mode/package used, a <PackageName>_FOUND variable will be set to indicate whether the package was found.

IMPORTANT

Always use the official, namespaced, imported targets (e.g., Boost::system, Qt5::Core, GTest::GTest). Never use the old-style _LIBRARIES and _INCLUDE_DIRS variables. Imported targets handle all dependency properties for you automatically.

IMPORTANT

Use a Find module for third party libraries that are not built with CMake that don't support clients to use CMake. Also, report this as a bug to their authors.

If you need to write a find module for a third-party library, report this as a bug to the authors. Because most people use CMake, it's a problem that don't support it.

Fetching Dependencies at Configure Time with FetchContent

For dependencies that you want to download and build alongside your project, FetchContent is the modern, preferred approach. It's great for ensuring all developers use the exact same version of a dependency.

include(FetchContent)

# Declare the dependency
FetchContent_Declare(
googletest
GIT_REPOSITORY https://github.com/google/googletest.git
GIT_TAG release-1.12.1
) # Make it available (downloads and adds it as a sub-project)
FetchContent_MakeAvailable(googletest) # Now you can link to it just like any other target in your project
target_link_libraries(my_tests PRIVATE GTest::GTest GTest::Main)

Note

FetchContent is generally preferred over Git submodules because it gives the parent project more control and is easier to manage.

Exporting Your Project as a Package

When you want other projects to use your library with find_package(), you need to generate package configuration files.

  1. Install Targets: Use install(TARGETS ...) to specify where your library files (.a, .so, .dll) and headers should be installed. Use the EXPORT keyword to associate them with a target export set.
  2. Install Export Set: Use install(EXPORT ...) to create a <name>Targets.cmake file. This file contains the definitions of your imported targets (e.g., MyLib::MyLib).
  3. Create Version and Config Files: Use CMakePackageConfigHelpers to generate a version file (<name>ConfigVersion.cmake) and write a config file (<name>Config.cmake). The config file is the entry point that find_package looks for.

This process tells other projects how to use your library by defining an imported target like YourProject::YourLib.

For example:

# standard steps to build the library
find_package(Bar 2.0 REQUIRED)
add_library(Foo ...)
target_link_libraries(Foo PRIVATE Bar::Bar) # 1. Install targets
install(TARGETS Foo EXPORT FooTargets
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib
RUNTIME DESTINATION bin
INCLUDES DESTINATION include) # 2. Install Export Set
install(EXPORT FooTargets
FILE FooTargets.cmake
NAMESPACE Foo::
DESTINATION lib/cmake/Foo) # 3. Create Version file
include(CMakePackageConfigHelpers)
write_basic_package_version_file("FooConfigVersion.cmake"
VERSION ${Foo_VERSION}
COMPATIBILITY SameMajorVersion
)
install(FILES "FooConfig.cmake" "FooConfigVersion.cmake"
DESTINATION lib/cmake/Foo)

Create and write config files FooConfig.cmake:

include(CMakeFindDependencyMacro)
find_dependency(Bar 2.0)
include("$${CMAKE_CURRENT_LIST_DIR}/FooTargets.cmake")

5. Testing with CTest

CTest is CMake's testing framework. It acts as a test driver, running test executables and reporting results.

Basic Setup

  1. Enable Testing: Add enable_testing() to your root CMakeLists.txt. This command should be called only once in the top-level project.
  2. Add Tests: Use add_test() to define a test case. A test is typically an executable that returns 0 for success and non-zero for failure.
# In your tests/CMakeLists.txt
add_executable(run_all_tests tests.cpp)
target_link_libraries(run_all_tests PRIVATE my_lib GTest::GTest GTest::Main) # Define a test named "MyLib.UnitTests" that runs the "run_all_tests" executable.
add_test(NAME MyLib.UnitTests COMMAND run_all_tests)

TIP

It's a good practice to adopt a naming convention for your tests, like Project.Component.TestType. This makes filtering much easier.

Running Tests

You can then run all tests from your build directory:

# Run all tests, with 4 parallel jobs and verbose output on failure
ctest -j4 --output-on-failure

Advanced: Filtering Tests

You can run a subset of your tests using a regular expression with the -R flag.

# Run only the tests whose names start with "MyLib."
ctest -R "^MyLib\\."

Advanced: Testing for Compile Failure

Sometimes, you want to ensure that certain code fails to compile (e.g., when testing static_assert). You can create a test for this.

# Create a library that is expected to fail compilation.
# EXCLUDE_FROM_ALL prevents it from being built during a normal build.
add_library(foo_fail STATIC EXCLUDE_FROM_ALL foo_fail.cpp) # Add a test that tries to build this specific target.
# The test "passes" if the build command fails.
add_test(NAME Foo.CompileFail
COMMAND ${CMAKE_COMMAND} --build ${CMAKE_BINARY_DIR} --target foo_fail
) # We can even check for a specific error message in the build output.
# The test passes if the build fails AND the output contains this regex.
set_tests_properties(Foo.CompileFail PROPERTIES
PASS_REGULAR_EXPRESSION "static assert message"
WILL_FAIL TRUE # Informs CTest that a non-zero return code is expected (success)
)

Advanced: Driving CTest with a Script

For complex testing scenarios, especially in CI/CD environments, you can use a CMake script to drive CTest.

build_and_test.cmake

# This script automates the configure, build, test, and submit steps.
set(CTEST_SOURCE_DIRECTORY "/path/to/source")
set(CTEST_BINARY_DIRECTORY "/path/to/build") set(CTEST_CMAKE_GENERATOR "Ninja") # Specify the generator ctest_start("Continuous") # Start a new CTest run
ctest_configure() # Run cmake
ctest_build() # Run cmake --build
ctest_test() # Run ctest
ctest_submit() # Submit results to a dashboard like CDash

You would run this script from the command line:

ctest -S build_and_test.cmake

This keeps CI-specific logic out of your main CMakeLists.txt.

6. Cross-Compiling with Toolchain Files

Cross-compiling is the process of building code on one machine (the host) that is intended to run on a different machine (the target), which may have a different architecture (e.g., building for ARM on an x86-64 host).

CMake handles this through Toolchain Files. A toolchain file tells CMake how to find the correct compilers, linkers, and libraries for the target system.

The Role of the Toolchain File

You specify the toolchain file when you first configure your project with cmake.

# Configure the project using the my-arm-toolchain.cmake file
cmake -S . -B build -DCMAKE_TOOLCHAIN_FILE=/path/to/my-arm-toolchain.cmake

WARNING

A toolchain file is for describing the environment, not for project logic. Avoid setting project options or variables in it. Its only job is to set up the compilers and search paths.

Example Toolchain File

Here is an example of a toolchain file for cross-compiling to Windows from a Linux host using MinGW-w64.

mingw-toolchain.cmake

# The name of the target operating system
set(CMAKE_SYSTEM_NAME Windows) # Specify the cross-compilers
set(CMAKE_C_COMPILER x86_64-w64-mingw32-gcc)
set(CMAKE_CXX_COMPILER x86_64-w64-mingw32-g++) # Where to find the target environment's headers and libraries
set(CMAKE_FIND_ROOT_PATH /usr/x86_64-w64-mingw32) # Adjust the search behavior for programs, libraries, and includes
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) # Set an emulator to run target executables on the host system
set(CMAKE_CROSSCOMPILING_EMULATOR "wine64")

Running Cross-Compiled Tests

You can't directly run a target executable on the host machine. The CMAKE_CROSSCOMPILING_EMULATOR variable (set in the toolchain file) tells CTest how to run the tests. It specifies a program (like wine for Windows apps on Linux, or qemu-arm for ARM binaries) that can emulate the target environment.

When you run ctest, it will automatically prefix test commands with the emulator:

# CTest will effectively run:
wine64 my_test_executable.exe

This allows you to run your unit tests on the host machine even when cross-compiling.

7. Packaging with CPack

CPack is CMake's packaging tool. It can create installers (NSIS, WiX), archives (.zip, .tar.gz), and Linux packages (.deb, .rpm).

  1. Include CPack: Add include(CPack) to your root CMakeLists.txt.
  2. Set CPack Variables: Configure package metadata by setting CPACK_* variables.
set(CPACK_PACKAGE_NAME "MyAwesomeApp")
set(CPACK_PACKAGE_VERSION "1.0.0")
set(CPACK_GENERATOR "ZIP;TGZ") # Create a .zip and a .tar.gz
# ... other variables

Run CPack from your build directory to create the packages:

cpack

8. Static Analysis Integration

Integrating static analysis into the build process is a powerful way to enforce code quality and catch bugs early.

The Philosophy of Handling Warnings

A common piece of advice is to "treat warnings as errors," typically by passing the -Werror flag to the compiler. However, this approach can be counterproductive and painful.

Why -Werror Causes Pain:

  • Blocks Progress: You cannot enable -Werror until you have fixed every single existing warning in the codebase.
  • Hinders Upgrades: You cannot upgrade your compiler or increase the warning level (e.g., from -Wall to -Wextra) until you have fixed all the new warings that appear. This creates a significant barrier to modernization.
  • Makes Deprecation Difficult: You cannot mark your own internal APIs as [[deprecated]] as long as they are still in use, because this would generate a warning and fail the build. But once they are no longer used, you might as well just remove them.

A Better Approach: Treat New Warnings as Errors

A more practical and agile strategy is to manage warnings in development cycles:

  1. Allow New Warnings Temporarily: At the beginning of a development cycle (e.g., a new sprint), allow new warnings to be introduced. This is the time to upgrade compilers, update dependencies, or enable more aggressive warning flags.
  2. Burn Down Warnings: During the cycle, the team's goal is to fix all existing warnings and drive the count back down to zero.
  3. Repeat: This iterative process allows for continuous improvement without bringing development to a halt.

Pull Out All the Stops: Powerful Analysis Tools

Modern C++ has a rich ecosystem of static analysis tools that go far beyond compiler warnings. You should integrate them into your build.

  • clang-tidy: A Clang-based linter framework to diagnose and fix typical programming errors, style violations, and interface misuse.

    cpplint: An automated checker to ensure code adheres to Google's C++ style guide.
  • include-what-you-use (IWYU): A tool for analyzing #includes to ensure you include exactly what you use, which can improve compile times and code clarity.
  • clazy: A Clang plugin that finds Qt-specific antipatterns and performance issues.

Modern CMake Integration via Target Properties

Modern CMake provides dedicated target properties to run these tools alongside the compiler.

  • <LANG>_CLANG_TIDY
  • <LANG>_CPPLINT
  • <LANG>_INCLUDE_WHAT_YOU_USE
  • LINK_WHAT_YOU_USE (Checks for unused library links)

Where <LANG> is C or CXX. These properties are initialized by their corresponding CMAKE_... variables.

# Example of enabling clang-tidy and include-what-you-use
set_target_properties(my_app PROPERTIES
CXX_CLANG_TIDY "clang-tidy;-checks=-*,readability-*,modernize-*"
CXX_INCLUDE_WHAT_YOU_USE "include-what-you-use;-Xiwyu;--mapping_file=/iwyu.imp"
)

IMPORTANT

The major advantage of this approach is that diagnostics from these tools are seamlessly integrated into your build output and appear directly in your IDE, just like regular compiler errors and warnings.

Best Practice: Analyzing Header Files

A common pitfall is that header files without an associated source file (.cpp) will not be analyzed.

TIP

For each header file, ensure there is an associated source file that #include it, preferably as the very first line. This source file can even be empty otherwise.

You can use a simple script to create these missing source files:

#!/usr/bin/env bash
# Create an empty .cpp file for every .h file that doesn't have one.
for fn in `comm -23 \
<(ls *.h | cut -d '.' -f 1 | sort) \
<(ls *.c *.cpp | cut -d '.' -f 1 | sort)`
do
echo "#include \"$fn.h\"" >> $fn.cpp
done

Enabling Analysis from Outside the Project

To keep your CMakeLists.txt clean, you can enable and configure these tools from the command line during the configure step. This is ideal for CI/CD pipelines.

Reference

Notes of Effective CMake的更多相关文章

  1. CMake Intro - CMakeLists.txt

    Notes:  directory structure:  cmake, cmake/Tutorial, cmake/Tutorial/MathLibs 1. File lists in cmake/ ...

  2. OSG-3.4.0 简要说明(Readme)

    欢迎来到OpenSceneGraph(OSG)世界. Welcome to the OpenSceneGraph (OSG). 对于项目最新信息, 以及如何编译和运行库和示例的更多细节, 可以查看OS ...

  3. 转:Effective c + + notes

    补充自己的. 转自:http://blog.csdn.net/ysu108/article/details/9853963#t0 Effective C++ 笔记 目录(?)[-] 第一章 从C转向C ...

  4. Effective Objective-C 2.0 Reading Notes

    1. Literal Syntax NSString *someString = @"Effective Objective-C 2.0"; NSNumber *someNumbe ...

  5. Effective C++ Notes

    Item 07 : 为多态基类声明virtual析构函数 #include <iostream> using namespace std; class Base { public: Bas ...

  6. Effective Java Index

    Hi guys, I am happy to tell you that I am moving to the open source world. And Java is the 1st langu ...

  7. Notes on <Assembly Language step by step>

    By brant-ruan Yeah, I feel very happy When you want to give up, think why you have held on so long. ...

  8. Reading Notes of Acceptance Test Engineering Guide

    The Acceptance Test Engineering Guide will provide guidance for technology stakeholders (developers, ...

  9. Cmake调用NSIS(一个可执行文件,其实就是一个编译器)编译NSIS脚本问题研究

    技术经理说,可以用Cmake当中的add_custom_command,add_custom_target命令来使用. 我初次研究了下,add_custom_command应该用官方文档中说明的第二种 ...

  10. gvim work notes.. a few days' work on 64bit vim and plugin compilations

    (a 600MB+ sized c/c++ compiler which is capable of hi-light and JB styled completion!! and of-course ...

随机推荐

  1. L3-3、从单轮到链式任务:设计协作型 Prompt 系统

    一.链式任务设计的概念与价值 在人工智能应用开发中,单轮对话往往无法满足复杂业务场景的需求.链式任务设计允许我们将复杂问题分解为一系列相互关联的子任务,每个子任务的输出可以作为下一个子任务的输入,从而 ...

  2. Java 下载网络资源

    从网络URL下载文件到指定目录,自适应文件类型,并且重命名下载后的文件名.这里使用XtremePapers如下URL的网络资源作为测试文件: https://papers.xtremepape.rs/ ...

  3. 代码随想录第三天 | 链表part01

    链表理论基础 建议:了解一下链表基础,以及链表和数组的区别 文章链接:https://programmercarl.com/链表理论基础.html 不是很了解链表和数组区别的可以先看看以上的文章. 2 ...

  4. fabric peer节点账本验证器相关代码解读

    账本验证器相关代码 fabric/core/commiter/txvalidator/v20/validator.go // Semaphore provides to the validator m ...

  5. TemplatesImpl结合cc6在Shiro中的利用

    TemplatesImpl结合cc6在Shiro中的利用 这个文章也是参考p牛的文章;但其中许多细节,就比如为什么普通的Transformer[]数组链不能再shiro中使用; 但其中大致原理还是说一 ...

  6. 如何优雅的关闭channel?

    一.channel使用存在的不方便地方 1.在不改变channel自身状态的情况下,无法获知一个channnel是否关闭. 2.关闭一个已经关闭的channel,会导致panic.因此,如果关闭cha ...

  7. 钓鱼攻击(phishing)详解和实现过程

    钓鱼攻击 定义:钓鱼攻击是一种常见的网络攻击手段,攻击者通过伪装成合法的网站.邮件或信息,诱骗用户提供敏感信息,如用户名.密码.银行卡号等,从而达到非法获取用户数据或进行欺诈的目的. 网络钓鱼(phi ...

  8. 基于Fastapi的区分聊天房间的聊天转发功能接口示例

    基于房间码(eCode)和用户uid,区分不同的聊天房间进行消息转发. 前端将收到的消息根据房间码(eCode)过滤到不同的聊天记录显示页面 后端demo代码如下: from fastapi impo ...

  9. ESP32-Arduino物联网工控(一)串口转TCP转发机项目简介

    Arduino 在物联网上配合ESP32简直就是神器! 然后现在大老板说新设备用ESP32来帮助原本没有联网功能的STM32单片机来进行调试...... 实现需求: 1.指定命令拍照,协议HTTP,并 ...

  10. 20w奖金池!魔乐社区国产算力应用创新大赛正式启程

    本文分享自魔乐社区公众号<​​20w奖金池!魔乐社区国产算力应用创新大赛正式启程​​> 当国产算力崛起成为 AI 发展新引擎,你是否渴望用创新方案解锁无限可能?魔乐社区国产算力应用创新大赛 ...