ตัวอย่าง CMake ที่เรียบง่าย แต่สมบูรณ์ที่สุด


117

ฉันรู้สึกสับสนโดยสิ้นเชิงกับวิธีการทำงานของ CMake ทุกครั้งที่ฉันคิดว่าฉันเริ่มเข้าใจวิธีการเขียน CMake มากขึ้นมันจะหายไปในตัวอย่างถัดไปที่ฉันอ่าน สิ่งที่ฉันอยากรู้คือฉันควรจัดโครงสร้างโครงการของฉันอย่างไรเพื่อให้ CMake ของฉันต้องการการบำรุงรักษาน้อยที่สุดในอนาคต ตัวอย่างเช่นฉันไม่ต้องการอัปเดต CMakeList.txt เมื่อฉันเพิ่มโฟลเดอร์ใหม่ในโครงสร้าง src ซึ่งทำงานเหมือนกับโฟลเดอร์ src อื่น ๆ ทั้งหมด

นี่เป็นวิธีที่ฉันจินตนาการถึงโครงสร้างโครงการของฉัน แต่โปรดเป็นเพียงตัวอย่างเท่านั้น หากวิธีที่แนะนำแตกต่างกันโปรดบอกฉันและบอกฉันว่าต้องทำอย่างไร

myProject
    src/
        module1/
            module1.h
            module1.cpp
        module2/
            [...]
        main.cpp
    test/
        test1.cpp
    resources/
        file.png
    bin
        [execute cmake ..]

อย่างไรก็ตามสิ่งสำคัญคือโปรแกรมของฉันต้องรู้ว่าทรัพยากรอยู่ที่ไหน ฉันต้องการทราบวิธีการจัดการทรัพยากรที่แนะนำ ฉันไม่ต้องการเข้าถึงทรัพยากรของฉันด้วย "../resources/file.png"


1
For example I don't want to update my CMakeList.txt when I am adding a new folder in my src treeคุณช่วยยกตัวอย่าง IDE ที่รวบรวมแหล่งข้อมูลโดยอัตโนมัติได้ไหม

7
ปกติแล้วไม่มี ide จะไม่รวบรวมแหล่งข้อมูลโดยอัตโนมัติเพราะไม่จำเป็นต้องทำ เมื่อฉันเพิ่มไฟล์หรือโฟลเดอร์ใหม่ฉันจะทำภายใน ide และโปรเจ็กต์ได้รับการอัปเดต ระบบบิลด์ในอีกด้านหนึ่งไม่สังเกตเห็นเมื่อฉันเปลี่ยนไฟล์บางไฟล์ดังนั้นจึงเป็นพฤติกรรมที่ต้องการที่จะรวบรวมไฟล์ต้นฉบับทั้งหมดโดยอัตโนมัติ
Arne

4
หากฉันเห็นลิงก์นั้นฉันรู้สึกว่า CMake ล้มเหลวในงานที่สำคัญที่สุดที่ต้องการแก้ไข: การสร้างระบบสร้างข้ามแพลตฟอร์มเป็นเรื่องง่าย
Arne

คำตอบ:


94

หลังจากการวิจัยตอนนี้ฉันมีตัวอย่าง cmake ที่เรียบง่ายที่สุด แต่สมบูรณ์แบบของตัวเองแล้ว นี่คือและพยายามที่จะครอบคลุมพื้นฐานส่วนใหญ่รวมถึงทรัพยากรและบรรจุภัณฑ์

สิ่งหนึ่งที่ไม่ได้มาตรฐานคือการจัดการทรัพยากร ตามค่าเริ่มต้น cmake ต้องการใส่ไว้ใน / usr / share /, / usr / local / share / และสิ่งที่เทียบเท่าบน windows ฉันต้องการมี zip / tar.gz แบบธรรมดาที่คุณสามารถแยกได้ทุกที่และเรียกใช้ ดังนั้นทรัพยากรจึงถูกโหลดโดยสัมพันธ์กับไฟล์ปฏิบัติการ

