mail archive of the barebox mailing list
 help / color / mirror / Atom feed
* [PATCH 0/5] boot: implement devboot in C
@ 2026-10-30 14:13 Sascha Hauer
  2026-10-30 14:13 ` [PATCH 1/5] boot: report the exit status of failing boot scripts Sascha Hauer
                   ` (4 more replies)
  0 siblings, 5 replies; 6+ messages in thread
From: Sascha Hauer @ 2026-10-30 14:13 UTC (permalink / raw)
  To: BAREBOX

devboot is a script on top of the boot command, which makes it hard to
integrate: Kernel command line fragments need a dedicated namespace to be
removed again, and booting with devboot by default needs a special boot
target. Also, boot overrides get lost when a boot script calls boot or
bootm itself.

This series hands overrides on to whatever a boot entry boots and turns
devboot into what it really is, an option to the boot command: boot -D,
or global.boot.devboot=1 to enable it by default. The devboot command
remains as a shorthand for boot -D.

The series is based on the "boot: UAF and double free fixes", "Add and
use file_exists() helper" and "boot: revert boot related globalvars after
booting" series.

Signed-off-by: Sascha Hauer <s.hauer@pengutronix.de>
---
Sascha Hauer (5):
      boot: report the exit status of failing boot scripts
      hush: export source_script()
      boot: hand overrides on to nested boots and the bootm command
      boot: implement devboot in C
      sandbox: defconfig: enable boot overrides and devboot

 Documentation/user/devboot.rst            |  76 +++++++-----
 arch/sandbox/configs/sandbox_defconfig    |   2 +
 commands/boot.c                           |  28 ++++-
 commands/bootm.c                          |   3 +-
 common/Kconfig                            |  12 +-
 common/Makefile                           |   1 +
 common/boot.c                             |  63 ++++++----
 common/bootchooser.c                      |  13 +-
 common/bootm.c                            |  22 +++-
 common/devboot.c                          | 197 ++++++++++++++++++++++++++++++
 common/hush.c                             |  17 ++-
 defaultenv/defaultenv-2-base/bin/devboot  |  52 +-------
 defaultenv/defaultenv-2-base/boot/devboot |   8 --
 include/boot.h                            |   9 ++
 include/bootchooser.h                     |   2 +-
 include/bootm.h                           |   3 +
 include/devboot.h                         |  31 +++++
 include/shell.h                           |   8 ++
 test/py/test_boot_overrides.py            |  92 ++++++++++++++
 test/py/test_boot_script.py               |  13 ++
 test/py/test_devboot.py                   | 131 ++++++++++++++++++++
 21 files changed, 653 insertions(+), 130 deletions(-)
---
base-commit: e23fbb0ea940ba8b0f7f153297cb48c39fcbf41d
change-id: 20261030-devboot-af494560e196

Best regards,
-- 
Sascha Hauer <s.hauer@pengutronix.de>




^ permalink raw reply	[flat|nested] 6+ messages in thread

* [PATCH 1/5] boot: report the exit status of failing boot scripts
  2026-10-30 14:13 [PATCH 0/5] boot: implement devboot in C Sascha Hauer
@ 2026-10-30 14:13 ` Sascha Hauer
  2026-10-30 14:13 ` [PATCH 2/5] hush: export source_script() Sascha Hauer
                   ` (3 subsequent siblings)
  4 siblings, 0 replies; 6+ messages in thread
From: Sascha Hauer @ 2026-10-30 14:13 UTC (permalink / raw)
  To: BAREBOX

When a boot script exits with a non-zero status, execute_binfmt()
returns that status as a positive value. bootscript_boot() passes it to
strerror(-ret) and boot_entry() to %pe, both of which expect a negative
errno, resulting in messages like:

  ERROR: Running script '/env/boot/mmc' failed: error -1
  ERROR: Booting entry 'mmc' failed: 0000000000000001

Allow boot entries to return a boot script's exit status and have
boot_entry() report it as such. bootscript_boot() now only reports
errors of its own, so a failing script results in a single

  ERROR: Booting entry 'mmc' failed: exit status 1

Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Sascha Hauer <s.hauer@pengutronix.de>
---
 common/boot.c               | 11 +++++++----
 include/boot.h              |  1 +
 test/py/test_boot_script.py | 13 +++++++++++++
 3 files changed, 21 insertions(+), 4 deletions(-)

diff --git a/common/boot.c b/common/boot.c
index 023eaf5145..b993cabf65 100644
--- a/common/boot.c
+++ b/common/boot.c
@@ -115,10 +115,11 @@ static int bootscript_boot(struct bootentry *entry, int verbose, int dryrun)
 	 * without the shell expanding or splitting it first.
 	 */
 	ret = execute_binfmt(bs->args ? 2 : 1, argv);
-	if (ret) {
-		pr_err("Running script '%s' failed: %s\n", bs->entry.path, strerror(-ret));
+	if (ret < 0)
+		pr_err("Running script '%s' failed: %pe\n", bs->entry.path, ERR_PTR(ret));
+	/* positive values are the exit status, reported by boot_entry() */
+	if (ret)
 		return ret;
-	}
 
 	/* No point in repeating bootm if script already called it */
 	if (bootm_nattempts != bootm_command_attempts())
@@ -206,7 +207,9 @@ int boot_entry(struct bootentry *be, int verbose, int dryrun)
 	scope = boot_globalvar_scope_push();
 
 	ret = be->boot(be, verbose, dryrun);
-	if (ret && ret != -ENOMEDIUM)
+	if (ret > 0)
+		pr_err("Booting entry '%s' failed: exit status %d\n", be->title, ret);
+	else if (ret && ret != -ENOMEDIUM)
 		pr_err("Booting entry '%s' failed: %pe\n", be->title, ERR_PTR(ret));
 
 	globalvar_scope_pop(scope);
diff --git a/include/boot.h b/include/boot.h
index b30cdd314a..bb99dacc85 100644
--- a/include/boot.h
+++ b/include/boot.h
@@ -20,6 +20,7 @@ struct bootentry {
 	const char *title;
 	char *description;
 	const char *path;
+	/* 0 on success, else negative errno or a boot script's exit status */
 	int (*boot)(struct bootentry *entry, int verbose, int dryrun);
 	void (*release)(struct bootentry *entry);
 	struct bootm_overrides overrides;
diff --git a/test/py/test_boot_script.py b/test/py/test_boot_script.py
new file mode 100644
index 0000000000..be08e964d9
--- /dev/null
+++ b/test/py/test_boot_script.py
@@ -0,0 +1,13 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+
+def test_boot_script_exit_status(barebox):
+    barebox.run_check("echo -o /env/boot/exit3 '#!/bin/sh'")
+    barebox.run_check("echo -a /env/boot/exit3 'exit 3'")
+
+    stdout, _, ret = barebox.run("boot exit3")
+    assert ret != 0
+    assert "ERROR: Booting entry 'exit3' failed: exit status 3" in stdout
+    assert not any("Running script" in line for line in stdout)
+
+    barebox.run_check("rm /env/boot/exit3")

-- 
2.47.3




^ permalink raw reply	[flat|nested] 6+ messages in thread

* [PATCH 2/5] hush: export source_script()
  2026-10-30 14:13 [PATCH 0/5] boot: implement devboot in C Sascha Hauer
  2026-10-30 14:13 ` [PATCH 1/5] boot: report the exit status of failing boot scripts Sascha Hauer
