개요

 

Gradle 을 사용하다가 보면 처음에 가장 이해하기 어려운 것이 dependency 입니다. 

 

특히 UE4 를 사용해서 cross-platform 작업을 하시는 분들은 AutomationTool 을 사용하고 있기 때문에 내부가 어떻게 돌아가고 있는지 잘 모르는 경향이 있습니다. 사실 저도 XBox 와 PS4 쪽 작업을 할 때는 그런 고민을 별로 안 했습니다.

 

그 플랫폼들은 자체 포맷으로 packaging 하는 것을 제외하고는 Neighborhood 와 같은 도구를 통해 쉽게 deploy 할 수 있도록 해 줍니다. 뭔가 추가적인 모듈을 사용하고 싶다면 이미 배포된 SDK/XDK 의 것을 가져다 쓰면 됩니다. 즉 C++ 코드 내에서 작업이 수행됩니다.

 

문제는 Android 입니다( IOS 는 제가 잘 모르겠습니다 ). 일단 Android 에서는 프로그램 실행을 위한 기본 언어가 Java 입니다. Native C++ 을 사용할 수 있도록 NDK( Native Development Kit ) 를 제공하고 있지만, 이건 그냥 라이브러리라고 생각하시면 됩니다. 물론 몇 가지 디버깅 툴이 있기는 하지만 이 문서의 주제와는 관련이 없으므로 그냥 넘어 가도록 하겠습니다. 어쨌든 UE4 에서 작업해서 나온 "libUE4.so" 파일을 사용하기 위해서는 JNI( Java Native Interface )를 통해서 Java 코드와 연관시켜야 합니다.

 

사실 이런 모든 작업은 AutomationTool 을 통해서 수행됩니다. 하지만! BUT! 본인이 UE4 에서 제공하는 모듈 이외의 것을 사용하려고 한다면, 그때부터는 지옥이 열립니다.

 

저 스스로에 대해서 한탄스럽게 생각하는 거긴 하지만, 일반적으로 Windwos 개발자들은 MAKE, CMake, Ant, Gradle 과 같은 도구 사용에 익숙하지 못합니다. 그런 게 존재하는지도 모르는 경우가 많죠.

 

우리는 Microsoft 의 Visual Studio 는 Windows 플랫폼에서 개발하기 위한 기본 설정들을 모두 해 줍니다. 하지만 외부 라이브러리 소스들을 받으면 조금 어렵습니다. "*.sln" 이나 ".vcxproj" 같은 구성 파일들이 없으면 힘들죠. 이 상황에서 매우 쉬운 길을 택하면 그냥 자기가 가지고 있던 프로젝트에 전부 포함시켜 버립니다. 그나마 좀 깔끔하게 라이브러리로 사용하겠다는 분들은 직접 프로젝트를 만들어서 넣습니다.

 

그런데 수작업하는 것도 규모가 작은 소스를 사용할 때의 이야기지 엄청난 규모의 소스를 사용하고 있고, 그 소스들이 내부적으로 서로를 DLL 로 참조해야 하는 제약이 있다거나 하면 '멘붕'에 빠지게 됩니다.

 

이를 좀 더 쉽게 할 수 있도록 해 주는 것이 CMake 입니다. 하지만 "알아야 면장이라도 한다"는 속담이 있듯이, 내가 원하는 대로 설정하려면 최소한의 기본지식은 있어야 하는데 지금까지 경험해 보지 못한 용어와 구조때문에 머리가 아픕니다. Visual Studio 에서는 NMAKE 프로젝트라는 것이 나오기도 했습니다. 현재 UE4 빌드시스템이 NMAKE 를 사용하고 있죠.

 

그나마 Windows 플랫폼에서 C++ 작업을 좀 해 보신 분들이라면 NMake 에는 익숙할 겁니다. 하지만 Ant, Gradle 은 신세계입니다. 기본적으로 Java 를 위한 프로젝트를 구성하고 컴파일, 배포까지 수행합니다. 뭔가 하는 일이 어마어마하게 많죠.

 

