CMake FetchContent의 target이 중복되는 문제

CMake의 FetchContent는 빌드 과정까지 처리해 주기에 git-submodule 보다 편리한 방식인 것 같다. 다만, version 3.22.1 기준으로 여러 프로젝트 들이 동일한 target이름을 중복해서 선언하는 경우에는 이를 잘 처리 하지 못하고 위와 같은 에러를 뱉고 종료되는 문제가 있다.

-- Found systemd via pkg-config.
CMake Error at build/_deps/grpc-src/CMakeLists.txt:667 (add_custom_target):
  add_custom_target cannot create target "tools" because another target with
  the same name already exists.  The existing target is a custom target
  created in source directory
  "<absolute_path_to_the_build_dir>/_deps/vsomeip-src".
  See documentation for policy CMP0002 for more details.

위의 오류는 COVESA의 vsomeip project와 gRPC를 FetchContent로 구성하는 중에 발생한 것인데, 두 프로젝트 모두 “tools”라는 custom target을 선언하기 때문에 중복으로 오류가 발생한 것이다.

아쉽게도 현재로써는 깔끔하게 해결하는 방법은 없는 것 같고, 두 프로젝트 중 하나의 custom target 이름을 변경해서 중복되지 않도록 해 주는 패치를 작성해서 FetchContent의 PATCH_COMMAND에 인자로 넣어주는 방법으로 회피가 가능하다.

먼저 vsomeip project의 custom target인 tools를 vsomeip_tools로 변경하는 패치를 다음과 같이 작성하고 fix_custom_target.patch로 저장한다.

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 3501e02..0467426 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -654,7 +654,7 @@ add_subdirectory( examples/routingmanagerd )
 endif()
 
 # build tools
-add_custom_target( tools )
+add_custom_target( vsomeip_tools )
 add_subdirectory( tools )
 
 # build examples
-- 
2.34.1

다음으로 FetchConetnet에 PATCH_COMMAND argument를 명시한다.

# VSOME/IP
set(
    fix_custom_target
    git apply ${CMAKE_CURRENT_SOURCE_DIR}/fix_custom_target.patch
)
FetchContent_Declare(
    vsomeip
    GIT_REPOSITORY https://github.com/COVESA/vsomeip.git
    GIT_TAG <GIT_TAG>
    PATCH_COMMAND ${fix_custom_target}
)

FetchContent_MakeAvailable(vsomeip gRPC)

이제 cmake를 실행하면 patch가 적용되어 중복되는 target 이름 문제를 해결할 수 있다.

하지만 이 해결책은 아직 깔끔하진 않은데 그 이유는 처음 cmake를 실행하면 patch가 적용되면서 잘 되지만 두번째로 cmake를 실행 할 때는 이미 적용된 patch 때문에 git apply 명령어가 실패 하기 때문에 매번 patch가 적용된 directory(_deps/vsomeip-src)로 이동해서 git checkout -f를 실행해 주어야 하기 때문이다.

최종버전

git apply의 –reverse –check 옵션을 사용하면 패치가 적용되어 있는지 여부를 검사할 수 있으니 이 것을 이용해서 다음과 같은 간단한 shell script를 만들고 apply_patch_if_not.sh로 저장한다.

#!/usr/bin/env sh
# apply_patch_if_not.sh
# Apply given patch if not already applied.
#
#                         - Sep 2024, litcoder

if [ -z $1 ]; then
  echo "Usage: $0 <patch file path>"
  exit 1
fi

# Check if the patch was already applied.
git apply --reverse --check $1
if [ $? -eq 0 ]; then
  echo "The patch already applied, skip."
  exit 0
else
  # Apply the patch
  git apply $1
fi

이제 앞에서 작성한 FetchContent_Declare가 이 script를 부르도록 변경한다.

# VSOME/IP
set(
    fix_custom_target
    apply_patch_if_not.sh ${CMAKE_CURRENT_SOURCE_DIR}/fix_custom_target.patch
)
FetchContent_Declare(
    vsomeip
    GIT_REPOSITORY https://github.com/COVESA/vsomeip.git
    GIT_TAG <GIT_TAG>
    PATCH_COMMAND ${fix_custom_target}
)