@ 2026-10-30 14:13 ` Sascha Hauer
  2026-10-30 14:13 ` [PATCH 3/5] boot: hand overrides on to nested boots and the bootm command Sascha Hauer
                   ` (2 subsequent siblings)
  4 siblings, 0 replies; 6+ messages in thread
From: Sascha Hauer @ 2026-10-30 14:13 UTC (permalink / raw)
  To: BAREBOX

C code that wants to source a shell script has to go through
run_command("source ..."), which parses the command line and looks the
script up in the PATH.

Export source_script() instead. As the simple parser can't source
scripts, it gets a stub returning -ENOSYS. Unlike its callers in hush,
source_script() can now be called directly, so check the security policy
in it, like run_command() and execute_script() do.

Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Sascha Hauer <s.hauer@pengutronix.de>
---
 common/hush.c   | 17 +++++++++++++++--
 include/shell.h |  8 ++++++++
 2 files changed, 23 insertions(+), 2 deletions(-)

diff --git a/common/hush.c b/common/hush.c
index ac885fbfe7..c5809b32cd 100644
--- a/common/hush.c
+++ b/common/hush.c
@@ -329,7 +329,6 @@ static char **make_list_in(char **inp, char *name);
 static char *insert_var_value(char *inp);
 static int set_local_var(const char *s, int flg_export);
 static int execute_script(const char *path, int argc, char *argv[]);
-static int source_script(const char *path, int argc, char *argv[]);
 
 static int b_check_space(o_string *o, int len)
 {
@@ -1975,12 +1974,26 @@ static int execute_script(const char *path, int argc, char *argv[])
 	return ret;
 }
 
-static int source_script(const char *path, int argc, char *argv[])
+/**
+ * source_script - execute a shell script in the current environment
+ * @path: path of the script
+ * @argc: number of positional parameters, including $0
+ * @argv: positional parameters, starting with $0
+ *
+ * Unlike executing a script, variables set by it remain set in the current
+ * environment context after it returns.
+ *
+ * Return: exit status of the last command executed, or a negative error code
+ */
+int source_script(const char *path, int argc, char *argv[])
 {
 	struct p_context ctx = {};
 	char *script;
 	int ret;
 
+	if (!IS_ALLOWED(SCONFIG_SHELL))
+		return -EPERM;
+
 	initialize_context(&ctx, true);
 
 	ctx.global_argc = argc;
diff --git a/include/shell.h b/include/shell.h
index b399564ff3..a0c3414fa6 100644
--- a/include/shell.h
+++ b/include/shell.h
@@ -6,17 +6,25 @@
 #ifndef __SHELL_H__
 #define __SHELL_H__
 
+#include <linux/errno.h>
+
 int shell_get_last_return_code(void);
 
 int run_shell(void);
 
 #ifdef CONFIG_SHELL_HUSH
 char *shell_expand(char *str);
+int source_script(const char *path, int argc, char *argv[]);
 #else
 static inline char *shell_expand(char *str)
 {
 	return strdup(str);
 }
+
+static inline int source_script(const char *path, int argc, char *argv[])
+{
+	return -ENOSYS;
+}
 #endif
 
 #ifdef CONFIG_BAREBOX_CMDLINE

-- 
2.47.3




^ permalink raw reply	[flat|nested] 6+ messages in thread

* [PATCH 3/5] boot: hand overrides on to nested boots and the bootm command
  2026-10-30 14:13 [PATCH 0/5] boot: implement devboot in C Sascha Hauer
  2026-10-30 14:13 ` [PATCH 1/5] boot: report the exit status of failing boot scripts Sascha Hauer
  2026-10-30 14:13 ` [PATCH 2/5] hush: export source_script() Sascha Hauer
@ 2026-10-30 14:13 ` Sascha Hauer
  2026-10-30 14:13 ` [PATCH 4/5] boot: implement devboot in C Sascha Hauer
  2026-10-30 14:13 ` [PATCH 5/5] sandbox: defconfig: enable boot overrides and devboot Sascha Hauer
  4 siblings, 0 replies; 6+ messages in thread
From: Sascha Hauer @ 2026-10-30 14:13 UTC (permalink / raw)
  To: BAREBOX

Overrides are stored in the boot entry they are given for and only
applied by bootm_entry(). They get lost when a boot entry boots something
else, like a boot script calling boot, or calling the bootm command.

Keep track of the overrides of the boot entry being booted. Hand them on
to boot entries booted from there, with the ones of the outer entry
taking precedence, and apply them in the bootm command. bootchooser no
longer needs to merge the overrides into its targets itself.

This is done in do_bootm() and not in bootm_boot(), which the Android
image handler calls while a bootm is already running, so it would apply
the overrides a second time.

Assisted-by: Claude:claude-opus-5-5
---
 Documentation/user/devboot.rst |  4 ++
 commands/bootm.c               |  3 +-
 common/boot.c                  | 44 ++++++++++++--------
 common/bootchooser.c           | 13 ++----
 common/bootm.c                 | 22 +++++++++-
 include/boot.h                 |  8 ++++
 include/bootchooser.h          |  2 +-
 include/bootm.h                |  3 ++
 test/py/test_boot_overrides.py | 92 ++++++++++++++++++++++++++++++++++++++++++
 9 files changed, 163 insertions(+), 28 deletions(-)

diff --git a/Documentation/user/devboot.rst b/Documentation/user/devboot.rst
index 62cc357c3a..e05541f05b 100644
--- a/Documentation/user/devboot.rst
+++ b/Documentation/user/devboot.rst
@@ -38,6 +38,10 @@ Overrides are applied after the boot entry has been loaded, so the original
 entry still selects the boot handler, kernel arguments, etc. Only the
 specified components are replaced.
 
+Overrides also apply to whatever a boot entry boots itself, like a boot script
+calling ``boot`` or ``bootm``, even if that is passed an image explicitly. If
+a nested ``boot`` has overrides of its own, the outer ones take precedence.
+
 Override rules
 ^^^^^^^^^^^^^^
 
diff --git a/commands/bootm.c b/commands/bootm.c
index 44abb953f6..0d6a95edae 100644
--- a/commands/bootm.c
+++ b/commands/bootm.c
@@ -16,6 +16,7 @@
 #include <fcntl.h>
 #include <fs.h>
 #include <errno.h>
+#include <boot.h>
 #include <bootm.h>
 #include <rtc.h>
 #include <init.h>
@@ -94,7 +95,7 @@ static int do_bootm(int argc, char *argv[])
 		goto err_out;
 	}
 