กฎพื้นฐานในการทำความเข้าใจคำสั่ง cmake คือไวยากรณ์ต่อไปนี้: <function-name>(<arg1> [<arg2> ...])โดยไม่มีเครื่องหมายจุลภาคหรือครึ่ง สี แต่ละอาร์กิวเมนต์เป็นสตริง foobar(3.0)และfoobar("3.0")ก็เหมือนกัน คุณสามารถตั้งค่ารายการ / ตัวแปรด้วยset(args arg1 arg2). ด้วยชุดตัวแปรนี้foobar(${args}) และfoobar(arg1 arg2)มีประสิทธิภาพเหมือนกัน ตัวแปรที่ไม่มีอยู่จริงเทียบเท่ากับรายการว่าง รายการภายในเป็นเพียงสตริงที่มีอัฒภาคเพื่อแยกองค์ประกอบ ดังนั้นรายการที่มีองค์ประกอบเดียวจึงเป็นไปตามความหมายขององค์ประกอบนั้นเท่านั้นจึงไม่มีการชกมวยเกิดขึ้น ตัวแปรเป็นแบบโกลบอล ฟังก์ชันในตัวเสนออาร์กิวเมนต์ที่มีชื่อบางรูปแบบโดยข้อเท็จจริงที่ว่าพวกเขาคาดหวังรหัสบางอย่างเช่นPUBLICหรือDESTINATIONในรายการอาร์กิวเมนต์เพื่อจัดกลุ่มอาร์กิวเมนต์ แต่นั่นไม่ใช่คุณลักษณะของภาษารหัสเหล่านั้นเป็นเพียงสตริงและแยกวิเคราะห์โดยการใช้ฟังก์ชัน

คุณสามารถโคลนทุกอย่างจากgithub

cmake_minimum_required(VERSION 3.0)
project(example_project)

###############################################################################
## file globbing ##############################################################
###############################################################################