FetchContent_MakeAvailable(vsomeip gRPC)

C++에서 오버라이드된 함수가 안불러져요

Data라고 하는 정보를 저장하는 클래스들의 기초 클래스가 있다고 해보자. Data의 type에 따라 로딩 하는 방법이 서로 달라지기 때문에 loadData()라고 하는 가상함수를 하나 정의해 두고, 상속받는 클래스들은 자기가 loading하는 방법을 정의하도록 구성한다.

class Data {
public:
  Data(int v) { loadData(v); }
  void dispValue() { cout << "Value is " << _val << endl; }

protected:
  // 상속받는 클래스에서 구현해야 함.
  // 호출되면 안됨.
  virtual bool loadData(int v) {
    cout << "Default loader, do not call me!" << endl;
    return false;
  }
  int _val = 0;
};

Data에서 상속받는 DerivedData 클래스는 자기 버전의 loadData()를 오버라이드해서 구현한다.

class DerivedData : public Data {
public:
  DerivedData(int v) : Data(v) {}

protected:
  bool loadData(int v) override {
    cout << "DerivedData1::loadData()" << endl;
    _val = v;
    return true;
  }
};

DerivedData를 생성하고 100으로 초기화해서 결과를 보여주도록 main()을 작성한다.

int main(void) {
  auto d = DerivedData(100);
  d.dispValue();
}

이제 컴파일하고 실행해보자.

$ g++ ./main.cpp 
$ ./a.out 
Default loader, do not call me!
Value is 0

예상과 달리 DerivedData의 loadData()가 아닌 Data의 loadData()가 불리고 있다. 분명히 DevriedData의 인스턴스를 만들었는데도 오버라이드한 멤버함수가 동작하지 않는 이유는 무엇일까?

이 예제에서는 Data에서 상속 받은 DerivedData 클래스의 생성자가 호출되기 전에 Data의 생성자가 먼저 호출된다. Data클래스의 생성자에서는 가상함수로 정의된 loadData()의 오버라이드 구조를 따라 내려가지 않고 현재 클래스인 Data에 정의되어 있는 loadData() 를 바인딩한다. 아직 DerrivedData 클래스가 초기화 되지 않은 시점이기 때문에 기초 클래스의 생성자에서 파생 클래스의 오버라이드 멤버를 바인딩하는 것은 아직 알지 못하는 정보를 참조해야 하는 일이되기 때문이다.

Scott Meyers의 명서인 Effective C++에서도 이 내용을 언급하고 있었다. “Item 9: Never call virtual functions during construction or destruction”(생성자나 소멸자에서 가상함수를 절대 호출하지 말것) 아마도 다시 정독할 때가 됐나 보다.

해결책으로 제시되는 것은 가상 함수를 부르는 대신 관련한 정보를 파라미터로 받아서 처리하는 것이다. 위의 경우 예를 들면 loadData()를 일반 멤버함수로 작성하고 DataLoader를 파라미터로 넘겨주는 방법을 고려해 볼수 있다.

#include <iostream>
using namespace std;

class DataLoader {
public:
  DataLoader(int v) { _val = v; }
  int getValue() { return _val; }

private:
  int _val;
};

class Data {
public:
  Data(DataLoader &ldr) { loadData(ldr); }
  void dispValue() { cout << "Value is " << _val << endl; }

protected:
  bool loadData(DataLoader &ldr) {
    cout << "Load data " << ldr.getValue() << endl;
    _val = ldr.getValue();
    return true;
  }
  int _val = 0;
};

class DerivedData : public Data {
public:
  DerivedData(DataLoader &ldr) : Data(ldr) {}
};

int main(void) {
  auto l = DataLoader(100);
  auto d = DerivedData(l);
  d.dispValue();
}
$ ./a.out 
Load data 100
Value is 100

VSCode에서 GTest 테스트 항목 표시하기

