comp-loader: add support for wireplumber.components.rules

This is a new rules section that allows defining rules to modify
component definitions. This is useful to add repetitive dependencies,
for example, as in the case of "type = script/lua" that always requires
the "support.lua-scripting" feature. This can also be useful to modify
other component properties, such as the arguments, in overriding
configuration files, without needing to redefine the whole components
section.
This commit is contained in:
George Kiagiadakis
2023-11-14 12:42:32 +02:00
parent 42b64bfc28
commit 4593245fbb
3 changed files with 208 additions and 75 deletions

View File

@@ -83,12 +83,83 @@ get_feature_state (WpProperties * dict, const gchar * feature)
}
}
static ComponentData *
component_data_new_from_json (WpSpaJson * json, WpProperties * features,
static gboolean
component_rule_match_cb (gpointer data, const gchar * action, WpSpaJson * value,
GError ** error)
{
WpProperties *props = data;
g_autoptr (WpIterator) it = NULL;
g_auto (GValue) item = G_VALUE_INIT;
gboolean merge;
if (!wp_spa_json_is_object (value)) {
g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVALID_ARGUMENT,
"expected JSON object instead of: %.*s", (int) wp_spa_json_get_size (value),
wp_spa_json_get_data (value));
return FALSE;
}
if (g_str_equal (action, "merge")) {
merge = TRUE;
} else if (g_str_equal (action, "override")) {
merge = FALSE;
} else {
g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVALID_ARGUMENT,
"invalid action '%s' in component rules", action);
return FALSE;
}
it = wp_spa_json_new_iterator (value);
do {
g_autofree gchar *key = NULL;
g_autofree gchar *val = NULL;
const gchar *old_val = NULL;
/* extract key */
if (!wp_iterator_next (it, &item))
break;
key = wp_spa_json_to_string (g_value_get_boxed (&item));
g_value_unset (&item);
/* extract value */
if (!wp_iterator_next (it, &item)) {
g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVALID_ARGUMENT,
"expected value for key '%s' in component rules", key);
return FALSE;
}
val = wp_spa_json_to_string (g_value_get_boxed (&item));
g_value_unset (&item);
old_val = wp_properties_get (props, key);
/* override if not merging or if the value is not a container */
if (!merge || !old_val || (*old_val != '[' && *old_val != '{')) {
wp_properties_set (props, key, val);
}
else {
g_autoptr (WpSpaJson) old_json = NULL;
g_autoptr (WpSpaJson) new_json = NULL;
g_autoptr (WpSpaJson) merged_json = NULL;
old_json = wp_spa_json_new_wrap_string (old_val);
new_json = wp_spa_json_new_wrap_string (val);
merged_json = wp_json_utils_merge_containers (old_json, new_json);
wp_properties_set (props, key,
merged_json ? wp_spa_json_get_data (merged_json) : val);
}
} while (TRUE);
return TRUE;
}
static ComponentData *
component_data_new_from_json (WpSpaJson * json, WpProperties * features,
WpSpaJson * rules, GError ** error)
{
g_autoptr (ComponentData) comp = NULL;
g_autoptr (WpSpaJson) comp_reqs = NULL, comp_wants = NULL;
g_autoptr (WpProperties) props = NULL;
const gchar *str;
if (!wp_spa_json_is_object (json)) {
g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVALID_ARGUMENT,
@@ -102,17 +173,24 @@ component_data_new_from_json (WpSpaJson * json, WpProperties * features,
comp->requires = g_ptr_array_new_with_free_func (g_free);
comp->wants = g_ptr_array_new_with_free_func (g_free);
if (!wp_spa_json_object_get (json, "type", "s", &comp->type, NULL)) {
props = wp_properties_new_json (json);
if (rules && !wp_json_utils_match_rules (rules, props, component_rule_match_cb,
props, error))
return NULL;
if (!(comp->type = g_strdup (wp_properties_get (props, "type")))) {
g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVALID_ARGUMENT,
"component 'type' is required at: %.*s", (int) wp_spa_json_get_size (json),
wp_spa_json_get_data (json));
return NULL;
}
wp_spa_json_object_get (json, "name", "s", &comp->name, NULL);
wp_spa_json_object_get (json, "arguments", "J", &comp->arguments, NULL);
comp->name = g_strdup (wp_properties_get (props, "name"));
str = wp_properties_get (props, "arguments");
comp->arguments = str ? wp_spa_json_new_from_string (str) : NULL;
if (wp_spa_json_object_get (json, "provides", "s", &comp->provides, NULL)) {
if ((str = wp_properties_get (props, "provides"))) {
comp->provides = g_strdup (str);
comp->state = get_feature_state (features, comp->provides);
if (comp->name) {
comp->printable_id =
@@ -126,7 +204,8 @@ component_data_new_from_json (WpSpaJson * json, WpProperties * features,
comp->printable_id = g_strdup_printf ("[%s: %s]", comp->type, comp->name);
}
if (wp_spa_json_object_get (json, "requires", "J", &comp_reqs, NULL)) {
if ((str = wp_properties_get (props, "requires"))) {
g_autoptr (WpSpaJson) comp_reqs = wp_spa_json_new_wrap_string (str);
g_autoptr (WpIterator) it = wp_spa_json_new_iterator (comp_reqs);
g_auto (GValue) item = G_VALUE_INIT;
@@ -136,7 +215,8 @@ component_data_new_from_json (WpSpaJson * json, WpProperties * features,
}
}
if (wp_spa_json_object_get (json, "wants", "J", &comp_wants, NULL)) {
if ((str = wp_properties_get (props, "wants"))) {
g_autoptr (WpSpaJson) comp_wants = wp_spa_json_new_wrap_string (str);
g_autoptr (WpIterator) it = wp_spa_json_new_iterator (comp_wants);
g_auto (GValue) item = G_VALUE_INIT;
@@ -171,6 +251,8 @@ struct _WpComponentArrayLoadTask
WpSpaJson *json;
/* the features profile */
WpProperties *profile;
/* the rules to apply on each component description */
WpSpaJson *rules;
/* all components that provide a feature; key: comp->provides, value: comp */
GHashTable *feat_components;
/* the final sorted list of components to load */
@@ -317,7 +399,7 @@ parse_components (WpComponentArrayLoadTask * self, GError ** error)
GError *e = NULL;
g_autoptr (ComponentData) comp = NULL;
if (!(comp = component_data_new_from_json (cjson, self->profile, &e))) {
if (!(comp = component_data_new_from_json (cjson, self->profile, self->rules, &e))) {
g_propagate_error (error, e);
return FALSE;
}
@@ -459,6 +541,7 @@ wp_component_array_load_task_finalize (GObject * object)
g_clear_pointer (&self->feat_components, g_hash_table_unref);
g_clear_pointer (&self->components, g_ptr_array_unref);
g_clear_pointer (&self->profile, wp_properties_unref);
g_clear_pointer (&self->rules, wp_spa_json_unref);
g_clear_pointer (&self->json, wp_spa_json_unref);
G_OBJECT_CLASS (wp_component_array_load_task_parent_class)->finalize (object);
@@ -478,7 +561,7 @@ wp_component_array_load_task_class_init (WpComponentArrayLoadTaskClass * klass)
static WpTransition *
wp_component_array_load_task_new (WpSpaJson * json, WpProperties * profile,
gpointer source_object, GCancellable * cancellable,
WpSpaJson * rules, gpointer source_object, GCancellable * cancellable,
GAsyncReadyCallback callback, gpointer callback_data)
{
WpTransition *t = wp_transition_new (wp_component_array_load_task_get_type (),
@@ -486,6 +569,7 @@ wp_component_array_load_task_new (WpSpaJson * json, WpProperties * profile,
WpComponentArrayLoadTask *task = WP_COMPONENT_ARRAY_LOAD_TASK (t);
task->json = wp_spa_json_ref (json);
task->profile = wp_properties_ref (profile);
task->rules = rules ? wp_spa_json_ref (rules) : NULL;
return t;
}
@@ -641,23 +725,30 @@ wp_internal_comp_loader_load (WpComponentLoader * self, WpCore * core,
if (g_str_equal (type, "profile") || g_str_equal (type, "array")) {
WpTransition *task = NULL;
g_autoptr (WpSpaJson) components = NULL;
g_autoptr (WpSpaJson) rules = NULL;
g_autoptr (WpProperties) profile = wp_properties_new_empty ();
if (g_str_equal (type, "profile")) {
/* component name is the profile name;
component list and profile features are loaded from config */
g_autoptr (WpConf) conf = wp_conf_get_instance (core);
g_autoptr (WpSpaJson) profile_json =
g_autoptr (WpSpaJson) profile_json = NULL;
profile_json =
wp_conf_get_value (conf, "wireplumber.profiles", component, NULL);
if (profile_json)
wp_properties_update_from_json (profile, profile_json);
components = wp_conf_get_section (conf, "wireplumber.components", NULL);
} else {
rules = wp_conf_get_section (conf, "wireplumber.components.rules", NULL);
}
else {
/* component list is retrieved from args; profile features are empty */
components = wp_spa_json_ref (args);
}
task = wp_component_array_load_task_new (components, profile, self,
task = wp_component_array_load_task_new (components, profile, rules, self,
cancellable, callback, data);
wp_transition_set_data (task, g_object_ref (core), g_object_unref);
wp_transition_set_source_tag (task, wp_internal_comp_loader_load);

View File

@@ -214,7 +214,6 @@ wireplumber.components = [
name = metadata.lua, type = script/lua
arguments = { metadata.name = default }
provides = metadata.default
requires = [ support.lua-scripting ]
}
## Provide the "filters" pw_metadata
@@ -222,7 +221,6 @@ wireplumber.components = [
name = metadata.lua, type = script/lua
arguments = { metadata.name = filters }
provides = metadata.filters
requires = [ support.lua-scripting ]
}
## Device monitors' optional features
@@ -243,54 +241,49 @@ wireplumber.components = [
{
name = monitors/alsa.lua, type = script/lua
provides = monitor.alsa
requires = [ support.lua-scripting, support.export-core ]
requires = [ support.export-core ]
wants = [ monitor.alsa.reserve-device ]
}
{
name = monitors/bluez.lua, type = script/lua
provides = monitor.bluez
requires = [ support.lua-scripting, support.export-core ]
requires = [ support.export-core ]
wants = [ monitor.bluetooth.seat-monitoring ]
}
{
name = monitors/bluez-midi.lua, type = script/lua
provides = monitor.bluez-midi
requires = [ support.lua-scripting, support.export-core ]
requires = [ support.export-core ]
wants = [ monitor.bluetooth.seat-monitoring ]
}
{
name = monitors/alsa-midi.lua, type = script/lua
provides = monitor.alsa-midi
requires = [ support.lua-scripting ]
wants = [ monitor.alsa-midi.monitoring ]
}
## v4l2 monitor hooks
{
name = monitors/v4l2/name-device.lua, type = script/lua
provides = hooks.monitor.v4l2-name-device
requires = [ support.lua-scripting,
support.export-core,
requires = [ support.export-core,
support.standard-event-source ]
}
{
name = monitors/v4l2/create-device.lua, type = script/lua
provides = hooks.monitor.v4l2-create-device
requires = [ support.lua-scripting,
support.export-core,
requires = [ support.export-core,
support.standard-event-source ]
}
{
name = monitors/v4l2/name-node.lua, type = script/lua
provides = hooks.monitor.v4l2-name-node
requires = [ support.lua-scripting,
support.export-core,
requires = [ support.export-core,
support.standard-event-source ]
}
{
name = monitors/v4l2/create-node.lua, type = script/lua
provides = hooks.monitor.v4l2-create-node
requires = [ support.lua-scripting,
support.export-core,
requires = [ support.export-core,
support.standard-event-source ]
}
{
@@ -304,8 +297,7 @@ wireplumber.components = [
{
name = monitors/v4l2/enumerate-device.lua, type = script/lua
provides = hooks.monitor.v4l2-enumerate-device
requires = [ support.lua-scripting,
support.export-core,
requires = [ support.export-core,
support.standard-event-source,
monitor.v4l2.hooks ]
}
@@ -319,29 +311,25 @@ wireplumber.components = [
{
name = monitors/libcamera/name-device.lua, type = script/lua
provides = hooks.monitor.libcamera-name-device
requires = [ support.lua-scripting,
support.export-core,
requires = [ support.export-core,
support.standard-event-source ]
}
{
name = monitors/libcamera/create-device.lua, type = script/lua
provides = hooks.monitor.libcamera-create-device
requires = [ support.lua-scripting,
support.export-core,
requires = [ support.export-core,
support.standard-event-source ]
}
{
name = monitors/libcamera/name-node.lua, type = script/lua
provides = hooks.monitor.libcamera-name-node
requires = [ support.lua-scripting,
support.export-core,
requires = [ support.export-core,
support.standard-event-source ]
}
{
name = monitors/libcamera/create-node.lua, type = script/lua
provides = hooks.monitor.libcamera-create-node
requires = [ support.lua-scripting,
support.export-core,
requires = [ support.export-core,
support.standard-event-source ]
}
{
@@ -355,8 +343,7 @@ wireplumber.components = [
{
name = monitors/libcamera/enumerate-device.lua, type = script/lua
provides = hooks.monitor.libcamera-enumerate-device
requires = [ support.lua-scripting,
support.export-core,
requires = [ support.export-core,
support.standard-event-source,
monitor.libcamera.hooks ]
}
@@ -370,12 +357,11 @@ wireplumber.components = [
{
name = client/access-default.lua, type = script/lua
provides = script.client.access-default
requires = [ support.lua-scripting ]
}
{
name = client/access-portal.lua, type = script/lua
provides = script.client.access-portal
requires = [ support.lua-scripting, support.portal-permissionstore ]
requires = [ support.portal-permissionstore ]
}
{
type = virtual, provides = policy.client.access
@@ -387,27 +373,22 @@ wireplumber.components = [
{
name = device/select-profile.lua, type = script/lua
provides = hooks.device.profile.select
requires = [ support.lua-scripting ]
}
{
name = device/find-best-profile.lua, type = script/lua
provides = hooks.device.profile.find-best
requires = [ support.lua-scripting ]
}
{
name = device/state-profile.lua, type = script/lua
provides = hooks.device.profile.state
requires = [ support.lua-scripting ]
}
{
name = device/apply-profile.lua, type = script/lua
provides = hooks.device.profile.apply
requires = [ support.lua-scripting ]
}
{
name = device/autoswitch-bluetooth-profile.lua, type = script/lua
provides = hooks.device.profile.autoswitch-bluetooth
requires = [ support.lua-scripting ]
}
{
type = virtual, provides = policy.device.profile
@@ -422,22 +403,18 @@ wireplumber.components = [
{
name = device/select-routes.lua, type = script/lua
provides = hooks.device.routes.select
requires = [ support.lua-scripting ]
}
{
name = device/find-best-routes.lua, type = script/lua
provides = hooks.device.routes.find-best
requires = [ support.lua-scripting ]
}
{
name = device/state-routes.lua, type = script/lua
provides = hooks.device.routes.state
requires = [ support.lua-scripting ]
}
{
name = device/apply-routes.lua, type = script/lua
provides = hooks.device.routes.apply
requires = [ support.lua-scripting ]
}
{
type = virtual, provides = policy.device.routes
@@ -451,27 +428,25 @@ wireplumber.components = [
{
name = default-nodes/rescan.lua, type = script/lua
provides = hooks.default-nodes.rescan
requires = [ support.lua-scripting ]
}
{
name = default-nodes/find-selected-default-node.lua, type = script/lua
provides = hooks.default-nodes.find-selected
requires = [ support.lua-scripting, metadata.default ]
requires = [ metadata.default ]
}
{
name = default-nodes/find-best-default-node.lua, type = script/lua
provides = hooks.default-nodes.find-best
requires = [ support.lua-scripting ]
}
{
name = default-nodes/state-default-nodes.lua, type = script/lua
provides = hooks.default-nodes.state
requires = [ support.lua-scripting, metadata.default ]
requires = [ metadata.default ]
}
{
name = default-nodes/apply-default-node.lua, type = script/lua,
provides = hooks.default-nodes.apply
requires = [ support.lua-scripting, metadata.default ]
requires = [ metadata.default ]
}
{
type = virtual, provides = policy.default-nodes
@@ -486,74 +461,67 @@ wireplumber.components = [
{
name = node/create-item.lua, type = script/lua
provides = hooks.node.create-session-item
requires = [ support.lua-scripting, si.audio-adapter, si.node ]
requires = [ si.audio-adapter, si.node ]
}
{
name = node/suspend-node.lua, type = script/lua
provides = hooks.node.suspend
requires = [ support.lua-scripting ]
}
{
name = node/state-stream.lua, type = script/lua
provides = hooks.stream.state
requires = [ support.lua-scripting ]
}
{
name = linking/filter-forward-format.lua, type = script/lua
provides = hooks.loopback.forward-format
requires = [ support.lua-scripting ]
}
{
name = node/create-virtual-item.lua, type = script/lua
provides = script.create-role-items
requires = [ support.lua-scripting, si.audio-virtual ]
requires = [ si.audio-virtual ]
}
## Linking hooks
{
name = linking/rescan.lua, type = script/lua
provides = hooks.linking.rescan
requires = [ support.lua-scripting ]
}
{
name = linking/move-follow.lua, type = script/lua
provides = hooks.linking.move-follow
requires = [ support.lua-scripting ]
}
{
name = linking/find-defined-target.lua, type = script/lua
provides = hooks.linking.target.find-defined
requires = [ support.lua-scripting ]
}
{
name = linking/find-filter-target.lua, type = script/lua
provides = hooks.linking.target.find-filter
requires = [ support.lua-scripting, metadata.filters ]
requires = [ metadata.filters ]
}
{
name = linking/find-default-target.lua, type = script/lua
provides = hooks.linking.target.find-default
requires = [ support.lua-scripting, api.default-nodes ]
requires = [ api.default-nodes ]
}
{
name = linking/find-best-target.lua, type = script/lua
provides = hooks.linking.target.find-best
requires = [ support.lua-scripting ]
}
{
name = linking/get-filter-from-target.lua, type = script/lua
provides = hooks.linking.target.get-filter-from
requires = [ support.lua-scripting, metadata.filters ]
requires = [ metadata.filters ]
}
{
name = linking/prepare-link.lua, type = script/lua
provides = hooks.linking.target.prepare-link
requires = [ support.lua-scripting, api.default-nodes ]
requires = [ api.default-nodes ]
}
{
name = linking/link-target.lua, type = script/lua
provides = hooks.linking.target.link
requires = [ support.lua-scripting, si.standard-link ]
requires = [ si.standard-link ]
}
{
type = virtual, provides = policy.linking.standard
@@ -572,12 +540,11 @@ wireplumber.components = [
{
name = linking/rescan-virtual-links.lua, type = script/lua
provides = hooks.linking.role-priority-system.links.rescan
requires = [ support.lua-scripting, api.mixer ]
requires = [ api.mixer ]
}
{
name = linking/find-virtual-target.lua, type = script/lua
provides = hooks.linking.role-priority-system.target.find
requires = [ support.lua-scripting ]
}
{
type = virtual, provides = policy.linking.role-priority-system
@@ -619,6 +586,38 @@ wireplumber.components = [
}
]
wireplumber.components.rules = [
## Rules to apply on top of wireplumber.components
## Syntax:
## {
## matches = [
## {
## [ <key> = <value> ... ]
## }
## ...
## ]
## actions = {
## <override|merge> = {
## [ <key> = <value> ... ]
## }
## ...
## }
## }
{
matches = [
{
type = "script/lua"
}
]
actions = {
merge = {
requires = [ support.lua-scripting ]
}
}
}
]
wireplumber.settings = {
## This main config file is only supposed to contain the common settings and
## rules. rest of the settings and rules are distributed across

View File

@@ -21,17 +21,17 @@ wireplumber.components = [
name = two
type = test
provides = support.two
requires = [ support.one support.six ]
requires = [ support.one ]
}
{
type = virtual
provides = virtual.four
requires = [ support.four ]
requires = [ INVALID ]
}
{
name = three
type = test
provides = support.three
provides = INVALID
wants = [ support.two ]
}
{
@@ -64,3 +64,46 @@ wireplumber.components = [
requires = [ support.four ]
}
]
wireplumber.components.rules = [
{
matches = [
{
name = two
}
]
actions = {
merge = {
# final array should be [ support.one, support.six ]
# if this fails, support.six will not be loaded
requires = [ support.six ]
}
}
}
{
matches = [
{
name = three
}
]
actions = {
merge = {
provides = support.three
}
}
}
{
matches = [
{
provides = virtual.four
}
]
actions = {
override = {
requires = [ support.four ]
}
}
}
]