From 071cf7b20f88df2c6a14ee85cc40c6ce635ec118 Mon Sep 17 00:00:00 2001
From: Matthias Schiffer <mschiffer@universe-factory.net>
Date: Fri, 14 Jun 2019 19:20:15 +0200
Subject: [PATCH] Switch to Lua for target definitions

The old bash-based parsing code was way too complex. Replace it with Lua.
---
 Makefile                        |  43 +--
 package/gluon-site/Makefile     |   4 +-
 scripts/clean_output.lua        |  17 +
 scripts/clean_output.sh         |  20 -
 scripts/common.inc.lua          | 246 +++++++++++++
 scripts/common.inc.sh           |  78 ----
 scripts/copy_output.lua         |  67 ++++
 scripts/copy_output.sh          | 202 ----------
 scripts/generate_manifest.lua   |  55 +++
 scripts/generate_manifest.sh    | 113 ------
 scripts/site.sh                 |   4 -
 scripts/site_config.lua         |  15 +-
 scripts/target_config.inc.lua   |  75 ++++
 scripts/target_config.inc.sh    |  18 -
 scripts/target_config.lua       |  30 ++
 scripts/target_config.sh        |  82 -----
 scripts/target_config_check.lua |  68 ++++
 scripts/target_config_check.sh  | 102 ------
 targets/ar71xx-generic          | 632 ++++++++++++++++++--------------
 targets/ar71xx-mikrotik         |  20 +-
 targets/ar71xx-nand             |  35 +-
 targets/ar71xx-tiny             | 162 ++++----
 targets/brcm2708-bcm2708        |  11 +-
 targets/brcm2708-bcm2709        |   9 +-
 targets/brcm2708-bcm2710        |   4 +-
 targets/brcm2708.inc            |   8 +-
 targets/generic                 |  59 +--
 targets/ipq40xx                 |  97 +++--
 targets/ipq806x                 |  50 +--
 targets/mpc85xx-generic         |   2 +-
 targets/mvebu-cortexa9          |   5 +-
 targets/ramips-mt7620           |  54 +--
 targets/ramips-mt7621           |  63 ++--
 targets/ramips-mt76x8           |  46 ++-
 targets/ramips-rt305x           |  33 +-
 targets/sunxi-cortexa7          |  22 +-
 targets/x86-64                  |  10 +-
 targets/x86-generic             |  17 +-
 targets/x86-geode               |  25 +-
 targets/x86.inc                 |  48 ++-
 40 files changed, 1423 insertions(+), 1228 deletions(-)
 create mode 100755 scripts/clean_output.lua
 delete mode 100755 scripts/clean_output.sh
 create mode 100644 scripts/common.inc.lua
 delete mode 100644 scripts/common.inc.sh
 create mode 100755 scripts/copy_output.lua
 delete mode 100755 scripts/copy_output.sh
 create mode 100755 scripts/generate_manifest.lua
 delete mode 100755 scripts/generate_manifest.sh
 delete mode 100755 scripts/site.sh
 create mode 100644 scripts/target_config.inc.lua
 delete mode 100644 scripts/target_config.inc.sh
 create mode 100755 scripts/target_config.lua
 delete mode 100755 scripts/target_config.sh
 create mode 100755 scripts/target_config_check.lua
 delete mode 100755 scripts/target_config_check.sh

diff --git a/Makefile b/Makefile
index 5ebefa21c..13c62a710 100644
--- a/Makefile
+++ b/Makefile
@@ -87,18 +87,14 @@ GLUON_CONFIG_VARS := \
 	BOARD='$(BOARD)' \
 	SUBTARGET='$(SUBTARGET)'
 
-OPENWRT_TARGET := $(BOARD)$(if $(SUBTARGET),-$(SUBTARGET))
 
-export OPENWRT_TARGET
-
-
-CheckTarget := [ '$(OPENWRT_TARGET)' ] \
+CheckTarget := [ '$(BOARD)' ] \
 	|| (echo 'Please set GLUON_TARGET to a valid target. Gluon supports the following targets:'; $(foreach target,$(GLUON_TARGETS),echo ' * $(target)';) false)
 
 CheckExternal := test -d openwrt || (echo 'You don'"'"'t seem to have obtained the external repositories needed by Gluon; please call `make update` first!'; false)
 
 define CheckSite
-	@GLUON_SITEDIR='$(GLUON_SITEDIR)' GLUON_SITE_CONFIG='$(1).conf' $(LUA) scripts/site_config.lua \
+	@GLUON_SITEDIR='$(GLUON_SITEDIR)' GLUON_SITE_CONFIG='$(1).conf' $(LUA) -e 'assert(dofile("scripts/site_config.lua")(os.getenv("GLUON_SITE_CONFIG")))' \
 		|| (echo 'Your site configuration ($(1).conf) did not pass validation.'; false)
 
 endef
@@ -123,18 +119,6 @@ define merge_packages
 endef
 $(eval $(call merge_packages,$(GLUON_DEFAULT_PACKAGES) $(GLUON_FEATURE_PACKAGES) $(GLUON_SITE_PACKAGES)))
 
-config: FORCE
-	@$(CheckExternal)
-	@$(CheckTarget)
-
-	@$(GLUON_CONFIG_VARS) \
-		scripts/target_config.sh '$(GLUON_TARGET)' '$(GLUON_PACKAGES)' \
-		> openwrt/.config
-	+@$(OPENWRTMAKE) defconfig
-
-	@$(GLUON_CONFIG_VARS) \
-		scripts/target_config_check.sh '$(GLUON_TARGET)' '$(GLUON_PACKAGES)'
-
 
 LUA := openwrt/staging_dir/hostpkg/bin/lua
 
@@ -145,14 +129,27 @@ $(LUA):
 	+@$(OPENWRTMAKE) tools/install
 	+@$(OPENWRTMAKE) package/lua/host/compile
 
-prepare-target: config $(LUA) ;
 
