Wednesday, February 28, 2024

AOSP 빌드 시스템의 진화: Android.mk에서 Android.bp까지

오늘날 모바일 생태계의 중심에 있는 Android는 단순한 운영체제를 넘어 거대한 플랫폼으로 자리 잡았습니다. 수십억 개의 디바이스를 구동하는 이 운영체제의 핵심에는 오픈소스 정신이 깃들어 있습니다. 바로 Android Open Source Project(AOSP)입니다. AOSP는 누구나 Android 소스 코드를 가져와서 수정하고, 자신만의 디바이스를 만들거나, 완전히 새로운 운영체제를 창조할 수 있는 자유를 부여합니다. 이러한 개방성은 삼성의 One UI, 구글의 Pixel UI와 같은 제조업체별 맞춤형 환경부터 LineageOS와 같은 커스텀 ROM에 이르기까지, Android 생태계의 다채로운 다양성을 만들어내는 원동력입니다.

그러나 이 거대한 소스 코드를 실제 동작하는 운영체제로 바꾸는 과정은 결코 간단하지 않습니다. 수십만 개의 파일, 수많은 라이브러리와 애플리케이션, 그리고 다양한 하드웨어 아키텍처를 모두 아우르기 위해서는 정교하고 강력한 빌드 시스템이 필수적입니다. AOSP의 빌드 시스템은 바로 이 복잡한 퍼즐을 맞추는 중추적인 역할을 담당합니다. 소스 코드를 컴파일하고, 리소스를 처리하며, 의존성을 해결하여 최종적으로 디바이스에 설치할 수 있는 이미지 파일을 생성하는 모든 과정이 빌드 시스템을 통해 이루어집니다. 따라서 AOSP를 깊이 이해하고 다루기 위해서는 빌드 시스템에 대한 이해가 선행되어야 합니다.

AOSP의 빌드 시스템은 지난 10여 년간 정체되어 있지 않았습니다. 프로젝트의 규모가 기하급수적으로 커지고, 개발자들의 요구사항이 복잡해짐에 따라 빌드 시스템 역시 끊임없이 진화해왔습니다. 초기의 GNU Make 기반의 Android.mk 파일 시스템에서 출발하여, 현재는 Go 언어로 작성된 Soong과 선언적 구문의 Android.bp 파일이 주류를 이루고 있습니다. 이 글에서는 AOSP 빌드 시스템의 역사적 흐름을 따라가며, 과거의 주역이었던 Android.mk의 구조와 한계를 살펴보고, 현재와 미래를 책임지는 Android.bp의 등장 배경과 강력한 기능들을 심층적으로 분석합니다. 이 여정을 통해 우리는 왜 이러한 변화가 필연적이었는지, 그리고 두 시스템이 어떻게 공존하며 작동하는지를 명확히 이해하게 될 것입니다.

1. Make의 시대: Android.mk의 구조와 한계

AOSP 초기, 빌드 시스템의 근간은 수십 년간 C/C++ 개발 세계에서 표준처럼 사용되어 온 GNU Make였습니다. Make는 파일 간의 의존성을 정의하고, 변경된 파일과 관련된 부분만 다시 빌드하는 강력한 기능을 제공했기 때문에 AOSP와 같이 거대한 프로젝트에 적합한 선택이었습니다. 이 시기, 개발자들은 Android.mk라는 특정 형식의 Makefile을 작성하여 각 모듈(라이브러리, 실행 파일, 애플리케이션 등)을 어떻게 빌드할지 시스템에 지시했습니다.

1.1. Android.mk의 기본 구조

Android.mk 파일은 본질적으로 GNU Make의 문법을 따르지만, AOSP 빌드 시스템이 제공하는 수많은 매크로와 변수를 활용하여 정형화된 방식으로 작성됩니다. 모든 Android.mk 파일은 거의 항상 동일한 패턴으로 시작하고 끝납니다.

가장 기본적인 Android.mk 파일의 구조는 다음과 같습니다.


# 1. 모듈이 위치한 디렉토리 설정
LOCAL_PATH := $(call my-dir)

# 2. 이전 빌드 설정 초기화
include $(CLEAR_VARS)

# 3. 모듈 관련 변수 설정
LOCAL_MODULE    := my_native_library
LOCAL_SRC_FILES := main.c utils.c
# ... 기타 변수들 ...

# 4. 빌드할 모듈 종류 지정
include $(BUILD_SHARED_LIBRARY)

