搜尋

Evan Gui

Love food and program!

作者

Evan Gui

Develop with Docker Compose

Docker Compose

Docker 是應用程式虛擬化技術,它改變了應用程式的使用方式,藉由 docker 環境的整合將變得不再是任何問題。Docker 官網上有不少的教學告訴我們怎麼去使用 container,用 container 來達到我們所需要的服務,但如果有一項 project 需要多個 container 時該怎麼辦呢?這些 container 之間該如何溝通?該如何去管理?這時後可以使用 Docker Compose 這個工具來解決上述的問題,我們可以事先定義好需要的 service,並定好 services 的相依性與網路等,最後只需要透過 docker-compose 的指令來操作,就可以很簡單的完成環境的管理。

Develop Django

這裡舉 Docker 網站上的 Compose QuickStart 的例子,假設我們想開發一個 Django 的網站:myweb,需要 Python3.5 的環境和 Django1.9 以及 Postgres 的 DB,如果不用 docker 的話,我們必須在開發的工作站上先安裝好這些軟體,往往會碰到套件相依和版本不同等問題,但如果是利用 docker 的話,我們只需要完成幾件事就可以解決這些困擾

  1. 將 python, django 包裝成一個 container: web,並將它 mount 在當前 project 的目錄下
  2. 然後 run 一個 postgres 的 container: db
  3. 將 django 的 db setting 設定連結到 container: db
  4. 透過 container: web 來執行 manage.py runserver

整個流程就像下圖這樣:

Untitled Diagram (14).png

那上述的這些事情該怎麼用 docker-compose 來做呢?我們要準備幾樣東西:docker-compose.yml、Dockerfile、requirements.txt、以及很短的 script 來簡化我們的指令,接下來則是就各部份來介紹:

requirements.txt

django==1.9
psycopg2

指定 pip install 要用的套件

Dockerfile

FROM python:3.5

MAINTAINER evan176.gui@gmail.com

WORKDIR /workspace

ADD requirements.txt

RUN pip install -r requirements.txt

CMD ["bash"]
  1. 指定一個工作目錄給 container: web,之後用來 mount 到目前的目錄
  2. 從目前目錄加入 requirements.txt,之後 build image 時用來 pip install

docker-compose.yml

version: '3'

services:
  web:
    build: .
    depends_on:
      - db
    volumes:
      - .:/workspace
    ports:
      - "8000:8000"
    tty: true

  db:
    image: postgres:9.6
    ports:
      - "8001:5432"

我們描述了兩個services:
1. web:
build: 用這個目錄下的 Dockerfile 來 build images
depends_on: 啟動這個 service 前,要先啟動 db
volumes: 將 /workspace mount 到當前目錄
ports: 將 docker host 的 8000 port 導到 container: web 的 8000 port
tty: 開啟虛擬終端機,之後可以用來 attach
2. db:
image: 使用 postgres 9.6 的 image
ports: 將 docker host 的 8001 port 導到 container db 裡頭的 5432 port

Networking in Compose

到這邊可能會有一點奇怪的地方,究竟 docker 的 port forwarding 是怎麼運作的,從 Networking in Compose 上的描述來看,docker-compose 會在開始時預設一個 network: myweb_default,並將 container: web, db 加入這個 network,這兩個 container 之間只要透過 service name 就可以溝通,例如從 web 去 access db 時,只需要用 postgres://db:5432。
那 host port 呢?在上述所寫的 host 其實是指 docker host 而不是目前的這台電腦,也就是說當我們從 host 去 access db 時,我們必須要透過 postgres://docker_host_ip:8001,像下圖這樣:
Untitled Diagram (12).png

myweb/settings.py

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'postgres',
        'USER': 'postgres',
        'HOST': 'db',
        'PORT': 5432,
    }
}

基於上述所說的 networking 機制,我們需要將 django settings 中的 HOST 換成 db

manage.sh

#!/bin/bash
SERVICE="web"
DOCKER_COMPOSE="docker-compose"

main() {
  if [[ $# -ne 0 ]]; then
    case "$1" in
      start)
        ${DOCKER_COMPOSE} down
        ${DOCKER_COMPOSE} up -d --build
        ;;
      stop)
        ${DOCKER_COMPOSE} down
        ;;
      bash)
        ${DOCKER_COMPOSE} exec ${SERVICE} bash
        ;;
      migrate)
        ${DOCKER_COMPOSE} exec ${SERVICE} python myweb/manage.py migrate
        ;;
      runserver)
        ${DOCKER_COMPOSE} exec ${SERVICE} python myweb/manage.py runserver 0.0.0.0:8080
        ;;
      *)
        ;;
    esac
  fi
}

