본문 바로가기

building system/bazel

새로운 의존성 설계 bzlMod 기본 예제 구현하기

반응형

개요

bazel 6.3 이후로 bazel 개발팀은 새로운 의존성 설계 모델인 bzlMod 사용을 권장하고 있고, 가까운 미래에 기존 방식을 완전히 대체할 예정이다. 이전 게시글, 모두 바로 이전 설계 모델 기반으로 설명한 내용이다.

이번 예제를 통해, 새로운 의존성 설계 모델인 bzlMod를 이해하도록 해보자.

풀어야 할 과제

전체 폴더 구조는 아래와 같다. 일단 기존 방식과 다른 점은 빌드 시스템의 맨 상단에 있어야 할 WORKSPACE 파일이 없다. 대신 MODULE.bazel 파일이 기존 WORKSPACE 파일의 역할을 대신한다. 단순히 이름만 바뀐 것이 아니라, 내부에 사용할 수 있는 rule 규약도 다르다.

그리고 무엇보다 중요한 부분은 bazel-central-registry(BCR)라는 repository를 제공한다. bazel-central-registry의 일부 파일 모듈을 참고하면, 의존되어 있는 모듈이 자동적으로 컴파일에 참여하게 된다.

자주 사용하는 라이브러리를 bazel-central-registry를 통해 제공한다. .bazelrcbzlMod로 작동하는데 필요한 일부 옵션을 추가하는 용도로 사용한다.

│  .bazelrc
│  MODULE.bazel
│
└─src
        BUILD.bazel
        c.jpg
        ctz_clz.c
        main.cc

일단 src 폴더에서 풀어야 할 내용을 이해하자.

main.cc

다소 장황한 코딩이다. 1시간 가량 코딩을 작성했는데, 전체 내용은 libjpeg_turbo를 기반으로 c.jpg 파일을 읽어 상하반전한 out.jpg를 생성한다. 여기서 특별한 점은 libjpeg_turbo 컴파일이다. libjpeg_turbonasm 프로그램을 기반으로 컴파일되는 환경에 맞게 필요한 어셈블러 파일을 선택하고 컴파일한다. nasm 파일 자체도 최근 버전을 기반으로 컴파일한다. 한마디로 libjpeg_turbo 컴파일이 난이도가 높다. 일반적으로 온전한 BUILD 파일을 만드는 조금 익숙한 사람도 꽤나 시간이 소요될 것이다.

bazel-central-registry에 자주 사용하는 라이브러리를 이미 컴파일할 수 있도록 중앙에 모아주고 있다. 필요한 라이브러리 정보를 코딩에 추가하면, 딱 필요한 라이브러리만 가져와 빌드에 참여한다. 아래 코딩이 조금 장황하지만, 빌드 과정도 내부적으로 복잡하다.

#include <stdio.h>
#include <jpeglib.h>
#include <iostream>
#include <fstream>
#include <memory>
#include <vector>

template<class T>
using unique_ptr_with_deleter = std::unique_ptr<T, void(*)(std::remove_extent_t<T>*)>;

void my_error_exit(j_common_ptr) {
    throw "jpeg decode error";
}

struct Result {
    int width{};
    int height{};
    int components{ 4 };
    J_COLOR_SPACE color_space{ JCS_EXT_BGRA };
    int quality{ 100 };
    std::vector<unsigned char> raw_data{};
};

