在WSL下為OpenWRT交叉編譯出CMake

xiaoandi發表於2024-07-25

最近想給家裡的路由器寫點東西,但習慣了CLion,又想能透過CLion進行遠端開發&除錯,可是好像配置遠端除錯需要目標機機器上裝有CMake,然而OpenWRT是沒有CMake的,找遍了OpenWRT的源也都沒看到有CMake,於是決定透過openwrt-sdk來自行編譯一個。
在這過程中遇到了很多坑,上網查資料也沒找到具體的解決方法,折騰了幾天終於搞定,現在記錄下來。廢話不多說,直接開幹。
首先是需要的以及工具環境有:

  1. ubuntu(或者你是其他的linux發行版),我是懶得裝雙系統或是虛擬機器軟體了,直接上WSL
  2. 開發機上要裝CMake
  3. 按照openwrt官網的指引,安裝好交叉編譯的各種依賴,詳情檢視 https://openwrt.org/docs/guide-developer/toolchain/install-buildsystem
    我是ubuntu22.04,所以是安裝這些內容

有了這些先決條件後,就可以下載openwrt-sdk來編譯自己想要的專案了。首先還是去官網下載自己機器對應版本的openwrt-sdk,不知道或者忘了自己是哪個版本可以登陸ssh上去,這裡就有寫

或者是開啟路由器管理頁面,一般在頁面下方會有寫,類似 Powered by LuCI openwrt-23.05......
然後下載openwrt-sdk

解壓,來到sdk的根路徑下。
此時,按照官網的指引,需要先執行命令來更新軟體包和依賴資訊

./scripts/feeds update -a
./scripts/feeds install -a

但實際上不需要的,我就沒有更新這些東西。直接寫Makefile然後編譯你的包就行了。
在package下建立cmake資料夾並建立Makefile

這個Makefile是有書寫格式的,官網上也有介紹:https://openwrt.org/docs/guide-developer/packages
總的來說我們需要定義好這幾個東西:

  • Package/CMake —— 包的類別和標題、依賴等
  • Build/Configure —— 編譯前的配置,比如 configure 操作
  • Package/CMake/install —— 定義如何安裝,要複製那些可執行檔案

我當時也沒多想,先直接按照cmake的編譯教程去寫,也就是在Build/Configure執行 ./bootstrap

# 注意Makefile裡只能用tab,不能用4個空格!

include $(TOPDIR)/rules.mk

PKG_NAME:=CMake
PKG_VERSION:=3.30.1
PKG_RELEASE:=1

PKG_SOURCE_URL:=https://github.com/Kitware/CMake.git
PKG_SOURCE_PROTO:=git
PKG_SOURCE_VERSION:=v3.30.1

include $(INCLUDE_DIR)/package.mk

define Package/CMake
        SECTION:=utils
        CATEGORY:=Utilities
        TITLE:=CMake compile tool
endef

define Build/Configure
        cd $(PKG_BUILD_DIR) && ./bootstrap
endef