main "$@"

在這裡設定了幾個指令,start、stop、bash、migrate、runserver

./manage.sh start

開啟這些服務,docker-compose up 的時候我們加上 -d 代表是在後台執行,之後只要使用 docker-compose exec 就可以將指令送到 container 中

./manage.sh bash

開啟 container: web 的 shell

./manage.sh runserver

用 container: web 來 run django server

./manage.sh stop

關閉這些 services

References

https://docs.docker.com/compose/django/
https://yeasy.gitbooks.io/docker_practice/content/compose/
https://realpython.com/blog/python/django-development-with-docker-compose-and-machine/
https://docs.docker.com/compose/networking/

廣告

輕鬆的 Python 專案

Python 是一個方便且強大的語言,給予開發者極大的彈性,但是這樣的彈性也造成很多開發上的混亂,例如不同的開發與部屬環境、工程師不同的開發習慣,所幸的是有不少的工具可以幫助我們解決這些問題,當環境與套件等問題被解決後,大家可以集中更多心力在程式上,藉由這些工具的組合可以讓團隊開發更輕鬆。

  1. 首先需要以下這4個工具:
    • git:版本控制
    • virtualenv:將專案所需要的環境獨立開來,避免污染
    • pip:套件安裝與管理
    • make:腳本工具,簡化專案控制
  2. 開啟一個 python 的專案,在裡頭包含以下 3 個檔案:
    • .gitignore:濾掉不需要被紀錄的檔案類型
    • requirements.txt:指名清楚需要的套件與版本
    • Makefile:描述在專案中會需要的操作指令
  3. 透過 git 與 make 來操作專案
    Clone project
    git clone https://repository/...
    
    為專案開虛擬環境,在 Makefile 中有預先定義好了 build27, build32, build33, build34, build35
    make build35
    source local/bin/activate
    
    安裝我們在 requirements.txt 指定的套件
    make install
    
    執行測試
    make test
    
    清除 pyc, pyo 或一些暫存的檔案
    make clean
    


以下再分別對.gitignore, requirements.txt, makefile作描述

.gitignore

用定義檔來忽略不需要被紀錄到 git 中的類型,保持專案的乾淨,這是別人整理好的各種 gitignore templates

# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]

# C extensions
*.so

# Distribution / packaging
bin/
build/
develop-eggs/
dist/
eggs/
lib/
env/
include/
local/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg

# Installer logs
pip-log.txt
pip-delete-this-directory.txt
pip-selfcheck.json

# Unit test / coverage reports
.tox/
.coverage
.cache
nosetests.xml
coverage.xml

# Translations
*.mo

# Mr Developer
.mr.developer.cfg
.project
.pydevproject

# Rope
.ropeproject

# Django stuff:
*.log
*.pot

# Sphinx documentation
docs/_build/

requirements.txt

在 python 中可以利用 pip 來安裝套件,這是個非常好用的工具,除了PyPI上的套件外,也可以用 pip 從私人的 git repository 來安裝。我們透過 requirements format 來描述專案需要的套件及版本,之後只需要透過 pip install -r requirements.txt 就可以安裝好套件。

requirements.txt:

beautifulsoup4>=4.4
-e git://github.com/kennethreitz/requests/@v2.13.0#egg=requests

如果因為網路受阻,無法去抓取 PyPI 的套件時,可以透過自建一個 private PyPI repo 來處理。

Makefile

Make 是個非常好的腳本工具,藉由設定好對應的規則可以簡化操作,避免下重複指令或輸入錯誤的指令,在這裡參考 python project makefile 定義了這些規則: build27, build32, build33, build34, build35, install, test, clean等等

Makefile:

SHELL=/bin/bash
TEST_PATH=./tests

.PHONY: auto install test clean

auto: build35 install

build27:
	virtualenv local --python=python2.7 --never-download --system-site-packages

build32:
	virtualenv local --python=python3.2 --never-download --system-site-packages

build33:
	virtualenv local --python=python3.3 --never-download --system-site-packages

build34:
	virtualenv local --python=python3.4 --never-download --system-site-packages

build35:
	virtualenv local --python=python3.5 --never-download --system-site-packages

install:
	local/bin/pip install -r requirements.txt