bool reverse_write_jpeg(std::string const& path, Result const& result) try {
    FILE* fp = std::fopen(path.c_str(), "wb");
    if (!fp) return false;

    auto close_fp = unique_ptr_with_deleter<FILE>{
        fp,
        [](FILE* obj) {
            if (!obj) return;
            fclose(obj);
        }
    };

    struct jpeg_compress_struct cinfo {};
    struct jpeg_error_mgr pub {};
    cinfo.err = jpeg_std_error(&pub);
    pub.error_exit = my_error_exit;

    jpeg_create_compress(&cinfo);
    auto destroy_com = unique_ptr_with_deleter<jpeg_compress_struct>{
        &cinfo,
        [](jpeg_compress_struct* obj) {
            if (!obj) return;
            jpeg_destroy_compress(obj);
        }
    };

    jpeg_stdio_dest(&cinfo, fp);

    cinfo.image_width = result.width;
    cinfo.image_height = result.height;
    cinfo.input_components = result.components;
    cinfo.in_color_space = result.color_space;

    jpeg_set_defaults(&cinfo);
    jpeg_set_quality(&cinfo, result.quality, TRUE/* limit to baseline-JPEG values */);

    jpeg_start_compress(&cinfo, TRUE);
    auto finish_com = unique_ptr_with_deleter<jpeg_compress_struct>{
        &cinfo,
        [](jpeg_compress_struct* obj) {
            if (!obj) return;
            jpeg_finish_compress(obj);
        }
    };

    auto row_stride = result.width * result.components;
    while (cinfo.next_scanline < cinfo.image_height) {
        unsigned char* raw[]{ (unsigned char*)&result.raw_data[(cinfo.image_height - 1 - cinfo.next_scanline) * row_stride] };
        jpeg_write_scanlines(&cinfo, raw, 1);
    }
    return true;
}
catch (...) {
    return false;
}

bool read_jpeg(std::string const& path, Result& result) try {
    std::ifstream fp{ path, std::ios::binary };
    if (!fp) return false;

    auto file_size = fp.seekg(0, std::ios::end).tellg();
    fp.seekg(0);

    std::vector<unsigned char> file_data((std::size_t)file_size);
    fp.read((char*)file_data.data(), file_size);

    struct jpeg_decompress_struct cinfo {};
    struct jpeg_error_mgr pub {};
    cinfo.err = jpeg_std_error(&pub);
    pub.error_exit = my_error_exit;

    jpeg_create_decompress(&cinfo);

    auto destroy_decom = unique_ptr_with_deleter<jpeg_decompress_struct>{
        &cinfo,
        [](jpeg_decompress_struct* obj) {
            if (!obj) return;
            jpeg_destroy_decompress(obj);
        }
    };

    jpeg_mem_src(&cinfo, file_data.data(), file_data.size());

    jpeg_read_header(&cinfo, TRUE);
    cinfo.out_color_space = result.color_space;
    jpeg_start_decompress(&cinfo);

    auto finish_decom = unique_ptr_with_deleter<jpeg_decompress_struct>{
        &cinfo,
        [](jpeg_decompress_struct* obj) {
            if (!obj) return;
            jpeg_finish_decompress(obj);
        }
    };

    result.width = cinfo.output_width;
    result.height = cinfo.output_height;
    auto row_stride = result.width * result.components;

    result.raw_data.resize(row_stride * result.height);

    while (cinfo.output_scanline < result.height) {
        unsigned char* raw[]{ &result.raw_data[row_stride * cinfo.output_scanline] };
        jpeg_read_scanlines(&cinfo, raw, 1);
    }
    return true;
}
catch (...) {
    return false;
}

int main() {
    do {
        Result read_data{};
        bool bread = read_jpeg("c.jpg", read_data);
        if (!bread) break;
        reverse_write_jpeg("out.jpg", read_data);
        std::cout << "success\n";
        return 0;
    } while (false);
    std::cout << "fail\n";
    return 0;
}

아래 코딩은 jpeg 라이브러리 컴파일 과정에서 컴파일 환경에 따라 다른 옵션이 추가하는데, BUILD 파일을 확인해보니, GCC 컴파일러에 적용되어 할 옵션이 MSVC 옵션까지 적용된 부분을 발견했다.

웹에서 필요한 소스를 다운받아 일부 항목을 수정함으로써 GCC 내부에 구현된 소스 역활을 하도록 해주었다.

