test/py: Add a test for ulib functionality

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.

Series-to: concept
Series-cc: heinrich
Cover-letter:
ulib: Complete initial U-Boot library

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.
END

Co-developed-by: Claude <noreply@anthropic.com>
Signed-off-by: Simon Glass <sjg@chromium.org>
This commit is contained in:
Simon Glass
2025-09-06 17:22:17 -06:00
parent cf01d4702f
commit f457b31524
4 changed files with 155 additions and 6 deletions

2
examples/ulib/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
/demo
/demo_static

View File

@@ -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;
}

141
test/py/tests/test_ulib.py Normal file
View File

@@ -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

View File

@@ -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