이 간단한 구조 안에 AOSP 빌드 시스템의 핵심 철학이 담겨 있습니다.

  • LOCAL_PATH := $(call my-dir): 빌드 시스템에게 현재 Android.mk 파일이 위치한 경로를 알려줍니다. 소스 파일을 찾기 위한 기준점이 됩니다.
  • include $(CLEAR_VARS): AOSP 빌드 시스템은 하나의 Make 실행 컨텍스트에서 수많은 Android.mk 파일을 순차적으로 읽어들입니다. CLEAR_VARS는 이전 모듈에서 설정했던 LOCAL_XXX 변수들을 모두 초기화하여, 모듈 간 설정이 섞이는 것을 방지하는 중요한 역할을 합니다. 이 구문이 없다면, A 모듈의 소스 파일이 B 모듈에 포함되는 등의 예기치 않은 문제가 발생할 수 있습니다.
  • LOCAL_XXX 변수들: 모듈의 특성을 정의하는 핵심 부분입니다. 모듈의 이름, 소스 파일 목록, 컴파일러 플래그, 의존하는 라이브러리 등을 이곳에서 지정합니다.
  • include $(BUILD_XXX): 설정된 LOCAL_XXX 변수들을 바탕으로, 실제로 어떤 종류의 결과물을 만들지 결정합니다. 예를 들어, BUILD_SHARED_LIBRARY는 공유 라이브러리(.so 파일)를, BUILD_EXECUTABLE은 실행 파일을 생성하는 빌드 규칙을 만듭니다.

1.2. 핵심적인 LOCAL_XXX 변수들

Android.mk의 진정한 힘은 다양한 LOCAL_XXX 변수를 조합하여 복잡한 빌드 요구사항을 처리하는 데 있습니다. 다음은 가장 빈번하게 사용되는 변수들의 상세한 설명입니다.

  • LOCAL_MODULE: 빌드 결과물(라이브러리, 실행 파일 등)의 이름을 지정합니다. 이 이름은 시스템 전체에서 유일해야 합니다.
  • LOCAL_SRC_FILES: 빌드에 사용될 소스 코드 파일의 목록입니다. LOCAL_PATH를 기준으로 상대 경로를 사용합니다. main.cpp helper/network.cpp와 같이 공백으로 구분하여 여러 파일을 지정할 수 있습니다.
  • LOCAL_C_INCLUDES: C/C++ 컴파일러가 헤더 파일(.h, .hpp)을 찾을 때 검색할 디렉토리 경로 목록입니다. $(LOCAL_PATH)/include와 같이 지정하여 프로젝트 내부 헤더를 포함하거나, AOSP 전역 경로를 지정할 수 있습니다.
  • LOCAL_SHARED_LIBRARIES: 현재 모듈이 의존하는 공유 라이브러리 목록입니다. 예를 들어, liblog libcutils와 같이 모듈 이름을 나열하면, 빌드 시스템은 링크 시점에 해당 .so 파일들을 연결해 줍니다.
  • LOCAL_STATIC_LIBRARIES: 현재 모듈이 의존하는 정적 라이브러리 목록입니다. 정적 라이브러리의 코드는 빌드 시점에 현재 모듈의 결과물에 직접 포함됩니다.
  • LOCAL_CFLAGS / LOCAL_CPPFLAGS: 각각 C와 C++ 소스 코드를 컴파일할 때 컴파일러에 전달할 추가 플래그를 지정합니다. 디버깅 정보(-g), 최적화 수준(-O2), 경고 수준(-Wall) 등을 설정하는 데 사용됩니다.
  • LOCAL_LDFLAGS: 링커에 전달할 추가 플래그입니다. 특정 라이브러리 검색 경로를 추가하거나 링커 동작을 제어할 때 사용됩니다.
  • LOCAL_MODULE_TAGS: 이 모듈이 어떤 종류의 빌드에 포함될지를 결정하는 태그입니다. user, eng, tests, optional 등의 값을 가질 수 있습니다. 예를 들어 tests로 태그된 모듈은 일반 사용자용 빌드에는 포함되지 않고, 테스트 빌드에서만 포함됩니다. optional이 가장 일반적이며, 제품 설정 파일에서 명시적으로 포함시켜야 빌드에 포함됩니다.
  • LOCAL_PROGUARD_ENABLED: 자바 모듈에서 ProGuard/R8을 사용한 코드 축소 및 난독화를 활성화할지 여부를 결정합니다.
  • LOCAL_STATIC_JAVA_LIBRARIES: 현재 자바 모듈이 의존하는 정적 자바 라이브러리(.jar) 목록입니다.

1.3. 다양한 빌드 타겟 (BUILD_XXX)

LOCAL_XXX 변수 설정이 끝나면, include $(BUILD_XXX) 구문을 통해 최종적으로 어떤 결과물을 만들지 명시해야 합니다.

  • $(BUILD_EXECUTABLE): C/C++ 소스 코드를 컴파일하여 네이티브 실행 파일을 생성합니다.
  • $(BUILD_SHARED_LIBRARY): C/C++ 소스 코드를 컴파일하여 공유 라이브러리(.so)를 생성합니다.
  • $(BUILD_STATIC_LIBRARY): C/C++ 소스 코드를 컴파일하여 정적 라이브러리(.a)를 생성합니다.
  • $(BUILD_JAVA_LIBRARY): 자바 소스 코드를 컴파일하여 .jar 라이브러리를 생성합니다.
  • $(BUILD_PACKAGE): Android 애플리케이션(.apk)을 생성합니다. AndroidManifest.xml 파일이 반드시 필요합니다.
  • $(BUILD_PREBUILT): 소스 코드를 컴파일하는 대신, 이미 빌드된 바이너리 파일을 시스템에 복사할 때 사용합니다. LOCAL_SRC_FILES에 바이너리 파일 경로를, LOCAL_MODULE_CLASS에 파일 종류를 지정해야 합니다.

