From f629b71a0f8084f42baa235546dfaade4a7a7b97 Mon Sep 17 00:00:00 2001
From: David Bauer <mail@david-bauer.net>
Date: Sun, 5 Nov 2023 16:08:07 +0100
Subject: [PATCH] build: implement flexible site-selection system

Implement a flexible system for handling site-defined features as well
as packages.

This system is inspired by the existing feature-system and allows for a
more flexible approach for selecting specific packages for devices.

Features are now defined in a `features` file in the site-root. The same
goes for packages.

These files are sequentially evaluated and the device-package list is
evaluated for each device independently.

Signed-off-by: David Bauer <mail@david-bauer.net>
---
 docs/site-example/image-customization | 20 ++++++
 docs/site-example/site.mk             | 28 ---------
 scripts/image_customization_lib.lua   | 89 +++++++++++++++++++++++++++
 scripts/target_config_lib.lua         | 40 +++++++-----
 4 files changed, 132 insertions(+), 45 deletions(-)
 create mode 100644 docs/site-example/image-customization
 create mode 100644 scripts/image_customization_lib.lua

diff --git a/docs/site-example/image-customization b/docs/site-example/image-customization
new file mode 100644
index 000000000..709987352
--- /dev/null
+++ b/docs/site-example/image-customization
@@ -0,0 +1,20 @@
+packages({'iwinfo'})
+
+features({
+	'autoupdater',
+	'ebtables-filter-multicast',
+	'ebtables-filter-ra-dhcp',
+	'ebtables-limit-arp',
+	'mesh-batman-adv-15',
+	'mesh-vpn-fastd',
+	'respondd',
+	'status-page',
+	'web-advanced',
+	'web-wizard'
+})
+
+if not device_class('tiny') then
+	features({
+		'wireless-encryption-wpa3'
+	})
+end
diff --git a/docs/site-example/site.mk b/docs/site-example/site.mk
index 30671b181..e50d404da 100644
--- a/docs/site-example/site.mk
+++ b/docs/site-example/site.mk
@@ -1,33 +1,5 @@
 ##	gluon site.mk makefile example
 
-##	GLUON_FEATURES
-#		Specify Gluon features/packages to enable;
-#		Gluon will automatically enable a set of packages
-#		depending on the combination of features listed
-
-GLUON_FEATURES := \
-	autoupdater \
-	ebtables-filter-multicast \
-	ebtables-filter-ra-dhcp \
-	ebtables-limit-arp \
-	mesh-batman-adv-15 \
-	mesh-vpn-fastd \
-	respondd \
-	status-page \
-	web-advanced \
-	web-wizard
-
-GLUON_FEATURES_standard := \
-  wireless-encryption-wpa3
-
-##	GLUON_SITE_PACKAGES
-#		Specify additional Gluon/OpenWrt packages to include here;
-#		A minus sign may be prepended to remove a packages from the
-#		selection that would be enabled by default or due to the
-#		chosen feature flags
-
-GLUON_SITE_PACKAGES := iwinfo
-
 ##	DEFAULT_GLUON_RELEASE
 #		version string to use for images
 #		gluon relies on
