《CMake實踐》筆記二:INSTALL/CMAKE_INSTALL_PREFIX

mjiansun發表於2020-12-16

四、更好一點的Hello World

沒有最好,只有更好

從本小節開始,後面所有的構建我們都將採用 out-of-source 外部構建,約定的構建目錄是工程目錄下的build自錄。

本小節的任務是讓前面的Hello World更像一個工程,我們需要作的是:

(1)、為工程新增一個子目錄src,用來放置工程原始碼;

(2)、新增一個子目錄doc,用來放置這個工程的文件hello.txt

(3)、在工程目錄新增文字檔案COPYRIGHT, README;

(4)、在工程目錄新增一個runhello.sh指令碼,用來呼叫hello二進位制

(5)、將構建後的目標檔案放入構建目錄的bin子目錄;

(6)、最終安裝這些檔案:將hello二進位制與runhello.sh安裝至/usr/bin,將doc目錄的內容以及COPYRIGHT/README安裝到/usr/share/doc/cmake/t2

1、準備工作:

在/backup/cmake/目錄下建立t2目錄。將t1工程的main.c和CMakeLists.txt拷貝到t2目錄中。

2、新增子目錄src:

mkdir src
mv main.c src

現在的工程看起來是這個樣子:一個子目錄src,一個CMakeLists.txt。

上一節我們提到,需要為任何子目錄建立一個CMakeLists.txt,進入子目錄src,編寫CMakeLists.txt如下:

ADD_EXECUTABLE(hello main.c)

將t2工程的CMakeLists.txt修改為:

PROJECT(HELLO)
ADD_SUBDIRECTORY(src bin)

然後建立build目錄,進入build目錄進行外部編譯。

cmake ..
make

構建完成後,你會發現生成的目標檔案hello位於build/bin目錄中。

語法解釋:

ADD_SUBDIRECTORY指令

ADD_SUBDIRECTORY(source_dir [binary_dir] [EXCLUDE_FROM_ALL])

這個指令用於向當前工程新增存放原始檔的子目錄,並可以指定中間二進位制和目標二進位制存放的位置。EXCLUDE_FROM_ALL引數的含義是將這個目錄從編譯過程中排除,比如,工程的example,可能就需要工程構建完成後,再進入example目錄單獨進行構建(當然,你也可以通過定義依賴來解決此類問題)。

上面的例子定義了將src子目錄加入工程,並指定編譯輸出(包含編譯中間結果)路徑為bin目錄。如果不進行bin目錄的指定,那麼編譯結果(包括中間結果)都將存放在build/src目錄(這個目錄跟原有的src目錄對應),指定bin目錄後,相當於在編譯時將src重新命名為bin,所有的中間結果和目標二進位制都將存放在bin目錄。這裡需要提一下的是SUBDIRS指令,使用方法是:SUBDIRS(dir1 dir2...),但是這個指令已經不推薦使用。它可以一次新增多個子目錄,並且,即使外部編譯,子目錄體系仍然會被儲存。如果我們在上面的例子中將ADD_SUBDIRECTORY (src bin)修改為SUBDIRS(src)。那麼在build目錄中將出現一個src目錄,生成的目的碼hello將存放在src目錄中。

3、換個地方儲存目標二進位制

不論是SUBDIRS還是ADD_SUBDIRECTORY指令(不論是否指定編譯輸出目錄),我們都可以通過SET指令重新定義EXECUTABLE_OUTPUT_PATH和LIBRARY_OUTPUT_PATH變數來指定最終的目標二進位制的位置(指最終生成的hello或者最終的共享庫,不包含編譯生成的中間檔案)

SET(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin)
SET(LIBRARY_OUTPUT_PATH ${PROJECT_BINARY_DIR}/lib)

在第一節我們提到了<projectname>_BINARY_DIR和PROJECT_BINARY_DIR變數,他們指的編譯發生的當前目錄,如果是內部編譯,就相當於PROJECT_SOURCE_DIR也就是工程程式碼所在目錄,如果是外部編譯,指的是外部編譯所在目錄,也就是本例中的build目錄。