1.4. Make 기반 시스템의 명백한 한계

Make와 Android.mk는 수년간 AOSP의 든든한 기반이었지만, 프로젝트의 규모가 커지면서 여러 문제점을 드러내기 시작했습니다.

  1. 빌드 성능 저하: Make의 가장 큰 문제점은 성능이었습니다. AOSP 전체 빌드는 수만 개의 모듈을 처리해야 합니다. Make는 빌드 시작 시 모든 .mk 파일을 읽어들여 의존성 트리를 생성하는데, 이 과정이 본질적으로 단일 스레드로 동작합니다. 프로젝트가 커질수록 이 "해석" 단계에만 수십 분이 소요될 수 있었습니다. make -j 옵션으로 실제 컴파일 과정은 병렬화할 수 있었지만, 초기 의존성 분석의 병목 현상은 해결할 수 없었습니다.
  2. 복잡성과 가독성 문제: Make는 매우 유연한 도구입니다. 이 유연성은 복잡한 로직을 구현할 수 있게 해주었지만, 동시에 Android.mk 파일을 매우 복잡하고 이해하기 어렵게 만드는 원인이 되었습니다. 파일 내부에 조건문(ifeq), 반복문(foreach), 사용자 정의 함수가 남용되면서, 단순한 모듈 정의 파일이 아니라 하나의 작은 프로그램처럼 변질되기도 했습니다. 이는 빌드 스크립트의 유지보수를 극도로 어렵게 만들었습니다.
  3. 오류 발생 가능성 및 디버깅의 어려움: Make의 변수 할당(:=, +=)과 문자열 기반 처리 방식은 사소한 오타나 실수로 인한 오류에 매우 취약했습니다. 예를 들어, 변수 이름에 오타가 있어도 Make는 오류를 발생시키는 대신 빈 문자열로 처리해버려, 빌드는 성공한 것처럼 보이지만 결과물이 비정상적으로 동작하는, 추적하기 어려운 버그를 낳았습니다. 또한, 빌드 실패 시 출력되는 오류 메시지가 모호하여 원인을 파악하는 데 많은 시간을 허비해야 했습니다.
  4. 상태 의존적인(Stateful) 설계: include $(CLEAR_VARS)의 존재 자체가 Make 시스템의 상태 의존적인 특성을 보여줍니다. 개발자는 항상 이전 모듈의 설정이 다음 모듈에 영향을 주지 않도록 명시적으로 "초기화"를 호출해야 했습니다. 이는 실수를 유발하기 쉬운 구조이며, 각 모듈이 독립적으로 선언되어야 한다는 현대적인 소프트웨어 공학 원칙에도 위배됩니다.

이러한 문제들은 AOSP의 개발 속도를 저해하고, 개발자 경험을 악화시키는 주요 요인이 되었습니다. 구글은 더 빠르고, 더 안정적이며, 더 이해하기 쉬운 차세대 빌드 시스템의 필요성을 절감하게 되었습니다.

2. 새로운 패러다임: Soong, Blueprint, 그리고 Ninja

Make 시스템의 한계를 극복하기 위해 구글은 AOSP 빌드 시스템을 처음부터 다시 설계하기로 결정했습니다. 그 결과물이 바로 Soong 빌드 시스템입니다. Soong은 단일 도구가 아니라, 세 가지 핵심 구성요소가 유기적으로 결합된 시스템입니다.

  • Blueprint: Android.mk를 대체하는 선언적인(declarative) 빌드 정의 파일(Android.bp)의 형식입니다.
  • Soong: Go 언어로 작성된 도구로, Android.bp 파일을 읽어서 실제 빌드 작업을 수행할 저수준 빌드 파일(Ninja 파일)을 생성하는 역할을 합니다.
  • Ninja: 구글에서 개발한 고성능 저수준 빌드 시스템입니다. 의존성 정보가 이미 계산된 상태로 입력받아, 가능한 최대의 병렬성으로 빌드 명령을 실행하는 데 초점을 맞춘 도구입니다.

이 새로운 시스템의 핵심 철학은 '책임의 분리'입니다. 개발자는 Blueprint(Android.bp)를 통해 "무엇을" 빌드할 것인지만 선언적으로 기술합니다. 그러면 Soong이 "어떻게" 빌드할 것인지에 대한 복잡한 논리와 의존성 분석을 처리하여 Ninja 파일로 변환합니다. 마지막으로 Ninja는 이 설계도를 받아 "가장 빠르게" 빌드 작업을 실행합니다. 이러한 구조는 Make가 가졌던 복잡성, 성능, 오류 발생 가능성의 문제를 근본적으로 해결합니다.

2.1. Blueprint (Android.bp)의 선언적 미학

Android.bp 파일은 JSON과 유사한 간결하고 명확한 문법을 사용합니다. Make처럼 조건문, 반복문, 복잡한 함수가 존재하지 않습니다. 오직 모듈 타입과 그 속성(properties)을 정의하는 것만이 허용됩니다.

