Conan2のパッケージにパッチを当てる
問題
Conan2はC/C++向け依存パッケージマネージャです。 Artifactoryと相性が良く、手軽にキャッシュをリモートに保存できます。 同類にvcpkgがあります。
libjxl/0.11.1というパッケージをDebugビルドしたところ、コンパイルエラーが発生しました。 既に#4079で修正されてmasterブランチにはマージされているものの、リリースされていないためビルドできないという状況です。 これではプロジェクトをDebugビルドできません。 そのため、こちらでパッチを当てる必要があります。
解決
プロジェクトからパッケージへパッチを当てることはできません。 つまり、パッケージをいじる必要があります。 キャッシュにパッチを当てても良いですが、それではCIのような新環境でのセットアップが億劫です。 仕方がないので新しくパッケージを作成してパッチを当てます。
簡潔に手順を記すと次のようになります:
- パッチファイルを作成
- パッケージの次のファイルを拝借:
- conanfile.py
- conandata.yml
- conan_deps.cmake
- 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")) # パッチを当てる
必要な次のファイルを同じ階層に拝借します:
- conandata.yml: conanfile.pyと同じディレクトリにあるはず? これがないと
self.conan_data
がNone
になる。 - conan_deps.cmake: conanfile.pyに近いどこかにあるはず? これがないとCMakeのコンフィグに失敗する。
新しく定義したパッケージをビルドします。
conan create . -s build_type=Debug
以上でlibjxl_fixedパッケージとしてパッチの当たったlibjxlを使えます。
雑記
- わざわざ記事にするほどでもない内容だけれど、まあいいか。
- C++のパッケージ管理つらすぎる。ただでさえコンパイル遅い言語なのに依存ライブラリもビルドしなくちゃいけないのつらすぎる。
- CMakeUserPresets.jsonの生成を抑制できないの大変苛立つ。そうでなくともプリセット名を変更できないのも余計に苛立つ。なぜconan-debugやらconan-releaseやらのプリセット名を強要しようとするのか。
■