從 Conan 第一篇開始,我們一路都用 conanfile.txt 作為 Conan 管理套件的入口。
如果你只是想在既有的專案中快速引入幾個函式庫,conanfile.txt 最方便,簡單。
但我們今天碰到了一個小問題。
假設有個情境:我們要想寫一個具備 GPU 平行運算能力的跨平台小程式,Windows / macOS / Linux 三個平台都要支援。
Windows 和 Linux 上我們打算用 OpenCL 來跑 GPU 運算。我們也很快的搜尋一下,發現 ConanCenter 上面已經有 opencl-icd-loader/2025.07.22 現成套件可以用。
但是很不巧, macOS 不能用 OpenCL。為什麼呢?原來蘋果早在 2018 年 WWDC 就已經正式宣布棄用 macOS/iOS 上的 OpenCL,舊版本凍結不再更新,並引導開發者全面改用蘋果自己的 Metal 框架。
所以在編譯 macOS 版本的時候,我們得改用 Metal 而非 OpenCL。
挖,這樣 conanfile.txt 要怎麼寫?
conanfile.txt 沒有語法可寫「只有 Windows/Linux 才要」這樣的邏輯。當然你也可以每個平台各放一份 conanfile.txt,但那樣很快就會變成維護地獄。
怎麼辦呢?
其實解法很簡單,就是換成 conanfile.py
雖然 conanfile.txt 是常見的入口,但 Conan 還有另一個更強大的入口檔案叫 conanfile.py
conanfile.py 是個純正的 python 腳本,任何的程式邏輯都能寫,所以 conanfile.txt 做不到的事情,對 conanfile.py 可說是一點難度都沒有。
以下就是我們這次要使用的範例:
from conan import ConanFile
from conan.tools.cmake import cmake_layout
class HelloComputeRecipe(ConanFile):
settings = "os", "compiler", "build_type", "arch"
generators = "CMakeDeps", "CMakeToolchain"
def requirements(self):
self.requires("fmt/12.2.0")
if self.settings.os in ("Windows", "Linux"):
self.requires("opencl-icd-loader/2025.07.22")
def layout(self):
cmake_layout(self)
暫時不理會其他細節,先看 requirements() 這個函數,這裡用一個簡單的 if 就把平台邏輯輕鬆解決了:只有 Windows & Linux 需要安裝 OpenCL 套件, macOS 就跳過。 因為 macOS 用的 Metal framework 是系統內建的,不需要透過 Conan 取得。
這樣問題就解決了,用我們程式設計師每天都在用的 if 條件式,而且是再熟悉不過的 python 語法。
比對我們之前用的 conanfile.txt,conanfile.py 的內容組成其實很容易對應起來。
[requires] 區塊變成 requirements() 方法。
原本每一行寫一個函式庫,現在變成每一行寫一個 self.requires()。
[generators] 區塊對應到 generators 屬性。
[layout] 區塊對應到 layout() 方法裡呼叫的 cmake_layout(self)。
以上都是我們前幾天在 conanfile.txt 已經多次使用的部分。
唯一可能沒見過的是這一行: settings = "os", "compiler", "build_type", "arch"
這是宣告這份入口檔案會用到哪些 settings。有宣告才能在 requirements() 裡讀 self.settings.os。這些值來自 profile。
接下來我們就親手來建立這個專案囉,這是專案的目錄結構:
hello-compute/
├── conanfile.py
├── CMakeLists.txt
└── src/
├── compute.h
├── main.cpp
├── compute_opencl.cpp
└── compute_macos.mm
Windows 跟 Linux 這次共用同一支 compute_opencl.cpp,因為兩邊都是 OpenCL 運算,程式碼一樣。
只有 macOS 因為使用完全不同的 Metal API 運算,需要獨立一個檔案。
接下來寫 CMake。
cmake_minimum_required(VERSION 3.25)
project(hello_compute CXX)
find_package(fmt REQUIRED)
add_executable(hello_compute src/main.cpp)
target_compile_features(hello_compute PRIVATE cxx_std_17)
target_link_libraries(hello_compute PRIVATE fmt::fmt)
if(WIN32 OR LINUX)
find_package(OpenCL REQUIRED)
target_sources(hello_compute PRIVATE src/compute_opencl.cpp)
target_link_libraries(hello_compute PRIVATE OpenCL::OpenCL)
elseif(APPLE)
enable_language(OBJCXX)
target_sources(hello_compute PRIVATE src/compute_macos.mm)
target_link_libraries(hello_compute PRIVATE "-framework Metal" "-framework Foundation")
endif()
這個部份我就不多作解釋了。作為範例專案,這裡簡單實做了一個列出GPU的功能。
總共有四個原始檔
#pragma once
void print_compute_info();
#include <fmt/base.h>
#include "compute.h"
int main() {
fmt::print("Hello, compute!\n");
print_compute_info();
return 0;
}
#include "compute.h"
#include <fmt/base.h>
#define CL_TARGET_OPENCL_VERSION 300
#include <CL/cl.h>
#include <vector>
void print_compute_info() {
cl_uint platform_count = 0;
clGetPlatformIDs(0, nullptr, &platform_count);
std::vector<cl_platform_id> platforms(platform_count);
clGetPlatformIDs(platform_count, platforms.data(), nullptr);
for (cl_platform_id platform : platforms) {
char platform_name[256] = {};
clGetPlatformInfo(platform, CL_PLATFORM_NAME, sizeof(platform_name), platform_name, nullptr);
fmt::print("Platform: {}\n", platform_name);
cl_uint device_count = 0;
clGetDeviceIDs(platform, CL_DEVICE_TYPE_ALL, 0, nullptr, &device_count);
std::vector<cl_device_id> devices(device_count);
clGetDeviceIDs(platform, CL_DEVICE_TYPE_ALL, device_count, devices.data(), nullptr);
for (cl_device_id device : devices) {
char device_name[256] = {};
clGetDeviceInfo(device, CL_DEVICE_NAME, sizeof(device_name), device_name, nullptr);
fmt::print(" [{}]\n", device_name);
}
}
}
compute_macos.mm(Metal):#include "compute.h"
#include <fmt/base.h>
#import <Metal/Metal.h>
void print_compute_info() {
id<MTLDevice> device = MTLCreateSystemDefaultDevice();
if (device == nil) {
fmt::print("No Metal device found\n");
return;
}
fmt::print("Platform: Metal\n");
fmt::print(" [{}]\n", [[device name] UTF8String]);
}
命令列進入專案目錄,然後打以下指令:
conan install . --build=missing
Conan 會自己找到 conanfile.py,並且安裝套件。
去看命令列輸出,Windows / Linux 上面顯示有安裝 OpenCL:
======== Computing dependency graph ========
Graph root
conanfile.py: C:\github\ironman-2026\D07\conanfile.py
Requirements
fmt/12.2.0#b445244e6d0151a425e229248cd2bed8 - Cache
opencl-clhpp-headers/2025.07.22#4eb7c91d676965f46d435c07cd7244bb - Cache
opencl-headers/2025.07.22#37f30f890e7e61f7baa895c1eb06a692 - Cache
opencl-icd-loader/2025.07.22#4485cccf0de603eb83ecd97a290d482e - Cache
相對的,macOS 上只顯示了 fmt,OpenCL 系列套件完全不會出現:
======== Computing dependency graph ========
Graph root
conanfile.py: /Users/matt/github/ironman-2026/D07/conanfile.py
Requirements
fmt/12.2.0#b445244e6d0151a425e229248cd2bed8 - Cache
所以 conanfile.py 的條件式發揮作用了
接下來,我們就依照標準的 CMake 步驟去編譯專案:
Windows
cmake --preset conan-default
cmake --build --preset conan-release
build\Release\hello_compute.exe
macOS / Linux
cmake --preset conan-release
cmake --build --preset conan-release
./build/Release/hello_compute
編譯完跑起來,在我的 Windows 上看到 GPU 裝置了!
Hello, compute!
Platform: Intel(R) OpenCL Graphics
[Intel(R) Arc(TM) 140T GPU (32GB)]
Platform: NVIDIA CUDA
[NVIDIA RTX PRO 500 Blackwell Generation Laptop GPU]
到此大功告成。
在這一章中,我們在 conanfile.py 裡面寫條件式,決定要不要引入 OpenCL 依賴,以後我們會更加倚賴 conanfile.py,在後續介紹更多進階的功能。
明天開始,我們先轉換到 vcpkg 系列。