앞서 Android.mk로 작성했던 공유 라이브러리를 Android.bp로 변환하면 다음과 같습니다.


cc_library_shared {
    name: "my_native_library",
    srcs: [
        "main.c",
        "utils.c",
    ],
    // ... 기타 속성들 ...
}

Android.mk와 비교했을 때 몇 가지 중요한 차이점이 즉시 눈에 띕니다.

  • 보일러플레이트 제거: LOCAL_PATHCLEAR_VARS 같은 상용구가 완전히 사라졌습니다. 각 Android.bp 파일은 그 자체로 완결된 정의이며, 다른 파일에 영향을 주거나 받지 않습니다.
  • 명시적인 타입: include $(BUILD_SHARED_LIBRARY) 대신 cc_library_shared라는 명시적인 모듈 타입을 사용합니다. 이는 코드의 가독성을 크게 향상시킵니다.
  • 속성 기반 정의: LOCAL_MODULE := ... 같은 변수 할당 대신, name: "..."와 같이 속성-값 쌍으로 모듈을 정의합니다. 이는 마치 객체 지향 프로그래밍에서 객체의 속성을 설정하는 것과 유사하여 직관적입니다.
  • 자료형 사용: 소스 파일 목록은 []를 사용한 문자열 배열로 표현됩니다. 이는 단순한 공백 구분 문자열보다 훨씬 더 구조적이고 오류에 강합니다.

2.2. 핵심적인 모듈 타입과 속성들

Soong은 다양한 종류의 모듈을 빌드하기 위해 사전 정의된 풍부한 모듈 타입들을 제공합니다.

주요 모듈 타입:

  • cc_library, cc_library_shared, cc_library_static: C/C++ 라이브러리를 생성합니다. cc_library는 기본적으로 공유 라이브러리를 만들지만, 링킹 방식에 따라 정적으로도 사용될 수 있습니다.
  • cc_binary: C/C++ 네이티브 실행 파일을 생성합니다.
  • cc_test: C/C++ 네이티브 테스트용 실행 파일을 생성합니다.
  • android_app: Android 애플리케이션(APK)을 생성합니다.
  • java_library: .jar 파일을 생성합니다.
  • android_library: Android 라이브러리(.aar)를 생성합니다.
  • prebuilt_etc: 이미 존재하는 파일을 빌드 결과물의 특정 위치(예: /system/etc)에 복사합니다.
  • filegroup: 소스 파일 그룹에 이름을 붙여 다른 모듈에서 재사용할 수 있게 합니다.
  • cc_defaults, java_defaults: 여러 모듈에 공통적으로 적용될 속성들을 묶어 정의하는 특별한 모듈입니다. 상속과 유사한 개념으로 코드 중복을 획기적으로 줄여줍니다.

주요 속성(Properties):

LOCAL_XXX 변수들은 대부분 Android.bp의 속성으로 일대일 대응됩니다.

  • name: 모듈의 이름 (LOCAL_MODULE에 해당)
  • srcs: 소스 파일 목록 (LOCAL_SRC_FILES에 해당)
  • include_dirs, local_include_dirs: 헤더 검색 경로 (LOCAL_C_INCLUDES에 해당)
  • shared_libs: 의존하는 공유 라이브러리 (LOCAL_SHARED_LIBRARIES에 해당)
  • static_libs: 의존하는 정적 라이브러리 (LOCAL_STATIC_LIBRARIES에 해당)
  • cflags, cppflags, ldflags: 컴파일러 및 링커 플래그 (각각 LOCAL_CFLAGS, LOCAL_CPPFLAGS, LOCAL_LDFLAGS에 해당)
  • tags: ["eng", "tests"] 와 같은 태그 목록 (LOCAL_MODULE_TAGS에 해당)
  • static_executable: C/C++ 바이너리를 정적으로 링크할지 여부 (예: Bionic libc를 포함)
  • owner: 모듈의 소유자 또는 관리하는 팀을 명시합니다.
  • vendor, soc_specific, product_specific: 모듈이 특정 파티션(vendor, odm 등)에 설치되어야 함을 나타냅니다. 이는 Treble 아키텍처에서 매우 중요합니다.

2.3. Android.bp의 고급 기능

Android.bp는 단순한 문법 외에도 대규모 프로젝트 관리를 용이하게 하는 여러 고급 기능을 제공합니다.

Defaults 모듈을 통한 중복 제거

동일한 컴파일러 플래그, 헤더 경로, 의존성을 공유하는 여러 모듈이 있다고 가정해 봅시다. Android.mk에서는 이러한 공통 설정을 변수로 빼내어 `include` 하는 방식으로 처리했지만, 이는 전역 변수 오염의 위험이 있었습니다. Android.bp에서는 defaults 모듈을 사용하여 훨씬 더 깔끔하게 해결할 수 있습니다.


// 공통 속성을 정의하는 defaults 모듈
cc_defaults {
    name: "my_library_defaults",
    cflags: [
        "-Wall",
        "-Werror",
        "-fvisibility=hidden",
    ],
    shared_libs: ["liblog"],
}