clean-pyc:
	find . -name '*.pyc' -exec rm --force {} +
	find . -name '*.pyo' -exec rm --force {} +
	find . -name '*~' -exec rm --force {} +

clean-build:
	rm --force --recursive build/
	rm --force --recursive dist/
	rm --force --recursive *.egg-info

test: clean-pyc
	. local/bin/activate && python -m pytest "$(TEST_PATH)"

clean: clean-pyc clean-build
	rm --recursive --force local

用 Mock 來做 Python Unit Test

Untitled (1)

單元測試可以幫助我們確保開發時有按照目標的規格,並且在程式變動後可以檢查部份變動所造成的影響。Python 這個程式本身就提供了很好的單元測試模組 unittest,這其實已經足夠滿足大部分的測試需求,而到了 python3 時則更進一步將 mock 加入到 unittest 模組中,藉由這個強大的模組我們可以更有效率的來處理單元測試。
那究竟什麼是 mock 呢?在物件導向程式中,程式設計師可以藉由偽造的物件來替換要執行的部份程式,這樣的作法可以使得測試的目標變得更明確並且與其他程式獨立開來,不會因為其他沒有通過測試的物件而影響到現在測試的目標。下面利用一個簡單的例子來介紹 mock 在單元測試中的好處!

example.py:

def func1(x):
    return x**2

def func2(x):
    return func1(x) + x*5

tests.py:

import unittest
from example import func1, func2


class ExampleTest(unittest.TestCase):
    """Test example
    """
    def test_func2(self):
        self.assertEqual(func2(5), 50)
        self.assertEqual(func2(-5), 0)

在 example 這個模組中 func2 中 會呼叫 func1 並加上 x*5 然後回傳結果,如果要對 func2 做單元測試時,要先確定 func1 這支函式是正確如預期的運作,test_func2 中的 assertEqual 才能成立。
在簡單的程式中這樣做自然不是問題,但如果 func1 變得很複雜且又有使用到其他函數時,要怎麼保證 func2 在測試時不會受到 func1 的影響呢?這時候就可以利用 mock 來將我們的目標獨立開來。

tests.py:

import unittest
from unittest import mock
from example import func1, func2

class ExampleTest(unittest.TestCase):
    """Test example for mocking
    """
    @mock.patch('example.func1')
    def test_func2(self, mock_func1):
        mock_func1.return_value = 0
        self.assertEqual(func2(5), 25)
        mock_func1.return_value = 10
        self.assertEqual(func2(-5), -15)

在 tests.py 中:
1. 利用 mock 將 example module 中 func1 替換成 mock_func1,之後在 fun2 中碰到 func1 的時候就會變成使用 mock_func1
2. 而在這裡我們定義 mock_func1.return_value = 0,也就是之後不管 func1 的輸入是什麼都只會回傳 0 這個值
3. 藉由這樣的方式就可以把 func1 獨立開來,只要確認 func2 的邏輯是否有符合我們的目標

接下來我們就對 mock 的兩個常用方法做介紹,第一個是 Mock 這個 class,提供很好的方式幫我們偽造一個想要的物件,讓我們自由的訂定輸入和輸出,第二個則是 patch,這個裝飾器幫助我們處理模組層的名稱替換,像第一個範例 func2 會呼叫到 func1,如果我們想要使用 mock_func1,就必須要將模組中的名稱替換成我們的目標,之後func2就會去使用我們替換的目標而不是呼叫原本的 func1,而 patch 則幫助我們實踐這一塊。

MagicMock

MagicMock 是 Mock 的 subclass,他預先幫我們處理了 python 中的 magic method,如果想要處理一些如 get index 等事情,使用 MagicMock 會方便很多。

初始化一個 MagicMock 的物件,利用 return_value 這個 attribute,我們就可以給這個物件一個想要的回傳值,當物件被呼叫時就會回傳我們預先設定好的結果。

>>> mock_thing = MagicMock()
>>> mock_thing.return_value = 10
>>> mock_thing()
10

或者我們希望物件能夠照序列的給出結果,這時候就可以使用 side_effect,給予這物件一個 list,之後呼叫物件時就會根據 list 內的順序來回傳

>>> mock_thing.side_effect = [1, 3, 5]
>>> mock_thing()
1
>>> mock_thing()
3
>>> mock_thing()
5

那如果今天希望物件被呼叫時能得到 exception 該怎麼做呢?只要將 side_effect 指定想要的 exception 就可以了

>>> moch_thing.side_effect = Exception('HaHa')
>>> mock_thing()
Traceback (most recent call last):
...
Exception: HaHa

