From be5a0b82024edb7fcfc298f06ae211fb89b2f30a Mon Sep 17 00:00:00 2001 From: Viorel Munteanu Date: Fri, 21 Aug 2026 12:31:40 +0000 Subject: [PATCH] Add a local YAML config file for persistent settings Reads $XDG_CONFIG_HOME/slocker-lite/config.yaml (falling back to $HOME/.config/slocker-lite/config.yaml), organized into sections. Only the "global" section's log-level is supported for now -- other options are one-shot flags, not standing preferences. An explicit --log-level on the command line always overrides the config file, the same way SPDLOG_LEVEL already does. Uses libyaml directly (yaml_dep was already declared in meson.build but unused). A missing config file isn't an error; unknown sections/keys are ignored for forward-compatibility; malformed YAML syntax is a hard error. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Gv3s5jckJKzh6JkMoi2Akz --- CLAUDE.md | 12 ++++++ README.md | 17 ++++++++ meson.build | 2 +- src/config_file.cpp | 95 +++++++++++++++++++++++++++++++++++++++++++++ src/config_file.h | 39 +++++++++++++++++++ src/main.cpp | 9 +++++ 6 files changed, 173 insertions(+), 1 deletion(-) create mode 100644 src/config_file.cpp create mode 100644 src/config_file.h diff --git a/CLAUDE.md b/CLAUDE.md index 19fd5e4..efd1e7f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -95,6 +95,18 @@ Source layout (all under `src/`): kill `slocker-lite` itself — without this, Ctrl-C (or `kill`) during `-r`'s `bwrap` run would skip `run_container()`'s unmount/cleanup entirely, leaving the layer imported and/or mounted. +- `config_file.{h,cpp}` — `load_config_file()` reads and parses (via libyaml's + document API, ``) the `global` section of the local YAML config file + located by `config_file_path()` (`$XDG_CONFIG_HOME/slocker-lite/config.yaml`, + falling back to `$HOME/.config/slocker-lite/config.yaml`). Only `global.log-level` + is supported today — other long options are one-shot flags, not settings, so they + don't belong in a persistent config file. A missing file returns a + default-constructed (empty) `AppConfig`, not an error; unknown sections/keys are + ignored for forward-compatibility; malformed YAML syntax is a hard error. `main()` + applies `config->log_level` (via the existing `apply_log_level()`) right after + `spdlog::cfg::load_env_levels()` and before parsing CLI options, so an explicit + `--log-level` on the command line always overwrites it afterward — same precedence + pattern already used for `SPDLOG_LEVEL`. Errors are logged via `spdlog::error`; every external command is also traced at debug level in `run_process()`/`run_process_foreground()` (`src/process.cpp`) — visible via diff --git a/README.md b/README.md index 2a54580..6c8ff4d 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,23 @@ sudo ./buildDir/slocker-lite -r myimage.tar --user git ./buildDir/slocker-lite -l ./images ``` +## Configuration + +Persistent settings can be kept in a local YAML config file at +`$XDG_CONFIG_HOME/slocker-lite/config.yaml` (falling back to +`$HOME/.config/slocker-lite/config.yaml` if `XDG_CONFIG_HOME` isn't set). The file is +organized into sections; only `global` exists today: + +```yaml +global: + log-level: debug +``` + +Only options that make sense as a standing preference are supported here — right now +just `log-level` (one-shot commands like `--mount`/`--run`/`--user` don't belong in a +config file). A missing config file is fine (nothing is overridden); an explicit +`--log-level` on the command line always overrides the config file. + ## How it works Image layers are imported into `containers-storage` (parent-chained) and the diff --git a/meson.build b/meson.build index cb62f4f..42fc82b 100644 --- a/meson.build +++ b/meson.build @@ -18,7 +18,7 @@ configure_file(output : 'config.h', configuration : conf_data) slocker_lite = executable('slocker-lite', ['src/main.cpp', 'src/process.cpp', 'src/oci_image.cpp', 'src/containers_storage.cpp', - 'src/bwrap.cpp', 'src/user_spec.cpp'], + 'src/bwrap.cpp', 'src/user_spec.cpp', 'src/config_file.cpp'], include_directories : include_directories('.'), dependencies : [fmt_dep, catch2_dep, yaml_dep, archive_dep, json_dep, spdlog_dep], install : true) diff --git a/src/config_file.cpp b/src/config_file.cpp new file mode 100644 index 0000000..1df3d3c --- /dev/null +++ b/src/config_file.cpp @@ -0,0 +1,95 @@ +// Copyright (C) 2026 Viorel Munteanu +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +#include "config_file.h" + +#include +#include + +#include +#include + +namespace { + +std::string_view scalar_value(const yaml_node_t& node) { + return {reinterpret_cast(node.data.scalar.value), node.data.scalar.length}; +} + +// Finds `key` in a YAML_MAPPING_NODE and returns its value node, or nullptr if +// `node` isn't a mapping or has no such (scalar) key. +const yaml_node_t* find_in_mapping(yaml_document_t& document, const yaml_node_t& node, + std::string_view key) { + if (node.type != YAML_MAPPING_NODE) { + return nullptr; + } + for (auto* pair = node.data.mapping.pairs.start; pair < node.data.mapping.pairs.top; ++pair) { + yaml_node_t* key_node = yaml_document_get_node(&document, pair->key); + if (key_node && key_node->type == YAML_SCALAR_NODE && scalar_value(*key_node) == key) { + return yaml_document_get_node(&document, pair->value); + } + } + return nullptr; +} + +} // namespace + +std::filesystem::path config_file_path() { + const char* xdg_config_home = std::getenv("XDG_CONFIG_HOME"); + std::filesystem::path config_home; + if (xdg_config_home && *xdg_config_home) { + config_home = xdg_config_home; + } else { + const char* home = std::getenv("HOME"); + config_home = std::filesystem::path(home ? home : "") / ".config"; + } + return config_home / "slocker-lite" / "config.yaml"; +} + +std::optional load_config_file(const std::filesystem::path& path) { + FILE* file = std::fopen(path.c_str(), "r"); + if (!file) { + return AppConfig{}; + } + + yaml_parser_t parser; + yaml_parser_initialize(&parser); + yaml_parser_set_input_file(&parser, file); + + yaml_document_t document; + bool loaded = yaml_parser_load(&parser, &document) != 0; + yaml_parser_delete(&parser); + std::fclose(file); + + if (!loaded) { + spdlog::error("failed to parse config file {}", path.string()); + return std::nullopt; + } + + AppConfig config; + yaml_node_t* root = yaml_document_get_root_node(&document); + if (root) { + if (const yaml_node_t* global = find_in_mapping(document, *root, "global")) { + if (const yaml_node_t* log_level = find_in_mapping(document, *global, "log-level")) { + if (log_level->type == YAML_SCALAR_NODE) { + config.log_level = std::string(scalar_value(*log_level)); + } + } + } + } + + yaml_document_delete(&document); + return config; +} diff --git a/src/config_file.h b/src/config_file.h new file mode 100644 index 0000000..729db22 --- /dev/null +++ b/src/config_file.h @@ -0,0 +1,39 @@ +// Copyright (C) 2026 Viorel Munteanu +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +#pragma once + +#include +#include +#include + +// Fields that make sense to persist across invocations (one-shot flags like +// -m/-r/--user don't belong here). Only the "global" section's log-level is +// supported today; add more optional fields as more long options gain config-file +// support. +struct AppConfig { + std::optional log_level; // global.log-level +}; + +// $XDG_CONFIG_HOME/slocker-lite/config.yaml, or $HOME/.config/slocker-lite/config.yaml +// if XDG_CONFIG_HOME is unset/empty. +std::filesystem::path config_file_path(); + +// Loads and parses `path`'s "global" section. A missing file is not an error -- +// returns a default-constructed AppConfig (nothing set). Unknown sections/keys are +// ignored, so the format stays forward-compatible. Malformed YAML syntax logs a +// specific error and returns nullopt. +std::optional load_config_file(const std::filesystem::path& path); diff --git a/src/main.cpp b/src/main.cpp index ded754e..3a63186 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -31,6 +31,7 @@ #include "bwrap.h" #include "config.h" +#include "config_file.h" #include "containers_storage.h" #include "oci_image.h" #include "process.h" @@ -311,6 +312,14 @@ int run_container(const std::filesystem::path& image_tar, int main(int argc, char* argv[]) { spdlog::cfg::load_env_levels(); + auto config = load_config_file(config_file_path()); + if (!config) { + return 1; + } + if (config->log_level) { + apply_log_level(*config->log_level); + } + Mode mode = Mode::kNone; std::string mode_arg; bool disable_nsenter = false;