tclibc-picolibc: Adds a new TCLIBC variant to build with picolibc as C library

Enables usage of TCLIBC=picolibc extending OE functionality to build and use
picolibc based toolchains to build baremetal applications.

Picolibc is a set of standard C libraries, both libc and libm, designed for
smaller embedded systems with limited ROM and RAM. Picolibc includes code
from Newlib and AVR Libc, but adresses some of newlibs concerns, it retains
newlibs directory structure, math, string and locale implementations, but
removed the GPL bits used to build the library, swiches old C style code for
C18 and replaces autotools with meson.

This patch adds a picolibc recipe for the C library, a picolibc-helloworld
recipe that contains an example application and a testcase that builds it.

Picolibc can be built for ARM and RISCV architectures, its been tested both
for 32 and 64 bits, the provided example recipe produces the following output:

hello, world

Runqemu does not automatically show any output since it hides QEMU stderr which
is where the QEMU monitors output is directed to when using semihosting, but,
manually running the same QEMU command does work properly.

(From OE-Core rev: c7535ecaccb72ef21a61f9aec5c68e61fb4f6fb6)

Signed-off-by: Alejandro Enedino Hernandez Samaniego <alejandro@enedino.org>
Signed-off-by: Richard Purdie <richard.purdie@linuxfoundation.org>
This commit is contained in:
Alejandro Hernandez Samaniego 2024-06-18 12:12:26 -06:00 committed by Richard Purdie
parent beabc787ca
commit 09b49a35e1
16 changed files with 296 additions and 5 deletions

View File

@ -16,8 +16,8 @@
# See meta-skeleton for a working example.
# Toolchain should be baremetal or newlib based.
# TCLIBC="baremetal" or TCLIBC="newlib"
# Toolchain should be baremetal or newlib/picolibc based.
# TCLIBC="baremetal" or TCLIBC="newlib" or TCLIBC="picolibc"
COMPATIBLE_HOST:libc-musl:class-target = "null"
COMPATIBLE_HOST:libc-glibc:class-target = "null"

View File