-all: prepare-target
+config: $(LUA) FORCE
+	@$(CheckExternal)
+	@$(CheckTarget)
 	$(foreach conf,site $(patsubst $(GLUON_SITEDIR)/%.conf,%,$(wildcard $(GLUON_SITEDIR)/domains/*.conf)),$(call CheckSite,$(conf)))
 
-	@scripts/clean_output.sh
+	@$(GLUON_CONFIG_VARS) \
+		$(LUA) scripts/target_config.lua '$(GLUON_TARGET)' '$(GLUON_PACKAGES)' \
+		> openwrt/.config
+	+@$(OPENWRTMAKE) defconfig
+
+	@$(GLUON_CONFIG_VARS) \
+		$(LUA) scripts/target_config_check.lua '$(GLUON_TARGET)' '$(GLUON_PACKAGES)'
+
+
+all: config
+	@$(GLUON_CONFIG_VARS) \
+		$(LUA) scripts/clean_output.lua
 	+@$(OPENWRTMAKE)
-	@GLUON_SITEDIR='$(GLUON_SITEDIR)' scripts/copy_output.sh '$(GLUON_TARGET)'
+	@$(GLUON_CONFIG_VARS) \
+		$(LUA) scripts/copy_output.lua '$(GLUON_TARGET)'
 
 clean download: config
 	+@$(OPENWRTMAKE) $@
@@ -173,7 +170,7 @@ manifest: $(LUA) FORCE
 		echo 'PRIORITY=$(GLUON_PRIORITY)' && \
 		echo && \
 		$(foreach GLUON_TARGET,$(GLUON_TARGETS), \
-			GLUON_SITEDIR='$(GLUON_SITEDIR)' scripts/generate_manifest.sh '$(GLUON_TARGET)' && \
+			GLUON_SITEDIR='$(GLUON_SITEDIR)' $(LUA) scripts/generate_manifest.lua '$(GLUON_TARGET)' && \
 		) : \
 	) > 'tmp/$(GLUON_BRANCH).manifest.tmp'
 
diff --git a/package/gluon-site/Makefile b/package/gluon-site/Makefile
index dd4432bc4..b1d2d91bb 100644
--- a/package/gluon-site/Makefile
+++ b/package/gluon-site/Makefile
@@ -37,7 +37,9 @@ config GLUON_MULTIDOMAIN
 endef
 
 define GenerateJSON
-	GLUON_SITEDIR='$$(GLUON_SITEDIR)' GLUON_SITE_CONFIG='$(1).conf' lua -e 'print(require("cjson").encode(assert(dofile("../../scripts/site_config.lua"))))' > '$$(PKG_BUILD_DIR)/$(1).json'
+	GLUON_SITEDIR='$$(GLUON_SITEDIR)' GLUON_SITE_CONFIG='$(1).conf' \
+		lua -e 'print(require("cjson").encode(assert(dofile("../../scripts/site_config.lua")(os.getenv("GLUON_SITE_CONFIG")))))' \
+		> '$$(PKG_BUILD_DIR)/$(1).json'
 endef
 
 define Build/Compile
diff --git a/scripts/clean_output.lua b/scripts/clean_output.lua
new file mode 100755
index 000000000..68510f93f
--- /dev/null
+++ b/scripts/clean_output.lua
@@ -0,0 +1,17 @@
+dofile('scripts/common.inc.lua')
+
+
+local subtarget = env.SUBTARGET
+if subtarget == '' then
+	subtarget = 'generic'
+end
+
+local bindir = env.BOARD .. '/' .. subtarget
+
+
+exec({'rm', '-f', 'openwrt/bin/targets/'..bindir..'/\0'}, true, '2>/dev/null')
+
+-- Full builds will output the "packages" directory, so clean up first
+if (env.GLUON_DEVICES or '') == '' then
+	exec {'rm', '-rf', 'openwrt/bin/targets/'..bindir..'/packages'}
+end
diff --git a/scripts/clean_output.sh b/scripts/clean_output.sh
deleted file mode 100755
index c71fc86ac..000000000
--- a/scripts/clean_output.sh
+++ /dev/null
@@ -1,20 +0,0 @@
-#!/usr/bin/env bash
-
-set -e
-
-[ "$OPENWRT_TARGET" ] || exit 1
-
-
-. scripts/common.inc.sh
-
-
-if [ "$(expr match "$OPENWRT_TARGET" '.*-.*')" -gt 0 ]; then
-	OPENWRT_BINDIR="${OPENWRT_TARGET//-/\/}"
-else
-	OPENWRT_BINDIR="${OPENWRT_TARGET}/generic"
-fi
-
-rm -f "openwrt/bin/targets/${OPENWRT_BINDIR}"/* 2>/dev/null || true
-
-# Full builds will output the "packages" directory, so clean up first
-[ "$GLUON_DEVICES" ] || rm -rf "openwrt/bin/targets/${OPENWRT_BINDIR}/packages"
diff --git a/scripts/common.inc.lua b/scripts/common.inc.lua
new file mode 100644
index 000000000..0e365b369
--- /dev/null
+++ b/scripts/common.inc.lua
@@ -0,0 +1,246 @@
+env = setmetatable({}, {
+	__index = function(t, k) return os.getenv(k) end
+})
+envtrue = setmetatable({}, {
+	__index = function(t, k) return (tonumber(os.getenv(k)) or 0) > 0 end
+})
+
+assert(env.GLUON_SITEDIR)
+assert(env.GLUON_TARGETSDIR)
+assert(env.GLUON_RELEASE)
+
+
+site_code = assert(assert(dofile('scripts/site_config.lua')('site.conf')).site_code)
+
+
+target_packages = {}
+
+local default_options = {
+	profile = false,
+	factory = '-squashfs-factory',
+	factory_ext = '.bin',
+	sysupgrade = '-squashfs-sysupgrade',
+	sysupgrade_ext = '.bin',
+	extra_images = {},
+	aliases = {},
+	manifest_aliases = {},
+	packages = {},
+	broken = false,
+}
+
+
+local gluon_devices, unknown_devices = {}, {}
+for dev in string.gmatch(env.GLUON_DEVICES or '', '%S+') do
+	gluon_devices[dev] = true
+	unknown_devices[dev] = true
+end
+
+local function want_device(dev, options)
+	if options.broken and not envtrue.BROKEN then
+		return false
+	end
+
+	if (env.GLUON_DEVICES or '') == '' then
+		return true
+	end
+
+	unknown_devices[dev] = nil
+	return gluon_devices[dev]
+end
+
+
+local function merge(a, b)
+	local ret = {}
+	for k, v in pairs(a) do
+		ret[k] = v
+	end
+	for k, v in pairs(b or {}) do
+		assert(ret[k] ~= nil, string.format("unknown option '%s'", k))
+		ret[k] = v
+	end
+	return ret
+end
+
+-- Escapes a single argument to be used in a shell command
+-- The argument is surrounded by single quotes, single quotes inside the
+-- argument are replaced by '\''.
+-- To allow using shell wildcards, zero bytes in the arguments are replaced
+-- by unquoted asterisks.
+function escape(s)
+	s = string.gsub(s, "'", "'\\''")
+	s = string.gsub(s, "%z", "'*'")
+	return "'" .. s .. "'"
+end
+
+local function escape_command(command, raw)
+	local ret = 'exec'
+	for _, arg in ipairs(command) do
+		ret = ret .. ' ' .. escape(arg)
+	end
+	if raw then
+		ret = ret .. ' ' .. raw
+	end
+	return ret
+end
+
+function exec_raw(command, may_fail)
+	local ret = os.execute(command)
+	assert((ret == 0) or may_fail)
+	return ret
+end
+
+function exec(command, may_fail, raw)
+	return exec_raw(escape_command(command, raw), may_fail)
+end
+
+function exec_capture_raw(command)
+	local f = io.popen(command)
+	assert(f)
+
+	local data = f:read('*a')
+	f:close()
+	return data
+end
+
+function exec_capture(command, raw)
+	return exec_capture_raw(escape_command(command, raw))
+end
+
+
+local image_mt = {
+	__index = {
+		dest_name = function(image, name, site, release)
+			return env.GLUON_IMAGEDIR..'/'..image.subdir, 'gluon-'..(site or site_code)..'-'..(release or env.GLUON_RELEASE)..'-'..name..image.out_suffix..image.extension
+		end,
+	},
+}
+
+local function add_image(image)
+	table.insert(images, setmetatable(image, image_mt))
+end
+
+
+-- Variables to be consumed by scripts using common.inc.lua
+devices = {}
+images = {}
+opkg = true
+
+
+function config() end
+function try_config() end
+
+function packages(pkgs)
+	for _, pkg in ipairs(pkgs) do
+		table.insert(target_packages, pkg)
+	end
+end
+
+function device(image, name, options)
+	options = merge(default_options, options)
+
+	if not want_device(image, options) then
+		return
+	end
+
+	table.insert(devices, {
+		image = image,
+		name = name,
+		options = options,
+	})
+
+	if options.factory then
+		add_image {
+			image = image,
+			name = name,
+			subdir = 'factory',
+			in_suffix = options.factory,
+			out_suffix = '',
+			extension = options.factory_ext,
+			aliases = options.aliases,
+			manifest_aliases = options.manifest_aliases,
+		}
+	end
+	if options.sysupgrade then
+		add_image {
+			image = image,
+			name = name,
+			subdir = 'sysupgrade',
+			in_suffix = options.sysupgrade,
+			out_suffix = '-sysupgrade',
+			extension = options.sysupgrade_ext,
+			aliases = options.aliases,
+			manifest_aliases = options.manifest_aliases,
+		}
+	end
+	for _, extra_image in ipairs(options.extra_images) do
+		add_image {
+			image = image,
+			name = name,
+			subdir = 'other',
+			in_suffix = extra_image[1],
+			out_suffix = extra_image[2],
+			extension = extra_image[3],
+			aliases = options.aliases,
+			manifest_aliases = options.manifest_aliases,
+		}
+	end
+end
+
+function factory_image(image, name, ext, options)
+	options = merge(default_options, options)
+
+	if not want_device(image, options) then
+		return
+	end
+
+	add_image {
+		image = image,
+		name = name,
+		subdir = 'factory',
+		in_suffix = '',
+		out_suffix = '',
+		extension = ext,
+		aliases = options.aliases,
+		manifest_aliases = options.manifest_aliases,
+	}
+end
+
+function sysupgrade_image(image, name, ext, options)
+	options = merge(default_options, options)
+
+	if not want_device(image, options) then
+		return
+	end
+
+	add_image {
+		image = image,
+		name = name,
+		subdir = 'sysupgrade',
+		in_suffix = '',
+		out_suffix = '-sysupgrade',
+		extension = ext,
+		aliases = options.aliases,
+		manifest_aliases = options.manifest_aliases,
+	}
+end
+
+function no_opkg()
+	opkg = false
+end
+
+function defaults(options)
+	default_options = merge(default_options, options)
+end
+
+
+function check_devices()
+	local device_list = {}
+	for device in pairs(unknown_devices) do
+		table.insert(device_list, device)
+	end
+	if #device_list ~= 0 then
+		table.sort(device_list)
+		io.stderr:write('Error: unknown devices given: ', table.concat(device_list, ' '), '\n')
+		os.exit(1)
+	end
+end
diff --git a/scripts/common.inc.sh b/scripts/common.inc.sh
deleted file mode 100644
index c3ad5670e..000000000
--- a/scripts/common.inc.sh
+++ /dev/null
@@ -1,78 +0,0 @@
-config() {
-	:
-}
-
-try_config() {
-	:
-}
-
-device() {
-	:
-}
-
-factory_image() {
-	:
-}
-
-sysupgrade_image() {
-	:
-}
-
-alias() {
-	:
-}
-
-manifest_alias() {
-	:
-}
-
-packages() {
-	:
-}
-
-factory() {
-	:
-}
-
-sysupgrade() {
-	:
-}
-
-extra_image() {
-	:
-}
-
-no_opkg() {
-	:
-}
-
-
-unknown_devices="$GLUON_DEVICES"
-
-want_device() {
-	[ "$GLUON_DEVICES" ] || return 0
-
-	local new_devices=''
-
-	for device in $unknown_devices; do
-		if [ "$device" != "$1" ]; then
-			new_devices="${new_devices:+${new_devices} }$device"
-		fi
-	done
-	unknown_devices=$new_devices
-
-	for device in $GLUON_DEVICES; do
-		if [ "$device" = "$1" ]; then
-			return 0
-		fi
-	done
-
-	return 1
-}
-
-check_devices() {
-	if [ "$unknown_devices" ]; then
-		echo "Error: unknown devices given: ${unknown_devices}" >&2
-		exit 1
-	fi
-}
diff --git a/scripts/copy_output.lua b/scripts/copy_output.lua
new file mode 100755
index 000000000..dc679507f
--- /dev/null
+++ b/scripts/copy_output.lua
@@ -0,0 +1,67 @@
+dofile('scripts/common.inc.lua')
+
+assert(env.GLUON_IMAGEDIR)
+assert(env.GLUON_PACKAGEDIR)
+assert(env.GLUON_TARGETSDIR)
+
+
+local target = arg[1]
+
+local openwrt_target
+local subtarget = env.SUBTARGET
+if subtarget ~= '' then
+	openwrt_target = env.BOARD .. '-' .. subtarget
+else
+	openwrt_target = env.BOARD
+	subtarget = 'generic'
+end
+
+local bindir = env.BOARD .. '/' .. subtarget
+
+
+local function mkdir(dir)
+	exec {'mkdir', '-p', dir}
+end
+
+mkdir(env.GLUON_IMAGEDIR..'/factory')
+mkdir(env.GLUON_IMAGEDIR..'/sysupgrade')
+mkdir(env.GLUON_IMAGEDIR..'/other')
+
+
+dofile(env.GLUON_TARGETSDIR..'/'..target)
+
+
+local function clean(image, name)
+	local dir, file = image:dest_name(name, '\0', '\0')
+	exec {'rm', '-f', dir..'/'..file}
+end
+
+for _, image in ipairs(images) do
+	clean(image, image.image)
+
+	local destdir, destname = image:dest_name(image.image)
+	local source = string.format('openwrt/bin/targets/%s/openwrt-%s-%s%s%s', bindir, openwrt_target, image.name, image.in_suffix, image.extension)
+
+	exec {'cp', source, destdir..'/'..destname}
+
+	for _, alias in ipairs(image.aliases) do
+		clean(image, alias)
+
+		local _, aliasname = image:dest_name(alias)
+		exec {'ln', '-s', destname, destdir..'/'..aliasname}
+	end
+end
+
+
+-- Copy opkg repo
+if opkg and (env.GLUON_DEVICES or '') == '' then
+	local package_prefix = string.format('gluon-%s-%s', site_code, env.GLUON_RELEASE)
+	local function dest_dir(prefix)
+		return env.GLUON_PACKAGEDIR..'/'..prefix..'/'..bindir
+	end
+
+	exec {'rm', '-f', dest_dir('\0')..'/\0'}
+	exec({'rmdir', '-p', dest_dir('\0')}, true, '2>/dev/null')
+	mkdir(dest_dir(package_prefix))
+	exec {'cp', 'openwrt/bin/targets/'..bindir..'/packages/\0', dest_dir(package_prefix)}
+end
diff --git a/scripts/copy_output.sh b/scripts/copy_output.sh
deleted file mode 100755
index 897a3f83c..000000000
--- a/scripts/copy_output.sh
+++ /dev/null
@@ -1,202 +0,0 @@
-#!/usr/bin/env bash
-
-set -e
-
-[ "$GLUON_IMAGEDIR" -a "$GLUON_PACKAGEDIR" -a "$OPENWRT_TARGET" -a "$GLUON_RELEASE" -a "$GLUON_SITEDIR" -a "$GLUON_TARGETSDIR" ] || exit 1
-
-
-default_factory_ext='.bin'
-default_factory_suffix='-squashfs-factory'
-default_sysupgrade_ext='.bin'
-default_sysupgrade_suffix='-squashfs-sysupgrade'
-default_extra_images=
-
-output=
-profile=
-aliases=
-
-factory_ext=
-factory_suffix=
-sysupgrade_ext=
-sysupgrade_suffix=
-extra_images=
-
-no_opkg=
-
-
-mkdir -p "${GLUON_IMAGEDIR}/factory" "${GLUON_IMAGEDIR}/sysupgrade" "${GLUON_IMAGEDIR}/other"
-
-if [ "$(expr match "$OPENWRT_TARGET" '.*-.*')" -gt 0 ]; then
-	OPENWRT_BINDIR="${OPENWRT_TARGET//-/\/}"
-else
-	OPENWRT_BINDIR="${OPENWRT_TARGET}/generic"
-fi
-
-SITE_CODE="$(scripts/site.sh site_code)"
-PACKAGE_PREFIX="gluon-${SITE_CODE}-${GLUON_RELEASE}"
-
-
-do_clean() {
-	local dir="$1"
-	local out_suffix="$2"
-	local ext="$3"
-	local name="$4"
-
-	rm -f "${GLUON_IMAGEDIR}/${dir}/gluon-"*"-${name}${out_suffix}${ext}"
-}
-
-get_file() {
-	local dir="$1"
-	local out_suffix="$2"
-	local ext="$3"
-	local name="$4"
-
-	echo "${GLUON_IMAGEDIR}/${dir}/gluon-${SITE_CODE}-${GLUON_RELEASE}-${name}${out_suffix}${ext}"
-}
-
-do_copy() {
-	local dir="$1"
-	local in_suffix="$2"
-	local out_suffix="$3"
-	local ext="$4"
-	local aliases="$5"
-
-	local file="$(get_file "$dir" "$out_suffix" "$ext" "$output")"
-
-	do_clean "$dir" "$out_suffix" "$ext" "$output"
-	cp "openwrt/bin/targets/${OPENWRT_BINDIR}/openwrt-${OPENWRT_TARGET}${profile}${in_suffix}${ext}" "$file"
-
-	for alias in $aliases; do
-		do_clean "$dir" "$out_suffix" "$ext" "$alias"
-		ln -s "$(basename "$file")" "$(get_file "$dir" "$out_suffix" "$ext" "$alias")"
-	done
-}
-
-copy() {
-	[ "$output" ] || return 0
-	want_device "$output" || return 0
-
-	[ -z "$factory_ext" ] || do_copy 'factory' "$factory_suffix" '' "$factory_ext" "$aliases"
-	[ -z "$sysupgrade_ext" ] || do_copy 'sysupgrade' "$sysupgrade_suffix" '-sysupgrade' "$sysupgrade_ext" "$aliases"
-
-	echo -n "$extra_images" | while read in_suffix && read out_suffix && read ext; do
-		do_copy 'other' "$in_suffix" "$out_suffix" "$ext" "$aliases"
-	done
-}
-
-
-. scripts/common.inc.sh
-
-device() {
-	copy
-
-	output="$1"
-	profile="-$2"
-	aliases=
-
-	factory_ext="$default_factory_ext"
-	factory_suffix="$default_factory_suffix"
-	sysupgrade_ext="$default_sysupgrade_ext"
-	sysupgrade_suffix="$default_sysupgrade_suffix"
-	extra_images="$default_extra_images"
-}
-
-factory_image() {
-	copy
-
-	output="$1"
-	aliases=
-
-	if [ "$3" ]; then
-		profile="-$2"
-		factory_ext="$3"
-	else
-		profile=""
-		factory_ext="$2"
-	fi
-
-	factory_suffix=
-	sysupgrade_ext=
-	sysupgrade_suffix=
-}
-
-sysupgrade_image() {
-	copy
-
-	output="$1"
-	aliases=
-
-	if [ "$3" ]; then
-		profile="-$2"
-		sysupgrade_ext="$3"
-	else
-		profile=""
-		sysupgrade_ext="$2"
-	fi
-
-	factory_ext=
-	factory_suffix=
-	sysupgrade_suffix=
-}
-
-alias() {
-	aliases="$aliases $1"
-}
-
-factory() {
-	if [ "$2" ]; then
-		factory_suffix="$1"
-		factory_ext="$2"
-	else
-		factory_ext="$1"
-	fi
-
-	if [ -z "$profile" ]; then
-		default_factory_ext="$factory_ext"
-		default_factory_suffix="$factory_suffix"
-	fi
-}
-
-sysupgrade() {
-	if [ "$2" ]; then
-		sysupgrade_suffix="$1"
-		sysupgrade_ext="$2"
-	else
-		sysupgrade_ext="$1"
-	fi
-
-	if [ -z "$output" ]; then
-		default_sysupgrade_ext="$sysupgrade_ext"
-		default_sysupgrade_suffix="$sysupgrade_suffix"
-	fi
-}
-
-extra_image() {
-	local in_suffix="$1"
-	local out_suffix="$2"
-	local ext="$3"
-
-	extra_images="$in_suffix
-$out_suffix
-$ext
-$extra_images"
-
-	if [ -z "$output" ]; then
-		default_extra_images="$extra_images"
-	fi
-}
-
-no_opkg() {
-	no_opkg=1
-}
-
-
-. "${GLUON_TARGETSDIR}/$1"; copy
-
-# Copy opkg repo
-if [ -z "$no_opkg" -a -z "$GLUON_DEVICES" ]; then
-	rm -f "$GLUON_PACKAGEDIR"/*/"$OPENWRT_BINDIR"/*
-	rmdir -p "$GLUON_PACKAGEDIR"/*/"$OPENWRT_BINDIR" 2>/dev/null || true
-	mkdir -p "${GLUON_PACKAGEDIR}/${PACKAGE_PREFIX}/${OPENWRT_BINDIR}"
-	cp "openwrt/bin/targets/${OPENWRT_BINDIR}/packages"/* "${GLUON_PACKAGEDIR}/${PACKAGE_PREFIX}/${OPENWRT_BINDIR}"
-fi
diff --git a/scripts/generate_manifest.lua b/scripts/generate_manifest.lua
new file mode 100755
index 000000000..8351d8bdb
--- /dev/null
+++ b/scripts/generate_manifest.lua
@@ -0,0 +1,55 @@
+dofile('scripts/common.inc.lua')
+
+assert(env.GLUON_IMAGEDIR)
+assert(env.GLUON_TARGETSDIR)
+
+
+local target = arg[1]
+
+dofile(env.GLUON_TARGETSDIR..'/'..target)
+
+
+local function strip(s)
+	return string.gsub(s, '\n$', '')
+end
+
+local function generate_line(model, dir, filename, filesize)
+	local exists = pcall(exec, {'test', '-e', dir..'/'..filename})
+	if not exists then
+		return
+	end
+
+	local file256sum = strip(exec_capture {'scripts/sha256sum.sh', dir..'/'..filename})
+	local file512sum = strip(exec_capture {'scripts/sha512sum.sh', dir..'/'..filename})
+
+	io.stdout:write(string.format('%s %s %s %s %s\n', model, env.GLUON_RELEASE, file256sum, filesize, filename))
+	io.stdout:write(string.format('%s %s %s %s\n', model, env.GLUON_RELEASE, file256sum, filename))
+	io.stdout:write(string.format('%s %s %s %s\n', model, env.GLUON_RELEASE, file512sum, filename))
+end
+
+local function generate(image)
+	local dir, filename = image:dest_name(image.image)
+	local exists = pcall(exec, {'test', '-e', dir..'/'..filename})
+	if not exists then
+		return
+	end
+
+	local filesize = strip(exec_capture {'scripts/filesize.sh', dir..'/'..filename})
+
+	generate_line(image.image, dir, filename, filesize)
+
+	for _, alias in ipairs(image.aliases) do
+		local aliasdir, aliasname = image:dest_name(alias)
+		generate_line(alias, aliasdir, aliasname, filesize)
+	end
+
+	for _, alias in ipairs(image.manifest_aliases) do
+		generate_line(alias, dir, filename, filesize)
+	end
+end
+
+for _, image in ipairs(images) do
+	if image.subdir == 'sysupgrade' then
+		generate(image)
+	end
+end
diff --git a/scripts/generate_manifest.sh b/scripts/generate_manifest.sh
deleted file mode 100755
index 04494a37f..000000000
--- a/scripts/generate_manifest.sh
+++ /dev/null
@@ -1,113 +0,0 @@
-#!/usr/bin/env bash
-
-set -e
-
-[ "$GLUON_IMAGEDIR" -a "$GLUON_RELEASE" -a "$GLUON_SITEDIR" -a "$GLUON_TARGETSDIR" ] || exit 1
-
-
-default_sysupgrade_ext='.bin'
-
-output=
-aliases=
-manifest_aliases=
-
-sysupgrade_ext=
-
-
-SITE_CODE="$(scripts/site.sh site_code)"
-
-
-get_filename() {
-	local name="$1"
-	echo -n "gluon-${SITE_CODE}-${GLUON_RELEASE}-${name}-sysupgrade${sysupgrade_ext}"
-}
-
-get_filepath() {
-	local filename="$1"
-	echo -n "${GLUON_IMAGEDIR}/sysupgrade/${filename}"
-}
-
-generate_line() {
-	local model="$1"
-	local filename="$2"
-	local filesize="$3"
-
-	local filepath="$(get_filepath "$filename")"
-	[ -e "$filepath" ] || return 0
-
-	local file256sum="$(scripts/sha256sum.sh "$filepath")"
-	local file512sum="$(scripts/sha512sum.sh "$filepath")"
-
-	echo "$model $GLUON_RELEASE $file256sum $filesize $filename"
-	echo "$model $GLUON_RELEASE $file256sum $filename"
-	echo "$model $GLUON_RELEASE $file512sum $filename"
-}
-
-generate() {
-	[ "${output}" ] || return 0
-	[ "$sysupgrade_ext" ] || return 0
-
-	local filename="$(get_filename "$output")"
-	local filepath="$(get_filepath "$filename")"
-	[ -e "$filepath" ] || return 0
-	local filesize="$(scripts/filesize.sh "$filepath")"
-
-	generate_line "$output" "$filename" "$filesize"
-
-	for alias in $aliases; do
-		generate_line "$alias" "$(get_filename "$alias")" "$filesize"
-	done
-
-	for alias in $manifest_aliases; do
-		generate_line "$alias" "$filename" "$filesize"
-	done
-}
-
-
-. scripts/common.inc.sh
-
-device() {
-	generate
-
-	output="$1"
-	aliases=
-	manifest_aliases=
-
-	sysupgrade_ext="$default_sysupgrade_ext"
-}
-
-sysupgrade_image() {
-	generate
-
-	output="$1"
-	aliases=
-	manifest_aliases=
-
-	if [ "$3" ]; then
-		sysupgrade_ext="$3"
-	else
-		sysupgrade_ext="$2"
-	fi
-}
-
-alias() {
-	aliases="$aliases $1"
-}
-
-manifest_alias() {
-	manifest_aliases="$manifest_aliases $1"
-}
-
-sysupgrade() {
-	if [ "$2" ]; then
-		sysupgrade_ext="$2"
-	else
-		sysupgrade_ext="$1"
-	fi
-
-	if [ -z "$output" ]; then
-		default_sysupgrade_ext="$sysupgrade_ext"
-	fi
-}
-
-. "${GLUON_TARGETSDIR}/$1"; generate
diff --git a/scripts/site.sh b/scripts/site.sh
deleted file mode 100755
index 76f39524a..000000000
--- a/scripts/site.sh
+++ /dev/null
@@ -1,4 +0,0 @@
-#!/bin/sh
-
-export GLUON_SITE_CONFIG=site.conf
-exec openwrt/staging_dir/hostpkg/bin/lua -e "print(assert(dofile('scripts/site_config.lua').$1))" 2>/dev/null
diff --git a/scripts/site_config.lua b/scripts/site_config.lua
index 2ac95de13..f49eb95c6 100644
--- a/scripts/site_config.lua
+++ b/scripts/site_config.lua
@@ -1,10 +1,11 @@
 local site = os.getenv('GLUON_SITEDIR') .. '/'
-local config = os.getenv('GLUON_SITE_CONFIG')
 
-local function loader()
-   coroutine.yield('return ')
-   coroutine.yield(io.open(site..config):read('*a'))
-end
+return function(config)
+	local function loader()
+		coroutine.yield('return ')
+		coroutine.yield(io.open(site..config):read('*a'))
+	end
 
--- setfenv doesn't work with Lua 5.2 anymore, but we're using 5.1
-return setfenv(assert(load(coroutine.wrap(loader), config)), {})()
+	-- setfenv doesn't work with Lua 5.2 anymore, but we're using 5.1
+	return setfenv(assert(load(coroutine.wrap(loader), config)), {})()
+end
diff --git a/scripts/target_config.inc.lua b/scripts/target_config.inc.lua
new file mode 100644
index 000000000..e001148bd
--- /dev/null
+++ b/scripts/target_config.inc.lua
@@ -0,0 +1,75 @@
+assert(env.BOARD)
+assert(env.SUBTARGET)
+
+
+local target = arg[1]
+local extra_packages = arg[2]
+
+local openwrt_config_target
+if env.SUBTARGET ~= '' then
+	openwrt_config_target = env.BOARD .. '_' .. env.SUBTARGET
+else
+	openwrt_config_target = env.BOARD
+end
+
+
+local function site_packages(profile)
+	return exec_capture_raw(string.format([[
+	MAKEFLAGS= make print PROFILE=%s --no-print-directory -s -f - <<'END_MAKE'
+include $(GLUON_SITEDIR)/site.mk
+
+print:
+	echo -n '$(GLUON_$(PROFILE)_SITE_PACKAGES)'
+END_MAKE
+	]], escape(profile)))
+end
+
+dofile(env.GLUON_TARGETSDIR .. '/generic')
+for pkg in string.gmatch(extra_packages, '%S+') do
+	packages {pkg}
+end
+dofile(env.GLUON_TARGETSDIR .. '/' .. target)
+
+check_devices()
+
+
+if not opkg then
+	config '# CONFIG_SIGNED_PACKAGES is not set'
+	config 'CONFIG_CLEAN_IPKG=y'
+	packages {'-opkg'}
+end
+
+
+local default_pkgs = ''
+for _, pkg in ipairs(target_packages) do
+	default_pkgs = default_pkgs .. ' ' .. pkg
+
+	if string.sub(pkg, 1, 1) == '-' then
+		try_config('# CONFIG_PACKAGE_%s is not set', string.sub(pkg, 2))
+	else
+		config_package(pkg, 'y')
+	end
+end
+
+for _, dev in ipairs(devices) do
+	local profile = dev.options.profile or dev.name
+	local device_pkgs = default_pkgs
+
+	local function handle_pkg(pkg)
+		if string.sub(pkg, 1, 1) ~= '-' then
+			config_package(pkg, 'm')
+		end
+		device_pkgs = device_pkgs .. ' ' .. pkg
+	end
+
+	for _, pkg in ipairs(dev.options.packages or {}) do
+		handle_pkg(pkg)
+	end
+	for pkg in string.gmatch(site_packages(profile), '%S+') do
+		handle_pkg(pkg)
+	end
+
+	config_message(string.format("unable to enable device '%s'", profile), 'CONFIG_TARGET_DEVICE_%s_DEVICE_%s=y', openwrt_config_target, profile)
+	config('CONFIG_TARGET_DEVICE_PACKAGES_%s_DEVICE_%s="%s"',
+		openwrt_config_target, profile, device_pkgs)
+end
diff --git a/scripts/target_config.inc.sh b/scripts/target_config.inc.sh
deleted file mode 100644
index b2d6fa8b7..000000000
--- a/scripts/target_config.inc.sh
+++ /dev/null
@@ -1,18 +0,0 @@
-site_packages() {
-	MAKEFLAGS= make print PROFILE="$1" --no-print-directory -s -f - <<'END_MAKE'
-include $(GLUON_SITEDIR)/site.mk
-
-print:
-	echo '$(GLUON_$(PROFILE)_SITE_PACKAGES)'
-END_MAKE
-}
-
-
-. scripts/common.inc.sh
-
-
-no_opkg() {
-	config '# CONFIG_SIGNED_PACKAGES is not set'
-	config 'CONFIG_CLEAN_IPKG=y'
-	packages '-opkg'
-}
diff --git a/scripts/target_config.lua b/scripts/target_config.lua
new file mode 100755
index 000000000..5e0c04a0b
--- /dev/null
+++ b/scripts/target_config.lua
@@ -0,0 +1,30 @@
+dofile('scripts/common.inc.lua')
+
+
+local output = {}
+
+
+function config(...)
+	table.insert(output, string.format(...))
+end
+
+try_config = config
+
+
+function config_message(msg, ...)
+	config(...)
+end
+
+function config_package(pkg, value)
+	config('CONFIG_PACKAGE_%s=%s', pkg, value)
+end
+
+
+dofile('scripts/target_config.inc.lua')
+
+
+-- The sort will make =y entries override =m ones
+table.sort(output)
+for _, line in ipairs(output) do
+	io.stdout:write(line, '\n')
+end
diff --git a/scripts/target_config.sh b/scripts/target_config.sh
deleted file mode 100755
index 559eabea1..000000000
--- a/scripts/target_config.sh
+++ /dev/null
@@ -1,82 +0,0 @@
-#!/usr/bin/env bash
-
-set -e
-
-[ "$OPENWRT_TARGET" -a "$GLUON_TARGETSDIR" ] || exit 1
-
-target="$1"
-packages=$2
-
-
-output=
-profile=
-default_packages=
-profile_packages=
-
-
-OPENWRT_CONFIG_TARGET="${OPENWRT_TARGET//-/_}"
-
-
-emit() {
-	[ "${output}" ] || return 0
-	want_device "${output}" || return 0
-
-	profile_packages="${profile_packages} $(site_packages "$output")"
-
-	for package in $profile_packages; do
-		[ "${package:0:1}" = '-' ] || echo "CONFIG_PACKAGE_${package}=m"
-	done
-
-	echo "CONFIG_TARGET_DEVICE_${OPENWRT_CONFIG_TARGET}_DEVICE_${profile}=y"
-	echo "CONFIG_TARGET_DEVICE_PACKAGES_${OPENWRT_CONFIG_TARGET}_DEVICE_${profile}=\"${profile_packages}\""
-}
-
-
-. scripts/target_config.inc.sh
-
-config() {
-	echo "$1"
-}
-
-try_config() {
-	echo "$1"
-}
-
-device() {
-	emit
-
-	output="$1"
-	profile="$3"
-	if [ -z "$profile" ]; then
-		profile="$2"
-	fi
-
-	profile_packages="${default_packages}"
-}
-
-packages() {
-	if [ "${output}" ]; then
-		profile_packages="${profile_packages} $@"
-	else
-		default_packages="${default_packages} $@"
-
-		for package in "$@"; do
-			if [ "${package:0:1}" = '-' ]; then
-				echo "# CONFIG_PACKAGE_${package:1} is not set"
-			else
-				echo "CONFIG_PACKAGE_${package}=y"
-			fi
-		done
-	fi
-}
-
-
-# The sort will not only remove duplicate entries,
-# but also magically make =y entries override =m ones
-(
-	. "${GLUON_TARGETSDIR}/generic"
-	packages $packages
-
-	. "${GLUON_TARGETSDIR}/$target"
-	emit
-) | sort -u
diff --git a/scripts/target_config_check.lua b/scripts/target_config_check.lua
new file mode 100755
index 000000000..abb1487b1
--- /dev/null
+++ b/scripts/target_config_check.lua
@@ -0,0 +1,68 @@
+dofile('scripts/common.inc.lua')
+
+
+local ret = 0
+
+
+local function fail(...)
+	if ret == 0 then
+		ret = 1
+		io.stderr:write('Configuration failed:', '\n')
+	end
+
+	io.stderr:write(' * ', string.format(...), '\n')
+end
+
+local function match_config(f)
+	for line in io.lines('openwrt/.config') do
+		if f(line) then
+			return true
+		end
+	end
+
+	return false
+end
+
+local function check_config(pattern)
+	return match_config(function(line) return line == pattern end)
+end
+
+local function check_config_prefix(pattern)
+	return match_config(function(line) return string.sub(line, 1, -2) == pattern end)
+end
+
+function config(...)
+	local pattern = string.format(...)
+
+	if not check_config(pattern) then
+		fail("unable to set '%s'", pattern)
+	end
+end
+
+function config_message(message, ...)
+	local pattern = string.format(...)
+
+	if not check_config(pattern) then
+		fail('%s', message)
+	end
+end
+
+function config_package(pkg, value)
+	local pattern = string.format('CONFIG_PACKAGE_%s=%s', pkg, value)
+	local ret
+	if value == 'y' then
+		res = check_config(pattern)
+	else
+		res = check_config_prefix(string.sub(pattern, 1, -2))
+	end
+
+	if not res then
+		fail("unable to enable package '%s'", pkg)
+	end
+end
+
+
+dofile('scripts/target_config.inc.lua')
+
+
+os.exit(ret)
diff --git a/scripts/target_config_check.sh b/scripts/target_config_check.sh
deleted file mode 100755
index 3ae018441..000000000
--- a/scripts/target_config_check.sh
+++ /dev/null
@@ -1,102 +0,0 @@
-#!/usr/bin/env bash
-
-set -e
-
-[ "$OPENWRT_TARGET" -a "$GLUON_TARGETSDIR" ] || exit 1
-
-target="$1"
-packages=$2
-
-output=
-
-ret=0
-
-OPENWRT_CONFIG_TARGET="${OPENWRT_TARGET//-/_}"
-
-
-fail() {
-	local message="$1"
-
-	if [ $ret -eq 0 ]; then
-		ret=1
-		echo "Configuration failed:" >&2
-	fi
-
-	echo " * $message" >&2
-}
-
-check_config() {
-	grep -q "$1" openwrt/.config
-}
-
-check_package() {
-	local package="$1"
-	local value="$2"
-
-	if ! check_config "^CONFIG_PACKAGE_${package}=${value}"; then
-		fail "unable to enable package '${package}'"
-	fi
-}
-
-
-. scripts/target_config.inc.sh
-
-config() {
-	local config="$1"
-
-	if ! check_config "^${config}\$"; then
-		fail "unable to set '${config}'"
-	fi
-}
-
-device() {
-	output="$1"
-	want_device "${output}" || return 0
-
-	local profile="$3"
-	if [ -z "$profile" ]; then
-		profile="$2"
-	fi
-
-	if ! check_config "CONFIG_TARGET_DEVICE_${OPENWRT_CONFIG_TARGET}_DEVICE_${profile}=y"; then
-		fail "unable to enable device '${profile}'"
-	fi
-
-	for package in $(site_packages "$output"); do
-		[ "${package:0:1}" = '-' ] || check_package "$package"
-	done
-}
-
-factory_image() {
-	output="$1"
-	want_device "${output}" || return 0
-}
-
-sysupgrade_image() {
-	output="$1"
-	want_device "${output}" || return 0
-}
-
-packages() {
-	if [ "${output}" ]; then
-		want_device "${output}" || return 0
-
-		for package in "$@"; do
-			[ "${package:0:1}" = '-' ] || check_package "$package"
-		done
-	else
-		for package in "$@"; do
-			[ "${package:0:1}" = '-' ] || check_package "$package" 'y'
-		done
-	fi
-}
-
-
-. "${GLUON_TARGETSDIR}/generic"
-packages $packages
-
-. "${GLUON_TARGETSDIR}/$target"
-check_devices
-
-
-exit $ret
diff --git a/targets/ar71xx-generic b/targets/ar71xx-generic
index 2175a267a..e2951f5b3 100644
--- a/targets/ar71xx-generic
+++ b/targets/ar71xx-generic
@@ -1,339 +1,435 @@
 config 'CONFIG_GLUON_SPECIALIZE_KERNEL=y'
 config 'CONFIG_TARGET_SQUASHFS_BLOCK_SIZE=64'
 
-ATH10K_PACKAGES=
-ATH10K_PACKAGES_QCA9887=
-if [ "$GLUON_WLAN_MESH" = 'ibss' ]; then
-	ATH10K_PACKAGES='-kmod-ath10k kmod-ath10k-ct -ath10k-firmware-qca988x ath10k-firmware-qca988x-ct'
-	ATH10K_PACKAGES_QCA9887='-kmod-ath10k kmod-ath10k-ct -ath10k-firmware-qca9887 ath10k-firmware-qca9887-ct'
-fi
+local ATH10K_PACKAGES = {}
+local ATH10K_PACKAGES_QCA9887 = {}
+if env.GLUON_WLAN_MESH == 'ibss' then
+	ATH10K_PACKAGES = {'-kmod-ath10k', 'kmod-ath10k-ct', '-ath10k-firmware-qca988x', 'ath10k-firmware-qca988x-ct'}
+	ATH10K_PACKAGES_QCA9887 = {'-kmod-ath10k', 'kmod-ath10k-ct', '-ath10k-firmware-qca9887', 'ath10k-firmware-qca9887-ct'}
+end
 
 
-# 8devices
+-- 8devices
 
-device 8devices-carambola2-board carambola2
-factory
+device('8devices-carambola2-board', 'carambola2', {
+	factory = false,
+})
 
 
-# ALFA NETWORK
+-- ALFA NETWORK
 
-device alfa-network-ap121f ap121f
-factory
+device('alfa-network-ap121f', 'ap121f', {
+	factory = false,
+})
 
-device alfa-network-hornet-ub hornet-ub HORNETUB
-alias alfa-network-ap121
-alias alfa-network-ap121u
+device('alfa-network-hornet-ub', 'hornet-ub', {
+	profile = 'HORNETUB',
+	aliases = { 'alfa-network-ap121', 'alfa-network-ap121u' },
+})
 
-device alfa-network-tube2h tube2h-8M TUBE2H8M
-device alfa-network-n2-n5 alfa-nx ALFANX
+device('alfa-network-tube2h', 'tube2h-8M', {
+	profile = 'TUBE2H8M',
+})
+device('alfa-network-n2-n5', 'alfa-nx', {
+	profile = 'ALFANX',
+})
 
 
-# Allnet
+-- Allnet
 
-device allnet-all0315n all0315n ALL0315N
-factory
+device('allnet-all0315n', 'all0315n', {
+	profile = 'ALL0315N',
+	factory = false,
+})
 
 
-# AVM
+-- AVM
 
-device avm-fritz-box-4020 fritz4020
-factory
+device('avm-fritz-box-4020', 'fritz4020', {
+	factory = false,
+})
 
-device avm-fritz-wlan-repeater-300e fritz300e
-factory
+device('avm-fritz-wlan-repeater-300e', 'fritz300e', {
+	factory = false,
+})
 
-device avm-fritz-wlan-repeater-450e fritz450e
-factory
+device('avm-fritz-wlan-repeater-450e', 'fritz450e', {
+	factory = false,
+})
 
 
-# Buffalo
+-- Buffalo
 
-device buffalo-wzr-hp-g300nh wzr-hp-g300nh WZRHPG300NH
-device buffalo-wzr-hp-g300nh2 wzr-hp-g300nh2 WZRHPG300NH2
-device buffalo-wzr-hp-g450h wzr-hp-g450h WZRHPG450H
+device('buffalo-wzr-hp-g300nh', 'wzr-hp-g300nh', {
+	profile = 'WZRHPG300NH',
+})
+device('buffalo-wzr-hp-g300nh2', 'wzr-hp-g300nh2', {
+	profile = 'WZRHPG300NH2',
+})
+device('buffalo-wzr-hp-g450h', 'wzr-hp-g450h', {
+	profile = 'WZRHPG450H',
+})
 
 
-device buffalo-wzr-hp-ag300h wzr-hp-ag300h WZRHPAG300H
-sysupgrade
-device buffalo-wzr-600dhp wzr-600dhp WZR600DHP
-sysupgrade
-sysupgrade_image buffalo-wzr-hp-ag300h-wzr-600dhp wzr-hp-ag300h-squashfs-sysupgrade .bin
+device('buffalo-wzr-hp-ag300h', 'wzr-hp-ag300h', {
+	profile = 'WZRHPAG300H',
+	sysupgrade = false,
+})
+device('buffalo-wzr-600dhp', 'wzr-600dhp', {
+	profile = 'WZR600DHP',
+	sysupgrade = false,
+})
+sysupgrade_image('buffalo-wzr-hp-ag300h-wzr-600dhp', 'wzr-hp-ag300h-squashfs-sysupgrade', '.bin')
 
 
-# D-Link
+-- D-Link
 
-device d-link-dap-1330-rev-a1 dap-1330-a1
-factory .img
+device('d-link-dap-1330-rev-a1', 'dap-1330-a1', {
+	factory_ext = '.img',
+})
 
-device d-link-dir-505-rev-a1 dir-505-a1 DIR505A1
-alias d-link-dir-505-rev-a2
+device('d-link-dir-505-rev-a1', 'dir-505-a1', {
+	profile = 'DIR505A1',
+	aliases = {'d-link-dir-505-rev-a2'},
+})
 
-device d-link-dir-825-rev-b1 dir-825-b1 DIR825B1
-factory
+device('d-link-dir-825-rev-b1', 'dir-825-b1', {
+	profile = 'DIR825B1',
+	factory = false,
+})
 
 
-# GL Innovations
+-- GL Innovations
 
-device gl-inet-6408a-v1 gl-inet-6408A-v1
+device('gl-inet-6408a-v1', 'gl-inet-6408A-v1')
 
-device gl-inet-6416a-v1 gl-inet-6416A-v1
+device('gl-inet-6416a-v1', 'gl-inet-6416A-v1')
 
-device gl.inet-gl-ar150 gl-ar150
-manifest_alias gl-ar150
-factory
+device('gl.inet-gl-ar150', 'gl-ar150', {
+	factory = false,
+	manifest_aliases = {'gl-ar150'},
+})
 
-device gl.inet-gl-ar300m gl-ar300m
-manifest_alias gl-ar300m
-factory
+device('gl.inet-gl-ar300m', 'gl-ar300m', {
+	factory = false,
+	manifest_aliases = {'gl-ar300m'},
+})
 
-device gl.inet-gl-ar750 gl-ar750
-manifest_alias gl-ar750
-packages $ATH10K_PACKAGES
-factory
+device('gl.inet-gl-ar750', 'gl-ar750', {
+	factory = false,
+	manifest_aliases = {'gl-ar750'},
+	packages = ATH10K_PACKAGES,
+})
 
-# Linksys by Cisco
 
-device linksys-wrt160nl wrt160nl WRT160NL
+-- Linksys by Cisco
 
+device('linksys-wrt160nl', 'wrt160nl', {
+	profile = 'WRT160NL',
+})
 
-# Meraki
 
-if [ "$BROKEN" ]; then
-device meraki-mr12 mr12 # BROKEN: MAC address uniqueness issues
-alias meraki-mr62
-factory
+-- Meraki
+device('meraki-mr12', 'mr12', {
+	factory = false,
+	aliases = {'meraki-mr62'},
+	broken = true, -- MAC address uniqueness issues
+})
 
-device meraki-mr16 mr16 # BROKEN: MAC address uniqueness issues
-alias meraki-mr66
-factory
-fi
+device('meraki-mr16', 'mr16', {
+	factory = false,
+	aliases = {'meraki-mr66'},
+	broken = true, -- MAC address uniqueness issues
+})
 
 
-# Netgear
+-- Netgear
 
-device netgear-wndr3700 wndr3700
-factory .img
+device('netgear-wndr3700', 'wndr3700', {
+	factory_ext = '.img',
+})
 
-device netgear-wndr3700v2 wndr3700v2
-factory .img
+device('netgear-wndr3700v2', 'wndr3700v2', {
+	factory_ext = '.img',
+})
 
-device netgear-wndr3800 wndr3800
-factory .img
+device('netgear-wndr3800', 'wndr3800', {
+	factory_ext = '.img',
+})
 
-device netgear-wndrmacv2 wndrmacv2
-factory .img
+device('netgear-wndrmacv2', 'wndrmacv2', {
+	factory_ext = '.img',
+})
 
-if [ "$BROKEN" ]; then
-device netgear-wndrmac wndrmac # BROKEN: Untested
-factory .img
+device('netgear-wndrmac', 'wndrmac', {
+	factory_ext = '.img',
+	broken = true, -- untested
+})
 
-device netgear-wnr2200 wnr2200 WNR2200 # BROKEN: Untested
-factory .img
-fi
+-- BROKEN: Untested
+device('netgear-wnr2200', 'wnr2200', {
+	profile = 'WNR2200',
+	factory_ext = '.img',
+	broken = true, -- untested
+})
 
 
-# OCEDO
+-- OCEDO
 
-device ocedo-koala koala
-factory
+device('ocedo-koala', 'koala', {
+	factory = false,
+})
 
 
-# Onion
+-- Onion
 
-if [ "$BROKEN" ]; then
-device onion-omega onion-omega # BROKEN: no ethernet
-fi
+device('onion-omega', 'onion-omega', {
+	broken = true, -- no Ethernet
+})
 
+-- OpenMesh
 
-# OpenMesh
+device('openmesh-a60', 'a60', {
+	profile = 'A60',
+	aliases = {'openmesh-a40'},
+	packages = ATH10K_PACKAGES,
+})
 
-device openmesh-a60 a60 A60
-alias openmesh-a40
-packages $ATH10K_PACKAGES
+device('openmesh-mr1750', 'mr1750', {
+	profile = 'MR1750',
+	aliases = {'openmesh-mr1750v2'},
+	packages = ATH10K_PACKAGES,
+})
 
-device openmesh-mr1750 mr1750 MR1750
-alias openmesh-mr1750v2
-packages $ATH10K_PACKAGES
+device('openmesh-mr600', 'mr600', {
+	profile = 'MR600',
+	aliases = {'openmesh-mr600v2'},
+})
 
-device openmesh-mr600 mr600 MR600
-alias openmesh-mr600v2
+device('openmesh-mr900', 'mr900', {
+	profile = 'MR900',
+	aliases = {'openmesh-mr900v2'},
+})
 
-device openmesh-mr900 mr900 MR900
-alias openmesh-mr900v2
+device('openmesh-om2p', 'om2p', {
+	profile = 'OM2P',
+	aliases = {
+		'openmesh-om2pv2',
+		'openmesh-om2pv4',
+		'openmesh-om2p-hs',
+		'openmesh-om2p-hsv2',
+		'openmesh-om2p-hsv3',
+		'openmesh-om2p-hsv4',
+		'openmesh-om2p-lc',
+	},
+})
 
-device openmesh-om2p om2p OM2P
-alias openmesh-om2pv2
-alias openmesh-om2pv4
-alias openmesh-om2p-hs
-alias openmesh-om2p-hsv2
-alias openmesh-om2p-hsv3
-alias openmesh-om2p-hsv4
-alias openmesh-om2p-lc
+device('openmesh-om5p', 'om5p', {
+	profile = 'OM5P',
+	aliases = {'openmesh-om5p-an'},
+})
 
-device openmesh-om5p om5p OM5P
-alias openmesh-om5p-an
+device('openmesh-om5p-ac', 'om5pac', {
+	profile = 'OM5PAC',
+	aliases = {'openmesh-om5p-acv2'},
+	packages = ATH10K_PACKAGES,
+})
 
-device openmesh-om5p-ac om5pac OM5PAC
-alias openmesh-om5p-acv2
-packages $ATH10K_PACKAGES
 
+-- TP-Link
 
-# TP-Link
+local tplink_region_suffix = ''
+if (env.GLUON_REGION or '') ~= '' then
+	tplink_region_suffix = '-' .. env.GLUON_REGION
+end
 
-device tp-link-cpe210-v1.0 cpe210-220-v1
-alias tp-link-cpe210-v1.1
-alias tp-link-cpe220-v1.1
+device('tp-link-cpe210-v1.0', 'cpe210-220-v1', {
+	aliases = {'tp-link-cpe210-v1.1', 'tp-link-cpe220-v1.1'},
+})
 
-device tp-link-cpe210-v2.0 cpe210-v2
+device('tp-link-cpe210-v2.0', 'cpe210-v2')
 
-device tp-link-cpe510-v1.0 cpe510-520-v1
-alias tp-link-cpe510-v1.1
-alias tp-link-cpe520-v1.1
+device('tp-link-cpe510-v1.0', 'cpe510-520-v1', {
+	aliases = {'tp-link-cpe510-v1.1', 'tp-link-cpe520-v1.1'},
+})
 
-device tp-link-wbs210-v1.20 wbs210-v1
-device tp-link-wbs510-v1.20 wbs510-v1
-
-device tp-link-tl-wr710n-v1 tl-wr710n-v1
-device tp-link-tl-wr710n-v2.1 tl-wr710n-v2.1
-
-device tp-link-tl-wr810n-v1 tl-wr810n-v1
-
-device tp-link-tl-wr842n-nd-v1 tl-wr842n-v1
-device tp-link-tl-wr842n-nd-v2 tl-wr842n-v2
-device tp-link-tl-wr842n-nd-v3 tl-wr842n-v3
-
-device tp-link-tl-wr1043n-nd-v1 tl-wr1043nd-v1
-device tp-link-tl-wr1043n-nd-v2 tl-wr1043nd-v2
-device tp-link-tl-wr1043n-nd-v3 tl-wr1043nd-v3
-device tp-link-tl-wr1043n-nd-v4 tl-wr1043nd-v4
-device tp-link-tl-wr1043n-v5 tl-wr1043n-v5
-
-device tp-link-tl-wdr3500-v1 tl-wdr3500-v1
-device tp-link-tl-wdr3600-v1 tl-wdr3600-v1
-device tp-link-tl-wdr4300-v1 tl-wdr4300-v1
-
-device tp-link-tl-wr2543n-nd-v1 tl-wr2543-v1
-
-device tp-link-archer-c5-v1 archer-c5-v1
-packages $ATH10K_PACKAGES
-
-device tp-link-archer-c7-v2 archer-c7-v2
-packages $ATH10K_PACKAGES
-factory -squashfs-factory${GLUON_REGION:+-${GLUON_REGION}} .bin
-
-device tp-link-archer-c7-v4 archer-c7-v4
-packages $ATH10K_PACKAGES
-
-device tp-link-archer-c7-v5 archer-c7-v5
-packages $ATH10K_PACKAGES
-
-if [ "$BROKEN" ]; then
-device tp-link-archer-c25-v1 archer-c25-v1 # BROKEN: OOM with 5GHz enabled in most environments
-packages $ATH10K_PACKAGES_QCA9887
-fi
-
-if [ "$BROKEN" ]; then
-device tp-link-archer-c58-v1 archer-c58-v1 # BROKEN: OOM with 5GHz enabled in most environments
-fi
-
-if [ "$BROKEN" ] || [ "$GLUON_WLAN_MESH" = '11s' ]; then
-device tp-link-archer-c59-v1 archer-c59-v1 # BROKEN: IBSS meshing not working
-fi
-
-if [ "$BROKEN" ]; then
-device tp-link-archer-c60-v1 archer-c60-v1 # BROKEN: OOM with 5GHz enabled in most environments
-fi
-
-if [ "$BROKEN" ]; then
-device tp-link-re355 re355-v1 # BROKEN: OOM with 5GHz enabled in most environments if device is 64M RAM variant
-fi
-
-if [ "$BROKEN" ]; then
-device tp-link-tl-wr902ac-v1 tl-wr902ac-v1 # BROKEN: OOM due to insufficient RAM for ath10k expected
-fi
-
-device tp-link-re450 re450-v1
-packages $ATH10K_PACKAGES
-
-
-# Ubiquiti
-
-device ubiquiti-airgateway ubnt-air-gateway
-alias ubiquiti-airgateway-lr
-
-device ubiquiti-airgateway-pro ubnt-air-gateway-pro
-
-device ubiquiti-airrouter ubnt-airrouter
-
-device ubiquiti-bullet-m ubnt-bullet-m
-alias ubiquiti-nanostation-loco-m2
-alias ubiquiti-nanostation-loco-m5
-alias ubiquiti-bullet-m2
-alias ubiquiti-bullet-m5
-alias ubiquiti-picostation-m2
-
-device ubiquiti-rocket-m ubnt-rocket-m
-alias ubiquiti-rocket-m2
-alias ubiquiti-rocket-m5
-
-device ubiquiti-nanostation-m ubnt-nano-m
-alias ubiquiti-nanostation-m2
-alias ubiquiti-nanostation-m5
-
-device ubiquiti-loco-m-xw ubnt-loco-m-xw
-alias ubiquiti-nanostation-loco-m2-xw
-alias ubiquiti-nanostation-loco-m5-xw
-if [ "$BROKEN" ]; then
-alias ubiquiti-nanobeam-m5 # BROKEN: Untested
-fi
-
-device ubiquiti-nanostation-m-xw ubnt-nano-m-xw
-alias ubiquiti-nanostation-m2-xw
-alias ubiquiti-nanostation-m5-xw
-
-device ubiquiti-rocket-m-xw ubnt-rocket-m-xw
-alias ubiquiti-rocket-m2-xw
-alias ubiquiti-rocket-m5-xw
-
-device ubiquiti-rocket-m-ti ubnt-rocket-m-ti
-alias ubiquiti-rocket-m2-ti
-alias ubiquiti-rocket-m5-ti
-
-device ubiquiti-unifi ubnt-unifi
-alias ubiquiti-unifi-ap
-alias ubiquiti-unifi-ap-lr
-
-device ubiquiti-unifi-ap-pro ubnt-uap-pro
-device ubiquiti-unifiap-outdoor ubnt-unifi-outdoor
-device ubiquiti-unifiap-outdoor+ ubnt-unifi-outdoor-plus
-
-if [ "$BROKEN" ]; then
-device ubiquiti-ls-sr71 ubnt-ls-sr71 # BROKEN: Untested
-fi
-
-device ubiquiti-unifi-ac-lite ubnt-unifiac-lite
-alias ubiquiti-unifi-ac-lr
-packages $ATH10K_PACKAGES
-factory
-
-device ubiquiti-unifi-ac-pro ubnt-unifiac-pro
-packages $ATH10K_PACKAGES
-factory
-
-device ubiquiti-unifi-ac-mesh ubnt-unifiac-mesh
-packages $ATH10K_PACKAGES
-factory
-
-device ubiquiti-unifi-ac-mesh-pro ubnt-unifiac-mesh-pro
-packages $ATH10K_PACKAGES
-factory
-
-
-# Western Digital
-
-device wd-my-net-n600 mynet-n600
-device wd-my-net-n750 mynet-n750
-
-# ZyXEL
-device zyxel-nbg6616 NBG6616
-packages $ATH10K_PACKAGES
+device('tp-link-wbs210-v1.20', 'wbs210-v1')
+device('tp-link-wbs510-v1.20', 'wbs510-v1')
+
+device('tp-link-tl-wr710n-v1', 'tl-wr710n-v1')
+device('tp-link-tl-wr710n-v2.1', 'tl-wr710n-v2.1')
+
+device('tp-link-tl-wr810n-v1', 'tl-wr810n-v1')
+
+device('tp-link-tl-wr842n-nd-v1', 'tl-wr842n-v1')
+device('tp-link-tl-wr842n-nd-v2', 'tl-wr842n-v2')
+device('tp-link-tl-wr842n-nd-v3', 'tl-wr842n-v3')
+
+device('tp-link-tl-wr1043n-nd-v1', 'tl-wr1043nd-v1')
+device('tp-link-tl-wr1043n-nd-v2', 'tl-wr1043nd-v2')
+device('tp-link-tl-wr1043n-nd-v3', 'tl-wr1043nd-v3')
+device('tp-link-tl-wr1043n-nd-v4', 'tl-wr1043nd-v4')
+device('tp-link-tl-wr1043n-v5', 'tl-wr1043n-v5')
+
+device('tp-link-tl-wdr3500-v1', 'tl-wdr3500-v1')
+device('tp-link-tl-wdr3600-v1', 'tl-wdr3600-v1')
+device('tp-link-tl-wdr4300-v1', 'tl-wdr4300-v1')
+
+device('tp-link-tl-wr2543n-nd-v1', 'tl-wr2543-v1')
+
+device('tp-link-archer-c5-v1', 'archer-c5-v1', {
+	packages = ATH10K_PACKAGES,
+})
+
+device('tp-link-archer-c7-v2', 'archer-c7-v2', {
+	packages = ATH10K_PACKAGES,
+	factory = '-squashfs-factory' .. tplink_region_suffix,
+})
+
+device('tp-link-archer-c7-v4', 'archer-c7-v4', {
+	packages = ATH10K_PACKAGES,
+})
+
+device('tp-link-archer-c7-v5', 'archer-c7-v5', {
+	packages = ATH10K_PACKAGES,
+})
+
+device('tp-link-archer-c25-v1', 'archer-c25-v1', {
+	packages = ATH10K_PACKAGES_QCA9887,
+	broken = true, -- OOM with 5GHz enabled in most environments
+})
+
+device('tp-link-archer-c58-v1', 'archer-c58-v1', {
+	broken = true, -- OOM with 5GHz enabled in most environments
+})
+
+device('tp-link-archer-c59-v1', 'archer-c59-v1', {
+	broken = (env.GLUON_WLAN_MESH ~= '11s'),
+})
+
+device('tp-link-archer-c60-v1', 'archer-c60-v1', {
+	broken = true, -- OOM with 5GHz enabled in most environments
+})
+
+device('tp-link-re355', 're355-v1', {
+	broken = true, -- OOM with 5GHz enabled in most environments if device is 64M RAM variant
+})
+
+device('tp-link-tl-wr902ac-v1', 'tl-wr902ac-v1', {
+	broken = true, -- OOM due to insufficient RAM for ath10k expected
+})
+
+device('tp-link-re450', 're450-v1', {
+	packages = ATH10K_PACKAGES,
+})
+
+
+-- Ubiquiti
+
+device('ubiquiti-airgateway', 'ubnt-air-gateway', {
+	aliases = {'ubiquiti-airgateway-lr'},
+})
+
+device('ubiquiti-airgateway-pro', 'ubnt-air-gateway-pro')
+
+device('ubiquiti-airrouter', 'ubnt-airrouter')
+
+device('ubiquiti-bullet-m', 'ubnt-bullet-m', {
+	aliases = {
+		'ubiquiti-nanostation-loco-m2',
+		'ubiquiti-nanostation-loco-m5',
+		'ubiquiti-bullet-m2',
+		'ubiquiti-bullet-m5',
+		'ubiquiti-picostation-m2',
+	},
+})
+
+device('ubiquiti-rocket-m', 'ubnt-rocket-m', {
+	aliases = {
+		'ubiquiti-rocket-m2',
+		'ubiquiti-rocket-m5',
+	},
+})
+
+device('ubiquiti-nanostation-m', 'ubnt-nano-m', {
+	aliases = {
+		'ubiquiti-nanostation-m2',
+		'ubiquiti-nanostation-m5',
+	},
+})
+
+device('ubiquiti-loco-m-xw', 'ubnt-loco-m-xw', {
+	aliases = {
+		'ubiquiti-nanostation-loco-m2-xw',
+		'ubiquiti-nanostation-loco-m5-xw',
+		-- 'ubiquiti-nanobeam-m5', -- untested
+	},
+})
+
+device('ubiquiti-nanostation-m-xw', 'ubnt-nano-m-xw', {
+	aliases = {
+		'ubiquiti-nanostation-m2-xw',
+		'ubiquiti-nanostation-m5-xw',
+	},
+})
+
+device('ubiquiti-rocket-m-xw', 'ubnt-rocket-m-xw', {
+	aliases = {
+		'ubiquiti-rocket-m2-xw',
+		'ubiquiti-rocket-m5-xw',
+	},
+})
+
+device('ubiquiti-rocket-m-ti', 'ubnt-rocket-m-ti', {
+	aliases = {
+		'ubiquiti-rocket-m2-ti',
+		'ubiquiti-rocket-m5-ti',
+	},
+})
+
+device('ubiquiti-unifi', 'ubnt-unifi', {
+	aliases = {
+		'ubiquiti-unifi-ap',
+		'ubiquiti-unifi-ap-lr',
+	},
+})
+
+device('ubiquiti-unifi-ap-pro', 'ubnt-uap-pro')
+device('ubiquiti-unifiap-outdoor', 'ubnt-unifi-outdoor')
+device('ubiquiti-unifiap-outdoor+', 'ubnt-unifi-outdoor-plus')
+
+device('ubiquiti-ls-sr71', 'ubnt-ls-sr71', {
+	broken = true, -- untested
+})
+
+device('ubiquiti-unifi-ac-lite', 'ubnt-unifiac-lite', {
+	factory = false,
+	packages = ATH10K_PACKAGES,
+	aliases = {'ubiquiti-unifi-ac-lr'},
+})
+
+device('ubiquiti-unifi-ac-pro', 'ubnt-unifiac-pro', {
+	factory = false,
+	packages = ATH10K_PACKAGES,
+})
+
+device('ubiquiti-unifi-ac-mesh', 'ubnt-unifiac-mesh', {
+	factory = false,
+	packages = ATH10K_PACKAGES,
+})
+
+device('ubiquiti-unifi-ac-mesh-pro', 'ubnt-unifiac-mesh-pro', {
+	factory = false,
+	packages = ATH10K_PACKAGES,
+})
+
+
+-- Western Digital
+
+device('wd-my-net-n600', 'mynet-n600')
+device('wd-my-net-n750', 'mynet-n750')
+
+-- ZyXEL
+device('zyxel-nbg6616', 'NBG6616', {
+	packages = ATH10K_PACKAGES,
+})
diff --git a/targets/ar71xx-mikrotik b/targets/ar71xx-mikrotik
index 388104f29..6a783b5e9 100644
--- a/targets/ar71xx-mikrotik
+++ b/targets/ar71xx-mikrotik
@@ -1,16 +1,12 @@
 config 'CONFIG_GLUON_SPECIALIZE_KERNEL=y'
 
-device mikrotik-rb-nor-flash-16M rb-nor-flash-16M
-factory
+defaults {
+	factory = false,
+}
 
-device mikrotik-rb-nor-flash-16M-ac rb-nor-flash-16M-ac
-factory
+device('mikrotik-rb-nor-flash-16M', 'rb-nor-flash-16M')
+device('mikrotik-rb-nor-flash-16M-ac', 'rb-nor-flash-16M-ac')
 
-device mikrotik-nand-64m nand-64m
-factory
-
-device mikrotik-nand-large nand-large
-factory
-
-device mikrotik-nand-large-ac nand-large-ac
-factory
+device('mikrotik-nand-64m', 'nand-64m')
+device('mikrotik-nand-large', 'nand-large')
+device('mikrotik-nand-large-ac', 'nand-large-ac')
diff --git a/targets/ar71xx-nand b/targets/ar71xx-nand
index 223a6ccc6..bc4209c6e 100644
--- a/targets/ar71xx-nand
+++ b/targets/ar71xx-nand
@@ -1,14 +1,31 @@
 config 'CONFIG_GLUON_SPECIALIZE_KERNEL=y'
 
-factory -ubi-factory .img
-sysupgrade -squashfs-sysupgrade .tar
+local ATH10K_PACKAGES = {}
+if env.GLUON_WLAN_MESH == 'ibss' then
+	ATH10K_PACKAGES = {'-kmod-ath10k', 'kmod-ath10k-ct', '-ath10k-firmware-qca988x', 'ath10k-firmware-qca988x-ct'}
+end
 
-# Netgear
 
-device netgear-wndr3700v4 wndr3700v4 WNDR3700V4
-device netgear-wndr4300 wndr4300 WNDR4300V1
+defaults {
+	sysupgrade_ext = '.tar',
+}
 
-# ZyXEL
-device zyxel-nbg6716 nbg6716 NBG6716
-packages $ATH10K_PACKAGES
-factory -squashfs-factory .bin
+
+-- Netgear
+
+device('netgear-wndr3700v4', 'wndr3700v4', {
+	profile = 'WNDR3700V4',
+	factory = '-ubi-factory',
+	factory_ext = '.img',
+})
+device('netgear-wndr4300', 'wndr4300', {
+	profile = 'WNDR4300V1',
+	factory = '-ubi-factory',
+	factory_ext = '.img',
+})
+
+-- ZyXEL
+device('zyxel-nbg6716', 'nbg6716', {
+	profile = 'NBG6716',
+	packages = ATH10K_PACKAGES,
+})
diff --git a/targets/ar71xx-tiny b/targets/ar71xx-tiny
index d43e4da68..d82d8f79f 100644
--- a/targets/ar71xx-tiny
+++ b/targets/ar71xx-tiny
@@ -1,107 +1,127 @@
 config 'CONFIG_GLUON_SPECIALIZE_KERNEL=y'
 
-no_opkg
-packages '-uboot-envtools' '-kmod-usb-core' '-kmod-usb-ohci' '-kmod-usb2' '-kmod-usb-ledtrig-usbport'
+no_opkg()
+packages {
+	'-uboot-envtools',
+	'-kmod-usb-core',
+	'-kmod-usb-ohci',
+	'-kmod-usb2',
+	'-kmod-usb-ledtrig-usbport',
+}
 
 
-# D-Link
+-- D-Link
 
-device d-link-dir-615-rev-c1 dir-615-c1 DIR615C1
+device('d-link-dir-615-rev-c1', 'dir-615-c1', {
+	profile = 'DIR615C1',
+})
 
 
-# TP-Link
+-- TP-Link
 
-device tp-link-tl-wa701n-nd-v1 tl-wa701nd-v1
-device tp-link-tl-wa701n-nd-v2 tl-wa701nd-v2
+local tplink_region_suffix = ''
+if (env.GLUON_REGION or '') ~= '' then
+	tplink_region_suffix = '-' .. env.GLUON_REGION
+end
 
-device tp-link-tl-wa7210n-v2 tl-wa7210n-v2
-device tp-link-tl-wa7510n-v1 tl-wa7510n-v1
+device('tp-link-tl-wa701n-nd-v1', 'tl-wa701nd-v1')
+device('tp-link-tl-wa701n-nd-v2', 'tl-wa701nd-v2')
 
-device tp-link-tl-wr703n-v1 tl-wr703n-v1
+device('tp-link-tl-wa7210n-v2', 'tl-wa7210n-v2')
+device('tp-link-tl-wa7510n-v1', 'tl-wa7510n-v1')
 
-device tp-link-tl-wr710n-v2 tl-wr710n-v2
+device('tp-link-tl-wr703n-v1', 'tl-wr703n-v1')
 
-device tp-link-tl-wr740n-nd-v1 tl-wr740n-v1
-device tp-link-tl-wr740n-nd-v3 tl-wr740n-v3
-device tp-link-tl-wr740n-nd-v4 tl-wr740n-v4
-device tp-link-tl-wr740n-nd-v5 tl-wr740n-v5
+device('tp-link-tl-wr710n-v2', 'tl-wr710n-v2')
 
-device tp-link-tl-wr741n-nd-v1 tl-wr741nd-v1
-device tp-link-tl-wr741n-nd-v2 tl-wr741nd-v2
-device tp-link-tl-wr741n-nd-v4 tl-wr741nd-v4
-device tp-link-tl-wr741n-nd-v5 tl-wr741nd-v5
+device('tp-link-tl-wr740n-nd-v1', 'tl-wr740n-v1')
+device('tp-link-tl-wr740n-nd-v3', 'tl-wr740n-v3')
+device('tp-link-tl-wr740n-nd-v4', 'tl-wr740n-v4')
+device('tp-link-tl-wr740n-nd-v5', 'tl-wr740n-v5')
 
-device tp-link-tl-wr743n-nd-v1 tl-wr743nd-v1
-device tp-link-tl-wr743n-nd-v2 tl-wr743nd-v2
+device('tp-link-tl-wr741n-nd-v1', 'tl-wr741nd-v1')
+device('tp-link-tl-wr741n-nd-v2', 'tl-wr741nd-v2')
+device('tp-link-tl-wr741n-nd-v4', 'tl-wr741nd-v4')
+device('tp-link-tl-wr741n-nd-v5', 'tl-wr741nd-v5')
 
-device tp-link-tl-wa801n-nd-v1 tl-wa801nd-v1
-device tp-link-tl-wa801n-nd-v2 tl-wa801nd-v2
-device tp-link-tl-wa801n-nd-v3 tl-wa801nd-v3
+device('tp-link-tl-wr743n-nd-v1', 'tl-wr743nd-v1')
+device('tp-link-tl-wr743n-nd-v2', 'tl-wr743nd-v2')
 
-if [ "$BROKEN" ]; then
-device tp-link-tl-wr802n-v1 tl-wr802n-v1 # BROKEN: Untested
-fi
+device('tp-link-tl-wa801n-nd-v1', 'tl-wa801nd-v1')
+device('tp-link-tl-wa801n-nd-v2', 'tl-wa801nd-v2')
+device('tp-link-tl-wa801n-nd-v3', 'tl-wa801nd-v3')
 
-device tp-link-tl-wr840n-v2 tl-wr840n-v2
+device('tp-link-tl-wr802n-v1', 'tl-wr802n-v1', {
+	broken = true, -- untested
+})
 
-device tp-link-tl-wr841n-nd-v3 tl-wr841-v3
-device tp-link-tl-wr841n-nd-v5 tl-wr841-v5
-device tp-link-tl-wr841n-nd-v7 tl-wr841-v7
-device tp-link-tl-wr841n-nd-v8 tl-wr841-v8
-device tp-link-tl-wr841n-nd-v9 tl-wr841-v9
-device tp-link-tl-wr841n-nd-v10 tl-wr841-v10
-device tp-link-tl-wr841n-nd-v11 tl-wr841-v11
-factory -squashfs-factory${GLUON_REGION:+-${GLUON_REGION}} .bin
-device tp-link-tl-wr841n-nd-v12 tl-wr841-v12
-factory -squashfs-factory${GLUON_REGION:+-${GLUON_REGION}} .bin
+device('tp-link-tl-wr840n-v2', 'tl-wr840n-v2')
 
-device tp-link-tl-wr843n-nd-v1 tl-wr843nd-v1
+device('tp-link-tl-wr841n-nd-v3', 'tl-wr841-v3')
+device('tp-link-tl-wr841n-nd-v5', 'tl-wr841-v5')
+device('tp-link-tl-wr841n-nd-v7', 'tl-wr841-v7')
+device('tp-link-tl-wr841n-nd-v8', 'tl-wr841-v8')
+device('tp-link-tl-wr841n-nd-v9', 'tl-wr841-v9')
+device('tp-link-tl-wr841n-nd-v10', 'tl-wr841-v10')
+device('tp-link-tl-wr841n-nd-v11', 'tl-wr841-v11', {
+	factory = '-squashfs-factory' .. tplink_region_suffix,
+})
+device('tp-link-tl-wr841n-nd-v12', 'tl-wr841-v12', {
+	factory = '-squashfs-factory' .. tplink_region_suffix,
+})
 
-device tp-link-tl-wr941n-nd-v2 tl-wr941nd-v2
-device tp-link-tl-wr941n-nd-v3 tl-wr941nd-v3
+device('tp-link-tl-wr843n-nd-v1', 'tl-wr843nd-v1')
 
-device tp-link-tl-wr941n-nd-v4 tl-wr941nd-v4
-alias tp-link-tl-wr940n-v1
+device('tp-link-tl-wr941n-nd-v2', 'tl-wr941nd-v2')
+device('tp-link-tl-wr941n-nd-v3', 'tl-wr941nd-v3')
 
-device tp-link-tl-wr941n-nd-v5 tl-wr941nd-v5
-alias tp-link-tl-wr940n-v2
+device('tp-link-tl-wr941n-nd-v4', 'tl-wr941nd-v4', {
+	aliases = {'tp-link-tl-wr940n-v1'},
+})
 
-device tp-link-tl-wr941n-nd-v6 tl-wr941nd-v6
-alias tp-link-tl-wr940n-v3
+device('tp-link-tl-wr941n-nd-v5', 'tl-wr941nd-v5', {
+	aliases = {'tp-link-tl-wr940n-v2'},
+})
 
-device tp-link-tl-wr940n-v4 tl-wr940n-v4
-alias tp-link-tl-wr940n-v5
-factory -squashfs-factory${GLUON_REGION:+-${GLUON_REGION}} .bin
+device('tp-link-tl-wr941n-nd-v6', 'tl-wr941nd-v6', {
+	aliases = {'tp-link-tl-wr940n-v3'},
+})
 
-device tp-link-tl-wr940n-v6 tl-wr940n-v6
-factory -squashfs-factory${GLUON_REGION:+-${GLUON_REGION}} .bin
+device('tp-link-tl-wr940n-v4', 'tl-wr940n-v4', {
+	factory = '-squashfs-factory' .. tplink_region_suffix,
+	aliases = {'tp-link-tl-wr940n-v5'},
+})
 
-device tp-link-tl-wa730re-v1 tl-wa730re-v1
+device('tp-link-tl-wr940n-v6', 'tl-wr940n-v6', {
+	factory = '-squashfs-factory' .. tplink_region_suffix,
+})
 
-device tp-link-tl-wa750re-v1 tl-wa750re-v1
+device('tp-link-tl-wa730re-v1', 'tl-wa730re-v1')
 
-device tp-link-tl-wa830re-v1 tl-wa830re-v1
-device tp-link-tl-wa830re-v2 tl-wa830re-v2
+device('tp-link-tl-wa750re-v1', 'tl-wa750re-v1')
 
-device tp-link-tl-wa850re-v1 tl-wa850re-v1
+device('tp-link-tl-wa830re-v1', 'tl-wa830re-v1')
+device('tp-link-tl-wa830re-v2', 'tl-wa830re-v2')
 
-device tp-link-tl-wa860re-v1 tl-wa860re-v1
+device('tp-link-tl-wa850re-v1', 'tl-wa850re-v1')
 
-device tp-link-tl-wa901n-nd-v1 tl-wa901nd-v1
-device tp-link-tl-wa901n-nd-v2 tl-wa901nd-v2
-device tp-link-tl-wa901n-nd-v3 tl-wa901nd-v3
-device tp-link-tl-wa901n-nd-v4 tl-wa901nd-v4
-device tp-link-tl-wa901n-nd-v5 tl-wa901nd-v5
+device('tp-link-tl-wa860re-v1', 'tl-wa860re-v1')
 
-device tp-link-tl-mr13u-v1 tl-mr13u-v1
+device('tp-link-tl-wa901n-nd-v1', 'tl-wa901nd-v1')
+device('tp-link-tl-wa901n-nd-v2', 'tl-wa901nd-v2')
+device('tp-link-tl-wa901n-nd-v3', 'tl-wa901nd-v3')
+device('tp-link-tl-wa901n-nd-v4', 'tl-wa901nd-v4')
+device('tp-link-tl-wa901n-nd-v5', 'tl-wa901nd-v5')
 
-device tp-link-tl-mr3020-v1 tl-mr3020-v1
+device('tp-link-tl-mr13u-v1', 'tl-mr13u-v1')
 
-device tp-link-tl-mr3040-v1 tl-mr3040-v1
-device tp-link-tl-mr3040-v2 tl-mr3040-v2
+device('tp-link-tl-mr3020-v1', 'tl-mr3020-v1')
 
-device tp-link-tl-mr3220-v1 tl-mr3220-v1
-device tp-link-tl-mr3220-v2 tl-mr3220-v2
+device('tp-link-tl-mr3040-v1', 'tl-mr3040-v1')
+device('tp-link-tl-mr3040-v2', 'tl-mr3040-v2')
 
-device tp-link-tl-mr3420-v1 tl-mr3420-v1
-device tp-link-tl-mr3420-v2 tl-mr3420-v2
+device('tp-link-tl-mr3220-v1', 'tl-mr3220-v1')
+device('tp-link-tl-mr3220-v2', 'tl-mr3220-v2')
+
+device('tp-link-tl-mr3420-v1', 'tl-mr3420-v1')
+device('tp-link-tl-mr3420-v2', 'tl-mr3420-v2')
diff --git a/targets/brcm2708-bcm2708 b/targets/brcm2708-bcm2708
index 719a04cf5..cbdba2345 100644
--- a/targets/brcm2708-bcm2708
+++ b/targets/brcm2708-bcm2708
@@ -1,5 +1,8 @@
-. "${GLUON_TARGETSDIR}/brcm2708.inc"
+dofile(env.GLUON_TARGETSDIR .. '/brcm2708.inc')
 
-device raspberry-pi rpi
-manifest_alias raspberry-pi-model-b-rev-2
-manifest_alias raspberry-pi-model-b-plus-rev-1.2
+device('raspberry-pi', 'rpi', {
+	manifest_aliases = {
+		'raspberry-pi-model-b-rev-2',
+		'raspberry-pi-model-b-plus-rev-1.2',
+	},
+})
diff --git a/targets/brcm2708-bcm2709 b/targets/brcm2708-bcm2709
index bc6321262..d812fdb6d 100644
--- a/targets/brcm2708-bcm2709
+++ b/targets/brcm2708-bcm2709
@@ -1,4 +1,7 @@
-. "${GLUON_TARGETSDIR}/brcm2708.inc"
+dofile(env.GLUON_TARGETSDIR .. '/brcm2708.inc')
 
-device raspberry-pi-2 rpi-2
-manifest_alias raspberry-pi-2-model-b-rev-1.1
+device('raspberry-pi-2', 'rpi-2', {
+	manifest_aliases = {
+		'raspberry-pi-2-model-b-rev-1.1',
+	},
+})
diff --git a/targets/brcm2708-bcm2710 b/targets/brcm2708-bcm2710
index c8f031af6..eb8b3abba 100644
--- a/targets/brcm2708-bcm2710
+++ b/targets/brcm2708-bcm2710
@@ -1,3 +1,3 @@
-. "${GLUON_TARGETSDIR}/brcm2708.inc"
+dofile(env.GLUON_TARGETSDIR .. '/brcm2708.inc')
 
-device raspberry-pi-3 rpi-3
+device('raspberry-pi-3', 'rpi-3')
diff --git a/targets/brcm2708.inc b/targets/brcm2708.inc
index fed621d3f..e15988b4d 100644
--- a/targets/brcm2708.inc
+++ b/targets/brcm2708.inc
@@ -1,2 +1,6 @@
-factory -ext4-factory .img.gz
-sysupgrade -ext4-sysupgrade .img.gz
+defaults {
+	factory = '-ext4-factory',
+	factory_ext = '.img.gz',
+	sysupgrade = '-ext4-sysupgrade',
+	sysupgrade_ext = '.img.gz',
+}
diff --git a/targets/generic b/targets/generic
index 517603a6b..72cc68b66 100644
--- a/targets/generic
+++ b/targets/generic
@@ -1,33 +1,40 @@
-[ "$GLUON_SITEDIR" -a "$GLUON_RELEASE" -a "$BOARD" ] || exit 1
+assert(env.GLUON_LANGS)
 
 
-. scripts/modules.sh
-. scripts/default_feeds.sh
+config('CONFIG_GLUON_SITEDIR="%s"', env.GLUON_SITEDIR)
+config('CONFIG_GLUON_RELEASE="%s"', env.GLUON_RELEASE)
+try_config('CONFIG_GLUON_BRANCH="%s"', env.GLUON_BRANCH or '')
 
+for lang in string.gmatch(env.GLUON_LANGS, '%S+') do
+	try_config('CONFIG_GLUON_WEB_LANG_%s=y', lang)
+end
 
-config "CONFIG_GLUON_SITEDIR=\"$GLUON_SITEDIR\""
-config "CONFIG_GLUON_RELEASE=\"$GLUON_RELEASE\""
-try_config "CONFIG_GLUON_BRANCH=\"$GLUON_BRANCH\""
+config('CONFIG_TARGET_%s=y', env.BOARD)
+if env.SUBTARGET ~= '' then
+	config('CONFIG_TARGET_%s_%s=y', env.BOARD, env.SUBTARGET)
+end
 
-for lang in $GLUON_LANGS; do
-	try_config "CONFIG_GLUON_WEB_LANG_${lang}=y"
-done
+-- Disable non-default feeds in distfeeds.conf
+config('# CONFIG_FEED_gluon_base is not set')
 
-[ "$BOARD" ] && config "CONFIG_TARGET_${BOARD}=y"
-[ -z "$SUBTARGET" ] || config "CONFIG_TARGET_${BOARD}_${SUBTARGET}=y"
+local default_feeds = {}
+for feed in string.gmatch(exec_capture_raw('. scripts/default_feeds.sh && echo "$DEFAULT_FEEDS"'), '%S+') do
+	default_feeds[feed] = true
+end
 
-# Disable non-default feeds in distfeeds.conf
-for feed in gluon_base $(echo "$FEEDS" | grep -vxF "$DEFAULT_FEEDS"); do
-	config "# CONFIG_FEED_${feed} is not set"
-done
+for feed in string.gmatch(exec_capture_raw('. scripts/modules.sh && echo -n "$FEEDS"'), '%S+') do
+	if not default_feeds[feed] then
+		config('# CONFIG_FEED_%s is not set', feed)
+	end
+end
 
 
 config '# CONFIG_TARGET_ROOTFS_INITRAMFS is not set'
 
 config 'CONFIG_ALL_NONSHARED=y'
 
-config '# CONFIG_PACKAGE_usbip is not set' # fails to build
-config '# CONFIG_PACKAGE_kmod-jool is not set' # fails to build
+config '# CONFIG_PACKAGE_usbip is not set' -- fails to build
+config '# CONFIG_PACKAGE_kmod-jool is not set' -- fails to build
 
 config 'CONFIG_BUSYBOX_CUSTOM=y'
 config '# CONFIG_BUSYBOX_CONFIG_FEATURE_PREFER_IPV4_ADDRESS is not set'
@@ -40,17 +47,23 @@ config '# CONFIG_KERNEL_IPV6_MROUTE is not set'
 try_config 'CONFIG_TARGET_MULTI_PROFILE=y'
 try_config 'CONFIG_TARGET_PER_DEVICE_ROOTFS=y'
 
-if [ "$GLUON_MULTIDOMAIN" = 1 ]; then
+if envtrue.GLUON_MULTIDOMAIN then
 	config 'CONFIG_GLUON_MULTIDOMAIN=y'
-fi
+end
 
-if [ "$GLUON_DEBUG" = 1 ]; then
+if envtrue.GLUON_DEBUG then
 	config 'CONFIG_DEBUG=y'
 	config 'CONFIG_NO_STRIP=y'
 	config '# CONFIG_USE_STRIP is not set'
 	config '# CONFIG_USE_SSTRIP is not set'
-fi
+end
 
 
-packages '-odhcpd-ipv6only' '-ppp' '-ppp-mod-pppoe' '-wpad-mini'
-packages 'gluon-core' 'ip6tables'
+packages {
+	'-odhcpd-ipv6only',
+	'-ppp',
+	'-ppp-mod-pppoe',
+	'-wpad-mini',
+	'gluon-core',
+	'ip6tables',
+}
diff --git a/targets/ipq40xx b/targets/ipq40xx
index 2c2a83e37..f3c3c33bb 100644
--- a/targets/ipq40xx
+++ b/targets/ipq40xx
@@ -1,58 +1,79 @@
-ATH10K_PACKAGES_IPQ40XX=
-ATH10K_PACKAGES_QCA9888='ath10k-firmware-qca9888'
-if [ "$GLUON_WLAN_MESH" = 'ibss' ]; then
-	ATH10K_PACKAGES_IPQ40XX='-kmod-ath10k kmod-ath10k-ct -ath10k-firmware-qca4019 ath10k-firmware-qca4019-ct'
-	ATH10K_PACKAGES_QCA9888='-ath10k-firmware-qca9888 ath10k-firmware-qca9888-ct'
-fi
+local ATH10K_PACKAGES_IPQ40XX = {}
+local ATH10K_PACKAGES_IPQ40XX_QCA9888 = {'ath10k-firmware-qca9888'}
+if env.GLUON_WLAN_MESH == 'ibss' then
+	ATH10K_PACKAGES_IPQ40XX = {
+		'-kmod-ath10k',
+		'kmod-ath10k-ct',
+		'-ath10k-firmware-qca4019',
+		'ath10k-firmware-qca4019-ct',
+	}
+	ATH10K_PACKAGES_IPQ40XX_QCA9888 = {
+		'-kmod-ath10k',
+		'kmod-ath10k-ct',
+		'-ath10k-firmware-qca4019',
+		'ath10k-firmware-qca4019-ct',
+		'-ath10k-firmware-qca9888',
+		'ath10k-firmware-qca9888-ct',
+	}
+end
 
 
-# AVM
+defaults {
+	packages = ATH10K_PACKAGES_IPQ40XX,
+}
 
-device avm-fritz-box-4040 avm_fritzbox-4040
-factory
-extra_image -squashfs-eva -bootloader .bin
-packages $ATH10K_PACKAGES_IPQ40XX
 
+-- AVM
 
-# GL.iNet
+device('avm-fritz-box-4040', 'avm_fritzbox-4040', {
+	factory = false,
+	extra_images = {
+		{'-squashfs-eva', '-bootloader', '.bin'},
+	},
+})
 
-device gl.inet-gl-b1300 glinet_gl-b1300
-factory
-packages $ATH10K_PACKAGES_IPQ40XX
 
+-- GL.iNet
 
-# NETGEAR
+device('gl.inet-gl-b1300', 'glinet_gl-b1300', {
+	factory = false,
+})
 
-device netgear-ex6100v2 netgear_ex6100v2
-factory .img
-packages $ATH10K_PACKAGES_IPQ40XX
 
-device netgear-ex6150v2 netgear_ex6150v2
-factory .img
-packages $ATH10K_PACKAGES_IPQ40XX
+-- NETGEAR
 
+device('netgear-ex6100v2', 'netgear_ex6100v2', {
+	factory_ext = '.img',
+})
 
-# OpenMesh
+device('netgear-ex6150v2', 'netgear_ex6150v2', {
+	factory_ext = '.img',
+})
 
-device openmesh-a42 openmesh_a42
-packages $ATH10K_PACKAGES_IPQ40XX
 
-device openmesh-a62 openmesh_a62
-packages $ATH10K_PACKAGES_IPQ40XX $ATH10K_PACKAGES_QCA9888
+-- OpenMesh
 
+device('openmesh-a42', 'openmesh_a42')
 
-# ZyXEL
+device('openmesh-a62', 'openmesh_a62', {
+	packages = ATH10K_PACKAGES_IPQ40XX_QCA9888,
+})
 
-device zyxel-nbg6617 zyxel_nbg6617
-packages $ATH10K_PACKAGES_IPQ40XX
 
-device zyxel-wre6606 zyxel_wre6606
-factory
-packages $ATH10K_PACKAGES_IPQ40XX
+-- ZyXEL
 
-# 8devices
+device('zyxel-nbg6617', 'zyxel_nbg6617')
 
-device 8devices-jalapeno 8dev_jalapeno
-factory -squashfs-nand-factory .ubi
-sysupgrade -squashfs-nand-sysupgrade .bin
-packages $ATH10K_PACKAGES_IPQ40XX
+device('zyxel-wre6606', 'zyxel_wre6606', {
+	factory = false,
+})
+
+
+-- 8devices
+
+device('8devices-jalapeno', '8dev_jalapeno', {
+	factory = '-squashfs-nand-factory',
+	factory_ext = '.ubi',
+	sysupgrade = '-squashfs-nand-sysupgrade',
+	sysupgrade_ext = '.bin',
+})
diff --git a/targets/ipq806x b/targets/ipq806x
index cce037d54..c9f8915ed 100644
--- a/targets/ipq806x
+++ b/targets/ipq806x
@@ -1,31 +1,33 @@
-# The QCA9980 was discontinued by Qualcomm. It didn't receive any firmware-update for over 4 years.
-# See https://github.com/kvalo/ath10k-firmware/tree/master/QCA99X0/hw2.0
-# 802.11s was never implemented for the chip's firmware. It will most likely be broken forever.
-# The QCA9984 on the other hand works fine for 11s meshes on both bands.
+-- The QCA9980 was discontinued by Qualcomm. It didn't receive any firmware-update for over 4 years.
+-- See https://github.com/kvalo/ath10k-firmware/tree/master/QCA99X0/hw2.0
+-- 802.11s was never implemented for the chip's firmware. It will most likely be broken forever.
+-- The QCA9984 on the other hand works fine for 11s meshes on both bands.
 
-QCA9980_PACKAGES='-kmod-ath10k kmod-ath10k-ct -ath10k-firmware-qca99x0 ath10k-firmware-qca99x0-ct'
-QCA9984_PACKAGES='kmod-ath10k -kmod-ath10k-ct ath10k-firmware-qca9984 -ath10k-firmware-qca9984-ct'
+local QCA9980_PACKAGES = {'-kmod-ath10k', 'kmod-ath10k-ct', '-ath10k-firmware-qca99x0', 'ath10k-firmware-qca99x0-ct'}
+local QCA9984_PACKAGES = {'kmod-ath10k', '-kmod-ath10k-ct', 'ath10k-firmware-qca9984', '-ath10k-firmware-qca9984-ct'}
 
 
-#
-# QCA9980
-#
+--
+-- QCA9980
+--
 
-if [ "$BROKEN" ]; then
-# TP-Link
-device tp-link-archer-c2600 tplink_c2600
-packages $QCA9980_PACKAGES
-fi
+-- TP-Link
+device('tp-link-archer-c2600', 'tplink_c2600', {
+	packages = QCA9980_PACKAGES,
+	broken = true,
+})
 
-#
-# QCA9984
-#
+--
+-- QCA9984
+--
 
-# -- ToDo --
-# The BROKEN flag for the target can be removed when switching to an OpenWrt base newer than 18.06,
-# as in 18.06 the LEDs are not yet functional. This is already fixed in the current OpenWrt master.
+-- -- ToDo --
+-- The BROKEN flag for the target can be removed when switching to an OpenWrt base newer than 18.06,
+-- as in 18.06 the LEDs are not yet functional. This is already fixed in the current OpenWrt master.
 
-# NETGEAR
-device netgear-nighthawk-x4s-r7800 netgear_r7800
-factory .img
-packages $QCA9984_PACKAGES
+-- NETGEAR
+device('netgear-nighthawk-x4s-r7800', 'netgear_r7800', {
+	factory_ext = '.img',
+	packages = QCA9984_PACKAGES,
+	broken = true,
+})
diff --git a/targets/mpc85xx-generic b/targets/mpc85xx-generic
index b796dc7cf..a806587b4 100644
--- a/targets/mpc85xx-generic
+++ b/targets/mpc85xx-generic
@@ -1 +1 @@
-device tp-link-tl-wdr4900-v1 tl-wdr4900-v1
+device('tp-link-tl-wdr4900-v1', 'tl-wdr4900-v1')
diff --git a/targets/mvebu-cortexa9 b/targets/mvebu-cortexa9
index ee4bbbeee..bcd149078 100644
--- a/targets/mvebu-cortexa9
+++ b/targets/mvebu-cortexa9
@@ -1,2 +1,3 @@
-device linksys-wrt1200ac linksys-wrt1200ac
-factory .img
+device('linksys-wrt1200ac', 'linksys-wrt1200ac', {
+	factory_ext = '.img',
+})
diff --git a/targets/ramips-mt7620 b/targets/ramips-mt7620
index d512d3eb8..8aa005f14 100644
--- a/targets/ramips-mt7620
+++ b/targets/ramips-mt7620
@@ -1,30 +1,40 @@
-# ASUS
-if [ "$BROKEN" ]; then
-device asus-rt-ac51u rt-ac51u # BROKEN: no 5GHz usable, LEDs not fully supported
-factory
-fi
+-- ASUS
 
-# GL Innovations
+device('asus-rt-ac51u', 'rt-ac51u', {
+	factory = false,
+	broken = true, -- no 5GHz usable, LEDs not fully supported
+})
 
-device gl-mt300a gl-mt300a
-factory
 
-device gl-mt300n gl-mt300n
-factory
+-- GL Innovations
 
-device gl-mt750 gl-mt750
-factory
+device('gl-mt300a', 'gl-mt300a', {
+	factory = false,
+})
 
-# Nexx
+device('gl-mt300n', 'gl-mt300n', {
+	factory = false,
+})
 
-device nexx-wt3020-8m wt3020-8M
-alias nexx-wt3020ad
-alias nexx-wt3020f
-alias nexx-wt3020h
+device('gl-mt750', 'gl-mt750', {
+	factory = false,
+})
 
-# Xiaomi
 
-if [ "$BROKEN" ]; then
-device xiaomi-miwifi-mini miwifi-mini # BROKEN: 2.4GHz WiFi is Unstable 
-factory
-fi
+-- Nexx
+
+device('nexx-wt3020-8m', 'wt3020-8M', {
+	aliases = {
+		'nexx-wt3020ad',
+		'nexx-wt3020f',
+		'nexx-wt3020h',
+	},
+})
+
+
+-- Xiaomi
+
+device('xiaomi-miwifi-mini', 'miwifi-mini', {
+	factory = false,
+	broken = true, -- 2.4GHz WiFi is unstable
+})
diff --git a/targets/ramips-mt7621 b/targets/ramips-mt7621
index 7be4dc14f..db42f1bb2 100644
--- a/targets/ramips-mt7621
+++ b/targets/ramips-mt7621
@@ -1,43 +1,46 @@
-## D-Link
+-- D-Link
 
-if [ "$BROKEN" ] || [ "$GLUON_WLAN_MESH" = '11s' ]; then
-device d-link-dir-860l-b1 dir-860l-b1 # BROKEN: IBSS untested
-fi
+device('d-link-dir-860l-b1', 'dir-860l-b1', {
+	broken = (env.GLUON_WLAN_MESH ~= '11s'),
+})
 
 
-## Netgear
+-- Netgear
 
-if [ "$BROKEN" ]; then
-device netgear-wndr3700v5 wndr3700v5 # BROKEN: Untested
-factory
-fi
+device('netgear-wndr3700v5', 'wndr3700v5', {
+	factory = false,
+	broken = true, -- untested
+})
 
 
-# BROKEN: IBSS untested
-if [ "$BROKEN" ] || [ "$GLUON_WLAN_MESH" = '11s' ]; then
+-- ZBT
 
-## ZBT
+device('zbt-wg3526-16m', 'zbt-wg3526-16M', {
+	factory = false,
+	manifest_aliases = {
+		'zbt-wg3526',
+	},
+	broken = (env.GLUON_WLAN_MESH ~= '11s'),
+})
 
-device zbt-wg3526-16m zbt-wg3526-16M
-factory
-manifest_alias zbt-wg3526
+device('zbt-wg3526-32m', 'zbt-wg3526-32M', {
+	factory = false,
+	broken = (env.GLUON_WLAN_MESH ~= '11s'),
+})
 
-device zbt-wg3526-32m zbt-wg3526-32M
-factory
 
-fi
+-- Devices without WLAN
 
+-- Ubiquiti
 
-# Devices without WLAN
+device('ubnt-erx', 'ubnt-erx', {
+	factory = false,
+	sysupgrade_ext = '.tar',
+	packages = {'-hostapd-mini'},
+})
 
-## Ubiquiti
-
-device ubnt-erx ubnt-erx
-packages '-hostapd-mini'
-factory
-sysupgrade '.tar'
-
-device ubnt-erx-sfp ubnt-erx-sfp
-packages '-hostapd-mini'
-factory
-sysupgrade '.tar'
+device('ubnt-erx-sfp', 'ubnt-erx-sfp', {
+	factory = false,
+	sysupgrade_ext = '.tar',
+	packages = {'-hostapd-mini'},
+})
diff --git a/targets/ramips-mt76x8 b/targets/ramips-mt76x8
index b0d8bf6ba..cb108535f 100644
--- a/targets/ramips-mt76x8
+++ b/targets/ramips-mt76x8
@@ -1,30 +1,40 @@
-# GL.iNet
+-- GL.iNet
 
-device gl-mt300n-v2 gl-mt300n-v2
-factory
+device('gl-mt300n-v2', 'gl-mt300n-v2', {
+	factory = false,
+})
 
 
-# Netgear
+-- Netgear
 
-device netgear-r6120 netgear_r6120
-factory .img
+device('netgear-r6120', 'netgear_r6120', {
+	factory_ext = '.img',
+})
 
 
-# TP-Link
+-- TP-Link
 
-device tp-link-archer-c50-v3 tplink_c50-v3
-factory
-extra_image -squashfs-tftp-recovery -bootloader .bin
+device('tp-link-archer-c50-v3', 'tplink_c50-v3', {
+	factory = false,
+	extra_images = {
+		{'-squashfs-tftp-recovery', '-bootloader', '.bin'},
+	},
+})
 
-device tp-link-archer-c50-v4 tplink_c50-v4
-factory
+device('tp-link-archer-c50-v4', 'tplink_c50-v4', {
+	factory = false,
+})
 
-device tp-link-tl-wr841n-v13 tl-wr841n-v13
-factory
-extra_image -squashfs-tftp-recovery -bootloader .bin
+device('tp-link-tl-wr841n-v13', 'tl-wr841n-v13', {
+	factory = false,
+	extra_images = {
+		{'-squashfs-tftp-recovery', '-bootloader', '.bin'},
+	},
+})
 
 
-# VoCore 2
+-- VoCore 2
 
-device vocore2 vocore2
-factory
+device('vocore2', 'vocore2', {
+	factory = false,
+})
diff --git a/targets/ramips-rt305x b/targets/ramips-rt305x
index c1641c8ed..2ee6cac3a 100644
--- a/targets/ramips-rt305x
+++ b/targets/ramips-rt305x
@@ -1,25 +1,30 @@
 config '# CONFIG_KERNEL_KALLSYMS is not set'
 
-# A5
+-- A5
 
-device a5-v11 a5-v11
+device('a5-v11', 'a5-v11')
 
 
-# D-Link
+-- D-Link
 
-device d-link-dir-615-h1 dir-615-h1
+device('d-link-dir-615-h1', 'dir-615-h1')
 
-device d-link-dir-615-d dir-615-d
-alias d-link-dir-615-d1
-alias d-link-dir-615-d2
-alias d-link-dir-615-d3
-alias d-link-dir-615-d4
+device('d-link-dir-615-d', 'dir-615-d', {
+	aliases = {
+		'd-link-dir-615-d1',
+		'd-link-dir-615-d2',
+		'd-link-dir-615-d3',
+		'd-link-dir-615-d4',
+	},
+})
 
 
-# VoCore
+-- VoCore
 
-device vocore-8M vocore-8M
-factory
+device('vocore-8M', 'vocore-8M', {
+	factory = false,
+})
 
-device vocore-16M vocore-16M
-factory
+device('vocore-16M', 'vocore-16M', {
+	factory = false,
+})
diff --git a/targets/sunxi-cortexa7 b/targets/sunxi-cortexa7
index 418fcefaf..8bc73e68b 100644
--- a/targets/sunxi-cortexa7
+++ b/targets/sunxi-cortexa7
@@ -1,13 +1,17 @@
-factory -ext4-sdcard .img.gz
-sysupgrade -ext4-sdcard .img.gz
+defaults {
+	factory = '-ext4-sdcard',
+	factory_ext = '.img.gz',
+	sysupgrade = '-ext4-sdcard',
+	sysupgrade_ext = '.img.gz',
+}
 
 
-device lemaker-banana-pi sun7i-a20-bananapi
+device('lemaker-banana-pi', 'sun7i-a20-bananapi')
 
-if [ "$BROKEN" ]; then
+device('lemaker-banana-pro', 'sun7i-a20-bananapro', {
+	broken = true, -- WiFi chip not supported
+})
 
-device lemaker-banana-pro sun7i-a20-bananapro # BROKEN: WiFi chip not supported
-
-device lamobo-r1 sun7i-a20-lamobo-r1 # BROKEN: AP+IBSS and AP+11s not working
-
-fi
+device('lamobo-r1', 'sun7i-a20-lamobo-r1', {
+	broken = true, -- AP+IBSS and AP+11s not working
+})
diff --git a/targets/x86-64 b/targets/x86-64
index 48d95cecc..7db3a032f 100644
--- a/targets/x86-64
+++ b/targets/x86-64
@@ -1,6 +1,6 @@
-. "${GLUON_TARGETSDIR}/x86.inc"
+dofile(env.GLUON_TARGETSDIR .. '/x86.inc')
 
-factory_image x86-64 combined-squashfs .img.gz
-factory_image x86-64 combined-squashfs .vdi
-factory_image x86-64 combined-squashfs .vmdk
-sysupgrade_image x86-64 combined-squashfs .img.gz
+factory_image('x86-64', 'combined-squashfs', '.img.gz')
+factory_image('x86-64', 'combined-squashfs', '.vdi')
+factory_image('x86-64', 'combined-squashfs', '.vmdk')
+sysupgrade_image('x86-64', 'combined-squashfs', '.img.gz')
diff --git a/targets/x86-generic b/targets/x86-generic
index 2699a316c..28eb889b7 100644
--- a/targets/x86-generic
+++ b/targets/x86-generic
@@ -1,8 +1,11 @@
-. "${GLUON_TARGETSDIR}/x86.inc"
+dofile(env.GLUON_TARGETSDIR .. '/x86.inc')
 
-factory_image x86-generic combined-squashfs .img.gz
-factory_image x86-generic combined-squashfs .vdi
-factory_image x86-generic combined-squashfs .vmdk
-sysupgrade_image x86-generic combined-squashfs .img.gz
-manifest_alias x86-kvm
-manifest_alias x86-xen_domu
+factory_image('x86-generic', 'combined-squashfs', '.img.gz')
+factory_image('x86-generic', 'combined-squashfs', '.vdi')
+factory_image('x86-generic', 'combined-squashfs', '.vmdk')
+sysupgrade_image('x86-generic', 'combined-squashfs', '.img.gz', {
+	manifest_aliases = {
+		'x86-kvm',
+		'x86-xen_domu',
+	},
+})
diff --git a/targets/x86-geode b/targets/x86-geode
index 58fb9aebc..57519eb1b 100644
--- a/targets/x86-geode
+++ b/targets/x86-geode
@@ -1,5 +1,22 @@
-packages 'kmod-3c59x' 'kmod-8139cp' 'kmod-8139too' 'kmod-e100' 'kmod-e1000' 'kmod-forcedeth' 'kmod-igb' 'kmod-natsemi' 'kmod-ne2k-pci'
-packages 'kmod-pcnet32' 'kmod-r8169' 'kmod-sis900' 'kmod-sky2' 'kmod-tg3' 'kmod-tulip' 'kmod-via-rhine' 'kmod-via-velocity'
+packages {
+	'kmod-3c59x',
+	'kmod-8139cp',
+	'kmod-8139too',
+	'kmod-e100',
+	'kmod-e1000',
+	'kmod-forcedeth',
+	'kmod-igb',
+	'kmod-natsemi',
+	'kmod-ne2k-pci',
+	'kmod-pcnet32',
+	'kmod-r8169',
+	'kmod-sis900',
+	'kmod-sky2',
+	'kmod-tg3',
+	'kmod-tulip',
+	'kmod-via-rhine',
+	'kmod-via-velocity',
+}
 
-factory_image x86-geode combined-squashfs .img.gz
-sysupgrade_image x86-geode combined-squashfs .img.gz
+factory_image('x86-geode', 'combined-squashfs', '.img.gz')
+sysupgrade_image('x86-geode', 'combined-squashfs', '.img.gz')
diff --git a/targets/x86.inc b/targets/x86.inc
index c66c34084..3e3b5e539 100644
--- a/targets/x86.inc
+++ b/targets/x86.inc
@@ -1,14 +1,42 @@
 config 'CONFIG_VDI_IMAGES=y'
 config 'CONFIG_VMDK_IMAGES=y'
 
-ATH10K_PACKAGES='kmod-ath10k-ct ath10k-firmware-qca9887 ath10k-firmware-qca988x'
-if [ "$GLUON_WLAN_MESH" = 'ibss' ]; then
-	ATH10K_PACKAGES='kmod-ath10k-ct ath10k-firmware-qca9887-ct ath10k-firmware-qca988x-ct'
-fi
+local ATH10K_PACKAGES = {'kmod-ath10k-ct', 'ath10k-firmware-qca9887', 'ath10k-firmware-qca988x'}
+if env.GLUON_WLAN_MESH == 'ibss' then
+	ATH10K_PACKAGES = {'kmod-ath10k-ct', 'ath10k-firmware-qca9887-ct', 'ath10k-firmware-qca988x-ct'}
+end
 
-packages 'kmod-3c59x' 'kmod-8139cp' 'kmod-8139too' 'kmod-e100' 'kmod-e1000' 'kmod-e1000e' 'kmod-forcedeth' 'kmod-igb' 'kmod-natsemi' 'kmod-ne2k-pci'
-packages 'kmod-pcnet32' 'kmod-r8169' 'kmod-sis900' 'kmod-sky2' 'kmod-tg3' 'kmod-tulip' 'kmod-via-rhine' 'kmod-via-velocity'
-packages 'kmod-ath9k'
-packages $ATH10K_PACKAGES
-packages 'kmod-gpio-button-hotplug' 'kmod-gpio-nct5104d' 'kmod-hwmon-core' 'kmod-leds-gpio' 'kmod-leds-apu2' 'kmod-sp5100_tco'
-packages 'kmod-usb-core' 'kmod-usb-ohci' 'kmod-usb2' 'kmod-usb3' 'kmod-usb-serial'
+packages {
+	'kmod-3c59x',
+	'kmod-8139cp',
+	'kmod-8139too',
+	'kmod-e100',
+	'kmod-e1000',
+	'kmod-e1000e',
+	'kmod-forcedeth',
+	'kmod-igb',
+	'kmod-natsemi',
+	'kmod-ne2k-pci',
+	'kmod-pcnet32',
+	'kmod-r8169',
+	'kmod-sis900',
+	'kmod-sky2',
+	'kmod-tg3',
+	'kmod-tulip',
+	'kmod-via-rhine',
+	'kmod-via-velocity',
+	'kmod-ath9k',
+	'kmod-gpio-button-hotplug',
+	'kmod-gpio-nct5104d',
+	'kmod-hwmon-core',
+	'kmod-leds-gpio',
+	'kmod-leds-apu2',
+	'kmod-sp5100_tco',
+	'kmod-usb-core',
+	'kmod-usb-ohci',
+	'kmod-usb2',
+	'kmod-usb3',
+	'kmod-usb-serial',
+}
+
+packages(ATH10K_PACKAGES)
-- 
GitLab