天狗会議録
Posts Pages About
Conan2のパッケージにパッチを当てる

問題

Conan2はC/C++向け依存パッケージマネージャです。 Artifactoryと相性が良く、手軽にキャッシュをリモートに保存できます。 同類にvcpkgがあります。

libjxl/0.11.1というパッケージをDebugビルドしたところ、コンパイルエラーが発生しました。 既に#4079で修正されてmasterブランチにはマージされているものの、リリースされていないためビルドできないという状況です。 これではプロジェクトをDebugビルドできません。 そのため、こちらでパッチを当てる必要があります。

解決

プロジェクトからパッケージへパッチを当てることはできません。 つまり、パッケージをいじる必要があります。 キャッシュにパッチを当てても良いですが、それではCIのような新環境でのセットアップが億劫です。 仕方がないので新しくパッケージを作成してパッチを当てます。

簡潔に手順を記すと次のようになります:

  1. パッチファイルを作成
  2. パッケージの次のファイルを拝借:
    • conanfile.py
    • conandata.yml
    • conan_deps.cmake
  3. conanfile.pyを変更:
    • 名前を変更
    • バージョンを指定
    • パッチファイルをエクスポート
    • パッチを適応

解決 (具体例)

以降、具体的にlibjxlにパッチを当ててみます。

まず、パッチファイルを作ります。 名前をfix.patchとしましょう。 改行コードがCRLFだとパースエラーが起こることに注意します。

--- a/lib/jxl/dec_modular.cc
+++ b/lib/jxl/dec_modular.cc
@@ -155,21 +155,21 @@ Status int_to_float(const pixel_type* const JXL_RESTRICT row_in,
 #if JXL_DEBUG_V_LEVEL >= 1
 std::string ModularStreamId::DebugString() const {
   std::ostringstream os;
-  os << (kind == GlobalData   ? "ModularGlobal"
-         : kind == VarDCTDC   ? "VarDCTDC"
-         : kind == ModularDC  ? "ModularDC"
-         : kind == ACMetadata ? "ACMeta"
-         : kind == QuantTable ? "QuantTable"
-         : kind == ModularAC  ? "ModularAC"
+  os << (kind == Kind::GlobalData   ? "ModularGlobal"
+         : kind == Kind::VarDCTDC   ? "VarDCTDC"
+         : kind == Kind::ModularDC  ? "ModularDC"
+         : kind == Kind::ACMetadata ? "ACMeta"
+         : kind == Kind::QuantTable ? "QuantTable"
+         : kind == Kind::ModularAC  ? "ModularAC"
                               : "");
-  if (kind == VarDCTDC || kind == ModularDC || kind == ACMetadata ||
-      kind == ModularAC) {
+  if (kind == Kind::VarDCTDC || kind == Kind::ModularDC || kind == Kind::ACMetadata ||
+      kind == Kind::ModularAC) {
     os << " group " << group_id;
   }
-  if (kind == ModularAC) {
+  if (kind == Kind::ModularAC) {
     os << " pass " << pass_id;
   }
-  if (kind == QuantTable) {
+  if (kind == Kind::QuantTable) {
     os << " " << quant_table_id;
   }
   return os.str();

キャッシュからconanfile.pyを拝借します。 fix.patchと同じディレクトリに置いておきます。

conanfile.py
import os

from conan import ConanFile
from conan.tools.build import cross_building, stdcpp_library, check_min_cppstd
from conan.tools.cmake import CMake, CMakeDeps, CMakeToolchain, cmake_layout
from conan.tools.env import VirtualBuildEnv
from conan.tools.files import copy, get, rmdir, save, rm, replace_in_file
from conan.tools.gnu import PkgConfigDeps
from conan.tools.microsoft import is_msvc
from conan.tools.scm import Version

required_conan_version = ">=2"


class LibjxlConan(ConanFile):
    name = "libjxl"
    description = "JPEG XL image format reference implementation"
    license = "BSD-3-Clause"
    url = "https://github.com/conan-io/conan-center-index"
    homepage = "https://github.com/libjxl/libjxl"
    topics = ("image", "jpeg-xl", "jxl", "jpeg")

    package_type = "library"
    settings = "os", "arch", "compiler", "build_type"
    options = {
        "shared": [True, False],
        "fPIC": [True, False],
        "avx512": [True, False],
        "avx512_spr": [True, False],
        "avx512_zen4": [True, False],
        "with_tcmalloc": [True, False],
    }
    default_options = {
        "shared": False,
        "fPIC": True,
        "avx512": False,
        "avx512_spr": False,
        "avx512_zen4": False,
        "with_tcmalloc": False,
    }

    def export_sources(self):
        copy(self, "conan_deps.cmake", self.recipe_folder, os.path.join(self.export_sources_folder, "src"))

    def config_options(self):
        if self.settings.os == "Windows":
            del self.options.fPIC
        if self.settings.arch not in ["x86", "x86_64"] or Version(self.version) < "0.9":
            del self.options.avx512
            del self.options.avx512_spr
            del self.options.avx512_zen4

    def configure(self):
        if self.options.shared:
            self.options.rm_safe("fPIC")

    def layout(self):
        cmake_layout(self, src_folder="src")

    def requirements(self):
        self.requires("brotli/1.1.0")
        self.requires("highway/1.1.0")
        self.requires("lcms/2.16")
        if self.options.with_tcmalloc:
            self.requires("gperftools/2.15")

    def validate(self):
        if self.settings.compiler.cppstd:
            check_min_cppstd(self, 11)

    def build_requirements(self):
        # Require newer CMake, which allows INCLUDE_DIRECTORIES to be set on INTERFACE targets
        # Also, v0.9+ require CMake 3.16
        self.tool_requires("cmake/[>=3.19 <4]")

    def source(self):
        get(self, **self.conan_data["sources"][self.version], strip_root=True)

    def generate(self):
        VirtualBuildEnv(self).generate()

        tc = CMakeToolchain(self)
        tc.variables["CMAKE_PROJECT_LIBJXL_INCLUDE"] = "conan_deps.cmake"
        tc.variables["BUILD_TESTING"] = False
        tc.variables["JPEGXL_STATIC"] = False
        tc.variables["JPEGXL_BUNDLE_LIBPNG"] = False
        tc.variables["JPEGXL_ENABLE_BENCHMARK"] = False
        tc.variables["JPEGXL_ENABLE_DOXYGEN"] = False
        tc.variables["JPEGXL_ENABLE_EXAMPLES"] = False
        tc.variables["JPEGXL_ENABLE_JNI"] = False
        tc.variables["JPEGXL_ENABLE_MANPAGES"] = False
        tc.variables["JPEGXL_ENABLE_OPENEXR"] = False
        tc.variables["JPEGXL_ENABLE_PLUGINS"] = False
        tc.variables["JPEGXL_ENABLE_SJPEG"] = False
        tc.variables["JPEGXL_ENABLE_SKCMS"] = False
        tc.variables["JPEGXL_ENABLE_TCMALLOC"] = self.options.with_tcmalloc
        tc.variables["JPEGXL_ENABLE_VIEWERS"] = False
        tc.variables["JPEGXL_ENABLE_TOOLS"] = False
        tc.variables["JPEGXL_FORCE_SYSTEM_BROTLI"] = True
        tc.variables["JPEGXL_FORCE_SYSTEM_GTEST"] = True
        tc.variables["JPEGXL_FORCE_SYSTEM_HWY"] = True
        tc.variables["JPEGXL_FORCE_SYSTEM_LCMS2"] = True
        tc.variables["JPEGXL_WARNINGS_AS_ERRORS"] = False
        tc.variables["JPEGXL_FORCE_NEON"] = False
        tc.variables["JPEGXL_ENABLE_AVX512"] = self.options.get_safe("avx512", False)
        tc.variables["JPEGXL_ENABLE_AVX512_SPR"] = self.options.get_safe("avx512_spr", False)
        tc.variables["JPEGXL_ENABLE_AVX512_ZEN4"] = self.options.get_safe("avx512_zen4", False)
        if cross_building(self):
            tc.variables["CMAKE_SYSTEM_PROCESSOR"] = str(self.settings.arch)
        # Allow non-cache_variables to be used
        tc.cache_variables["CMAKE_POLICY_DEFAULT_CMP0077"] = "NEW"
        # Skip the buggy custom FindAtomic and force the use of atomic library directly for libstdc++
        tc.variables["ATOMICS_LIBRARIES"] = "atomic" if self._atomic_required else ""
        if Version(self.version) >= "0.8":
            # TODO: add support for the jpegli JPEG encoder library
            tc.variables["JPEGXL_ENABLE_JPEGLI"] = False
            tc.variables["JPEGXL_ENABLE_JPEGLI_LIBJPEG"] = False
        # TODO: can hopefully be removed in newer versions
        # https://github.com/libjxl/libjxl/issues/3159
        if Version(self.version) >= "0.9" and self.settings.build_type == "Debug" and is_msvc(self):
            tc.preprocessor_definitions["JXL_DEBUG_V_LEVEL"] = 1
        tc.generate()

        deps = CMakeDeps(self)
        deps.set_property("brotli", "cmake_file_name", "Brotli")
        deps.set_property("highway", "cmake_file_name", "HWY")
        deps.set_property("lcms", "cmake_file_name", "LCMS2")
        deps.generate()

        # For tcmalloc
        deps = PkgConfigDeps(self)
        deps.generate()

    @property
    def _atomic_required(self):
        return self.settings.get_safe("compiler.libcxx") in ["libstdc++", "libstdc++11"]

    def _patch_sources(self):
        # Disable tools, extras and third_party
        save(self, os.path.join(self.source_folder, "tools", "CMakeLists.txt"), "")
        save(self, os.path.join(self.source_folder, "third_party", "CMakeLists.txt"), "")
        # FindAtomics.cmake values are set by CMakeToolchain instead
        save(self, os.path.join(self.source_folder, "cmake", "FindAtomics.cmake"), "")

        # Allow fPIC to be set by Conan
        replace_in_file(self, os.path.join(self.source_folder, "CMakeLists.txt"),
                        "set(CMAKE_POSITION_INDEPENDENT_CODE TRUE)", "")
        for cmake_file in ["jxl.cmake", "jxl_threads.cmake", "jxl_cms.cmake", "jpegli.cmake"]:
            path = os.path.join(self.source_folder, "lib", cmake_file)
            if os.path.exists(path):
                fpic = "ON" if self.options.get_safe("fPIC", True) else "OFF"
                replace_in_file(self, path, "POSITION_INDEPENDENT_CODE ON", f"POSITION_INDEPENDENT_CODE {fpic}")

    def build(self):
        self._patch_sources()
        cmake = CMake(self)
        cmake.configure()
        cmake.build()

    def package(self):
        cmake = CMake(self)
        cmake.install()
        copy(self, "LICENSE", self.source_folder, os.path.join(self.package_folder, "licenses"))
        rmdir(self, os.path.join(self.package_folder, "lib", "pkgconfig"))
        if self.options.shared:
            rm(self, "*.a", os.path.join(self.package_folder, "lib"))
            rm(self, "*-static.lib", os.path.join(self.package_folder, "lib"))

    def _lib_name(self, name):
        if Version(self.version) < "0.9" and not self.options.shared and self.settings.os == "Windows":
            return name + "-static"
        return name

    def package_info(self):
        libcxx = stdcpp_library(self)

        # jxl
        self.cpp_info.components["jxl"].set_property("pkg_config_name", "libjxl")
        self.cpp_info.components["jxl"].libs = [self._lib_name("jxl")]
        self.cpp_info.components["jxl"].requires = ["brotli::brotli", "highway::highway", "lcms::lcms"]
        if self.options.with_tcmalloc:
            self.cpp_info.components["jxl"].requires.append("gperftools::tcmalloc_minimal")
        if self._atomic_required:
            self.cpp_info.components["jxl"].system_libs.append("atomic")
        if not self.options.shared:
            self.cpp_info.components["jxl"].defines.append("JXL_STATIC_DEFINE")
        if libcxx:
            self.cpp_info.components["jxl"].system_libs.append(libcxx)

        # jxl_cms
        if Version(self.version) >= "0.9.0":
            self.cpp_info.components["jxl_cms"].set_property("pkg_config_name", "libjxl_cms")
            self.cpp_info.components["jxl_cms"].libs = [self._lib_name("jxl_cms")]
            self.cpp_info.components["jxl_cms"].requires = ["lcms::lcms", "highway::highway"]
            if not self.options.shared:
                self.cpp_info.components["jxl"].defines.append("JXL_CMS_STATIC_DEFINE")
            if libcxx:
                self.cpp_info.components["jxl_cms"].system_libs.append(libcxx)
            self.cpp_info.components["jxl"].requires.append("jxl_cms")

        # jxl_dec
        if Version(self.version) < "0.9.0":
            if not self.options.shared:
                self.cpp_info.components["jxl_dec"].set_property("pkg_config_name", "libjxl_dec")
                self.cpp_info.components["jxl_dec"].libs = [self._lib_name("jxl_dec")]
                self.cpp_info.components["jxl_dec"].requires = ["brotli::brotli", "highway::highway", "lcms::lcms"]
                if libcxx:
                    self.cpp_info.components["jxl_dec"].system_libs.append(libcxx)

        # jxl_threads
        self.cpp_info.components["jxl_threads"].set_property("pkg_config_name", "libjxl_threads")
        self.cpp_info.components["jxl_threads"].libs = [self._lib_name("jxl_threads")]
        if self.settings.os in ["Linux", "FreeBSD"]:
            self.cpp_info.components["jxl_threads"].system_libs = ["pthread"]
        if not self.options.shared:
            self.cpp_info.components["jxl_threads"].defines.append("JXL_THREADS_STATIC_DEFINE")
        if libcxx:
            self.cpp_info.components["jxl_threads"].system_libs.append(libcxx)

改造します。

...
from conan.tools.files import copy, get, rmdir, save, rm, replace_in_file, patch # patch()を使うので追加
...
class LibjxlConan(ConanFile):
    name = "libjxl_fixed" # 名前を変えておく
    version = "0.11.1"    # バージョンを指定しておく
...
    def export_sources(self):
        copy(self, "fix.patch", self.recipe_folder, os.path.join(self.export_sources_folder, "src")) # fix.patchをエクスポートする
        copy(self, "conan_deps.cmake", self.recipe_folder, os.path.join(self.export_sources_folder, "src"))
...
    def source(self):
        get(self, **self.conan_data["sources"][self.version], strip_root=True)
        patch(self, patch_file=os.path.join(self.source_folder, "fix.patch")) # パッチを当てる

必要な次のファイルを同じ階層に拝借します:

新しく定義したパッケージをビルドします。

conan create . -s build_type=Debug

以上でlibjxl_fixedパッケージとしてパッチの当たったlibjxlを使えます。

雑記