define Package/CMake/install
        $(INSTALL_DIR) $(1)/usr/bin
        $(CP) $(PKG_BUILD_DIR)/bin/* $(1)/usr/bin/
endef

$(eval $(call BuildPackage,CMake))

然後回到openwrt-sdk根路徑,執行make package/cmake/compile V=s,第一次好像都會彈出make menuconfig的視覺化介面,但是我沒管他,直接exit掉。最後,也是意料之中的報錯了:

這個報錯的意思大概是說,在打包成ipk的時候發現依賴這些動態庫,但是sdk沒能在包裡找到這些庫檔案。我尋思著,這些庫檔案在路由器上都有,既然已經編譯完了,那我直接傳可執行檔案上去不就得了。果然,在編譯目錄下能找到可執行檔案,那我直接傳上去就行了。

上傳後執行,我懵了,啊?

不對不對,仔細分析,這個情況應該是這個可執行檔案根本就不是openwrt可以執行的,換句話說它就不是交叉編譯的產物。這點其實在編譯過程中我已經有了點懷疑:

這不就是在用ubuntu上的g++來編譯嗎?這能行嗎,這不能行!
那問題出在哪呢?我想了想,應該是configure階段,按照我Makefile裡的指令碼,是直接執行 ./bootstrap,而bootstrap的主要作用就是檢測當前環境並生成Makefile,我這樣直接執行那不還是檢測的ubuntu環境嗎?
重新翻翻openwrt的文件,好像是有針對configure傳遞交叉編譯變數的方法:

那我先在cmake原始碼裡試試

且不說我傳的變數對不對,它好像根本就不支援 --build這個引數,檢視了一下 --help,確實是沒找到 --build和--host。

改為用 openwrt-toolchain 編譯

我決定放棄用openwrt-sdk編譯一個完整的ipk包了,直接使用toolchain裡的編譯工具,結合CMake的CMAKE_TOOLCHAIN_FILE,把交叉編譯的工具鏈傳遞給CMake,然後用CMake來編譯CMake,這也是CMake官方提供的一種編譯方式。
安裝openwrt-toolchain的過程就不多說了,也是在和sdk同樣的頁面處下載 toolchain包,然後解壓(openwrt-sdk裡其實自帶了toolchain,就在staging_dir下的toolchain開頭的資料夾內就是交叉編譯工具,事實上建議用sdk的toolchain)。下載CMake的原始碼到隨便一個路徑下,為了方便我就放在projects下了

進入CMake並建立一個build目錄用於編譯構建

mkdir -p CMake/build
cd CMake/build

此時需要交叉編譯的話,我們需要提供一個額外的.cmake檔案,用來傳遞交叉編譯的工具鏈的路徑

touch openwrt_toolchain.cmake
vim openwrt_toolchain.cmake


# openwrt_toolchain.cmake
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR openwrt)

set(toolchain /home/xiaoandi/github/blog/openwrt-toolchain-23.05.3-x86-64_gcc-12.3.0_musl.Linux-x86_64/toolchain-x86_64_gcc-12.3.0_musl)

# 指定交叉編譯器的路徑
set(CMAKE_C_COMPILER ${toolchain}/bin/x86_64-openwrt-linux-musl-gcc)
set(CMAKE_CXX_COMPILER ${toolchain}/bin/x86_64-openwrt-linux-musl-g++)
set(CMAKE_AR ${toolchain}/bin/x86_64-openwrt-linux-musl-ar)
set(CMAKE_LINKER ${toolchain}/bin/x86_64-openwrt-linux-musl-ld)
set(CMAKE_RANLIB ${toolchain}/bin/x86_64-openwrt-linux-ranlib)
set(CMAKE_NM ${toolchain}/bin/x86_64-openwrt-linux-nm)
set(CMAKE_OBJDUMP ${toolchain}/bin/x86_64-openwrt-linux-objdump)
set(CMAKE_OBJCOPY ${toolchain}/bin/x86_64-openwrt-linux-objcopy)
set(CMAKE_STRIP ${toolchain}/bin/x86_64-openwrt-linux-strip)

set(CMAKE_FIND_ROOT_PATH ${toolchain}/lib ${toolchain}/usr/lib ${toolchain}/include ${toolchain}/usr/include)

# 使find_program, find_library, find_path, find_file等命令
# 先在CMAKE_FIND_ROOT_PATH中搜尋
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY BOTH)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)

然後執行

cmake .. -DCMAKE_TOOLCHAIN_FILE=./openwrt_toolchain.cmake


果然啊,問題可能會遲到,但不會缺席~
還是看看是什麼情況吧,開啟看看這個CMakeError.log,搜一下unique_ptr

這...看不太懂,但我疑惑的是為啥這裡會include到我ubuntu上的標頭檔案呢?
這個問題我琢磨了很久,甚至把241行的編譯命令拿出來自行編譯,也是不行。最後我才注意到第242行有一個warning:environment variable 'STAGING_DIR' not defined
通常warning我都直接忽略的,但此時也沒有更好的辦法,想著是不是因為這個環境變數沒配置導致的?雖然好像看起來沒啥關係,我決定還是試試。
我在openwrt_toolchain.cmake裡對這個環境變數進行了設定

set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR openwrt)

set(toolchain /home/xiaoandi/github/blog/openwrt-toolchain-23.05.3-x86-64_gcc-12.3.0_musl.Linux-x86_64/toolchain-x86_64_gcc-12.3.0_musl)
set(ENV{STAGING_DIR} /home/xiaoandi/github/openwrt-sdk-23.05.3-x86-64_gcc-12.3.0_musl.Linux-x86_64/staging_dir)

# 指定交叉編譯器的路徑
set(CMAKE_C_COMPILER ${toolchain}/bin/x86_64-openwrt-linux-musl-gcc)
set(CMAKE_CXX_COMPILER ${toolchain}/bin/x86_64-openwrt-linux-musl-g++)
set(CMAKE_AR ${toolchain}/bin/x86_64-openwrt-linux-musl-ar)
set(CMAKE_LINKER ${toolchain}/bin/x86_64-openwrt-linux-musl-ld)
set(CMAKE_RANLIB ${toolchain}/bin/x86_64-openwrt-linux-ranlib)
set(CMAKE_NM ${toolchain}/bin/x86_64-openwrt-linux-nm)
set(CMAKE_OBJDUMP ${toolchain}/bin/x86_64-openwrt-linux-objdump)
set(CMAKE_OBJCOPY ${toolchain}/bin/x86_64-openwrt-linux-objcopy)
set(CMAKE_STRIP ${toolchain}/bin/x86_64-openwrt-linux-strip)

set(CMAKE_FIND_ROOT_PATH ${toolchain}/lib ${toolchain}/usr/lib ${toolchain}/include ${toolchain}/usr/include)

# 使find_program, find_library, find_path, find_file等命令
# 先在CMAKE_FIND_ROOT_PATH中搜尋
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY BOTH)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)

其實我也不清楚這個staging_dir應該設定哪個路徑,但是我看到之前openwrt-sdk的目錄裡,有一個staging_dir,我覺得設定為這個肯定不會錯。刪除掉CMakeCache.txt後,再次執行

這次是說缺少openssl的相關變數?我直接從路由器上複製下來 libcrypto.so.3 和 libssl.so.3,用這兩個應該不會錯,但是我在路由器上沒找到openssl的標頭檔案。我想著標頭檔案應該都一樣吧,直接用ubuntu上的標頭檔案是否也行呢?
試試再說,刪除CMakeCache.txt後執行

cmake .. -DCMAKE_TOOLCHAIN_FILE=./openwrt_toolchain.cmake -DOPENSSL_CRYPTO_LIBRARY=./libcrypto.so.3 -DOPENSSL_SSL_LIBRARY=./libssl.so.3 -DOPENSSL_INCLUDE_DIR=/usr/lib

又來新的問題了:

檢視CMakeError.txt,發現這次的問題是CMake的測試程式碼在連線crypto時沒找到庫檔案:

應該是在搜尋libcrypto.so的時候沒搜到,因為我直接複製下來的是libcrypto.so.3,所以只要建立一個軟連結指向原始檔即可(所以剛才讓我傳遞的那幾個變數有什麼用呢?)

然後在openwrt_toolchain.cmake裡新增CMAKE_FIND_ROOT_PATH,把當前的路徑新增進去,讓編譯器能夠搜尋到當前路徑下的庫檔案

清除CMakeCache.txt,重新構建,這次終於成功了,不容易啊...
接著make吧,果然還有問題在等著我

這次是缺少opensslconf.h,因為我是用開發機的標頭檔案,果然還是不行嗎,那就只有最後的方案了:先交叉編譯出openssl,再編譯cmake;
這裡說一下,決定編譯openssl是還有其他原因的。我有試過直接去下載一個opensslconf.h,然後再接著編譯,最終能編譯出來可執行檔案,但是當我把他放到機器上時,執行時會提示缺少某些openssl的符號,經過後來的確認,應該是版本的問題:我的機器上openssl是3.0.14版本,如果我編譯CMake 3.30.1版本,那麼3.0.14的openssl確實是缺少符號的,而3.28.0的CMake能夠使用3.0.14的openssl,雖然最後我編譯的也是
3.28.0的CMake(因為截止到目前的CLion好像只支援到CMake 3.28.0),但是還有一點不容忽略的是,openwrt上自帶,或者從源下載安裝的libopenssl3,是不包含標頭檔案的,後續想要使用openssl開發時,這就會是個問題,所以我乾脆順便把openssl也給編譯了,在機器上裝兩個版本的openssl庫檔案以供後續使用。

編譯openssl-dev
為了區別於目標機器上的libopenssl3,這裡先把將要編譯的稱為openssl-dev
還是直接從github上拉取openssl的程式碼,openssl比起CMake就方便許多了,它的config命令直接提供了交叉編譯的引數

cd openssl
mkdir build
# --prefix是輸出目錄,--cross-compile-prefix是指編譯工具的路徑,
# 比如../../toolchain-x86_64_gcc-12.3.0_musl/bin/x86_64-openwrt-linux-musl- 
# 實際會用到的gcc就是../../toolchain-x86_64_gcc-12.3.0_musl/bin/x86_64-openwrt-linux-musl-gcc
# 實際上就是把交叉編譯要用到的gcc/g++傳遞過去,注意這裡一定要是完整路徑,不能是相對路徑
./config no-asm shared no-async --prefix=./build --cross-compile-prefix=/home/xiaoandi/github/blog/openwrt-toolchain-23.05.3-x86-64_gcc-12.3.0_musl.Linux-x86_64/toolchain-x86_64_gcc-12.3.0_musl/bin/x86_64-openwrt-linux-musl-

出現這樣就是配置成功了

接著執行 make && make install即可編譯安裝,完成後在./build裡即可找到相應的標頭檔案和庫檔案。
有了openssl-dev,我們就可以接著編譯CMake啦。

讓我們回到CMake/build裡,修改openwrt_toolchain.cmake,把CMAKE_FIND_ROOT_PATH修改一下:

# openwrt_toolchain
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR openwrt)

set(toolchain /home/xiaoandi/github/blog/openwrt-toolchain-23.05.3-x86-64_gcc-12.3.0_musl.Linux-x86_64/toolchain-x86_64_gcc-12.3.0_musl)
set(openssl-dev /home/xiaoandi/github/blog/openwrt-toolchain-23.05.3-x86-64_gcc-12.3.0_musl.Linux-x86_64/projects/openssl/build)
set(ENV{STAGING_DIR} /home/xiaoandi/github/openwrt-sdk-23.05.3-x86-64_gcc-12.3.0_musl.Linux-x86_64/staging_dir)

# 指定交叉編譯器的路徑
set(CMAKE_C_COMPILER ${toolchain}/bin/x86_64-openwrt-linux-musl-gcc)
set(CMAKE_CXX_COMPILER ${toolchain}/bin/x86_64-openwrt-linux-musl-g++)
set(CMAKE_AR ${toolchain}/bin/x86_64-openwrt-linux-musl-ar)
set(CMAKE_LINKER ${toolchain}/bin/x86_64-openwrt-linux-musl-ld)
set(CMAKE_RANLIB ${toolchain}/bin/x86_64-openwrt-linux-ranlib)
set(CMAKE_NM ${toolchain}/bin/x86_64-openwrt-linux-nm)
set(CMAKE_OBJDUMP ${toolchain}/bin/x86_64-openwrt-linux-objdump)
set(CMAKE_OBJCOPY ${toolchain}/bin/x86_64-openwrt-linux-objcopy)
set(CMAKE_STRIP ${toolchain}/bin/x86_64-openwrt-linux-strip)

# set(CMAKE_FIND_ROOT_PATH ${topdir}/projects/CMake/build ${toolchain}/lib ${toolchain}/usr/lib ${toolchain}/include ${toolchain}/usr/include)
set(CMAKE_FIND_ROOT_PATH ${openssl-dev}/lib64 ${openssl-dev}/include)
set(CMAKE_INSTALL_RPATH "/usr/local/openssl-dev/lib64")
set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE)

# 使find_program, find_library, find_path, find_file等命令
# 先在CMAKE_FIND_ROOT_PATH中搜尋
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY BOTH)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)

實際上只需要新增openssl-dev的路徑就可以了。這裡說一下CMAKE_INSTALL_RPATH,因為我們需要在目標機上安裝兩個版本的openssl,其中自行編譯的版本暫時只提供給我們編譯的CMake使用,那麼我把它放在一個單獨的路徑下 /usr/local/openssl-dev 但是如果不指定rpath,那麼可執行檔案會從預設的路徑也就是PATH處開始搜尋,那肯定會先搜到原來的libopenssl3,這不是我希望看到的,因此需要新增rpath,讓可執行檔案先從我指定的路徑搜尋庫檔案。
好了,開始編譯吧,cmake的命令也要改一改,將我們編譯的openssl路徑傳遞給那幾個引數

# 清除CMake快取
rm CMakeCache.txt
mkdir output
# 注意,我最後加了 -DCMAKE_INSTALL_PREFIX=./output,用於編譯完成後make install到指定路徑下
cmake .. -DCMAKE_TOOLCHAIN_FILE=./openwrt_toolchain.cmake -DOPENSSL_CRYPTO_LIBRARY=/home/xiaoandi/github/blog/openwrt-toolchain-23.05.3-x86-64_gcc-12.3.0_musl.Linux-x86_64/projects/openssl/build/lib64/libcrypto.so.3 -DOPENSSL_SSL_LIBRARY=/home/xiaoandi/github/blog/openwrt-toolchain-23.05.3-x86-64_gcc-12.3.0_musl.Linux-x86_64/projects/openssl/build/lib64/libssl.so.3 -DOPENSSL_INCLUDE_DIR=/home/xiaoandi/github/blog/openwrt-toolchain-23.05.3-x86-64_gcc-12.3.0_musl.Linux-x86_64/projects/openssl/build/include -DCMAKE_INSTALL_PREFIX=./output

# 編譯,這次我直接開8執行緒編譯了,因為我知道肯定沒問題了
make -j8 && make install

編譯完成後在output目錄下找到最終的產物

注意
並不是只把bin裡的可執行檔案複製到機器上就完事了,同時 share/cmake-3.30目錄也要複製過去,否則你執行cmake的時候會提示找不到CMAKE_ROOT!
share/cmake-3.30資料夾複製到 /usr/share/cmake-3.30
最後使用CLion連線,遠端開發,完事。(可能還需要安裝rsync、sftp等,不過這些都已經有ipk包了,不需要再折騰自行編譯)

相關文章