自訂命令

軟體專案的建置過程,經常不僅僅是編譯函式庫和可執行檔而已。在許多情況下,在建置過程中或之後可能需要額外的任務。常見的例子包括:使用文件套件編譯文件、透過執行另一個可執行檔產生原始檔、使用 CMake 沒有規則的工具(例如 lex 和 yacc)產生檔案、移動產生的可執行檔、對可執行檔進行後處理等等。CMake 使用 add_custom_commandadd_custom_target 命令來支援這些額外任務。本章將描述如何使用自訂命令和目標來執行 CMake 本身不支援的複雜任務。

可攜式自訂命令

在詳細介紹如何使用自訂命令之前,我們將討論如何處理它們的一些可攜性問題。自訂命令通常涉及使用檔案作為輸入或輸出執行程式。即使是簡單的命令,例如複製檔案,也可能難以跨平台執行。例如,在 UNIX 上複製檔案是使用 cp 命令完成的,而在 Windows 上則使用 copy 命令完成。更糟的是,檔案的名稱在不同的平台上經常會改變。Windows 上的可執行檔以 .exe 結尾,而在 UNIX 上則沒有。即使在 UNIX 實作之間也存在差異,例如共享函式庫使用的副檔名:.so、.sl、.dylib 等等。

CMake 提供了三個主要工具來處理這些差異。第一個是 cmake-E 選項(execute 的縮寫)。當 cmake 可執行檔傳遞 -E 選項時,它會作為通用的跨平台工具命令。 -E 選項後面的引數表示 cmake 應該做什麼。這些選項提供了一種與平台無關的方式來執行一些常見的任務,包括複製或刪除檔案、比較和有條件地複製、計時、建立符號連結等等。cmake 可執行檔可以使用 CMakeLists 檔案中的 CMAKE_COMMAND 變數來參照,稍後的範例將會說明。

當然,CMake 不會限制您在所有自訂命令中使用 cmake -E。您可以使用任何您喜歡的命令,但重要的是在這樣做時要考慮到可攜性問題。常見的做法是使用 find_program 來尋找可執行檔(例如 Perl),然後在您的自訂命令中使用該可執行檔。

CMake 提供的第二個解決可攜性問題的工具是一些描述平台特性的變數。 cmake-variables(7) 手冊列出了許多對於需要參照具有平台相關名稱的檔案的自訂命令有用的變數。這些包括 CMAKE_EXECUTABLE_SUFFIXCMAKE_SHARED_LIBRARY_PREFIX 等,這些描述了檔案命名慣例。

最後,CMake 在自訂命令中支援 產生器表達式。這些是使用特殊語法 $<...> 的表達式,它們不會在處理 CMake 輸入檔案時評估,而是延遲到產生最終建置系統時才進行。因此,它們的替代值知道它們的評估內容的所有細節,包括目前的建置配置以及與目標關聯的所有建置屬性。

新增自訂命令

現在我們將考慮 add_custom_command 的簽名。在 Makefile 術語中,add_custom_command 會將規則新增到 Makefile 中。對於那些更熟悉 Visual Studio 的人來說,它會將自訂建置步驟新增到檔案中。 add_custom_command 有兩個主要的簽名:一個用於將自訂命令新增到目標,另一個用於新增自訂命令來建置檔案。

目標是您要新增自訂命令的 CMake 目標(可執行檔、函式庫或自訂)的名稱。您可以選擇何時執行自訂命令。您可以為一個自訂命令指定任意數量的命令。它們將按照指定的順序執行。

現在讓我們考慮一個簡單的自訂命令,用於複製建置完成的可執行檔。

# first define the executable target as usual
add_executable(Foo bar.c)

# then add the custom command to copy it
add_custom_command(
  TARGET Foo
  POST_BUILD
  COMMAND ${CMAKE_COMMAND}
  ARGS -E copy $<TARGET_FILE:Foo> /testing_department/files
  )

