[PATCH 00/18] ulib: Complete initial U-Boot library
From: Simon Glass <sjg@chromium.org> This series completes support for building U-Boot as a shared or static library, enabling reuse of U-Boot functionality in external programs and test suites. The U-Boot library (ulib) allows developers to: - Link against U-Boot functionality without a full U-Boot image - Use U-Boot's OS abstraction layer, drivers, and utility functions - Build test programs that can exercise U-Boot code in isolation - Create applications that benefit from U-Boot's hardware support Key features: - Builds both shared (libu-boot.so) and static (libu-boot.a) libraries - Preserves U-Boot linker lists for proper driver/subsystem init - Configurable symbol renaming to avoid conflicts with system libraries - Generated API headers with renamed function declarations - Documentation and working examples - Currently only supports sandbox architecture The series includes: - More build-infrastructure and Makefile integration - Python-based mechanism for symbol renaming and API generation - Test programs demonstrating basic library usage - A simple example program showing real-world usage patterns Symbol renaming ensures that U-Boot functions don't conflict with system libraries. For example, printf() remains the standard library function while ub_printf() provides access to U-Boot's printf implementation. This is handled automatically during the build process. The library excludes main() to allow external programs to provide their own entry points while still accessing U-Boot functionality through ulib_init() and ulib_uninit(). For example: #include <u-boot-lib.h> #include <u-boot-api.h> int main(int argc, char *argv[]) { if (ulib_init(argv[0]) < 0) return 1; ub_printf("Hello from U-Boot library!\n"); ulib_uninit(); return 0; } License implications are documented - the GPL-2.0+ license applies to any programs linked with the library, requiring source code distribution for compliant usage. Future work will look at expanding support to other architectures. Simon Glass (18): lib: Tidy up comments for vsprintf functions u_boot_pylib: Correct docs for run_test_coverage() required os.h: Add standard includes for types used in os.h sandbox: Enable ULIB just for the sandbox build test: Move the non-LTO test to sandbox_flattree test/py: Allow setting the cwd with run_and_log() ulib: Move ulib into its own directory ulib: scripts: Add a script to support symbol-renaming ulib: Use the correct copyright message ulib: Disable LTO when building the library ulib: Provide a symbol-rename file ulib: Makefile: Create a library with renamed symbols ulib: Makefile: Plumb in renaming symbols for ulib ulib: Makefile: Plumb in creation of the API header ulib: Expand the ulib example to have two files ulib: Adjust the test to check symbol renaming ulib: doc: Expand the documentation to cover new features test/py: Add a test for ulib functionality Kconfig | 3 +- Makefile | 54 ++- arch/sandbox/cpu/u-boot-lib.lds | 2 +- configs/sandbox_defconfig | 1 + doc/develop/ulib.rst | 179 +++++++- examples/ulib/.gitignore | 2 + examples/ulib/Makefile | 60 ++- examples/ulib/demo.c | 14 +- examples/ulib/demo_helper.c | 28 ++ examples/ulib/demo_helper.h | 31 ++ include/os.h | 2 + include/u-boot-lib.h | 2 +- include/vsprintf.h | 25 +- lib/Makefile | 2 +- lib/ulib/Makefile | 6 + lib/ulib/rename.syms | 30 ++ lib/{ => ulib}/ulib.c | 2 +- scripts/build_api.py | 714 +++++++++++++++++++++++++++++ test/py/tests/test_sandbox_opts.py | 8 +- test/py/tests/test_ulib.py | 141 ++++++ test/py/utils.py | 13 +- test/run | 8 + test/scripts/test_build_api.py | 704 ++++++++++++++++++++++++++++ test/ulib/ulib_test.c | 12 +- tools/u_boot_pylib/test_util.py | 2 +- 25 files changed, 1970 insertions(+), 75 deletions(-) create mode 100644 examples/ulib/.gitignore create mode 100644 examples/ulib/demo_helper.c create mode 100644 examples/ulib/demo_helper.h create mode 100644 lib/ulib/Makefile create mode 100644 lib/ulib/rename.syms rename lib/{ => ulib}/ulib.c (95%) create mode 100755 scripts/build_api.py create mode 100644 test/py/tests/test_ulib.py create mode 100644 test/scripts/test_build_api.py -- 2.43.0 base-commit: 75c3ab4cd8407e7ede9b61be050d4b88f75f7360 branch: ulif
From: Simon Glass <sjg@chromium.org> Some of the functions in this file do not follow the normal style. Fix this so that things are more consistent. Signed-off-by: Simon Glass <sjg@chromium.org> --- include/vsprintf.h | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/include/vsprintf.h b/include/vsprintf.h index 9da6ce7cc4d..3cb335515e4 100644 --- a/include/vsprintf.h +++ b/include/vsprintf.h @@ -11,7 +11,7 @@ #include <linux/types.h> /** - * simple_strtoul - convert a string to an unsigned long + * simple_strtoul() - convert a string to an unsigned long * * @cp: The string to be converted * @endp: Updated to point to the first character not converted @@ -32,7 +32,7 @@ ulong simple_strtoul(const char *cp, char **endp, unsigned int base); /** - * hex_strtoul - convert a string in hex to an unsigned long + * hex_strtoul() - convert a string in hex to an unsigned long * * @cp: The string to be converted * @endp: Updated to point to the first character not converted @@ -45,7 +45,7 @@ ulong simple_strtoul(const char *cp, char **endp, unsigned int base); unsigned long hextoul(const char *cp, char **endp); /** - * hex_strtoull - convert a string in hex to an unsigned long long + * hex_strtoull() - convert a string in hex to an unsigned long long * * @cp: The string to be converted * @endp: Updated to point to the first character not converted @@ -58,7 +58,7 @@ unsigned long hextoul(const char *cp, char **endp); unsigned long long hextoull(const char *cp, char **endp); /** - * dec_strtoul - convert a string in decimal to an unsigned long + * dec_strtoul() - convert a string in decimal to an unsigned long * * @cp: The string to be converted * @endp: Updated to point to the first character not converted @@ -71,7 +71,7 @@ unsigned long long hextoull(const char *cp, char **endp); unsigned long dectoul(const char *cp, char **endp); /** - * strict_strtoul - convert a string to an unsigned long strictly + * strict_strtoul() - convert a string to an unsigned long strictly * @cp: The string to be converted * @base: The number base to use (0 for the default) * @res: The converted result value @@ -100,6 +100,7 @@ unsigned long dectoul(const char *cp, char **endp); * */ int strict_strtoul(const char *cp, unsigned int base, unsigned long *res); + unsigned long long simple_strtoull(const char *cp, char **endp, unsigned int base); long simple_strtol(const char *cp, char **endp, unsigned int base); @@ -178,7 +179,7 @@ void panic(const char *fmt, ...) void panic_str(const char *str) __attribute__ ((noreturn)); /** - * Format a string and place it in a buffer + * sprintf() - Format a string and place it in a buffer * * @buf: The buffer to place the result into * @fmt: The format string to use @@ -193,7 +194,7 @@ int sprintf(char *buf, const char *fmt, ...) __attribute__ ((format (__printf__, 2, 3))); /** - * Format a string and place it in a buffer (va_list version) + * vsprintf() - Format a string and place it in a buffer (va_list version) * * @buf: The buffer to place the result into * @fmt: The format string to use @@ -232,7 +233,7 @@ char *simple_itoa(ulong val); char *simple_xtoa(ulong num); /** - * Format a string and place it in a buffer + * scnprintf() - Format a string and place it in a buffer * * @buf: The buffer to place the result into * @size: The size of the buffer, including the trailing null space @@ -248,7 +249,7 @@ int scnprintf(char *buf, size_t size, const char *fmt, ...) __attribute__ ((format (__printf__, 3, 4))); /** - * Format a string and place it in a buffer (base function) + * vsnprintf() - Format a string and place it in a buffer (base function) * * @buf: The buffer to place the result into * @size: The size of the buffer, including the trailing null space @@ -273,7 +274,7 @@ int scnprintf(char *buf, size_t size, const char *fmt, ...) int vsnprintf(char *buf, size_t size, const char *fmt, va_list args); /** - * Format a string and place it in a buffer (va_list version) + * vscnprintf() - Format a string and place it in a buffer (va_list version) * * @buf: The buffer to place the result into * @size: The size of the buffer, including the trailing null space @@ -349,7 +350,7 @@ const char **str_to_list(const char *instr); void str_free_list(const char **ptr); /** - * vsscanf - Unformat a buffer into a list of arguments + * vsscanf() - Unformat a buffer into a list of arguments * @inp: input buffer * @fmt0: format of buffer * @ap: arguments @@ -357,7 +358,7 @@ void str_free_list(const char **ptr); int vsscanf(const char *inp, char const *fmt0, va_list ap); /** - * sscanf - Unformat a buffer into a list of arguments + * sscanf() - Unformat a buffer into a list of arguments * @buf: input buffer * @fmt: formatting of buffer * @...: resulting arguments -- 2.43.0
On 9/9/25 17:17, Simon Glass wrote:
From: Simon Glass <sjg@chromium.org>
Some of the functions in this file do not follow the normal style. Fix this so that things are more consistent.
Signed-off-by: Simon Glass <sjg@chromium.org>
Reviewed-by: Heinrich Schuchardt <xypron.glpk@gmx.de>
---
include/vsprintf.h | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-)
diff --git a/include/vsprintf.h b/include/vsprintf.h index 9da6ce7cc4d..3cb335515e4 100644 --- a/include/vsprintf.h +++ b/include/vsprintf.h @@ -11,7 +11,7 @@ #include <linux/types.h>
/** - * simple_strtoul - convert a string to an unsigned long + * simple_strtoul() - convert a string to an unsigned long * * @cp: The string to be converted * @endp: Updated to point to the first character not converted @@ -32,7 +32,7 @@ ulong simple_strtoul(const char *cp, char **endp, unsigned int base);
/** - * hex_strtoul - convert a string in hex to an unsigned long + * hex_strtoul() - convert a string in hex to an unsigned long * * @cp: The string to be converted * @endp: Updated to point to the first character not converted @@ -45,7 +45,7 @@ ulong simple_strtoul(const char *cp, char **endp, unsigned int base); unsigned long hextoul(const char *cp, char **endp);
/** - * hex_strtoull - convert a string in hex to an unsigned long long + * hex_strtoull() - convert a string in hex to an unsigned long long * * @cp: The string to be converted * @endp: Updated to point to the first character not converted @@ -58,7 +58,7 @@ unsigned long hextoul(const char *cp, char **endp); unsigned long long hextoull(const char *cp, char **endp);
/** - * dec_strtoul - convert a string in decimal to an unsigned long + * dec_strtoul() - convert a string in decimal to an unsigned long * * @cp: The string to be converted * @endp: Updated to point to the first character not converted @@ -71,7 +71,7 @@ unsigned long long hextoull(const char *cp, char **endp); unsigned long dectoul(const char *cp, char **endp);
/** - * strict_strtoul - convert a string to an unsigned long strictly + * strict_strtoul() - convert a string to an unsigned long strictly * @cp: The string to be converted * @base: The number base to use (0 for the default) * @res: The converted result value @@ -100,6 +100,7 @@ unsigned long dectoul(const char *cp, char **endp); * */ int strict_strtoul(const char *cp, unsigned int base, unsigned long *res); + unsigned long long simple_strtoull(const char *cp, char **endp, unsigned int base); long simple_strtol(const char *cp, char **endp, unsigned int base); @@ -178,7 +179,7 @@ void panic(const char *fmt, ...) void panic_str(const char *str) __attribute__ ((noreturn));
/** - * Format a string and place it in a buffer + * sprintf() - Format a string and place it in a buffer * * @buf: The buffer to place the result into * @fmt: The format string to use @@ -193,7 +194,7 @@ int sprintf(char *buf, const char *fmt, ...) __attribute__ ((format (__printf__, 2, 3)));
/** - * Format a string and place it in a buffer (va_list version) + * vsprintf() - Format a string and place it in a buffer (va_list version) * * @buf: The buffer to place the result into * @fmt: The format string to use @@ -232,7 +233,7 @@ char *simple_itoa(ulong val); char *simple_xtoa(ulong num);
/** - * Format a string and place it in a buffer + * scnprintf() - Format a string and place it in a buffer * * @buf: The buffer to place the result into * @size: The size of the buffer, including the trailing null space @@ -248,7 +249,7 @@ int scnprintf(char *buf, size_t size, const char *fmt, ...) __attribute__ ((format (__printf__, 3, 4)));
/** - * Format a string and place it in a buffer (base function) + * vsnprintf() - Format a string and place it in a buffer (base function) * * @buf: The buffer to place the result into * @size: The size of the buffer, including the trailing null space @@ -273,7 +274,7 @@ int scnprintf(char *buf, size_t size, const char *fmt, ...) int vsnprintf(char *buf, size_t size, const char *fmt, va_list args);
/** - * Format a string and place it in a buffer (va_list version) + * vscnprintf() - Format a string and place it in a buffer (va_list version) * * @buf: The buffer to place the result into * @size: The size of the buffer, including the trailing null space @@ -349,7 +350,7 @@ const char **str_to_list(const char *instr); void str_free_list(const char **ptr);
/** - * vsscanf - Unformat a buffer into a list of arguments + * vsscanf() - Unformat a buffer into a list of arguments * @inp: input buffer * @fmt0: format of buffer * @ap: arguments @@ -357,7 +358,7 @@ void str_free_list(const char **ptr); int vsscanf(const char *inp, char const *fmt0, va_list ap);
/** - * sscanf - Unformat a buffer into a list of arguments + * sscanf() - Unformat a buffer into a list of arguments * @buf: input buffer * @fmt: formatting of buffer * @...: resulting arguments
From: Simon Glass <sjg@chromium.org> This should be a set, not a list. Fix it. Signed-off-by: Simon Glass <sjg@chromium.org> --- tools/u_boot_pylib/test_util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/u_boot_pylib/test_util.py b/tools/u_boot_pylib/test_util.py index d258a1935c9..7bd12705557 100644 --- a/tools/u_boot_pylib/test_util.py +++ b/tools/u_boot_pylib/test_util.py @@ -36,7 +36,7 @@ def run_test_coverage(prog, filter_fname, exclude_list, build_dir, exclude_list: List of file patterns to exclude from the coverage calculation build_dir: Build directory, used to locate libfdt.py - required: List of modules which must be in the coverage report + required: Set of modules which must be in the coverage report extra_args (str): Extra arguments to pass to the tool before the -t/test arg single_thread (str): Argument string to make the tests run -- 2.43.0
On 9/9/25 17:17, Simon Glass wrote:
From: Simon Glass <sjg@chromium.org>
This should be a set, not a list. Fix it.
Signed-off-by: Simon Glass <sjg@chromium.org> ---
tools/u_boot_pylib/test_util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tools/u_boot_pylib/test_util.py b/tools/u_boot_pylib/test_util.py index d258a1935c9..7bd12705557 100644 --- a/tools/u_boot_pylib/test_util.py +++ b/tools/u_boot_pylib/test_util.py @@ -36,7 +36,7 @@ def run_test_coverage(prog, filter_fname, exclude_list, build_dir, exclude_list: List of file patterns to exclude from the coverage calculation build_dir: Build directory, used to locate libfdt.py - required: List of modules which must be in the coverage report + required: Set of modules which must be in the coverage report
This change looks inconsistent with the usage in the code: line 81: missing_list = required Required cannot be both a list and a set. Best regards Heinrich
extra_args (str): Extra arguments to pass to the tool before the -t/test arg single_thread (str): Argument string to make the tests run
Hi Heinrich, On Wed, 10 Sept 2025 at 01:21, Heinrich Schuchardt <xypron.glpk@gmx.de> wrote:
On 9/9/25 17:17, Simon Glass wrote:
From: Simon Glass <sjg@chromium.org>
This should be a set, not a list. Fix it.
Signed-off-by: Simon Glass <sjg@chromium.org> ---
tools/u_boot_pylib/test_util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tools/u_boot_pylib/test_util.py b/tools/u_boot_pylib/test_util.py index d258a1935c9..7bd12705557 100644 --- a/tools/u_boot_pylib/test_util.py +++ b/tools/u_boot_pylib/test_util.py @@ -36,7 +36,7 @@ def run_test_coverage(prog, filter_fname, exclude_list, build_dir, exclude_list: List of file patterns to exclude from the coverage calculation build_dir: Build directory, used to locate libfdt.py - required: List of modules which must be in the coverage report + required: Set of modules which must be in the coverage report
This change looks inconsistent with the usage in the code:
line 81: missing_list = required
Required cannot be both a list and a set.
Ah yes, that should really be renamed. I'll send a patch.
Best regards
Heinrich
extra_args (str): Extra arguments to pass to the tool before the -t/test arg single_thread (str): Argument string to make the tests run
Regards, SImon
From: Simon Glass <sjg@chromium.org> Improve the compatibility of this file by including standard headers for bool and uint64_t types. Signed-off-by: Simon Glass <sjg@chromium.org> --- include/os.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/include/os.h b/include/os.h index dfd6e3c7692..bc4c9073cff 100644 --- a/include/os.h +++ b/include/os.h @@ -11,6 +11,8 @@ #ifndef __OS_H__ #define __OS_H__ +#include <stdbool.h> +#include <inttypes.h> #include <linux/types.h> struct rtc_time; -- 2.43.0
From: Simon Glass <sjg@chromium.org> It doesn't really make sense to use ulib in anything other than the main sandbox build, since it has the most features enabled. Move the setting into defconfig so that other boards don't enable it. Signed-off-by: Simon Glass <sjg@chromium.org> --- Kconfig | 1 - configs/sandbox_defconfig | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/Kconfig b/Kconfig index 76cfa74dc8b..c110c9f2a6e 100644 --- a/Kconfig +++ b/Kconfig @@ -100,7 +100,6 @@ endchoice config ULIB bool "Build U-Boot as a library" - default y if SANDBOX help Enable this to build a library which can be linked to other programs, to extend U-Boot's functionality. diff --git a/configs/sandbox_defconfig b/configs/sandbox_defconfig index d10b029f540..614947f0213 100644 --- a/configs/sandbox_defconfig +++ b/configs/sandbox_defconfig @@ -11,6 +11,7 @@ CONFIG_PCI=y CONFIG_DEBUG_UART=y CONFIG_SYS_MEMTEST_START=0x00100000 CONFIG_SYS_MEMTEST_END=0x00101000 +CONFIG_ULIB=y CONFIG_EXAMPLES=y CONFIG_EFI_SECURE_BOOT=y CONFIG_EFI_RT_VOLATILE_STORE=y -- 2.43.0
From: Simon Glass <sjg@chromium.org> The sandbox build is about to be used for ulib, so will not support LTO. Use the sandbox_flatree build to check disabling LTO. There is limited value in this, since sandbox will already check building without LTO. But it seems reasonable to keep the test working, making sure that the same board can build with and without LTO. Signed-off-by: Simon Glass <sjg@chromium.org> --- test/py/tests/test_sandbox_opts.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/py/tests/test_sandbox_opts.py b/test/py/tests/test_sandbox_opts.py index 48f5b313870..43569a1b22e 100644 --- a/test/py/tests/test_sandbox_opts.py +++ b/test/py/tests/test_sandbox_opts.py @@ -19,10 +19,10 @@ def test_sandbox_cmdline(ubman): '-a', '~CMDLINE', '-o', TMPDIR]) @pytest.mark.slow -@pytest.mark.boardspec('sandbox') +@pytest.mark.boardspec('sandbox_flattree') def test_sandbox_lto(ubman): - """Test building sandbox without CONFIG_LTO""" + """Test building sandbox_flattree without CONFIG_LTO""" utils.run_and_log( - ubman, ['./tools/buildman/buildman', '-m', '--board', 'sandbox', - '-a', '~LTO', '-o', TMPDIR]) + ubman, ['./tools/buildman/buildman', '-m', '--board', + 'sandbox_flattree', '-a', '~LTO', '-o', TMPDIR]) -- 2.43.0
From: Simon Glass <sjg@chromium.org> Sometimes it is useful to run a command in a particular subdirectory. Add support for this to the run_and_log() functions. Signed-off-by: Simon Glass <sjg@chromium.org> --- test/py/utils.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/test/py/utils.py b/test/py/utils.py index 05e2a16d3b4..7812b2a201b 100644 --- a/test/py/utils.py +++ b/test/py/utils.py @@ -157,7 +157,8 @@ def wait_until_file_open_fails(fn, ignore_errors): return raise Exception('File can still be opened') -def run_and_log_no_ubman(log, cmd, ignore_errors=False, stdin=None, env=None): +def run_and_log_no_ubman(log, cmd, ignore_errors=False, stdin=None, env=None, + cwd=None): """Run a command and log its output. This is useful when you don't want to use a ubman fixture @@ -173,6 +174,7 @@ def run_and_log_no_ubman(log, cmd, ignore_errors=False, stdin=None, env=None): such problems occur. stdin (str): Input string to pass to the command as stdin (or None) env (dict): Environment to use, or None to use the current one + cwd (str): directory to run the command in, or None for current dir Returns: The output as a string. @@ -180,11 +182,13 @@ def run_and_log_no_ubman(log, cmd, ignore_errors=False, stdin=None, env=None): if isinstance(cmd, str): cmd = cmd.split() runner = log.get_runner(cmd[0], sys.stdout) - output = runner.run(cmd, ignore_errors=ignore_errors, stdin=stdin, env=env) + output = runner.run(cmd, ignore_errors=ignore_errors, stdin=stdin, env=env, + cwd=cwd) runner.close() return output -def run_and_log(ubman, cmd, ignore_errors=False, stdin=None, env=None): +def run_and_log(ubman, cmd, ignore_errors=False, stdin=None, env=None, + cwd=None): """Run a command and log its output. Args: @@ -198,11 +202,12 @@ def run_and_log(ubman, cmd, ignore_errors=False, stdin=None, env=None): such problems occur. stdin (str): Input string to pass to the command as stdin (or None) env (dict): Environment to use, or None to use the current one + cwd (str): directory to run the command in, or None for current dir Returns: The output as a string. """ - return run_and_log_no_ubman(ubman.log, cmd, ignore_errors, stdin, env) + return run_and_log_no_ubman(ubman.log, cmd, ignore_errors, stdin, env, cwd) def run_and_log_expect_exception(ubman, cmd, retcode, msg): """Run a command that is expected to fail. -- 2.43.0
From: Simon Glass <sjg@chromium.org> Before adding more files, create a new lib/ulib directory and put the only existing file in there. Signed-off-by: Simon Glass <sjg@chromium.org> --- lib/Makefile | 2 +- lib/ulib/Makefile | 6 ++++++ lib/{ => ulib}/ulib.c | 0 3 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 lib/ulib/Makefile rename lib/{ => ulib}/ulib.c (100%) diff --git a/lib/Makefile b/lib/Makefile index eb6da6d63c9..fd18002181d 100644 --- a/lib/Makefile +++ b/lib/Makefile @@ -166,7 +166,7 @@ obj-$(CONFIG_LIB_ELF) += elf.o obj-$(CONFIG_$(PHASE_)SEMIHOSTING) += semihosting.o -obj-$(CONFIG_ULIB) += ulib.o +obj-$(CONFIG_ULIB) += ulib/ # # Build a fast OID lookup registry from include/linux/oid_registry.h diff --git a/lib/ulib/Makefile b/lib/ulib/Makefile new file mode 100644 index 00000000000..baa45053d69 --- /dev/null +++ b/lib/ulib/Makefile @@ -0,0 +1,6 @@ +# SPDX-License-Identifier: GPL-2.0+ + +# Copyright 2025 Canonical +# Written by Simon Glass <simon.glass@canonical.com> + +obj-y += ulib.o diff --git a/lib/ulib.c b/lib/ulib/ulib.c similarity index 100% rename from lib/ulib.c rename to lib/ulib/ulib.c -- 2.43.0
From: Simon Glass <sjg@chromium.org> Canonical should be written with the 'Ltd.' suffix, so add this to a few files that need it. Signed-off-by: Simon Glass <sjg@chromium.org> --- arch/sandbox/cpu/u-boot-lib.lds | 2 +- examples/ulib/Makefile | 2 +- examples/ulib/demo.c | 2 +- include/u-boot-lib.h | 2 +- lib/ulib/Makefile | 2 +- lib/ulib/ulib.c | 2 +- test/ulib/ulib_test.c | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/arch/sandbox/cpu/u-boot-lib.lds b/arch/sandbox/cpu/u-boot-lib.lds index f57292b0a8d..f5434780ad6 100644 --- a/arch/sandbox/cpu/u-boot-lib.lds +++ b/arch/sandbox/cpu/u-boot-lib.lds @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: GPL-2.0+ */ /* - * Copyright 2025 Canonical + * Copyright 2025 Canonical Ltd. * Written by Simon Glass <simon.glass@canonical.com> * * Linker script for U-Boot shared library (libu-boot.so) diff --git a/examples/ulib/Makefile b/examples/ulib/Makefile index 68862cc09f8..f70333a3b9b 100644 --- a/examples/ulib/Makefile +++ b/examples/ulib/Makefile @@ -2,7 +2,7 @@ # # Standalone Makefile for U-Boot library examples # -# Copyright 2025 Canonical +# Copyright 2025 Canonical Ltd. # Written by Simon Glass <simon.glass@canonical.com> # This Makefile can be used to build the examples. See doc/develop/ulib.rst diff --git a/examples/ulib/demo.c b/examples/ulib/demo.c index 4b12e91b17e..2a9f7720beb 100644 --- a/examples/ulib/demo.c +++ b/examples/ulib/demo.c @@ -5,7 +5,7 @@ * This demonstrates using U-Boot library functions in sandbox like os_* * from external programs. * - * Copyright 2025 Canonical + * Copyright 2025 Canonical Ltd. * Written by Simon Glass <simon.glass@canonical.com> */ diff --git a/include/u-boot-lib.h b/include/u-boot-lib.h index 7157ef6ba60..aabc77aa4eb 100644 --- a/include/u-boot-lib.h +++ b/include/u-boot-lib.h @@ -6,7 +6,7 @@ * * Library functions must be individually accessed via their respective headers. * - * Copyright 2025 Canonical + * Copyright 2025 Canonical Ltd. * Written by Simon Glass <simon.glass@canonical.com> */ diff --git a/lib/ulib/Makefile b/lib/ulib/Makefile index baa45053d69..3b94780544b 100644 --- a/lib/ulib/Makefile +++ b/lib/ulib/Makefile @@ -1,6 +1,6 @@ # SPDX-License-Identifier: GPL-2.0+ -# Copyright 2025 Canonical +# Copyright 2025 Canonical Ltd. # Written by Simon Glass <simon.glass@canonical.com> obj-y += ulib.o diff --git a/lib/ulib/ulib.c b/lib/ulib/ulib.c index 03acbe93fd1..385faa60844 100644 --- a/lib/ulib/ulib.c +++ b/lib/ulib/ulib.c @@ -2,7 +2,7 @@ /* * Simplified U-Boot library interface implementation * - * Copyright 2025 Canonical + * Copyright 2025 Canonical Ltd. * Written by Simon Glass <simon.glass@canonical.com> */ diff --git a/test/ulib/ulib_test.c b/test/ulib/ulib_test.c index 51b1c62e6b2..5daf949a74c 100644 --- a/test/ulib/ulib_test.c +++ b/test/ulib/ulib_test.c @@ -4,7 +4,7 @@ * * This demonstrates linking against libu-boot.so and libu-boot.a * - * Copyright 2025 Canonical + * Copyright 2025 Canonical Ltd. * Written by Simon Glass <simon.glass@canonical.com> */ -- 2.43.0
From: Simon Glass <sjg@chromium.org> The library uses symbol renaming, but this is not supported with LTO. Ensure that LTO is disabled if ULIB is used. Signed-off-by: Simon Glass <sjg@chromium.org> --- Kconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Kconfig b/Kconfig index c110c9f2a6e..6461443566e 100644 --- a/Kconfig +++ b/Kconfig @@ -128,7 +128,7 @@ config ARCH_SUPPORTS_LTO config LTO bool "Enable Link Time Optimizations" - depends on ARCH_SUPPORTS_LTO + depends on ARCH_SUPPORTS_LTO && !ULIB help This option enables Link Time Optimization (LTO), a mechanism which allows the compiler to optimize between different compilation units. -- 2.43.0
From: Simon Glass <sjg@chromium.org> Add a file which lists various symbols to rename when building the U-Boot library. For now it contains printf() and related functions, but more can be added later, as needed. Signed-off-by: Simon Glass <sjg@chromium.org> --- lib/ulib/rename.syms | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 lib/ulib/rename.syms diff --git a/lib/ulib/rename.syms b/lib/ulib/rename.syms new file mode 100644 index 00000000000..a0ecc6c1f40 --- /dev/null +++ b/lib/ulib/rename.syms @@ -0,0 +1,30 @@ +# SPDX-License-Identifier: GPL-2.0+ +# +# Symbol redefinition list for U-Boot library +# +# Copyright 2025 Canonical Ltd. +# Written by Simon Glass <simon.glass@canonical.com> +# + +# Format: +# file:filename - indicates the header file which contains the following symbols +# +# Symbols renames are indented at least one position: +# symbol_name - Prefix the symbol with 'ub_' (e.g., printf → ub_printf) +# original=new - Explicit mapping, so 'original' becomes 'new' +# +# Lines starting with # are comments + +# Standard library functions +file: stdio.h + printf + snprintf + vprintf + +file: vsprintf.h + sprintf + vsprintf + vsnprintf + +# Example of explicit mapping for custom naming +# scnprintf=ub_scnprintf_custom -- 2.43.0
From: Simon Glass <sjg@chromium.org> Add a way to create a .ulib-objs file which contains all of the object files from the build, but with symbols renamed according to the rename.syms file. The file excludes main() which is present in the sandbox build, so that programs which link with libu-boot can provide their own main() For now this file is not used. Signed-off-by: Simon Glass <sjg@chromium.org> --- Makefile | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/Makefile b/Makefile index cfd24de82d5..f6527cca136 100644 --- a/Makefile +++ b/Makefile @@ -1859,6 +1859,25 @@ ifeq ($(CONFIG_RISCV),y) @tools/prelink-riscv $@ endif +# Common step: create archive and prepare modified object files +quiet_cmd_ulib-objs = OBJS $@ + cmd_ulib-objs = \ + rm -f $@.tmp $@.objlist $@; \ + $(AR) rcT $@.tmp $(u-boot-init) $(u-boot-main) \ + $(u-boot-keep-syms-lto); \ + $(AR) t $@.tmp | grep -v "arch/sandbox/cpu/main\.o$$" > $@.objlist; \ + mkdir -p $@.objdir; \ + $(PYTHON3) $(srctree)/scripts/build_api.py \ + $(srctree)/lib/ulib/rename.syms \ + --redefine $$(cat $@.objlist) --output-dir $@.objdir \ + $(if $(filter -j%,$(MAKEFLAGS)),--jobs $(patsubst -j%,%,$(filter -j%,$(MAKEFLAGS)))) \ + > $@; \ + rm -f $@.tmp $@.objlist + +.ulib-objs: $(u-boot-init) $(u-boot-main) $(u-boot-keep-syms-lto) \ + $(srctree)/lib/ulib/rename.syms FORCE + $(call if_changed,ulib-objs) + # Build U-Boot as a shared library quiet_cmd_libu-boot.so = LD $@ cmd_libu-boot.so = $(CC) -shared -o $@ -Wl,--build-id=none \ -- 2.43.0
From: Simon Glass <sjg@chromium.org> Plumb this feature in, so that symbols are renamed as expected. Signed-off-by: Simon Glass <sjg@chromium.org> --- Makefile | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/Makefile b/Makefile index f6527cca136..f157de21008 100644 --- a/Makefile +++ b/Makefile @@ -1880,32 +1880,26 @@ quiet_cmd_ulib-objs = OBJS $@ # Build U-Boot as a shared library quiet_cmd_libu-boot.so = LD $@ - cmd_libu-boot.so = $(CC) -shared -o $@ -Wl,--build-id=none \ + cmd_libu-boot.so = \ + $(CC) -shared -o $@ -Wl,--build-id=none \ -Wl,-T,$(LIB_LDS) \ - $(u-boot-init) \ $(KBUILD_LDFLAGS:%=-Wl,%) $(SANITIZERS) $(LTO_FINAL_LDFLAGS) \ -Wl,--whole-archive \ - $(filter-out %/main.o,$(u-boot-main)) \ - $(u-boot-keep-syms-lto) \ + $$(cat .ulib-objs) \ -Wl,--no-whole-archive \ $(PLATFORM_LIBS) -Wl,-Map -Wl,libu-boot.map -libu-boot.so: $(u-boot-init) $(u-boot-main) $(u-boot-keep-syms-lto) \ - $(LIB_LDS) FORCE +libu-boot.so: .ulib-objs $(LIB_LDS) include/u-boot-api.h FORCE $(call if_changed,libu-boot.so) # Build U-Boot as a static library # Create a fat archive with all object files (except arch/sandbox/cpu/main.o) # Avoid partial linking so as to preserve the linker-list sections quiet_cmd_libu-boot.a = AR $@ - cmd_libu-boot.a = rm -f $@ $@.tmp $@.objlist; \ - $(AR) rcT $@.tmp $(u-boot-init) $(u-boot-main) \ - $(u-boot-keep-syms-lto); \ - $(AR) t $@.tmp | grep -v "arch/sandbox/cpu/main\.o$$" > $@.objlist; \ - cat $@.objlist | xargs $(AR) rcs $@; \ - rm -f $@.tmp $@.objlist + cmd_libu-boot.a = rm -f $@; \ + cat .ulib-objs | xargs $(AR) rcs $@ -libu-boot.a: $(u-boot-init) $(u-boot-main) $(u-boot-keep-syms-lto) FORCE +libu-boot.a: .ulib-objs include/u-boot-api.h FORCE $(call if_changed,libu-boot.a) # Build ulib_test that links with shared library @@ -2347,7 +2341,9 @@ CLEAN_FILES += include/autoconf.mk* include/bmp_logo.h include/bmp_logo_data.h \ mkimage.rom.mkimage mkimage-in-simple-bin* rom.map simple-bin* \ idbloader-spi.img lib/efi_loader/helloworld_efi.S *.itb \ Test* capsule*.*.efi-capsule capsule*.map \ - test/ulib/ulib_test test/ulib/ulib_test_static + test/ulib/ulib_test test/ulib/ulib_test_static \ + libu-boot.so.tmp libu-boot.so.objlist \ + libu-boot.a.tmp libu-boot.a.objlist # Directories & files removed with 'make mrproper' MRPROPER_DIRS += include/config include/generated spl tpl vpl \ -- 2.43.0
From: Simon Glass <sjg@chromium.org> Plumb in generation of the u-boot-api.h file, containing renamed symbols for inclusion by the program being linked with ulib. Signed-off-by: Simon Glass <sjg@chromium.org> --- Makefile | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index f157de21008..c4a5dbea199 100644 --- a/Makefile +++ b/Makefile @@ -1902,6 +1902,16 @@ quiet_cmd_libu-boot.a = AR $@ libu-boot.a: .ulib-objs include/u-boot-api.h FORCE $(call if_changed,libu-boot.a) +# Generate API header with renamed function declarations +quiet_cmd_u-boot-api.h = APIH $@ + cmd_u-boot-api.h = $(PYTHON3) $(srctree)/scripts/build_api.py \ + $(srctree)/lib/ulib/rename.syms --api $@ \ + --include-dir $(srctree)/include + +include/u-boot-api.h: $(srctree)/lib/ulib/rename.syms \ + $(srctree)/scripts/build_api.py FORCE + $(call if_changed,u-boot-api.h) + # Build ulib_test that links with shared library quiet_cmd_ulib_test = HOSTCC $@ cmd_ulib_test = $(HOSTCC) $(HOSTCFLAGS) \ @@ -2343,7 +2353,8 @@ CLEAN_FILES += include/autoconf.mk* include/bmp_logo.h include/bmp_logo_data.h \ Test* capsule*.*.efi-capsule capsule*.map \ test/ulib/ulib_test test/ulib/ulib_test_static \ libu-boot.so.tmp libu-boot.so.objlist \ - libu-boot.a.tmp libu-boot.a.objlist + libu-boot.a.tmp libu-boot.a.objlist \ + include/u-boot-api.h # Directories & files removed with 'make mrproper' MRPROPER_DIRS += include/config include/generated spl tpl vpl \ -- 2.43.0
From: Simon Glass <sjg@chromium.org> It is possible to have some files that build in the system environment and some in the U-Boot environment. To build for the system, the system headers must be used, or at least have priority. For a U-Boot file, its headers must be used exclusively. Expand the Makefile example to have two files, one of which is built using U-Boot headers. This shows how a program can be built which straddles both domains. Signed-off-by: Simon Glass <sjg@chromium.org> --- examples/ulib/Makefile | 58 ++++++++++++++++++++++++++----------- examples/ulib/demo.c | 12 ++++++-- examples/ulib/demo_helper.c | 30 +++++++++++++++++++ examples/ulib/demo_helper.h | 31 ++++++++++++++++++++ 4 files changed, 111 insertions(+), 20 deletions(-) create mode 100644 examples/ulib/demo_helper.c create mode 100644 examples/ulib/demo_helper.h diff --git a/examples/ulib/Makefile b/examples/ulib/Makefile index f70333a3b9b..81885cf8726 100644 --- a/examples/ulib/Makefile +++ b/examples/ulib/Makefile @@ -35,36 +35,60 @@ LIB_STATIC_LDS ?= static.lds # The main Makefile passes in Q=@ for quiet output Q ?= +# Common compiler flags for programs using system headers first +SYSTEM_CFLAGS := -I$(UBOOT_BUILD)/include -idirafter$(srctree)/include \ + -include $(srctree)/include/linux/compiler_attributes.h + +# Common compiler flags for programs using U-Boot headers first (like U-Boot +# internal build) +UBOOT_CFLAGS := -nostdinc -isystem $(shell $(CC) -print-file-name=include) \ + -I$(UBOOT_BUILD)/include \ + -I$(srctree)/include \ + -I$(srctree)/arch/sandbox/include \ + -include $(UBOOT_BUILD)/include/config.h \ + -include $(srctree)/include/linux/kconfig.h \ + -I$(srctree)/dts/upstream/include \ + -D__KERNEL__ -DCONFIG_SYS_TEXT_BASE=0 + +SHARED_LDFLAGS := -L$(UBOOT_BUILD) -lu-boot -Wl,-rpath,$(UBOOT_BUILD) + +STATIC_LDFLAGS := -Wl,-T,$(LIB_STATIC_LDS) \ + -Wl,--whole-archive $(UBOOT_BUILD)/libu-boot.a -Wl,--no-whole-archive \ + -lpthread -ldl $(PLATFORM_LIBS) -Wl,-z,noexecstack + +# Program definitions - can be single file or multi-object DEMO_SRC := $(EXAMPLE_DIR)/demo.c +demo-objs := demo.o demo_helper.o DEMO_BINS := $(OUTDIR)/demo $(OUTDIR)/demo_static +ALL_BINS := $(DEMO_BINS) + # Default target builds both programs -all: $(DEMO_BINS) +all: $(ALL_BINS) # Create the output directory if it doesn't exist $(OUTDIR): @mkdir -p $@ -# The U-Boot library must be built before we can link against it. This is -# signalled by the presence of the $(UBOOT_BUILD)/examples/ulib directory. -# This is an order-only prerequisite, so it does not trigger a rebuild if the -# timestamp of the directory changes. -$(DEMO_BINS): | $(UBOOT_BUILD)/examples/ulib $(OUTDIR) +# Pattern rule for building object files with system headers first (default) +$(OUTDIR)/%.o: $(EXAMPLE_DIR)/%.c | $(OUTDIR) + $(CC) $(CFLAGS) $(SYSTEM_CFLAGS) -c -o $@ $< + +# Pattern rule for building object files with U-Boot headers first +$(OUTDIR)/%_uboot.o: $(EXAMPLE_DIR)/%.c | $(OUTDIR) + $(CC) $(CFLAGS) $(UBOOT_CFLAGS) -c -o $@ $< + +# The U-Boot library must be built before we can link against it +# Order-only prerequisites ensure libraries exist before linking +$(ALL_BINS): | $(UBOOT_BUILD)/libu-boot.a $(UBOOT_BUILD)/libu-boot.so $(OUTDIR) # Build demo (dynamically linked with libu-boot.so) -$(OUTDIR)/demo: $(DEMO_SRC) - $(CC) $(CFLAGS) \ - -idirafter$(srctree)/include -o $@ $< \ - -L$(UBOOT_BUILD) -lu-boot \ - -Wl,-rpath,$(UBOOT_BUILD) +$(OUTDIR)/demo: $(if $(demo-objs),$(addprefix $(OUTDIR)/,$(demo-objs)),$(DEMO_SRC)) + $(if $(demo-objs),$(CC) $(CFLAGS) -o $@ $^ $(SHARED_LDFLAGS),$(CC) $(CFLAGS) $(SYSTEM_CFLAGS) -o $@ $< $(SHARED_LDFLAGS)) # Build demo_static (statically linked with libu-boot.a) -$(OUTDIR)/demo_static: $(DEMO_SRC) - $(CC) $(CFLAGS) \ - -idirafter$(srctree)/include -o $@ $< \ - -Wl,-T,$(LIB_STATIC_LDS) \ - -Wl,--whole-archive $(UBOOT_BUILD)/libu-boot.a -Wl,--no-whole-archive \ - -lpthread -ldl $(PLATFORM_LIBS) -Wl,-z,noexecstack +$(OUTDIR)/demo_static: $(if $(demo-objs),$(addprefix $(OUTDIR)/,$(demo-objs)),$(DEMO_SRC)) + $(if $(demo-objs),$(CC) $(CFLAGS) -o $@ $^ $(STATIC_LDFLAGS),$(CC) $(CFLAGS) $(SYSTEM_CFLAGS) -o $@ $< $(STATIC_LDFLAGS)) clean: $(Q)rm -f $(DEMO_BINS) diff --git a/examples/ulib/demo.c b/examples/ulib/demo.c index 2a9f7720beb..9d916d3878a 100644 --- a/examples/ulib/demo.c +++ b/examples/ulib/demo.c @@ -17,10 +17,11 @@ #include <os.h> #include <u-boot-lib.h> #include <version_string.h> +#include "demo_helper.h" int main(int argc, char *argv[]) { - int fd, lines = 0; + int fd, result, lines = 0; char line[256]; /* Init U-Boot library */ @@ -29,8 +30,7 @@ int main(int argc, char *argv[]) return 1; } - printf("1U-Boot Library Demo\n"); - printf("================================\n"); + demo_show_banner(); printf("U-Boot version: %s\n", version_string); printf("\n"); @@ -54,6 +54,12 @@ int main(int argc, char *argv[]) printf("\nRead %d line(s) using U-Boot library functions.\n", lines); + /* Test the helper function */ + result = demo_add_numbers(42, 13); + printf("Helper function result: %d\n", result); + + demo_show_footer(); + /* Clean up */ ulib_uninit(); diff --git a/examples/ulib/demo_helper.c b/examples/ulib/demo_helper.c new file mode 100644 index 00000000000..935443657bd --- /dev/null +++ b/examples/ulib/demo_helper.c @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Helper functions for U-Boot library demo + * + * Copyright 2025 Canonical Ltd. + * Written by Simon Glass <simon.glass@canonical.com> + */ + +#include <u-boot-api.h> + +void demo_show_banner(void) +{ + ub_printf("=================================\n"); + ub_printf(" U-Boot Library Demo Helper\n"); + ub_printf("=================================\n"); +} + +void demo_show_footer(void) +{ + ub_printf("=================================\n"); + ub_printf(" Demo Complete!\n"); + ub_printf("=================================\n"); +} + +int demo_add_numbers(int a, int b) +{ + ub_printf("Helper: Adding %d + %d = %d\n", a, b, a + b); + + return a + b; +} diff --git a/examples/ulib/demo_helper.h b/examples/ulib/demo_helper.h new file mode 100644 index 00000000000..b4ab862941b --- /dev/null +++ b/examples/ulib/demo_helper.h @@ -0,0 +1,31 @@ +/* SPDX-License-Identifier: GPL-2.0+ */ +/* + * Helper functions for U-Boot library demo + * + * Copyright 2025 Canonical Ltd. + * Written by Simon Glass <simon.glass@canonical.com> + */ + +#ifndef __DEMO_HELPER_H +#define __DEMO_HELPER_H + +/** + * demo_show_banner() - Show the demo banner + */ +void demo_show_banner(void); + +/** + * demo_show_footer() - Show the demo footer + */ +void demo_show_footer(void); + +/** + * demo_add_numbers() - Add two numbers and print the result + * + * @a: First number + * @b: Second number + * Return: Sum of the two numbers + */ +int demo_add_numbers(int a, int b); + +#endif /* __DEMO_HELPER_H */ -- 2.43.0
From: Simon Glass <sjg@chromium.org> Update the test to use both the system printf() and the U-Boot one, in the same program. This provides verification that symbol-renaming is working as expected. Signed-off-by: Simon Glass <sjg@chromium.org> --- test/ulib/ulib_test.c | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/test/ulib/ulib_test.c b/test/ulib/ulib_test.c index 5daf949a74c..c2d518e1b28 100644 --- a/test/ulib/ulib_test.c +++ b/test/ulib/ulib_test.c @@ -13,6 +13,7 @@ #include <string.h> #include <os.h> +#include <u-boot-api.h> #include <u-boot-lib.h> /* Runtime detection of link type using /proc/self/maps */ @@ -48,13 +49,16 @@ int main(int argc, char *argv[]) { int ret; + printf("Uses libc printf before ulib_init\n"); + ret = ulib_init(argv[0]); if (ret) return 1; - printf("Hello, world\n"); - printf("\n- U-Boot\n"); - printf("\nPS: This program is %s\n", detect_link_type()); + ub_printf("Hello, world from ub_printf\n"); + ub_printf("\n- U-Boot\n"); + printf("another printf()\n"); + ub_printf("\nPS: This program is %s\n", detect_link_type()); return ret; } -- 2.43.0
From: Simon Glass <sjg@chromium.org> Most of the ulib functionality is in place now. Update the documentation to match. Include details of how to run the example. Signed-off-by: Simon Glass <sjg@chromium.org> --- doc/develop/ulib.rst | 179 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 171 insertions(+), 8 deletions(-) diff --git a/doc/develop/ulib.rst b/doc/develop/ulib.rst index 8df3a6f99bc..d68ffcaa964 100644 --- a/doc/develop/ulib.rst +++ b/doc/develop/ulib.rst @@ -80,16 +80,25 @@ main entry point while still using U-Boot functionality. The libraries preserve U-Boot's linker lists, which are essential for driver registration and other U-Boot subsystems. +**Link Time Optimization (LTO) Compatibility** + +When building with ``CONFIG_ULIB=y``, Link Time Optimization (LTO) is +automatically disabled. This is because the symbol renaming process uses +``objcopy --redefine-sym``, which is incompatible with LTO-compiled object +files. The build system handles this automatically. + Building outside the U-Boot tree -------------------------------- -This is possible, but as soon as you want to call a function that is not in -u-boot-lib.h you will have problems, as described in the following sections. +This is possible using the provided examples as a template. The ``examples/ulib`` +directory contains a standalone Makefile that can build programs against a +pre-built U-Boot library. -This will be addressed with future work. +The examples works as expected, but note that as soon as you want to call +functions that are not in the main API headers, you may have problems with +missing dependencies and header files. See below. -With that caveat, see example/ulib/README for instructions on how to use the -provided example. +See the **Example Programs** section above for build instructions. Including U-Boot header files from outside ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -114,10 +123,13 @@ CONFIG settings would need to be exported from the build and packaged with the library. -Test Programs -------------- +Test Programs and Examples +-------------------------- + +U-Boot includes several test programs and examples that demonstrate library +usage: -U-Boot includes test programs that demonstrate library usage: +**Test Programs** * ``test/ulib/ulib_test`` - Uses the shared library * ``test/ulib/ulib_test_static`` - Uses the static library @@ -132,6 +144,42 @@ Run the static library version:: ./test/ulib/ulib_test_static +**Example Programs** + +The ``examples/ulib`` directory contains more complete examples: + +* ``demo`` - Dynamically linked demo program showing U-Boot functionality +* ``demo_static`` - Statically linked version of the demo program + +These examples demonstrate: + +* Proper library initialization with ``ulib_init()`` +* Using U-Boot OS functions like ``os_open()``, ``os_fgets()``, ``os_close()`` +* Using renamed U-Boot library functions via ``u-boot-api.h`` + (e.g., ``ub_printf()``) +* Multi-file program structure (``demo.c`` + ``demo_helper.c``) +* Proper cleanup with ``ulib_uninit()`` + +To build and run the examples:: + + # Make sure U-Boot itself is built + make O=/tmp/b/sandbox sandbox_defconfig all + + cd examples/ulib + make UBOOT_BUILD=/tmp/b/sandbox srctree=../.. + ./demo_static + +**Building Examples Outside U-Boot Tree** + +The examples can be built independently if you have a pre-built U-Boot library:: + + cd examples/ulib + make UBOOT_BUILD=/path/to/uboot/build srctree=/path/to/uboot/source + +The Makefile supports both single-file and multi-object programs through the +``demo-objs`` variable. Set this to build from multiple object files, or leave +empty to build directly from source. + Linking and the Linker Script ----------------------------- @@ -246,6 +294,120 @@ Limitations program * EFI runtime-services and relocation are disabled +Symbol Renaming and API Generation +----------------------------------- + +U-Boot includes a build script (``scripts/build_api.py``) that supports symbol +renaming and API-header generation for library functions. This allows creating +namespaced versions of standard library functions to avoid conflicts. + +For example, when linking with the library, printf() refers to the stdio +printf() function, while ub_printf() refers to U-Boot's version. + +Build System Integration +~~~~~~~~~~~~~~~~~~~~~~~~ + +The symbol renaming system is automatically integrated into the U-Boot build +process: + +* The symbol definitions are stored in ``lib/ulib/rename.syms`` +* During the sandbox build, the build system automatically: + + - Renames symbols in object files using ``--redefine`` when building the + U-Boot libraries (``libu-boot.so`` and ``libu-boot.a``) + - Generates ``include/u-boot-api.h`` with renamed function declarations + using ``--api`` + +* The API header provides clean interfaces for external programs linking + against the U-Boot library +* Symbol renaming ensures no conflicts between U-Boot functions and system + library functions + +Symbol Definition File Format +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The script uses a symbol definition file (``rename.syms``) with this format:: + + # Comment lines start with # + file: stdio.h + printf + sprintf=ub_sprintf_custom + + file: string.h + strlen + strcpy + +The format rules are: + +* Lines starting with ``file:`` specify a header file +* Indented lines (space or tab) define symbols from that header +* Use ``symbol=new_name`` for custom renaming, otherwise ``ub_`` a prefix is + added by default. No space around ``=`` +* Use ``#`` at the beginning of a line for a comment +* Empty lines are allowed + +Script Usage +~~~~~~~~~~~~ + +The build script provides several functions: + +**Parse and display symbols**:: + + python scripts/build_api.py rename.syms --dump + +**Apply symbol renaming to object files**:: + + python scripts/build_api.py rename.syms \ + --redefine file1.o file2.o \ + --output-dir /tmp/renamed_objects + +**Generate API header with renamed functions**:: + + python scripts/build_api.py rename.syms \ + --api ulib_api.h \ + --include-dir /path/to/headers \ + --output-dir /tmp/objects + +Script Architecture +~~~~~~~~~~~~~~~~~~~ + +The build script consists mostly of these classes: + +* **RenameSymsParser**: Parse the symbol definition file format and validate + syntax +* **DeclExtractor**: Extract function declarations with comments from headers +* **SymbolRedefiner**: Apply symbol renaming to object files using ``objcopy`` +* **ApiGenerator**: Create unified API headers with renamed function + declarations + +Symbol renaming operations copy files to an output directory rather than +modifying them in-place, to avoid race conditions. + +Object File Processing +~~~~~~~~~~~~~~~~~~~~~~ + +When processing object files, the script: + +1. Uses ``nm`` to check which files contain target symbols +2. Copies unchanged files that don't contain target symbols +3. Applies ``objcopy --redefine-sym`` for files needing renaming +4. Creates unique output filenames by replacing path separators with + underscores + +API Header Generation +~~~~~~~~~~~~~~~~~~~~~ + +The API header generation process: + +1. Groups symbols by their source header files +2. Searches for original header files in the specified include directory +3. Extracts function declarations (including comments) from source headers +4. Applies symbol renaming to the extracted declarations +5. Combines everything into a single API header file + +If any required headers or function declarations are missing, the script fails +with detailed error messages listing exactly what couldn't be found. + Future Work ----------- @@ -254,3 +416,4 @@ Future Work * API versioning and stability guarantees * pkg-config support for easier integration * Support for calling functions in any U-Boot header +* Improved symbol renaming with namespace support -- 2.43.0
From: Simon Glass <sjg@chromium.org> Provide a test that checks that ulib operates as expected. Add a .gitignore file for the executables thus created There is a strange interaction with PLATFORM_LIBS which can cause the examples to fail to build via 'make qcheck': - CI runs 'make qcheck' - the main Makefile sets PLATFORM_LIBS - test/run calls test.py - at some point test_ulib_demos() starts, with PLATFORM_LIBS set - the test calls 'make' on examples/ulib/Makefile - PLATFORM_LIBS is left alone, since it already has a value - lSDL ends up not being in the link line Thank you to Claude for helping to debug this and figure out the PLATFORM_LIBS interaction. Co-developed-by: Claude <noreply@anthropic.com> Signed-off-by: Simon Glass <sjg@chromium.org> --- examples/ulib/.gitignore | 2 + examples/ulib/demo_helper.c | 10 +-- test/py/tests/test_ulib.py | 141 ++++++++++++++++++++++++++++++++++++ test/run | 8 ++ 4 files changed, 155 insertions(+), 6 deletions(-) create mode 100644 examples/ulib/.gitignore create mode 100644 test/py/tests/test_ulib.py diff --git a/examples/ulib/.gitignore b/examples/ulib/.gitignore new file mode 100644 index 00000000000..d2f0dfa7a93 --- /dev/null +++ b/examples/ulib/.gitignore @@ -0,0 +1,2 @@ +/demo +/demo_static diff --git a/examples/ulib/demo_helper.c b/examples/ulib/demo_helper.c index 935443657bd..e3a2c6bdcfb 100644 --- a/examples/ulib/demo_helper.c +++ b/examples/ulib/demo_helper.c @@ -10,21 +10,19 @@ void demo_show_banner(void) { - ub_printf("=================================\n"); - ub_printf(" U-Boot Library Demo Helper\n"); - ub_printf("=================================\n"); + ub_printf("U-Boot Library Demo Helper\n"); + ub_printf("==========================\n"); } void demo_show_footer(void) { ub_printf("=================================\n"); - ub_printf(" Demo Complete!\n"); - ub_printf("=================================\n"); + ub_printf("Demo complete\n"); } int demo_add_numbers(int a, int b) { - ub_printf("Helper: Adding %d + %d = %d\n", a, b, a + b); + ub_printf("helper: Adding %d + %d = %d\n", a, b, a + b); return a + b; } diff --git a/test/py/tests/test_ulib.py b/test/py/tests/test_ulib.py new file mode 100644 index 00000000000..76d40e1385c --- /dev/null +++ b/test/py/tests/test_ulib.py @@ -0,0 +1,141 @@ +# SPDX-License-Identifier: GPL-2.0+ +# Copyright (c) 2025, Canonical Ltd. + +import os +import subprocess +import pytest +import utils + +def check_output(out): + """Check output from the ulib test""" + assert 'Hello, world from ub_printf' in out + assert '- U-Boot' in out + assert 'Uses libc printf before ulib_init' in out + assert 'another printf()' in out + +@pytest.mark.buildconfigspec("ulib") +def test_ulib_shared(ubman): + """Test the ulib shared library test program""" + + build = ubman.config.build_dir + prog = os.path.join(build, 'test', 'ulib', 'ulib_test') + + # Skip test if ulib_test doesn't exist (clang) + if not os.path.exists(prog): + pytest.skip('ulib_test not found - library build may be disabled') + + out = utils.run_and_log(ubman, [prog], cwd=build) + check_output(out) + assert 'dynamically linked' in out + +@pytest.mark.boardspec('sandbox') +def test_ulib_static(ubman): + """Test the ulib static library test program""" + + build = ubman.config.build_dir + prog = os.path.join(build, 'test', 'ulib', 'ulib_test_static') + + # Skip test if ulib_test_static doesn't exist (clang) + if not os.path.exists(prog): + pytest.skip('ulib_test_static not found - library build may be disabled') + + out = utils.run_and_log(ubman, [prog]) + check_output(out) + assert 'statically linked' in out + +def check_demo_output(ubman, out): + """Check output from the ulib demo programs exactly line by line""" + lines = out.split('\n') + + # Read the actual system version from /proc/version + with open('/proc/version', 'r', encoding='utf-8') as f: + proc_version = f.read().strip() + + expected = [ + 'U-Boot Library Demo Helper\r', + '==========================\r', + 'System version:helper: Adding 42 + 13 = 55\r', + '=================================\r', + 'Demo complete\r', + f'U-Boot version: {ubman.u_boot_version_string}', + '', + f' {proc_version}', + '', + 'Read 1 line(s) using U-Boot library functions.', + 'Helper function result: 55', + '' + ] + + assert len(lines) == len(expected), \ + f"Expected {len(expected)} lines, got {len(lines)}" + + for i, expected in enumerate(expected): + # Exact match for all other lines + assert lines[i] == expected, \ + f"Line {i}: expected '{expected}', got '{lines[i]}'" + +@pytest.mark.boardspec('sandbox') +def test_ulib_demos(ubman): + """Test both ulib demo programs (dynamic and static).""" + + build = ubman.config.build_dir + src = ubman.config.source_dir + examples = os.path.join(src, 'examples', 'ulib') + test_program = os.path.join(build, 'test', 'ulib', 'ulib_test') + + # Skip test if ulib_test doesn't exist (clang) + if not os.path.exists(test_program): + pytest.skip('ulib_test not found - library build may be disabled') + + # Build the demo programs - clean first to ensure fresh build, since this + # test is run in the source directory + cmd = ['make', 'clean'] + utils.run_and_log(ubman, cmd, cwd=examples) + + cmd = ['make', f'UBOOT_BUILD={os.path.abspath(build)}', f'srctree={src}'] + utils.run_and_log(ubman, cmd, cwd=examples) + + # Test static demo program + demo_static = os.path.join(examples, 'demo_static') + out_static = utils.run_and_log(ubman, [demo_static]) + check_demo_output(ubman, out_static) + + # Test dynamic demo program (with proper LD_LIBRARY_PATH) + demo = os.path.join(examples, 'demo') + env = os.environ.copy() + env['LD_LIBRARY_PATH'] = os.path.abspath(build) + out_dynamic = utils.run_and_log(ubman, [demo], env=env) + check_demo_output(ubman, out_dynamic) + +@pytest.mark.boardspec('sandbox') +def test_ulib_api_header(ubman): + """Test that the u-boot-api.h header is generated correctly.""" + + hdr = os.path.join(ubman.config.build_dir, 'include', 'u-boot-api.h') + + # Skip if header doesn't exist (clang) + if not os.path.exists(hdr): + pytest.skip('u-boot-api.h not found - library build may be disabled') + + # Read and verify header content + with open(hdr, 'r', encoding='utf-8') as inf: + out = inf.read() + + # Check header guard + assert '#ifndef __ULIB_API_H' in out + assert '#define __ULIB_API_H' in out + assert '#endif /* __ULIB_API_H */' in out + + # Check required includes + assert '#include <stdarg.h>' in out + assert '#include <stddef.h>' in out + + # Check for renamed function declarations + assert 'ub_printf' in out + assert 'ub_snprintf' in out + assert 'ub_vprintf' in out + + # Check that functions have proper signatures + assert 'ub_printf(const char *fmt, ...)' in out + assert 'ub_snprintf(char *buf, size_t size, const char *fmt, ...)' in out + assert 'ub_vprintf(const char *fmt, va_list args)' in out diff --git a/test/run b/test/run index 2ba8324a0c4..80f79317aed 100755 --- a/test/run +++ b/test/run @@ -18,6 +18,14 @@ quiet=-q # Clean up things the Makefile created unset MAKE MAKEFLAGS MAKELEVEL MAKEOVERRIDES MAKE_TERMERR MAKE_TERMOUT +# Unset this since this script is generally run from 'make qcheck' et al, which +# targets are in no-dot-config-targets and thus dot-config is 0 and thus +# config.mk was not included in the main Makefile, thus PLATFORM_LIBS does not +# have the arch-specific settings (e.g. SDL libraries on sandbox). Better to +# leave it empty than have it be wrong. This particularly affects +# example/ulib/Makefile when called from 'make qcheck' +unset PLATFORM_LIBS + # Select test attributes ut_mark_expr=test_ut if [ "$1" = "quick" ]; then -- 2.43.0
From: Simon Glass <sjg@chromium.org> When U-Boot is used as a library with other programs, some U-Boot function names may conflict with the program, or with standard-library functions. For example, printf() is defined by U-Boot but is typically used by the program as well. The easiest solution is to rename symbols in the object file, so that they appear with a 'ub_' prefix when linked with the program. Add a new build_api.py script which can: - rename symbols based on a rename.syms file - generate a header file (with the renamed symbols) for use by the program This makes use of the 'objcopy --redefine-sym' feature. The tool has 100% test coverage. Co-developed-by: Claude <noreply@anthropic.com> Signed-off-by: Simon Glass <sjg@chromium.org> --- scripts/build_api.py | 714 +++++++++++++++++++++++++++++++++ test/scripts/test_build_api.py | 704 ++++++++++++++++++++++++++++++++ 2 files changed, 1418 insertions(+) create mode 100755 scripts/build_api.py create mode 100644 test/scripts/test_build_api.py diff --git a/scripts/build_api.py b/scripts/build_api.py new file mode 100755 index 00000000000..7adc6c978a3 --- /dev/null +++ b/scripts/build_api.py @@ -0,0 +1,714 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0 +"""Script to parse rename.syms files and generate API headers""" + +import argparse +import filecmp +import os +import re +import subprocess +import sys +import time +from concurrent.futures import ThreadPoolExecutor, as_completed +from dataclasses import dataclass +from itertools import groupby +from typing import List + +# Add the tools directory to the path for u_boot_pylib +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'tools')) + +# pylint: disable=wrong-import-position,import-error +from u_boot_pylib import tools +from u_boot_pylib import test_util + +# API header template parts +API_HEADER = '''#ifndef __ULIB_API_H +#define __ULIB_API_H + +#include <stdarg.h> +#include <stddef.h> + +/* Auto-generated header with renamed U-Boot library functions */ + +''' + +API_FOOTER = '''#endif /* __ULIB_API_H */ +''' + +def rename_function(src, old_name, new_name): + """Rename a function in C source code + + Args: + src (str): The source code containing the function + old_name (str): Current function name to rename + new_name (str): New function name + + Returns: + str: Source code with the function renamed + """ + # Pattern to match function declaration/definition + # Matches: return_type func(parameters) + pattern = r'\b' + re.escape(old_name) + r'\b(?=\s*\()' + + # Replace all occurrences of the function name (in function comment too) + renamed_code = re.sub(pattern, new_name, src) + return renamed_code + + +@dataclass +class Symbol: + """Represents a symbol rename operation for library functions. + + Used to track how functions from a header file should be renamed + to create a namespaced API (e.g., printf -> ub_printf). + """ + hdr: str # Header file containing the function + orig: str # Original function name + new_name: str # New function name after renaming + + +class RenameSymsParser: + """Parser for rename.syms files. + + Format: + file: header.h + symbol1 + symbol2=renamed_symbol2 + symbol3 + + Lines starting with 'file:' specify a header file. + Lines indented with space or tab specify symbols from that header. + Symbol lines can use '=' for explicit renaming, otherwise 'ub_' prefix is + added. + Comments start with '#' and must begin at start of line. + Empty lines are allowed. + Trailing spaces are stripped but no other whitespace is allowed except for + symbol indentation. + """ + def __init__(self, fname: str): + """Initialize the parser with a rename.syms file path + + Args: + fname (str): Path to the rename.syms file to parse + """ + self.fname = fname + self.syms: List[Symbol] = [] + + def parse_line(self, line: str, hdr: str) -> Symbol: + """Parse a line and return a Symbol or None + + Args: + line (str): The line to parse (already stripped) + hdr (str): Current header file name + + Returns: + Symbol or None: Symbol if line contains a symbol definition, + None otherwise + """ + if '=' in line: + # Explicit mapping: orig=new + orig, new = line.split('=', 1) + orig = orig.strip() + new = new.strip() + else: + # Default mapping: add 'ub_' prefix + orig = line + new = f'ub_{orig}' + + return Symbol(hdr=hdr, orig=orig, new_name=new) + + def parse(self) -> List[Symbol]: + """Parse the rename.syms file and return list of symbols + + Returns: + List[Symbol]: List of symbol rename operations + """ + hdr = None + content = tools.read_file(self.fname, binary=False) + for line_num, line in enumerate(content.splitlines(), 1): + line = line.rstrip() + + # Skip empty lines and comments + if not line or line.startswith('#'): + continue + + # Check for file directive + if line.startswith('file:'): + hdr = line.split(':', 1)[1].strip() + continue + + # Check for symbol (indented line with space or tab) + if line[0] not in [' ', '\t']: + # Non-indented, non-file lines are invalid + raise ValueError(f'Line {line_num}: Invalid format - ' + f"symbols must be indented: '{line}'") + + if hdr is None: + raise ValueError(f"Line {line_num}: Symbol '{line.strip()}' " + f'found without a header file directive') + + # Process valid symbol + symbol = self.parse_line(line.strip(), hdr) + if symbol: + self.syms.append(symbol) + return self.syms + + def dump(self): + """Print the parsed symbols in a formatted way""" + print(f'Parsed {len(self.syms)} symbols from ' + f'{self.fname}:') + print() + hdr = None + for sym in self.syms: + if sym.hdr != hdr: + hdr = sym.hdr + print(f'Header: {hdr}') + print(f' {sym.orig} -> {sym.new_name}') + print(f'\nTotal: {len(self.syms)} symbols') + + +class DeclExtractor: + """Extracts function declarations from header files with comments + + Expects functions to have an optional preceding comment block (either /**/ + style or // single-line) followed immediately by the function declaration. + The declaration may span multiple lines until a semicolon or opening brace. + + Properties: + lines (str): List of lines from the header file, set by extract() + """ + + def __init__(self, fname: str): + """Initialize with header file path + + Args: + fname (str): Path to the header file + """ + self.fname = fname + self.lines = [] + + def find_function(self, func: str): + """Find the line index of a function declaration + + Args: + func (str): Name of the function to find + + Returns: + int or None: Line index of function declaration, or None if + not found + """ + pattern = r'\b' + re.escape(func) + r'\s*\(' + + for i, full_line in enumerate(self.lines): + line = full_line.strip() + # Skip comment lines and find actual function declarations + if (not line.startswith('*') and not line.startswith('//') and + re.search(pattern, full_line)): + return i + + return None + + def find_preceding_comment(self, func_idx: int): + """Find comment block preceding a function declaration + + Args: + func_idx (int): Line index of the function declaration + + Returns: + int or None: Start line index of comment block, or None if not found + """ + # Search backwards from the line before the function declaration + for i in range(func_idx - 1, -1, -1): + line = self.lines[i].strip() + if not line: + continue # Skip empty lines + if line.startswith('*/'): + # Find the start of this comment block + for j in range(i, -1, -1): + if '/**' in self.lines[j]: + return j + break + if line.startswith('//'): + # Found single-line comment, include it if it's the first + # non-empty line before function + return i + if not line.startswith('*'): + # Hit non-comment content, no preceding comment + break + return None + + def extract_lines(self, start_idx: int, func_idx: int): + """Extract comment and function declaration lines + + Args: + start_idx (int): Starting line index (comment or function) + func_idx (int): Function declaration line index + + Returns: + str: Lines containing the complete declaration joined with newlines + """ + lines = [] + + # Add comment lines if found + if start_idx < func_idx: + lines.extend(self.lines[start_idx:func_idx]) + + # Add function declaration lines + for line in self.lines[func_idx:func_idx + 10]: + lines.append(line) + if ';' in line or '{' in line: + break + + return '\n'.join(lines) + + def extract(self, func: str): + """Find a function declaration in a header file, including its comment + + Args: + func (str): Name of the function to find + + Returns: + str or None: The function declaration with its comment, or None + if not found + """ + self.lines = tools.read_file(self.fname, binary=False).split('\n') + + func_idx = self.find_function(func) + if func_idx is None: + return None + + comment_idx = self.find_preceding_comment(func_idx) + start_idx = comment_idx if comment_idx is not None else func_idx + + return self.extract_lines(start_idx, func_idx) + + @staticmethod + def extract_decl(fname, func): + """Find a function declaration in a header file, including its comment + + Args: + fname (str): Path to the header file + func (str): Name of the function to find + + Returns: + str or None: The function declaration with its comment, or None + if not found + """ + extractor = DeclExtractor(fname) + return extractor.extract(func) + + +class SymbolRedefiner: + """Applies symbol redefinitions to object files using objcopy + + Processes object files to rename symbols using objcopy --redefine-sym. + Always copies modified files to an output directory. + + Properties: + redefine_args (List[str]): objcopy arguments for symbol redefinition + symbol_names (set): Set of original symbol names to look for + """ + + def __init__(self, syms: List[Symbol], outdir: str, max_workers, + verbose=False): + """Initialize with symbols and output settings + + Args: + syms (List[Symbol]): List of symbols to redefine + outdir (str): Directory to write modified object files + max_workers (int): Number of parallel workers + verbose (bool): Whether to show verbose output + """ + self.syms = syms + self.outdir = outdir + self.verbose = verbose + self.max_workers = max_workers + self.redefine_args = [] + self.symbol_names = set() + + # Build objcopy command arguments and symbol set + for sym in syms: + self.redefine_args.extend(['--redefine-sym', + f'{sym.orig}={sym.new_name}']) + self.symbol_names.add(sym.orig) + + def redefine_file(self, infile: str, outfile: str): + """Apply symbol redefinitions to a single object file + + Args: + infile (str): Input object file path + outfile (str): Output object file path + """ + cmd = ['objcopy'] + self.redefine_args + [infile, outfile] + subprocess.run(cmd, check=True, capture_output=True, text=True) + if self.verbose: + print(f'Copied and modified {infile} -> {outfile}') + + def _process_single_file(self, path: str, outfile: str) -> bool: + """Process a single file (for parallel execution) + + Args: + path (str): Input file path + outfile (str): Output file path + + Returns: + bool: True if file was modified, False otherwise + """ + # Always run objcopy to apply redefinitions + self.redefine_file(path, outfile) + + # Check if the file was actually modified + return not filecmp.cmp(path, outfile, shallow=False) + + def process(self, work_items: List[tuple[str, str]]) -> \ + tuple[List[str], int]: + """Process object files and apply symbol redefinitions + + Args: + work_items (List[tuple[str, str]]): List of + (input_path, output_path) tuples + + Returns: + tuple[List[str], int]: List of output object file paths and + count of modified files + """ + # Process files in parallel + outfiles = [] + modified = 0 + + with ThreadPoolExecutor(max_workers=self.max_workers) as executor: + # Submit all jobs + future_to_item = { + executor.submit(self._process_single_file, path, outfile): + (path, outfile) + for path, outfile in work_items + } + + # Collect results + for future in as_completed(future_to_item): + path, outfile = future_to_item[future] + was_modified = future.result() + if was_modified: + modified += 1 + outfiles.append(outfile) + + # Sort outfiles to maintain consistent order + outfiles.sort() + return outfiles, modified + + @staticmethod + def apply_renames(obj_files, syms, outdir: str, max_workers, verbose=False): + """Apply symbol redefinitions to object files using objcopy + + Args: + obj_files (List[str]): List of object file paths + syms (List[Symbol]): List of symbols + outdir (str): Directory to write modified object files + max_workers (int): Number of parallel workers + verbose (bool): Whether to show verbose output + + Returns: + tuple[List[str], int]: List of output object file paths and + count of modified files + """ + if not syms: + return obj_files, 0 + + redefiner = SymbolRedefiner(syms, outdir, max_workers, verbose) + + # Setup: create output directory and prepare work items + os.makedirs(outdir, exist_ok=True) + + # Prepare work items - just input and output paths + work_items = [] + for path in obj_files: + uniq = os.path.relpath(path).replace('/', '_') + outfile = os.path.join(outdir, uniq) + work_items.append((path, outfile)) + + return redefiner.process(work_items) + + +class ApiGenerator: + """Generates API headers with renamed function declarations + + Processes symbols and creates a unified header file with renamed function + declarations extracted from original header files. + """ + + def __init__(self, syms: List[Symbol], include_dir: str, verbose=False): + """Initialize with symbols and include directory + + Args: + syms (List[Symbol]): List of symbols + include_dir (str): Directory to search for header files + verbose (bool): Whether to print status messages + """ + self.syms = syms + self.include_dir = include_dir + self.verbose = verbose + self.missing_decls = [] + self.missing_hdrs = [] + + def process_header(self, hdr: str, header_syms: List[Symbol]): + """Process a single header file and its symbols + + Args: + hdr (str): Header file name + header_syms (List[Symbol]): Symbols from this header + + Returns: + List[str]: Lines for this header section + """ + lines = [f'/* Functions from {hdr} */'] + + path = os.path.join(self.include_dir, hdr) + if not os.path.exists(path): + self.missing_hdrs.append(hdr) + else: + # Extract and rename declarations from the actual header + for sym in header_syms: + orig = DeclExtractor.extract_decl( + path, sym.orig) + if orig: + # Rename the function in the declaration + renamed_decl = rename_function( + orig, sym.orig, sym.new_name) + lines.append(renamed_decl) + else: + self.missing_decls.append((sym.orig, hdr)) + lines.append('') + + lines.append('') + return lines + + def check_errors(self): + """Check for missing headers or declarations and build error message + + Returns: + str: Error messages, or '' if None + """ + msgs = [] + if self.missing_hdrs: + msgs.append('') + msgs.append('Missing header files:') + for header in self.missing_hdrs: + msgs.append(f' - {header}') + + if self.missing_decls: + msgs.append('') + msgs.append('Missing function declarations:') + for func_name, hdr in self.missing_decls: + msgs.append(f' - {func_name} in {hdr}') + + return '\n'.join(msgs) + + def generate(self, outfile: str): + """Generate the API header file + + Args: + outfile (str): Path where to write the new header file + + Returns: + int: 0 on success, 1 on error + """ + # Process each header file + out = [] + sorted_syms = sorted(self.syms, key=lambda s: s.hdr) + by_header = {hdr: list(syms) + for hdr, syms in groupby(sorted_syms, key=lambda s: s.hdr)} + for hdr, syms in by_header.items(): + out.extend(self.process_header(hdr, syms)) + + # Check for errors and abort if any declarations are missing + error_msg = self.check_errors() + if error_msg: + print(error_msg, file=sys.stderr) + return 1 + + # Write the header file + content = API_HEADER + '\n'.join(out) + API_FOOTER + tools.write_file(outfile, content, binary=False) + if self.verbose: + print(f'Generated API header: {outfile}') + + return 0 + + @staticmethod + def generate_hdr(syms, include_dir, outfile, verbose=False): + """Generate a new header file with renamed function declarations + + Args: + syms (List[Symbol]): List of symbols + include_dir (str): Directory to search for header files + outfile (str): Path where to write the new header file + verbose (bool): Whether to print status messages + + Returns: + int: 0 on success, 1 on error + """ + if not syms: + print('Warning: No symbols found', file=sys.stderr) + return 0 + + generator = ApiGenerator(syms, include_dir, verbose) + return generator.generate(outfile) + + +def run_tests(processes, test_name): # pragma: no cover + """Run all the tests we have for build_api + + Args: + processes (int): Number of processes to use to run tests + test_name (str): Name of specific test to run, or None to run all tests + + Returns: + int: 0 if successful, 1 if not + """ + # pylint: disable=import-outside-toplevel,import-error + # Import our test module + test_dir = os.path.join(os.path.dirname(__file__), '../test/scripts') + sys.path.insert(0, test_dir) + + import test_build_api + + sys.argv = [sys.argv[0]] + + result = test_util.run_test_suites( + toolname='build_api', debug=True, verbosity=2, no_capture=False, + test_preserve_dirs=False, processes=processes, test_name=test_name, + toolpath=[], + class_and_module_list=[test_build_api.TestBuildApi]) + + return 0 if result.wasSuccessful() else 1 + + +def run_test_coverage(): # pragma: no cover + """Run the tests and check that we get 100% coverage""" + sys.argv = [sys.argv[0]] + test_util.run_test_coverage('scripts/build_api.py', None, + ['tools/u_boot_pylib/*', '*/test*'], '.') + + +def parse_args(argv): + """Parse and validate command line arguments + + Args: + argv (List[str]): Arguments to parse + + Returns: + tuple: (args, error_code) where args is argparse.Namespace or None, + and error_code is 0 for success or 1 for error + """ + parser = argparse.ArgumentParser( + description='Parse rename.syms file and show symbols') + parser.add_argument('rename_syms', nargs='?', + help='Path to rename.syms file') + parser.add_argument('-d', '--dump', action='store_true', + help='Dump parsed symbols') + parser.add_argument('-r', '--redefine', nargs='*', metavar='OBJ_FILE', + help='Apply symbol redefinitions to object files') + parser.add_argument('-a', '--api', metavar='HEADER_FILE', + help='Generate API header with renamed functions') + parser.add_argument('-i', '--include-dir', metavar='DIR', + help='Include directory containing header files') + parser.add_argument('-o', '--output-dir', metavar='DIR', + help='Output directory for modified object files') + parser.add_argument('-v', '--verbose', action='store_true', + help='Show verbose output') + parser.add_argument('-j', '--jobs', type=int, metavar='N', + help='Number of parallel jobs for symbol processing') + parser.add_argument('-P', '--processes', type=int, + help='set number of processes to use for running tests') + parser.add_argument('-t', '--test', action='store_true', dest='test', + default=False, help='run tests') + parser.add_argument('-T', '--test-coverage', action='store_true', + default=False, + help='run tests and check for 100%% coverage') + args = parser.parse_args(argv) + + # Check if running tests - if so, rename_syms is optional + running_tests = args.test or args.test_coverage + + if not running_tests and not args.rename_syms: # pragma: no cover + print('Error: rename_syms is required unless running tests', + # pragma: no cover + file=sys.stderr) # pragma: no cover + return None, 1 # pragma: no cover + + # Validate argument combinations + if args.redefine is not None and not args.redefine: + # args.redefine is [] when --redefine used with no object files + print('Error: --redefine requires at least one object file', + file=sys.stderr) + return None, 1 + + if args.redefine is not None and not args.output_dir: + print('Error: --output-dir is required with --redefine', + file=sys.stderr) + return None, 1 + + if args.api and not args.include_dir: + print('Error: --include-dir is required with --api', + file=sys.stderr) + return None, 1 + + return args, 0 + + +def main(argv=None): + """Main entry point for the script + + Args: + argv (List[str], optional): Arguments to parse. Uses sys.argv[1:] + if None. + + Returns: + int: Exit code (0 for success, 1 for error) + """ + if argv is None: + argv = sys.argv[1:] + args, error_code = parse_args(argv) + if error_code: + return error_code + + # Handle test options + if args.test: # pragma: no cover + test_name = args.rename_syms # pragma: no cover + return run_tests(args.processes, test_name) # pragma: no cover + + if args.test_coverage: # pragma: no cover + run_test_coverage() # pragma: no cover + return 0 # pragma: no cover + + symbols_parser = RenameSymsParser(args.rename_syms) + syms = symbols_parser.parse() + + if args.dump: + symbols_parser.dump() + + if args.redefine is not None: + # Determine number of jobs + jobs = args.jobs if args.jobs else min(os.cpu_count() or 4, 8) + start_time = time.time() + outfiles, modified = SymbolRedefiner.apply_renames( + args.redefine, syms, args.output_dir, jobs, args.verbose) + # Print the list of output files for the build system to use + if args.output_dir: + print('\n'.join(outfiles)) + elapsed = time.time() - start_time + if args.verbose: + print(f'Processed {len(args.redefine)} files ({modified} modified) ' + f'in {elapsed:.3f} seconds ({jobs} threads)', file=sys.stderr) + + if args.api: + result = ApiGenerator.generate_hdr(syms, args.include_dir, args.api, + args.verbose) + if result: + return result + + return 0 + + +if __name__ == '__main__': # pragma: no cover + sys.exit(main()) diff --git a/test/scripts/test_build_api.py b/test/scripts/test_build_api.py new file mode 100644 index 00000000000..8295c08715e --- /dev/null +++ b/test/scripts/test_build_api.py @@ -0,0 +1,704 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0 +# pylint: disable=cyclic-import +"""Test suite for build_api.py script""" + +import contextlib +from io import StringIO +import os +import subprocess +import sys +import tempfile +import unittest + +# Add the scripts directory to the path +script_dir = os.path.join(os.path.dirname(__file__), '..', '..', 'scripts') +sys.path.insert(0, script_dir) + +# Add the tools directory to the path for u_boot_pylib +tools_dir = os.path.join(os.path.dirname(__file__), '..', '..', 'tools') +sys.path.insert(0, tools_dir) + +# pylint: disable=wrong-import-position,import-error +from build_api import rename_function, RenameSymsParser, DeclExtractor +from build_api import ApiGenerator, SymbolRedefiner, main +from u_boot_pylib import tools + + +class TestBuildApi(unittest.TestCase): + # pylint: disable=too-many-public-methods + """Test suite for build_api.py script""" + + def setUp(self): + """Create temporary files for testing""" + # pylint: disable=R1732 + self.tmpdir = tempfile.TemporaryDirectory() + # Create a temp file path for symbols.syms that tests can write to + self.sympath = os.path.join(self.tmpdir.name, 'symbols.syms') + + def tearDown(self): + """Clean up temporary files""" + self.tmpdir.cleanup() + + def write_tmp(self, content, filename): + """Create a temporary text file with given content""" + temp_path = os.path.join(self.tmpdir.name, filename) + tools.write_file(temp_path, content, binary=False) + return temp_path + + def test_rename_function(self): + """Test basic function renaming""" + source_code = ''' +/** + * sprintf() - Format a string and place it in a buffer + * + * @buf: The buffer to place the result into + * @fmt: The format string to use + * @...: Arguments for the format string + * + * The function returns the number of characters written + * into @buf. + * + * See the vsprintf() documentation for format string extensions over C99. + */ +int sprintf(char *buf, const char *fmt, ...) +__attribute__ ((format (__printf__, 2, 3))); +''' + result = rename_function(source_code, 'sprintf', 'my_sprintf') + + # Check that the function name was changed + assert 'int my_sprintf(char *buf' in result + assert 'int sprintf(char *buf' not in result + + def test_rename_sym_parser(self): + """Test parsing symbol definition file format""" + content = '''# Test symbols.syms file +file: stdio.h + printf + scanf + +file: string.h + strcpy + strlen=ub_str_length + +file: stdlib.h + malloc=custom_malloc +''' + tools.write_file(self.sympath, content, binary=False) + + parser = RenameSymsParser(self.sympath) + renames = parser.parse() + + # Check we got the right number of renames + assert len(renames) == 5 + + # Check default prefix mapping + printf_rename = next(r for r in renames if r.orig == 'printf') + assert printf_rename.hdr == 'stdio.h' + assert printf_rename.new_name == 'ub_printf' + + # Check explicit mapping + strlen_rename = next(r for r in renames if r.orig == 'strlen') + assert strlen_rename.hdr == 'string.h' + assert strlen_rename.new_name == 'ub_str_length' + malloc_rename = next(r for r in renames if r.orig == 'malloc') + assert malloc_rename.hdr == 'stdlib.h' + assert malloc_rename.new_name == 'custom_malloc' + + def test_rename_sym_with_real_file(self): + """Test parsing with realistic symbols.syms file""" + symbols_content = '''# Symbols for U-Boot library +file: stdio.h + printf + sprintf + snprintf + scanf + sscanf + +file: string.h + memcpy + memset + strlen + strcpy + strcmp + +file: stdlib.h + malloc + free + calloc +''' + symbols_path = self.write_tmp(symbols_content, 'realistic_symbols.syms') + + parser = RenameSymsParser(symbols_path) + + # Should have some renames + renames = parser.parse() + assert renames + + # Check that printf gets renamed to ub_printf + printf_rename = next((r for r in renames if r.orig == 'printf'), None) + assert printf_rename is not None + assert printf_rename.hdr == 'stdio.h' + assert printf_rename.new_name == 'ub_printf' + + def test_rename_with_parser(self): + """Test integration between parser and renaming""" + content = '''file: stdio.h + sprintf + printf +''' + tools.write_file(self.sympath, content, binary=False) + parser = RenameSymsParser(self.sympath) + renames = parser.parse() + # Use the parser results to rename functions in source code + source_code = ''' +int sprintf(char *buf, const char *fmt, ...); +int printf(const char *fmt, ...); +''' + result = source_code + for rename in renames: + result = rename_function(result, rename.orig, rename.new_name) + + # Check that both functions were renamed + assert 'int ub_sprintf(char *buf' in result + assert 'int ub_printf(const char *fmt' in result + assert 'int sprintf(char *buf' not in result + assert 'int printf(const char *fmt' not in result + + def test_redefine_option(self): + """Test symbol redefinition in object files""" + content = '''file: stdio.h + printf +''' + rename_syms = self.write_tmp(content, 'redefine_symbols.syms') + + # Create a simple C file with printf (use format string to prevent + # optimization to puts) + c_code = ''' +#include <stdio.h> +void test_function() { + printf("%s %d\\n", "Hello", 123); +} +''' + c_file_path = self.write_tmp(c_code, 'test.c') + obj_file_path = c_file_path.replace('.c', '.o') + # obj file will be cleaned up automatically with tmpdir + + # Compile the C file to object file + compile_cmd = ['gcc', '-c', c_file_path, '-o', obj_file_path] + subprocess.run(compile_cmd, capture_output=True, text=True, check=True) + + # Check that the object file contains printf symbol + nm_cmd = ['nm', obj_file_path] + result = subprocess.run(nm_cmd, capture_output=True, text=True, + check=True) + assert 'printf' in result.stdout + + # Test the parser + parser = RenameSymsParser(rename_syms) + renames = parser.parse() + + # Verify we have the expected rename + assert len(renames) == 1 + assert renames[0].orig == 'printf' + assert renames[0].new_name == 'ub_printf' + assert renames[0].hdr == 'stdio.h' + + # Test the actual symbol redefinition + outfiles, modified = SymbolRedefiner.apply_renames( + [obj_file_path], renames, self.tmpdir.name, 1) + assert outfiles + assert modified == 1 # Should have modified 1 file + obj_file_path = outfiles[0] # Use the output file for checking + + # Check that the symbol was renamed + nm_cmd = ['nm', obj_file_path] + result = subprocess.run(nm_cmd, capture_output=True, text=True, + check=True) + + # Should now have ub_printf instead of printf + out = result.stdout.replace('ub_printf', '') + assert 'ub_printf' in result.stdout + assert 'printf' not in out + + def test_extract_decl(self): + """Test extracting function declarations from headers""" + content = '''#ifndef TEST_H +#define TEST_H + +/** + * sprintf() - Format a string and place it in a buffer + * + * @buf: The buffer to place the result into + * @fmt: The format string to use + * @...: Arguments for the format string + * + * The function returns the number of characters written + * into @buf. + */ +int sprintf(char *buf, const char *fmt, ...) +\t\t__attribute__ ((format (__printf__, 2, 3))); + +// Another function without detailed comment + +int printf(const char *fmt, ...); + +/** + * strlen() - Calculate the length of a string + * @s: The string to measure + * + * Return: The length of the string + */ + +size_t strlen(const char *s); + +/* Broken comment block - ends without proper start */ +*/ +#define SOME_MACRO 1 +int broken_comment_func(void); + +/* Normal function preceded by non-comment content */ +int other_content_func(void); + +#endif +''' + hdr = self.write_tmp(content, 'test.h') + # Test finding sprintf with comment + decl = DeclExtractor.extract_decl(hdr, 'sprintf') + assert decl is not None + expected = '''/** + * sprintf() - Format a string and place it in a buffer + * + * @buf: The buffer to place the result into + * @fmt: The format string to use + * @...: Arguments for the format string + * + * The function returns the number of characters written + * into @buf. + */ +int sprintf(char *buf, const char *fmt, ...) +\t\t__attribute__ ((format (__printf__, 2, 3)));''' + assert decl == expected, ( + f'Expected:\n{expected}\n\nGot:\n{decl}') + + # Test finding printf without detailed comment + decl = DeclExtractor.extract_decl(hdr, 'printf') + assert decl is not None + expected = '''// Another function without detailed comment + +int printf(const char *fmt, ...);''' + assert decl == expected, ( + f'Expected:\n{expected}\n\nGot:\n{decl}') + + # Test finding strlen with comment + strlen_decl = DeclExtractor.extract_decl(hdr, 'strlen') + assert strlen_decl is not None + expected_strlen = '''/** + * strlen() - Calculate the length of a string + * @s: The string to measure + * + * Return: The length of the string + */ + +size_t strlen(const char *s);''' + assert strlen_decl == expected_strlen, ( + f'Expected:\n{expected_strlen}\n\nGot:\n{strlen_decl}') + + # Test function not found + assert not DeclExtractor.extract_decl(hdr, 'nonexistent') + + # Test function with broken comment block (should return None) + broken_decl = DeclExtractor.extract_decl(hdr, 'broken_comment_func') + assert broken_decl is not None + assert 'int broken_comment_func(void);' in broken_decl + + # Test function preceded by non-comment content (no comment) + other_decl = DeclExtractor.extract_decl(hdr, 'other_content_func') + assert other_decl is not None + assert 'int other_content_func(void);' in other_decl + + def test_extract_decl_malformed_comment(self): + """Test extracting declaration with malformed comment block""" + # Create header where */ appears but no /** is found backwards + content = '''#ifndef TEST_H +#define TEST_H + +some code here +*/ +int malformed_func(void); + +#endif +''' + hdr = self.write_tmp(content, 'malformed.h') + + # This should find the function but no comment (malformed comment) + decl = DeclExtractor.extract_decl(hdr, 'malformed_func') + assert decl is not None + assert decl == 'int malformed_func(void);' + + def test_symbol_redefiner_coverage(self): + """Test SymbolRedefiner edge cases for better coverage""" + content = '''file: stdio.h + printf + custom_func +''' + rename_syms = self.write_tmp(content, 'coverage_symbols.syms') + + # Create C file with defined symbol (not just undefined reference) + c_code_defined = ''' +void printf(const char *fmt, ...) { + // Custom printf implementation +} +''' + c_file_defined = self.write_tmp(c_code_defined, 'defined_symbol.c') + obj_file_defined = c_file_defined.replace('.c', '.o') + + # Compile to create object with defined symbol + compile_cmd = ['gcc', '-c', c_file_defined, '-o', obj_file_defined] + subprocess.run(compile_cmd, capture_output=True, text=True, check=True) + + # Create C file with no target symbols at all + c_code_no_symbols = ''' +void other_func(void) { + int x = 42; +} +''' + c_file_no_symbols = self.write_tmp(c_code_no_symbols, 'no_symbols.c') + obj_file_no_symbols = c_file_no_symbols.replace('.c', '.o') + + compile_cmd = ['gcc', '-c', c_file_no_symbols, '-o', + obj_file_no_symbols] + subprocess.run(compile_cmd, capture_output=True, text=True, check=True) + + # Test with both files + parser = RenameSymsParser(rename_syms) + renames = parser.parse() + + # This should process both files - one with defined symbol, one without + # target symbols + # Test with verbose output + stdout = StringIO() + with contextlib.redirect_stdout(stdout): + outfiles, modified = SymbolRedefiner.apply_renames( + [obj_file_defined, obj_file_no_symbols], renames, + self.tmpdir.name, 1, verbose=True) + + assert outfiles + assert len(outfiles) == 2 + # Should have modified 1 file (the one with defined symbol) + assert modified == 1 + assert 'Copied and modified' in stdout.getvalue() + + def test_apply_renames_empty_symbols(self): + """Test SymbolRedefiner.apply_renames with empty symbol list""" + # Create a simple object file + c_code = ''' +void test_func(void) { + int x = 42; +} +''' + c_file = self.write_tmp(c_code, 'test_empty_syms.c') + obj_file = c_file.replace('.c', '.o') + + compile_cmd = ['gcc', '-c', c_file, '-o', obj_file] + subprocess.run(compile_cmd, capture_output=True, text=True, check=True) + + # Call apply_renames with empty symbol list + empty_syms = [] + obj_files = [obj_file] + result_files, modified = SymbolRedefiner.apply_renames( + obj_files, empty_syms, self.tmpdir.name, 1) + + # Should return the original obj_files unchanged and 0 modified + assert result_files == obj_files + assert modified == 0 + + def test_api_generation_empty_symbols(self): + """Test API generation with empty symbol list""" + api_file = self.write_tmp('', 'empty_api.h') + + # Test generate_hdr with empty symbol list + stderr = StringIO() + with contextlib.redirect_stderr(stderr): + result = ApiGenerator.generate_hdr([], '/nonexistent', api_file) + + # Should return 0 and print warning + assert result == 0 + assert 'Warning: No symbols found' in stderr.getvalue() + + def test_parse_args_errors(self): + """Test main() with parse_args validation errors""" + + # Test 1: --redefine with no object files + test_args = ['test.syms', '--redefine', '--output-dir', '/tmp'] + + stderr = StringIO() + with contextlib.redirect_stderr(stderr): + result = main(test_args) + + assert result == 1 + assert 'Error: --redefine requires at least one object file' in \ + stderr.getvalue() + + # Test 2: --redefine without --output-dir + test_args = ['test.syms', '--redefine', 'test.o'] + + stderr = StringIO() + with contextlib.redirect_stderr(stderr): + result = main(test_args) + + assert result == 1 + assert 'Error: --output-dir is required with --redefine' in \ + stderr.getvalue() + + # Test 3: --api without --include-dir + test_args = ['test.syms', '--api', 'api.h'] + + stderr = StringIO() + with contextlib.redirect_stderr(stderr): + result = main(test_args) + + assert result == 1 + assert 'Error: --include-dir is required with --api' in stderr.getvalue() + + def test_main_function_paths(self): + """Test main function with different argument combinations""" + + # Create test files + content = '''file: stdio.h + printf +''' + rename_syms = self.write_tmp(content, 'rename.syms') + + c_code = ''' +#include <stdio.h> +void test_function() { + printf("%s\\n", "test"); +} +''' + c_file = self.write_tmp(c_code, 'main_test.c') + obj_file = c_file.replace('.c', '.o') + + compile_cmd = ['gcc', '-c', c_file, '-o', obj_file] + subprocess.run(compile_cmd, capture_output=True, text=True, check=True) + + # Test redefine path + test_args = [rename_syms, '--redefine', obj_file, '--output-dir', + self.tmpdir.name, '--verbose'] + stdout = StringIO() + stderr = StringIO() + with (contextlib.redirect_stdout(stdout), + contextlib.redirect_stderr(stderr)): + result = main(test_args) + assert result == 0 + + # Check that timing message was printed to stderr with verbose + stderr = stderr.getvalue() + assert 'Processed 1 files (0 modified) in' in stderr + + def test_main_function_with_jobs(self): + """Test main function with --jobs option to exercise max_workers path""" + + # Create test files + content = '''file: stdio.h + printf +''' + rename_syms = self.write_tmp(content, 'rename.syms') + + c_code = ''' +#include <stdio.h> +void test_function() { + printf("%s\\n", "test"); +} +''' + c_file = self.write_tmp(c_code, 'jobs_test.c') + obj_file = c_file.replace('.c', '.o') + + compile_cmd = ['gcc', '-c', c_file, '-o', obj_file] + subprocess.run(compile_cmd, capture_output=True, text=True, check=True) + + # Test redefine path with explicit --jobs option + test_args = [rename_syms, '--redefine', obj_file, '--output-dir', + self.tmpdir.name, '--jobs', '2', '--verbose'] + stdout = StringIO() + stderr = StringIO() + with (contextlib.redirect_stdout(stdout), + contextlib.redirect_stderr(stderr)): + result = main(test_args) + assert result == 0 + + # Check that timing message includes thread count + stderr = stderr.getvalue() + assert 'Processed 1 files (0 modified) in' in stderr + + # Test API generation path with verbose output + fake_stdio = '''#ifndef STDIO_H +#define STDIO_H +int printf(const char *fmt, ...); +#endif +''' + self.write_tmp(fake_stdio, 'stdio.h') + api_file = self.write_tmp('', 'main_api.h') + + test_args = [rename_syms, '--api', api_file, '--include-dir', + self.tmpdir.name, '--output-dir', self.tmpdir.name, + '--verbose'] + + stdout = StringIO() + with contextlib.redirect_stdout(stdout): + result = main(test_args) + + assert result == 0 + assert 'Generated API header:' in stdout.getvalue() + + def test_main_api_generation_failure(self): + """Test main() when API generation fails""" + + # Create test files that will cause API generation to fail + content = '''file: nonexistent.h + missing_function +''' + rename_syms = self.write_tmp(content, 'failing_api.syms') + api_file = self.write_tmp('', 'failing_api.h') + + # This will fail because nonexistent.h doesn't exist + test_args = [rename_syms, '--api', api_file, '--include-dir', + '/nonexistent_dir', '--output-dir', self.tmpdir.name] + + stderr = StringIO() + with contextlib.redirect_stderr(stderr): + result = main(test_args) + + # Should return 1 because API generation failed + assert result == 1 + assert 'Missing header files:' in stderr.getvalue() + + def test_api_generation(self): + """Test API header generation""" + content = '''file: stdio.h + printf +''' + tools.write_file(self.sympath, content, binary=False) + + api = self.write_tmp('', 'api.h') + parser = RenameSymsParser(self.sympath) + renames = parser.parse() + + # Generate the API header - this will fail since stdio.h is not found + captured = StringIO() + with contextlib.redirect_stderr(captured): + result = ApiGenerator.generate_hdr(renames, '/nonexistent', api) + + # This test expects failure since stdio.h header is not available + assert result == 1 + + def test_api_generation_missing_headers(self): + """Test API generation error handling for missing header files""" + content = '''file: nonexistent.h + missing_func +''' + tools.write_file(self.sympath, content, binary=False) + + api = self.write_tmp('', 'api.h') + parser = RenameSymsParser(self.sympath) + renames = parser.parse() + + # This should exit with an error + captured = StringIO() + with contextlib.redirect_stderr(captured): + result = ApiGenerator.generate_hdr(renames, '/nonexistent', api) + assert result == 1, f'Expected return code 1, got {result}' + + assert 'Missing header files:' in captured.getvalue() + assert 'nonexistent.h' in captured.getvalue() + + def test_api_generation_missing_functions(self): + """Test API generation error handling for missing functions""" + # Create a fake stdio.h with a different function for testing + fake_stdio_content = '''#ifndef STDIO_H +#define STDIO_H +int existing_func(void); +#endif +''' + self.write_tmp(fake_stdio_content, 'stdio.h') + include_dir = self.tmpdir.name + + content = '''file: stdio.h + nonexistent_function +''' + tools.write_file(self.sympath, content, binary=False) + + api = self.write_tmp('', 'api.h') + parser = RenameSymsParser(self.sympath) + renames = parser.parse() + + # This should exit with an error for missing function declarations + captured = StringIO() + with contextlib.redirect_stderr(captured): + result = ApiGenerator.generate_hdr(renames, include_dir, api) + assert result == 1, f'Expected return code 1, got {result}' + + assert 'Missing function declarations:' in captured.getvalue() + assert 'nonexistent_function in stdio.h' in captured.getvalue() + + def test_parser_exceptions(self): + """Test parser error handling for invalid formats""" + + # Test 1: Symbol without header file + inval1 = '''# Test file with symbol before header + printf +file: stdio.h + scanf +''' + temp_path1 = self.write_tmp(inval1, 'test1.syms') + parser = RenameSymsParser(temp_path1) + with self.assertRaises(ValueError) as cm: + parser.parse() + self.assertIn("Symbol 'printf' found without a header file directive", + str(cm.exception)) + + # Test 2: Invalid format (non-indented, non-file line) + inval2 = '''file: stdio.h + printf +invalid_line_here + scanf +''' + temp_path2 = self.write_tmp(inval2, 'test2.syms') + parser = RenameSymsParser(temp_path2) + with self.assertRaises(ValueError) as cm: + parser.parse() + self.assertIn("Invalid format - symbols must be indented", + str(cm.exception)) + + def test_main_dump_symbols(self): + """Test main function with dump option""" + content = '''file: stdio.h + printf + sprintf + +file: string.h + strlen +''' + rename_syms = self.write_tmp(content, 'test_symbols.syms') + + # Mock sys.argv to simulate command line arguments + original_argv = sys.argv + try: + sys.argv = ['build_api.py', rename_syms, '--dump'] + + # Capture stdout to check dump output + captured = StringIO() + with contextlib.redirect_stdout(captured): + result = main() + + assert result == 0 + output = captured.getvalue() + assert all(item in output for item in + ['printf', 'sprintf', 'strlen', 'stdio.h', 'string.h']) + + finally: + sys.argv = original_argv + + +if __name__ == "__main__": + unittest.main() -- 2.43.0
participants (3)
-
Heinrich Schuchardt -
Simon Glass -
Simon Glass