diff --git a/apps/ll-cli/src/main.cpp b/apps/ll-cli/src/main.cpp index 6a3579c51..2077bb69f 100644 --- a/apps/ll-cli/src/main.cpp +++ b/apps/ll-cli/src/main.cpp @@ -607,6 +607,44 @@ void addInspectCommand(CLI::App &commandParser, ->check(validatorString); } +// Function to add the extension subcommand +void addExtensionCommand(CLI::App &commandParser, + ExtensionOptions &extensionOptions, + const std::string &group) +{ + auto *cliExtension = + commandParser.add_subcommand("extension", _("Manage extension overrides")) + ->group(group) + ->usage(_("Usage: ll-cli extension SUBCOMMAND [OPTIONS]")); + + cliExtension->require_subcommand(1); + + auto *cliImportCdi = + cliExtension + ->add_subcommand("import-cdi", _("Import CDI rules into extension overrides")) + ->usage(_("Usage: ll-cli extension import-cdi [OPTIONS]")); + cliImportCdi + ->add_option("--name", + extensionOptions.name, + _("Specify extension name prefix to update")) + ->type_name("NAME") + ->check(validatorString); + cliImportCdi + ->add_option("--config", + extensionOptions.configPath, + _("Specify config path (default: ~/.config/linglong/config.json)")) + ->type_name("FILE"); + cliImportCdi + ->add_option("--cdi", + extensionOptions.cdiPath, + _("Specify CDI JSON path (default: nvidia-ctk cdi generate --format json)")) + ->type_name("FILE"); + cliImportCdi + ->add_flag("--apply-when-installed", + extensionOptions.applyWhenInstalled, + _("Apply overrides even when extension is installed")); +} + } // namespace using namespace linglong::utils::global; @@ -690,12 +728,14 @@ You can report bugs to the linyaps team under this project: https://github.com/O ContentOptions contentOptions{}; RepoOptions repoOptions{}; InspectOptions inspectOptions{}; + ExtensionOptions extensionOptions{}; // groups for subcommands auto *CliBuildInGroup = _("Managing installed applications and runtimes"); auto *CliAppManagingGroup = _("Managing running applications"); auto *CliSearchGroup = _("Finding applications and runtimes"); auto *CliRepoGroup = _("Managing remote repositories"); + auto *CliExtensionGroup = _("Managing extensions"); // add all subcommands using the new functions addRunCommand(commandParser, runOptions, CliAppManagingGroup); @@ -712,6 +752,7 @@ You can report bugs to the linyaps team under this project: https://github.com/O addContentCommand(commandParser, contentOptions, CliBuildInGroup); addPruneCommand(commandParser, CliAppManagingGroup); addInspectCommand(commandParser, inspectOptions, CliHiddenGroup); + addExtensionCommand(commandParser, extensionOptions, CliExtensionGroup); auto res = transformOldExec(argc, argv); CLI11_PARSE(commandParser, std::move(res)); @@ -920,6 +961,8 @@ You can report bugs to the linyaps team under this project: https://github.com/O result = cli->inspect(*ret, inspectOptions); } else if (name == "repo") { result = cli->repo(*ret, repoOptions); + } else if (name == "extension") { + result = cli->extension(*ret, extensionOptions); } else { // if subcommand name is not found, print help std::cout << commandParser.help("", CLI::AppFormatMode::All); diff --git a/docs/pages/en/guide/reference/driver.md b/docs/pages/en/guide/reference/driver.md index 434aa464b..c58dc3675 100644 --- a/docs/pages/en/guide/reference/driver.md +++ b/docs/pages/en/guide/reference/driver.md @@ -20,6 +20,6 @@ The base that applications depend on already includes the appropriate version of Drivers not included in the base that require additional installation: -- NVIDIA proprietary drivers: Install via `sudo ll-cli install org.deepin.driver.display.nvidia.570-124-04`. The `570-124-04` is the driver version number, which must match the driver version installed on the host system. Check the host driver version through the `/sys/module/nvidia/version` file. +- NVIDIA proprietary drivers: Install via `sudo ll-cli install org.deepin.driver.display.nvidia.570-124-04`. The `570-124-04` is the driver version number, which must match the driver version installed on the host system. Check the host driver version through the `/sys/module/nvidia/version` file. If the extension is not installed, linyaps will attempt to link NVIDIA driver files from the host at runtime. - Glenfly graphics drivers: Install via `sudo ll-cli install com.glenfly.driver.display.arise`. - Intel video codec drivers (VAAPI): Install via `sudo ll-cli install org.deepin.driver.media.intel`, which includes support for both new and legacy Intel graphics cards. diff --git a/docs/pages/guide/reference/driver.md b/docs/pages/guide/reference/driver.md index 7876a72d2..af23ed9af 100644 --- a/docs/pages/guide/reference/driver.md +++ b/docs/pages/guide/reference/driver.md @@ -20,6 +20,6 @@ 不在 base 中携带的,需要额外安装的驱动: -- 英伟达闭源驱动,通过 `sudo ll-cli install org.deepin.driver.display.nvidia.570-124-04` 安装。其中 `570-124-04` 是驱动版本号,需要与宿主机安装的驱动版本匹配,通过宿主机 `/sys/module/nvidia/version` 文件查看宿主机驱动的版本。 +- 英伟达闭源驱动,推荐通过 `sudo ll-cli install org.deepin.driver.display.nvidia.570-124-04` 安装。其中 `570-124-04` 是驱动版本号,需要与宿主机安装的驱动版本匹配,通过宿主机 `/sys/module/nvidia/version` 文件查看宿主机驱动的版本。未安装扩展时,linyaps 会在运行时尝试从宿主机自动链接 NVIDIA 驱动文件。 - 格兰菲显卡驱动,通过 `sudo ll-cli install com.glenfly.driver.display.arise` 安装。 - 英特尔视频编解码驱动(VAAPI),通过 `sudo ll-cli install org.deepin.driver.media.intel` 安装,包含了新/旧 Intel 显卡的支持。 diff --git a/libs/linglong/CMakeLists.txt b/libs/linglong/CMakeLists.txt index ae1c5f1ba..2a0473054 100644 --- a/libs/linglong/CMakeLists.txt +++ b/libs/linglong/CMakeLists.txt @@ -29,12 +29,16 @@ pfl_add_library( src/linglong/cli/dbus_notifier.h src/linglong/cli/dummy_notifier.cpp src/linglong/cli/dummy_notifier.h + src/linglong/cli/extension_override.cpp + src/linglong/cli/extension_override.h src/linglong/cli/interactive_notifier.h src/linglong/cli/json_printer.cpp src/linglong/cli/json_printer.h src/linglong/cli/printer.h src/linglong/cli/terminal_notifier.cpp src/linglong/cli/terminal_notifier.h + src/linglong/extension/cdi.cpp + src/linglong/extension/cdi.h src/linglong/extension/extension.cpp src/linglong/extension/extension.h src/linglong/package/architecture.cpp @@ -94,6 +98,7 @@ pfl_add_library( src/linglong/runtime/container_builder.h src/linglong/runtime/container.cpp src/linglong/runtime/container.h + src/linglong/runtime/host_nvidia_fallback.cpp src/linglong/runtime/run_context.cpp src/linglong/runtime/run_context.h src/linglong/runtime/security_context.cpp diff --git a/libs/linglong/src/linglong/cli/cli.cpp b/libs/linglong/src/linglong/cli/cli.cpp index ddab2a1cb..adf81a341 100644 --- a/libs/linglong/src/linglong/cli/cli.cpp +++ b/libs/linglong/src/linglong/cli/cli.cpp @@ -25,6 +25,7 @@ #include "linglong/api/types/v1/State.hpp" #include "linglong/api/types/v1/UpgradeListResult.hpp" #include "linglong/cli/printer.h" +#include "linglong/cli/extension_override.h" #include "linglong/common/dir.h" #include "linglong/common/strings.h" #include "linglong/oci-cfg-generators/container_cfg_builder.h" @@ -58,7 +59,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -67,6 +70,7 @@ #include #include +#include #include using namespace linglong::utils::error; @@ -519,10 +523,21 @@ int Cli::run(const RunOptions &options) linglong::runtime::ResolveOptions opts; opts.baseRef = options.base; opts.runtimeRef = options.runtime; - // 处理多个扩展 if (!options.extensions.empty()) { opts.extensionRefs = options.extensions; } + auto configPath = extension_override::getUserConfigPath(); + if (configPath) { + auto overrides = extension_override::loadOverrides(*configPath); + if (overrides) { + if (!overrides->empty()) { + runContext.setExtensionOverrides(std::move(*overrides)); + } + } else { + qWarning() << "failed to load extension overrides:" + << overrides.error().message().c_str(); + } + } // 调整日志输出,打印扩展列表(用逗号拼接) std::string extStr = @@ -2137,6 +2152,22 @@ utils::error::Result Cli::generateLDCache(runtime::RunContext &runContext, { LINGLONG_TRACE("generate ld cache"); + { + struct rlimit limit {}; + if (::getrlimit(RLIMIT_NOFILE, &limit) == 0) { + rlim_t target = limit.rlim_max; + if (target == RLIM_INFINITY) { + target = 65535; + } + if (limit.rlim_cur < target) { + struct rlimit newLimit { target, limit.rlim_max }; + if (::setrlimit(RLIMIT_NOFILE, &newLimit) != 0) { + qWarning() << "failed to raise RLIMIT_NOFILE:" << ::strerror(errno); + } + } + } + } + auto appLayerItem = runContext.getCachedAppItem(); if (!appLayerItem) { return LINGLONG_ERR(appLayerItem); @@ -2211,6 +2242,28 @@ utils::error::Result Cli::generateLDCache(runtime::RunContext &runContext, process.args = std::vector{ "/sbin/ldconfig", "-X", "-C", "/run/linglong/cache/ld.so.cache" }; + { + // Ensure ldconfig inside the container has a sufficiently large FD limit. + // Raising RLIMIT_NOFILE only on the host process may not reliably propagate + // into the OCI process, depending on the runtime's defaults. + rlim_t target = 65535; + struct rlimit limit {}; + if (::getrlimit(RLIMIT_NOFILE, &limit) == 0) { + if (limit.rlim_max != RLIM_INFINITY) { + target = limit.rlim_max; + } + } + if (target == RLIM_INFINITY) { + target = 65535; + } + int64_t nofile = static_cast(target); + process.rlimits = std::vector{ + ocppi::runtime::config::types::Rlimit{ .hard = nofile, + .soft = nofile, + .type = "RLIMIT_NOFILE" }, + }; + } + ocppi::runtime::RunOption opt{}; auto result = (*container)->run(process, opt); if (!result) { @@ -2327,6 +2380,21 @@ int Cli::inspect(CLI::App *app, const InspectOptions &options) return 0; } +int Cli::extension(CLI::App *app, const ExtensionOptions &options) +{ + LINGLONG_TRACE("command extension"); + + auto argsParseFunc = [&app](const std::string &name) -> bool { + return app->get_subcommand(name)->parsed(); + }; + + if (argsParseFunc("import-cdi")) { + return importCdi(options); + } + + return 0; +} + int Cli::getLayerDir(const InspectOptions &options) { LINGLONG_TRACE("Get Layer dir"); @@ -2388,6 +2456,35 @@ int Cli::getBundleDir(const InspectOptions &options) return 0; } +int Cli::importCdi(const ExtensionOptions &options) +{ + LINGLONG_TRACE("import CDI config"); + + std::filesystem::path configPath; + if (options.configPath) { + configPath = *options.configPath; + } else { + auto userConfigPath = extension_override::getUserConfigPath(); + if (!userConfigPath) { + this->printer.printErr(LINGLONG_ERRV("failed to resolve user config path")); + return -1; + } + configPath = *userConfigPath; + } + + auto res = extension_override::importCdiOverrides(configPath, + options.cdiPath, + options.name, + !options.applyWhenInstalled); + if (!res) { + this->printer.printErr(res.error()); + return -1; + } + + this->printer.printMessage("CDI config imported into " + configPath.string()); + return 0; +} + utils::error::Result Cli::initInteraction() { LINGLONG_TRACE("initInteraction"); diff --git a/libs/linglong/src/linglong/cli/cli.h b/libs/linglong/src/linglong/cli/cli.h index d8a10ba63..e50b16306 100644 --- a/libs/linglong/src/linglong/cli/cli.h +++ b/libs/linglong/src/linglong/cli/cli.h @@ -134,6 +134,14 @@ struct InspectOptions std::string dirType{ "layer" }; }; +struct ExtensionOptions +{ + std::optional configPath; + std::optional cdiPath; + std::string name{ "org.deepin.driver.display.nvidia" }; + bool applyWhenInstalled{ false }; +}; + enum class TaskType : int { None, Install, @@ -186,6 +194,7 @@ class Cli : public QObject int content(const ContentOptions &options); int prune(); int inspect(CLI::App *subcommand, const InspectOptions &options); + int extension(CLI::App *subcommand, const ExtensionOptions &options); void cancelCurrentTask(); @@ -225,6 +234,7 @@ class Cli : public QObject std::vector getRunningAppContainers(const std::string &appid); int getLayerDir(const InspectOptions &options); int getBundleDir(const InspectOptions &options); + int importCdi(const ExtensionOptions &options); utils::error::Result initInteraction(); void detectDrivers(); diff --git a/libs/linglong/src/linglong/cli/extension_override.cpp b/libs/linglong/src/linglong/cli/extension_override.cpp new file mode 100644 index 000000000..70a36db44 --- /dev/null +++ b/libs/linglong/src/linglong/cli/extension_override.cpp @@ -0,0 +1,925 @@ +/* + * SPDX-FileCopyrightText: 2025 UnionTech Software Technology Co., Ltd. + * + * SPDX-License-Identifier: LGPL-3.0-or-later + */ + +#include "linglong/cli/extension_override.h" + +#include "linglong/extension/cdi.h" +#include "linglong/extension/extension.h" +#include "linglong/utils/command/cmd.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace linglong::cli::extension_override { + +namespace { + +using linglong::api::types::v1::DeviceNode; +using linglong::runtime::ExtensionOverride; +using linglong::extension::cdi::ContainerEdits; + + +template +std::optional readElfSonameImpl(std::ifstream &file) +{ + Ehdr header{}; + file.seekg(0); + file.read(reinterpret_cast(&header), sizeof(header)); + if (!file) { + return std::nullopt; + } + if (header.e_phoff == 0 || header.e_phnum == 0) { + return std::nullopt; + } + + file.seekg(header.e_phoff); + std::vector phdrs(header.e_phnum); + file.read(reinterpret_cast(phdrs.data()), sizeof(Phdr) * phdrs.size()); + if (!file) { + return std::nullopt; + } + + std::optional dynamicPhdr; + for (const auto &phdr : phdrs) { + if (phdr.p_type == PT_DYNAMIC) { + dynamicPhdr = phdr; + break; + } + } + if (!dynamicPhdr || dynamicPhdr->p_offset == 0 || dynamicPhdr->p_filesz == 0) { + return std::nullopt; + } + + file.seekg(dynamicPhdr->p_offset); + size_t entryCount = dynamicPhdr->p_filesz / sizeof(Dyn); + std::optional sonameOffset; + std::optional strtabAddr; + std::optional strtabSize; + for (size_t i = 0; i < entryCount; ++i) { + Dyn entry{}; + file.read(reinterpret_cast(&entry), sizeof(entry)); + if (!file) { + return std::nullopt; + } + if (entry.d_tag == DT_NULL) { + break; + } + if (entry.d_tag == DT_SONAME) { + sonameOffset = static_cast(entry.d_un.d_val); + } else if (entry.d_tag == DT_STRTAB) { + strtabAddr = static_cast(entry.d_un.d_ptr); + } else if (entry.d_tag == DT_STRSZ) { + strtabSize = static_cast(entry.d_un.d_val); + } + } + + if (!sonameOffset || !strtabAddr || !strtabSize) { + return std::nullopt; + } + + std::optional strtabOffset; + for (const auto &phdr : phdrs) { + if (phdr.p_type != PT_LOAD) { + continue; + } + auto begin = static_cast(phdr.p_vaddr); + auto end = begin + static_cast(phdr.p_memsz); + auto addr = static_cast(*strtabAddr); + if (addr >= begin && addr < end) { + strtabOffset = static_cast(phdr.p_offset) + (addr - begin); + break; + } + } + if (!strtabOffset) { + return std::nullopt; + } + + std::vector buffer(*strtabSize); + file.seekg(*strtabOffset); + file.read(buffer.data(), buffer.size()); + if (!file) { + return std::nullopt; + } + + if (*sonameOffset >= buffer.size()) { + return std::nullopt; + } + + const char *start = buffer.data() + *sonameOffset; + size_t maxLen = buffer.size() - *sonameOffset; + size_t len = strnlen(start, maxLen); + if (len == 0 || len == maxLen) { + return std::nullopt; + } + + return std::string(start, len); +} + +std::optional readElfSoname(const std::filesystem::path &path) +{ + std::ifstream file(path, std::ios::binary); + if (!file.is_open()) { + return std::nullopt; + } + + unsigned char ident[EI_NIDENT]{}; + file.read(reinterpret_cast(ident), sizeof(ident)); + if (!file) { + return std::nullopt; + } + if (ident[EI_MAG0] != ELFMAG0 || ident[EI_MAG1] != ELFMAG1 || ident[EI_MAG2] != ELFMAG2 + || ident[EI_MAG3] != ELFMAG3) { + return std::nullopt; + } + if (ident[EI_DATA] != ELFDATA2LSB) { + return std::nullopt; + } + + if (ident[EI_CLASS] == ELFCLASS64) { + return readElfSonameImpl(file); + } + if (ident[EI_CLASS] == ELFCLASS32) { + return readElfSonameImpl(file); + } + return std::nullopt; +} + + +bool isElf32(const std::filesystem::path &path) +{ + std::ifstream stream(path, std::ios::binary); + if (!stream.is_open()) { + return false; + } + + std::array header{}; + stream.read(reinterpret_cast(header.data()), header.size()); + if (!stream || header[0] != 0x7f || header[1] != 'E' || header[2] != 'L' || header[3] != 'F') { + return false; + } + + return header[4] == 1; +} + + + +std::string trim(const std::string &input) +{ + auto start = input.find_first_not_of(" \t\n\r"); + if (start == std::string::npos) { + return {}; + } + auto end = input.find_last_not_of(" \t\n\r"); + return input.substr(start, end - start + 1); +} + +bool isVendorLibraryName(const std::string &name) +{ + if (name.empty()) { + return false; + } + if (name.rfind("libnvme", 0) == 0) { + return false; + } + if (name.rfind("libcuda", 0) == 0) { + return true; + } + if (name.rfind("libnv", 0) == 0) { + return true; + } + return name.find("nvidia") != std::string::npos; +} + +std::unordered_map> readLdconfigCache() +{ + std::unordered_map> cache; + linglong::utils::command::Cmd cmd("ldconfig"); + if (!cmd.exists()) { + return cache; + } + auto output = cmd.exec(QStringList{ "-p" }); + if (!output) { + return cache; + } + std::stringstream stream(output->toStdString()); + std::string line; + while (std::getline(stream, line)) { + auto arrow = line.find("=>"); + if (arrow == std::string::npos) { + continue; + } + auto left = trim(line.substr(0, arrow)); + auto right = trim(line.substr(arrow + 2)); + if (left.empty() || right.empty()) { + continue; + } + auto space = left.find(' '); + std::string name = (space == std::string::npos) ? left : left.substr(0, space); + if (name.empty()) { + continue; + } + cache[name].push_back(std::filesystem::path(right)); + } + return cache; +} + +bool isPathUnder(const std::filesystem::path &path, const std::filesystem::path &dir) +{ + auto normalizedPath = path.lexically_normal().generic_string(); + auto normalizedDir = dir.lexically_normal().generic_string(); + if (normalizedPath == normalizedDir) { + return true; + } + if (!normalizedDir.empty() && normalizedDir.back() != '/') { + normalizedDir.push_back('/'); + } + return normalizedPath.rfind(normalizedDir, 0) == 0; +} + +std::string resolveExtensionMountName(const std::string &name) +{ + if (name != linglong::extension::ExtensionImplNVIDIADisplayDriver::Identify) { + return name; + } + + linglong::extension::ExtensionImplNVIDIADisplayDriver impl; + std::string resolved = name; + if (impl.shouldEnable(resolved)) { + return resolved; + } + return name; +} + + +utils::error::Result loadJsonFile(const std::filesystem::path &path) +{ + LINGLONG_TRACE("load extension override json file"); + + std::ifstream file(path); + if (!file.is_open()) { + return LINGLONG_ERR("failed to open file: " + path.string()); + } + + nlohmann::json root; + try { + file >> root; + } catch (const std::exception &ex) { + return LINGLONG_ERR(std::string("failed to parse json: ") + ex.what()); + } + + return root; +} + +utils::error::Result buildOverrideFromEdits(const ContainerEdits &edits, + const std::string &name, + bool fallbackOnly) +{ + LINGLONG_TRACE("build override from CDI edits"); + + if (name.empty()) { + return LINGLONG_ERR("extension name is empty"); + } + + nlohmann::json overrideJson = nlohmann::json::object(); + overrideJson["name"] = name; + overrideJson["fallback_only"] = fallbackOnly; + auto env = edits.env; + auto setEnvIfEmpty = [](std::map &envMap, + const std::string &key, + const std::string &value) { + if (value.empty()) { + return; + } + auto it = envMap.find(key); + if (it != envMap.end() && !it->second.empty()) { + return; + } + envMap[key] = value; + }; + auto appendEnvPath = [](std::map &envMap, + const std::string &key, + const std::vector &paths) { + if (paths.empty()) { + return; + } + std::vector ordered; + std::unordered_set seen; + for (const auto &path : paths) { + if (!path.empty() && seen.insert(path).second) { + ordered.push_back(path); + } + } + + auto it = envMap.find(key); + if (it != envMap.end() && !it->second.empty()) { + const auto ¤t = it->second; + size_t start = 0; + while (start <= current.size()) { + size_t end = current.find(':', start); + std::string segment = (end == std::string::npos) + ? current.substr(start) + : current.substr(start, end - start); + if (!segment.empty() && seen.insert(segment).second) { + ordered.push_back(segment); + } + if (end == std::string::npos) { + break; + } + start = end + 1; + } + } + + if (ordered.empty()) { + return; + } + std::string merged; + for (size_t i = 0; i < ordered.size(); ++i) { + if (i) { + merged.push_back(':'); + } + merged.append(ordered[i]); + } + envMap[key] = std::move(merged); + }; + if (!edits.mounts.empty()) { + const std::string mountName = resolveExtensionMountName(name); + const std::filesystem::path prefix = std::filesystem::path("/opt/extensions") / mountName; + std::vector candidateDirs; + std::unordered_set candidateDirSet; + auto addCandidateDir = [&](const std::filesystem::path &dir) { + if (dir.empty()) { + return; + } + auto value = dir.lexically_normal().string(); + if (candidateDirSet.insert(value).second) { + candidateDirs.push_back(dir); + } + }; + auto appendUnique = [](std::unordered_set &seen, + std::vector &ordered, + const std::string &value) { + if (!value.empty() && seen.insert(value).second) { + ordered.push_back(value); + } + }; + auto isUnderOptExtensions = [](const std::filesystem::path &path) { + const std::string prefix = "/opt/extensions/"; + auto value = path.generic_string(); + return value.rfind(prefix, 0) == 0; + }; + auto isLibraryCandidate = [](const std::filesystem::path &path) { + auto filename = path.filename().string(); + if (filename.rfind("lib", 0) == 0 && filename.find(".so") != std::string::npos) { + return true; + } + return false; + }; + + auto mapLibraryDestination = [&](const std::string &source) + -> std::optional { + if (source.empty()) { + return std::nullopt; + } + std::filesystem::path sourcePath{ source }; + std::error_code ec; + if (!std::filesystem::is_regular_file(sourcePath, ec) + && !std::filesystem::is_symlink(sourcePath, ec)) { + return std::nullopt; + } + if (!isLibraryCandidate(sourcePath)) { + return std::nullopt; + } + auto filename = sourcePath.filename(); + if (filename.empty()) { + return std::nullopt; + } + std::string destName = filename.string(); + if (auto soname = readElfSoname(sourcePath)) { + if (!soname->empty()) { + destName = *soname; + } + } + bool is32 = isElf32(sourcePath); + auto destDir = prefix / (is32 ? "orig/32" : "orig"); + return destDir / destName; + }; + + nlohmann::json mounts = nlohmann::json::array(); + std::unordered_set mountKeySet; + std::unordered_set mountDestSet; + std::unordered_set libDirSet; + std::unordered_set eglExternalDirSet; + std::unordered_set eglVendorDirSet; + std::unordered_set vkIcdFileSet; + std::optional nvidiaSmiSource; + std::vector libDirs; + std::vector eglExternalDirs; + std::vector eglVendorDirs; + std::vector vkIcdFiles; + bool hasGlxLib = false; + auto addMount = [&](const std::filesystem::path &destination, + const std::string &source, + const std::optional &type, + const std::optional> &options) { + if (source.empty()) { + return; + } + auto destKey = destination.string(); + if (!mountDestSet.insert(destKey).second) { + return; + } + std::string key = destKey + "|" + source; + if (!mountKeySet.insert(key).second) { + return; + } + nlohmann::json mountJson = nlohmann::json::object(); + mountJson["source"] = source; + mountJson["destination"] = destination.string(); + if (type && !type->empty()) { + mountJson["type"] = *type; + } + if (options && !options->empty()) { + mountJson["options"] = *options; + } + mounts.push_back(std::move(mountJson)); + }; + + auto isRegularOrSymlink = [](const std::filesystem::path &path) { + std::error_code ec; + return std::filesystem::is_regular_file(path, ec) || std::filesystem::is_symlink(path, ec); + }; + + + auto findNvidiaSmiFallback = [&](const std::vector &dirs) + -> std::optional { + std::vector candidates; + std::unordered_set seen; + auto addCandidate = [&](const std::filesystem::path &path) { + if (path.empty()) { + return; + } + auto value = path.lexically_normal().string(); + if (seen.insert(value).second) { + candidates.push_back(path); + } + }; + + addCandidate("/usr/bin/nvidia-smi"); + addCandidate("/usr/sbin/nvidia-smi"); + addCandidate("/sbin/nvidia-smi"); + addCandidate("/usr/local/bin/nvidia-smi"); + + for (const auto &dir : dirs) { + addCandidate(dir / "nvidia-smi"); + } + + for (const auto &candidate : candidates) { + if (isRegularOrSymlink(candidate)) { + return candidate.string(); + } + } + + return std::nullopt; + }; + + auto considerSourcePath = [&](const std::string &source) { + if (source.empty()) { + return; + } + std::filesystem::path sourcePath{ source }; + if (sourcePath.filename() == "nvidia-smi") { + if (isRegularOrSymlink(sourcePath)) { + nvidiaSmiSource = source; + } + } + std::error_code ec; + if (std::filesystem::is_directory(sourcePath, ec)) { + addCandidateDir(sourcePath); + return; + } + if (!isRegularOrSymlink(sourcePath)) { + return; + } + addCandidateDir(sourcePath.parent_path()); + }; + + auto recordMount = [&](const std::filesystem::path &destPath, + const std::string &source, + const std::optional &type, + const std::optional> &options) { + addMount(destPath, source, type, options); + + bool underOptExtensions = isUnderOptExtensions(destPath); + if (underOptExtensions && isLibraryCandidate(destPath)) { + auto dir = destPath.parent_path().string(); + appendUnique(libDirSet, libDirs, dir); + auto filename = destPath.filename().string(); + if (filename.rfind("libGLX_nvidia", 0) == 0) { + hasGlxLib = true; + } + } + + if (underOptExtensions) { + auto parent = destPath.parent_path(); + if (parent.filename() == "egl_external_platform.d") { + appendUnique(eglExternalDirSet, eglExternalDirs, parent.string()); + } else if (parent.filename() == "egl_vendor.d") { + appendUnique(eglVendorDirSet, eglVendorDirs, parent.string()); + } else if (parent.filename() == "icd.d" + && parent.parent_path().filename() == "vulkan") { + appendUnique(vkIcdFileSet, vkIcdFiles, destPath.string()); + } + } + }; + + for (const auto &mount : edits.mounts) { + std::string source = mount.source.value_or(""); + considerSourcePath(source); + std::filesystem::path destPath{ mount.destination }; + if (!mount.destination.empty()) { + if (auto mapped = mapLibraryDestination(source)) { + destPath = *mapped; + } else if (!isUnderOptExtensions(destPath) && destPath.is_absolute()) { + std::filesystem::path remapRel = destPath.relative_path(); + auto normalized = destPath.lexically_normal(); + if (normalized == "/usr/lib" + || normalized.generic_string().rfind("/usr/lib/", 0) == 0) { + auto rel = normalized.lexically_relative("/usr/lib"); + if (!rel.empty() && rel.native() != "..") { + remapRel = rel; + } + } + destPath = prefix / remapRel; + } + } + recordMount(destPath, source, mount.type, mount.options); + } + + + if (!nvidiaSmiSource && extension::isNvidiaDisplayDriverExtension(name)) { + nvidiaSmiSource = findNvidiaSmiFallback(candidateDirs); + } + + std::vector vendorLibPaths; + std::unordered_set vendorLibSet; + auto addVendorLib = [&](const std::filesystem::path &path) { + if (path.empty()) { + return; + } + auto key = path.lexically_normal().string(); + if (!vendorLibSet.insert(key).second) { + return; + } + vendorLibPaths.push_back(path); + }; + auto ldconfigCache = readLdconfigCache(); + if (!ldconfigCache.empty()) { + for (const auto &entry : ldconfigCache) { + if (!isVendorLibraryName(entry.first)) { + continue; + } + for (const auto &path : entry.second) { + for (const auto &dir : candidateDirs) { + if (isPathUnder(path, dir)) { + addVendorLib(path); + break; + } + } + } + } + } + if (vendorLibPaths.empty()) { + for (const auto &dir : candidateDirs) { + std::error_code ec; + for (const auto &entry : std::filesystem::directory_iterator(dir, ec)) { + if (ec) { + break; + } + const auto &path = entry.path(); + if (!isLibraryCandidate(path)) { + continue; + } + if (!isVendorLibraryName(path.filename().string())) { + continue; + } + if (isRegularOrSymlink(path)) { + addVendorLib(path); + } + } + } + } + + const std::optional> vendorOptions = + std::vector{ "rbind", "ro" }; + if (nvidiaSmiSource) { + recordMount(std::filesystem::path("/usr/bin/nvidia-smi"), + *nvidiaSmiSource, + std::nullopt, + vendorOptions); + } + for (const auto &path : vendorLibPaths) { + if (auto mapped = mapLibraryDestination(path.string())) { + recordMount(*mapped, path.string(), std::nullopt, vendorOptions); + } + } + + overrideJson["mounts"] = std::move(mounts); + appendEnvPath(env, "LD_LIBRARY_PATH", libDirs); + appendEnvPath(env, "EGL_EXTERNAL_PLATFORM_CONFIG_DIRS", eglExternalDirs); + appendEnvPath(env, "__EGL_EXTERNAL_PLATFORM_CONFIG_DIRS", eglExternalDirs); + appendEnvPath(env, "__EGL_VENDOR_LIBRARY_DIRS", eglVendorDirs); + appendEnvPath(env, "VK_ICD_FILENAMES", vkIcdFiles); + appendEnvPath(env, "VK_ADD_DRIVER_FILES", vkIcdFiles); + if (!libDirs.empty()) { + env["NVIDIA_CTK_LIBCUDA_DIR"] = (prefix / "orig").string(); + } + if (hasGlxLib) { + setEnvIfEmpty(env, "__GLX_VENDOR_LIBRARY_NAME", "nvidia"); + setEnvIfEmpty(env, "__NV_PRIME_RENDER_OFFLOAD", "1"); + } + } + if (!edits.deviceNodes.empty()) { + nlohmann::json nodes = nlohmann::json::array(); + for (const auto &node : edits.deviceNodes) { + nlohmann::json nodeJson = nlohmann::json::object(); + nodeJson["path"] = node.path; + if (node.hostPath) { + nodeJson["hostPath"] = *node.hostPath; + } + nodes.push_back(std::move(nodeJson)); + } + overrideJson["device_nodes"] = std::move(nodes); + } + + if (!env.empty()) { + overrideJson["env"] = std::move(env); + } + + return overrideJson; +} + +std::vector parseOverridesFromRoot(const nlohmann::json &root) +{ + std::vector overrides; + auto overridesJson = root.find("extension_overrides"); + if (overridesJson == root.end() || !overridesJson->is_array()) { + return overrides; + } + + for (const auto &item : *overridesJson) { + if (!item.is_object()) { + continue; + } + + std::string name = item.value("name", ""); + if (name.empty()) { + continue; + } + + ExtensionOverride overrideDef{ + .name = std::move(name), + .fallbackOnly = item.value("fallback_only", false), + }; + + auto env = item.find("env"); + if (env != item.end() && env->is_object()) { + for (auto it = env->begin(); it != env->end(); ++it) { + if (it.value().is_string()) { + overrideDef.env.emplace(it.key(), it.value().get()); + } + } + } + + auto mounts = item.find("mounts"); + if (mounts != item.end() && mounts->is_array()) { + for (const auto &mountItem : *mounts) { + if (!mountItem.is_object()) { + continue; + } + std::string source = mountItem.value("source", ""); + std::string destination = mountItem.value("destination", ""); + if (source.empty() || destination.empty()) { + continue; + } + + std::vector options; + auto optionsJson = mountItem.find("options"); + if (optionsJson != mountItem.end()) { + if (optionsJson->is_array()) { + for (const auto &opt : *optionsJson) { + if (opt.is_string()) { + options.emplace_back(opt.get()); + } + } + } else if (optionsJson->is_string()) { + options.emplace_back(optionsJson->get()); + } + } + if (options.empty()) { + options = { "rbind", "ro" }; + } + + ocppi::runtime::config::types::Mount mount{ + .destination = destination, + .options = options, + .source = source, + .type = mountItem.value("type", "bind"), + }; + overrideDef.mounts.push_back(std::move(mount)); + } + } + + auto deviceNodes = item.find("device_nodes"); + if (deviceNodes != item.end() && deviceNodes->is_array()) { + for (const auto &nodeItem : *deviceNodes) { + if (!nodeItem.is_object()) { + continue; + } + std::string path = nodeItem.value("path", ""); + if (path.empty()) { + continue; + } + + std::optional hostPath; + auto hostPathValue = nodeItem.find("hostPath"); + if (hostPathValue != nodeItem.end() && hostPathValue->is_string()) { + hostPath = hostPathValue->get(); + } else { + auto hostPathAlt = nodeItem.find("host_path"); + if (hostPathAlt != nodeItem.end() && hostPathAlt->is_string()) { + hostPath = hostPathAlt->get(); + } + } + + overrideDef.deviceNodes.emplace_back(DeviceNode{ + .hostPath = hostPath, + .path = path, + }); + } + } + + overrides.push_back(std::move(overrideDef)); + } + + return overrides; +} + +} // namespace + +std::optional getUserConfigPath() noexcept +{ + const char *configHome = std::getenv("XDG_CONFIG_HOME"); + std::filesystem::path base; + if (configHome && configHome[0] != '\0') { + base = std::filesystem::path{ configHome }; + } else { + const char *home = std::getenv("HOME"); + if (!home || home[0] == '\0') { + return std::nullopt; + } + base = std::filesystem::path{ home } / ".config"; + } + return base / "linglong" / "config.json"; +} + +utils::error::Result> +loadOverrides(const std::filesystem::path &configPath) noexcept +{ + LINGLONG_TRACE("load extension overrides"); + + std::error_code ec; + if (!std::filesystem::exists(configPath, ec) || ec) { + return std::vector{}; + } + + auto root = loadJsonFile(configPath); + if (!root) { + return LINGLONG_ERR(root.error()); + } + + if (!root->is_object()) { + return std::vector{}; + } + + return parseOverridesFromRoot(*root); +} + +utils::error::Result> +loadCdiOverrides(const std::optional &cdiPath, + const std::string &name, + bool fallbackOnly) noexcept +{ + LINGLONG_TRACE("load CDI overrides"); + + auto edits = cdiPath ? linglong::extension::cdi::loadFromFile(*cdiPath) + : linglong::extension::cdi::loadFromNvidiaCtk(); + if (!edits) { + return LINGLONG_ERR(edits.error()); + } + + auto overrideJson = buildOverrideFromEdits(*edits, name, fallbackOnly); + if (!overrideJson) { + return LINGLONG_ERR(overrideJson.error()); + } + + nlohmann::json root = nlohmann::json::object(); + root["extension_overrides"] = nlohmann::json::array({ *overrideJson }); + return parseOverridesFromRoot(root); +} + +utils::error::Result importCdiOverrides(const std::filesystem::path &configPath, + const std::optional &cdiPath, + const std::string &name, + bool fallbackOnly) noexcept +{ + LINGLONG_TRACE("import CDI overrides"); + + utils::error::Result edits = LINGLONG_OK; + if (cdiPath) { + edits = linglong::extension::cdi::loadFromFile(*cdiPath); + } else { + edits = linglong::extension::cdi::loadFromNvidiaCtk(); + if (!edits) { + // nvidia-ctk is optional; fallback to builtin host discovery. + if (std::string(edits.error().message()).find("nvidia-ctk not found") + != std::string::npos) { + edits = linglong::extension::cdi::loadFromHostNvidia(); + } + } + } + if (!edits) { + return LINGLONG_ERR(edits.error()); + } + + auto overrideItem = buildOverrideFromEdits(*edits, name, fallbackOnly); + if (!overrideItem) { + return LINGLONG_ERR(overrideItem.error()); + } + + nlohmann::json root = nlohmann::json::object(); + std::error_code ec; + if (std::filesystem::exists(configPath, ec) && !ec) { + auto loaded = loadJsonFile(configPath); + if (!loaded) { + return LINGLONG_ERR(loaded.error()); + } + root = std::move(*loaded); + if (!root.is_object()) { + root = nlohmann::json::object(); + } + } + + nlohmann::json overrides = nlohmann::json::array(); + auto existingOverrides = root.find("extension_overrides"); + if (existingOverrides != root.end() && existingOverrides->is_array()) { + overrides = *existingOverrides; + } + + bool replaced = false; + for (auto &item : overrides) { + if (!item.is_object()) { + continue; + } + if (item.value("name", "") == name) { + item = std::move(*overrideItem); + replaced = true; + break; + } + } + if (!replaced) { + overrides.push_back(std::move(*overrideItem)); + } + root["extension_overrides"] = std::move(overrides); + + auto configDir = configPath.parent_path(); + if (!configDir.empty()) { + std::filesystem::create_directories(configDir, ec); + if (ec) { + return LINGLONG_ERR("failed to create config directory: " + ec.message()); + } + } + + std::ofstream out(configPath); + if (!out.is_open()) { + return LINGLONG_ERR("failed to write config: " + configPath.string()); + } + out << root.dump(2); + return LINGLONG_OK; +} + +} // namespace linglong::cli::extension_override diff --git a/libs/linglong/src/linglong/cli/extension_override.h b/libs/linglong/src/linglong/cli/extension_override.h new file mode 100644 index 000000000..389eb7806 --- /dev/null +++ b/libs/linglong/src/linglong/cli/extension_override.h @@ -0,0 +1,36 @@ +/* + * SPDX-FileCopyrightText: 2025 UnionTech Software Technology Co., Ltd. + * + * SPDX-License-Identifier: LGPL-3.0-or-later + */ + +#pragma once + +#include "linglong/runtime/run_context.h" +#include "linglong/utils/error/error.h" + +#include +#include +#include +#include + +namespace linglong::cli::extension_override { + +std::optional getUserConfigPath() noexcept; + +utils::error::Result> +loadOverrides(const std::filesystem::path &configPath) noexcept; + +// Build overrides in-memory from CDI container edits. +// This is the same rule used by `ll-cli extension import-cdi`, but does not write to config.json. +utils::error::Result> +loadCdiOverrides(const std::optional &cdiPath, + const std::string &name, + bool fallbackOnly) noexcept; + +utils::error::Result importCdiOverrides(const std::filesystem::path &configPath, + const std::optional &cdiPath, + const std::string &name, + bool fallbackOnly) noexcept; + +} // namespace linglong::cli::extension_override diff --git a/libs/linglong/src/linglong/extension/cdi.cpp b/libs/linglong/src/linglong/extension/cdi.cpp new file mode 100644 index 000000000..f5c4661fb --- /dev/null +++ b/libs/linglong/src/linglong/extension/cdi.cpp @@ -0,0 +1,463 @@ +/* + * SPDX-FileCopyrightText: 2025 UnionTech Software Technology Co., Ltd. + * + * SPDX-License-Identifier: LGPL-3.0-or-later + */ + +#include "linglong/extension/cdi.h" + +#include "linglong/utils/command/cmd.h" + +#include + +#include +#include + +namespace linglong::extension::cdi { + +namespace { + +using linglong::utils::command::Cmd; + +utils::error::Result loadJsonFile(const std::filesystem::path &path) +{ + LINGLONG_TRACE("load CDI json file"); + + std::ifstream file(path); + if (!file.is_open()) { + return LINGLONG_ERR("failed to open file: " + path.string()); + } + + nlohmann::json root; + try { + file >> root; + } catch (const std::exception &ex) { + return LINGLONG_ERR(std::string("failed to parse json: ") + ex.what()); + } + + return root; +} + +utils::error::Result loadJsonString(const std::string &text) +{ + LINGLONG_TRACE("parse CDI json string"); + + nlohmann::json root; + try { + root = nlohmann::json::parse(text); + } catch (const std::exception &ex) { + auto extractPayload = [&](char open, char close) -> std::optional { + auto start = text.find(open); + if (start == std::string::npos) { + return std::nullopt; + } + + int depth = 0; + bool inString = false; + bool escaped = false; + for (size_t i = start; i < text.size(); ++i) { + char c = text[i]; + if (inString) { + if (escaped) { + escaped = false; + } else if (c == '\\') { + escaped = true; + } else if (c == '"') { + inString = false; + } + continue; + } + if (c == '"') { + inString = true; + continue; + } + if (c == open) { + ++depth; + } else if (c == close) { + --depth; + if (depth == 0) { + return text.substr(start, i - start + 1); + } + } + } + return std::nullopt; + }; + + std::optional payload = extractPayload('{', '}'); + if (!payload) { + payload = extractPayload('[', ']'); + } + if (!payload) { + return LINGLONG_ERR(std::string("failed to parse json: ") + ex.what() + + "; no JSON payload found"); + } + + try { + root = nlohmann::json::parse(*payload); + } catch (const std::exception &inner) { + return LINGLONG_ERR(std::string("failed to parse json: ") + ex.what() + + "; fallback parse failed: " + inner.what()); + } + } + return root; +} + +void mergeEnvFromJson(const nlohmann::json &envJson, std::map &env) +{ + if (envJson.is_object()) { + for (auto it = envJson.begin(); it != envJson.end(); ++it) { + if (it.value().is_string()) { + env[it.key()] = it.value().get(); + } + } + return; + } + + if (!envJson.is_array()) { + return; + } + + for (const auto &item : envJson) { + if (!item.is_string()) { + continue; + } + const auto entry = item.get(); + auto pos = entry.find('='); + if (pos == std::string::npos || pos == 0) { + continue; + } + env[entry.substr(0, pos)] = entry.substr(pos + 1); + } +} + +void mergeMountsFromJson(const nlohmann::json &mountsJson, + std::vector &mounts, + std::unordered_set &seen) +{ + if (!mountsJson.is_array()) { + return; + } + + for (const auto &item : mountsJson) { + if (!item.is_object()) { + continue; + } + std::string source = item.value("hostPath", ""); + if (source.empty()) { + source = item.value("source", ""); + } + std::string destination = item.value("containerPath", ""); + if (destination.empty()) { + destination = item.value("destination", ""); + } + if (source.empty() || destination.empty()) { + continue; + } + + std::string key = destination + "|" + source; + if (!seen.insert(key).second) { + continue; + } + + std::vector options; + auto optionsJson = item.find("options"); + if (optionsJson != item.end()) { + if (optionsJson->is_array()) { + for (const auto &opt : *optionsJson) { + if (opt.is_string()) { + options.emplace_back(opt.get()); + } + } + } else if (optionsJson->is_string()) { + options.emplace_back(optionsJson->get()); + } + } + if (options.empty()) { + options = { "rbind", "ro" }; + } + + std::string type = item.value("type", "bind"); + mounts.push_back(ocppi::runtime::config::types::Mount{ + .destination = destination, + .options = options, + .source = source, + .type = type, + }); + } +} + +void mergeDeviceNodesFromJson(const nlohmann::json &nodesJson, + std::vector &nodes, + std::unordered_set &seen) +{ + if (!nodesJson.is_array()) { + return; + } + + for (const auto &item : nodesJson) { + if (!item.is_object()) { + continue; + } + std::string path = item.value("path", ""); + if (path.empty()) { + continue; + } + if (!seen.insert(path).second) { + continue; + } + + std::optional hostPath; + auto hostPathValue = item.find("hostPath"); + if (hostPathValue != item.end() && hostPathValue->is_string()) { + hostPath = hostPathValue->get(); + } else { + auto hostPathAlt = item.find("host_path"); + if (hostPathAlt != item.end() && hostPathAlt->is_string()) { + hostPath = hostPathAlt->get(); + } + } + + nodes.emplace_back(api::types::v1::DeviceNode{ + .hostPath = hostPath, + .path = path, + }); + } +} + +void mergeContainerEdits(const nlohmann::json &editsJson, + ContainerEdits &edits, + std::unordered_set &mountKeys, + std::unordered_set &nodeKeys) +{ + if (!editsJson.is_object()) { + return; + } + + auto env = editsJson.find("env"); + if (env != editsJson.end()) { + mergeEnvFromJson(*env, edits.env); + } + + auto mounts = editsJson.find("mounts"); + if (mounts != editsJson.end()) { + mergeMountsFromJson(*mounts, edits.mounts, mountKeys); + } + + auto deviceNodes = editsJson.find("deviceNodes"); + if (deviceNodes != editsJson.end()) { + mergeDeviceNodesFromJson(*deviceNodes, edits.deviceNodes, nodeKeys); + } +} + +utils::error::Result parseContainerEdits(const nlohmann::json &cdi) +{ + LINGLONG_TRACE("parse CDI container edits"); + + ContainerEdits edits; + std::unordered_set mountKeys; + std::unordered_set nodeKeys; + + auto mergeSpec = [&](const nlohmann::json &spec) { + if (!spec.is_object()) { + return; + } + auto containerEdits = spec.find("containerEdits"); + if (containerEdits != spec.end()) { + mergeContainerEdits(*containerEdits, edits, mountKeys, nodeKeys); + } + auto devices = spec.find("devices"); + if (devices != spec.end() && devices->is_array()) { + for (const auto &device : *devices) { + if (!device.is_object()) { + continue; + } + auto deviceEdits = device.find("containerEdits"); + if (deviceEdits != device.end()) { + mergeContainerEdits(*deviceEdits, edits, mountKeys, nodeKeys); + } + } + } + }; + + if (cdi.is_array()) { + for (const auto &spec : cdi) { + mergeSpec(spec); + } + } else { + mergeSpec(cdi); + } + + if (edits.empty()) { + return LINGLONG_ERR("no containerEdits found in CDI spec; " + "please provide a valid CDI JSON"); + } + + return edits; +} + +} // namespace + +utils::error::Result loadFromFile(const std::filesystem::path &path) noexcept +{ + LINGLONG_TRACE("load CDI from file"); + + auto root = loadJsonFile(path); + if (!root) { + return LINGLONG_ERR(root.error()); + } + + return parseContainerEdits(*root); +} + +utils::error::Result loadFromJson(const std::string &jsonText) noexcept +{ + LINGLONG_TRACE("load CDI from json string"); + + auto root = loadJsonString(jsonText); + if (!root) { + return LINGLONG_ERR(root.error()); + } + + return parseContainerEdits(*root); +} + +utils::error::Result loadFromNvidiaCtk() noexcept +{ + LINGLONG_TRACE("load CDI from nvidia-ctk"); + + Cmd cmd("nvidia-ctk"); + if (!cmd.exists()) { + return LINGLONG_ERR("nvidia-ctk not found"); + } + + auto output = cmd.exec({ "cdi", "generate", "--format", "json" }); + if (!output) { + return LINGLONG_ERR(output.error()); + } + + return loadFromJson(output->toStdString()); +} + +utils::error::Result loadFromHostNvidia() noexcept +{ + LINGLONG_TRACE("load CDI from host NVIDIA (builtin)"); + + ContainerEdits edits; + + auto addMountIfExists = [&](const std::filesystem::path &path, + const std::vector &options, + bool noexec = false) { + if (path.empty()) { + return; + } + std::error_code ec; + if (!std::filesystem::exists(path, ec) || ec) { + return; + } + std::vector opts = options; + if (noexec) { + opts.push_back("noexec"); + } + edits.mounts.push_back(ocppi::runtime::config::types::Mount{ + .destination = path.string(), + .options = opts, + .source = path.string(), + .type = "bind", + }); + }; + + // Options modeled after nvidia-ctk generated CDI mounts. + const std::vector kRoDirOptions = { + "ro", + "nosuid", + "nodev", + "rbind", + "rprivate", + }; + + // Common NVIDIA-related directories (best-effort). + addMountIfExists("/sbin", kRoDirOptions); + addMountIfExists("/usr/bin", kRoDirOptions); + addMountIfExists("/run/nvidia-persistenced", kRoDirOptions, true); + + addMountIfExists("/usr/share/vulkan/icd.d", kRoDirOptions); + addMountIfExists("/usr/share/vulkan/implicit_layer.d", kRoDirOptions); + addMountIfExists("/usr/share/egl/egl_external_platform.d", kRoDirOptions); + addMountIfExists("/usr/share/glvnd/egl_vendor.d", kRoDirOptions); + + addMountIfExists("/usr/share/nvidia", kRoDirOptions); + addMountIfExists("/usr/share/X11/xorg.conf.d", kRoDirOptions); + addMountIfExists("/usr/lib/xorg/modules/drivers", kRoDirOptions); + + // Library roots + addMountIfExists("/usr/lib/x86_64-linux-gnu", kRoDirOptions); + addMountIfExists("/usr/lib/x86_64-linux-gnu/nvidia/current", kRoDirOptions); + addMountIfExists("/usr/lib/i386-linux-gnu", kRoDirOptions); + addMountIfExists("/usr/lib/i386-linux-gnu/nvidia/current", kRoDirOptions); + + // Firmware directory (versioned). Best-effort by scanning /lib/firmware/nvidia/* + { + std::error_code ec; + const std::filesystem::path fwRoot = "/lib/firmware/nvidia"; + if (std::filesystem::exists(fwRoot, ec) && std::filesystem::is_directory(fwRoot, ec)) { + // Prefer the first versioned subdir that looks like digits.digits.digits + for (const auto &entry : std::filesystem::directory_iterator(fwRoot, ec)) { + if (ec) { + break; + } + if (!entry.is_directory(ec)) { + ec.clear(); + continue; + } + addMountIfExists(entry.path(), kRoDirOptions); + break; + } + } + } + + // Devices: follow nvidia-ctk behavior: mount device nodes explicitly. + auto addDevNodeIfExists = [&](const std::string &path) { + std::error_code ec; + if (!std::filesystem::exists(path, ec) || ec) { + return; + } + edits.deviceNodes.push_back(api::types::v1::DeviceNode{ + .hostPath = path, + .path = path, + }); + }; + + addDevNodeIfExists("/dev/nvidiactl"); + addDevNodeIfExists("/dev/nvidia0"); + addDevNodeIfExists("/dev/nvidia1"); + addDevNodeIfExists("/dev/nvidia-modeset"); + addDevNodeIfExists("/dev/nvidia-uvm"); + addDevNodeIfExists("/dev/nvidia-uvm-tools"); + + { + std::error_code ec; + const std::filesystem::path driDir = "/dev/dri"; + if (std::filesystem::exists(driDir, ec) && std::filesystem::is_directory(driDir, ec)) { + for (const auto &entry : std::filesystem::directory_iterator(driDir, ec)) { + if (ec) { + break; + } + auto name = entry.path().filename().string(); + if (name.rfind("card", 0) == 0 || name.rfind("renderD", 0) == 0) { + addDevNodeIfExists(entry.path().string()); + } + } + } + } + + // Match nvidia-ctk environment seen in CDI mode + edits.env["NVIDIA_VISIBLE_DEVICES"] = "void"; + + if (edits.empty()) { + return LINGLONG_ERR("no NVIDIA files/devices found for builtin CDI fallback"); + } + + return edits; +} + +} // namespace linglong::extension::cdi diff --git a/libs/linglong/src/linglong/extension/cdi.h b/libs/linglong/src/linglong/extension/cdi.h new file mode 100644 index 000000000..1e89ccc43 --- /dev/null +++ b/libs/linglong/src/linglong/extension/cdi.h @@ -0,0 +1,40 @@ +/* + * SPDX-FileCopyrightText: 2025 UnionTech Software Technology Co., Ltd. + * + * SPDX-License-Identifier: LGPL-3.0-or-later + */ + +#pragma once + +#include "linglong/api/types/v1/DeviceNode.hpp" +#include "linglong/utils/error/error.h" +#include "ocppi/runtime/config/types/Mount.hpp" + +#include +#include +#include +#include + +namespace linglong::extension::cdi { + +struct ContainerEdits +{ + std::map env; + std::vector mounts; + std::vector deviceNodes; + + bool empty() const noexcept + { + return env.empty() && mounts.empty() && deviceNodes.empty(); + } +}; + +utils::error::Result loadFromFile(const std::filesystem::path &path) noexcept; +utils::error::Result loadFromJson(const std::string &jsonText) noexcept; +utils::error::Result loadFromNvidiaCtk() noexcept; + +// Built-in NVIDIA CDI-like discovery without relying on nvidia-ctk. +// Intended as a fallback for `ll-cli extension import-cdi`. +utils::error::Result loadFromHostNvidia() noexcept; + +} // namespace linglong::extension::cdi diff --git a/libs/linglong/src/linglong/extension/extension.cpp b/libs/linglong/src/linglong/extension/extension.cpp index ad9753ba6..a82e639df 100644 --- a/libs/linglong/src/linglong/extension/extension.cpp +++ b/libs/linglong/src/linglong/extension/extension.cpp @@ -6,11 +6,562 @@ #include "extension.h" +#include "linglong/common/strings.h" +#include "linglong/utils/log/log.h" + #include +#include +#include +#include +#include +#include +#include +#include #include +#include +#include +#include namespace linglong::extension { +namespace { + +std::string toLower(std::string_view input) +{ + std::string out; + out.reserve(input.size()); + for (unsigned char ch : input) { + out.push_back(static_cast(std::tolower(ch))); + } + return out; +} + +bool isElf32(const std::filesystem::path &path) +{ + std::ifstream stream(path, std::ios::binary); + if (!stream.is_open()) { + return false; + } + + std::array header{}; + stream.read(reinterpret_cast(header.data()), header.size()); + if (!stream || header[0] != 0x7f || header[1] != 'E' || header[2] != 'L' || header[3] != 'F') { + return false; + } + + return header[4] == 1; +} + +template +std::optional readElfSonameImpl(std::ifstream &file) +{ + Ehdr header{}; + file.seekg(0); + file.read(reinterpret_cast(&header), sizeof(header)); + if (!file) { + return std::nullopt; + } + if (header.e_phoff == 0 || header.e_phnum == 0) { + return std::nullopt; + } + + file.seekg(header.e_phoff); + std::vector phdrs(header.e_phnum); + file.read(reinterpret_cast(phdrs.data()), sizeof(Phdr) * phdrs.size()); + if (!file) { + return std::nullopt; + } + + std::optional dynamicPhdr; + for (const auto &phdr : phdrs) { + if (phdr.p_type == PT_DYNAMIC) { + dynamicPhdr = phdr; + break; + } + } + if (!dynamicPhdr || dynamicPhdr->p_offset == 0 || dynamicPhdr->p_filesz == 0) { + return std::nullopt; + } + + file.seekg(dynamicPhdr->p_offset); + size_t entryCount = dynamicPhdr->p_filesz / sizeof(Dyn); + std::optional sonameOffset; + std::optional strtabAddr; + std::optional strtabSize; + for (size_t i = 0; i < entryCount; ++i) { + Dyn entry{}; + file.read(reinterpret_cast(&entry), sizeof(entry)); + if (!file) { + return std::nullopt; + } + if (entry.d_tag == DT_NULL) { + break; + } + if (entry.d_tag == DT_SONAME) { + sonameOffset = static_cast(entry.d_un.d_val); + } else if (entry.d_tag == DT_STRTAB) { + strtabAddr = static_cast(entry.d_un.d_ptr); + } else if (entry.d_tag == DT_STRSZ) { + strtabSize = static_cast(entry.d_un.d_val); + } + } + + if (!sonameOffset || !strtabAddr || !strtabSize) { + return std::nullopt; + } + + std::optional strtabOffset; + for (const auto &phdr : phdrs) { + if (phdr.p_type != PT_LOAD) { + continue; + } + auto begin = static_cast(phdr.p_vaddr); + auto end = begin + static_cast(phdr.p_memsz); + auto addr = static_cast(*strtabAddr); + if (addr >= begin && addr < end) { + strtabOffset = static_cast(phdr.p_offset) + (addr - begin); + break; + } + } + if (!strtabOffset) { + return std::nullopt; + } + + std::vector buffer(*strtabSize); + file.seekg(*strtabOffset); + file.read(buffer.data(), buffer.size()); + if (!file) { + return std::nullopt; + } + + if (*sonameOffset >= buffer.size()) { + return std::nullopt; + } + + const char *start = buffer.data() + *sonameOffset; + size_t maxLen = buffer.size() - *sonameOffset; + size_t len = strnlen(start, maxLen); + if (len == 0 || len == maxLen) { + return std::nullopt; + } + + return std::string(start, len); +} + +std::optional readElfSoname(const std::filesystem::path &path) +{ + std::ifstream file(path, std::ios::binary); + if (!file.is_open()) { + return std::nullopt; + } + + unsigned char ident[EI_NIDENT]{}; + file.read(reinterpret_cast(ident), sizeof(ident)); + if (!file) { + return std::nullopt; + } + if (ident[EI_MAG0] != ELFMAG0 || ident[EI_MAG1] != ELFMAG1 || ident[EI_MAG2] != ELFMAG2 + || ident[EI_MAG3] != ELFMAG3) { + return std::nullopt; + } + if (ident[EI_DATA] != ELFDATA2LSB) { + return std::nullopt; + } + + if (ident[EI_CLASS] == ELFCLASS64) { + return readElfSonameImpl(file); + } + if (ident[EI_CLASS] == ELFCLASS32) { + return readElfSonameImpl(file); + } + return std::nullopt; +} + + +bool isSimpleSoName(const std::string &filename) +{ + auto pos = filename.find(".so"); + if (pos == std::string::npos) { + return false; + } + pos += 3; + if (pos == filename.size()) { + return true; + } + if (filename[pos] != '.') { + return false; + } + for (size_t i = pos + 1; i < filename.size(); ++i) { + if (!std::isdigit(static_cast(filename[i]))) { + return false; + } + } + return true; +} + +struct NvidiaLibEntry +{ + std::filesystem::path source; + std::string name; + std::vector aliases; + bool is32{ false }; +}; + +std::vector listNvidiaLibs() +{ + std::vector libs; + std::unordered_map entryIndex; + + std::string cmd = "ldconfig -p"; + if (std::filesystem::exists("/sbin/ldconfig")) { + cmd = "/sbin/ldconfig -p"; + } else if (std::filesystem::exists("/usr/sbin/ldconfig")) { + cmd = "/usr/sbin/ldconfig -p"; + } + + auto *pipe = popen(cmd.c_str(), "r"); + if (!pipe) { + LogW("failed to run ldconfig for NVIDIA driver discovery"); + return libs; + } + + std::array buffer{}; + while (fgets(buffer.data(), static_cast(buffer.size()), pipe) != nullptr) { + std::string line(buffer.data()); + auto lower = toLower(line); + if (lower.find("nvidia") == std::string::npos && lower.find("libcuda") == std::string::npos) { + continue; + } + + auto pos = line.find("=>"); + if (pos == std::string::npos) { + continue; + } + + auto namePart = line.substr(0, pos); + auto nameTrimmed = linglong::common::strings::trim(namePart, " \t\n"); + if (nameTrimmed.empty()) { + continue; + } + auto paren = nameTrimmed.find('('); + auto name = linglong::common::strings::trim(nameTrimmed.substr(0, paren), " \t\n"); + if (name.empty()) { + continue; + } + + auto path = linglong::common::strings::trim(line.substr(pos + 2), " \t\n"); + if (path.empty()) { + continue; + } + + std::error_code ec; + auto resolved = std::filesystem::canonical(path, ec); + if (ec) { + continue; + } + + auto soname = readElfSoname(resolved); + std::string canonicalName = name; + if (soname && !soname->empty()) { + canonicalName = *soname; + } + + bool is32 = isElf32(resolved); + auto key = canonicalName + (is32 ? "#32" : "#64"); + auto existing = entryIndex.find(key); + if (existing != entryIndex.end()) { + if (name != canonicalName) { + auto &aliases = libs[existing->second].aliases; + if (std::find(aliases.begin(), aliases.end(), name) == aliases.end()) { + aliases.push_back(name); + } + } + continue; + } + + NvidiaLibEntry entry{ + .source = resolved, + .name = canonicalName, + .aliases = {}, + .is32 = is32, + }; + if (name != canonicalName) { + entry.aliases.push_back(name); + } + libs.push_back(std::move(entry)); + entryIndex.emplace(key, libs.size() - 1); + } + + pclose(pipe); + + std::unordered_set extraDirs; + extraDirs.reserve(libs.size()); + for (const auto &lib : libs) { + extraDirs.insert(lib.source.parent_path().string()); + } + + for (const auto &dir : extraDirs) { + std::error_code ec; + for (const auto &dirEntry : std::filesystem::directory_iterator(dir, ec)) { + if (ec) { + break; + } + if (!dirEntry.is_regular_file(ec) && !dirEntry.is_symlink(ec)) { + ec.clear(); + continue; + } + auto filename = dirEntry.path().filename().string(); + auto lower = toLower(filename); + if (lower.find("nvidia") == std::string::npos && lower.find("libcuda") == std::string::npos) { + continue; + } + if (filename.find(".so") == std::string::npos) { + continue; + } + if (!isSimpleSoName(filename)) { + continue; + } + auto resolved = std::filesystem::canonical(dirEntry.path(), ec); + if (ec) { + ec.clear(); + continue; + } + auto soname = readElfSoname(resolved); + std::string name = soname.value_or(filename); + auto nameLower = toLower(name); + if (nameLower.find("nvidia") == std::string::npos + && nameLower.find("libcuda") == std::string::npos) { + continue; + } + bool is32 = isElf32(resolved); + auto key = name + (is32 ? "#32" : "#64"); + auto existing = entryIndex.find(key); + if (existing != entryIndex.end()) { + if (filename != name) { + auto &aliases = libs[existing->second].aliases; + if (std::find(aliases.begin(), aliases.end(), filename) == aliases.end()) { + aliases.push_back(filename); + } + } + continue; + } + NvidiaLibEntry libEntry{ + .source = resolved, + .name = name, + .aliases = {}, + .is32 = is32, + }; + if (filename != name) { + libEntry.aliases.push_back(filename); + } + libs.push_back(std::move(libEntry)); + entryIndex.emplace(key, libs.size() - 1); + } + } + + return libs; +} + +std::optional +findNvidiaSmiPath(const std::vector &libs) +{ + std::vector candidates = { + "/usr/bin/nvidia-smi", + "/usr/sbin/nvidia-smi", + "/sbin/nvidia-smi", + "/usr/local/bin/nvidia-smi", + }; + + std::unordered_set seen; + for (const auto &candidate : candidates) { + seen.insert(candidate); + } + + for (const auto &lib : libs) { + auto dir = lib.source.parent_path(); + if (!dir.empty()) { + auto cand = dir / "nvidia-smi"; + if (seen.insert(cand.string()).second) { + candidates.push_back(cand); + } + } + } + + for (const auto &candidate : candidates) { + std::error_code ec; + if (std::filesystem::exists(candidate, ec) && std::filesystem::is_regular_file(candidate, ec)) { + return candidate; + } + } + + return std::nullopt; +} + +std::vector collectVendorJsons(const std::filesystem::path &dir) +{ + std::vector results; + std::error_code ec; + if (!std::filesystem::exists(dir, ec)) { + return results; + } + + for (const auto &entry : std::filesystem::directory_iterator(dir, ec)) { + if (ec) { + break; + } + if (!entry.is_regular_file(ec)) { + ec.clear(); + continue; + } + + auto filename = entry.path().filename().string(); + auto lower = toLower(filename); + if (lower.find("nvidia") == std::string::npos) { + continue; + } + if (toLower(entry.path().extension().string()) != ".json") { + continue; + } + + results.push_back(entry.path()); + } + + std::sort(results.begin(), results.end()); + return results; +} + +bool updateJsonLibraryPath(nlohmann::json &node, + const std::string &extensionName, + const std::unordered_map &libIs32, + std::unordered_set &missingLibs) +{ + if (!node.is_object()) { + return false; + } + + auto it = node.find("library_path"); + if (it == node.end() || !it->is_string()) { + return false; + } + + std::string oldPath = it->get(); + auto oldPathFs = std::filesystem::path(oldPath); + auto libName = oldPathFs.filename().string(); + if (libName.empty()) { + return false; + } + + std::string resolvedName = libName; + if (oldPathFs.is_absolute()) { + if (auto soname = readElfSoname(oldPathFs)) { + if (!soname->empty()) { + resolvedName = *soname; + } + } + } + + bool is32 = false; + auto match = libIs32.find(resolvedName); + if (match == libIs32.end()) { + if (oldPathFs.is_absolute()) { + std::error_code ec; + if (std::filesystem::exists(oldPathFs, ec) && !ec) { + is32 = isElf32(oldPathFs); + } + missingLibs.insert(oldPathFs.string()); + } + auto newPath = std::filesystem::path("/opt/extensions") / extensionName + / (is32 ? "orig/32" : "orig") / resolvedName; + *it = newPath.string(); + return true; + } + + is32 = match->second; + auto newPath = std::filesystem::path("/opt/extensions") / extensionName + / (is32 ? "orig/32" : "orig") / resolvedName; + *it = newPath.string(); + return true; +} + +struct JsonWriteResult +{ + std::filesystem::path path; + std::vector missingLibs; + bool hasLibraryPath{ false }; +}; + +std::optional +writeNvidiaJson(const std::filesystem::path &source, + const std::filesystem::path &destination, + const std::string &extensionName, + const std::unordered_map &libIs32) +{ + std::ifstream input(source); + if (!input.is_open()) { + return std::nullopt; + } + + nlohmann::json json; + try { + input >> json; + } catch (const std::exception &ex) { + LogW("failed to parse JSON {}: {}", source.string(), ex.what()); + std::error_code ec; + if (!std::filesystem::copy_file(source, + destination, + std::filesystem::copy_options::overwrite_existing, + ec)) { + return std::nullopt; + } + return JsonWriteResult{ + .path = destination, + .missingLibs = {}, + .hasLibraryPath = false, + }; + } + + bool handled = false; + std::unordered_set missingLibs; + if (json.contains("ICD")) { + auto &icd = json["ICD"]; + if (icd.is_object()) { + handled = updateJsonLibraryPath(icd, extensionName, libIs32, missingLibs) || handled; + } else if (icd.is_array()) { + for (auto &entry : icd) { + handled = updateJsonLibraryPath(entry, extensionName, libIs32, missingLibs) || handled; + } + } + } + if (!handled) { + handled = updateJsonLibraryPath(json, extensionName, libIs32, missingLibs); + } + + if (!handled) { + LogW("no library_path found in {}", source.string()); + } + + std::ofstream output(destination); + if (!output.is_open()) { + return std::nullopt; + } + + output << json.dump(2); + + std::vector missing; + missing.reserve(missingLibs.size()); + for (const auto &entry : missingLibs) { + missing.emplace_back(entry); + } + + return JsonWriteResult{ + .path = destination, + .missingLibs = std::move(missing), + .hasLibraryPath = handled, + }; +} + +} // namespace + ExtensionIf::~ExtensionIf() { } std::unique_ptr ExtensionFactory::makeExtension(const std::string &name) @@ -55,4 +606,258 @@ std::string ExtensionImplNVIDIADisplayDriver::hostDriverEnable() return version; } +bool isNvidiaDisplayDriverExtension(const std::string &extensionName) +{ + const std::string prefix = ExtensionImplNVIDIADisplayDriver::Identify; + if (extensionName == prefix) { + return true; + } + if (extensionName.size() <= prefix.size()) { + return false; + } + const auto prefixWithDot = prefix + "."; + return extensionName.find(prefixWithDot, 0) == 0; +} + +std::optional +prepareHostNvidiaExtension(const std::filesystem::path &baseDir, + const std::string &extensionName) +{ + if (!isNvidiaDisplayDriverExtension(extensionName)) { + return std::nullopt; + } + + std::error_code ec; + std::filesystem::create_directories(baseDir, ec); + if (ec) { + LogW("failed to create host extension base dir {}: {}", baseDir.string(), ec.message()); + return std::nullopt; + } + + auto root = baseDir / extensionName; + std::filesystem::create_directories(root, ec); + if (ec) { + LogW("failed to create host extension dir {}: {}", root.string(), ec.message()); + return std::nullopt; + } + + HostExtensionInfo info{ + .root = root, + .extraBinds = {}, + .vkIcdFile = std::nullopt, + .eglExternalPlatformDir = std::nullopt, + .eglVendorDir = std::nullopt, + }; + std::unordered_set extraBindTargets; + extraBindTargets.reserve(2048); + auto ensureFile = [&](const std::filesystem::path &path) { + if (path.empty()) { + return; + } + std::error_code ec; + std::filesystem::create_directories(path.parent_path(), ec); + ec.clear(); + if (std::filesystem::exists(path, ec)) { + if (std::filesystem::is_directory(path, ec)) { + ec.clear(); + std::filesystem::remove_all(path, ec); + } + } + ec.clear(); + std::ofstream ofs(path, std::ios::binary | std::ios::out | std::ios::app); + }; + auto bindHostFile = [&](const std::filesystem::path &destRel, + const std::filesystem::path &source) { + if (destRel.empty() || source.empty() || !source.is_absolute()) { + return; + } + auto destInRoot = root / destRel; + ensureFile(destInRoot); + + auto destInContainer = std::filesystem::path("/opt/extensions") / extensionName / destRel; + if (extraBindTargets.insert(destInContainer.string()).second) { + info.extraBinds.push_back(HostExtensionFileBind{ + .source = source, + .destination = destInContainer, + }); + } + }; + + bool hasLibs = false; + bool has32Bit = false; + std::unordered_map libIs32; + auto libs = listNvidiaLibs(); + for (const auto &lib : libs) { + if (lib.name.empty()) { + continue; + } + + auto libDir = lib.is32 ? (root / "orig/32") : (root / "orig"); + std::filesystem::create_directories(libDir, ec); + if (ec) { + LogW("failed to create host NVIDIA lib dir {}: {}", libDir.string(), ec.message()); + ec.clear(); + continue; + } + + auto destRelDir = lib.is32 ? std::filesystem::path("orig/32") : std::filesystem::path("orig"); + auto destRelPath = destRelDir / lib.name; + bindHostFile(destRelPath, lib.source); + + for (const auto &alias : lib.aliases) { + if (alias.empty() || alias == lib.name) { + continue; + } + auto aliasPath = libDir / alias; + if (std::filesystem::is_symlink(aliasPath, ec)) { + auto target = std::filesystem::read_symlink(aliasPath, ec); + if (!ec && target == std::filesystem::path(lib.name)) { + ec.clear(); + continue; + } + } + ec.clear(); + if (std::filesystem::exists(aliasPath, ec)) { + std::filesystem::remove(aliasPath, ec); + } + ec.clear(); + std::filesystem::create_symlink(lib.name, aliasPath, ec); + ec.clear(); + } + + + auto it = libIs32.find(lib.name); + if (it == libIs32.end()) { + libIs32.emplace(lib.name, lib.is32); + } else if (it->second && !lib.is32) { + it->second = false; + } + hasLibs = true; + has32Bit = has32Bit || lib.is32; + if (lib.name.rfind("libGLX_nvidia", 0) == 0) { + info.hasGlxLib = true; + } + } + + if (!hasLibs) { + LogD("no NVIDIA driver libs found via ldconfig"); + return std::nullopt; + } + + if (auto nvidiaSmi = findNvidiaSmiPath(libs)) { + std::filesystem::path rel = std::filesystem::path("usr/bin/nvidia-smi"); + bindHostFile(rel, *nvidiaSmi); + auto rootDest = std::filesystem::path("/usr/bin/nvidia-smi"); + if (extraBindTargets.insert(rootDest.string()).second) { + info.extraBinds.push_back(HostExtensionFileBind{ + .source = *nvidiaSmi, + .destination = rootDest, + }); + } + } + + auto ldConfPath = root / "etc/ld.so.conf"; + std::filesystem::create_directories(ldConfPath.parent_path(), ec); + if (!ec) { + std::ofstream ldConf(ldConfPath); + if (ldConf.is_open()) { + ldConf << "/opt/extensions/" << extensionName << "/orig\n"; + if (has32Bit) { + ldConf << "/opt/extensions/" << extensionName << "/orig/32\n"; + } + } + } + + const std::array kVulkanIcdCandidates = { + std::filesystem::path{ "/etc/vulkan/icd.d/nvidia_icd.json" }, + std::filesystem::path{ "/usr/share/vulkan/icd.d/nvidia_icd.json" }, + }; + const std::filesystem::path kEglExternalDir = "/usr/share/egl/egl_external_platform.d"; + const std::filesystem::path kEglVendorDir = "/usr/share/glvnd/egl_vendor.d"; + const std::filesystem::path kGlxVendorDir = "/usr/share/glvnd/glx_vendor.d"; + const auto kEglExternalRel = kEglExternalDir.relative_path(); + const auto kEglVendorRel = kEglVendorDir.relative_path(); + const auto kGlxVendorRel = kGlxVendorDir.relative_path(); + + auto addMissingLibBinds = + [&bindHostFile](const std::vector &missingLibs) { + for (const auto &missingLib : missingLibs) { + if (!missingLib.is_absolute()) { + continue; + } + auto normalized = missingLib.lexically_normal(); + if (!normalized.has_filename()) { + continue; + } + + std::error_code ec; + auto resolved = std::filesystem::canonical(normalized, ec); + if (ec) { + ec.clear(); + continue; + } + + bool is32 = isElf32(resolved); + std::string destName = normalized.filename().string(); + if (auto soname = readElfSoname(resolved)) { + if (!soname->empty()) { + destName = *soname; + } + } + auto relDir = is32 ? "orig/32" : "orig"; + auto destRel = std::filesystem::path(relDir) / destName; + bindHostFile(destRel, resolved); + } + }; + auto addVendorJsons = [&](const std::filesystem::path &sourceDir, + const std::filesystem::path &destRelDir, + std::optional *exportDir) { + for (const auto &vendorJson : collectVendorJsons(sourceDir)) { + auto dest = root / destRelDir / vendorJson.filename(); + std::filesystem::create_directories(dest.parent_path(), ec); + ec.clear(); + auto written = writeNvidiaJson(vendorJson, dest, extensionName, libIs32); + if (!written) { + continue; + } + if (exportDir && !*exportDir) { + *exportDir = + std::filesystem::path("/opt/extensions") / extensionName / destRelDir; + } + if (!written->hasLibraryPath) { + LogW("no library_path found in {}", vendorJson.string()); + } + addMissingLibBinds(written->missingLibs); + } + }; + + for (const auto &kVulkanIcd : kVulkanIcdCandidates) { + if (!std::filesystem::exists(kVulkanIcd, ec)) { + ec.clear(); + continue; + } + + ec.clear(); + auto dest = root / "etc/vulkan/icd.d/nvidia_icd.json"; + std::filesystem::create_directories(dest.parent_path(), ec); + ec.clear(); + auto written = writeNvidiaJson(kVulkanIcd, dest, extensionName, libIs32); + if (written) { + info.vkIcdFile = + std::filesystem::path("/opt/extensions") / extensionName / "etc/vulkan/icd.d/nvidia_icd.json"; + if (!written->hasLibraryPath) { + LogW("no library_path found in {}", kVulkanIcd.string()); + } + addMissingLibBinds(written->missingLibs); + } + break; + } + + addVendorJsons(kEglExternalDir, kEglExternalRel, &info.eglExternalPlatformDir); + addVendorJsons(kEglVendorDir, kEglVendorRel, &info.eglVendorDir); + addVendorJsons(kGlxVendorDir, kGlxVendorRel, nullptr); + + return info; +} + } // namespace linglong::extension diff --git a/libs/linglong/src/linglong/extension/extension.h b/libs/linglong/src/linglong/extension/extension.h index f8b2044b5..23063824e 100644 --- a/libs/linglong/src/linglong/extension/extension.h +++ b/libs/linglong/src/linglong/extension/extension.h @@ -6,8 +6,11 @@ #pragma once +#include #include +#include #include +#include namespace linglong::extension { @@ -57,4 +60,26 @@ class ExtensionImplDummy : public ExtensionIf bool shouldEnable([[maybe_unused]] std::string &extensionName) override { return true; } }; +struct HostExtensionFileBind +{ + std::filesystem::path source; + std::filesystem::path destination; +}; + +struct HostExtensionInfo +{ + std::filesystem::path root; + std::vector extraBinds; + std::optional vkIcdFile; + std::optional eglExternalPlatformDir; + std::optional eglVendorDir; + bool hasGlxLib{ false }; +}; + +bool isNvidiaDisplayDriverExtension(const std::string &extensionName); + +std::optional +prepareHostNvidiaExtension(const std::filesystem::path &baseDir, + const std::string &extensionName); + } // namespace linglong::extension diff --git a/libs/linglong/src/linglong/runtime/host_nvidia_fallback.cpp b/libs/linglong/src/linglong/runtime/host_nvidia_fallback.cpp new file mode 100644 index 000000000..be55bf0a1 --- /dev/null +++ b/libs/linglong/src/linglong/runtime/host_nvidia_fallback.cpp @@ -0,0 +1,190 @@ +/* + * SPDX-FileCopyrightText: 2025 UnionTech Software Technology Co., Ltd. + * + * SPDX-License-Identifier: LGPL-3.0-or-later + */ + +#include "run_context.h" + +#include "linglong/extension/extension.h" +#include "linglong/utils/log/log.h" + +#include +#include +#include + +namespace linglong::runtime { + +void RunContext::setupHostNvidiaFallbacks( + generator::ContainerCfgBuilder &builder, + std::vector &extensionMounts) +{ + if (!hostNvidiaFallbackEnabled) { + return; + } + + if (hostExtensions.empty() || !baseLayer) { + return; + } + + bool hasInstalledNvidiaExtension = false; + for (const auto &ext : extensionLayers) { + const auto &name = ext.getReference().id; + if (extension::isNvidiaDisplayDriverExtension(name)) { + hasInstalledNvidiaExtension = true; + break; + } + } + + if (hasInstalledNvidiaExtension) { + return; + } + + auto hostBase = bundle / "host-extensions"; + auto appendPathEnv = [this](const std::string &key, const std::string &value) { + if (value.empty()) { + return; + } + auto it = environment.find(key); + std::string current; + if (it != environment.end() && !it->second.empty()) { + current = it->second; + } else { + auto *envValue = ::getenv(key.c_str()); + if (envValue != nullptr && envValue[0] != '\0') { + current = envValue; + } + } + if (current.empty()) { + environment[key] = value; + return; + } + std::string haystack = ":" + current + ":"; + std::string needle = ":" + value + ":"; + if (haystack.find(needle) != std::string::npos) { + environment[key] = current; + return; + } + environment[key] = value + ":" + current; + }; + auto setEnvIfEmpty = [this](const std::string &key, const std::string &value) { + if (value.empty()) { + return; + } + auto it = environment.find(key); + if (it != environment.end() && !it->second.empty()) { + return; + } + auto *envValue = ::getenv(key.c_str()); + if (envValue != nullptr && envValue[0] != '\0') { + return; + } + environment[key] = value; + }; + for (const auto &overrideDef : extensionOverrides) { + if (extension::isNvidiaDisplayDriverExtension(overrideDef.name)) { + return; + } + } + + std::unordered_set deviceBindSet; + auto addDeviceBind = [&](const std::filesystem::path &devicePath) { + std::error_code ec; + auto status = std::filesystem::status(devicePath, ec); + if (ec) { + return; + } + if (!std::filesystem::exists(status)) { + return; + } + if (!std::filesystem::is_character_file(status) + && !std::filesystem::is_block_file(status) + && !std::filesystem::is_directory(status)) { + return; + } + auto key = devicePath.string(); + if (!deviceBindSet.insert(key).second) { + return; + } + builder.addExtraMount(ocppi::runtime::config::types::Mount{ + .destination = key, + .options = { { "rbind" } }, + .source = key, + .type = "bind", + }); + }; + + addDeviceBind("/dev/dxg"); + std::error_code devEc; + for (const auto &entry : std::filesystem::directory_iterator("/dev", devEc)) { + if (devEc) { + break; + } + auto name = entry.path().filename().string(); + if (name.rfind("nvidia", 0) != 0) { + continue; + } + addDeviceBind(entry.path()); + } + + for (const auto &name : hostExtensions) { + auto hostExt = extension::prepareHostNvidiaExtension(hostBase, name); + if (!hostExt) { + LogW("failed to prepare host NVIDIA driver for {}", name); + continue; + } + extensionMounts.push_back(ocppi::runtime::config::types::Mount{ + .destination = "/opt/extensions/" + name, + .gidMappings = {}, + .options = { { "rbind", "ro" } }, + .source = hostExt->root.string(), + .type = "bind", + .uidMappings = {}, + }); + + for (const auto &bind : hostExt->extraBinds) { + ocppi::runtime::config::types::Mount mount = { + .destination = bind.destination.string(), + .options = { { "rbind", "ro" } }, + .source = bind.source.string(), + .type = "bind", + }; + builder.addExtraMount(mount); + } + + { + std::error_code ec; + auto extensionRoot = std::filesystem::path("/opt/extensions") / name; + auto libPath = extensionRoot / "orig"; + auto lib32Path = extensionRoot / "orig/32"; + if (std::filesystem::exists(hostExt->root / "orig/32", ec)) { + appendPathEnv("LD_LIBRARY_PATH", lib32Path.string()); + } + ec.clear(); + if (std::filesystem::exists(hostExt->root / "orig", ec)) { + appendPathEnv("LD_LIBRARY_PATH", libPath.string()); + setEnvIfEmpty("NVIDIA_CTK_LIBCUDA_DIR", libPath.string()); + } + } + + if (hostExt->vkIcdFile) { + appendPathEnv("VK_ADD_DRIVER_FILES", hostExt->vkIcdFile->string()); + appendPathEnv("VK_ICD_FILENAMES", hostExt->vkIcdFile->string()); + } + if (hostExt->eglExternalPlatformDir) { + appendPathEnv("__EGL_EXTERNAL_PLATFORM_CONFIG_DIRS", + hostExt->eglExternalPlatformDir->string()); + appendPathEnv("EGL_EXTERNAL_PLATFORM_CONFIG_DIRS", + hostExt->eglExternalPlatformDir->string()); + } + if (hostExt->eglVendorDir) { + appendPathEnv("__EGL_VENDOR_LIBRARY_DIRS", hostExt->eglVendorDir->string()); + } + if (hostExt->hasGlxLib) { + setEnvIfEmpty("__GLX_VENDOR_LIBRARY_NAME", "nvidia"); + setEnvIfEmpty("__NV_PRIME_RENDER_OFFLOAD", "1"); + } + } +} + +} // namespace linglong::runtime diff --git a/libs/linglong/src/linglong/runtime/run_context.cpp b/libs/linglong/src/linglong/runtime/run_context.cpp index 1b65dfdb7..41ef871ab 100644 --- a/libs/linglong/src/linglong/runtime/run_context.cpp +++ b/libs/linglong/src/linglong/runtime/run_context.cpp @@ -9,6 +9,9 @@ #include "linglong/runtime/container_builder.h" #include "linglong/utils/log/log.h" +#include +#include +#include #include namespace linglong::runtime { @@ -403,6 +406,10 @@ RunContext::resolveExtension(const std::vector repo.clearReference(*fuzzyRef, { .fallbackToRemote = false, .semanticMatching = true }); if (!ref) { LogD("extension is not installed: {}", fuzzyRef->toString()); + if (extension::isNvidiaDisplayDriverExtension(name)) { + hostExtensions.insert(name); + continue; + } if (skipOnNotFound) { continue; } @@ -502,8 +509,283 @@ void RunContext::detectDisplaySystem(generator::ContainerCfgBuilder &builder) no } } -utils::error::Result RunContext::fillContextCfg( - linglong::generator::ContainerCfgBuilder &builder, const std::string &bundleSuffix) +void RunContext::applyExtensionOverrides( + generator::ContainerCfgBuilder &builder, + std::vector &extensionMounts) +{ + if (extensionOverrides.empty()) { + return; + } + + auto matchesPrefix = [](const std::string &extensionName, const std::string &prefix) -> bool { + if (extensionName == prefix) { + return true; + } + if (extensionName.size() <= prefix.size()) { + return false; + } + return extensionName.compare(0, prefix.size(), prefix) == 0 + && extensionName[prefix.size()] == '.'; + }; + auto hasInstalledExtension = [&](const std::string &prefix) -> bool { + for (const auto &ext : extensionLayers) { + if (matchesPrefix(ext.getReference().id, prefix)) { + return true; + } + } + return false; + }; + auto ensureExtensionRoot = [&](const std::string &rootName, bool has32Bit) { + if (rootName.empty()) { + return; + } + std::string destination = "/opt/extensions/" + rootName; + for (const auto &mount : extensionMounts) { + if (mount.destination == destination) { + return; + } + } + if (bundle.empty()) { + LogW("bundle path is empty, skip CDI extension mount for {}", rootName); + return; + } + std::filesystem::path sourceDir = bundle / "cdi-extensions" / rootName; + std::error_code ec; + std::filesystem::create_directories(sourceDir / "etc", ec); + if (ec) { + LogW("failed to create CDI extension dir {}: {}", sourceDir.string(), ec.message()); + return; + } + std::filesystem::create_directories(sourceDir / "orig", ec); + if (has32Bit) { + std::filesystem::create_directories(sourceDir / "orig/32", ec); + } + + std::ofstream ldConf(sourceDir / "etc/ld.so.conf", std::ios::binary | std::ios::out | std::ios::trunc); + if (ldConf.is_open()) { + ldConf << destination << "/orig\n"; + if (has32Bit) { + ldConf << destination << "/orig/32\n"; + } + } + + extensionMounts.push_back(ocppi::runtime::config::types::Mount{ + .destination = destination, + .gidMappings = {}, + .options = { { "rbind", "ro" } }, + .source = sourceDir.string(), + .type = "bind", + .uidMappings = {}, + }); + }; + auto collectExtensionRoots = [&](const ExtensionOverride &overrideDef) { + std::unordered_map roots; + const std::string prefix = "/opt/extensions/"; + for (const auto &mount : overrideDef.mounts) { + if (mount.destination.empty()) { + continue; + } + const auto &dest = mount.destination; + if (dest.rfind(prefix, 0) != 0) { + continue; + } + auto rest = dest.substr(prefix.size()); + if (rest.empty()) { + continue; + } + auto slash = rest.find('/'); + std::string root = (slash == std::string::npos) ? rest : rest.substr(0, slash); + if (root.empty()) { + continue; + } + bool has32 = dest.find("/orig/32") != std::string::npos; + auto it = roots.find(root); + if (it == roots.end()) { + roots.emplace(root, has32); + } else if (has32) { + it->second = true; + } + } + for (const auto &item : roots) { + ensureExtensionRoot(item.first, item.second); + } + }; + + std::unordered_map hostDirRelMap; + std::unordered_map hostDirCounters; + std::unordered_set hostDirMounts; + auto ensureHostDir = [&](const std::string &rootName, + const std::filesystem::path &sourceDir, + const std::vector &options) + -> std::optional { + if (sourceDir.empty() || !sourceDir.is_absolute()) { + return std::nullopt; + } + auto key = rootName + "|" + sourceDir.lexically_normal().string(); + auto it = hostDirRelMap.find(key); + if (it != hostDirRelMap.end()) { + return it->second; + } + int &counter = hostDirCounters[rootName]; + auto rel = std::filesystem::path("host") / std::to_string(counter++); + hostDirRelMap.emplace(key, rel); + std::error_code ec; + std::filesystem::create_directories(bundle / "cdi-extensions" / rootName / rel, ec); + ec.clear(); + auto mountKey = rootName + "|" + rel.string(); + if (hostDirMounts.insert(mountKey).second) { + builder.addExtraMount(ocppi::runtime::config::types::Mount{ + .destination = (std::filesystem::path("/opt/extensions") / rootName / rel).string(), + .gidMappings = {}, + .options = options, + .source = sourceDir.string(), + .type = "bind", + .uidMappings = {}, + }); + } + return rel; + }; + + auto linkOverrideMount = [&](const ocppi::runtime::config::types::Mount &mount) -> bool { + if (!mount.source || mount.source->empty() || mount.destination.empty()) { + return false; + } + std::filesystem::path sourcePath{ *mount.source }; + if (!sourcePath.is_absolute()) { + return false; + } + std::filesystem::path destPath{ mount.destination }; + const std::string prefix = "/opt/extensions/"; + auto destStr = destPath.generic_string(); + if (destStr.rfind(prefix, 0) != 0) { + return false; + } + auto rest = destStr.substr(prefix.size()); + auto slash = rest.find('/'); + if (slash == std::string::npos) { + return false; + } + std::string rootName = rest.substr(0, slash); + if (rootName.empty()) { + return false; + } + if (bundle.empty()) { + return false; + } + std::filesystem::path rel = + destPath.lexically_relative(std::filesystem::path("/opt/extensions") / rootName); + if (rel.empty() || rel == "." || rel.native().rfind("..", 0) == 0) { + return false; + } + std::vector options = + mount.options.value_or(std::vector{ "rbind", "ro" }); + auto relDir = ensureHostDir(rootName, sourcePath.parent_path(), options); + if (!relDir) { + return false; + } + std::filesystem::path destInRoot = bundle / "cdi-extensions" / rootName / rel; + std::filesystem::path target = + std::filesystem::path("/opt/extensions") / rootName / *relDir / sourcePath.filename(); + std::error_code ec; + std::filesystem::create_directories(destInRoot.parent_path(), ec); + ec.clear(); + if (std::filesystem::is_symlink(destInRoot, ec)) { + auto existing = std::filesystem::read_symlink(destInRoot, ec); + if (!ec && existing == target) { + ec.clear(); + return true; + } + } + ec.clear(); + if (std::filesystem::exists(destInRoot, ec)) { + std::filesystem::remove(destInRoot, ec); + } + ec.clear(); + std::filesystem::create_symlink(target, destInRoot, ec); + if (ec) { + LogW("failed to create CDI link {} -> {}: {}", + destInRoot.string(), + target.string(), + ec.message()); + ec.clear(); + return false; + } + auto destFilename = destPath.filename(); + auto sourceFilename = sourcePath.filename(); + if (!sourceFilename.empty() && destFilename != sourceFilename) { + std::filesystem::path aliasPath = destInRoot.parent_path() / sourceFilename; + std::filesystem::path aliasTarget = destFilename; + std::error_code aliasEc; + bool needsCreate = true; + if (std::filesystem::is_symlink(aliasPath, aliasEc)) { + auto existing = std::filesystem::read_symlink(aliasPath, aliasEc); + if (!aliasEc && existing == aliasTarget) { + needsCreate = false; + } + } + aliasEc.clear(); + if (needsCreate) { + if (std::filesystem::exists(aliasPath, aliasEc)) { + std::filesystem::remove(aliasPath, aliasEc); + } + aliasEc.clear(); + std::filesystem::create_symlink(aliasTarget, aliasPath, aliasEc); + if (aliasEc) { + LogW("failed to create CDI alias {} -> {}: {}", + aliasPath.string(), + aliasTarget.string(), + aliasEc.message()); + } + } + } + + return true; + }; + + for (const auto &overrideDef : extensionOverrides) { + bool installed = hasInstalledExtension(overrideDef.name); + bool isNvidiaOverride = extension::isNvidiaDisplayDriverExtension(overrideDef.name); + if (installed && (overrideDef.fallbackOnly || isNvidiaOverride)) { + continue; + } + + collectExtensionRoots(overrideDef); + + for (const auto &env : overrideDef.env) { + environment[env.first] = env.second; + } + + for (auto mount : overrideDef.mounts) { + if (mount.destination.empty() || !mount.source) { + continue; + } + if (!mount.options || mount.options->empty()) { + mount.options = std::vector{ "rbind", "ro" }; + } + if (!mount.type) { + mount.type = "bind"; + } + if (linkOverrideMount(mount)) { + continue; + } + builder.addExtraMount(mount); + } + + for (const auto &node : overrideDef.deviceNodes) { + ocppi::runtime::config::types::Mount mount = { + .destination = node.path, + .options = { { "bind" } }, + .source = node.hostPath.value_or(node.path), + .type = "bind", + }; + builder.addExtraMount(mount); + } + } +} + +utils::error::Result +RunContext::fillContextCfg(linglong::generator::ContainerCfgBuilder &builder, + const std::string &bundleSuffix) { LINGLONG_TRACE("fill ContainerCfgBuilder with run context"); @@ -582,6 +864,9 @@ utils::error::Result RunContext::fillContextCfg( .uidMappings = {}, }); } + + setupHostNvidiaFallbacks(builder, extensionMounts); + applyExtensionOverrides(builder, extensionMounts); if (!extensionMounts.empty()) { builder.setExtensionMounts(extensionMounts); } @@ -729,6 +1014,9 @@ api::types::v1::ContainerProcessStateInfo RunContext::stateInfo() for (auto &ext : extensionLayers) { state.extensions->push_back(ext.getReference().toString()); } + for (const auto &name : hostExtensions) { + state.extensions->push_back(name); + } return state; } diff --git a/libs/linglong/src/linglong/runtime/run_context.h b/libs/linglong/src/linglong/runtime/run_context.h index cfc9e32e4..96a441e68 100644 --- a/libs/linglong/src/linglong/runtime/run_context.h +++ b/libs/linglong/src/linglong/runtime/run_context.h @@ -8,6 +8,7 @@ #include "linglong/api/types/v1/BuilderProject.hpp" #include "linglong/api/types/v1/ContainerProcessStateInfo.hpp" +#include "linglong/api/types/v1/DeviceNode.hpp" #include "linglong/api/types/v1/ExtensionDefine.hpp" #include "linglong/oci-cfg-generators/container_cfg_builder.h" #include "linglong/repo/ostree_repo.h" @@ -16,6 +17,9 @@ #include #include +#include +#include +#include namespace linglong::runtime { @@ -62,6 +66,15 @@ struct ResolveOptions std::optional> extensionRefs; }; +struct ExtensionOverride +{ + std::string name; + bool fallbackOnly{ false }; + std::map env; + std::vector mounts; + std::vector deviceNodes; +}; + class RunContext { public: @@ -99,6 +112,13 @@ class RunContext utils::error::Result getCachedAppItem(); + void setExtensionOverrides(std::vector overrides) + { + extensionOverrides = std::move(overrides); + } + + void setHostNvidiaFallbackEnabled(bool enabled) { hostNvidiaFallbackEnabled = enabled; } + bool hasRuntime() const { return !!runtimeLayer; } private: @@ -111,6 +131,11 @@ class RunContext bool skipOnNotFound = false); utils::error::Result fillExtraAppMounts(generator::ContainerCfgBuilder &builder); void detectDisplaySystem(generator::ContainerCfgBuilder &builder) noexcept; + void setupHostNvidiaFallbacks( + generator::ContainerCfgBuilder &builder, + std::vector &extensionMounts); + void applyExtensionOverrides(generator::ContainerCfgBuilder &builder, + std::vector &extensionMounts); utils::error::Result> makeManualExtensionDefine(const std::vector &refs); @@ -120,6 +145,9 @@ class RunContext std::optional runtimeLayer; std::optional appLayer; std::list extensionLayers; + std::unordered_set hostExtensions; + std::vector extensionOverrides; + bool hostNvidiaFallbackEnabled{ true }; std::string targetId; std::optional appOutput; diff --git a/libs/oci-cfg-generators/src/linglong/oci-cfg-generators/container_cfg_builder.cpp b/libs/oci-cfg-generators/src/linglong/oci-cfg-generators/container_cfg_builder.cpp index 2492d5f28..78718d083 100644 --- a/libs/oci-cfg-generators/src/linglong/oci-cfg-generators/container_cfg_builder.cpp +++ b/libs/oci-cfg-generators/src/linglong/oci-cfg-generators/container_cfg_builder.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include @@ -1813,6 +1814,23 @@ bool ContainerCfgBuilder::mergeMount() noexcept std::move(extraMount->begin(), extraMount->end(), std::back_inserter(mounts)); } + if (!mounts.empty()) { + std::unordered_set seen; + seen.reserve(mounts.size()); + std::vector deduped; + deduped.reserve(mounts.size()); + for (auto it = mounts.rbegin(); it != mounts.rend(); ++it) { + const auto &dest = it->destination; + if (dest.empty()) { + continue; + } + if (seen.insert(dest).second) { + deduped.push_back(std::move(*it)); + } + } + std::reverse(deduped.begin(), deduped.end()); + mounts = std::move(deduped); + } config.mounts = std::move(mounts); return true;