Windows 가 개발자에게 매력있게 다가올 수 있는 이유는 엄청난 자동화와 매우 편리한 IDE 일 것입니다. 하지만 오픈소스 프로젝트들은 누가 본격적으로 관리하지 않는 오픈소스답게 초심자들에게는 불편한 점이 많습니다. 그래서 Ant, Gradle 같은 자동화 도구들을 만드는 것 같은데, Visual Studio 를 사용하는 입장에서는 매우 불편하게 느껴지는 것이 현실입니다. 그리고 Windows 플랫폼에서 응용프로그램 개발을 하는 분들이라면 C# 이나 C++ 를 사용하지, Java 는 거의 사용하지 않을 겁니다( 물론 사용하는 분들이 있기는 하겠지만 적어도 게임업계에서는 그렇습니다 ).

 

서론이 길었는데요, 어쨌든 UE4 에서 Android 개발을 하는데, 뭔가 커스텀한 일을 수행하고자 한다면, Java 와 Gradle 은 필수라는 겁니다. 예전에는 Ant 가 기본 빌드 시스템이었는데, 최근에 Gradle 로 갈아탔다고 하더군요.

 

최근에 회사에서 Gradle 에 대해서 분석하고 해 봤는데, 제가 형식에만 집중하고 알맹이는 쏙 빠뜨렸다는 생각이 들어서 공부를 더 하고 있습니다. Gradle build script, Groovy, UPL, 이 세 가지 부분에 집중했는데, 정말 중요한 부분을 빠뜨리고 공부했습니다. 사실 앞에서 언급한 것들은 기본으로 알아야 하는 것이고, dependency 야 말로 가장 중요한 부분입니다. 제가 겪은 대부분의 에러는 이쪽에서 발생했습니다( 물론 제가 경험이 엄청 많다는 이야기는 아닙니다 ).

 

이 문서에서는 Gradle 의 dependency 개념에 대해 소개하고, UPL 과 dependency 가 어떻게 연관되는 지 설명할 것입니다. 아직 Gradle 의 기본 개념과 Groovy 문법에 대해서 모르신다면, 공부를 하고 보시는 것이 좋을 것 같습니다. 제가 그런 부분에 대해서까지 설명하기에는 좀 부족함이 많네요. UPL 에 대해서는 제 블로그의 [ UnrealPluginLanguage ( UPL ) 이란 ] 에 정리해 뒀습니다.

 

 

Gradle Dependency

 

Dependency 를 매우 저렴하게 표현하자면, Visual C++ 에서 볼 수 있는 헤더 경로와 라이브러리 경로입니다. 예를 들어 다음과 같은 호출을 생각할 수 있겠죠.

 

 

내가 사용하고자 하는 모듈의 헤더와 라이브러리를 포함하지 않고서는 해당 모듈이 제공하는 기능을 이용할 수 없습니다. 컴파일 에러나 링크 에러가 나겠죠. 그러면 이런 헤더와 라이브러리는 어디에서 구해야 하는 걸까요? 보통 VS 로 개발할 때는 소스코드를 아예 프로젝트에 포함시키거나, 소스코드의 라이브러리를 받아서 import 합니다. 혹은 라이브러리를 그대로 받아서 import 하죠.

 

Linux 에 관심이 있어서 깔아 보신 분들이라면, 아래와 같은 형식으로 패키지를 설치하는 걸 보신 적이 있을 겁니다.

 

 

특정 플랫폼을 대상으로 하는 특정 버전의 gzip 을 설치하는 거죠. 이렇게 하면 자동으로 http 를 통해 설치하고자 하는 패키지의 소스코드를 다운로드해서 빌드해서 설치해 줍니다. 참 쉽죠?

 

어쨌든 Linux 같은 오픈소스 진영에서는 외부에 저장소( repository )를 두고 거기에서 소스를 다운로드 받는 것이 일반화되어 있는 것 같습니다. Linux 는 매우 변종이 많기 때문에 정확한 플랫폼과 버전을 지정하지 않으면 안 되겠죠. Windows 야 자기들이 OS 를 만드니까 알아서 패치할 수 있지만, Linux 에서는 쉽지 않습니다. Ubuntu 같은 OS 들도 자기들이 원하는 건 설치하겠지만, 남들이 만들어 놓은 것은 어떻게 할 수 없겠죠.

 