@ -36,7 +36,7 @@ python () {
if d.getVar("MODIFYTOS") != "1":
return
if d.getVar("TCLIBC") in [ 'baremetal', 'newlib' ]:
if d.getVar("TCLIBC") in [ 'baremetal', 'newlib', 'picolibc' ]:
return
tos = d.getVar("TARGET_OS")

View File

@ -576,6 +576,8 @@ RECIPE_MAINTAINER:pn-pcmanfm = "Alexander Kanavin <alex.kanavin@gmail.com>"
RECIPE_MAINTAINER:pn-perf = "Bruce Ashfield <bruce.ashfield@gmail.com>"
RECIPE_MAINTAINER:pn-perl = "Alexander Kanavin <alex.kanavin@gmail.com>"
RECIPE_MAINTAINER:pn-perlcross = "Alexander Kanavin <alex.kanavin@gmail.com>"
RECIPE_MAINTAINER:pn-picolibc = "Alejandro Hernandez <alejandro@enedino.org>"
RECIPE_MAINTAINER:pn-picolibc-helloworld = "Alejandro Hernandez <alejandro@enedino.org>"
RECIPE_MAINTAINER:pn-piglit = "Ross Burton <ross.burton@arm.com>"
RECIPE_MAINTAINER:pn-pigz = "Hongxu Jia <hongxu.jia@windriver.com>"
RECIPE_MAINTAINER:pn-pinentry = "Unassigned <unassigned@yoctoproject.org>"

View File

@ -0,0 +1,40 @@
#
# Picolibc configuration
#
LIBCEXTENSION = "-picolibc"
LIBCOVERRIDE = ":libc-picolibc"
PREFERRED_PROVIDER_virtual/libc ?= "picolibc"
PREFERRED_PROVIDER_virtual/libiconv ?= "picolibc"
PREFERRED_PROVIDER_virtual/libintl ?= "picolibc"
PREFERRED_PROVIDER_virtual/nativesdk-libintl ?= "nativesdk-glibc"
PREFERRED_PROVIDER_virtual/nativesdk-libiconv ?= "nativesdk-glibc"
DISTRO_FEATURES_BACKFILL_CONSIDERED += "ldconfig"
IMAGE_LINGUAS = ""
LIBC_DEPENDENCIES = " \
picolibc-dbg \
picolibc-dev \
libgcc-dev \
libgcc-dbg \
libstdc++-dev \
libstdc++-staticdev \
"
ASSUME_PROVIDED += "virtual/crypt"
TARGET_OS = "elf"
TARGET_OS:arm = "eabi"
TOOLCHAIN_HOST_TASK ?= "packagegroup-cross-canadian-${MACHINE} nativesdk-qemu nativesdk-sdk-provides-dummy"
TOOLCHAIN_TARGET_TASK ?= "${LIBC_DEPENDENCIES}"
TOOLCHAIN_NEED_CONFIGSITE_CACHE:remove = "zlib ncurses"
# RISCV linker doesnt support PIE
SECURITY_CFLAGS:libc-picolibc:qemuriscv32 = "${SECURITY_NOPIE_CFLAGS}"
SECURITY_CFLAGS:libc-picolibc:qemuriscv64 = "${SECURITY_NOPIE_CFLAGS}"

View File

@ -421,7 +421,7 @@ TARGET_FPU[doc] = "Specifies the method for handling FPU code. For FPU-less targ
TARGET_OS[doc] = "Specifies the target's operating system."
TARGET_PREFIX[doc] = "The prefix for the cross-compile toolchain (e.g. arm-linux-)."
TARGET_SYS[doc] = "The target system is comprised of TARGET_ARCH,TARGET_VENDOR and TARGET_OS."
TCLIBC[doc] = "Specifies C library (libc) variant to use during the build process. You can select 'baremetal', 'glibc', 'musl' or 'newlib'."
TCLIBC[doc] = "Specifies C library (libc) variant to use during the build process. You can select 'baremetal', 'glibc', 'musl', 'newlib', or 'picolibc'."
TCMODE[doc] = "Enables an external toolchain (where provided by an additional layer) if set to a value other than 'default'."
TESTIMAGE_AUTO[doc] = "Enables test booting of virtual machine images under the QEMU emulator after any root filesystems are created and runs tests against those images each time an image is built."
TEST_QEMUBOOT_TIMEOUT[doc] = "The time in seconds allowed for an image to boot before automated runtime tests begin to run against an image."

View File

@ -11,5 +11,6 @@ TUNE_CCARGS:append = "${@bb.utils.contains('TUNE_FEATURES', 'riscv64nc', ' -marc
# Fix: ld: unrecognized option '--hash-style=sysv'
LINKER_HASH_STYLE:libc-newlib = ""
LINKER_HASH_STYLE:libc-picolibc = ""
# Fix: ld: unrecognized option '--hash-style=gnu'
LINKER_HASH_STYLE:libc-baremetal = ""

View File

@ -55,7 +55,7 @@ but their recipes claim otherwise by setting UPSTREAM_VERSION_UNKNOWN. Please re
return False
def is_maintainer_exception(entry):
exceptions = ["musl", "newlib", "linux-yocto", "linux-dummy", "mesa-gl", "libgfortran", "libx11-compose-data",
exceptions = ["musl", "newlib", "picolibc", "linux-yocto", "linux-dummy", "mesa-gl", "libgfortran", "libx11-compose-data",
"cve-update-nvd2-native",]
for i in exceptions:
if i in entry:

View File

@ -0,0 +1,18 @@
#
# Copyright OpenEmbedded Contributors
#
# SPDX-License-Identifier: MIT
#
from oeqa.selftest.case import OESelftestTestCase
from oeqa.utils.commands import bitbake, get_bb_var
class PicolibcTest(OESelftestTestCase):
def test_picolibc(self):
compatible_machines = ['qemuarm', 'qemuarm64', 'qemuriscv32', 'qemuriscv64']
machine = get_bb_var('MACHINE')
if machine not in compatible_machines:
self.skipTest('This test only works with machines : %s' % ' '.join(compatible_machines))
self.write_config('TCLIBC = "picolibc"')
bitbake("picolibc-helloworld")

View File

@ -0,0 +1,40 @@
require picolibc.inc
# baremetal-image overrides
BAREMETAL_BINNAME ?= "hello_picolibc_${MACHINE}"
IMAGE_LINK_NAME ?= "baremetal-picolibc-image-${MACHINE}"
IMAGE_NAME_SUFFIX ?= ""
QB_DEFAULT_KERNEL ?= "${IMAGE_LINK_NAME}.elf"
inherit baremetal-image
COMPATIBLE_MACHINE = "qemuarm|qemuarm64|qemuriscv32|qemuriscv64"
# Use semihosting to test via QEMU
QB_OPT_APPEND:append = " -semihosting-config enable=on"
# picolibc comes with a set of linker scripts, set the file
# according to the architecture being built.
PICOLIBC_LINKERSCRIPT:qemuarm64 = "aarch64.ld"
PICOLIBC_LINKERSCRIPT:qemuarm = "arm.ld"
PICOLIBC_LINKERSCRIPT:qemuriscv32 = "riscv.ld"
PICOLIBC_LINKERSCRIPT:qemuriscv64 = "riscv.ld"
# Simple compile function that manually exemplifies usage; as noted,
# use a custom linker script, the GCC specs provided by picolibc
# and semihost to be able to test via QEMU's monitor
do_compile(){
${CC} ${CFLAGS} ${LDFLAGS} --verbose -T${S}/hello-world/${PICOLIBC_LINKERSCRIPT} -specs=picolibc.specs --oslib=semihost -o ${BAREMETAL_BINNAME}.elf ${S}/hello-world/hello-world.c
${OBJCOPY} -O binary ${BAREMETAL_BINNAME}.elf ${BAREMETAL_BINNAME}.bin
}
do_install(){
install -d ${D}/${base_libdir}/firmware
install -m 755 ${B}/${BAREMETAL_BINNAME}.elf ${D}/${base_libdir}/firmware/${BAREMETAL_BINNAME}.elf
install -m 755 ${B}/${BAREMETAL_BINNAME}.bin ${D}/${base_libdir}/firmware/${BAREMETAL_BINNAME}.bin
}
FILES:${PN} += " \
${base_libdir}/firmware/${BAREMETAL_BINNAME}.elf \
${base_libdir}/firmware/${BAREMETAL_BINNAME}.bin \
"

View File

@ -0,0 +1,21 @@
SUMMARY = "C Libraries for Smaller Embedded Systems"
HOMEPAGE = "https://keithp.com/picolibc"
DESCRIPTION = "Picolibc is a set of standard C libraries, both libc and libm, designed for smaller embedded systems with limited ROM and RAM. Picolibc includes code from Newlib and AVR Libc."
SECTION = "libs"
# Newlib based code but GPL related bits removed, test/printf-tests.c and test/testcases.c
# are GPLv2 and GeneratePicolibcCrossFile.sh is AGPL3 but not part of the artifacts.
LICENSE = "BSD-2-Clause & BSD-3-Clause"
LIC_FILES_CHKSUM = " \
file://COPYING.GPL2;md5=59530bdf33659b29e73d4adb9f9f6552 \
file://COPYING.NEWLIB;md5=08ae03456feb75b81cfdb359e0f1ef85 \
file://COPYING.picolibc;md5=e50fa9458a40929689861ed472d46bc7 \
"
BASEVER = "1.8.6"
PV = "${BASEVER}+git"
SRC_URI = "git://github.com/picolibc/picolibc.git;protocol=https;branch=main"
SRCREV="764ef4e401a8f4c6a86ab723533841f072885a5b"
S = "${WORKDIR}/git"
B = "${WORKDIR}/build"

View File

@ -0,0 +1,119 @@
Upstream-Status: Pending
Picolibc uses its own specs file: picolibc.specs to facilitate compilation, this
needs to be passed down to GCC via the -specs argument.
Using this specs file overrides some of the default options our toolchain was
built with, in this case, they modify the include_dir and lib_dir paths used for
compilation, their intention was to add support for -picolibc-prefix and
-picolibc-buildtype arguments via the C preprocessor.
-isystem %{-picolibc-prefix=*:%*/include/; -picolibc-buildtype=*:/usr/include/%*; :/usr/include} %(picolibc_cpp)
This had the unwanted effect of defaulting to /usr/include for include_dir if
those arguments are not being passed, this works fine for their flow but for us
it pollutes the include directories with paths from the host. The same effect is
applicable for lib_dir and for the c runtime file.
Our toolchain relies on --sysroot to avoid using any paths from the host, here we
manually add support for a third possible argument: -sysroot , if this is passed
then the paths used by the compiler will be relative to the path passed by the
--sysroot= cmdline argument, setting back the behavior that we intended in the
first place.
Signed-off-by: Alejandro Enedino Hernandez Samaniego <alejandro@enedino.org>
Index: git/meson.build
===================================================================
--- git.orig/meson.build
+++ git/meson.build
@@ -622,12 +622,13 @@ else
#
picolibc_prefix_format = '-picolibc-prefix=*:@0@'
picolibc_buildtype_format = '-picolibc-buildtype=*:@0@'
+sysroot_format = '-sysroot=*:@0@'
gen_format = '@0@'
#
# How to glue the three options together
#
-specs_option_format = '%{@0@; @1@; :@2@}'
+specs_option_format = '%{@0@; @1@; @2@; :@3@}'
#
# Build the -isystem value
@@ -639,10 +640,13 @@ isystem_prefix = picolibc_prefix_format.
buildtype_include_dir = specs_prefix_format.format(get_option('includedir') / '%*')
isystem_buildtype = picolibc_buildtype_format.format(buildtype_include_dir)
+sysroot_include_dir = '%*'
+isystem_sysroot = sysroot_format.format(sysroot_include_dir)
+
gen_include_dir = specs_prefix_format.format(get_option('includedir'))
isystem_gen = gen_format.format(gen_include_dir)
-specs_isystem = '-isystem ' + specs_option_format.format(isystem_prefix, isystem_buildtype, isystem_gen)
+specs_isystem = '-isystem ' + specs_option_format.format(isystem_prefix, isystem_buildtype, isystem_sysroot, isystem_gen)
#
# Build the non-multilib -L value
@@ -654,10 +658,13 @@ lib_prefix = picolibc_prefix_format.form
buildtype_lib_dir = specs_prefix_format.format(get_option('libdir') / '%*')
lib_buildtype = picolibc_buildtype_format.format(buildtype_lib_dir)
+sysroot_lib_dir = '%*'
+lib_sysroot = sysroot_format.format(sysroot_lib_dir)
+
gen_lib_dir = specs_prefix_format.format(get_option('libdir'))
lib_gen = gen_format.format(gen_lib_dir)
-specs_libpath = '-L' + specs_option_format.format(lib_prefix, lib_buildtype, lib_gen)
+specs_libpath = '-L' + specs_option_format.format(lib_prefix, lib_buildtype, lib_sysroot, lib_gen)
#
# Build the non-multilib *startfile options
@@ -669,6 +676,9 @@ crt0_prefix = picolibc_prefix_format.for
buildtype_crt0_path = specs_prefix_format.format(get_option('libdir') / '%*' / crt0_expr)
crt0_buildtype = picolibc_buildtype_format.format(buildtype_crt0_path)
+sysroot_crt0_path = '%*' + '/' + get_option('libdir') + '/' + '%*' + '/' + crt0_expr
+crt0_sysroot = picolibc_buildtype_format.format(sysroot_crt0_path)
+
gen_crt0_path = specs_prefix_format.format(get_option('libdir') / crt0_expr)
crt0_gen = gen_format.format(gen_crt0_path)
@@ -686,10 +696,13 @@ if enable_multilib
buildtype_multilib_dir = specs_prefix_format.format(get_option('libdir') / '%*/%M')
multilib_buildtype = picolibc_buildtype_format.format(buildtype_multilib_dir)
+ sysroot_multilib_dir = '%*' + '/' + get_option('libdir') + '/' + '%*/%M'
+ multilib_sysroot = sysroot_format.format(sysroot_multilib_dir)
+
gen_multilib_dir = specs_prefix_format.format(get_option('libdir') / '%M')
multilib_gen = gen_format.format(gen_multilib_dir)
- specs_multilibpath = '-L' + specs_option_format.format(multilib_prefix, multilib_buildtype, multilib_gen)
+ specs_multilibpath = '-L' + specs_option_format.format(multilib_prefix, multilib_buildtype, multilib_sysroot, multilib_gen)
#
# Prepend the multilib -L option to the non-multilib option
@@ -705,6 +718,9 @@ if enable_multilib
buildtype_multilib_crt0_path = specs_prefix_format.format(get_option('libdir') / '%*/%M' / crt0_expr)
crt0_buildtype = picolibc_buildtype_format.format(buildtype_multilib_crt0_path)
+ sysroot_multilib_crt0_path = '%*' + prefix + '/' + get_option('libdir') + '/' + '/%M' + '/' + crt0_expr
+ crt0_sysroot = sysroot_format.format(sysroot_multilib_crt0_path)
+
gen_multilib_crt0_path = specs_prefix_format.format(get_option('libdir') / '%M' / crt0_expr)
crt0_gen = gen_format.format(gen_multilib_crt0_path)
endif
@@ -714,7 +730,7 @@ endif
# above. As there's only one value, it's either the
# multilib path or the non-multilib path
#
-specs_startfile = specs_option_format.format(crt0_prefix, crt0_buildtype, crt0_gen)
+specs_startfile = specs_option_format.format(crt0_prefix, crt0_buildtype, crt0_sysroot, crt0_gen)
endif
specs_data = configuration_data()

View File

@ -0,0 +1,6 @@
# We need to explicitly bypass mesons sanity check to avoid early compiler errors
# otherwise meson will try to compile AND run test applications:
# ../git/meson.build:35:0: ERROR: Executables created by c compiler are not runnable...
[properties]
skip_sanity_check=true

View File

@ -0,0 +1,35 @@
require picolibc.inc
INHIBIT_DEFAULT_DEPS = "1"
DEPENDS = "virtual/${TARGET_PREFIX}gcc"
PROVIDES += "virtual/libc virtual/libiconv virtual/libintl"
COMPATIBLE_HOST:libc-musl:class-target = "null"
COMPATIBLE_HOST:libc-glibc:class-target = "null"
COMPATIBLE_MACHINE = "qemuarm|qemuarm64|qemuriscv32|qemuriscv64"
SRC_URI:append = " file://avoid_polluting_cross_directories.patch"
SRC_URI:append = " file://no-early-compiler-checks.cross"
# This is being added by picolibc meson files as well to avoid
# early compiler tests from failing, cant remember why I added it
# to the newlib recipe but I would assume it was for the same reason
TARGET_CC_ARCH:append = " -nostdlib"
# When using RISCV64 use medany for both C library and application recipes
TARGET_CFLAGS:append:qemuriscv64 = " -mcmodel=medany"
inherit meson
MESON_CROSS_FILE:append = " --cross-file=${UNPACKDIR}/no-early-compiler-checks.cross"
PACKAGECONFIG ??= " specsdir"
# Install GCC specs on libdir
PACKAGECONFIG[specsdir] = "-Dspecsdir=${libdir},-Dspecsdir=none"
FILES:${PN}-dev:append = " ${libdir}/*.specs ${libdir}/*.ld"
# No rpm package is actually created but -dev depends on it, avoid dnf error
DEV_PKG_DEPENDENCY:libc-picolibc = ""

View File

@ -34,6 +34,7 @@ EXTRA_OECONF += "\
EXTRA_OECONF:append:libc-baremetal = " --without-headers"
EXTRA_OECONF:remove:libc-baremetal = "--enable-threads=posix"
EXTRA_OECONF:remove:libc-newlib = "--enable-threads=posix"
EXTRA_OECONF:remove:libc-picolibc = "--enable-threads=posix"
EXTRA_OECONF_PATHS = "\
--with-gxx-include-dir=/not/exist${target_includedir}/c++/${BINV} \

View File

@ -17,6 +17,7 @@ EXTRA_OECONF_PATHS = "\
EXTRA_OECONF:append:linuxstdbase = " --enable-clocale=gnu"
EXTRA_OECONF:append = " --cache-file=${B}/config.cache"
EXTRA_OECONF:append:libc-newlib = " --with-newlib --with-target-subdir"
EXTRA_OECONF:append:libc-picolibc = " --with-newlib --with-target-subdir"
EXTRA_OECONF:append:libc-baremetal = " --with-target-subdir"
# Disable ifuncs for libatomic on arm conflicts -march/-mcpu
@ -27,6 +28,7 @@ DISABLE_STATIC:class-nativesdk ?= ""
# Newlib does not support symbol versioning on libsdtcc++
SYMVERS_CONF:libc-newlib = ""
SYMVERS_CONF:libc-picolibc = ""
# Building with thumb enabled on armv6t fails
ARM_INSTRUCTION_SET:armv6 = "arm"
@ -47,6 +49,7 @@ RUNTIMETARGET = "${RUNTIMELIBSSP} libstdc++-v3 libgomp libatomic ${RUNTIMELIBITM
"
# Only build libstdc++ for newlib
RUNTIMETARGET:libc-newlib = "libstdc++-v3"
RUNTIMETARGET:libc-picolibc = "libstdc++-v3"
# libiberty
# libgfortran needs separate recipe due to libquadmath dependency

View File

@ -53,6 +53,11 @@ do_install:append:libc-newlib () {
rmdir ${D}${base_libdir}
fi
}
do_install:append:libc-picolibc () {
if [ "${base_libdir}" != "${libdir}" ]; then
rmdir ${D}${base_libdir}
fi
}
# No rpm package is actually created but -dev depends on it, avoid dnf error
DEV_PKG_DEPENDENCY:libc-baremetal = ""