여기서 핵심은 파일 확장자가 .c 라서 C 언어 규약을 따르고 기존 static function를 extern function으로 변경해서, C 언어에서 만든 함수 이름이 외부에 노출되도록 했다. 딱 여기에서 필요한 기능이다.

#ifdef _MSC_VER
#include <intrin.h>


extern inline int __builtin_ctz(unsigned x){
    return (int)_tzcnt_u32(x);
}

extern inline int __builtin_ctzll(unsigned long long x){
#ifdef _WIN64
    return (int)_tzcnt_u64(x);
#else
    return !!unsigned(x) ? __builtin_ctz((unsigned)x) : 32 + __builtin_ctz((unsigned)(x >> 32));
#endif
}

extern inline int __builtin_ctzl(unsigned long x){
    return sizeof(x) == 8 ? __builtin_ctzll(x) : __builtin_ctz((unsigned)x);
}

extern inline int __builtin_clz(unsigned x){
    return (int)_lzcnt_u32(x);
}

extern inline int __builtin_clzll(unsigned long long x){
#ifdef _WIN64
    return (int)_lzcnt_u64(x);
#else
    return !!unsigned(x >> 32) ? __builtin_clz((unsigned)(x >> 32)) : 32 + __builtin_clz((unsigned)x);
#endif
}

extern inline int __builtin_clzl(unsigned long x){
    return sizeof(x) == 8 ? __builtin_clzll(x) : __builtin_clz((unsigned)x);
}
#endif

MODULE.bazel

MODULE.bazel 파일은 한줄 만 적어주면 된다. 우리가 필요한 libjpeg_turbo 라이브러리과 사용할 버전이다.
https://github.com/bazelbuild/bazel-central-registry/ 에서 modules 폴더 이름을 name으로 지정하고, 해당 폴더 내부에 있는 버전 폴더 이름을 적어주면 된다. 버전 정보 폴더 내부에 presubmit.yml 파일을 열어보면, build_targets 정보를 확인할 수 있는데, 이 부분을 BUILDdeps 항목에 추가하면 된다.

Bazel Central Registry를 줄여서 BCR infrastructure 라고 부른다.

MODULE.bazel에 버전 정보가 추가되면서, 사용하는 서로 다른 라이브러리가 서로 다른 버전을 참고하면, 최신 버전으로 자동 선택한다. 다른 라이브러리가 버전을 하면, 자연스럽게 버전이 가능해진다. 새로운 라이브러리에 필요한 수정 작업을 지정해야 한다면, 별도 제어 명령어로 특정 버전 라이브러리를 지정할 수 있다.

bazel_dep(name = "libjpeg_turbo", version = "2.1.91")

libjpeg_turbo/2.1.91/presubmit.yml는 아래와 같다. 지원하는 플랫폼에 windows가 포함되어 있고, @libjpeg_turbo//:jpeg로 사용할 수 있다.

아래 링크를 통해 사용 가능한 라이브러리 목록을 알아낼 수 있다.
사용 가능한 라이브러리 확인 사이트

matrix:
  platform:
  - debian10
  - ubuntu2004
  - macos
  - windows
tasks:
  verify_targets:
    name: Verify build targets
    platform: ${{ platform }}
    build_targets:
    - '@libjpeg_turbo//:jpeg'

.bazelrc로 bzlMod 옵션 켜기

common 뒤에 모든 컴파일 옵션에 추가할 옵션이 온다.

common --enable_bzlmod

src/BUILD.bazel

복잡한 빌드 파일을 구축하지 않고, 너무나 간단히 아래처럼 적어주면 끝이다.

cc_binary(
    name = "main",
    srcs = ["main.cc","ctz_clz.c"],
    deps = ["@libjpeg_turbo//:jpeg"]
)

실행 결과 보기

프로젝트 파일 첨부

bzlMod.zip
3.5 MB

728x90
반응형