此範例中的第一個命令是從原始檔清單建立可執行檔的標準命令。在這種情況下,從原始檔 bar.c 建立一個名為 Foo 的可執行檔。接下來是 add_custom_command 呼叫。這裡的目標很簡單是 Foo,我們正在新增一個建置後命令。要執行的命令是 cmake,其完整路徑在 CMAKE_COMMAND 變數中指定。它的引數是 -E copy 以及來源和目的地位置。在這種情況下,它會將 Foo 可執行檔從其建置位置複製到 /testing_department/files 目錄。請注意,TARGET 參數接受 CMake 目標(在此範例中為 Foo),但為 COMMAND 參數指定的引數通常需要完整路徑。在這種情況下,我們將完整路徑傳遞到 cmake -E copy,該完整路徑透過 $<TARGET_FILE:...> 產生器表達式參照。

產生檔案

第二種使用 add_custom_command 的方式是新增一個規則,說明如何建置輸出檔案。這裡提供的規則會取代任何目前用於建置該檔案的規則。請記住,add_custom_command 的輸出必須由相同範圍內的目標使用。如同稍後討論的,add_custom_target 命令可以用於此目的。

使用可執行檔建置原始碼檔案

有時軟體專案會建置一個可執行檔,然後用來產生原始碼檔案,而這些檔案又用於建置其他可執行檔或程式庫。這聽起來可能很奇怪,但實際上很常見。一個例子是 TIFF 程式庫的建置過程,它會建立一個可執行檔,然後執行該檔以產生一個包含系統特定資訊的原始碼檔案。然後,這個檔案會作為建置主要 TIFF 程式庫的原始碼檔案。另一個例子是 Visualization Toolkit (VTK),它會建置一個名為 vtkWrapTcl 的可執行檔,將 C++ 類別包裝到 Tcl 中。這個可執行檔會被建置,然後用於建立更多用於建置過程的原始碼檔案。

###################################################
# Test using a compiled program to create a file
####################################################

# add the executable that will create the file
# build creator executable from creator.cxx
add_executable(creator creator.cxx)

# add the custom command to produce created.c
add_custom_command(
  OUTPUT ${PROJECT_BINARY_DIR}/created.c
  DEPENDS creator
  COMMAND creator ${PROJECT_BINARY_DIR}/created.c
  )

# add an executable that uses created.c
add_executable(Foo ${PROJECT_BINARY_DIR}/created.c)

這個範例的第一部分會從原始碼檔案 creator.cxx 產生 creator 可執行檔。然後,自訂命令會設定一個規則,透過執行可執行檔 creator 來產生原始碼檔案 created.c。自訂命令依賴 creator 目標,並將其結果寫入輸出樹狀結構 (PROJECT_BINARY_DIR)。最後,新增一個名為 Foo 的可執行檔目標,它會使用 created.c 原始碼檔案來建置。CMake 會在 Makefile (或 Visual Studio 工作區) 中建立所有必要的規則,以便在您建置專案時,會先建置 creator 可執行檔,並執行它以建立 created.c,然後使用該檔案來建置 Foo 可執行檔。

新增自訂目標

到目前為止的討論中,CMake 目標通常指的是可執行檔和程式庫。CMake 支援更廣泛的目標概念,稱為自訂目標,只要您想要目標的概念,但最終產品不是程式庫或可執行檔時,就可以使用自訂目標。自訂目標的範例包括建置文件、執行測試或更新網頁的目標。若要新增自訂目標,請使用 add_custom_target 命令。

指定的名稱將會是該目標的名稱。您可以使用該名稱,透過 Makefiles (make name) 或 Visual Studio (在目標上按一下滑鼠右鍵,然後選取「建置」) 來專門建置該目標。如果指定了選用的 ALL 引數,這個目標將會包含在 ALL_BUILD 目標中,並且會在每次建置 Makefile 或專案時自動建置。命令和引數是選用的;如果指定,它們會以建置後命令的形式新增到目標。對於只會執行命令的自訂目標,這就是您所需要的一切。更複雜的自訂目標可能會依賴其他檔案,在這些情況下,DEPENDS 引數會用來列出此目標依賴的檔案。我們將考慮這兩種情況的範例。首先,讓我們看看一個沒有依賴項的自訂目標

