C++の環境をいい感じに構築

# 初めに
本記事では、自分が考えたC++の勉強し易い環境を作成する事を目的として記載しています。
その為、他の人から見たらあれこれあると思いますが・・・お優しい目で見ていいただければなと思います。

# 更新履歴
- 2020/11/22:libtorch＋google testにを合わせたビルド環境に修正。

# 目的
C++開発での単体テスト勉強やcmakeの勉強を行える環境を構築する事を目的とする。
また、makeコマンドでいい感じにビルドやプロジェクトの作成をできるようにする。

# ディレクトリ構成
以下のようディレクトリ構成にしました。

```bash
cpp_train/
├─cpp/                # C++の開発環境ディレクトリ
│  ├─project/         # プロジェクトごとにディレクトリを切る
│  ├─ ...
│  ├─.editorconfig    # c,h,cpp,hppに対しての設定
│  └─makefile         # build,clean,projectのコマンドを記載 
├─doc/                # 開発時のドキュメントやメモのディレクトリ
├─Docker/             # Dockerイメージのディレクトリ(環境ごとにディレクトリを切る)
│  ├─ubuntu/        
│  │  └─Dockerfile    # 必ずDockerfileにする。(変なファイル名にして指定する事はなるべくやりたく無いから)
│  └─windows/         # 今回は省略（サイレントインストール難しかったから）
│     └─Dockerfile
├─scripts/            # 環境開発に対してのスクリプトのディレクトリ(今回は特に必要ないので省略)
│  ├─switch_linux.bat # docker cliでLinuxコンテナに切り替えるスクリプト
│  └─switch_win.bat   # docker cliでWindowsコンテナに切り替えるスクリプト
├─template/           # make projectコマンドのテンプレート
│  └─project/
│       ├─include/
│       ├─lib/
│       ├─src/
│       ├─test/
│       └─CMakeLits.txt
├─.gitignore          # ignoreファイル
├─docker-compose.yml  # マウントを簡略化する為
└─README.md
```

# Dockerイメージ

```dockerfile

FROM ubuntu:20.04

ENV BOOST_VERSION 1.70.0
ENV BOOST_FILE_VERSION 1_70_0
ENV GOOGLE_TEST_VERSION 1.10.0
ENV PYTORCH_VERSION 1.7.0
ENV DEBIAN_FRONTEND=noninteractive


# 開発に必要なパッケージをインストール(ダウンロード元のリポジトリを日本サーバーに変更)
RUN sed -i 's@archive.ubuntu.com@ftp.jaist.ac.jp/pub/Linux@g' /etc/apt/sources.list \
    && apt-get -o Acquire::Check-Valid-Until=false -o Acquire::Check-Date=false update \
    && apt-get install --no-install-recommends -y \
        tzdata \
        build-essential \
        zlib1g-dev \
        libffi-dev \
        libpthread-stubs0-dev \
        cmake \
        gdb \
        curl \
        openssh-server \
        ca-certificates \
        zip \
        unzip \
    && apt-get clean \
    && rm -r /var/lib/apt/lists/* \
    && mkdir /tmp/src
# BoostとGoogle testとlibtorchをダウンロードし解凍する。
WORKDIR /tmp/src
RUN curl -OL https://dl.bintray.com/boostorg/release/${BOOST_VERSION}/source/boost_${BOOST_FILE_VERSION}.tar.gz\
    && tar -zxvf boost_${BOOST_FILE_VERSION}.tar.gz \
    && curl -OL https://github.com/google/googletest/archive/release-${GOOGLE_TEST_VERSION}.tar.gz \
    && tar -zxvf release-${GOOGLE_TEST_VERSION}.tar.gz \
    && mkdir ./googletest-release-${GOOGLE_TEST_VERSION}/build \
    && curl -OL https://download.pytorch.org/libtorch/cpu/libtorch-shared-with-deps-${PYTORCH_VERSION}%2Bcpu.zip \
    && unzip libtorch-shared-with-deps-${PYTORCH_VERSION}%2Bcpu.zip
# boostのインストール
WORKDIR /tmp/src/boost_${BOOST_FILE_VERSION}
RUN ./bootstrap.sh --prefix=/usr/local/ --with-libraries=program_options \
    && ./b2 install
# Libtorchのインストール(CPU only)
WORKDIR /tmp/src/libtorch
RUN cp -v -r include/* /usr/local/include \
    && cp -v -r lib/* /usr/local/lib \
    && cp -v -r share/* /usr/local/share/
# Google testのインストール
WORKDIR /tmp/src/googletest-release-${GOOGLE_TEST_VERSION}/build
# libtorchとgoogletestの競合対応(gtestするときはlibtorchのリンクが必須になる)
RUN sed -i -e "11i add_compile_definitions(_GLIBCXX_USE_CXX11_ABI=0)" ../CMakeLists.txt  \
    && cmake ..\
    && make \
    && make install

# インストール作業で出た一時的なファイルを削除
WORKDIR /tmp
RUN rm -rf ./src/
# 最後に作業場を参照
WORKDIR /tmp/project

```
### ハマった点
- ubuntuイメージでとあるpackageをapt-getするとtime zoneを聞かれるので```ENV DEBIAN_FRONTEND=noninteractive```を入れて対応しています。
- Google Testがlibtorchを入れるとABI周りで変なエラーが出力される為
Google Test側の``CMakeLists.txt``にコンパイルオプションを追加しています。


# docker-compose.yml
マウントを簡略化する為だけに使用しています。
いちいちdockerコマンドをスクリプト化するよりいいかなと思います。

開発プロジェクト配下とプロジェクトテンプレート配下をマウントしています。