VSCode의 Test Explorer에 test case들을 표시하면 귀찮은 파라미터 입력 없이도 특정 테스트케이스를 실행하거나 디버그할 수 있어서 여러모로 편리한 점이 많다. 하지만 GTest로 작성한 C++ 테스트케이스들은 자동으로 discover되지 않아서 약간의 설정을 해주어야 한다. 복잡한 것은 아니고 아래와 같이 GTest에서 제공하는 CMake 함수인 gtest_discover_tests()를 이용하면 된다.

# Allow ctest to discover unittests.
enable_testing()
include(GoogleTest)
gtest_discover_tests(
    ${TEST_EXE}
    WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
)

gtest_discover_tests() 자세한 내용은 GoogleTest CMake 문서에서 찾을 수 있으니 참고 하도록 하자. 여기에서는 target을 실행파일의 이름으로, working directory를 build로 설정해 주었다. 전체 코드는 아래와 같다.

부동산 서바이벌 – HUG 임대인 보증보험 전세금 돌려받기

9월 6일: 기간 종료 통보

전세계약을 연장하지 않겠다는 문자 메세지를 보내자 집주인으로 부터 전화가 와서 사정이 좋지 않으니 전세계약 중도해지 합의서를 써주겠으니 그걸로 HUG에 보증금 신청을 하라고 한다.

9월 8일

이리저리 합의서 양식과 필요한 서류를 알아보고 나서 집주인에게 어디로 보내주면 사인해서 보내 줄래 물었더니 자기가 필요한 서류와 양식 정보를 이미 모두 가지고 있으니 내가 받을 곳을 말하라고 한다. 이 사람 프로다.

9월 11일

지난 주 금요일에 임대인이 보냈다던 중도계약해지 합의서가 등기로 도착했다. 미리 도장이 찍힌 서류들과 함께 선처 탄원서가 들어있었다. 그냥 잠수 타버려서 연락이 안된다던 다른 임대인 모씨와는 달리 서류도 잘 준비 해주고 해서 당장이라도 사인해서 보내주고 싶긴했지만 일단 잘처리 된 다음으로 미루고 일단 임차권등기명령 신청부터 해야한다.

9월 12일(D+0): 임차권 등기명령 신청

관할지법인 남부지방법원으로 임차권등기명령을 신청하러 반차를 냈다. 필요한 서류는 다음과 같다.

부동산 등기부등본(당해, 발급용)
임대차 계약서 사본
주민등록초본
임대차 종료 입증서류(내용증명 배달증명, 문자메세지 혹은 임대차 해지 합의서)

들고 간 서류들을 가지고 신청서를 작성하고, 송달료, 수수료, 인지세 등등 으로 대략 4만 3천원 정도가 들었다. 카드를 사용할 수는 있으나 추가 서류를 작성하기가 번거로울 것이라는 안내가 있어서 법원 내에 있는 은행에서 현금으로 지불 하고 영수증을 포함한 서류를 받아들고 다시 담당자를 찾아갔다.

다시 번호표를 뽑고 기다린 끝에 만난 담당자는 이리저리 꼼꼼한 첨삭지도를 한 다음 접수해 주면서 기간은 4주 정도 걸릴 것이라고 한다.

9월 27일(D+15)

아직 우편송달은 오지 않았지만 왠만큼 기다렸다 싶어서 안내 받았던 대로 법원의 홈페이지의 ‘나의 사건 검색’ 항목에서 사건 번호를 넣어 봤더니 엊그제 일자로 인용 결정이 되었고 아직 우편 송달은 완료되지 않았다는 내용을 검색할 수 있었다. 인터넷으로 등기를 확인해 봤더니 “교합대기”로 나오는 것으로 봐서 아직 등기가 모두 완료되지는 않은 모양이다. 신청으로 부터 대략 2주 정도 지났는데 오늘 부터는 추석기간이니 좀 더 시간을 두고 봐야겠다.

10월 5일(D+23): 임차권 등기 완료

우편으로 등기내역이 도착했다. 등기사항을 조회해보니 주택임차권이 등기된 내역을 확인할 수 있었다. 이제 드디어 보증보험을 신청 할 수 있게 되었다.

Screenshot

10월10일(D+28)