所以,上面兩個指令分別定義了:可執行二進位制的輸出路徑為build/bin和庫的輸出路徑為build/lib.本節我們沒有提到共享庫和靜態庫的構建,所以,你可以不考慮第二條指令。
問題是,我應該把這兩條指令寫在工程的CMakeLists.txt還是src目錄下的CMakeLists.txt,把握一個簡單的原則,在哪裡ADD_EXECUTABLE或ADD_LIBRARY,如果需要改變目標存放路徑,就在哪裡加入上述的定義。在這個例子裡,當然就是指src下的CMakeLists.txt了。

4、如何安裝

安裝的需要有兩種,一種是從程式碼編譯後直接make install安裝,一種是打包時的指定目錄安裝。所以,即使最簡單的手工編寫的Makefile,看起來也是這個樣子的:

DESTDIR=
install:
mkdir -p $(DESTDIR)/usr/bin
install -m 755 hello $(DESTDIR)/usr/bin

你可以通過:

make install

將hello直接安裝到/usr/bin目錄,也可以通過make install DESTDIR=/tmp/test將他安裝在/tmp/test/usr/bin目錄,打包時這個方式經常被使用。

稍微複雜一點的是還需要定義PREFIX,一般autotools工程,會執行這樣的指令:

./configure –prefix=/usr 或者 ./configure --prefix=/usr/local 來指定PREFIX.比如上面的Makefile就可以改寫成:
DESTDIR=
PREFIX=/usr
install:
mkdir -p $(DESTDIR)/$(PREFIX)/bin
install -m 755 hello $(DESTDIR)/$(PREFIX)/bin

那麼我們的HelloWorld應該怎麼進行安裝呢?

這裡需要引入一個新的cmake 指令 INSTALL和一個非常有用的變數CMAKE_INSTALL_PREFIX

CMAKE_INSTALL_PREFIX變數類似於configure指令碼的 –prefix,常見的使用方法看起來是這個樣子:

cmake -DCMAKE_INSTALL_PREFIX=/usr .

INSTALL指令用於定義安裝規則,安裝的內容可以包括目標二進位制、動態庫、靜態庫以及檔案、目錄、指令碼等。

INSTALL指令包含了各種安裝型別,我們需要一個個分開解釋:

目標檔案的安裝:

INSTALL(TARGETS targets...
[[ARCHIVE|LIBRARY|RUNTIME]
[DESTINATION <dir>]
[PERMISSIONS permissions...]
[CONFIGURATIONS
[Debug|Release|...]]
[COMPONENT <component>]
[OPTIONAL]
] [...])

引數中的TARGETS後面跟的就是我們通過ADD_EXECUTABLE或者ADD_LIBRARY定義的目標檔案,可能是可執行二進位制、動態庫、靜態庫。目標型別也就相對應的有三種,ARCHIVE特指靜態庫,LIBRARY特指動態庫,RUNTIME特指可執行目標二進位制。

DESTINATION定義了安裝的路徑,如果路徑以/開頭,那麼指的是絕對路徑,這時候CMAKE_INSTALL_PREFIX其實就無效了。如果你希望使用CMAKE_INSTALL_PREFIX來定義安裝路徑,就要寫成相對路徑,即不要以/開頭,那麼安裝後的路徑就是${CMAKE_INSTALL_PREFIX}/<DESTINATION定義的路徑>

舉個簡單的例子:

INSTALL(TARGETS myrun mylib mystaticlib
RUNTIME DESTINATION bin
LIBRARY DESTINATION lib
ARCHIVE DESTINATION libstatic
)

上面的例子會將:

可執行二進位制myrun安裝到${CMAKE_INSTALL_PREFIX}/bin目錄動態庫libmylib安裝到${CMAKE_INSTALL_PREFIX}/lib目錄,靜態庫libmystaticlib安裝到${CMAKE_INSTALL_PREFIX}/libstatic目錄,特別注意的是:你不需要關心TARGETS具體生成的路徑,只需要寫上TARGETS名稱就可以了。

普通檔案的安裝:

