diff --git a/lib/wp/private/internal-comp-loader.c b/lib/wp/private/internal-comp-loader.c index d3de4b9f..711b016b 100644 --- a/lib/wp/private/internal-comp-loader.c +++ b/lib/wp/private/internal-comp-loader.c @@ -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); diff --git a/src/config/wireplumber.conf b/src/config/wireplumber.conf index d291662c..06835974 100644 --- a/src/config/wireplumber.conf +++ b/src/config/wireplumber.conf @@ -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 = [ + ## { + ## [ = ... ] + ## } + ## ... + ## ] + ## actions = { + ## = { + ## [ = ... ] + ## } + ## ... + ## } + ## } + + { + 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 diff --git a/tests/wp/component-loader.conf b/tests/wp/component-loader.conf index f642e2ab..4e1229fa 100644 --- a/tests/wp/component-loader.conf +++ b/tests/wp/component-loader.conf @@ -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 ] + } + } + } +]