From 6ca841bad59ca04be54e73cf07e7152aabab16cf Mon Sep 17 00:00:00 2001
From: Matthias Schiffer <mschiffer@universe-factory.net>
Date: Sun, 9 Jul 2017 01:09:15 +0200
Subject: [PATCH] build: introduce GLUON_FEATURES

To reduce the number of packages that need to be listed in
GLUON_SITE_PACKAGES, this adds a new variable GLUON_FEATURES. Sets of
packages are enabled automatically based on the combination of listed
feature flags.

Site-specified package feeds can provide their own feature flag
definitions.
---
 Makefile                   |  8 +++-
 docs/dev/feature-flags.rst | 45 ++++++++++++++++++++++
 docs/index.rst             |  1 +
 docs/site-example/site.mk  | 46 +++++++++++------------
 docs/user/site.rst         | 43 ++++++++++++++++++---
 package/features           | 26 +++++++++++++
 scripts/features.sh        | 76 ++++++++++++++++++++++++++++++++++++++
 7 files changed, 216 insertions(+), 29 deletions(-)
 create mode 100644 docs/dev/feature-flags.rst
 create mode 100644 package/features
 create mode 100755 scripts/features.sh

diff --git a/Makefile b/Makefile
index 8154c61da..8403981e7 100644
--- a/Makefile
+++ b/Makefile
@@ -77,13 +77,19 @@ list-targets: FORCE
 
 GLUON_DEFAULT_PACKAGES := -odhcpd -ppp -ppp-mod-pppoe -wpad-mini gluon-core ip6tables hostapd-mini
 
+GLUON_FEATURE_PACKAGES := $(shell scripts/features.sh '$(GLUON_FEATURES)')
+ifneq ($(.SHELLSTATUS),0)
+$(error Error while evaluating GLUON_FEATURES)
+endif
+
+
 GLUON_PACKAGES :=
 define merge_packages
   $(foreach pkg,$(1),
     GLUON_PACKAGES := $$(strip $$(filter-out -$$(patsubst -%,%,$(pkg)) $$(patsubst -%,%,$(pkg)),$$(GLUON_PACKAGES)) $(pkg))
   )
 endef
-$(eval $(call merge_packages,$(GLUON_DEFAULT_PACKAGES) $(GLUON_SITE_PACKAGES)))
+$(eval $(call merge_packages,$(GLUON_DEFAULT_PACKAGES) $(GLUON_FEATURE_PACKAGES) $(GLUON_SITE_PACKAGES)))
 
 GLUON_PACKAGES_YES := $(filter-out -%,$(GLUON_PACKAGES))
 GLUON_PACKAGES_NO := $(patsubst -%,%,$(filter -%,$(GLUON_PACKAGES)))
diff --git a/docs/dev/feature-flags.rst b/docs/dev/feature-flags.rst
new file mode 100644
index 000000000..19f43af74
--- /dev/null
+++ b/docs/dev/feature-flags.rst
@@ -0,0 +1,45 @@
+Feature flags
+=============
+
+Feature flags provide a convenient way to define package selections without
+making it necessary to list each package explicitly.
+
+The main feature flag definition file is ``package/features``, but each package
+feed can provide addition defintions in a file called ``features`` at the root
+of the feed repository.
+
+Each flag *$flag* without any explicit definition will simply include the package
+with the name *gluon-$flag* by default. The feature definition file can modify
+the package selection in two ways:
+
+* The *nodefault* function suppresses default of including the *gluon-$flag*
+  package
+* The *packages* function adds a list of packages (or removes, when package
+  names are prepended with minus signs) when a given logical expression
+  is satisfied
+
+Example::
+
+    nodefault 'web-wizard'
+
+    packages 'web-wizard' \
+      'gluon-config-mode-hostname' \
+      'gluon-config-mode-geo-location' \
+      'gluon-config-mode-contact-info'
+
+    packages 'web-wizard & (mesh-vpn-fastd | mesh-vpn-tunneldigger)' \
+      'gluon-config-mode-mesh-vpn'
+
+This will
+
+* Disable the inclusion of a (non-existent) package called *gluon-web-wizard*
+* Enable three config mode packages when the *web-wizard* feature is enabled
+* Enable *gluon-config-mode-mesh-vpn* when both *web-wizard* and one
+  of *mesh-vpn-fastd* and *mesh-vpn-tunneldigger* is enabled
+
+Supported syntax elements of logical expressions are:
+
+* \& (and)
+* \| (or)
+* \! (not)
+* parentheses
diff --git a/docs/index.rst b/docs/index.rst
index 7778b23d5..53d6c33e8 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -33,6 +33,7 @@ Several Freifunk communities in Germany use Gluon as the foundation of their Fre
    :maxdepth: 2
 
    dev/basics