Gradle 은 아래 그림에서 보이듯이 gradle 이 dependency 를 받는 방법은 3 가지 입니다. Network 를 통해 외부 repository 에서 받는 경우가 있고, local repository 에서 받는 경우가 있습니다. 마지막으로 이미 netowrk 를 통해 받은 repository 를 caching 해 놓고 거기에서 가지고 오는 경우가 있습니다.

 

 

 

머리가 아프실테니, 이야기를 더 진행하기 전에 기본 용어부터 정리하도록 하겠습니다. 아래 인용부분은 [ 2 ] 의 번역입니다.

 

Configuration

 

특정 목적을 위해 그룹화된 dependency 들의 named set 을 의미합니다: 예를 들어 "implementation" configuration 은 프로젝트를 컴파일하는 데 요구되는 dependency 들입니다. Configuration 은 module 과 artifact 에 대한 접근을 제공합니다. Configuration 이라는 말은 dependency management 문맥 바깥에서는 다른 의미를 가질 수 있으니 주의하세요.

 

Dependency

 

Module 을 빌드하고 테스트하고 실행하는 데 요구되는 소프트웨어의 일부에 대한 pointer 입니다.

 

Dependency constraint

 

Dependency constraint 는 어떤 dependency 에 대해서 module 이 유효한 결과를 산출하도록 하기 위해 필요한 요구사항을 의미합니다. 예를 들어 dependency constraint 는 지원되는 module version 집합을 한정할 수 있습니다. Dependency constraint 는 transitive dependency 에 대한 요청과 같은 것을 표현하는 데 사용될 수도 있습니다. 더 많은 정보를 원하신다면, "Managing versions of transitive dependencies with dependency constraints" 항목을 참조하세요.

 

Module

 

Google Guava 와 같은 소프트웨어 조각을 의미합니다. 이것은 시간이 지남에 따라서 바뀌고 있고 모든 버전을 포함한 개념입니다. 모든 module 은 이름을 가지고 있습니다. Module 의 각 배포본( release )은 module version 에 의해 표현됩니다. 편리하게 사용될 수 있도록 하기 위해, module 은 repository 에서 호스팅될 수 있습니다.

 

Module metadata

 

Module 배포본은 metadata 를 제공할 수 있습니다. Metadata는 module 에 대해 자세히 기술하는 데이터입니다. 예를 들어 repository 의 좌표( coordinate )라든가, project 나 요구되는 transitive dependency 에 대한 정보같은 것들이 있습니다. Maven 에서 metadata 파일은 ".pom" 이라 불리며, Ivy 에서는 "ivy.xml" 이라 불립니다.

 

Module version

 

Module version 은 배포된 module 이 변화할 때마다 만들어진 독립된 집합을 의미합니다. 예를 들어 18.0 은 com.google:gujava:18.0 이라는 좌표를 사용하는 module version 임을 의미합니다. 경험적으로 볼 때, module version 의 스키마에 대한 제약은 없습니다. 시간, 숫자, "-GA" 같은 특별한 접미어들이 식별자로 허용되고 있습니다. 가장 널리 사용되고 있는 versioning 전략은 "semantic versioning" 입니다.

 

Repository

 

Repository( 저장소 ) 는 module 집합을 호스팅합니다. 각각은 module version 에 의해 식별되는 하나 이상의 배포본들을 제공합니다. Repository 는 binary repository product( 예를 들어 Artifactory 나 Nexus ) 나 파일 시스템의 directory sturcture 에 기반하고 있습니다. 더 많은 정보를 원한다면 "Declaring repository" 를 참조하세요.

 

Resolution rule

 

Resolution rule 은 dependency 가 풀리는( resolved ) 방식에 영향을 줍니다. Resolution rule 은 빌드 로직의 일부로 정의됩니다. 더 많은 정보를 원하신다면 "Customizing Dependency Resolution Behavior" 를 참조하세요.

 