전경련에 있는 도시보증공사 서부지사를 찾아갔다. 지사별로 관할하는 임대인이 다른 모양인데, 입구에서 집주인의 주민번호를 이를 확인해 주신다. 주섬주섬 꺼내드는 서류에 적힌 임대인의 이름을 쓱 보더니 “(임대인이) OOO 에요? 그럼 여기 맞아요” 하신다. 이 지사 보증사고 1등 이랜다.

대기시간이 너무 길어서 오픈런하는게 낫다는 이야기를 들었는데, 오후 2시쯤에 방문했더니 앞에 15명 있었다. 처음에는 금방 빠지겠거니 했는데 대기 예상 대기 시간은 무려 4시간 30분 이라고 나온다. 다들 이 긴 예상시간을 보고 다른 일을 하러 갔던 것인지 대기 순번이 없어서 건너 뛰는 경우가 많았다. 그 와중에 자기 번호가 이미 넘어 갔으니 먼저 처리해 달라는 사람들이 몇몇 있었고 결국 2시간 정도가 걸려서 내 순서가 되었다.

적은 돈이 아니기에 예상은 했지만, 나름대로 꼼꼼히 준비해 간 서류였는데도 여러건의 빨간펜을 받았다.

  1. 보증 사고 접수는 계약 해지로 부터 2달 이후부터 가능하다. 즉, 나는 9월 11일자로 해지 계약을 했으니 11월 12월 부터 접수가 가능하다.
  2. 임대인의 인감 증명서는 관계 없으나 임차인의 인감 증명서(내꺼)는 등기사항이 포함된 접수일 1개월 이내의 것이 필요하다. 4부를 제출 해야 한다.
  3. 주민등록 초본은 임차권 등기 이후의 것을 제출해야 하며 이 것 역시 접수일 1개월 이내의 것이어야 한다.
  4. 부동산 등기부 등본은 법원의 등기사항이 기재되어 있는 것이어야 한다.

심사일정은 빠르면 한 달 정도 소요되고 새로운 주택을 구할 일정이 충분히 주어지니 계약서 상의 날짜에는 구애받을 필요가 없다는 설명을 들었다.

11월 13일(D+62): 전세 보증보험 신청

오늘은 오전 반차를 내고 오픈런 할 생각이었는데 생각해 보니 지난 번에 필요하다던 서류를 준비 못했다. 서둘러서 동사무소에 들러 인감증명, 주민등록 초본, 등기부 등본을 챙기고 HUG로 달려서 9시 40분 무렵에 도착했는데 대기순번을 등록하고 보니 앞에 10여명이 이미 있었고 대기 예상시간은 1시간 30분 이라고 했다. 시간이 꽤 지나고 앞에 5명이 남았는데 여전히 대기 예상시간은 1시간 30분이라고 한다. 인원 수 당 시간으로 대기 예상시간을 계산하는 것이 아닌가 보다. 옆에서 들리기로는 한 사람당 대략 30분 걸린다고 하니, 1시간 30분은 표시되는 가장 큰 값인지도 모르겠다.

실제로는 대략 30분 정도가 걸려서 내 차례가 되었다. 자신 만만하게 들이민 서류를 유심히 보던 다른 접수원은 일단은 접수하고 미비한 서류를 추가로 구비해서 보내주면 심사 절차를 시작하겠다고 한다.

보완 해야 할 서류 목록을 받았다.

  • 주택 임차권등기명령 법원 결정문의 원본: 사본은 안되고 원본이 있어야 함.
  • 신분증 사본: 누락되었으므로 추가해야 함. 양면 복사.
  • 전세금 입금 했던 통장 내역: 받는 사람의 이름이 나와 있어야 함.
  • 주민등록 등본: 원래는 등본/초본 중 하나로도 되지만 이왕 미비 서류 보완하는 김에 추가하라고 함.

다른 건 다 마련하면 되겠지만 법원 결정문 원본 서류를 찾을 래야 찾을 수가 없었다. 분명 소중히 모셔 둔답시고 어딘가에 고이 보관한 기억이 있는데, 다른 서류들과 함께 둔 뭉치에서는 도무지 찾을 수가 없었다. 어떡하냐 법원으로 달려야지 뭐…