+   dev/feature-flags
    dev/hardware
    dev/upgrade
    dev/wan
diff --git a/docs/site-example/site.mk b/docs/site-example/site.mk
index 8f03eb5e9..091fb50d1 100644
--- a/docs/site-example/site.mk
+++ b/docs/site-example/site.mk
@@ -1,29 +1,29 @@
 ##	gluon site.mk makefile example
 
 ##	GLUON_SITE_PACKAGES
-#		specify Gluon/LEDE packages to include here
-
-GLUON_SITE_PACKAGES := \
-	gluon-alfred \
-	gluon-respondd \
-	gluon-autoupdater \
-	gluon-config-mode-autoupdater \
-	gluon-config-mode-contact-info \
-	gluon-config-mode-geo-location \
-	gluon-config-mode-hostname \
-	gluon-config-mode-mesh-vpn \
-	gluon-ebtables-filter-multicast \
-	gluon-ebtables-filter-ra-dhcp \
-	gluon-web-admin \
-	gluon-web-autoupdater \
-	gluon-web-network \
-	gluon-web-wifi-config \
-	gluon-mesh-batman-adv-15 \
-	gluon-mesh-vpn-fastd \
-	gluon-radvd \
-	gluon-status-page \
-	haveged \
-	iwinfo
+#		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 \
+	mesh-batman-adv-15 \
+	mesh-vpn-fastd \
+	radvd \
+	respondd \
+	status-page \
+	web-advanced \
+	web-wizard
+
+##	GLUON_SITE_PACKAGES
+#		Specify additional Gluon/LEDE 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 := haveged iwinfo
 
 ##	DEFAULT_GLUON_RELEASE
 #		version string to use for images
diff --git a/docs/user/site.rst b/docs/user/site.rst
index d82cdd956..723bd4a7a 100644
--- a/docs/user/site.rst
+++ b/docs/user/site.rst
@@ -382,15 +382,20 @@ legacy \: package
              wifi_names = {'wifi_freifunk', 'wifi_freifunk5', 'wifi_mesh', 'wifi_mesh5'},
       }
 
-Packages
---------
+Build configuration
+-------------------
 
-The ``site.mk`` is a Makefile which should define constants
+The ``site.mk`` is a Makefile which defines various values
 involved in the build process of Gluon.
 
+GLUON_FEATURES
+    Defines a list of features to include. The feature list is used to generate
+    the default package set.
+
 GLUON_SITE_PACKAGES
-    Defines a list of packages which should be installed additionally
-    to the ``gluon-core`` package.
+    Defines a list of packages which should be installed in addition to the
+    default package set. It is also possible to remove packages from the
+    default set by prepending a minus sign to the package name.
 
 GLUON_RELEASE
     The current release version Gluon should use.
@@ -407,6 +412,34 @@ GLUON_LANGS
     List of languages (as two-letter-codes) to be included in the web interface. Should always contain
     ``en``.
 