INSTALL(FILES files... DESTINATION <dir>
[PERMISSIONS permissions...]
[CONFIGURATIONS [Debug|Release|...]]
[COMPONENT <component>]
[RENAME <name>] [OPTIONAL])

可用於安裝一般檔案,並可以指定訪問許可權,檔名是此指令所在路徑下的相對路徑。如果預設不定義許可權PERMISSIONS,安裝後的許可權為:OWNER_WRITE, OWNER_READ, GROUP_READ,和WORLD_READ,即644許可權。

非目標檔案的可執行程式安裝(比如指令碼之類):

INSTALL(PROGRAMS files... DESTINATION <dir>
[PERMISSIONS permissions...]
[CONFIGURATIONS [Debug|Release|...]]
[COMPONENT <component>]
[RENAME <name>] [OPTIONAL])

跟上面的FILES指令使用方法一樣,唯一的不同是安裝後許可權為:OWNER_EXECUTE, GROUP_EXECUTE, 和WORLD_EXECUTE,即755許可權

目錄的安裝:

INSTALL(DIRECTORY dirs... DESTINATION <dir>
[FILE_PERMISSIONS permissions...]
[DIRECTORY_PERMISSIONS permissions...]
[USE_SOURCE_PERMISSIONS]
[CONFIGURATIONS [Debug|Release|...]]
[COMPONENT <component>]
[[PATTERN <pattern> | REGEX <regex>]
[EXCLUDE] [PERMISSIONS permissions...]] [...])

這裡主要介紹其中的DIRECTORY、PATTERN以及PERMISSIONS引數。

DIRECTORY後面連線的是所在Source目錄的相對路徑,但務必注意:abc和abc/有很大的區別。如果目錄名不以/結尾,那麼這個目錄將被安裝為目標路徑下的abc,如果目錄名以/結尾,代表將這個目錄中的內容安裝到目標路徑,但不包括這個目錄本身。PATTERN用於使用正規表示式進行過濾,PERMISSIONS用於指定PATTERN過濾後的檔案許可權。

我們來看一個例子:

INSTALL(DIRECTORY icons scripts/ DESTINATION share/myproj
PATTERN "CVS" EXCLUDE
PATTERN "scripts/*"
PERMISSIONS OWNER_EXECUTE OWNER_WRITE OWNER_READ
GROUP_EXECUTE GROUP_READ)

這條指令的執行結果是:

