NdkBuildするC/C++コードにdefineマクロの値をProductFlavorで自動的に渡す

# 前提
cocos2dライブラリを使った結構古めのプロジェクトに、Product Flavor対応をしました。
が、一部C++のコードの中にもFlavorに依存して変えたい部分があり、対応方法を探しました。

条件としては以下があります。

- cocos2d (version3.17.2)
  - かなり古いプロジェクトで上げることが出来ない
- NDKのビルドには`ndkBuild`を使う
  - `cmake`ではなく、`ndkBuild`です

また、筆者はC/C++経験は長かったものの、既に離れて10年経過しており、そもそもmakeファイルはちゃんと書いたことがない(IDEに頼ってビルドしてきた)人間です。
その辺しっかり分かっている方には「今更何を」感があるかも知れませんが、ご容赦下さい。

# 現状

## C++コード
```Foo.cpp
#define SAMPE_ID=xxxxx


#if SAMMPLE_ID==xxxx
// ID=xxxxの時のコード
#elif SMAPLE_ID=yyyy
// ID=yyyyの時のコード
#elif SAMPLE_ID=zzzz
// ID=zzzzの時のコード
#endif
```

みたいな感じで、`SAMPLE_ID`に応じてdefineマクロでコードを変えていました。

## Androidプロジェクト

Androidプロジェクト側は、同じく`SAMPLE_ID`に応じて、リソースファイルが差し替わるなどの対応があり、Product Flavorに対応させました。

```build.gradle

    flavorDimensions "demo"

    productFlavors {
        sampleXXXX {
            dimension "demo"

            applicationId "com.example.cocos2d.sampleX"
            versionCode 51

            manifestPlaceholders = [sampleScheme:"fooxxxx",
                                    appName:"XXXX cocosSample"]
        }
        sampleZZZZ {
            dimension "demo"

            applicationId "com.example.cocos2d.sampleZ"
            versionCode 52

            manifestPlaceholders = [sampleScheme:"foozzzz",
                                    appName:"ZZZZ cocosSample"]
        }
    }

```

以前はリソースファイルをわざわざ別のフォルダからコピペしてきたりして切り替えていたのですが、これだけでも多少切替が楽になりました。

が、`xxxx`というFlavorでビルドしたとき、C++コードの`SAMPLE_ID`のdefineを`xxxx`とし、`yyyy`でビルドしたいときには、`yyyy`に書き変えるという手動コード書き換えが発生していました。

結果、Flavorを切り替えてビルドする際、`Fioo.cpp`ファイルの書き換え漏れを個人的に多発させてしまい、「アプリがちゃんと動かない！」と勝手に焦っていることが多くありました。

そこで、

**「Product Flavorに追随すべき内容なのだから、Product Flavorの設定で出来る方法は無いか？」**

と探した結果がこの記事です。

# 結論

結論から言うと、コンパイル時フラグ(cppFlags)を使うのですが、ここで前提であったcocos2d/ndkBuildを使っている、ということがネックになりハマりました。

`cmake`でしたら、`cppFlag`にそのまま追加すれば良さそうなのですが、それをやるとcocos2dのビルドが失敗するようになりました。

```build.gradle
    flavorDimensions "demo"

    productFlavors {
        sampleXXXX {
            dimension "demo"

            applicationId "com.example.cocos2d.sampleX"
            versionCode 51

            manifestPlaceholders = [sampleScheme:"fooxxxx",
                                    appName:"XXXX cocosSample"]

            externalNativeBuild.ndkBuild {
                cppFlags += '-DSAMPLE_APP_ID=xxxx'
            }

        }
        sampleZZZZ {
            dimension "demo"

            applicationId "com.example.cocos2d.sampleZ"
            versionCode 52

            manifestPlaceholders = [sampleScheme:"foozzzz",
                                    appName:"ZZZZ cocosSample"]

            externalNativeBuild.ndkBuild {
                cppFlags += '-DSAMPLE_APP_ID=zzzz'
            }
        }
    }

```

そう、我々が使っているのは`ndkBuild`であり、`Application.mk`でコンパイルフラグなどは設定しており、`build.gradle`で`cppFlags`を設定すると、どうやらその内容を上書きしてしまうようなのです。

cocos2dでデフォルトで作成される`Application.mk`はこんな感じです。

```Applicaton.mk
APP_STL := c++_static

# Uncomment this line to compile to armeabi-v7a, your application will run faster but support less devices
#APP_ABI := armeabi-v7a

APP_CPPFLAGS := -frtti -DCC_ENABLE_CHIPMUNK_INTEGRATION=1 -std=c++11 -fsigned-char -Wno-extern-c-compat
APP_LDFLAGS := -latomic

APP_ABI := armeabi-v7a
APP_SHORT_COMMANDS := true

USE_ARM_MODE := 1

ifeq ($(NDK_DEBUG),1)
  APP_CPPFLAGS += -DCOCOS2D_DEBUG=1
  APP_OPTIM := debug
else
  APP_CPPFLAGS += -DNDEBUG
  APP_OPTIM := release
endif
```

これで言うところの、`APP_CPPFLAGS`なんかをまるっと奪ってしまうようなんですね、`build.gradle`で`cppFlags`に追加すると。
そのためcocos2dのライブラリのビルドに失敗するということのようです。


解決策は、`arguments`を使う、というものでした。