정리하자면 어떤 module 은 module version 과 함께 배포( release )되며, 이것은 repository 좌표를 찍어서 얻어 올 수 있는 겁니다.

 

Declaring Repository

 

그러면 여기에서 "어떤 repository 들이 있을가?" 라는 의문이 생기겠죠. Repository 의 종류는 "Repository Types" 문서에서 찾아 볼 수 있습니다. 일반적으로는 세 가지 종류의 repository 를 사용하고 있습니다.

 

출처 : [ 3 ]

 

예를 들어 JCenter 를 사용한다고 하면, Gradle 빌드 스크립트에 다음과 같이 적습니다.

 

 

하지만 Crashlytics 와 같은 특정 서비스들은 자신만의 repository 를 제공하기도 합니다. 그런 경우에는 URL 을 통해 접근해야 하죠.

 

 

URL 위쪽에 "maven" 이라는 closure 로 묶여 있는 것을 볼 수 있는데요, 이것은 RepositoryHandler 라고 부릅니다. "Repository Types" 에서 지정하고 있는 repository 에 대해 구체적인 정보를 설정할 수 있게 해 주는 것이죠.

 

Declaring Dependency

 

이제 사용할 dependency 를 선언해야 합니다. 일반적으로는 module 제공하는 사람들이 repository 와 version 을 알려줍니다. 예를 들어 Fabric Crashlytics 같은 경우에는 documentation 에서 이런 예제를 제공하죠.

 

 

 

첫 번째 코드는 gradle 프로젝트의 "base/build.gradle" 에 입력할 내용이고 두 번째 코드는 "library/build.grale" 에 입력할 코드입니다.

 

보시면 알겠지만 하나는 "dependencies" closure 에 "classpath" 라는 식별자를 사용하고 있고, 다른 하나는 "compile" 이라는 식별자를 사용하고 있습니다. 이 두 가지는 차이가 있습니다. "classpath" 는 플러그인에 접근하는 데 사용하는 경로입니다. 하지만 "compile" 은 라이브러리에 접근하는 데 사용합니다.

 

여기에서 구체적으로 언급하기에는 너무 내용이 많습니다. 그러므로 "Gradle User Guide" 에 가서 링크를 찾아 다니면서 내용을 숙지하시는 것이 좋습니다. 여기에서는 dependency 가 뭐고 UPL 과 어떤 관계를 가지는 지에 대해서만 정리할 것입니다.

 

UPL 과 Dependency

 

 

UnrealPluginLanguage 는 크게 "AndroidManifest.xml", "buildAddition.gradle", "*Activiy.java" 파일들을 수정할 수 있도록 해주는 일종의 스크립트입니다. 가장 쉬운 예제는 "Engine/Source/Runtime/Advertising/AndroidAdvertising" 에 있는 "AndroidAdvertising" 모듈에서 찾아 볼 수 있습니다.

 

이 모듈에서 핵심적인 파일은 두 개 입니다.

 

 

"AndroidAdvertising.Build.cs" 에서는 "AndroidAdvertising_APL.xml" 을 등록합니다. 여기에서 "UPL" 이 아니라 "APL" 이라는 이름을 사용해서 헷갈리는 분들이 계실텐데요, "APL" 은 "Android Plugin Language" 의 약자로 UPL 의 전신입니다. Android 전용으로만 사용되었을 때 사용하던 용어입니다.

 

어쨌든 다음과 같이 UPL 을 등록합니다.

 

 

그러면 AutomationTool 에서 Android BuildTool 을 실행하면서 이 UPL 파일을 평가하게 되는거죠. 이 UPL 파일 안쪽에 보면, "<buildGradleAdditions>" 엘리먼트에서 다음과 같이 "buildAddtions.gradle" 파일에 코드를 삽입하는 것을 볼 수 있습니다.

 

 

이건 또 "implementation" 이라는 키워드를 사용하는데요, 프로젝트 컴파일에 사용하는 구성이므로 그렇습니다.

 

UPL 은 정말 많은 커스터마이징 기능을 가지고 있습니다. 그 목록은 [ UnrealPluginLanguage ( UPL ) 이란 ] 에서 찾아 보세요.

 