# these instructions search the directory tree when cmake is
# invoked and put all files that match the pattern in the variables 
# `sources` and `data`
file(GLOB_RECURSE sources      src/main/*.cpp src/main/*.h)
file(GLOB_RECURSE sources_test src/test/*.cpp)
file(GLOB_RECURSE data resources/*)
# you can use set(sources src/main.cpp) etc if you don't want to
# use globing to find files automatically

###############################################################################
## target definitions #########################################################
###############################################################################

# add the data to the target, so it becomes visible in some IDE
add_executable(example ${sources} ${data})

# just for example add some compiler flags
target_compile_options(example PUBLIC -std=c++1y -Wall -Wfloat-conversion)

# this lets me include files relative to the root src dir with a <> pair
target_include_directories(example PUBLIC src/main)

# this copies all resource files in the build directory
# we need this, because we want to work with paths relative to the executable
file(COPY ${data} DESTINATION resources)

###############################################################################
## dependencies ###############################################################
###############################################################################

# this defines the variables Boost_LIBRARIES that contain all library names
# that we need to link to
find_package(Boost 1.36.0 COMPONENTS filesystem system REQUIRED)

target_link_libraries(example PUBLIC
  ${Boost_LIBRARIES}
  # here you can add any library dependencies
)

###############################################################################
## testing ####################################################################
###############################################################################

# this is for our testing framework
# we don't add REQUIRED because it's just for testing
find_package(GTest)

if(GTEST_FOUND)
  add_executable(unit_tests ${sources_test} ${sources})

  # we add this define to prevent collision with the main
  # this might be better solved by not adding the source with the main to the
  # testing target
  target_compile_definitions(unit_tests PUBLIC UNIT_TESTS)

  # this allows us to use our executable as a link library
  # therefore we can inherit all compiler options and library dependencies
  set_target_properties(example PROPERTIES ENABLE_EXPORTS on)

  target_link_libraries(unit_tests PUBLIC
    ${GTEST_BOTH_LIBRARIES}
    example
  )

  target_include_directories(unit_tests PUBLIC
    ${GTEST_INCLUDE_DIRS} # doesn't do anything on Linux
  )
endif()

###############################################################################
## packaging ##################################################################
###############################################################################

# all install commands get the same destination. this allows us to use paths
# relative to the executable.
install(TARGETS example DESTINATION example_destination)
# this is basically a repeat of the file copy instruction that copies the
# resources in the build directory, but here we tell cmake that we want it
# in the package
install(DIRECTORY resources DESTINATION example_destination)

# now comes everything we need, to create a package
# there are a lot more variables you can set, and some
# you need to set for some package types, but we want to
# be minimal here
set(CPACK_PACKAGE_NAME "MyExample")
set(CPACK_PACKAGE_VERSION "1.0.0")

# we don't want to split our program up into several things
set(CPACK_MONOLITHIC_INSTALL 1)

# This must be last
include(CPack)

8
@SteveLorimer ฉันไม่เห็นด้วยการ globbing ไฟล์นั้นเป็นรูปแบบที่ไม่ดีฉันคิดว่าการคัดลอกแผนผังไฟล์ไปยัง CMakeLists.txt ด้วยตนเองนั้นเป็นรูปแบบที่ไม่ดีเพราะมันซ้ำซ้อน แต่ฉันรู้ว่ามีคนไม่เห็นด้วยในหัวข้อนี้ดังนั้นฉันจึงแสดงความคิดเห็นไว้ในโค้ดซึ่งคุณสามารถแทนที่ globbing ด้วยรายการที่มีไฟล์ต้นฉบับทั้งหมดอย่างชัดเจน ค้นหาset(sources src/main.cpp).
Arne

3
@SteveLorimer ใช่บ่อยครั้งฉันต้องเรียกใช้ cmake อีกครั้ง ทุกครั้งที่ฉันเพิ่มบางสิ่งในแผนผังไดเร็กทอรีฉันจำเป็นต้องเรียกใช้ cmake อีกครั้งด้วยตนเองเพื่อให้ได้รับการประเมินค่า globbing อีกครั้ง หากคุณใส่ไฟล์ลงในCMakeLists.txtmake ปกติ (หรือนินจา) จะทำให้เกิดการย้ายตำแหน่งใหม่ของ cmake ดังนั้นคุณจึงไม่สามารถลืมได้ นอกจากนี้ยังเป็นมิตรกับทีมเล็กน้อยเพราะสมาชิกในทีมก็ไม่สามารถลืมที่จะดำเนินการ cmake ได้ แต่ฉันคิดว่าไม่ควรแตะ makefile เพียงเพราะมีคนเพิ่มไฟล์ เขียนครั้งเดียวและไม่มีใครต้องคิดเรื่องนี้อีกเลย
Arne

3
@SteveLorimer ฉันไม่เห็นด้วยกับรูปแบบที่จะใส่ CMakeLists.txt หนึ่งรายการในทุกไดเรกทอรีของโครงการเพียงกระจายการกำหนดค่าโครงการไปทุกหนทุกแห่งฉันคิดว่าไฟล์เดียวที่จะทำทั้งหมดก็น่าจะเพียงพอมิฉะนั้นคุณจะสูญเสียภาพรวมของสิ่งที่ ทำได้จริงในขั้นตอนการสร้าง นั่นไม่ได้หมายความว่าจะไม่มีไดเร็กทอรีย่อยที่มี CMakeLists.txt ของตัวเองฉันแค่คิดว่ามันควรจะเป็นข้อยกเว้น
Arne

2
สมมติว่า"VCS"ย่อมาจาก"version control system"นั่นไม่เกี่ยวข้อง ปัญหาไม่ได้อยู่ที่อาร์ติแฟกต์จะไม่ถูกเพิ่มในการควบคุมแหล่งที่มา ปัญหาคือ CMake จะไม่สามารถประเมินไฟล์ต้นฉบับที่เพิ่มเข้ามาอีกครั้ง มันจะไม่สร้างไฟล์อินพุตระบบบิลด์ขึ้นมาใหม่ ระบบสร้างจะยึดติดกับไฟล์อินพุตที่ล้าสมัยอย่างมีความสุขซึ่งอาจนำไปสู่ข้อผิดพลาด (หากคุณโชคดี) หรือไม่มีใครสังเกตเห็นหากคุณหมดโชค GLOBbing ก่อให้เกิดช่องว่างในห่วงโซ่การคำนวณการพึ่งพา นี่เป็นปัญหาสำคัญและความคิดเห็นไม่ได้รับทราบอย่างเหมาะสม
IInspectable

2
CMake และ VCS ทำงานแยกกันอย่างสมบูรณ์ VCS ไม่รู้จัก CMake และ CMake ไม่รู้จัก VCS ใด ๆ ไม่มีความเชื่อมโยงระหว่างกัน เว้นแต่คุณจะแนะนำว่านักพัฒนาควรทำตามขั้นตอนด้วยตนเองโดยนำข้อมูลออกจาก VCS และใช้ CMake แบบฮิวริสติกและรันซ้ำ นั่นไม่ได้ขยายขนาดอย่างเห็นได้ชัดและมีความอ่อนไหวต่อการเข้าใจผิดซึ่งเป็นเรื่องแปลกสำหรับมนุษย์ ไม่ขออภัยคุณยังไม่ได้สร้างจุดที่น่าเชื่อถือสำหรับไฟล์ GLOBbing
IInspectable

39

ตัวอย่างพื้นฐานที่สุด แต่สมบูรณ์สามารถพบได้ในบทช่วยสอน CMake :

cmake_minimum_required (VERSION 2.6)
project (Tutorial)
add_executable(Tutorial tutorial.cxx)

สำหรับตัวอย่างโครงการของคุณคุณอาจมี:

cmake_minimum_required (VERSION 2.6)
project (MyProject)
add_executable(myexec src/module1/module1.cpp src/module2/module2.cpp src/main.cpp)
add_executable(mytest test1.cpp)

สำหรับคำถามเพิ่มเติมของคุณวิธีหนึ่งที่จะไปคืออีกครั้งในบทช่วยสอน: สร้างไฟล์ส่วนหัวที่กำหนดค่าได้ซึ่งคุณรวมไว้ในโค้ดของคุณ สำหรับสิ่งนี้ให้สร้างไฟล์ที่configuration.h.inมีเนื้อหาต่อไปนี้:

#define RESOURCES_PATH "@RESOURCES_PATH@"

จากนั้นในการCMakeLists.txtเพิ่มของคุณ:

set(RESOURCES_PATH "${PROJECT_SOURCE_DIR}/resources/"
# configure a header file to pass some of the CMake settings
# to the source code
configure_file (
  "${PROJECT_SOURCE_DIR}/configuration.h.in"
  "${PROJECT_BINARY_DIR}/configuration.h"
)

# add the binary tree to the search path for include files
# so that we will find TutorialConfig.h
include_directories("${PROJECT_BINARY_DIR}")

สุดท้ายที่คุณต้องการเส้นทางในโค้ดของคุณคุณสามารถทำได้:

#include "configuration.h"

...

string resourcePath = string(RESOURCE_PATH) + "file.png";

ขอบคุณมากโดยเฉพาะอย่างยิ่งสำหรับ RESOURCE_PATH ฉันไม่เข้าใจว่า config_file คือสิ่งที่ฉันกำลังมองหา แต่คุณเพิ่มไฟล์ทั้งหมดจากโปรเจ็กต์ด้วยตนเองมีวิธีที่ดีกว่าในการกำหนดรูปแบบที่จะเพิ่มไฟล์ทั้งหมดจากโครงสร้าง src หรือไม่?
Arne

ดูคำตอบของ Dieter แต่ยังรวมถึงความคิดเห็นของฉันเกี่ยวกับสาเหตุที่คุณไม่ควรใช้ หากคุณต้องการทำให้เป็นอัตโนมัติวิธีที่ดีกว่าคือการเขียนสคริปต์ที่คุณสามารถเรียกใช้เพื่อสร้างรายการไฟล์ต้นฉบับใหม่ (หรือใช้ cmake Awareness IDE ที่ทำสิ่งนี้ให้คุณฉันไม่คุ้นเคยกับสิ่งใด ๆ )
sgvd

3
@sgvd string resourcePath = string(RESOURCE_PATH) + "file.png"IMHO เป็นความคิดที่ดีที่จะฮาร์ดโค้ดพา ธสัมบูรณ์ไปยังไดเร็กทอรีต้นทาง เกิดอะไรขึ้นถ้าคุณต้องการติดตั้งโครงการของคุณ?

2
ฉันรู้ว่าการรวบรวมแหล่งที่มาโดยอัตโนมัตินั้นฟังดูดี แต่อาจทำให้เกิดภาวะแทรกซ้อนได้ทุกประเภท ดูคำถามนี้จากขณะที่ผ่านมาสำหรับการสนทนาสั้น: stackoverflow.com/q/10914607/1401351
ปีเตอร์

2
คุณจะได้รับข้อผิดพลาดเดียวกันทุกประการหากคุณไม่ได้เรียกใช้ cmake การเพิ่มไฟล์ด้วยมือจะใช้เวลาหนึ่งวินาทีต่อครั้งการเรียกใช้ cmake ในทุก ๆ คอมไพล์จะใช้เวลาหนึ่งวินาทีทุกครั้ง คุณทำลายคุณลักษณะของ cmake ใครบางคนที่ทำงานในโปรเจ็กต์เดียวกันและดึงการเปลี่ยนแปลงของคุณจะทำ: รัน make -> รับการอ้างอิงที่ไม่ได้กำหนด -> หวังว่าอย่าลืมรีรัน cmake หรือไฟล์บั๊กกับคุณ -> รัน cmake -> รันทำให้สำเร็จในขณะที่คุณเพิ่มไฟล์ เขาทำด้วยมือ: ทำงานให้สำเร็จ -> ใช้เวลากับครอบครัว สรุปได้ว่าอย่าขี้เกียจและเผื่อตัวเองและคนอื่น ๆ ให้ปวดหัวในอนาคต
sgvd

2

ที่นี่ฉันเขียนตัวอย่างไฟล์ CMakeLists.txt ที่เรียบง่าย แต่สมบูรณ์ที่สุด

รหัสแหล่งที่มา

  1. บทแนะนำจาก hello world ไปจนถึงข้ามแพลตฟอร์ม Android / iOS / เว็บ / เดสก์ท็อป
  2. ฉันปล่อยตัวอย่างแอปพลิเคชันแต่ละแพลตฟอร์ม
  3. โครงสร้างไฟล์ 08-cross_platform ได้รับการยืนยันโดยงานของฉัน
  4. มันอาจจะไม่สมบูรณ์แบบ แต่มีประโยชน์และแนวทางปฏิบัติที่ดีที่สุดสำหรับทีมของฉันเอง

หลังจากนั้นฉันก็เสนอเอกสารสำหรับรายละเอียด

หากคุณมีคำถามใด ๆ โปรดติดต่อฉันและฉันอยากจะอธิบาย

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.