又或者想要模擬一個物件的 method 時該怎麼辦做?Mock 提供了很簡潔的方式,我們只需要在 mock 物件後面直接使用我們想要的 method name or attribute name

>>> mock_thing.some_method.return_value = 5
>>> mock_thing.some_method(1, 3, 5)
5

>>> mock_thing.some_method.side_effect = ['a', 'b']
>>> mock_thing.some_method()
a
>>> mock_thing.some_method()
b

>>> mock_thing.some_attribute = 'This is attribute!'
>>> mock_thing.some_attribute
This is attribute!

下面介紹幾個在測試中很實用的方法

1. called

這個 attribute 告訴我們 mock 的物件有沒有被呼叫過

>>> mock_thing = mock.MagicMock()
>>> mock_thing.called
False
>>> mock_thing()
>>> mock_thing.called
True

2. call_count

而這則是可以知道 mock 的物件被呼叫了幾次

>>> mock_thing.some_method2()
>>> mock_thing.some_method2()
>>> mock_thing.some_method2.call_count
2

3. assert_called_with(*args, **kwargs)

或者我們想測試物件是否有被輸入指定的參數來呼叫,如果沒有則會 trigger AssertionError

>>> mock_thing.some_method3(a=1, b=4)
>>> mock_thing.some_method3.assert_called_with(a=1, b=4)
>>> mock_thing.some_method3.assert_called_with(a=1, b=5)
Traceback (most recent call last):
...
raise AssertionError(_error_message()) from cause
AssertionError: Expected call: some_method3(a=1, b=5)
Actual call: some_method3(a=1, b=4)

4. call_args

列出被呼叫的參數

>>> mock_thing.some_method3(a=1, b=4)
>>> mock_thing.some_method3.call_args
call(a=1, b=4)

5. call_args_list

跟上面很像,不同的是會將曾經被呼叫過的都顯示出來

>>> mock_thing.some_method3(a=1, b=4)
>>> mock_thing.some_method3(a=1, b=5)
>>> mock_thing.some_method3.call_args_list
[call(a=1, b=4), call(a=1, b=5)]

6. reset_mock

將 mock 物件重置,要注意的是這個 method 只是清除呼叫的紀錄,對於reutrn_value,side_effect不會有影響

>>> mock_thing = mock.MagicMock()
>>> mock_thing.return_value = 10
>>> mock_thing()
10
>>> mock_thing.called
True
>>> mock_thing.reset_mock()
>>> mock_thing.called
False
>>> mock_thing()
10

Patch

接下來介紹 patch 這個 method,竟然 Mock 已經這麼方便了,為什麼還需要 patch 這個東西呢?

a.py
-> Defines SomeClass
b.py
-> from a import SomeClass
-> some_function instantiates SomeClass

假設 B 模組裡會使用到 A 模組的 class,而這時因為 B 已經有 A 的 reference,如果我們直接 mock SomeClass 是不會有任何影響的,B 模組還是會參考到原本的 A 模組,那應該怎麼做呢?
實際上應該是將模組中參照的地方替換成我們的 mock class,我們只需要把他當成 decorator 放在測試案例前面來處理我們想要替換的名稱就可以了,像這樣子:

@mock.patch('module_name.SomeClassName.some_method_name')

example.py:

def func1(x):
    return x**2

def func2(x):
    return func1(x) + x*5

def func3(x):
    return func1(x) + func2(x) + x*3

tests.py:

import unittest
from unittest import mock
from example import func1, func2, func3


class ExampleTest(unittest.TestCase):
    """Test example for patch
    """
    @mock.patch('example.func2')
    @mock.patch('example.func1')
    def test_func2(self, mock_func1, mock_func2):
        mock_func1.return_value = 0
        mock_func2.return_value = 0
        self.assertEqual(func3(5), 15)

而 patch 還有像是給特定用法的 patch.object,patch.dict,patch.multiple 等等,這些可以在官方的說明看到更詳細的用法,最後就用一個實際的例子,來體會一下這強大的工具到底有多方便吧!

Practice

utils.py

import gzip


class Reader(object):

    def __init__(self, filename):
        self.f = self.open(filename)

    def open(self, filename):
        try:
            f = gzip.open(filename, 'rb')
        except:
            f = open(filename, 'r')
        return f

    def get(self):
        return self.f.readline()


def convert(reader):
    return reader.get().split(',')

tests.py

import unittest
try:
    # Python3
    from unittest import mock
except:
    # Python2
    import mock