diff --git a/scripts/image_customization_lib.lua b/scripts/image_customization_lib.lua
new file mode 100644
index 000000000..6808383d8
--- /dev/null
+++ b/scripts/image_customization_lib.lua
@@ -0,0 +1,89 @@
+local M = {}
+
+local function collect_keys(t)
+	local ret = {}
+	for v in pairs(t) do
+		table.insert(ret, v)
+	end
+	return ret
+end
+
+local function evaluate_device(files, env, dev)
+	local selections = {}
+	local funcs = {}
+	local device_disabled = false
+
+	local function add_elements(element_type, element_list)
+		for _, element in ipairs(element_list) do
+			if not selections[element_type] then
+				selections[element_type] = {}
+			end
+
+			selections[element_type][element] = true
+		end
+	end
+
+	function funcs.features(features)
+		add_elements('feature', features)
+	end
+
+	function funcs.packages(packages)
+		add_elements('package', packages)
+	end
+
+	function funcs.device(device_names)
+		assert(
+			type(device_names) == 'table',
+			'Incorrect use of device(): pass a list of device names as argument')
+
+		for _, device_name in ipairs(device_names) do
+			if device_name == dev.image then
+				return true
+			end
+		end
+
+		return false
+	end
+
+	function funcs.target(target, subtarget)
+		assert(
+			type(target) == 'string',
+			'Incorrect use of target(): pass a target name as first argument')
+
+		if target ~= env.BOARD then
+			return false
+		end
+
+		if subtarget and subtarget ~= env.SUBTARGET then
+			return false
+		end
+
+		return true
+	end
+
+	function funcs.device_class(class)
+		return dev.options.class == class
+	end
+
+	-- Evaluate the feature definition files
+	for _, file in ipairs(files) do
+		local f, err = loadfile(file)
+		if not f then
+			error('Failed to parse feature definition: ' .. err)
+		end
+		setfenv(f, funcs)
+		f()
+	end
+
+	return {
+		selections = selections,
+		device_disabled = device_disabled,
+	}
+end
+
+function M.get_selection(selection_type, files, env, dev)
+	local eval_result = evaluate_device(files, env, dev)
+	return collect_keys(eval_result.selections[selection_type] or {})
+end
+
+return M
diff --git a/scripts/target_config_lib.lua b/scripts/target_config_lib.lua
index ef487f061..6895e9c2b 100644
--- a/scripts/target_config_lib.lua
+++ b/scripts/target_config_lib.lua
@@ -1,5 +1,6 @@
 local lib = dofile('scripts/target_lib.lua')
 local feature_lib = dofile('scripts/feature_lib.lua')
+local image_customization_lib = dofile('scripts/image_customization_lib.lua')
 local env = lib.env
 
 local target = env.GLUON_TARGET
@@ -86,10 +87,6 @@ END_MAKE
 	lib.escape(var)))
 end
 
-local function site_packages(image)
-	return split(site_vars(string.format('$(GLUON_%s_SITE_PACKAGES)', image)))
-end
-
 local function feature_packages(features)
 	local files = {'package/features'}
 	for _, feed in ipairs(feeds) do
@@ -102,20 +99,30 @@ local function feature_packages(features)
 	return feature_lib.get_packages(files, features)
 end
 
--- This involves running a few processes to evaluate site.mk, so we add a simple cache
-local class_cache = {}
-local function class_packages(class)
-	if class_cache[class] then
-		return class_cache[class]
-	end
+local function site_specific_packages(dev_info)
+	local image_custoization = env.GLUON_SITEDIR .. '/image-customization'
+
+	local site_packages = {}
+	local feature_inherited_pkgs = {}
 
-	local features = site_vars(string.format('$(GLUON_FEATURES) $(GLUON_FEATURES_%s)', class))
-	features = compact_list(split(features), false)
+	if file_exists(image_custoization) then
+		local site_features
+
+		-- First read enabled features from site
+		site_features = image_customization_lib.get_selection('feature', {image_custoization}, env, dev_info)
+		site_features = compact_list(site_features, false)
+
+		-- Create List from packages inherited from features
+		feature_inherited_pkgs = feature_packages(site_features)
+
+		-- Read list of packages from site
+		site_packages = image_customization_lib.get_selection('package', {image_custoization}, env, dev_info)
+	end
 
-	local pkgs = feature_packages(features)
-	pkgs = concat_list(pkgs, split(site_vars(string.format('$(GLUON_SITE_PACKAGES) $(GLUON_SITE_PACKAGES_%s)', class))))
+	-- Concat feature-packages with site-packages
+	local pkgs = concat_list(feature_inherited_pkgs, site_packages)
 
-	class_cache[class] = pkgs
+	-- Negations for the resulting package-list are dealt with in the calling function
 	return pkgs
 end
 
@@ -192,9 +199,8 @@ for _, dev in ipairs(lib.devices) do
 	end
 
 	handle_pkgs(lib.target_packages)
-	handle_pkgs(class_packages(dev.options.class))
 	handle_pkgs(dev.options.packages or {})
-	handle_pkgs(site_packages(dev.image))
+	handle_pkgs(site_specific_packages(dev))
 
 	local profile_config = string.format('%s_DEVICE_%s', openwrt_config_target, dev.name)
 	lib.config(
-- 
GitLab