```build.gradle
    flavorDimensions "demo"

    productFlavors {
        sampleXXXX {
            dimension "demo"

            applicationId "com.example.cocos2d.sampleX"
            versionCode 51

            manifestPlaceholders = [sampleScheme:"fooxxxx",
                                    appName:"XXXX cocosSample"]

            externalNativeBuild.ndkBuild {
                arguments 'SAMPLE_APP_ID=xxxx'
            }

        }
        sampleZZZZ {
            dimension "demo"

            applicationId "com.example.cocos2d.sampleZ"
            versionCode 52

            manifestPlaceholders = [sampleScheme:"foozzzz",
                                    appName:"ZZZZ cocosSample"]

            externalNativeBuild.ndkBuild {
                arguments 'SAMPLE_APP_ID=zzzz'
            }
        }
    }
```

`arguments`で渡したものは、`Application.mk`で次のように参照して`APP_CPPFLAGS`に追加します。

```Applictaion.mk
ifneq ($(SAMPLE_ID),)
  APP_CPPFLAGS += -DSAMPLE_ID=$(SAMPLE_ID)
endif
```

なんかちょっと2度手間になってはいる気はしますが、`arguments`で受け取った`$(SAMPLE_ID)`の値を、未設定でなければ、`-D`オプションで`SAMPLE_ID`のdefine値として渡しています。

で、`Foo.cpp`の方からは、`SAMPLE_ID`の定義部分は削除してしまいます。

```Foo.cpp
// #define SAMPE_ID=xxxxx // 削除

#if SAMMPLE_ID==xxxx
...
```

これで、ProductFlavorに`Foo.cpp`内のコードが一緒に連動して変わるようになりました。

恐らく最初から設計できるのであればもっとスマートな方法(`targets`を使う方法とか)あるのでしょうが、現状のコードや仕組みを大きく変えること無く、コンパイルオプションで解決できたので、まずまずと思っています。

# 【おまけ】iOSアプリも同じことをしている場合

実は当該プロジェクトは`Foo.cpp`ファイルはAndroidとiOSとで共有しているコードです。
なので、iOSでも同じようにやりたくなります。

NDKでコンパイルオプションで解決できたので、iOSというかXcodeでも同じようにコンパイルオプションを指定すれば良いのは分かりますね。

Build SettingsとUser-Defined settingを使ってこんな風に実現しました。

- コンパイルフラグ

**Apple Clang - Custom Compiler Flags** ※flagsで絞り込んだ方が探しやすいです

**Other C++ Flags**に`-DSAMPLE_ID=$(SAMPLE_ID)`を追加する


|![Apple Clang - Custom Compiler Flags](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103886/12209cc9-af04-7715-973e-86d369c6e556.png)|
|:--|

- ユーザー定義設定

**User-Defined setting** ※Build Settingsの一番下の方にあります

key=SAMPLE_ID, value=xxxxで作成する

|![User-Defined setting](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103886/df4e775f-44d0-351f-4129-80b067818fdf.png)|
|:--|

あとは`info.plist`も実はこの`SAMPLE_ID`を使いたい部分があったのでこんな感じにしました。

```info.plist
	<key>CFBundleURLTypes</key>
	<array>
		<dict>
			<key>CFBundleTypeRole</key>
			<string>Editor</string>
			<key>CFBundleURLName</key>
			<string>com.example.cocos2d.sample</string>
			<key>CFBundleURLSchemes</key>
			<array>
				<string>foo${SAMPLE_ID}</string>
			</array>
		</dict>
	</array>

```


これで、`User-Defined`の`SAMPLE_ID`を書き変えるだけで`Foo.cpp`の実装や`info.plist`の値を切り替えることが出来ました。

が、これだといちいち`SAMPLE_ID`の値を書き変えなければなりません。個人的に、この設定部分が使いづらくて良く意図しない場所を編集してしまうため、出来ればXcode上で触りたくありません。

Android StudioでBuild Variantsを切り替えるのと同じような、UIで切り替えられると便利です。

※本当はコマンドラインビルドのことも考えなければならないのですが、いったん~~担当者が違うので~~無視しています。

そこで気付いたのが"Target"です。

**Targetごとに、User-Defindeの値だけを変えればいいんじゃない？**

ということで、`SAMPLE_ID`の数だけ、Targetを作りました。

- 10000の場合

|![target10000.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103886/f59c5fc8-7b90-9425-a836-6340b32c9b83.png)|
|:--|



- 20000の場合

|![target20000.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103886/fa9d26ef-badd-c1a6-871d-f661c177209d.png)|
|:--|


# まとめ

- AndroidのndkBuildでnative(C/C++)コードをProduct Flavorに追随させたい場合は、`arguments`を使う。
- cmakeならcppFlagsでやれるんじゃないかな？
  - cocos2dがデフォルトで持ってるmakeファイルをよく見てね
- XcodeならTargetとBuild SettingsのOptional Custom Compiler Flags/User-Defined Settingを使うと同じような感じにできる

# 参考サイト

- cppFlagとかの情報
https://developer.android.com/ndk/guides/android_mk?hl=ja
https://developer.android.com/ndk/guides/cpp-support

- cppFlagでProductFlavor追随できるよと言う情報
https://stackoverflow.com/questions/47350245/ndk-support-with-different-product-flavour
https://stackoverflow.com/questions/9052792/how-to-pass-macro-definition-from-make-command-line-arguments-d-to-c-source


- ndkBuildではargumentsを使うヒントになりました
https://qiita.com/kenmasu/items/598496840eef775db142
https://developer.android.com/studio/projects/gradle-external-native-builds?hl=ja
https://stackoverflow.com/questions/44169071/gradle-seems-to-overrwrite-ndk-build-arguments-while-adding-product-flavors


- XcodeのTargetの使い方の参考になりました。
https://qiita.com/alt_yamamoto/items/c3b78b24d0ff5ddc93f9