+Features
+^^^^^^^^
+
+Most feature flags enable only a single package that is derived from the flag
+name; for example, the flag *mesh-batman-adv-15* will include the package
+*gluon-mesh-batman-adv-15*.
+
+The following flags will add multiple packages:
+
+* *web-wizard*
+
+  - *gluon-config-mode-hostname*
+  - *gluon-config-mode-geo-location*
+  - *gluon-config-mode-contact-info*
+  - *gluon-config-mode-autoupdater* (if the *autoupdater* feature is enabled)
+  - *gluon-config-mode-mesh-vpn* (if the *mesh-vpn-fastd* or *mesh-vpn-tunneldigger* feature is enabled)
+
+* *web-advanced*
+
+  - *gluon-web-admin*
+  - *gluon-web-network*
+  - *gluon-web-wifi-config*
+  - *gluon-web-autoupdater* (if the *autoupdater* feature is enabled)
+  - *gluon-web-mesh-vpn-fastd* (if the *mesh-vpn-fastd* feature is enabled)
+
+Site-provided package feeds can define additional feature flags.
+
+
 .. _site-config-mode-texts:
 
 Config mode texts
diff --git a/package/features b/package/features
new file mode 100644
index 000000000..265158a01
--- /dev/null
+++ b/package/features
@@ -0,0 +1,26 @@
+nodefault 'web-wizard'
+
+packages 'web-wizard' \
+	'gluon-config-mode-hostname' \
+	'gluon-config-mode-geo-location' \
+	'gluon-config-mode-contact-info'
+
+packages 'web-wizard & autoupdater' \
+	'gluon-config-mode-autoupdater'
+
+packages 'web-wizard & (mesh-vpn-fastd | mesh-vpn-tunneldigger)' \
+	'gluon-config-mode-mesh-vpn'
+
+
+nodefault 'web-advanced'
+
+packages 'web-advanced' \
+	'gluon-web-admin' \
+	'gluon-web-network' \
+	'gluon-web-wifi-config'
+
+packages 'web-advanced & autoupdater' \
+	'gluon-web-autoupdater'
+
+packages 'web-advanced & mesh-vpn-fastd' \
+	'gluon-web-mesh-vpn-fastd'
diff --git a/scripts/features.sh b/scripts/features.sh
new file mode 100755
index 000000000..2d35fdeeb
--- /dev/null
+++ b/scripts/features.sh
@@ -0,0 +1,76 @@
+#!/bin/bash --norc
+
+set -e
+shopt -s nullglob
+
+
+nodefault() {
+	# We define a function instead of a variable, as variables could
+	# be predefined in the environment (in theory)
+	eval "gluon_feature_nodefault_$1() {
+		:
+	}"
+}
+
+packages() {
+	:
+}
+
+for f in package/features packages/*/features; do
+	. "$f"
+done
+
+
+# Shell variables can't contain minus signs, so we escape them
+# using underscores (and also escape underscores to avoid mapping
+# multiple inputs to the same output)
+sanitize() {
+	local v="$1"
+	v="${v//_/_1}"
+	v="${v//-/_2}"
+	echo -n "$v"
+}
+
+vars=
+
+for feature in $1; do
+	if [ "$(type -t gluon_feature_nodefault_${feature})" != 'function' ]; then
+		echo "gluon-${feature}"
+	fi
+
+	vars="$vars $(sanitize "$feature")=1"
+done
+
+
+nodefault() {
+	:
+}
+
+packages() {
+	local cond="$(sanitize "$1")"
+	shift
+
+	# We only allow variable names, parentheses and the operators: & | !
+	if [ "$(expr match "$cond" '.*[^A-Za-z0-9_()&|! ].*')" -gt 0 ]; then
+		exit 1
+	fi
+
+	# Let will return false when the result of the passed expression is 0,
+	# so we always add 1. This way false is only returned for syntax errors.
+	local ret="$(env -i $vars bash --norc -ec "let _result_='1+($cond)'; echo -n \"\$_result_\"" 2>/dev/null)"
+	case "$ret" in
+	2)
+		for pkg in "$@"; do
+			echo "$pkg"
+		done
+		;;
+	1)
+		;;
+	*)
+		exit 1
+	esac
+}
+
+for f in package/features packages/*/features; do
+	. "$f"
+done
-- 
GitLab