add_custom_target(FooJAR ALL
  ${JAR} -cvf "\"${PROJECT_BINARY_DIR}/Foo.jar\""
              "\"${PROJECT_SOURCE_DIR}/Java\""
  )

有了上述定義,每當建置 FooJAR 目標時,它就會執行 Java 的封存工具 (jar) 從 ${PROJECT_SOURCE_DIR}/Java 目錄中的 java 類別建立 Foo.jar 檔案。本質上,這種自訂目標允許開發人員將命令連結到目標,以便在建置過程中方便地調用它。現在,讓我們考慮一個更複雜的範例,它大致模擬從 LaTeX 產生 PDF 檔案。在這種情況下,自訂目標依賴其他產生的檔案 (主要是最終產品 .pdf 檔案)

# Add the rule to build the .dvi file from the .tex
# file. This relies on LATEX being set correctly
#
add_custom_command(
  OUTPUT  ${PROJECT_BINARY_DIR}/doc1.dvi
  DEPENDS ${PROJECT_SOURCE_DIR}/doc1.tex
  COMMAND ${LATEX} ${PROJECT_SOURCE_DIR}/doc1.tex
  )

# Add the rule to produce the .pdf file from the .dvi
# file. This relies on DVIPDF being set correctly
#
add_custom_command(
  OUTPUT  ${PROJECT_BINARY_DIR}/doc1.pdf
  DEPENDS ${PROJECT_BINARY_DIR}/doc1.dvi
  COMMAND ${DVIPDF} ${PROJECT_BINARY_DIR}/doc1.dvi
  )

# finally add the custom target that when invoked
# will cause the generation of the pdf file
#
add_custom_target(TDocument ALL
  DEPENDS ${PROJECT_BINARY_DIR}/doc1.pdf
  )

這個範例同時使用了 add_custom_commandadd_custom_target。兩個 add_custom_command 調用用於指定從 .tex 檔案產生 .pdf 檔案的規則。在這種情況下,有兩個步驟和兩個自訂命令。首先,透過執行 LaTeX 從 .tex 檔案產生 .dvi 檔案,然後處理 .dvi 檔案以產生所需的 .pdf 檔案。最後,新增一個名為 TDocument 的自訂目標。它的命令只會輸出它正在執行的動作,而真正的工作是由兩個自訂命令完成的。DEPENDS 引數會設定自訂目標和自訂命令之間的依賴關係。當建置 TDocument 時,它會先查看是否已建置其所有依賴項。如果沒有建置任何依賴項,它會調用適當的自訂命令來建置它們。此範例可以透過將兩個自訂命令合併為一個自訂命令來縮短,如下列範例所示

# Add the rule to build the .pdf file from the .tex
# file. This relies on LATEX and DVIPDF being set correctly
#
add_custom_command(
  OUTPUT  ${PROJECT_BINARY_DIR}/doc1.pdf
  DEPENDS ${PROJECT_SOURCE_DIR}/doc1.tex
  COMMAND ${LATEX}  ${PROJECT_SOURCE_DIR}/doc1.tex
  COMMAND ${DVIPDF} ${PROJECT_BINARY_DIR}/doc1.dvi
  )

# finally add the custom target that when invoked
# will cause the generation of the pdf file

add_custom_target(TDocument ALL
  DEPENDS ${PROJECT_BINARY_DIR}/doc1.pdf
  )

現在考慮文件包含多個檔案的情況。可以修改上述範例,透過使用輸入清單和 foreach 迴圈來處理多個檔案。例如

# set the list of documents to process
set(DOCS doc1 doc2 doc3)

