[PATCH v5 03/15] config: Introduce global runtime configuration

Milan Zamazal mzamazal at redhat.com
Thu Dec 5 12:33:46 CET 2024


Hi Barnabás,

thank you for review.

Barnabás Pőcze <pobrn at protonmail.com> writes:

> Hi
>
>
> 2024. október 1., kedd 12:27 keltezéssel, Milan Zamazal <mzamazal at redhat.com> írta:
>
>> Currently, libcamera can be configured in runtime using several
>> environment variables.  With introducing more and more variables, this
>> mechanism reaches its limits.  It would be simpler and more flexible if
>> it was possible to configure libcamera in a single file.
>> 
>> For example, there have been a request for defining pipeline precedence
>> in runtime.  We want to compile in multiple pipelines, in order to have
>> them accessible within single packages in distributions.  And then being
>> able to select among the pipelines manually as needed based on the
>> particular hardware or operating system environment.  Having the
>> configuration file then allows easy switching between hardware, GPU or
>> CPU IPAs.  Another possible use case is tuning image output, especially
>> with software ISP, to user liking.  For example, some users may prefer
>> higher contrast without the need to use the corresponding knobs, if
>> present at all, in every application.  The configuration file can also
>> be used to enable or disable experimental features and avoid the need to
>> track local patches changing configuration options hard-wired in the
>> code when working on new features.
>> 
>> This patch introduces basic support for configuration files.
>> GlobalConfiguration class reads, stores and accesses the configuration.
>> Its instance is stored as a private singleton accessed using a static
>> method of the class.  There is currently no reason to have more than one
>> instance.
>> 
>> libcamera configuration can be specified using a system-wide
>> configuration file or a user configuration file.  The user configuration
>> file takes precedence if present.  There is currently no way to merge
>> multiple configuration files, the one found is used as the only
>> configuration file.  If no configuration file is present, nothing
>> changes to the current libcamera behavior (except for some log
>> messages related to configuration file lookup).
>> 
>> The configuration file is a YAML file.  We already have a mechanism for
>> handling YAML configuration files in libcamera and the given
>> infrastructure can be reused for the purpose.  However, the
>> configuration type is abstracted to make contingent future change of the
>> underlying class easier while retaining (most of) the original API.
>> 
>> The configuration is versioned.  This has currently no particular
>> meaning but is likely to have its purpose in future, especially once
>> configuration validation is introduced.
>> 
>> The configuration YAML file looks as follows:
>> 
>>   ---
>>   version: 1
>>   configuration:
>>     WHATEVER CONFIGURATION NEEDED
>> 
>> There is no logging about reading the configuration file and contingent
>> errors.  This is on purpose because logging will query configuration,
>> which can lead to various problems when done during configuration
>> initialization.  Reporting the errors will be added later.
>> 
>> The global configuration is intended to be used as a hidden singleton,
>> with static class methods around it providing the configuration
>> information.  The constructor must be still public so that unique_ptr
>> can be used.
>> 
>> A complication arises from the fact that when the configuration is
>> loaded, logging may be called and logging will ask for the
>> configuration.  This is error-prone and may lead to subtle problems.
>> For this reason, the global configuration is instantiated to a pointer,
>> with an empty configuration initially.  The real configuration will be
>> created through initialize() method.  It will be clearer how it helps in
>> the followup patch introducing logging configuration.
>> 
>> Logging is also the most notable component from base that uses global
>> configuration.  In order to be able to do it, the global configuration
>> must be put to base.
>> 
>> This patch introduces just the basic idea.  Actually using the
>> configuration in the corresponding places (everything what is currently
>> configurable via environment variables should be configurable in the
>> file configuration) and other enhancements are implemented in the
>> followup patches.
>> 
>> Signed-off-by: Milan Zamazal <mzamazal at redhat.com>
>> ---
>>  .../libcamera/internal/global_configuration.h |  41 +++++
>>  include/libcamera/internal/meson.build        |   1 +
>>  src/libcamera/base/global_configuration.cpp   | 154 ++++++++++++++++++
>>  src/libcamera/base/meson.build                |   1 +
>>  4 files changed, 197 insertions(+)
>>  create mode 100644 include/libcamera/internal/global_configuration.h
>>  create mode 100644 src/libcamera/base/global_configuration.cpp
>> 
>> diff --git a/include/libcamera/internal/global_configuration.h b/include/libcamera/internal/global_configuration.h
>> new file mode 100644
>> index 000000000..628d85cb8
>> --- /dev/null
>> +++ b/include/libcamera/internal/global_configuration.h
>> @@ -0,0 +1,41 @@
>> +/* SPDX-License-Identifier: LGPL-2.1-or-later */
>> +/*
>> + * Copyright (C) 2024 Red Hat, inc.
>> + *
>> + * global_configuration.h - Global configuration handling
>> + */
>> +
>> +#pragma once
>> +
>> +#include <filesystem>
>> +#include <vector>
>> +
>> +#include "libcamera/internal/yaml_parser.h"
>> +
>> +namespace libcamera {
>> +
>> +class GlobalConfiguration
>> +{
>> +public:
>> +	using Configuration = const YamlObject &;
>> +
>> +	/* The constructor must be public to be able to use unique_ptr. */
>
> If one does
>
>   std::unique_ptr<T>(new T)
>
> then the constructor does not need to be public.

Ah, cool.

>> +	GlobalConfiguration();
>> +	static void initialize();
>> +
>> +	static unsigned int version();
>> +	static Configuration configuration();
>> +
>> +private:
>> +	static const std::vector<std::filesystem::path> globalConfigurationFiles;
>> +
>> +	static std::unique_ptr<GlobalConfiguration> instance_;
>> +
>> +	std::unique_ptr<YamlObject> configuration_;
>> +
>> +	bool loadFile(const std::filesystem::path &fileName);
>> +	void load();
>> +	static Configuration get();
>> +};
>> +
>> +} /* namespace libcamera */
>> diff --git a/include/libcamera/internal/meson.build b/include/libcamera/internal/meson.build
>> index 1c5eef9ca..6fc6406d7 100644
>> --- a/include/libcamera/internal/meson.build
>> +++ b/include/libcamera/internal/meson.build
>> @@ -21,6 +21,7 @@ libcamera_internal_headers = files([
>>      'dma_buf_allocator.h',
>>      'formats.h',
>>      'framebuffer.h',
>> +    'global_configuration.h',
>>      'ipa_data_serializer.h',
>>      'ipa_manager.h',
>>      'ipa_module.h',
>> diff --git a/src/libcamera/base/global_configuration.cpp b/src/libcamera/base/global_configuration.cpp
>> new file mode 100644
>> index 000000000..7dcf97265
>> --- /dev/null
>> +++ b/src/libcamera/base/global_configuration.cpp
>> @@ -0,0 +1,154 @@
>> +/* SPDX-License-Identifier: LGPL-2.1-or-later */
>> +/*
>> + * Copyright (C) 2024 Red Hat, inc.
>> + *
>> + * global_configuration.cpp - Global configuration handling
>> + */
>> +
>> +#include "libcamera/internal/global_configuration.h"
>> +
>> +#include <filesystem>
>> +#include <memory>
>> +#include <sys/types.h>
>> +
>> +#include <libcamera/base/file.h>
>> +#include <libcamera/base/log.h>
>> +#include <libcamera/base/utils.h>
>> +
>> +#include "libcamera/internal/yaml_parser.h"
>> +
>> +namespace libcamera {
>> +
>> +std::unique_ptr<GlobalConfiguration> GlobalConfiguration::instance_ =
>> +	std::make_unique<GlobalConfiguration>();
>
> Why is a unique_ptr needed here?

Most notably to stick with the libcamera policy of avoiding using plain
pointers.  And for the instance disposal in initialize() below.

>> +
>> +/**
>> + * \brief Do not create GlobalConfiguration instance directly, use initialize()
>> + */
>> +void GlobalConfiguration::initialize()
>> +{
>> +	std::unique_ptr<GlobalConfiguration> instance =
>> +		std::make_unique<GlobalConfiguration>();
>> +	instance->load();
>> +	instance_ = std::move(instance);
>> +}
>> +
>> +/**
>> + * \class GlobalConfiguration
>> + * \brief Support for global libcamera configuration
>> + *
>> + * The configuration file is a YAML file and the configuration itself is stored
>> + * under `configuration' top-level item.
>> + *
>> + * The configuration file is looked up in user's home directory first and if it
>> + * is not found then in system-wide configuration directories. If multiple
>> + * configuration files exist then only the first one found is used and no
>> + * configuration merging is performed.
>> + *
>> + * The class is used as a private singleton and the configuration can be
>> + * accessed using GlobalConfiguration::configuration().
>> + */
>> +
>> +/**
>> + * \typedef GlobalConfiguration::Configuration
>> + * \brief Type representing global libcamera configuration
>> + *
>> + * All code outside GlobalConfiguration must use this type declaration and not
>> + * the underlying type.
>> + */
>> +
>> +GlobalConfiguration::GlobalConfiguration()
>> +	: configuration_(std::make_unique<YamlObject>())
>> +{
>> +}
>> +
>> +const std::vector<std::filesystem::path>
>> +	GlobalConfiguration::globalConfigurationFiles = {
>> +		std::filesystem::path(LIBCAMERA_SYSCONF_DIR) / "configuration.yaml",
>> +		std::filesystem::path("/etc/libcamera/configuration.yaml"),
>> +	};
>
> I think the above array can go into an anonymous namespace as there does not
> appear to be used anywhere else.

OK.

>> +
>> +void GlobalConfiguration::load()
>> +{
>> +	std::filesystem::path userConfigurationDirectory;
>> +	char *xdgConfigHome = utils::secure_getenv("XDG_CONFIG_HOME");
>> +	if (xdgConfigHome) {
>> +		userConfigurationDirectory = xdgConfigHome;
>> +	} else {
>> +		const char *home = utils::secure_getenv("HOME");
>> +		if (home)
>> +			userConfigurationDirectory =
>> +				std::filesystem::path(home) / ".config";
>> +	}
>> +
>> +	if (!userConfigurationDirectory.empty()) {
>> +		std::filesystem::path user_configuration_file =
>> +			userConfigurationDirectory / "libcamera" / "configuration.yaml";
>> +		if (loadFile(user_configuration_file))
>> +			return;
>> +	}
>> +
>> +	for (auto path : globalConfigurationFiles)
>
> const auto &path

OK.

>> +		if (loadFile(path))
>> +			return;
>> +}
>> +
>> +bool GlobalConfiguration::loadFile(const std::filesystem::path &fileName)
>> +{
>> +	File file(fileName);
>> +	if (!file.exists()) {
>> +		return false;
>> +	}
>> +
>> +	if (!file.open(File::OpenModeFlag::ReadOnly))
>> +		return true;
>> +
>> +	auto root = YamlParser::parse(file);
>> +	if (!root)
>> +		return true;
>> +	configuration_ = std::move(root);
>> +
>> +	return true;
>> +}
>> +
>> +GlobalConfiguration::Configuration GlobalConfiguration::get()
>> +{
>> +	return *instance_->configuration_;
>> +}
>> +
>> +/**
>> + * \brief Return configuration version
>> + *
>> + * The version is (optionally) declared in the configuration file in the
>> + * top-level section `version', alongside `configuration'. This has currently no
>> + * real use but may be needed in future if configuration incompatibilities
>> + * occur.
>> + *
>> + * \return Configuration version as declared in the configuration file or 0 if
>> + * no version is declared there
>> + */
>> +unsigned int GlobalConfiguration::version()
>> +{
>> +	return get()["version"].get<unsigned int>().value_or(0);
>> +}
>> +
>> +/**
>> + * \brief Return libcamera global configuration
>> + *
>> + * This returns the whole configuration stored in the top-level section
>> + * `configuration' of the YAML configuration file.
>> + *
>> + * The requested part of the configuration can be accessed using \a YamlObject
>> + * methods.
>> + *
>> + * \note \a YamlObject type itself shouldn't be used in type declarations to
>> + * avoid trouble if we decide to change the underlying data objects in future.
>> + *
>> + * \return The whole configuration section
>> + */
>> +GlobalConfiguration::Configuration GlobalConfiguration::configuration()
>> +{
>> +	return get()["configuration"];
>> +}
>> +
>> +} /* namespace libcamera */
>> diff --git a/src/libcamera/base/meson.build b/src/libcamera/base/meson.build
>> index 94843eb95..4c0032845 100644
>> --- a/src/libcamera/base/meson.build
>> +++ b/src/libcamera/base/meson.build
>> @@ -16,6 +16,7 @@ libcamera_base_internal_sources = files([
>>      'event_dispatcher_poll.cpp',
>>      'event_notifier.cpp',
>>      'file.cpp',
>> +    'global_configuration.cpp',
>>      'log.cpp',
>>      'memfd.cpp',
>>      'message.cpp',
>> --
>> 2.44.1
>> 
>> 
>
> Inspired by pipewire, I think the following two would be quite useful:
>   1. `LIBCAMERA_CONFIG_DIR` environmental variable, which denotes a (list of) directory(s)
>      that is searched first.
>   2. `LIBCAMERA_CONFIG_NAME` environmental variable, which denotes
>       a) the path of the configuration file to load if it is an absolute path; or
>       b) the path of the configuration file to load, relative to the configuration
>          directories
>
>   (1) would make it easy to integrate with `meson devenv`
>   (2) would make it easy to switch between configurations, which is not too easy
>       to do if the filename is hard-coded

Yes, this would be useful and I think somebody has already suggested
making the configuration location flexible.  I'll add the variables, in
additional patches.

> Regards,
> Barnabás Pőcze



More information about the libcamera-devel mailing list