from utils import Reader, convert


class ReaderTest(unittest.TestCase):

    @mock.patch('utils.open')
    @mock.patch('gzip.open')
    def test_gzip_open(self, mock_gzip, mock_open):
        mock_gzip.return_value = 'Mock Gzip'
        reader = Reader('test.csv.gz')
        mock_gzip.assert_called_with('test.csv.gz', 'rb')
        mock_open.assert_not_called()
        self.assertEqual(reader.f, 'Mock Gzip')

    @mock.patch('utils.open')
    @mock.patch('gzip.open')
    def test_builtins_open(self, mock_gzip, mock_open):
        mock_gzip.side_effect = Exception('Not this')
        mock_open.return_value = 'Open'
        reader = Reader('test.csv')
        mock_gzip.assert_called_with('test.csv', 'rb')
        mock_open.assert_called_with('test.csv', 'r')
        self.assertEqual(reader.f, 'Open')

    @mock.patch('utils.Reader.open')
    def test_get(self, mock_open):
        mock_open.return_value.readline.side_effect = [1, 2]
        reader = Reader('test.csv')
        self.assertEqual(reader.get(), 1)
        self.assertEqual(reader.get(), 2)
        with self.assertRaises(StopIteration):
            reader.get()


class ConverterTest(unittest.TestCase):

    def test_convert(self):
        mock_reader = mock.MagicMock()
        mock_reader.get.return_value = '1,2,3'
        self.assertEqual(convert(mock_reader), ['1', '2', '3'])


if __name__ == '__main__':
    unittest.main()

References

https://docs.python.org/3/library/unittest.mock.html
https://docs.python.org/3/library/unittest.mock-examples.html
https://www.toptal.com/python/an-introduction-to-mocking-in-python
http://python-mock-tutorial.readthedocs.org/en/latest/mock.html

CMake 簡單入門

cmake_logo-main

CMake 是一套 open source 的建制專案工具, 利用寫好的 CMakeLists 可以產生出不同平台上的建構檔, 在 Linux 是 Makefile, Windows 則是 Visual Studio, 或者是 Mac 上的 XCode. CMake 除了跨平台外的優點外就是使用起來很彈性, 已經有很多知名的專案都是透過 CMake 來做建置.

與一般的自動化系統不同, CMake 為了不讓編譯的過程污染到原始程式碼的目錄, 會將 build 的目錄跟 source code 的位置分開來, 通常會是在 project 中寫好 CMakeLists.txt, 然後在名為 build 的目錄底下利用 cmake command 來產生建構檔, 之後再根據各平台所使用的編譯工具來編譯目標檔案, 例如 linux 下通常會是 make.

接下來以一個很簡單的例子 MyProject 來開始, 在這目錄底會下包含 include, src 這兩個子資料夾以及 main.cpp, 在這裡我們希望可以產生兩個含式庫: libio.a, libhello.a, 並且將這兩個 library 跟目標執行檔 main.o 連結在一起, 那該怎麼用 CMake 來做到這個簡單的任務呢?

CMakeLists.txt

example1

CMakeLists.txt:

cmake_minimum_required(VERSION 2.6)
project(MyProject)

include_directories(include)
set(IO_SOURCES src/input.cpp src/print.cpp)
set(HELLO_SOURCES src/hello.cpp)
set(LIBRARY_OUTPUT_PATH ${PROJECT_BINARY_DIR}/lib)
set(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin)

add_library(io ${IO_SOURCES})
add_library(hello ${HELLO_SOURCES})
add_executable(main main.cpp)
target_link_libraries(main io hello)

這個範例的 CMakeLists.txt 乍看之下很複雜, 但其實只有三個主要的部份, 接下來就從這三個部份開始來介紹 CMake 的寫法

1. Set environment

cmake_minimum_required(VERSION 2.6): 檢查cmake版本, 當發現版本不對時還是可以執行, 只是會發出警告來提醒你要確認版本

project(MyProject): 用來指名 project 名稱, 作為之後參考的變數

2. Set include directory, source path and output path

include_directories(include): 指定 include header path

set(IO_SOURCES src/input.cpp src/print.cpp):
set(variable values)
CMake 中 set command 是用來設定變數, 將第二個所給予的內容指定到第一個變數名稱當中(CMake的指令沒有大小寫之分)
在這裡則是 IO_SOURCES 這個變數來代替 src/input.cpp src/print.cpp 這兩隻檔案
set(HELLO_SOURCES src/hello.cpp): 而這個則是用 HELLO_SOURCES 來代表 hello.cpp