// defaults를 상속받는 첫 번째 라이브러리
cc_library_shared {
    name: "libfeature_a",
    defaults: ["my_library_defaults"],
    srcs: ["feature_a.c"],
}

// defaults를 상속받고 속성을 추가/오버라이드하는 두 번째 라이브러리
cc_library_shared {
    name: "libfeature_b",
    defaults: ["my_library_defaults"],
    srcs: ["feature_b.c"],
    cflags: ["-DLOG_TAG=\"FeatureB\""], // 기존 cflags에 추가됨
}

위 예시에서 libfeature_alibfeature_bmy_library_defaults에 정의된 cflagsshared_libs를 자동으로 상속받습니다. 이를 통해 공통 설정이 변경될 경우 defaults 모듈 하나만 수정하면 되어 유지보수성이 크게 향상됩니다.

아키텍처 및 타겟별 속성 제어

Android는 ARM, ARM64, x86, x86-64 등 다양한 CPU 아키텍처를 지원합니다. 특정 아키텍처에서만 필요한 소스 파일을 컴파일하거나 다른 컴파일러 플래그를 적용해야 하는 경우가 많습니다. Android.bp는 이를 위한 직관적인 구조를 제공합니다.


cc_library {
    name: "libarch_specific",
    srcs: ["common.c"],
    arch: {
        arm: {
            srcs: ["arch_arm.c"],
            cflags: ["-DARM_OPTIMIZATION"],
        },
        arm64: {
            srcs: ["arch_arm64.c"],
            cflags: ["-DARM64_OPTIMIZATION"],
        },
        x86: {
            // x86 관련 설정
        },
    },
    // 호스트(빌드 머신)와 타겟(디바이스)을 구분
    target: {
        android: {
            // 디바이스에서 빌드될 때만 적용
            shared_libs: ["libandroid"],
        },
        host: {
            // 호스트에서 빌드될 때(예: 빌드 도구)만 적용
        },
    },
}

이러한 구조적 속성 제어는 Android.mk의 복잡한 ifeq ($(TARGET_ARCH), arm) ... endif 블록을 대체하며, 빌드 구성의 명확성을 높여줍니다. Soong은 빌드 시점의 타겟 아키텍처에 맞춰 필요한 속성만 자동으로 선택하여 Ninja 파일을 생성합니다.

3. 공존과 전환의 기술

AOSP처럼 거대한 프로젝트의 빌드 시스템을 하루아침에 교체하는 것은 불가능합니다. 수만 개의 기존 Android.mk 파일을 한 번에 Android.bp로 변환하는 것은 엄청난 노력이 필요하며, 그 과정에서 발생할 수 있는 잠재적인 문제를 감당하기 어렵습니다. 따라서 구글은 두 시스템이 원활하게 공존하며 점진적으로 전환할 수 있는 전략을 택했습니다.

3.1. Soong의 Android.mk 처리 방식

놀랍게도, Soong 빌드 시스템은 Android.mk 파일을 무시하지 않습니다. 빌드 과정이 시작되면, Soong은 먼저 프로젝트 전체의 Android.bp 파일을 스캔하여 빌드 그래프를 구성합니다. 그 후, androidmk라는 내부 도구를 실행하여 남아있는 모든 Android.mk 파일을 찾아냅니다.

androidmk 도구는 Android.mk 파일을 읽어서 그 내용을 Soong이 이해할 수 있는 중간 형태(일종의 JSON 데이터)로 변환하는 역할을 합니다. 그러면 Soong은 이 변환된 정보를 기존 Android.bp 기반의 빌드 그래프에 통합합니다. 즉, Soong의 관점에서는 모든 모듈이 결국 동일한 내부 표현으로 처리되는 것입니다. 이 영리한 설계 덕분에 Android.bp로 정의된 모듈이 Android.mk로 정의된 라이브러리에 의존하거나, 그 반대의 경우도 아무런 문제 없이 처리될 수 있습니다. 개발자는 두 파일 형식이 혼재된 환경에서도 평소와 같이 빌드를 수행할 수 있습니다.

3.2. 언제 무엇을 사용해야 하는가?

두 시스템이 공존하는 현 상황에서 개발자는 어떤 파일을 사용해야 할지 고민하게 됩니다. 구글의 공식적인 가이드라인은 명확합니다.

  • 새로운 모듈 작성 시: 무조건 Android.bp를 사용해야 합니다. Android.mk는 레거시 시스템으로 간주되며, 새로운 모듈을 Android.mk로 작성하는 것은 권장되지 않습니다.
  • 기존 모듈 수정 시:
    • 단순한 버그 수정이나 소스 파일 몇 개를 추가하는 정도의 작은 변경이라면 기존의 Android.mk 파일을 그대로 수정해도 괜찮습니다.
    • 하지만 새로운 의존성을 추가하거나, 빌드 플래그를 대대적으로 변경하거나, 모듈의 구조를 리팩토링하는 등 의미 있는 변경이 필요하다면, 이번 기회에 Android.bp 파일로 전환하는 것이 좋습니다.
  • Android.mk가 여전히 필요한 경우: 대부분의 모듈은 Android.bp로 표현 가능하지만, 아주 드물게 Make의 복잡한 로직이나 특정 기능이 필요한 경우가 있을 수 있습니다. 하지만 이는 매우 예외적인 상황이며, 대부분의 개발자는 마주치지 않을 것입니다. 제품의 전체 패키지 목록을 정의하는 최상위 제품 설정 파일(예: aosp_arm.mk) 등은 여전히 Make 형식을 사용하고 있습니다.