書いてて思ったのですが、
build/dockerfileの所がDockerfile指定しちゃっているので、ダサいですね。

```yml
version: '3'
services:
  dev_linux:
    build:
      context: .
      dockerfile: ./Docker/ubuntu/Dockerfile
    tty: True
    volumes:
      - ./cpp:/tmp/project
      - ./template:/tmp/src/template
```

# makefile

ここでは、C++開発を効率的に行う為のコマンドを記載します。
その為、cppディレクトリ直下に置いています。

開発時は常にcppディレクトリ上でコマンドを使用するイメージです。

 ``build``はプロジェクト配下にあるCMakeLists.txtを元にビルドまで行います。
出力は``<c++プロジェクト>/build``に格納されるようにしています。

``clean``はbuildで作成されたbuildディレクトリを削除する為のコマンドです。

``project``はC++開発のプロジェクトテンプレートを、指定した名前のプロジェクト名に置き換えて作成するコマンドです。プロジェクトテンプレートは``docker-compose.yml``でマウントした先にある為そこを指定しコピーを行っています。その後CMakeLists.txtにproject名を記入するコマンドを行っています。

変数名を統一したほうがよかったと思っていますが、
変数必須の書き方忘れてしまった。。

```makefile

.ONESHELL:
.PHONY: clean build

clean:
	-rm -r ./$(dir)/build

build: 
	cmake -S ./$(dir) -B ./$(dir)/build
	cmake --build ./$(dir)/build

.PHONY: project
project:
	cp -r /tmp/src/template/project $(name)
	sed -i -e "2i project($(name))" $(name)/CMakeLists.txt
```


# template/
C++のプロジェクトテンプレートを格納しているディレクトリです。
最初はシェルコマンドでテンプレートが作成されるようにしようと思ったのですが、
難しいし、編集するのに癖があると思ったので実際にテンプレートプロジェクトを作成コピーさせるようにしました。

CMakeLits.txtファイル1つでビルドとテストが行えるようにしました。

```
project/
│       ├─include/ # ビルド・テスト両方で必要なソースを展開
│       ├─lib/  # 使用予定ないけどあったほうがそれっぽいので作っています。
│       ├─src/  # ビルドのみに必要なソースを展開
│       ├─test/ # テストのみに必要なソースを展開
│       └─CMakeLits.txt
```

## CMakeLists.txt
CMakeを全然勉強していなかったのでここでかなり苦労しましたｗ
CMakeは人によって書き方がかなり違ってたりしたので、参考資料を集め公式のドキュメントと照らし合わせながら勉強しました。

### 工夫点
ここではあえてプロジェクト名を入れていません。
``make project``で挿入されます。

testのif文は別に無くてもいいけど
あったほうが個人的に読みやすかったので、つけています。
testのパスもできるしね！

### 反省点
``target_compile_features``で指定しているオプションを変数に
入れてtestとbuildで統一したかったですが、setを使うのは推奨されていないっぽいので
どうしたらいいのかわからず直書きしてしまいました。

```
cmake_minimum_required(VERSION 3.16)

# 必須
find_package(Torch REQUIRED)

# Set macros
add_definitions(
)

# project
file(
    GLOB_RECURSE
    SOURCES
    ${PROJECT_SOURCE_DIR}/src/*.cpp
    ${PROJECT_SOURCE_DIR}/include/*.cpp
)

# test
file(
    GLOB_RECURSE
    TEST_SOURCES
    ${PROJECT_SOURCE_DIR}/test/*.cpp
    ${PROJECT_SOURCE_DIR}/include/*.cpp
)

# libs
include_directories(
    ${PROJECT_BINARY_DIR}
    ${PROJECT_SOURCE_DIR}/include
)
link_directories(
    ${PROJECT_SOURCE_DIR}/libs
)

# tests
if(NOT out-test)

    find_package(GTest REQUIRED)

    add_executable(
        ${PROJECT_NAME}-Test 
        ${TEST_SOURCES}
    )

    enable_testing()

    target_include_directories(
        ${PROJECT_NAME}-Test PRIVATE
        ${INCLUDE_DIRECTORIES}
        ${GTEST_INCLUDE_DIRS}
    )
    target_link_libraries(
        ${PROJECT_NAME}-Test PRIVATE
        ${LINK_DIRECTORIES}
        ${TORCH_LIBRARIES} #GTestを行う為必須
        GTest::GTest
        GTest::Main
    )
    target_compile_features(
        ${PROJECT_NAME}-Test PRIVATE
        cxx_std_14
    )

    gtest_add_tests(TARGET ${PROJECT_NAME}-Test)

endif()


add_executable(
    ${PROJECT_NAME} 
    ${SOURCES}
)
target_link_libraries(
    ${PROJECT_NAME} 
    ${LINK_DIRECTORIES}
    ${TORCH_LIBRARIES}
)
target_compile_features(
    ${PROJECT_NAME} PRIVATE
    cxx_std_14
)
```

# 開発イメージ

1. ``docker-compose up -d``でイメージをビルドしコンテナを起動
2. ``dcoekr-compose exec dev_linux /bin/bash`` で開発環境にログイン
3. ``make project name=<プロジェクト名>``でプロジェクトを作成
4. ``make build dir=<3のプロジェクト名>``でビルドを行う。
5. ``./<プロジェクト名>/build/<プロジェクト名>``で実行ファイルを実行
6. ``./<プロジェクト名>/build/<プロジェクト名>-Test``でテスト結果を出力

ド素人ながら頑張ったなと思います。

# 所感
今後はWindows周りのコンテナやCLIを勉強しWindows・Linux環境の両方に対応した環境を構築したいと思っています。がサイレントインストールが難しいです。