set(LIBRARY_OUTPUT_PATH ${PROJECT_BINARY_DIR}/lib)
set(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin)
這兩行指令用來指名編譯好的含式庫和執行檔之後要放的位置
PROJECT_BINARY_DIR: 建置檔放的位置, 通常是執行 cmake 指令的位置, 假設在 build 目錄下執行 cmake ../MyProject 那麼 PROJECT_BINARY_DIR 就會是 build/
LIBRARY_OUTPUT_PATH: 這個變數告訴 library 要放在哪裡
EXECUTABLE_OUTPUT_PATH: 執行檔要放在什麼位置

3. Set target library or executable

add_library(io ${IO_SOURCES}): 將目標編譯成指定的 library 名稱, 在這裡預設會產生出 libio.a
如果不用變數的話就寫成這樣子 add_library(io src/input.cpp src/print.cpp)
在這裡也可以指名要產生靜態或是動態含式庫:
Static library: add_library(io STATIC src/input.cpp src/print.cpp)
Shared library: add_library(io SHARED src/input.cpp src/print.cpp)

add_executable(main main.cpp): 將 main.cpp 編譯成執行檔, 檔名就是 main

target_link_libraries(main io hello): 將執行檔與指定的這些含式庫連結起來

來看一下剛剛這個例子的實際操作, 利用 CMakeLists.txt 來產生出 Makefile 後, 接著我們就可以用 make 指令來編譯這個專案了!

output_YBMDVm


Install

當編譯好之後可能會有需要 install 的需求, 這時候就可以利用 install 這個指令, 繼續上面的CMakeLists.txt 加上下面幾行之後, 就可以在 make 後利用 make install 了

file(GLOB HEADER_PATH include/*.h)
install(FILES ${HEADER_PATH} DESTINATION include)
install(TARGETS io hello DESTINATION lib)
install(TARGETS main DESTINATION bin)

file(GLOB HEADER_PATH include/*.h):
file(GLOB variable globbing-expressions) CMake 中設定通則表示示, 符合此規則的檔案會被指定到 variable
在這裡則是將 include/input.h hello.h print.h 指定到變數 HEADER_PATH

install(FILES ${HEADER_PATH} DESTINATION include):
install(FILES files DESTINATION dir)
install(TARGETS targets DESTINATION dir)
CMake 會根據預設的 CMAKE_INSTALL_PREFIX 來將指定的目標檔案安裝或複製到目的地
在 linux 底下預設的 CMAKE_INSTALL_PREFIX = /usr/local
windows 則為 CMAKE_INSTALL_PREFIX = C:/Program Files

有些時候可能會需要安裝到一些特定的位置去, 這時候就可以透過修改 CMAKE_INSTALL_PREFIX , 將目標檔改安裝到指定的資料夾底下, 例如:
set(CMAKE_INSTALL_PREFIX ${PROJECT_BINARY_DIR}/test_install)
就會將原本安裝到 /usr/local/bin 改到 build/test_install/bin 底下

Multiple projects

CMake 也能處理一個專案中包含多個子專案的狀況, 藉由分開的 CMakeListst.txt 可以將專案分開來管理. 下面這個例子 MyProject 包含了兩個子專案 SubProject1, SubProject2 以及 CMakeListst.txt. 而這兩個子專案也各有一個屬於子專案的 CMakeListst.txt, 這時候我們只要在 MyProject/CMakeLists.txt 中利用 add_subdirectory 這個指令就可以將這兩個子專案加入到 MyProject 中.

example2

MyProject/CMakerLists.txt:

cmake_minimum_required(VERSION 2.6)
project(MyProject)
add_subdirectory(SubProject1)
add_subdirectory(SubProject2)

MyProject/SubProject1/CMakerLists.txt:

cmake_minimum_required(VERSION 2.6)
project(SubProject1)
....

到這邊為止就是基本 cmake 的介紹, 藉由這些的指令其實我們就可以完成許多專案的需求了!
如果還想了解更多 cmake command 可以參考官網, 有更詳細的說明, Enjoy ~

References

  1. https://cmake.org/cmake/help/cmake2.6docs.html
  2. https://cmake.org/cmake-tutorial/
  3. https://zh.wikibooks.org/zh-tw/CMake_%E5%85%A5%E9%96%80
  4. http://mirkokiefer.com/blog/2013/03/cmake-by-example/

在WordPress.com寫網誌.

向上 ↑