Compare commits

...

1 Commits

Author SHA1 Message Date
insleker 41f4de40e5 refactor: make linux kbuild tree recipe-only 2026-05-23 00:04:59 +08:00
2 changed files with 294 additions and 298 deletions

View File

@ -9,200 +9,87 @@ from conan.tools.build import cross_building
from conan.tools.files import copy, get, save from conan.tools.files import copy, get, save
class LinuxKbuildTreeConan(ConanFile): def prepare(conanfile, output=None, defconfig=None, make_modules=None, url=None):
name = "linux-kbuild-tree" if conanfile.settings.os != "Linux":
package_type = "application"
settings = "os", "arch", "compiler", "build_type"
options = {
"url": ["ANY"],
"defconfig": ["ANY"],
"make_modules": [True, False],
}
default_options = {
"url": "",
"defconfig": "defconfig",
"make_modules": False,
}
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") raise ConanInvalidConfiguration("linux-kbuild-tree requires a Linux target")
if not self.version: pyreq = conanfile.python_requires["linux-kbuild-tree"]
raise ConanInvalidConfiguration( version = str(pyreq.ref.version)
"linux-kbuild-tree requires a package version, e.g. " arch = kernel_arch(conanfile)
"conan create recipes/linux-kbuild-tree --version=6.5.9" cross_compile = _cross_compile(conanfile)
) if cross_compile is None:
self._kernel_arch
if cross_building(self) and self._cross_compile is None:
raise ConanInvalidConfiguration( raise ConanInvalidConfiguration(
"Cross-building a kernel tree requires " "Cross-building a kernel tree requires "
"tools.build:compiler_executables with a GCC-like C compiler" "tools.build:compiler_executables with a GCC-like C compiler"
) )
def source(self): if defconfig is None:
source_url, sha256 = self._linux_source() defconfig = conanfile.conf.get("user.linux-kbuild-tree:defconfig", default="defconfig")
get( if make_modules is None:
self, make_modules = conanfile.conf.get(
source_url, "user.linux-kbuild-tree:make_modules",
sha256=sha256, default=False,
destination=os.path.join(self.source_folder, "linux"),
strip_root=True,
) )
make_modules = _as_bool(make_modules)
if url is None:
url = conanfile.conf.get("user.linux-kbuild-tree:url", default=None)
def build(self): linux = output or os.path.join(conanfile.build_folder, "linux-kbuild-tree")
linux = os.path.join(self.source_folder, "linux") source_url, sha256 = _linux_source(pyreq.path, version, url=url)
arch = self._kernel_arch
cross_compile = self._cross_compile or "" if not os.path.exists(os.path.join(linux, "Makefile")):
jobs = self.conf.get("tools.build:jobs", default=None) 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_jobs = f" -j{jobs}" if jobs else ""
make_base = ( make_base = (
f"make --no-print-directory -C {shlex.quote(linux)}{make_jobs} " f"make --no-print-directory -C {shlex.quote(linux)}{make_jobs} "
f"ARCH={shlex.quote(str(arch))} " f"ARCH={shlex.quote(str(arch))} "
f"CROSS_COMPILE={shlex.quote(str(cross_compile))} " f"CROSS_COMPILE={shlex.quote(str(cross_compile or ''))} "
f"{self._host_warning_flags_args}" f"{_host_warning_flags_args(version)}"
) )
self._install_config(linux, make_base) _install_config(conanfile, pyreq.path, linux, make_base, defconfig)
conanfile.run(f"{make_base} olddefconfig prepare modules_prepare")
self.run(f"{make_base} olddefconfig prepare modules_prepare") if make_modules:
if self.options.make_modules:
# Required when CONFIG_MODVERSIONS=y and the target kernel exports # Required when CONFIG_MODVERSIONS=y and the target kernel exports
# symbols that are not available from a vendor-provided Module.symvers. # symbols that are not available from a vendor-provided Module.symvers.
self.run(f"{make_base} modules") conanfile.run(f"{make_base} modules")
kernelrelease = self._capture(f"{make_base} kernelrelease") kernelrelease = _capture(f"{make_base} kernelrelease")
compiler = self._capture(f"{self._cross_gcc} --version").splitlines()[0] compiler = _capture(f"{_cross_gcc(cross_compile)} --version").splitlines()[0]
source_url, sha256 = self._linux_source() info_path = os.path.join(linux, "kbuild-tree-info.txt")
save( save(
self, conanfile,
os.path.join(self.build_folder, "kbuild-tree-info.txt"), info_path,
"\n".join( "\n".join(
[ [
f"url={source_url}", f"url={source_url}",
f"ref={self._linux_ref_from_version()}", f"ref={_linux_ref_from_version(version)}",
f"sha256={sha256 or ''}", f"sha256={sha256 or ''}",
f"kernelrelease={kernelrelease}", f"kernelrelease={kernelrelease}",
f"arch={arch}", f"arch={arch}",
f"cross_compile={cross_compile}", f"cross_compile={cross_compile or ''}",
f"compiler={compiler}", f"compiler={compiler}",
f"make_modules={self.options.make_modules}", f"make_modules={make_modules}",
"", "",
] ]
), ),
) )
def package(self): return {
linux = os.path.join(self.source_folder, "linux") "path": linux,
"info_path": info_path,
"kernelrelease": kernelrelease,
"arch": arch,
"cross_compile": cross_compile or "",
}
# 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): def kernel_arch(conanfile):
# Upstream Linux tags base releases as v6.8, while the kernel release arch = str(conanfile.settings.arch)
# 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 = { arch_map = {
"x86": "x86", "x86": "x86",
"x86_64": "x86", "x86_64": "x86",
@ -238,57 +125,169 @@ class LinuxKbuildTreeConan(ConanFile):
) )
return arch_map[arch] return arch_map[arch]
@property
def _cross_compile(self): def _linux_ref_from_version(version):
if not cross_building(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(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 "" return ""
c_compiler = self._compiler_executable("c") 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
def _version_lt(version, other):
return _version_tuple(str(version)) < _version_tuple(other)
def _version_tuple(version):
return tuple(int(part) for part in version.split("."))
def _cross_compile(conanfile):
if not cross_building(conanfile):
return ""
c_compiler = _compiler_executable(conanfile, "c")
if not c_compiler: if not c_compiler:
return None return None
return self._cross_compile_from_executable(c_compiler) return _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): def _cross_compile_from_executable(c_compiler):
executable = os.path.basename(c_compiler) executable = os.path.basename(c_compiler)
match = re.match(r"^(.+?)(?:gcc|cc)(?:-\d+(?:\.\d+)*)?$", executable) match = re.match(r"^(.+?)(?:gcc|cc)(?:-\d+(?:\.\d+)*)?$", executable)
if not match: if not match:
return None return None
return match.group(1) 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): def _cross_gcc(cross_compile):
executables = self.conf.get("tools.build:compiler_executables", default={}) 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: if not executables:
return None return None
if hasattr(executables, "get"): if hasattr(executables, "get"):
return executables.get(language) return executables.get(language)
return None return None
def _install_config(self, linux, make_base):
defconfig = str(self.options.defconfig) def _install_config(conanfile, recipe_path, linux, make_base, defconfig):
defconfig = "" if defconfig is None else str(defconfig)
if not defconfig: if not defconfig:
if not os.path.exists(os.path.join(linux, ".config")): if not os.path.exists(os.path.join(linux, ".config")):
raise ConanInvalidConfiguration( raise ConanInvalidConfiguration(
"Missing kernel config: set linux-kbuild-tree/*:defconfig " "Missing kernel config: pass defconfig or provide a source tree with .config"
"or provide a source tree with .config"
) )
return return
exported_config = os.path.join(self.source_folder, "configs", defconfig) exported_config = os.path.join(recipe_path, "configs", defconfig)
if os.path.isfile(exported_config): if os.path.isfile(exported_config):
copy( copy(
self, conanfile,
defconfig, defconfig,
src=os.path.dirname(exported_config), src=os.path.dirname(exported_config),
dst=linux, dst=linux,
@ -297,9 +296,10 @@ class LinuxKbuildTreeConan(ConanFile):
os.replace(os.path.join(linux, defconfig), os.path.join(linux, ".config")) os.replace(os.path.join(linux, defconfig), os.path.join(linux, ".config"))
return return
self.run(f"{make_base} {shlex.quote(defconfig)}") conanfile.run(f"{make_base} {shlex.quote(defconfig)}")
def _capture(self, command):
def _capture(command):
output = subprocess.check_output( output = subprocess.check_output(
command, command,
shell=True, shell=True,
@ -309,3 +309,11 @@ class LinuxKbuildTreeConan(ConanFile):
if not output: if not output:
raise ConanInvalidConfiguration(f"Command produced no output: {command}") raise ConanInvalidConfiguration(f"Command produced no output: {command}")
return output return output
class LinuxKbuildTreeConan(ConanFile):
name = "linux-kbuild-tree"
package_type = "python-require"
upload_policy = "skip"
exports = "configs/*"

View File

@ -8,23 +8,26 @@ from conan.tools.files import copy
class TestPackageConan(ConanFile): class TestPackageConan(ConanFile):
test_type = "explicit" test_type = "explicit"
python_requires = "tested_reference_str"
settings = "os", "compiler", "build_type", "arch" settings = "os", "compiler", "build_type", "arch"
exports_sources = "Makefile", "test_kbuild_module.c" exports_sources = "Makefile", "test_kbuild_module.c"
def requirements(self):
self.requires(self.tested_reference_str)
def layout(self): def layout(self):
self.folders.build = "build" self.folders.build = "build"
self.folders.generators = os.path.join(self.folders.build, "generators") self.folders.generators = os.path.join(self.folders.build, "generators")
def build(self): 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") module_dir = os.path.join(self.build_folder, "module")
copy(self, "*", src=self.source_folder, dst=module_dir) copy(self, "*", src=self.source_folder, dst=module_dir)
tree_info = self._read_tree_info(kernel_tree)
arch = tree_info.get("arch", "") arch = tree_info.get("arch", "")
cross_compile = tree_info.get("cross_compile", "") cross_compile = tree_info.get("cross_compile", "")
jobs = self.conf.get("tools.build:jobs", default=None) 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") module_path = os.path.join(self.build_folder, "module", "test_kbuild_module.ko")
if not os.path.isfile(module_path): if not os.path.isfile(module_path):
raise ConanException(f"Kernel module was not built: {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