3.3. 자동 변환 도구: `androidmk`

기존 Android.mk 파일을 Android.bp로 수동으로 변환하는 것은 지루하고 실수가 발생하기 쉬운 작업입니다. 다행히 AOSP는 이 과정을 자동화해주는 androidmk라는 도구를 제공합니다. (이 도구는 Soong 내부에서 사용되는 것과 동일한 로직을 가집니다.)

사용법은 매우 간단합니다. AOSP 소스 트리 루트에서 다음 명령을 실행하면 됩니다.


# 셸 환경 설정
source build/envsetup.sh
lunch aosp_arm64-eng

# androidmk 도구 실행
androidmk path/to/your/Android.mk > path/to/your/Android.bp

이 명령은 지정된 Android.mk 파일을 분석하여, 그에 상응하는 Android.bp 파일의 내용을 표준 출력으로 내보냅니다.

변환 예시

다음과 같은 조금 더 복잡한 Android.mk 파일이 있다고 가정해 보겠습니다.

Android.mk:

LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)

LOCAL_MODULE := libdata_processor
LOCAL_MODULE_TAGS := optional

LOCAL_SRC_FILES := \
    processor.cpp \
    parser.cpp

LOCAL_C_INCLUDES := $(LOCAL_PATH)/include

LOCAL_SHARED_LIBRARIES := \
    libcutils \
    liblog

LOCAL_CFLAGS := -Wextra

include $(BUILD_SHARED_LIBRARY)

위 파일에 대해 androidmk를 실행하면 다음과 유사한 Android.bp 파일이 생성됩니다.

Android.bp (생성 결과):

cc_library_shared {
    name: "libdata_processor",
    cflags: ["-Wextra"],
    srcs: [
        "processor.cpp",
        "parser.cpp",
    ],
    local_include_dirs: ["include"],
    shared_libs: [
        "libcutils",
        "liblog",
    ],
}

대부분의 변환이 깔끔하게 이루어지는 것을 볼 수 있습니다. LOCAL_C_INCLUDES$(LOCAL_PATH)는 자동으로 local_include_dirs로 변환되었고, 나머지 변수들도 적절한 속성으로 매핑되었습니다. LOCAL_MODULE_TAGSoptional이 기본값이므로 생성된 파일에서는 생략될 수 있습니다.

자동 변환의 한계

androidmk 도구는 매우 강력하지만 만능은 아닙니다. Android.mk 파일 내부에 복잡한 Make 문법(조건문, 반복문, 커스텀 함수 등)이 포함된 경우, 도구가 이를 완벽하게 해석하지 못하고 변환에 실패하거나 불완전한 결과를 내놓을 수 있습니다. 따라서 자동 변환 후에는 반드시 생성된 Android.bp 파일의 내용을 검토하고, 필요하다면 수동으로 수정하는 과정이 필요합니다.

4. 빌드의 전체 그림: 제품 설정과 실행

지금까지 우리는 개별 모듈을 정의하는 Android.mkAndroid.bp 파일에 초점을 맞췄습니다. 하지만 AOSP 빌드는 이러한 수많은 모듈들을 모아 특정 디바이스에 맞는 최종 운영체제 이미지를 만드는 과정입니다. 이 상위 레벨의 "제품 구성"은 어떻게 이루어질까요?

흥미롭게도, 이 제품 구성 단계는 아직까지 대부분 Make 시스템에 의존하고 있습니다.

4.1. 제품 구성 Makefile

AOSP 소스 트리 내의 device/ 디렉토리에는 각 하드웨어 제조사와 디바이스 모델에 맞는 구성 파일들이 존재합니다. 예를 들어, 구글 픽셀용 구성 파일은 device/google/pixel/ 아래에 있습니다. 이 디렉토리들 안에는 .mk 확장자를 가진 여러 파일들이 있으며, 이들이 모여 하나의 "제품"을 정의합니다.