# add the custom commands for each document
foreach(DOC ${DOCS})
  add_custom_command(
    OUTPUT  ${PROJECT_BINARY_DIR}/${DOC}.pdf
    DEPENDS ${PROJECT_SOURCE_DIR}/${DOC}.tex
    COMMAND ${LATEX} ${PROJECT_SOURCE_DIR}/${DOC}.tex
    COMMAND ${DVIPDF} ${PROJECT_BINARY_DIR}/${DOC}.dvi
    )

  # build a list of all the results
  list(APPEND DOC_RESULTS ${PROJECT_BINARY_DIR}/${DOC}.pdf)
endforeach()

# finally add the custom target that when invoked
# will cause the generation of the pdf file
add_custom_target(TDocument ALL
  DEPENDS ${DOC_RESULTS}
  )

在此範例中,建置自訂目標 TDocument 將會導致產生所有指定的 .pdf 檔案。將新文件新增到清單中,只需將其檔案名稱新增到範例頂端的 DOCS 變數中即可。

指定依賴項和輸出

當使用自訂命令和自訂目標時,您通常會指定依賴項。當您指定自訂命令的依賴項或輸出時,應始終指定完整路徑。例如,如果命令在二元樹狀結構中產生 foo.h,則其輸出應該類似於 ${PROJECT_BINARY_DIR}/foo.h。如果未指定,CMake 會嘗試確定檔案的正確路徑;複雜的專案通常會同時使用來源樹狀結構和建置樹狀結構中的檔案,如果未指定完整路徑,最終可能會導致錯誤。

當指定目標作為依賴項時,您可以省略完整路徑和可執行檔副檔名,只需以其名稱參照即可。請考慮本章稍早範例中,將產生器目標指定為 add_custom_command 依賴項。CMake 會將 creator 識別為符合現有目標,並正確處理依賴項。

當一個輸出沒有一個規則時

使用自訂命令時,可能會出現一些不尋常的情況,需要進一步解釋。第一種情況是一個命令 (或可執行檔) 可以建立多個輸出,第二種情況是可以使用多個命令來建立單一輸出。

單一命令產生多個輸出

在 CMake 中,自訂命令可以產生多個輸出,只需在 OUTPUT 關鍵字後列出多個輸出即可。CMake 會為您的建置系統建立正確的規則,以便無論哪個目標需要哪個輸出,都會執行正確的規則。如果可執行檔恰好產生了一些輸出,但建置過程只使用其中一個輸出,那麼您可以在建立自訂命令時忽略其他輸出。假設可執行檔產生一個用於建置過程的原始碼檔案,以及一個未使用的執行記錄。自訂命令應將原始碼檔案指定為輸出,並忽略也產生記錄檔的事實。

具有多個輸出的一個命令的另一種情況是命令相同,但其引數會變更。這實際上與具有不同命令相同,而且每種情況都應該有自己的自訂命令。其中一個例子是文件範例,其中為每個 .tex 檔案新增了一個自訂命令。命令相同,但每次傳遞給它的引數都會變更。

具有可由不同命令產生的單一輸出

在極少數情況下,您可能會發現有多個命令可用於產生輸出。大多數建置系統 (例如 make 和 Visual Studio) 不支援此功能,CMake 也不支援。有兩種常用的方法可以解決這個問題。如果您確實有兩個不同的命令可以產生相同的輸出,而且沒有其他重要的輸出,那麼您可以簡單地選擇其中一個命令並為它建立自訂命令。

在更複雜的情況下,有多個具有多個輸出的命令;例如

Command1 produces foo.h and bar.h
Command2 produces widget.h and bar.h

在這種情況下,可以使用幾種方法。您可以考慮將兩個命令和所有三個輸出合併到單個自訂命令中,以便在需要一個輸出時,同時建立所有三個輸出。您也可以建立三個自訂命令,每個命令對應一個唯一的輸出。foo.h 的自訂命令會調用 Command1,而 widget.h 的自訂命令會調用 Command2。在指定 bar.h 的自訂命令時,您可以選擇 Command1 或 Command2。