법원에서 발급해 준 결정문 정본은 접수하고 인지세 1000원을 납부하자 곧바로 발급되었다. 보완 서류들을 우편으로 보낼 수도 있지만 4시 이전까지는 직접 방문해서 제출할 수도 있다. 다행히 보완 서류는 번호표를 뽑지 않고 빠르게 접수 할 수 있었다.

11월 14일 (D+63)

HUG로 부터 담당자가 배정되었다는 문자 메세지를 받았다.

12월 11일 (D+90): 심사승인!

5주 정도를 기다린 끝에 드디어 심사 승인되었다는 문자 메세지가 왔다.

이제 본격적으로 이사 할 집을 알아 봐야겠다.

12월 19일

지난주에 부동산에서 추천해 주었던 다섯채 중에 네채를 둘러 보고 이사갈 집을 결정했다.

1월 6일: 이사갈 집 계약

이사 갈 집에 대한 전세 계약서를 작성하고 HUG에서 받은 문자에 적혀 있는 담당자의 이메일로 “2월 29일에 이사예정이니 명도확인 절차에 대한 안내를 부탁드립니다.”라고 메일을 보냈다.

안심전세 앱에 있는 악성임대인 조회에서 예전 집주인을 검색해봤더니 반환 채무가 65억이 넘는다. 크게 땡기셨고만…

2월 27일

관리회사에 연락 해서 관리비 완납 증명서를 받고 가스앱에서 이사예약을 신청했다.

2월 28일

이사예정일 하루 전이 되자 HUG에서 보증금이 입금될 예정이라는 카톡이 왔다. 저녁 6시 이후에 안심전세 앱에서 “명도증빙자료제출”을 누르니 자료를 올리는 화면이 나왔다.

2월 29일: 이사날

전기와 수도 요금은 모두 당일에 정산하고 영수증을 받는 것이 가능했는데 디지털 전기계량기의 전력사용량을 보는 방법이 복잡해서 조금 헤맸다.

인터넷도 끊겨있어서 4G잡고 천신만고 끝에 관련서류들을 모두 올렸는데, 담당자에게서 전화가 와서 “전산이 고장나서” 서류를 확인 할 수 없다고 한다. 뿐만 아니라 보증금 반환도 지연되고 있다고. 아뉘! 부동산, 집주인, 이전 살던 사람, 나까지 기다리고 있는 사람들이 줄줄이인데 지금…

일단 이메일로 보완 할 서류가 있는지 확인해 주겠다고 해서 앱에 올렸던 내용을 담당자에게 모두 보내고 기다리자 확약서가 빠져있었다면서 양식을 보내 줄테니 자필로 써서 보내라고 했다. 이건 제출 서류에서 못봐서 생각지도 못했는데 “내가 모든 요금을 정산했고 이후에 문제가 있으면 책임지겠다.” 이런 내용을 쓰고 자필 서명을 했다.

그리고 얼마간 기다리고 있으니 시스템이 복구 되었다며 보증금이 들어 왔다. 최악의 경우에는 모두가 오후까지 기다리거나 이사가 나가리 될 수도 있었는데 어찌저찌 잔금을 치르고 이사를 마칠 수 있게 되었다.

참조

보증이행 안내 https://www.khug.or.kr/hug/web/ge/er/geer001100.jsp

GitHub release에 바이너리 첨부 자동화

GitHub에서 release를 생성하면 source code의 snapshot이 zip과 tar.gz로 저장된다. 여기에 추가해서 컴파일된 결과가 자동으로 추가하도록 한다면, 간단히 source code와 연계된 바이너리도 함께 배포할 수 있을 것이다.

이 글에서는 안드로이드 프로젝트를 가정해서 release를 생성할 때 안드로이드 APK를 빌드하고 source code와 함께 배포하는 간단한 workflow를 설명한다.

전체 코드

Event trigger

on:
  release:
    types: [published]

Release에서만 동작하므로 event trigger는 release – published이다. 이 event는 web상에서 새로운 release package를 publish 할때 trigger 된다.

환경변수

env:
  TAG: ${{ github.ref_name }}
  ASSET_FILE_PATH: "./prebuilt-${{ github.ref_name }}.zip"

Release package를 생성할 때 넣는 version의 이름은 github.ref_name으로 참조 된다. 첨부되는 파일의 이름은 prebuilt-<version_tag>.zip으로 설정한다. 참고로 GitHub에서는 release에 추가되는 소스코드외의 파일들을 “Asset”이라 부른다.

빌드 수행 및 Asset 생성

    # Checkout source code and build Android APKs.
    - name: Checkout
      uses: actions/checkout@v3
    - name: Setup JDK 17
      uses: actions/setup-java@v3
      with:
        java-version: '17'
        distribution: 'temurin'
        cache: gradle
    - name: Build Android APK and zip
      run: |
        sh ./gradlew assemble
        zip ${{ env.ASSET_FILE_PATH }} \
          ./app/build/outputs/apk/debug/app-debug.apk \
          ./app/build/outputs/apk/release/app-release*.apk

Android build를 위한 JAVA를 설정하고 Gradle의 assemble target을 설정하여 APK file에 대한 빌드를 수행한다. Assemble은 debug와 release용 두개의 apk를 성성하므로 이들을 하나의 zip파일로 만들어서 prebuilt-<version_tag>.zip에 추가한다.

Release에 Asset 추가

Release에 asset을 추가해주는 GitHub action은 보이질 않는다. 그래서 GitHub script를 이용해서 release에 asset을 추가하는 동작을 다음과 같이 정의했다. 이는 두 개의 동작으로 이루어지는데 하나는 주어진 tag로 release 정보를 찾아오는 것이고, 다른 하나는 가져온 release 정보에 파일을 업로드하는 것이다.

동작 1. Tag로 release 가져오기

GitHub script project의 README.md에 따르면 GitHub script상에서 github객체는 사전인증된(pre-authenticated) Octokit client라고 한다. 따라서 새롭게 instance를 만들지 않고 바로 Octokit의 API를 사용할 수 있다. getReleaseByTag()에 tag를 넘겨주면 통해 release에 대한 객체가 반환된다. 그 중에 release를 구분하기 위한 ID만 사용한다.

    // Get a release for given tag.
    const release = await github.rest.repos.getReleaseByTag({
       owner: context.repo.owner,
       repo: context.repo.repo,
       tag: process.env.TAG
    });
    const release_id = release.data.id;
    console.log("Release id for the tag " + process.env.TAG + ": " + release_id);

동작 2. Release에 파일 업로드 하기

앞의 과정에서 만든 업로드할 zip파일과 release를 구분할 ID 정보를 uploadReleaseAsset()에 넘겨 주면 해당 파일이 asset으로 등록된다.

    // Upload release assets.
    const fs = require("fs");
    const filename = process.env.ASSET_FILE_PATH.replace(/^.*[\\/]/, "");
    var uploaded = await github.rest.repos.uploadReleaseAsset({
       owner: context.repo.owner,
       repo: context.repo.repo,
       release_id: release_id,
       name: filename,
       data: await fs.readFileSync(process.env.ASSET_FILE_PATH),
    });
    console.log(
       process.env.ASSET_FILE_PATH
       + " has been uploaded as " + filename
       + " to the release " + process.env.TAG);

주의: upload 동작은 release를 수정하는 것이므로 workflow가 repository에 대한 write permission이 있어야 하므로 settings 항목에서 에서 write permission이 허가되어 있는지 확인한다. 이 부분이 안되어 있다면 403 error가 날 것이다.

Release workflow 실행

GitHub repo의 Code 탭에서 Tags -> Releases -> Tag -> Draft a new release를 선택한 후 Choose a tag를 눌러서 나오는 입력창에 새로 생성할 version tag를 입력해 Create new tag를 선택한 다음 Publish release 버튼을 누른다.

이 때 만들어지는 release에는 소스코드만 들어 있지만, 정상적으로 동작했다면 Actions tab에 release workflow가 등록되어 수행되는 것이 보일 것이다. Workflow가 정상 종료된 후 해당 release로 다시 가보면 asset이 등록되어 있는 것을 볼 수 있다.