가장 중요한 파일 중 하나는 제품의 메인 Makefile이며, 흔히 aosp_<device>.mk 또는 lineage_<device>.mk와 같은 이름을 가집니다. 이 파일은 다른 .mk 파일들을 `include`하며 다음과 같은 핵심 정보들을 설정합니다.

  • PRODUCT_NAME, PRODUCT_DEVICE, PRODUCT_BRAND, PRODUCT_MODEL: 빌드 결과물의 이름과 디바이스의 메타데이터를 정의합니다. (예: `aosp_arm`, `generic_arm64`)
  • PRODUCT_PACKAGES: 이 제품 빌드에 포함될 모듈(애플리케이션, 라이브러리, 실행 파일 등)의 목록입니다. 여기에 Android.mkAndroid.bp에서 정의한 모듈의 name을 나열하면, 해당 모듈들이 최종 시스템 이미지에 포함됩니다.
  • PRODUCT_COPY_FILES: 특정 파일을 빌드 결과물의 특정 위치로 복사하는 규칙을 정의합니다. <source_file>:<destination_path> 형식으로 지정하며, 주로 펌웨어, 설정 파일 등을 복사하는 데 사용됩니다.
  • PRODUCT_PROPERTY_OVERRIDES: 시스템 속성(/system/build.prop 파일에 기록됨)의 기본값을 설정합니다. ro.product.model=MyDevice와 같이 지정합니다.

4.2. 빌드 실행 과정: `lunch` 와 `m`

개발자가 AOSP를 빌드할 때 사용하는 명령어들은 이 모든 복잡한 과정을 추상화해줍니다.

  1. source build/envsetup.sh: 빌드에 필요한 환경 변수를 설정하고, lunch, m과 같은 헬퍼 함수들을 현재 셸 세션에 로드합니다. 이 과정은 AOSP 작업을 시작할 때마다 한 번씩 실행해야 합니다.
  2. lunch: "Launch"의 줄임말로, 빌드할 타겟 제품을 선택하는 명령어입니다. lunch를 인자 없이 실행하면 선택 가능한 제품 목록이 나타나고, 사용자는 그중 하나를 선택할 수 있습니다. 예를 들어 lunch aosp_arm64-eng는 `aosp_arm64` 제품을 `eng` (엔지니어링) 빌드 변형으로 빌드하도록 설정합니다. 이 명령은 내부적으로 TARGET_PRODUCT, TARGET_BUILD_VARIANT와 같은 환경 변수를 설정하여 이후의 빌드 과정에 영향을 줍니다.
  3. m: "Make"의 줄임말로, 실제 빌드를 트리거하는 메인 명령어입니다. m을 단독으로 실행하면 전체 AOSP 소스 코드를 빌드합니다. 이 명령어는 겉보기에는 단순한 Make 래퍼(wrapper) 같지만, 내부적으로는 Soong과 Ninja를 호출하는 복잡한 로직을 수행합니다.
    • 우선, Soong이 실행되어 모든 Android.bpAndroid.mk 파일을 분석하고, 최종적인 build-<product>.ninja 파일을 out/ 디렉토리에 생성합니다.
    • 그 다음, Ninja가 이 생성된 .ninja 파일을 읽어서 실제 컴파일, 링크, 패키징 등의 작업을 고도로 병렬화하여 실행합니다.
    또한 mm (현재 디렉토리의 모듈만 빌드), mmm <dir> (지정된 디렉토리의 모듈만 빌드)와 같은 유용한 변형 명령어들도 제공되어 개발 생산성을 높여줍니다.

이처럼, AOSP 빌드는 최상위 제품 구성 레벨에서는 Make가, 개별 모듈 정의와 실제 빌드 실행 레벨에서는 Soong/Ninja가 역할을 분담하는 하이브리드 시스템으로 동작하고 있습니다.

5. 실전 가이드 및 모범 사례

AOSP 빌드 시스템을 효과적으로 사용하기 위해서는 몇 가지 모범 사례를 따르는 것이 중요합니다. 이는 빌드 파일의 가독성을 높이고, 유지보수를 용이하게 하며, 잠재적인 빌드 오류를 사전에 방지하는 데 도움이 됩니다.

5.1. Android.bp 파일 작성 모범 사례

  • 알파벳 순서 유지: 모듈 내의 속성들(name 제외)과 속성 내의 리스트 항목들을 알파벳 순으로 정렬하세요. 이는 파일의 일관성을 유지하고, 다른 사람이 변경 사항을 리뷰하기 쉽게 만들어줍니다. bpfmt라는 AOSP 내장 포맷팅 도구를 사용하면 이 과정을 자동화할 수 있습니다.
  • defaults 모듈 적극 활용: 두 개 이상의 모듈이 동일한 속성을 공유한다면, 주저하지 말고 cc_defaultsjava_defaults 모듈을 만드세요. 이는 코드 중복을 줄이고, 향후 공통 설정을 변경해야 할 때 한 곳만 수정하면 되도록 만들어줍니다.
  • filegroup으로 소스 그룹화: 논리적으로 연관된 여러 소스 파일을 filegroup으로 묶고, 실제 라이브러리/바이너리 모듈에서 이 filegroup을 참조하게 하세요. 이는 특히 여러 모듈(예: 실제 라이브러리와 테스트 바이너리)이 동일한 소스 파일 집합을 공유할 때 유용합니다.
  • glob 사용 최소화: srcs: ["**/*.cpp"]와 같은 와일드카드(glob) 패턴은 편리하지만, 의도치 않은 파일이 포함되거나 빌드 시스템이 파일 변경을 제대로 감지하지 못하는 문제를 일으킬 수 있습니다. 가능하면 소스 파일 목록을 명시적으로 나열하는 것이 더 안전하고 빌드 안정성에 좋습니다.

5.2. 빌드 문제 디버깅

빌드 오류는 AOSP 개발의 피할 수 없는 부분입니다. 문제 발생 시 효과적으로 원인을 찾는 몇 가지 방법을 알아두는 것이 중요합니다.

  • 오류 메시지 자세히 읽기: 당연한 말처럼 들리지만, 많은 경우 오류 메시지 자체에 문제의 원인과 해결 방법에 대한 힌트가 들어있습니다. Soong은 Make보다 훨씬 더 상세하고 친절한 오류 메시지를 출력하는 경향이 있습니다. 문법 오류, 존재하지 않는 모듈 참조, 의존성 순환 등의 문제를 명확하게 알려줍니다.
  • m -v 사용: 빌드가 실패했을 때, m -v (verbose) 옵션을 붙여 다시 실행해 보세요. 이 옵션은 Ninja가 실행하는 실제 커맨드라인(예: clang++ ...)을 그대로 출력해 줍니다. 이를 통해 컴파일러에 어떤 플래그와 파일이 전달되었는지 정확히 확인할 수 있어, 컴파일 또는 링크 오류의 원인을 파악하는 데 결정적인 단서를 제공합니다.
  • out/ 디렉토리 탐색: 빌드의 중간 산출물과 로그는 모두 out/ 디렉토리에 저장됩니다. 특히 out/soong/ 디렉토리에는 Soong이 생성한 다양한 디버깅 정보가 있으며, out/build-<product>.ninja 파일을 직접 열어보면 모듈 간의 최종적인 의존성 관계를 확인할 수도 있습니다. (물론 이 파일은 기계가 생성한 것이라 가독성이 매우 낮습니다.)

5.3. Android.mk vs. Android.bp 비교 요약

두 시스템의 차이점을 한눈에 파악할 수 있도록 표로 정리하면 다음과 같습니다.

특징 Android.mk (Make) Android.bp (Soong/Blueprint)
패러다임 명령형 (Imperative) 선언형 (Declarative)
문법 GNU Makefile 문법 JSON과 유사한 간결한 문법
로직 처리 조건문, 반복문, 함수 등 복잡한 로직 허용 로직 없음. 단순 속성 정의만 가능
성능 느린 의존성 분석, 단일 스레드 파싱 빠르고 병렬화된 파싱 및 분석
빌드 백엔드 Make 규칙 직접 생성 고성능 Ninja 빌드 파일 생성
오류 처리 오류에 관대하며, 종종 조용한 실패(silent failure) 발생 엄격한 문법 검사, 파싱 시점에 명확한 오류 보고
유지보수성 복잡해지기 쉽고, 가독성이 떨어질 수 있음 항상 단순하고 예측 가능하며, 가독성이 높음
주요 용도 레거시 모듈, 최상위 제품 구성 모든 신규 네이티브 및 자바 모듈

결론: 진화는 계속된다

AOSP 빌드 시스템은 Make의 시대에서 Soong과 Blueprint의 시대로 성공적으로 전환하며 중요한 진화를 이루었습니다. Android.mk의 명령형, 텍스트 기반 접근 방식은 프로젝트 초기에 유연성을 제공했지만, 규모가 커짐에 따라 성능, 복잡성, 안정성 측면에서 한계를 드러냈습니다. 이에 대한 해답으로 등장한 Android.bp의 선언적, 구조적 접근 방식은 빌드 구성을 훨씬 더 빠르고, 안정적이며, 이해하기 쉽게 만들었습니다. 이는 Android 플랫폼 자체의 지속적인 성장을 뒷받침하는 핵심적인 기술적 발전입니다.

현재 AOSP 개발자는 두 시스템이 공존하는 과도기적 환경에서 작업하고 있습니다. 따라서 레거시 코드를 유지보수하기 위해 Android.mk의 작동 방식을 이해하는 동시에, 새로운 개발을 위해 Android.bp의 강력한 기능들을 능숙하게 활용할 수 있는 능력이 모두 요구됩니다. 이 글에서 다룬 내용들이 두 시스템의 차이점을 이해하고, 전환 과정을 탐색하며, 더 효율적으로 빌드 문제를 해결하는 데 든든한 기반이 되기를 바랍니다.

AOSP의 진화는 여기서 멈추지 않을 것입니다. 구글은 내부적으로 Bazel이라는 또 다른 강력한 빌드 시스템을 광범위하게 사용하고 있으며, bp2build와 같은 도구를 통해 Android.bp 파일을 Bazel 빌드 파일로 변환하려는 시도를 계속하고 있습니다. 이는 장기적으로 Android 빌드 시스템을 Bazel 생태계와 통합하려는 더 큰 그림의 일부일 수 있습니다. 이처럼 AOSP 빌드 시스템은 플랫폼의 요구에 맞춰 끊임없이 발전하고 있으며, AOSP 개발자에게는 이러한 변화의 흐름을 지속적으로 학습하고 적응하는 자세가 무엇보다 중요할 것입니다.


0 개의 댓글:

Post a Comment