-	ret = bootm_boot(&data);
+	ret = bootm_boot_overrides(&data, boot_get_active_overrides());
 	if (ret) {
 		printf("handler failed with: %pe\n", ERR_PTR(ret));
 		goto err_out;
diff --git a/common/boot.c b/common/boot.c
index b993cabf65..271cfd3a29 100644
--- a/common/boot.c
+++ b/common/boot.c
@@ -187,8 +187,23 @@ BAREBOX_MAGICVAR(global.boot.watchdog_timeout,
 BAREBOX_MAGICVAR(global.user,
 		"Username used by the default environment to construct network boot paths");
 
+/* The overrides of the boot entry currently being booted */
+static const struct bootm_overrides *boot_active_overrides;
+
+/**
+ * boot_get_active_overrides - get the overrides of the current boot entry
+ *
+ * Return: the overrides of the boot entry currently being booted, NULL if
+ * there is none
+ */
+const struct bootm_overrides *boot_get_active_overrides(void)
+{
+	return boot_active_overrides;
+}
+
 int boot_entry(struct bootentry *be, int verbose, int dryrun)
 {
+	const struct bootm_overrides *outer = boot_active_overrides;
 	struct globalvar_scope *scope;
 	int ret;
 
@@ -206,7 +221,19 @@ int boot_entry(struct bootentry *be, int verbose, int dryrun)
 
 	scope = boot_globalvar_scope_push();
 
+	/*
+	 * A boot entry may boot other entries, for example a boot script
+	 * calling boot. Hand the overrides on to them, the ones of the outer
+	 * entry take precedence.
+	 */
+	if (outer)
+		bootm_merge_overrides(&be->overrides, outer);
+	boot_active_overrides = &be->overrides;
+
 	ret = be->boot(be, verbose, dryrun);
+
+	boot_active_overrides = outer;
+
 	if (ret > 0)
 		pr_err("Booting entry '%s' failed: exit status %d\n", be->title, ret);
 	else if (ret && ret != -ENOMEDIUM)
@@ -642,20 +669,5 @@ BAREBOX_MAGICVAR(global.boot.default, "default boot order");
  */
 int bootm_entry(struct bootentry *be, const struct bootm_data *bootm_data)
 {
-	struct image_data *data;
-	int ret;
-
-	data = bootm_boot_prep(bootm_data);
-	if (IS_ERR(data))
-		return PTR_ERR(data);
-
-	ret = bootm_apply_overrides(data, &be->overrides);
-	if (ret)
-		goto out;
-
-	ret = bootm_boot_handler(data);
-
-out:
-	bootm_boot_cleanup(data);
-	return ret;
+	return bootm_boot_overrides(bootm_data, &be->overrides);
 }
diff --git a/common/bootchooser.c b/common/bootchooser.c
index 2049425ab3..c8f72871f3 100644
--- a/common/bootchooser.c
+++ b/common/bootchooser.c
@@ -840,8 +840,7 @@ void bootchooser_lock_attempts(bool locked)
 	attempts_locked = locked;
 }
 
-static int bootchooser_boot_one(struct bootchooser *bc, int *tryagain,
-				struct bootentry *parent_entry)
+static int bootchooser_boot_one(struct bootchooser *bc, int *tryagain)
 {
 	char *system;
 	struct bootentries *entries;
@@ -878,10 +877,6 @@ static int bootchooser_boot_one(struct bootchooser *bc, int *tryagain,
 	ret = -ENOENT;
 
 	bootentries_for_each_entry(entries, entry) {
-		if (parent_entry)
-			bootm_merge_overrides(&entry->overrides,
-					      &parent_entry->overrides);
-
 		ret = boot_entry(entry, bc->verbose, bc->dryrun);
 		if (!ret) {
 			*tryagain = 0;
@@ -898,12 +893,12 @@ static int bootchooser_boot_one(struct bootchooser *bc, int *tryagain,
 	return ret;
 }
 
-int bootchooser_boot(struct bootchooser *bc, struct bootentry *entry)
+int bootchooser_boot(struct bootchooser *bc)
 {
 	int ret, tryagain;
 
 	do {
-		ret = bootchooser_boot_one(bc, &tryagain, entry);
+		ret = bootchooser_boot_one(bc, &tryagain);
 
 		if (!retry)
 			break;
@@ -924,7 +919,7 @@ int bootchooser_entry_boot(struct bootentry *entry, int verbose, int dryrun)
 	bc->verbose = verbose;
 	bc->dryrun = dryrun;
 
-	ret = bootchooser_boot(bc, entry);
+	ret = bootchooser_boot(bc);
 
 	bootchooser_put(bc);
 
diff --git a/common/bootm.c b/common/bootm.c
index 2ad8c34bb4..2ed377da18 100644
--- a/common/bootm.c
+++ b/common/bootm.c
@@ -785,7 +785,15 @@ void bootm_boot_cleanup(struct image_data *data)
 /*
  * bootm_boot - Boot an application image described by bootm_data
  */
-int bootm_boot(const struct bootm_data *bootm_data)
+/**
+ * bootm_boot_overrides - boot with overrides applied
+ * @bootm_data: bootm data
+ * @overrides: overrides to apply, may be NULL
+ *
+ * Return: 0 on success, or negative error code otherwise
+ */
+int bootm_boot_overrides(const struct bootm_data *bootm_data,
+			 const struct bootm_overrides *overrides)
 {
 	struct image_data *data;
 	int ret;
@@ -794,12 +802,24 @@ int bootm_boot(const struct bootm_data *bootm_data)
 	if (IS_ERR(data))
 		return PTR_ERR(data);
 
+	if (overrides) {
+		ret = bootm_apply_overrides(data, overrides);
+		if (ret)
+			goto out;
+	}
+
 	ret = bootm_boot_handler(data);
 
+out:
 	bootm_boot_cleanup(data);
 	return ret;
 }
 
+int bootm_boot(const struct bootm_data *bootm_data)
+{
+	return bootm_boot_overrides(bootm_data, NULL);
+}
+
 bool bootm_efi_check_image(struct image_handler *handler,
 			   struct image_data *data,
 			   enum filetype detected_filetype)
diff --git a/include/boot.h b/include/boot.h
index bb99dacc85..72cc769d59 100644
--- a/include/boot.h
+++ b/include/boot.h
@@ -56,6 +56,14 @@ int bootentry_create_from_name(struct bootentries *bootentries,
 void bootsources_menu(struct bootentries *bootentries, unsigned default_entry, int timeout);
 void bootsources_list(struct bootentries *bootentries);
 int boot_entry(struct bootentry *be, int verbose, int dryrun);
+#ifdef CONFIG_BOOT
+const struct bootm_overrides *boot_get_active_overrides(void);
+#else
+static inline const struct bootm_overrides *boot_get_active_overrides(void)
+{
+	return NULL;
+}
+#endif
 
 struct bootm_data;
 int bootm_entry(struct bootentry *be, const struct bootm_data *data);
diff --git a/include/bootchooser.h b/include/bootchooser.h
index da3b974ef6..36cbd0dd0a 100644
--- a/include/bootchooser.h
+++ b/include/bootchooser.h
@@ -14,7 +14,7 @@ int bootchooser_put(struct bootchooser *bootchooser);
 
 void bootchooser_info(struct bootchooser *bootchooser);
 
-int bootchooser_boot(struct bootchooser *bc, struct bootentry *entry);
+int bootchooser_boot(struct bootchooser *bc);
 
 struct bootchooser_target *bootchooser_get_last_chosen(struct bootchooser *bootchooser);
 void bootchooser_lock_attempts(bool locked);
diff --git a/include/bootm.h b/include/bootm.h
index 06eb90c5ed..d63e9fc4f6 100644
--- a/include/bootm.h
+++ b/include/bootm.h
@@ -75,6 +75,9 @@ struct bootm_data {
 int bootm_image_name_and_part(const char *name, char **filename, char **part);
 
 int bootm_boot(const struct bootm_data *data);
+struct bootm_overrides;
+int bootm_boot_overrides(const struct bootm_data *data,
+			 const struct bootm_overrides *overrides);
 
 struct image_data *bootm_boot_prep(const struct bootm_data *bootm_data);
 int bootm_open_os_compressed(struct image_data *data);
diff --git a/test/py/test_boot_overrides.py b/test/py/test_boot_overrides.py
new file mode 100644
index 0000000000..674b3a2116
--- /dev/null
+++ b/test/py/test_boot_overrides.py
@@ -0,0 +1,92 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+import pytest
+from .helper import skip_disabled
+from .test_boot_globalvar_scope import write_bootscript
+
+OVERRIDE = "/tmp/override-image"
+BASE = "/tmp/base-image"
+OUTER = "/tmp/outer-image"
+
+
+def make_image(barebox, path):
+    # bootm wants a full page of a known image type. There's no handler for
+    # ELF images here, but it only fails after reporting the image it boots
+    barebox.run_check(f"echo -o {path} image")
+    for _ in range(6):
+        barebox.run_check(f"cat -o {path}.tmp {path} {path}")
+        barebox.run_check(f"cat -o {path} {path}.tmp {path}.tmp")
+    barebox.run_check(f"rm {path}.tmp")
+    barebox.run_check(f"mw -b -d {path} 0 0x7f 0x45 0x4c 0x46")
+
+
+def booted(stdout):
+    return [line for line in stdout if line.startswith("Loading ")]
+
+
+@pytest.fixture(scope="function")
+def override_env(barebox, barebox_config):
+    skip_disabled(barebox_config, "CONFIG_BOOT_OVERRIDE", "CONFIG_CMD_ECHO",
+                  "CONFIG_CMD_CAT", "CONFIG_CMD_MW")
+
+    make_image(barebox, BASE)
+    make_image(barebox, OVERRIDE)
+    make_image(barebox, OUTER)
+
+    write_bootscript(barebox, "ovr-target", [
+        f"global.bootm.image={BASE}",
+    ])
+    write_bootscript(barebox, "ovr-nested", [
+        "boot ovr-target",
+    ])
+    write_bootscript(barebox, "ovr-nested-override", [
+        f"boot -o bootm.image={OVERRIDE} ovr-target",
+    ])
+    write_bootscript(barebox, "ovr-bootm", [
+        f"bootm {BASE}",
+    ])
+
+    yield
+
+    barebox.run(f"rm {BASE} {OVERRIDE} {OUTER}")
+    barebox.run("rm /env/boot/ovr-target /env/boot/ovr-nested "
+                "/env/boot/ovr-nested-override /env/boot/ovr-bootm")
+
+
+@pytest.mark.usefixtures("override_env")
+def test_override_entry(barebox):
+    stdout, _, _ = barebox.run("boot ovr-target")
+    assert any(BASE in line for line in booted(stdout))
+
+    stdout, _, _ = barebox.run(f"boot -o bootm.image={OVERRIDE} ovr-target")
+    assert any(OVERRIDE in line for line in booted(stdout))
+
+
+@pytest.mark.usefixtures("override_env")
+def test_override_nested_boot(barebox):
+    stdout, _, _ = barebox.run(f"boot -o bootm.image={OVERRIDE} ovr-nested")
+    assert any(OVERRIDE in line for line in booted(stdout))
+    assert not any(BASE in line for line in booted(stdout))
+
+
+@pytest.mark.usefixtures("override_env")
+def test_override_outer_takes_precedence(barebox):
+    stdout, _, _ = barebox.run(f"boot -o bootm.image={OUTER} ovr-nested-override")
+    assert any(OUTER in line for line in booted(stdout))
+    assert not any(OVERRIDE in line for line in booted(stdout))
+
+
+@pytest.mark.usefixtures("override_env")
+def test_override_bootm_command(barebox):
+    stdout, _, _ = barebox.run(f"boot -o bootm.image={OVERRIDE} ovr-bootm")
+    assert any(OVERRIDE in line for line in booted(stdout))
+    assert not any(BASE in line for line in booted(stdout))
+
+
+@pytest.mark.usefixtures("override_env")
+def test_override_ends_with_boot(barebox):
+    # the override must not stick to a later bootm outside of the boot command
+    barebox.run(f"boot -o bootm.image={OVERRIDE} ovr-target")
+
+    stdout, _, _ = barebox.run(f"bootm {BASE}")
+    assert any(BASE in line for line in booted(stdout))

-- 
2.47.3




^ permalink raw reply	[flat|nested] 6+ messages in thread

* [PATCH 4/5] boot: implement devboot in C
  2026-10-30 14:13 [PATCH 0/5] boot: implement devboot in C Sascha Hauer
                   ` (2 preceding siblings ...)
  2026-10-30 14:13 ` [PATCH 3/5] boot: hand overrides on to nested boots and the bootm command Sascha Hauer
@ 2026-10-30 14:13 ` Sascha Hauer
  2026-10-30 14:13 ` [PATCH 5/5] sandbox: defconfig: enable boot overrides and devboot Sascha Hauer
  4 siblings, 0 replies; 6+ messages in thread
From: Sascha Hauer @ 2026-10-30 14:13 UTC (permalink / raw)
  To: BAREBOX

devboot is a shell script that brings up the network, sources a per-user,
per-board script from global.net.fetchdir and translates the variables it
sets into boot -o options. Being a script on top of the boot command, it
has a hard time integrating with it: The kernel command line fragments
the devboot script sets have to be put into the linux.bootargs.devboot.
namespace to be removed afterwards, and the devboot boot target needed
to boot with devboot by default takes only a single boot target and boot
options.

All devboot really is, is an option to the boot command. Implement it
in C as such: boot -D, or global.boot.devboot=1 to enable it by default,
does what the devboot script did before booting the boot targets. The
script's changes to global.linux.bootargs.* and global.bootm.* are
reverted after the boot command returns, as is the working directory.
A boot command started while devboot is active doesn't source the script
again, it inherits the overrides from the boot entry it is started from.
Overrides given with -o still take precedence.

The devboot command remains as a shorthand for boot -D. The devboot boot
target is removed, nv boot.devboot=1 replaces nv boot.default=devboot@...

Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Sascha Hauer <s.hauer@pengutronix.de>
---
 Documentation/user/devboot.rst            |  72 ++++++-----
 commands/boot.c                           |  28 ++++-
 common/Kconfig                            |  12 +-
 common/Makefile                           |   1 +
 common/boot.c                             |   8 +-
 common/devboot.c                          | 197 ++++++++++++++++++++++++++++++
 defaultenv/defaultenv-2-base/bin/devboot  |  52 +-------
 defaultenv/defaultenv-2-base/boot/devboot |   8 --
 include/devboot.h                         |  31 +++++
 test/py/test_devboot.py                   | 131 ++++++++++++++++++++
 10 files changed, 444 insertions(+), 96 deletions(-)

diff --git a/Documentation/user/devboot.rst b/Documentation/user/devboot.rst
index e05541f05b..f513d1ecd6 100644
--- a/Documentation/user/devboot.rst
+++ b/Documentation/user/devboot.rst
@@ -5,9 +5,9 @@ Devboot
 
 Devboot is a development convenience that builds on top of the
 :ref:`boot override <boot_overrides>` mechanism. It sources a per-user,
-per-board script from the network and translates it into ``boot -o``
-arguments, making it easy to redirect any combination of kernel, device tree
-and initrd to network-hosted files without touching the boot medium.
+per-board script from the network and takes boot overrides from it,
+making it easy to redirect any combination of kernel, device tree and
+initrd to network-hosted files without touching the boot medium.
 
 .. _boot_overrides:
 
@@ -114,21 +114,28 @@ Override rules
    When secure boot is enforced (``bootm_signed_images_are_forced()``),
    overrides are silently ignored.
 
-The devboot script
-------------------
+Booting with devboot
+--------------------
 
-The ``devboot`` command (``/env/bin/devboot``) automates the override
-workflow for network-based development:
+With ``CONFIG_BOOT_DEVBOOT`` enabled, devboot is enabled for a boot by
+passing ``-D`` to the ``boot`` command or by setting
+:ref:`global.boot.devboot <magicvar_global_boot_devboot>`. Before booting
+the boot targets, the ``boot`` command then:
 
-1. Brings up the network (``ifup -a1``).
+1. Brings up the network, like ``ifup -a1``.
 2. Changes into the fetch directory
    (:ref:`global.net.fetchdir <magicvar_global_net_fetchdir>`, default
    ``/mnt/tftp``).
 3. Sources a configuration script named
    ``${global.user}-devboot-${global.hostname}``. If that file does not
    exist, it falls back to ``${global.user}-devboot-${global.arch}``.
-4. Translates the variables set by the script into ``boot -o`` arguments.
-5. Passes any extra arguments through to the ``boot`` command.
+   If neither exists or the script fails, the boot is aborted.
+4. Takes the variables set by the script as boot overrides. Overrides
+   given with ``-o`` take precedence.
+
+Once the boot command returns, it changes back to the previous
+directory and reverts the changes the script made to
+``global.linux.bootargs.*`` and ``global.bootm.*`` variables.
 
 .. note::
 
@@ -142,10 +149,14 @@ Usage
 .. code-block:: sh
 
   # Boot from MMC with devboot overrides
-  devboot mmc
+  boot -D mmc
 
   # Boot from the default boot entry
-  devboot
+  boot -D
+
+``devboot`` is a shorthand for ``boot -D``, so ``devboot mmc`` does the
+same as ``boot -D mmc``. Like ``-o``, devboot doesn't apply to the
+boot menu (``-m``) and listing (``-l``).
 
 Configuration script
 ^^^^^^^^^^^^^^^^^^^^
@@ -181,26 +192,31 @@ kernel command line fragments:
 
 With this script in place and :ref:`global.user=afa <magicvar_global_user>`,
 :ref:`global.hostname=rock3a <magicvar_global_hostname>`,
-running ``devboot mmc`` on the board expands to::
+running ``boot -D mmc`` on the board is equivalent to running::
 
   boot -o bootm.image="afa-fit-rock3a" \
        -o bootm.oftree="" \
        -o bootm.initrd=":afa-modules-arm64" \
        mmc
 
+from within the fetch directory, with ``rsinit.bind_modules`` added to the
+kernel command line.
+
 This fetches the FIT image ``afa-fit-rock3a`` from the fetch directory,
 discards the FIT's device tree (falling back to the barebox-internal one
 when ``CONFIG_BOOTM_OFTREE_FALLBACK`` is enabled), and appends the
 ``afa-modules-arm64`` CPIO archive to whatever initrd the FIT already
 contains.
 
-Kernel command line fragments set by the script should use the
-``global.linux.bootargs.devboot.`` namespace. Unlike variables set
-by the boot entries themselves, which are reverted whenever a boot entry
-returns, it survives falling through to the next boot target, e.g. for
-``devboot system0 system1``. The ``devboot`` command removes the
-namespace after the boot command returns, so a subsequent plain
-``boot`` is unaffected by it.
+The script runs only once per ``boot`` command, even if multiple boot
+targets are given, e.g. for ``boot -D system0 system1``. Kernel
+command line fragments it sets apply to all of them. They are reverted
+once the ``boot`` command returns, so a subsequent ``boot`` without
+devboot is unaffected by them.
+
+A boot command started while devboot is active, e.g. by a boot script
+that boots other boot targets, doesn't source the script again, but
+applies the same overrides.
 
 Snippets under /env/devboot/
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -258,22 +274,16 @@ script is loaded. Make them persistent with:
 :ref:`global.hostname <magicvar_global_hostname>` is typically derived from
 the device tree, but can be overridden.
 
-devboot boot target
-^^^^^^^^^^^^^^^^^^^
+Booting with devboot by default
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
-``devboot`` is available as boot entry as well, so it can be used
-wherever a boot target is expected. The boot target to hand the
-overrides to is appended after an ``@``:
+To boot with devboot by default, including the boot on startup, set:
 
 .. code-block:: sh
 
-   boot devboot@system0
-
-This is most useful to boot with the overrides by default:
-
-.. code-block:: sh
+   nv boot.devboot=1
 
-   nv boot.default=devboot@system0
+``boot`` then always boots with devboot, whatever the boot targets.
 
 Forwarding a remote build directory over the internet
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
diff --git a/commands/boot.c b/commands/boot.c
index 81f392075d..e1c21c43ba 100644
--- a/commands/boot.c
+++ b/commands/boot.c
@@ -7,6 +7,7 @@
 #include <malloc.h>
 #include <boot.h>
 #include <bootm.h>
+#include <devboot.h>
 #include <fnmatch.h>
 #include <complete.h>
 
@@ -111,11 +112,13 @@ static int do_boot(int argc, char *argv[])
 	struct bootentries *entries;
 	struct bootentry *entry;
 	struct bootm_overrides overrides = {};
+	struct devboot *devboot = NULL;
+	bool do_devboot = devboot_enabled();
 	void *handle;
 	const char *name;
 	char *(*next)(void *);
 
-	while ((opt = getopt(argc, argv, "vldmM:t:w:o:")) > 0) {
+	while ((opt = getopt(argc, argv, "vldmM:t:w:o:D")) > 0) {
 		switch (opt) {
 		case 'v':
 			verbose++;
@@ -138,6 +141,13 @@ static int do_boot(int argc, char *argv[])
 		case 'w':
 			boot_set_watchdog_timeout(simple_strtoul(optarg, NULL, 0));
 			break;
+		case 'D':
+			if (!IS_ENABLED(CONFIG_BOOT_DEVBOOT)) {
+				ret = -ENOSYS;
+				goto out_overrides;
+			}
+			do_devboot = true;
+			break;
 		case 'o':
 			ret = boot_add_override(&overrides, optarg);
 			if (ret)
@@ -163,6 +173,16 @@ static int do_boot(int argc, char *argv[])
 		next = next_word;
 	}
 
+	/* like -o, devboot doesn't apply to entries booted from the menu */
+	if (do_devboot && !do_list && !do_menu) {
+		devboot = devboot_start(&overrides);
+		if (IS_ERR(devboot)) {
+			ret = PTR_ERR(devboot);
+			free(freep);
+			goto out_overrides;
+		}
+	}
+
 	entries = bootentries_alloc();
 
 	while ((name = next(&handle)) != NULL) {
@@ -211,6 +231,7 @@ static int do_boot(int argc, char *argv[])
 out:
 	bootentries_free(entries);
 	free(freep);
+	devboot_finish(devboot);
 out_overrides:
 	boot_free_overrides(&overrides);
 
@@ -266,13 +287,16 @@ BAREBOX_CMD_HELP_OPT ("-w SECS","Start watchdog with timeout SECS before booting
 BAREBOX_CMD_HELP_OPT ("-o VAR[=VAL]","override VAR (bootm.{image,oftree,initrd}) with VAL")
 BAREBOX_CMD_HELP_OPT ("            ","if VAL is not specified, the value of VAR is taken")
 #endif
+#ifdef CONFIG_BOOT_DEVBOOT
+BAREBOX_CMD_HELP_OPT ("-D","devboot: take overrides from a script in global.net.fetchdir")
+#endif
 BAREBOX_CMD_HELP_OPT ("-t SECS","specify timeout in SECS")
 BAREBOX_CMD_HELP_END
 
 BAREBOX_CMD_START(boot)
 	.cmd	= do_boot,
 	BAREBOX_CMD_DESC("boot from script, device, ...")
-	BAREBOX_CMD_OPTS("[-vdlmMwto] [BOOTSRC...]")
+	BAREBOX_CMD_OPTS("[-vdlmMwtoD] [BOOTSRC...]")
 	BAREBOX_CMD_GROUP(CMD_GRP_BOOT)
 	BAREBOX_CMD_HELP(cmd_boot_help)
 BAREBOX_CMD_END
diff --git a/common/Kconfig b/common/Kconfig
index da4cc8a5c9..fc762f724b 100644
--- a/common/Kconfig
+++ b/common/Kconfig
@@ -677,6 +677,16 @@ config BOOT_OVERRIDE
 	  Allow overriding of bootm image, oftree and initrd by passing
 	  -o as an argument to the boot command.
 
+config BOOT_DEVBOOT
+	bool "Support devboot"
+	depends on BOOT_OVERRIDE && SHELL_HUSH
+	default y
+	help
+	  Add a -D option to the boot command and a global.boot.devboot
+	  variable, which take boot overrides and kernel command line
+	  fragments from a per-user, per-board script in the network
+	  fetch directory. See Documentation/user/devboot.rst.
+
 config BOOTM_SHOW_TYPE
 	bool
 	depends on BOOTM && UIMAGE
@@ -1243,7 +1253,7 @@ config DEFAULT_ENVIRONMENT_GENERIC_NEW_SECURITY_POLICY
 config DEFAULT_ENVIRONMENT_GENERIC_NEW_DEVBOOT
 	bool "Reusable devboot snippets under /env/devboot/"
 	depends on DEFAULT_ENVIRONMENT_GENERIC_NEW
-	depends on BOOT_OVERRIDE
+	depends on BOOT_DEVBOOT
 	help
 	  Extend the defaultenv template with snippets under /env/devboot/
 	  that a devboot script can source: defaults for the image, oftree
diff --git a/common/Makefile b/common/Makefile
index b1045c3acb..054a30a04b 100644
--- a/common/Makefile
+++ b/common/Makefile
@@ -31,6 +31,7 @@ obj-$(CONFIG_BLSPEC)		+= blspec.o
 obj-$(CONFIG_BOOTM)		+= bootm.o booti.o
 obj-$(CONFIG_BOOTM_AIMAGE)	+= bootm-android-image.o
 obj-$(CONFIG_BOOT_OVERRIDE)	+= bootm-overrides.o
+obj-$(CONFIG_BOOT_DEVBOOT)	+= devboot.o
 obj-$(CONFIG_CMD_LOADS)		+= s_record.o
 obj-$(CONFIG_MEMTEST)		+= memtest.o
 obj-$(CONFIG_COMMAND_SUPPORT)	+= command.o
diff --git a/common/boot.c b/common/boot.c
index 271cfd3a29..9919a9a8bc 100644
--- a/common/boot.c
+++ b/common/boot.c
@@ -353,7 +353,7 @@ static int bootscript_scan_path(struct bootentries *bootentries, const char *pat
 /*
  * Only the first character of a boot script argument is restricted, so
  * that other forms can be added later without changing the meaning of
- * existing targets, e.g. devboot@@mmc0@mmc1 or devboot@[some-condition].
+ * existing targets, e.g. SCRIPT@@ARG1@ARG2 or SCRIPT@[some-condition].
  * Paths and URLs are fine as arguments.
  */
 static bool is_valid_bootarg(const char *s)
@@ -366,8 +366,8 @@ static bool is_valid_bootarg(const char *s)
  *
  * name is either an absolute path or a name relative to /env/boot. A name
  * of the form SCRIPT@ARGUMENT refers to the boot script SCRIPT and has
- * ARGUMENT passed to it as first argument, e.g. devboot@system0 runs
- * /env/boot/devboot with argument system0. A name that exists as-is takes
+ * ARGUMENT passed to it as first argument, e.g. mmc@1 runs /env/boot/mmc
+ * with argument 1. A name that exists as-is takes
  * precedence, so scripts and directories with an @ in their name keep
  * working. An empty SCRIPT is rejected, so that @ARGUMENT doesn't expand
  * to every script in /env/boot.
@@ -552,7 +552,7 @@ static char *parse_nfs_url(const char *url)
  * name can be:
  * - a name of a boot script under /env/boot
  * - a name of a boot script under /env/boot followed by an @ and an
- *   argument to pass to the script, e.g. devboot@system0
+ *   argument to pass to the script, e.g. mmc@1
  * - a full path of a boot script
  * - a full path of a bootloader spec entry
  * - a device name
diff --git a/common/devboot.c b/common/devboot.c
new file mode 100644
index 0000000000..4880e2fb0f
--- /dev/null
+++ b/common/devboot.c
@@ -0,0 +1,197 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * devboot - boot with overrides configured by a script on the network
+ *
+ * See Documentation/user/devboot.rst
+ */
+
+#define pr_fmt(fmt) "devboot: " fmt
+
+#include <boot.h>
+#include <bootm-overrides.h>
+#include <devboot.h>
+#include <environment.h>
+#include <globalvar.h>
+#include <init.h>
+#include <libfile.h>
+#include <magicvar.h>
+#include <net.h>
+#include <shell.h>
+#include <stdio.h>
+#include <unistd.h>
+#include <linux/err.h>
+#include <linux/stat.h>
+
+struct devboot {
+	struct globalvar_scope *scope;
+	char *oldpwd;
+};
+
+static int devboot_default;
+static struct devboot *devboot_active;
+
+bool devboot_enabled(void)
+{
+	return devboot_default;
+}
+
+/*
+ * Find the devboot script in the current directory, preferring a
+ * board-specific over an architecture-specific one.
+ */
+static char *devboot_find_script(const char *fetchdir)
+{
+	const char *user = globalvar_get("user") ?: "";
+	char *board, *arch;
+
+	board = xasprintf("%s-devboot-%s", user, globalvar_get("hostname") ?: "");
+	if (file_exists_type(board, S_IFREG))
+		return board;
+
+	arch = xasprintf("%s-devboot-%s", user, globalvar_get("arch") ?: "");
+	if (file_exists_type(arch, S_IFREG)) {
+		free(board);
+		return arch;
+	}
+
+	pr_err("Neither %s/%s nor %s/%s found\n", fetchdir, board, fetchdir, arch);
+	pr_err("Create one of them and set as needed:\n");
+	pr_err("  devboot_image, devboot_initrd, devboot_oftree\n");
+
+	free(board);
+	free(arch);
+
+	return NULL;
+}
+
+/*
+ * Take over a variable set by the devboot script as override, unless it's
+ * already given on the command line. Unset variables are ignored, while
+ * empty ones are taken over, so that e.g. an empty devboot_oftree discards
+ * the boot entry's device tree.
+ */
+static void devboot_take_override(const char **override, const char *var)
+{
+	const char *val;
+
+	val = getenv(var);
+	if (val && !*override)
+		*override = xstrdup(val);
+}
+
+static int devboot_source(const char *fetchdir, struct bootm_overrides *overrides)
+{
+	char *script, *path;
+	char *argv[2] = {};
+	int ret;
+
+	script = devboot_find_script(fetchdir);
+	if (!script)
+		return -ENOENT;
+
+	path = xasprintf("%s/%s", fetchdir, script);
+	pr_info("Sourcing %s\n", path);
+
+	/* Local variables of the script vanish with the context */
+	env_push_context();
+
+	argv[0] = path;
+	ret = source_script(path, 1, argv);
+	if (ret) {
+		pr_err("%s failed with %d\n", path, ret);
+		ret = -EINVAL;
+	} else {
+		devboot_take_override(&overrides->os_file, "devboot_image");
+		devboot_take_override(&overrides->oftree_file, "devboot_oftree");
+		devboot_take_override(&overrides->initrd_file, "devboot_initrd");
+	}
+
+	env_pop_context();
+	free(path);
+	free(script);
+
+	return ret;
+}
+
+/**
+ * devboot_start - prepare a boot with devboot
+ * @overrides: overrides given on the command line, completed by the ones
+ *             the devboot script sets
+ *
+ * Brings up the network, changes into global.net.fetchdir and sources the
+ * devboot script found there. Changes the script makes to globalvars and
+ * the working directory persist until devboot_finish(), so relative paths
+ * in the overrides are resolved against the fetch directory.
+ *
+ * A boot started while devboot is active, e.g. by a boot script, doesn't
+ * source the script again. It inherits the overrides from the boot entry
+ * it is started from.
+ *
+ * Return: a handle for devboot_finish(), NULL if devboot was not started,
+ * or an error pointer
+ */
+struct devboot *devboot_start(struct bootm_overrides *overrides)
+{
+	struct devboot *db;
+	const char *fetchdir;
+	int ret;
+
+	if (devboot_active)
+		return NULL;
+
+	fetchdir = getenv_nonempty("global.net.fetchdir");
+	if (!fetchdir) {
+		pr_err("global.net.fetchdir is not set\n");
+		return ERR_PTR(-EINVAL);
+	}
+
+	db = xzalloc(sizeof(*db));
+	db->scope = boot_globalvar_scope_push();
+	db->oldpwd = xstrdup(getcwd());
+	devboot_active = db;
+
+	/* global.net.server and global.hostname may be set by DHCP */
+	if (IS_ENABLED(CONFIG_NET_IFUP))
+		ifup_all(IFUP_FLAG_PARALLEL | IFUP_FLAG_UNTIL_NET_SERVER);
+
+	ret = chdir(fetchdir);
+	if (ret) {
+		pr_err("Cannot change to %s: %pe\n", fetchdir, ERR_PTR(ret));
+		goto err;
+	}
+
+	ret = devboot_source(fetchdir, overrides);
+	if (ret)
+		goto err;
+
+	return db;
+err:
+	devboot_finish(db);
+	return ERR_PTR(ret);
+}
+
+/**
+ * devboot_finish - revert the changes of devboot_start()
+ * @db: handle returned by devboot_start(), may be NULL or an error pointer
+ */
+void devboot_finish(struct devboot *db)
+{
+	if (IS_ERR_OR_NULL(db))
+		return;
+
+	chdir(db->oldpwd);
+	globalvar_scope_pop(db->scope);
+	free(db->oldpwd);
+	free(db);
+
+	devboot_active = NULL;
+}
+
+static int devboot_init(void)
+{
+	return globalvar_add_simple_bool("boot.devboot", &devboot_default);
+}
+device_initcall(devboot_init);
+
+BAREBOX_MAGICVAR(global.boot.devboot,
+		 "Boot with devboot overrides by default, see boot -D");
diff --git a/defaultenv/defaultenv-2-base/bin/devboot b/defaultenv/defaultenv-2-base/bin/devboot
index 1a43246337..fee733247e 100644
--- a/defaultenv/defaultenv-2-base/bin/devboot
+++ b/defaultenv/defaultenv-2-base/bin/devboot
@@ -1,52 +1,4 @@
 #!/bin/sh
 
-devboot_path="${global.net.fetchdir}"
-
-# global.net.server and global.hostname may be set by DHCP, so trigger it first
-ifup -a1
-
-cd "${devboot_path}" || exit 1
-saved_pwd="$OLDPWD"
-
-devboot_script="${global.user}-devboot-${global.hostname}"
-if [ ! -f "${devboot_script}" ]; then
-	devboot_script2="${global.user}-devboot-${global.arch}"
-	if [ ! -f "${devboot_script2}" ]; then
-		echo "Error: Neither ${devboot_script} nor ${devboot_script2} found"
-		echo "Create one of them and set as needed:"
-		echo "  devboot_image, devboot_initrd, devboot_oftree"
-		cd "${saved_pwd}"
-		exit 1
-	fi
-	devboot_script="${devboot_script2}"
-fi
-
-echo Sourcing "${devboot_path}/${devboot_script}"
-source "${devboot_path}/${devboot_script}"
-if [ $? -ne 0 ]; then
-	cd "${saved_pwd}"
-	exit 1
-fi
-
-override_args=""
-
-if [[ -v devboot_image ]]; then
-	override_args="${override_args} -o bootm.image=\"${devboot_image}\""
-fi
-
-if [[ -v devboot_oftree ]]; then
-	override_args="${override_args} -o bootm.oftree=\"${devboot_oftree}\""
-fi
-
-if [[ -v devboot_initrd ]]; then
-	override_args="${override_args} -o bootm.initrd=\"${devboot_initrd}\""
-fi
-
-echo boot ${override_args} $*
-boot ${override_args} $*
-ret=$?
-
-global -r "linux.bootargs.devboot.*"
-cd "${saved_pwd}"
-
-exit $ret
+# hush only splits $* into separate words when the line contains ${}
+boot -D ${} $*
diff --git a/defaultenv/defaultenv-2-base/boot/devboot b/defaultenv/defaultenv-2-base/boot/devboot
deleted file mode 100644
index 4dd0626774..0000000000
--- a/defaultenv/defaultenv-2-base/boot/devboot
+++ /dev/null
@@ -1,8 +0,0 @@
-#!/bin/sh
-
-if [ $# = 0 ]; then
-	echo "usage: boot devboot@<target>, e.g. boot devboot@mmc"
-	exit 1
-fi
-
-devboot $*
diff --git a/include/devboot.h b/include/devboot.h
new file mode 100644
index 0000000000..c33638d83b
--- /dev/null
+++ b/include/devboot.h
@@ -0,0 +1,31 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef __DEVBOOT_H
+#define __DEVBOOT_H
+
+#include <linux/stddef.h>
+#include <linux/types.h>
+
+struct bootm_overrides;
+struct devboot;
+
+#ifdef CONFIG_BOOT_DEVBOOT
+bool devboot_enabled(void);
+struct devboot *devboot_start(struct bootm_overrides *overrides);
+void devboot_finish(struct devboot *db);
+#else
+static inline bool devboot_enabled(void)
+{
+	return false;
+}
+
+static inline struct devboot *devboot_start(struct bootm_overrides *overrides)
+{
+	return NULL;
+}
+
+static inline void devboot_finish(struct devboot *db)
+{
+}
+#endif
+
+#endif
diff --git a/test/py/test_devboot.py b/test/py/test_devboot.py
new file mode 100644
index 0000000000..f533d7585b
--- /dev/null
+++ b/test/py/test_devboot.py
@@ -0,0 +1,131 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+import pytest
+from .helper import skip_disabled
+from .test_boot_globalvar_scope import write_bootscript, globals_now
+from .test_boot_overrides import make_image, booted
+
+FETCHDIR = "/tmp/devboot"
+
+
+@pytest.fixture(scope="function")
+def devboot_env(barebox, barebox_config):
+    skip_disabled(barebox_config, "CONFIG_BOOT_DEVBOOT")
+
+    saved = {var: barebox.run_check(f"echo ${{global.{var}}}")[0]
+             for var in ("user", "hostname", "net.fetchdir", "boot.devboot",
+                         "bootm.appendroot")}
+    pwd = barebox.run_check("pwd")[0]
+
+    barebox.run(f"mkdir {FETCHDIR}")
+    barebox.run_check(f"global.net.fetchdir={FETCHDIR}")
+    barebox.run_check("global.user=scope")
+    barebox.run_check("global.hostname=test")
+    barebox.run_check("global.bootm.appendroot=0")
+    barebox.run_check(f"echo -o {FETCHDIR}/scope-devboot-test "
+                      "'global linux.bootargs.extra=extra=1'")
+    barebox.run_check(f"echo -a {FETCHDIR}/scope-devboot-test "
+                      "'global.bootm.appendroot=1'")
+    barebox.run_check(f"echo -a {FETCHDIR}/scope-devboot-test "
+                      "'devboot_local=leak'")
+    write_bootscript(barebox, "devboot-target", [
+        "global.bootm.image=/does/not/exist",
+        "echo target-sees ${global.linux.bootargs.extra} "
+        "${global.bootm.appendroot}",
+    ])
+    write_bootscript(barebox, "devboot-nested", [
+        "boot devboot-target",
+    ])
+
+    yield pwd
+
+    for var, val in saved.items():
+        barebox.run_check(f"global.{var}=\"{val}\"")
+    barebox.run(f"rm -r {FETCHDIR}")
+    barebox.run("rm /env/boot/devboot-target /env/boot/devboot-nested")
+
+
+def assert_reverted(barebox, pwd):
+    after = globals_now(barebox)
+    assert "linux.bootargs.extra" not in after
+    assert after["bootm.appendroot"] == "0"
+    assert barebox.run_check("pwd") == [pwd]
+    assert barebox.run_check("echo [$devboot_local]") == ["[]"]
+
+
+def sourced(stdout):
+    return sum("devboot: Sourcing " in line for line in stdout)
+
+
+def test_devboot_option(barebox, devboot_env):
+    stdout, _, ret = barebox.run("boot -D devboot-target devboot-target")
+    assert ret != 0
+    assert sourced(stdout) == 1
+    # the devboot script's variables are visible in, and survive, each
+    # boot target...
+    assert stdout.count("target-sees extra=1 1") == 2
+    # ...and are reverted afterwards
+    assert_reverted(barebox, devboot_env)
+
+
+def test_devboot_globalvar(barebox, devboot_env):
+    barebox.run_check("global.boot.devboot=1")
+
+    stdout, _, _ = barebox.run("boot devboot-target")
+    assert sourced(stdout) == 1
+    assert "target-sees extra=1 1" in stdout
+    assert_reverted(barebox, devboot_env)
+
+
+def test_devboot_command(barebox, devboot_env):
+    stdout, _, _ = barebox.run("devboot devboot-target devboot-target")
+    assert sourced(stdout) == 1
+    assert stdout.count("target-sees extra=1 1") == 2
+    assert_reverted(barebox, devboot_env)
+
+
+def test_devboot_nested(barebox, devboot_env):
+    # a boot script booting other entries doesn't source the script again
+    stdout, _, _ = barebox.run("boot -D devboot-nested")
+    assert sourced(stdout) == 1
+    assert "target-sees extra=1 1" in stdout
+    assert_reverted(barebox, devboot_env)
+
+
+def test_devboot_nested_override(barebox, barebox_config, devboot_env):
+    skip_disabled(barebox_config, "CONFIG_CMD_CAT", "CONFIG_CMD_MW")
+
+    base = "/tmp/devboot-base"
+    make_image(barebox, base)
+    make_image(barebox, f"{FETCHDIR}/devboot-override")
+    barebox.run_check(f"echo -a {FETCHDIR}/scope-devboot-test "
+                      "devboot_image=devboot-override")
+    write_bootscript(barebox, "devboot-target", [
+        f"global.bootm.image={base}",
+    ])
+
+    stdout, _, _ = barebox.run("boot -D devboot-nested")
+    barebox.run_check(f"rm {base}")
+
+    assert sourced(stdout) == 1
+    # relative to global.net.fetchdir, which is the working directory
+    assert any("'devboot-override'" in line for line in booted(stdout))
+    assert_reverted(barebox, devboot_env)
+
+
+def test_devboot_dryrun(barebox, devboot_env):
+    stdout, _, ret = barebox.run("boot -D -d devboot-target")
+    assert ret == 0
+    assert sourced(stdout) == 1
+    assert "Would run /env/boot/devboot-target" in stdout
+    assert_reverted(barebox, devboot_env)
+
+
+def test_devboot_missing_script(barebox, devboot_env):
+    barebox.run_check("global.hostname=nohost")
+
+    stdout, _, ret = barebox.run("boot -D devboot-target")
+    assert ret != 0
+    assert any("Neither" in line for line in stdout)
+    assert not any("Booting entry" in line for line in stdout)
+    assert_reverted(barebox, devboot_env)

-- 
2.47.3




^ permalink raw reply	[flat|nested] 6+ messages in thread

* [PATCH 5/5] sandbox: defconfig: enable boot overrides and devboot
  2026-10-30 14:13 [PATCH 0/5] boot: implement devboot in C Sascha Hauer
                   ` (3 preceding siblings ...)
  2026-10-30 14:13 ` [PATCH 4/5] boot: implement devboot in C Sascha Hauer
@ 2026-10-30 14:13 ` Sascha Hauer
  4 siblings, 0 replies; 6+ messages in thread
From: Sascha Hauer @ 2026-10-30 14:13 UTC (permalink / raw)
  To: BAREBOX

Enable boot overrides and with it devboot along with the devboot
snippets, so that the devboot tests run on sandbox as well.

Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Sascha Hauer <s.hauer@pengutronix.de>
---
 arch/sandbox/configs/sandbox_defconfig | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/arch/sandbox/configs/sandbox_defconfig b/arch/sandbox/configs/sandbox_defconfig
index 5b2cfdd515..46f1270178 100644
--- a/arch/sandbox/configs/sandbox_defconfig
+++ b/arch/sandbox/configs/sandbox_defconfig
@@ -4,12 +4,14 @@ CONFIG_HUSH_FANCY_PROMPT=y
 CONFIG_CMDLINE_EDITING=y
 CONFIG_AUTO_COMPLETE=y
 CONFIG_MENU=y
+CONFIG_BOOT_OVERRIDE=y
 CONFIG_CONSOLE_ACTIVATE_ALL=y
 CONFIG_CONSOLE_ALLOW_COLOR=y
 CONFIG_PARTITION_DISK_EFI=y
 CONFIG_DEFAULT_COMPRESSION_GZIP=y
 CONFIG_DEFAULT_ENVIRONMENT_GENERIC_NEW=y
 CONFIG_DEFAULT_ENVIRONMENT_GENERIC_NEW_REBOOT_MODE=y
+CONFIG_DEFAULT_ENVIRONMENT_GENERIC_NEW_DEVBOOT=y
 CONFIG_DEFAULT_ENVIRONMENT_PATH=""
 CONFIG_STATE=y
 CONFIG_STATE_CRYPTO=y

-- 
2.47.3




^ permalink raw reply	[flat|nested] 6+ messages in thread

end of thread, other threads:[~2026-10-30 15:19 UTC | newest]

Thread overview: 6+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-10-30 14:13 [PATCH 0/5] boot: implement devboot in C Sascha Hauer
2026-10-30 14:13 ` [PATCH 1/5] boot: report the exit status of failing boot scripts Sascha Hauer
2026-10-30 14:13 ` [PATCH 2/5] hush: export source_script() Sascha Hauer
2026-10-30 14:13 ` [PATCH 3/5] boot: hand overrides on to nested boots and the bootm command Sascha Hauer
2026-10-30 14:13 ` [PATCH 4/5] boot: implement devboot in C Sascha Hauer
2026-10-30 14:13 ` [PATCH 5/5] sandbox: defconfig: enable boot overrides and devboot Sascha Hauer

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox