ArticlesScrapsPagesAbout

Conan2のパッケージにパッチを当てる

$devenv#conan2025/04/17

問題

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

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

解決

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

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

  1. パッチファイルを作成

  2. パッケージの次のファイルを拝借:

    • conanfile.py

    • conandata.yml

    • 他あればファイル

  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を使えます。

雑記