참고자료

 

[ 1 ] Introduction to Dependency Management, Gradle Docs.

[ 2 ] Dependency Management Terminology, Gradle Docs.

[ 3 ] Declaring Repositories, Gradle Docs.

UE4 에서 Android 를 위해서 Crashlystics 를 붙여 보려고 하는데, 이를 위해서는 "AndroidMenifest.xml" 과 "build.gradle" 파일이 수정되어야 합니다. 하지만 이 파일들은 packaging 시에 생성되므로, 어떻게 손을 댈 방법이 없습니다.


어떤 사람들은 apk 가 나온 뒤에 Android Studio 같은 도구를 사용해서 바꾸라고 하는데, 저는 이렇게 추가적인 프로세스가 들어 가는 것은 불합리하다고 생각을 했습니다. 그래서 검색을 좀 해 보니, UE4 4.17 에 gradle 지원을 하면서, UnrealPluginLanguage 에 대한 언급을 한게 있더군요.


UE4 4.17 을 릴리스하면서, Gradle 에 대한 지원이 실험 빌드 옵션으로 제공됩니다. 이는 Android Project Settings 의 프로젝트당 설정으로 활성화되는데요, "Enable instead of Ant [Experimental]" 체크박스를 사용합니다. 만약 여러분이 Android SDK 라이선스 동의를 하지 않았다면, "동의" 라는 표시와 함께 다이얼로그가 활성화됩니다. "동의" 를 누르면 적절한 라이선스 파일이 생성될 것이며, Gradle 이 종속성을 다운로드할 수 있게 됩니다.


aar-imports.txt 나 Unreal Plugin Language( UPL )<AARImports/> 를 사용해 등록되는 AAR 파일들이 Gradle 의 종속성으로서 자동으로 추가됩니다. build.gradle 에 대한 추가사항은 UPL<buildGradleAdditions/> 노드를 사용해 만들어질 수 있습니다. build.gradle 을 추가하는 다른 방법은 "additions.gradle" 이라는 파일을 <prebuildCopies/> 노드를 사용해 JavaLibs 에 복사되는 디렉토리 중 하나에 저장하는 것입니다( Engine/Source/ThirdParty/AndroidPermission/permission_library 를 예제로 참고하세요 ).


새로운 Gradle build path 의 유용한 이점은 Android Studio 에서 launch 하거나 packging 한 후에 결과로 나오는 build.gradle 을 로드할 수 있다는 겁니다. 이 파일은 여러분의 프로젝트의 "Intermediate/Android/APK/gradle" 디렉토리에 존재할 겁니다. 이 구현을 사용해서 native debugging 이 가능하지는 않지만, 배치 파일을 먼저 실행해 packaging 으로부터 실행되거나 설치된 OBB 를 가지고 있다면, Run 이나 Debug 버튼을 누를 수 있습니다. Native 코드를 변경하지만 않았다면, Java 코드를 변경하거나 Java 를 디버깅하기 위해 중단점을 설정하거나, 이터레이션을 수행할 수 있습니다. 경고: 이 위치에서는 Java 코드의 복사본을 가지고 작업하고 있기 때문에, UE4 에디터에서 launch 나 package 를 사용하기 전에 정상 위치에 변경사항을 반영하시기 바랍니다. 그렇지 않으면 덮어써질 겁니다!


이 릴리스에서 Gradle 을 사용하다가 문제가 생기면 알려 주세요; Google 이 더 이상 Ant 를 지원하지 않으므로, 그것을 제거하려고 합니다.


출처 : Gradle support in UE4 4.17


여기에서 알 수 있는 건 단지 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 를 사용하려면 다음과 같이 해야 한다는 것에 주의하세요 :


< = &lt;

> = &gt;

& = &amp;


<insertValue value=""/> 는 삽입 전에 value 의 변수를 평가합니다. 만약 value 가 쌍따옴표( " ) 를 포함하고 있다면, 그것을 &quot; 로 작성해야 합니다:


<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 와 연동하는 작업을 해 볼 계획입니다.

+ Recent posts