將icons目錄安裝到 <prefix>/share/myproj,將scripts/中的內容安裝到<prefix>/share/myproj不包含目錄名為CVS的目錄,對於scripts/*檔案指定許可權為 OWNER_EXECUTE OWNER_WRITE OWNER_READ ROUP_EXECUTE GROUP_READ。

安裝時CMAKE指令碼的執行:

INSTALL([[SCRIPT <file>] [CODE <code>]] [...])
SCRIPT引數用於在安裝時呼叫cmake指令碼檔案(也就是<abc>.cmake檔案)
CODE引數用於執行CMAKE指令,必須以雙引號括起來。比如:
INSTALL(CODE "MESSAGE(\"Sample install message.\")")
安裝還有幾個被標記為過時的指令,比如INSTALL_FILES等,這些指令已經不再推薦使用,所以,這裡就不再贅述了。下面,我們就來改寫我們的工程檔案,讓他來支援各種檔案的安裝,並且,我們要使用CMAKE_INSTALL_PREFIX指令。

5、修改Helloworld支援安裝

首先我們先補上為新增的檔案。

新增doc目錄及檔案:

cd /backup/cmake/t2
mkdir doc
vi doc/hello.txt

隨便填寫一些內容並儲存

在工程目錄新增runhello.sh指令碼,內容為:

./hello

新增工程目錄中的COPYRIGHT和README

touch COPYRIGHT
touch README

下面改寫各目錄的CMakeLists.txt檔案。

(1)、安裝COPYRIGHT/README,直接修改主工程檔案CMakelists.txt,加入以下指令:

INSTALL(FILES COPYRIGHT README DESTINATION share/doc/cmake/t2)

(2)、安裝runhello.sh,直接修改主工程檔案CMakeLists.txt,加入如下指令(這個說實話不知道幹麻用的):

INSTALL(PROGRAMS runhello.sh DESTINATION bin)

(3)、安裝doc中的hello.txt,這裡有兩種方式:

一是通過在doc目錄建立 CMakeLists.txt並將doc目錄通過ADD_SUBDIRECTORY加入工程來完成。

另一種方法是直接在工程目錄通過 INSTALL(DIRECTORY來完成),前者比較簡單,各位可以根據興趣自己完成,我們來嘗試後者,順便演示以下DIRECTORY的安裝。因為hello.txt要安裝到/<prefix>/share/doc/cmake/t2,所以我們不能直接安裝整個doc目錄,這裡採用的方式是安裝doc目錄中的內容,也就是使用”doc/”在工程檔案中新增

INSTALL(DIRECTORY doc/ DESTINATION share/doc/cmake/t2)

(4)、安裝hello可執行程式,修改主程式檔案CMakelists.txt,加入以下指令:

INSTALL(TARGETS hello RUNTIME DESTINATION bin)

6、嘗試我們修改的結果

現在進入build目錄進行外部編譯,注意使用CMAKE_INSTALL_PREFIX引數,這裡我們將它安裝到了/tmp/t2目錄:

cmake -DCMAKE_INSTALL_PREFIX=/tmp/t2/usr ..

然後執行

make
make install

讓我們進入/tmp/t2目錄看一下安裝結果:

./usr
./usr/share
./usr/share/doc
./usr/share/doc/cmake
./usr/share/doc/cmake/t2
./usr/share/doc/cmake/t2/hello.txt
./usr/share/doc/cmake/t2/README
./usr/share/doc/cmake/t2/COPYRIGHT
./usr/bin
./usr/bin/hello
./usr/bin/runhello.sh

如果你要直接安裝到系統,可以使用如下指令:

cmake -DCMAKE_INSTALL_PREFIX=/usr ..

7、一個疑問

如果我沒有定義CMAKE_INSTALL_PREFIX會安裝到什麼地方?你可以嘗試以下,cmake ..;make;make install,你會發現CMAKE_INSTALL_PREFIX的預設定義是/usr/local

8、小結

本小節主要描述瞭如何在工程中使用多目錄、各種安裝指令以及CMAKE_INSTALL_PREFIX變數(你真夠牛的,這麼點東西居然羅唆了這麼多文字)在下一小節,我們將探討如何在cmake中構建動態庫和靜態庫,以及如何使用外部標頭檔案和外部共享庫,畢竟,這是程式編寫中最常使用的(對了,你知道用怎樣的gcc引數可以直接構建靜態庫和動態庫嗎?)

未完,待續。。。。

 


以下是我自己增加的東西。

把這一節試了一下,感覺還是有點不爽,因為要編譯這個工程,首先要自己建立一個目錄,然後進入這個目錄後先執行一次cmake的命令,在執行make 和 make install。因此個人將這個例子自己稍微改了一下,讓安裝和解除安裝更方便使用。

首先目錄結構還是一樣的,工程目錄下一個src目錄,一個CMakeLists.txt,一個空的COPYRIGHT和README,一個doc目錄,裡面有一個空的hello.txt檔案。

然後自己增加一個install.sh和uninstall.sh指令碼,用於安裝這個工程和解除安裝這個工程,整個目錄結構如下:

 

指令碼的內容也很簡單,可以指定安裝目錄,或者用預設的安裝目錄:

#!/bin/sh

if [ "$#" -eq 0 ]
then 
    echo "use default install dir: target"
    INSTALL_DIR="../target"
elif [ "$#" -eq 1 ]
then 
    echo "use custom install dir: $1"
    INSTALL_DIR=$1
else
    echo "error"
fi

rm -drf bulid
rm -drf $INSTALL_DIR

mkdir bulid

cd bulid
cmake -DCMAKE_INSTALL_PREFIX=$INSTALL_DIR ..
make
make install

這樣,在拿到這個共亨的gz檔案後,解壓,直接執行install.sh指令碼,入過不帶路徑,安裝在當前目錄下的target目錄下,否則安裝在指定的目錄下:

 

相關文章