From 41f4de40e58248df07c13d71b35b6998799bf37c Mon Sep 17 00:00:00 2001 From: insleker Date: Sat, 23 May 2026 00:04:59 +0800 Subject: [PATCH] refactor: make linux kbuild tree recipe-only --- recipes/linux-kbuild-tree/all/conanfile.py | 564 +++++++++--------- .../all/test_package/conanfile.py | 28 +- 2 files changed, 294 insertions(+), 298 deletions(-) diff --git a/recipes/linux-kbuild-tree/all/conanfile.py b/recipes/linux-kbuild-tree/all/conanfile.py index bfde83a..6a37725 100644 --- a/recipes/linux-kbuild-tree/all/conanfile.py +++ b/recipes/linux-kbuild-tree/all/conanfile.py @@ -9,303 +9,311 @@ from conan.tools.build import cross_building from conan.tools.files import copy, get, save -class LinuxKbuildTreeConan(ConanFile): - name = "linux-kbuild-tree" - package_type = "application" +def prepare(conanfile, output=None, defconfig=None, make_modules=None, url=None): + if conanfile.settings.os != "Linux": + raise ConanInvalidConfiguration("linux-kbuild-tree requires a Linux target") - settings = "os", "arch", "compiler", "build_type" + pyreq = conanfile.python_requires["linux-kbuild-tree"] + version = str(pyreq.ref.version) + arch = kernel_arch(conanfile) + cross_compile = _cross_compile(conanfile) + if cross_compile is None: + raise ConanInvalidConfiguration( + "Cross-building a kernel tree requires " + "tools.build:compiler_executables with a GCC-like C compiler" + ) - options = { - "url": ["ANY"], - "defconfig": ["ANY"], - "make_modules": [True, False], + if defconfig is None: + defconfig = conanfile.conf.get("user.linux-kbuild-tree:defconfig", default="defconfig") + if make_modules is None: + make_modules = conanfile.conf.get( + "user.linux-kbuild-tree:make_modules", + default=False, + ) + make_modules = _as_bool(make_modules) + if url is None: + url = conanfile.conf.get("user.linux-kbuild-tree:url", default=None) + + linux = output or os.path.join(conanfile.build_folder, "linux-kbuild-tree") + source_url, sha256 = _linux_source(pyreq.path, version, url=url) + + if not os.path.exists(os.path.join(linux, "Makefile")): + get(conanfile, source_url, sha256=sha256, destination=linux, strip_root=True) + + jobs = conanfile.conf.get("tools.build:jobs", default=None) + make_jobs = f" -j{jobs}" if jobs else "" + make_base = ( + f"make --no-print-directory -C {shlex.quote(linux)}{make_jobs} " + f"ARCH={shlex.quote(str(arch))} " + f"CROSS_COMPILE={shlex.quote(str(cross_compile or ''))} " + f"{_host_warning_flags_args(version)}" + ) + + _install_config(conanfile, pyreq.path, linux, make_base, defconfig) + conanfile.run(f"{make_base} olddefconfig prepare modules_prepare") + + if make_modules: + # Required when CONFIG_MODVERSIONS=y and the target kernel exports + # symbols that are not available from a vendor-provided Module.symvers. + conanfile.run(f"{make_base} modules") + + kernelrelease = _capture(f"{make_base} kernelrelease") + compiler = _capture(f"{_cross_gcc(cross_compile)} --version").splitlines()[0] + info_path = os.path.join(linux, "kbuild-tree-info.txt") + + save( + conanfile, + info_path, + "\n".join( + [ + f"url={source_url}", + f"ref={_linux_ref_from_version(version)}", + f"sha256={sha256 or ''}", + f"kernelrelease={kernelrelease}", + f"arch={arch}", + f"cross_compile={cross_compile or ''}", + f"compiler={compiler}", + f"make_modules={make_modules}", + "", + ] + ), + ) + + return { + "path": linux, + "info_path": info_path, + "kernelrelease": kernelrelease, + "arch": arch, + "cross_compile": cross_compile or "", } - default_options = { - "url": "", - "defconfig": "defconfig", - "make_modules": False, + +def kernel_arch(conanfile): + arch = str(conanfile.settings.arch) + arch_map = { + "x86": "x86", + "x86_64": "x86", + "armv5el": "arm", + "armv5hf": "arm", + "armv6": "arm", + "armv7": "arm", + "armv7hf": "arm", + "armv8": "arm64", + "armv8_32": "arm", + "armv8.3": "arm64", + "armv8.5": "arm64", + "armv8.6": "arm64", + "armv8.7": "arm64", + "armv8.8": "arm64", + "armv8.9": "arm64", + "ppc32": "powerpc", + "ppc64": "powerpc", + "ppc64le": "powerpc", + "riscv32": "riscv", + "riscv64": "riscv", + "s390": "s390", + "s390x": "s390", + "sparc": "sparc", + "sparcv9": "sparc", + "mips": "mips", + "mips64": "mips", } - - exports_sources = "configs/*" - - def layout(self): - self.folders.build = "build" - self.folders.generators = os.path.join(self.folders.build, "generators") - - def package_id(self): - c_compiler = self._compiler_executable("c") - if c_compiler is not None: - self.info.conf.define("tools.build:compiler_executables", {"c": str(c_compiler)}) - - def validate(self): - if self.settings.os != "Linux": - raise ConanInvalidConfiguration("linux-kbuild-tree requires a Linux target") - - if not self.version: - raise ConanInvalidConfiguration( - "linux-kbuild-tree requires a package version, e.g. " - "conan create recipes/linux-kbuild-tree --version=6.5.9" - ) - - self._kernel_arch - - if cross_building(self) and self._cross_compile is None: - raise ConanInvalidConfiguration( - "Cross-building a kernel tree requires " - "tools.build:compiler_executables with a GCC-like C compiler" - ) - - def source(self): - source_url, sha256 = self._linux_source() - get( - self, - source_url, - sha256=sha256, - destination=os.path.join(self.source_folder, "linux"), - strip_root=True, + if arch not in arch_map: + raise ConanInvalidConfiguration( + f"Cannot derive Linux ARCH from settings.arch={arch!r}; " + "add this Conan architecture to the recipe arch map" ) + return arch_map[arch] - def build(self): - linux = os.path.join(self.source_folder, "linux") - arch = self._kernel_arch - cross_compile = self._cross_compile or "" - jobs = self.conf.get("tools.build:jobs", default=None) - make_jobs = f" -j{jobs}" if jobs else "" - make_base = ( - f"make --no-print-directory -C {shlex.quote(linux)}{make_jobs} " - f"ARCH={shlex.quote(str(arch))} " - f"CROSS_COMPILE={shlex.quote(str(cross_compile))} " - f"{self._host_warning_flags_args}" +def _linux_ref_from_version(version): + # Upstream Linux tags base releases as v6.8, while the kernel release + # and Conan package version are commonly represented as 6.8.0. + version_parts = str(version).split(".") + if len(version_parts) == 3 and version_parts[2] == "0": + return f"v{version_parts[0]}.{version_parts[1]}" + return f"v{version}" + + +def _linux_archive_version(version): + version_parts = str(version).split(".") + if len(version_parts) == 3 and version_parts[2] == "0": + return f"{version_parts[0]}.{version_parts[1]}" + return str(version) + + +def _linux_source(recipe_path, version, url=None): + source = _conandata_sources(recipe_path).get(str(version), {}) + source_url = url or source.get("url") or _kernel_cdn_url(version) + return source_url, source.get("sha256") + + +def _kernel_cdn_url(version): + archive_version = _linux_archive_version(version) + major = archive_version.split(".", 1)[0] + return ( + f"https://cdn.kernel.org/pub/linux/kernel/v{major}.x/" + f"linux-{archive_version}.tar.gz" + ) + + +def _conandata_sources(recipe_path): + path = os.path.join(recipe_path, "conandata.yml") + if not os.path.isfile(path): + return {} + + sources = {} + current = None + in_sources = False + with open(path, encoding="utf-8") as conandata: + for raw_line in conandata: + line = raw_line.rstrip() + stripped = line.strip() + if not stripped or stripped.startswith("#"): + continue + if stripped == "sources:": + in_sources = True + continue + if not in_sources: + continue + if not line.startswith(" "): + break + version_match = re.match(r'^\s{2}"?([^":]+)"?:\s*$', line) + if version_match: + current = version_match.group(1) + sources[current] = {} + continue + field_match = re.match(r'^\s{4}([A-Za-z0-9_-]+):\s*"?([^"]*)"?\s*$', line) + if current and field_match: + key, value = field_match.groups() + sources[current][key] = value + return sources + + +def _host_warning_flags_args(version): + hostcflags = [] + + if _version_lt(version, "5.16.0") and _host_compiler_accepts("-Wno-error=use-after-free"): + # Older objtool builds use -Werror and trip GCC 14's realloc(ptr) + # use-after-free diagnostic in tools/lib/subcmd/subcmd-util.h. + hostcflags.append("-Wno-error=use-after-free") + + if not hostcflags: + return "" + + flags = shlex.quote(" ".join(hostcflags)) + return f"HOSTCFLAGS={flags} EXTRA_CFLAGS={flags}" + + +def _as_bool(value): + if isinstance(value, str): + return value.lower() in ("1", "true", "yes", "on") + return bool(value) + + +def _host_compiler_accepts(flag): + try: + subprocess.run( + [ + "gcc", + flag, + "-x", + "c", + "-c", + "-o", + os.devnull, + "-", + ], + input="int main(void) { return 0; }\n", + text=True, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + check=True, ) + except (OSError, subprocess.CalledProcessError): + return False + return True - self._install_config(linux, make_base) - self.run(f"{make_base} olddefconfig prepare modules_prepare") +def _version_lt(version, other): + return _version_tuple(str(version)) < _version_tuple(other) - if self.options.make_modules: - # Required when CONFIG_MODVERSIONS=y and the target kernel exports - # symbols that are not available from a vendor-provided Module.symvers. - self.run(f"{make_base} modules") - kernelrelease = self._capture(f"{make_base} kernelrelease") - compiler = self._capture(f"{self._cross_gcc} --version").splitlines()[0] - source_url, sha256 = self._linux_source() +def _version_tuple(version): + return tuple(int(part) for part in version.split(".")) - save( - self, - os.path.join(self.build_folder, "kbuild-tree-info.txt"), - "\n".join( - [ - f"url={source_url}", - f"ref={self._linux_ref_from_version()}", - f"sha256={sha256 or ''}", - f"kernelrelease={kernelrelease}", - f"arch={arch}", - f"cross_compile={cross_compile}", - f"compiler={compiler}", - f"make_modules={self.options.make_modules}", - "", - ] - ), - ) - def package(self): - linux = os.path.join(self.source_folder, "linux") +def _cross_compile(conanfile): + if not cross_building(conanfile): + return "" - # External module builds need source files, generated headers, scripts, - # .config, and often Module.symvers. Packaging the whole prepared tree is - # intentionally conservative and can be optimized after the flow is stable. - copy(self, "*", src=linux, dst=self.package_folder, excludes=(".git", ".git/*")) - copy(self, ".config", src=linux, dst=self.package_folder, keep_path=False) - copy( - self, - "kbuild-tree-info.txt", - src=self.build_folder, - dst=self.package_folder, - keep_path=False, - ) - - def _linux_ref_from_version(self): - # Upstream Linux tags base releases as v6.8, while the kernel release - # and Conan package version are commonly represented as 6.8.0. - version_parts = str(self.version).split(".") - if len(version_parts) == 3 and version_parts[2] == "0": - return f"v{version_parts[0]}.{version_parts[1]}" - return f"v{self.version}" - - def _linux_archive_version(self): - version_parts = str(self.version).split(".") - if len(version_parts) == 3 and version_parts[2] == "0": - return f"{version_parts[0]}.{version_parts[1]}" - return str(self.version) - - def _linux_source(self): - source = self.conan_data.get("sources", {}).get(str(self.version)) or {} - source_url = source.get("url") or self._kernel_cdn_url() - return source_url, source.get("sha256") - - def _kernel_cdn_url(self): - archive_version = self._linux_archive_version() - major = archive_version.split(".", 1)[0] - return ( - f"https://cdn.kernel.org/pub/linux/kernel/v{major}.x/" - f"linux-{archive_version}.tar.gz" - ) - - @property - def _host_warning_flags_args(self): - hostcflags = [] - - if self._version_lt("5.16.0") and self._host_compiler_accepts( - "-Wno-error=use-after-free" - ): - # Older objtool builds use -Werror and trip GCC 14's realloc(ptr) - # use-after-free diagnostic in tools/lib/subcmd/subcmd-util.h. - hostcflags.append("-Wno-error=use-after-free") - - if not hostcflags: - return "" - - flags = shlex.quote(" ".join(hostcflags)) - return f"HOSTCFLAGS={flags} EXTRA_CFLAGS={flags}" - - def _host_compiler_accepts(self, flag): - try: - subprocess.run( - [ - "gcc", - flag, - "-x", - "c", - "-c", - "-o", - os.devnull, - "-", - ], - input="int main(void) { return 0; }\n", - text=True, - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - check=True, - ) - except (OSError, subprocess.CalledProcessError): - return False - return True - - def _version_lt(self, other): - return self._version_tuple(str(self.version)) < self._version_tuple(other) - - def _version_tuple(self, version): - return tuple(int(part) for part in version.split(".")) - - @property - def _kernel_arch(self): - arch = str(self.settings.arch) - arch_map = { - "x86": "x86", - "x86_64": "x86", - "armv5el": "arm", - "armv5hf": "arm", - "armv6": "arm", - "armv7": "arm", - "armv7hf": "arm", - "armv8": "arm64", - "armv8_32": "arm", - "armv8.3": "arm64", - "armv8.5": "arm64", - "armv8.6": "arm64", - "armv8.7": "arm64", - "armv8.8": "arm64", - "armv8.9": "arm64", - "ppc32": "powerpc", - "ppc64": "powerpc", - "ppc64le": "powerpc", - "riscv32": "riscv", - "riscv64": "riscv", - "s390": "s390", - "s390x": "s390", - "sparc": "sparc", - "sparcv9": "sparc", - "mips": "mips", - "mips64": "mips", - } - if arch not in arch_map: - raise ConanInvalidConfiguration( - f"Cannot derive Linux ARCH from settings.arch={arch!r}; " - "add this Conan architecture to the recipe arch map" - ) - return arch_map[arch] - - @property - def _cross_compile(self): - if not cross_building(self): - return "" - - c_compiler = self._compiler_executable("c") - if not c_compiler: - return None - - return self._cross_compile_from_executable(c_compiler) - - def _cross_compile_from_compiler_executables(self): - c_compiler = self._compiler_executable("c") - if not c_compiler: - return None - return self._cross_compile_from_executable(c_compiler) - - def _cross_compile_from_executable(self, c_compiler): - executable = os.path.basename(c_compiler) - match = re.match(r"^(.+?)(?:gcc|cc)(?:-\d+(?:\.\d+)*)?$", executable) - if not match: - return None - return match.group(1) - - @property - def _cross_gcc(self): - cross_compile = self._cross_compile or "" - return shlex.quote(f"{cross_compile}gcc") - - def _compiler_executable(self, language): - executables = self.conf.get("tools.build:compiler_executables", default={}) - if not executables: - return None - if hasattr(executables, "get"): - return executables.get(language) + c_compiler = _compiler_executable(conanfile, "c") + if not c_compiler: return None - def _install_config(self, linux, make_base): - defconfig = str(self.options.defconfig) - if not defconfig: - if not os.path.exists(os.path.join(linux, ".config")): - raise ConanInvalidConfiguration( - "Missing kernel config: set linux-kbuild-tree/*:defconfig " - "or provide a source tree with .config" - ) - return + return _cross_compile_from_executable(c_compiler) - exported_config = os.path.join(self.source_folder, "configs", defconfig) - if os.path.isfile(exported_config): - copy( - self, - defconfig, - src=os.path.dirname(exported_config), - dst=linux, - keep_path=False, + +def _cross_compile_from_executable(c_compiler): + executable = os.path.basename(c_compiler) + match = re.match(r"^(.+?)(?:gcc|cc)(?:-\d+(?:\.\d+)*)?$", executable) + if not match: + return None + return match.group(1) + + +def _cross_gcc(cross_compile): + return shlex.quote(f"{cross_compile or ''}gcc") + + +def _compiler_executable(conanfile, language): + executables = conanfile.conf.get("tools.build:compiler_executables", default={}) + if not executables: + return None + if hasattr(executables, "get"): + return executables.get(language) + return None + + +def _install_config(conanfile, recipe_path, linux, make_base, defconfig): + defconfig = "" if defconfig is None else str(defconfig) + if not defconfig: + if not os.path.exists(os.path.join(linux, ".config")): + raise ConanInvalidConfiguration( + "Missing kernel config: pass defconfig or provide a source tree with .config" ) - os.replace(os.path.join(linux, defconfig), os.path.join(linux, ".config")) - return + return - self.run(f"{make_base} {shlex.quote(defconfig)}") + exported_config = os.path.join(recipe_path, "configs", defconfig) + if os.path.isfile(exported_config): + copy( + conanfile, + defconfig, + src=os.path.dirname(exported_config), + dst=linux, + keep_path=False, + ) + os.replace(os.path.join(linux, defconfig), os.path.join(linux, ".config")) + return - def _capture(self, command): - output = subprocess.check_output( - command, - shell=True, - stderr=subprocess.STDOUT, - text=True, - ).strip() - if not output: - raise ConanInvalidConfiguration(f"Command produced no output: {command}") - return output + conanfile.run(f"{make_base} {shlex.quote(defconfig)}") + + +def _capture(command): + output = subprocess.check_output( + command, + shell=True, + stderr=subprocess.STDOUT, + text=True, + ).strip() + if not output: + raise ConanInvalidConfiguration(f"Command produced no output: {command}") + return output + + +class LinuxKbuildTreeConan(ConanFile): + name = "linux-kbuild-tree" + package_type = "python-require" + upload_policy = "skip" + + exports = "configs/*" diff --git a/recipes/linux-kbuild-tree/all/test_package/conanfile.py b/recipes/linux-kbuild-tree/all/test_package/conanfile.py index 51c9150..ac60fc6 100644 --- a/recipes/linux-kbuild-tree/all/test_package/conanfile.py +++ b/recipes/linux-kbuild-tree/all/test_package/conanfile.py @@ -8,23 +8,26 @@ from conan.tools.files import copy class TestPackageConan(ConanFile): test_type = "explicit" + python_requires = "tested_reference_str" settings = "os", "compiler", "build_type", "arch" exports_sources = "Makefile", "test_kbuild_module.c" - def requirements(self): - self.requires(self.tested_reference_str) - def layout(self): self.folders.build = "build" self.folders.generators = os.path.join(self.folders.build, "generators") def build(self): - kernel_tree = self.dependencies["linux-kbuild-tree"].package_folder + linux_kbuild_tree = self.python_requires["linux-kbuild-tree"].module + tree_info = linux_kbuild_tree.prepare( + self, + output=os.path.join(self.build_folder, "linux-kbuild-tree"), + ) + + kernel_tree = tree_info["path"] module_dir = os.path.join(self.build_folder, "module") copy(self, "*", src=self.source_folder, dst=module_dir) - tree_info = self._read_tree_info(kernel_tree) arch = tree_info.get("arch", "") cross_compile = tree_info.get("cross_compile", "") jobs = self.conf.get("tools.build:jobs", default=None) @@ -44,18 +47,3 @@ class TestPackageConan(ConanFile): module_path = os.path.join(self.build_folder, "module", "test_kbuild_module.ko") if not os.path.isfile(module_path): raise ConanException(f"Kernel module was not built: {module_path}") - - def _read_tree_info(self, kernel_tree): - path = os.path.join(kernel_tree, "kbuild-tree-info.txt") - if not os.path.isfile(path): - raise ConanException(f"Missing linux-kbuild-tree metadata: {path}") - - result = {} - with open(path, encoding="utf-8") as tree_info: - for line in tree_info: - line = line.strip() - if not line or "=" not in line: - continue - key, value = line.split("=", 1) - result[key] = value - return result