UE4 에서 Android 를 위해서 Crashlystics 를 붙여 보려고 하는데, 이를 위해서는 "AndroidMenifest.xml" 과 "build.gradle" 파일이 수정되어야 합니다. 하지만 이 파일들은 packaging 시에 생성되므로, 어떻게 손을 댈 방법이 없습니다.
어떤 사람들은 apk 가 나온 뒤에 Android Studio 같은 도구를 사용해서 바꾸라고 하는데, 저는 이렇게 추가적인 프로세스가 들어 가는 것은 불합리하다고 생각을 했습니다. 그래서 검색을 좀 해 보니, UE4 4.17 에 gradle 지원을 하면서, UnrealPluginLanguage 에 대한 언급을 한게 있더군요.
여기에서 알 수 있는 건 단지 Gradle 로 빌드 체인을 변경하면서, UnrealPluginLanguage 라는 것을 사용해서 종속성을 추가해야 한다는 것입니다. 어떻게 사용하는 지는 모르겠구요.
찾아 보니, 이것에 대한 공식 문서는 존재하지 않고, 그냥 "UnrealPluginLanguage.cs" 파일에 주석으로 설명해 놨더군요.
UnrealPluginLanguage ( UPL ) 은 간단한 XML 기반 언어로 XML 을 조작하고 문자열을 반환하는데 사용됩니다. 이것은 <init> 섹션을 포함하고 있는데, 이 섹션은 다른 섹션들이 평가되기 전에 아키텍쳐 당 한 번만 평가됩니다. 상태가 보존되고, 다음 섹션들이 평가될 때 이전됩니다. 그러므로 섹션이 실행되는 순서는 중요합니다.
UPL 은 XML 을 수정하고 질의하는 데 있어 일반적인 시스템이지만, 그것은 특별히 플러그인들이 그것이 구성하고 있는 package 의 전역 설정에 영향을 줄 수 있도록 하는데 사용합니다. 예를 들어, 이것은 플러그인이 Android 의 APK AndroidManifest.xml 파일을 수정하거나 IOS IPA 의 plist 파일을 수정할 수 있도록 해 줍니다. UBT 는 플러그인의 UPL xml 파일을 질의해서 Android 의 .java 파일들과 같은 패키지에 있어 일반적으로 포함되어야 하는 파일들에 문자들을 포함시킬 수 있도록 합니다.
만약 여러분이 플러그인 칸텍스트( context )에서 실행될 명령들을 보기 원한다면 추적( tracing )을 활성화하십시오 :
<trace enabled="true"/>
이 명령 이후에는 <trace enable="false"/> 가 실행될 때까지 여러분의 칸텍스트에서 실행되는 모든 노드들이 로그에 기록될 겁니다. 다음 명령을 사용해서 여러분의 칸텍스트에 있는 모든 변수들을 출력할 수 있습니다 :
<dumpvars/>
Bool, Int, String 변수 유형들이 지원됩니다. 모든 애트리뷰트( attribute )들은 대부분 변수를 참조할 것이며, 다음 구문을 사용해 평가되기 전에 문자열로 치환될 겁니다.
$B(name) = boolean 변수 "name" 의 값.
$I(name) = integer 변수 "name" 의 값.
$S(name) = string 변수 "name" 의 값.
$E(name_ = element 변수 "name" 의 값.
다음 변수들은 자동으로 초기화됩니다:
$S(Output) = 이 섹션을 평가해서 반환된 출력( Input 에 대해 초기화됨 ).
$S(Architecture) = 대상 아키텍쳐( armeabi-armv7a, arm64-v8a, x86, x86_64 ).
$S(PluginDir) = XML 파일이 로드된 디렉토리.
$S(EngineDir) = 엔진 디렉토리.
$S(BuildDir) = 프로젝트의 플랫폼 관련 빌드 디렉토리( Intermediate 폴너 내부 ).
$S(Configuration) = 구성 유형( Debug, DebugGame, Development, Test, Shipping ).
$B(Distribution) = Distribution 빌드이면 true.
주의 : 위에 있는 변수들에서 예외가 발생하면, 플러그인의 칸텍스트에 있는 모든 것들은 namespace 충돌을 막습니다; 새로운 값을 위의 변수들에 설정하려고 시도하는데, Output 에서 예외가 발생하면, 현재 칸텍스트에만 영향을 줄 것입니다.
다음 노드들은 변수를 조작하는 데 사용됩니다:
<setBool result="" value=""/>
<setInt result="" value=""/>
<setString result="" value=""/>
<setElement result="" value=""/>
<setElement result="" value="" text=""/>
<setElement result="" xml=""/>
value 와 함께 사용되는 <setElement> 는 태그( tag )가 value 로 설정된 비어있는 XML 엘리먼트를 생성합니다.
value 및 text 와 함께 사용되는 <setElement> 는 태그가 value 로 설정된 파싱되지 않은 text 에 대한 XML 엘리먼트를 생성합니다.
xml 과 함께 사용되는 <setElement> 는 제공된 XML 을 파싱할 겁니다. escape character 를 넣으면 안 됩니다.
변수들은 ini 파일에 있는 속성( property )들로부터 설정될 수도 있습니다:
<setBoolFromProperty result="" ini="" section="" property="" default=""/>
<setBoolFromPropertyContains result="" ini="" section="" property="" default="" contains=""/>
<setIntFromProperty result="" ini="" section="" property="" default=""/>
<setStringFromProperty result="" ini="" section="" property="" default=""/>
문자열들은 <setStringFromEnvVar> 노드를 사용해 환경 변수로부터 설정될 수도 있습니다.
환경 변수들은 반드시 '%' 문자의 쌍으로 묶여 있어야 하며 'value' 애트리뷰트로서 지정되어야만 합니다.
<setStringFromEnvVar result="" value=""/>
특정 환경 변수가 정의되어 있는지를 검사하는 것도 가능합니다( 역시, '%' 문자로 묶여야 합니다 ):
<setBoolEnvVarDefined result="" value=""/>
환경 변수 노드를 사용하는 일반적인 예제는 다음과 같습니다:
<setBoolEnvVarDefined result="bHasNDK" value="%NDKROOT%"/>
<setStringFromEnvVar result="NDKPath" value="%NDKROOT%"/>
Boolean 변수들은 연산자가 적용된 결과로 설정될 수도 있습니다:
<setBoolNot result="" source=""/>
<setBoolAnd result="" arg1="" arg2=""/>
<setBoolOr result="" arg1="" arg2=""/>
<setBoolIsEqual result="" arg1="" arg2=""/>
<setBoolIsLess result="" arg1="" arg2=""/>
<setBoolIsLessEqual result="" arg1="" arg2=""/>
<setBoolIsGreater result="" arg1="" arg2=""/>
<setBoolIsGreaterEqual result="" arg1="" arg2=""/>
<setBoolFromPropertyContains result="" ini="" section="" property="" contains=""/>
Integer 변수들은 수학 연산자들을 사용할 수 있습니다:
<setIntAdd result="" arg1="" arg2=""/>
<setIntSubtract result="" arg1="" arg2=""/>
<setIntMultiply result="" arg1="" arg2=""/>
<setIntDivide result="" arg1="" arg2=""/>
String 변수들은 다음과 같이 조작될 수 있습니다:
<setStringAdd result="" arg1="" arg2=""/>
<setStringSubstring result="" source="" start="" length=""/>
<setStringReplace result="" source="" find="" with=""/>
String 의 길이를 획득할 수도 있습니다:
<setIntLength result="" source=""/>
소스로부터 문자열을 검색해 인덱스를 획득할 수도 있습니다 :
<setIntFindString result="" source="" find=""/>
다음 예제는 문자열을 비교하는데, <setIntFindString> 을 사용하는 대신에 result 를 검사합니다:
<setBoolStartsWith result="" source="" find=""/>
<setBoolEndsWith result="" source="" find=""/>
<setBoolContains result="" source="" find=""/>
다음 노드를 사용하면 로그에 메시지를 기록할 수 있습니다:
<log text=""/>
다음 형식을 사용하면 조건 실행이 가능합니다:
<if condition="">
<true>
<!-- condition 이 true 이면 실행 -->
</true>
<false>
<!-- condition 이 false 이면 실행 -->
</false>
</if>
<true> 와 <false> 블락은 선택적입니다. 조건은 반드시 boolean 변수여야 합니다.
Boolean 연산자 노드를 결합해서 더욱 복잡한 조건을 위한 최종 상태를 만들 수 있습니다 :
<setBoolNot result="notDistribution" source="$B(Distribution)/>
<setBoolIsEqual result="isX86" arg1="$S(Architecture)" arg2="x86"/>
<setBoolIsEqual result="isX86_64" arg2="$S(Architecture)" arg2="x86_64/">
<setBoolOr result="isIntel" arg1="$B(isX86)" arg2="$B(isX86_64)"/>
<setBoolAnd result="intelAndNotDistribution" arg1="$B(isIntel)" arg2="$B(notDistribution)"/>
<if condition="intelAndNotDistribution">
<true>
<!-- do something for Intel if not a distribution build -->
</true>
</if>
"isIntel" 은 다음과 같이 수행될 수도 있음에 주목하세요:
<setStringSubstring result="subarch" source="$S(Architecture)" start="0" length="3"/>
<setBoolEquals result="isIntel" arg1="$S(subarch)" arg2="x86"/>
조건 실행을 위해서 아래 두 노드를 이용할 수 있습니다:
<isArch arch="armeabi-armv7">
<!-- do stuff -->
</isArch>
이것은 다음과 동일합니다:
<setBoolEquals result="temp" arg1="$S(Architecture)" arg2="armeabi-armv7">
<if condition="temp">
<true>
<!-- do stuff -->
</true>
</if>
그리고
<isDistribution>
<!-- do stuff -->
</isDistribution>
는 다음과 동일합니다 :
<if condition="Distribution">
<!-- do stuff -->
</if>
다음 노드들을 사용해 루프를 돌릴 수도 있습니다:
<while condition="">
<!-- do stuff -->
</while>
<break/>
<continue/>
<while> 바디는 condition 이 false 이거나 <break/> 를 만날 때까지 실행될 겁니다. <continue/> 는 condition 이 여전히 true 인 동안에는 루프 실행을 재개할 것이며 그렇지 않으면 루프를 나갈 겁니다.
주의 : <while> 바디 바깥쪽의 <break/> 는 <return/> 과 동일하게 동작합니다.
여기에 1 에서 5 까지 로그를 출력하는데, 3 은 건너 뛰는 예제가 있습니다. while 조건의 갱신은 continue 전에 수행되어야 한다는 것에 주목하세요. 그렇지 않으면 루프를 나가버립니다.
<setInt result="index" value="0"/>
<setBool result="loopRun" value="true"/>
<while condition="loopRun">
<setIntAdd result="index" arg1="$I(index)" arg2="1"/>
<setBoolIsLess result="loopRun" arg1="$I(index)" arg2="5"/>
<setBoolIsEqual result="indexIs3" arg1="$I(index)" arg2="3"/>
<if condition="indexIs3">
<true>
<continue/>
</true>
</if>
<log text="$I(index)"/>
</while>
result 와 name 을 생성할 때 변수 치환을 하는 것도 가능합니다. 이것은 루프 안에서 배열을 생성하는 것을 가능하게 해 줍니다:
<setString result="array_$I(index)" value="element $I(index) in array"/>
다음 노드를 사용해서 그것을 획득할 수 있습니다( 값이 변수 이름으로 취급됩니다 ):
<setStringFrom result="out" value="array_$I(index)"/>
Boolean 및 integer 유형에 대해 여러분은 <setBoolFrom/> 과 <setIntFrom/> 을 사용할 수 있습니다.
섹션에 텍스트를 삽입하기 위한 노드는 다음과 같습니다:
<insert> body </insert>
<insertNewline/>
<insertValue value=""/>
<loadLibrary name="" failmsg=""/>
첫 번째 것은 텍스트나 노드를 반환되는 섹션 문자열에 삽입합니다. escape character 를 사용하려면 다음과 같이 해야 한다는 것에 주의하세요 :
< = <
> = >
& = &
<insertValue value=""/> 는 삽입 전에 value 의 변수를 평가합니다. 만약 value 가 쌍따옴표( " ) 를 포함하고 있다면, 그것을 " 로 작성해야 합니다:
<loadLibrary name="" failmsg=""/> 는 system.LoadLibrary try/catch 블락을 삽입하기 위한 단축명령이며, 로딩에 실패했을 때 사용할 선택적인 로그 메시지를 포함합니다.
Output 을 검색해서 치환할 수도 있습니다:
<replace find="" with=""/>
실제 $S(Output) 을 직접 조작할 수도 있다는 것을 기억하세요. 위의 것이 훨씬 효율적입니다:
<setStringAdd result="Input" arg1="$S(Output)" arg2="sample\n"/>
<setStringReplace result="Input" source="$S(Output)" find=".LAUNCH" with=".INFO"/>
XML 조작은 다음 노드를 사용합니다:
<addElement tag="" name=""/>
<addElements tag=""> body </addElements>
<removeElement tag=""/>
<setStringFromTag result="" tag=""/>
<setStringFromAttribute result="" tag="" name=""/>
<setStringFromTagText result="" tag=""/>
<addAttribute tag="" name="" value=""/>
<removeAttribute tag="" name=""/>
<loopElements tag=""> instructions </loopElements>
현재 엘리먼트는 tag="$" 에 의해 참조됩니다. 엘리먼트 변수들은 $varname 을 사용해 참조되는데, $E(varname) 을 사용하면 XML 의 string 으로 확장되 버리기 때문입니다.
addElement, addElements, removeElement 는 기본적으로 일치하는 태그들에 대해 모두 적용됩니다. 선택적인 once="true" 애트리뷰트가 지정되면 처음으로 일치하는 태그에 대해서만 적용되도록 할 수 있습니다.
<uses-permission>, <uses-feature>, <uses-library> 는 다음과 같이 갱신되었습니다:
addPermission android:name="" .. />
<addFeature android:name="" .. />
<addLibrary android:name="" .. />
위의 명령들에서 모든 애트리뷰트들은 매니페이스에 추가되는 엘리먼트들에 복사되므로, 여러분은 다음과 같이 할 수 있습니다:
<addFeature android:name="android.hardware.usb.host" android:required="true"/>
마지막으로, 다음 노드들은 jar 이나 파일들을 스테이징하기 위해서 파일을을 복사하는 것을 허용합니다:
<copyFile src="" dst="" force=""/>
<copyDir src="" dst="" force=""/>
만약 force 가 false 라면, 길이나 수정시간이 다를 때만 파일들이 대체될 것입니다. 기본값은 true 입니다.
다음은 src 와 dst 경로를 위해 base 로 사용되어야 합니다:
$S(PluginDir) = XML 파일이 로드된 디렉토리.
$S(EngineDir) = 엔진 디렉토리.
$S(BuildDir) = 프로젝트의 플랫폼 관련 빌드 디렉토리.
APK 디렉토리 외부에 파일을 쓰는 것도 가능하지만, 추천하지는 않습니다.
만약 파일을 제거해야 한다면( 예를 들어 development-only 파일을 distribution 빌드에서 제거하려면 ), 다음 노드를 사용할 수 있습니다:
<deleteFiles filespec=""/>
이건 BuildDir 에서 파일을 제거하는 용도로만 사용될 수 있습니다. Occlus Signature( soig ) 파일들을 assets 디렉토리에서 제거하는 예제가 있습니다:
<deleteFiles filespec="assets/oculussig_*"/>
다음 섹션은 패키징과 디플로잉( deploy ) 스테이지에서 평가됩니다 :
** For all platforms **
<!-- init section is always evaluated once per architecture -->
<init> </init>
** Android Specific sections **
<!-- optional updates applied to AndroidManifest.xml -->
<androidManifestUpdates> </androidManifestUpdates>
<!-- optional additions to proguard -->
<proguardAdditions> </proguardAdditions>
<!-- optional AAR imports additions -->
<AARImports> </AARImports>
<!-- optional base build.gradle additions -->
<baseBuildGradleAdditions> </baseBuildGradleAdditions>
<!-- optional base build.gradle buildscript additions -->
<buildscriptGradleAdditions> </buildscriptGradleAdditions>
<!-- optional app build.gradle additions -->
<buildGradleAdditions> </buildGradleAdditions>
<!-- optional additions to generated build.xml before ${sdk.dir}/tools/ant/build.xml import -->
<buildXmlPropertyAdditions> </buildXmlPropertyAdditions>
<!-- optional files or directories to copy or delete from Intermediate/Android/APK before ndk-build -->
<prebuildCopies> </prebuildCopies>
<!-- optional files or directories to copy or delete from Intermediate/Android/APK after ndk-build -->
<resourceCopies> </resourceCopies>
<!-- optional files or directories to copy or delete from Intermediate/Android/APK before Gradle -->
<gradleCopies> </gradleCopies>
<!-- optional properties to add to gradle.properties -->
<gradleProperties> </gradleProperties>
<!-- optional parameters to add to Gradle commandline (prefix with a space or will run into previous parameter(s)) -->
<gradleParameters> </gradleParameters>
<!-- optional minimum SDK API level required -->
<minimumSDKAPI> </minimumSDKAPI>
<!-- optional additions to the GameActivity imports in GameActivity.java -->
<gameActivityImportAdditions> </gameActivityImportAdditions>
<!-- optional additions to the GameActivity after imports in GameActivity.java -->
<gameActivityPostImportAdditions> </gameActivityPostImportAdditions>
<!-- optional additions to the GameActivity class in GameActivity.java -->
<gameActivityClassAdditions> </gameActivityOnClassAdditions>
<!-- optional additions to GameActivity onCreate metadata reading in GameActivity.java -->
<gameActivityReadMetadata> </gameActivityReadMetadata>
<!-- optional additions to GameActivity onCreate in GameActivity.java -->
<gameActivityOnCreateAdditions> </gameActivityOnCreateAdditions>
<!-- optional additions to GameActivity onDestroy in GameActivity.java -->
<gameActivityOnDestroyAdditions> </gameActivityOnDestroyAdditions>
<!-- optional additions to GameActivity onStart in GameActivity.java -->
<gameActivityOnStartAdditions> </gameActivityOnStartAdditions>
<!-- optional additions to GameActivity onStop in GameActivity.java -->
<gameActivityOnStopAdditions> </gameActivityOnStopAdditions>
<!-- optional additions to GameActivity onPause in GameActivity.java -->
<gameActivityOnPauseAdditions> </gameActivityOnPauseAdditions>
<!-- optional additions to GameActivity onResume in GameActivity.java -->
<gameActivityOnResumeAdditions> </gameActivityOnResumeAdditions>
<!-- optional additions to GameActivity onNewIntent in GameActivity.java -->
<gameActivityOnNewIntentAdditions> </gameActivityOnNewIntentAdditions>
<!-- optional additions to GameActivity onActivityResult in GameActivity.java -->
<gameActivityOnActivityResultAdditions> </gameActivityOnActivityResultAdditions>
<!-- optional libraries to load in GameActivity.java before libUE4.so -->
<soLoadLibrary> </soLoadLibrary>
아래에는 지원되는 전체 노드 목록이 있습니다.
<isArch arch="">
<isDistribution>
<if> => <true> / <false>
<while condition="">
<return/>
<break/>
<continue/>
<log text=""/>
<insert> </insert>
<insertValue value=""/>
<replace find="" with""/>
<copyFile src="" dst=""/>
<copyDir src="" dst=""/>
<loadLibrary name="" failmsg=""/>
<setBool result="" value=""/>
<setBoolEnvVarDefined result="" value=""/>
<setBoolFrom result="" value=""/>
<setBoolFromProperty result="" ini="" section="" property="" default=""/>
<setBoolFromPropertyContains result="" ini="" section="" property="" contains=""/>
<setBoolNot result="" source=""/>
<setBoolAnd result="" arg1="" arg2=""/>
<setBoolOr result="" arg1="" arg2=""/>
<setBoolIsEqual result="" arg1="" arg2=""/>
<setBoolIsLess result="" arg1="" arg2=""/>
<setBoolIsLessEqual result="" arg1="" arg2=""/>
<setBoolIsGreater result="" arg1="" arg2=""/>
<setBoolIsGreaterEqual result="" arg1="" arg2=""/>
<setInt result="" value=""/>
<setIntFrom result="" value=""/>
<setIntFromProperty result="" ini="" section="" property="" default=""/>
<setIntAdd result="" arg1="" arg2=""/>
<setIntSubtract result="" arg1="" arg2=""/>
<setIntMultiply result="" arg1="" arg2=""/>
<setIntDivide result="" arg1="" arg2=""/>
<setIntLength result="" source=""/>
<setIntFindString result="" source="" find=""/>
<setString result="" value=""/>
<setStringFrom result="" value=""/>
<setStringFromEnvVar result="" value=""/>
<setStringFromProperty result="" ini="" section="" property="" default=""/>
<setStringAdd result="" arg1="" arg2=""/>
<setStringSubstring result="" source="" index="" length=""/>
<setStringReplace result="" source="" find="" with=""/>
뭔가 상당히 복잡해 보이는데, 과정에 익숙해지면 플랫폼별 빌드 구성을 하는데 있어서 매우 편리한 도구가 될 수 있을거라는 생각이 드네요.
일단 이 문서는 UPL 이 뭔지 정리하는 데 목적을 두고 있기 때문에 여기에서 마무리할 계획입니다. 다음에는 실제로 UPL 을 사용해 Crashlystics 와 연동하는 작업을 해 볼 계획입니다.