diff --git a/.github/workflows/libc-fullbuild-tests.yml b/.github/workflows/libc-fullbuild-tests.yml index 24d75f58d45e0..947980ac40107 100644 --- a/.github/workflows/libc-fullbuild-tests.yml +++ b/.github/workflows/libc-fullbuild-tests.yml @@ -112,8 +112,6 @@ jobs: --target install - name: Test - # Skip UEFI tests until we have testing set up. - if: ${{ ! endsWith(matrix.target, '-uefi-llvm') }} run: > cmake --build ${{ steps.strings.outputs.build-output-dir }} diff --git a/libc/CMakeLists.txt b/libc/CMakeLists.txt index 507b3aa88babf..4ccbbe82a7f5d 100644 --- a/libc/CMakeLists.txt +++ b/libc/CMakeLists.txt @@ -260,6 +260,13 @@ endif() if(LIBC_TARGET_OS_IS_GPU) include(prepare_libc_gpu_build) +endif() + +if(LIBC_TARGET_OS_IS_UEFI) + set(uefi_test_exe "${LIBC_SOURCE_DIR}/test/scripts/uefi_runner.py") +endif() + +if(LIBC_TARGET_OS_IS_GPU OR LIBC_TARGET_OS_IS_UEFI) set(LIBC_ENABLE_UNITTESTS OFF) endif() diff --git a/libc/cmake/modules/LLVMLibCTestRules.cmake b/libc/cmake/modules/LLVMLibCTestRules.cmake index 1cd09816e223f..d554acedc4bc9 100644 --- a/libc/cmake/modules/LLVMLibCTestRules.cmake +++ b/libc/cmake/modules/LLVMLibCTestRules.cmake @@ -309,6 +309,15 @@ function(create_libc_unittest fq_target_name) endif() endforeach() + if(LIBC_TARGET_OS_IS_UEFI) + # Linker does not recognize to link libc and crt1 + list(APPEND link_libraries libc.startup.uefi.crt1 ${LIBC_BUILD_DIR}/lib/libc.a) + + # Needed to make symbols actually link + target_link_options(${fq_build_target_name} PRIVATE + ${LIBC_COMPILE_OPTIONS_DEFAULT} "-Wl,/lldmingw") + endif() + set_target_properties(${fq_build_target_name} PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) @@ -612,6 +621,8 @@ function(add_integration_test test_name) set(test_cmd ${INTEGRATION_TEST_ENV} $<$:${gpu_loader_exe}> + $<$:${uefi_test_exe}> + $<$:${LIBC_TARGET_TRIPLE}> ${CMAKE_CROSSCOMPILING_EMULATOR} ${INTEGRATION_TEST_LOADER_ARGS} $ ${INTEGRATION_TEST_ARGS}) @@ -810,7 +821,9 @@ function(add_libc_hermetic test_name) if(NOT HERMETIC_TEST_NO_RUN_POSTBUILD) set(test_cmd ${HERMETIC_TEST_ENV} - $<$:${gpu_loader_exe}> ${CMAKE_CROSSCOMPILING_EMULATOR} ${HERMETIC_TEST_LOADER_ARGS} + $<$:${gpu_loader_exe}> + $<$:${uefi_test_exe}> + ${CMAKE_CROSSCOMPILING_EMULATOR} ${HERMETIC_TEST_LOADER_ARGS} $ ${HERMETIC_TEST_ARGS}) add_custom_target( ${fq_target_name} diff --git a/libc/config/uefi/app.h b/libc/config/uefi/app.h index 0374a47ba3402..dcd8dbb2ecdf6 100644 --- a/libc/config/uefi/app.h +++ b/libc/config/uefi/app.h @@ -27,7 +27,7 @@ struct AppProperties { EFI_HANDLE image_handle; }; -[[gnu::weak]] extern AppProperties app; +extern AppProperties app; } // namespace LIBC_NAMESPACE_DECL diff --git a/libc/config/uefi/config.json b/libc/config/uefi/config.json index 51aa8fecf8b30..1babf0990e1d7 100644 --- a/libc/config/uefi/config.json +++ b/libc/config/uefi/config.json @@ -3,5 +3,10 @@ "LIBC_CONF_ERRNO_MODE": { "value": "LIBC_ERRNO_MODE_SHARED" } + }, + "printf": { + "LIBC_CONF_PRINTF_DISABLE_FLOAT": { + "value": true + } } } diff --git a/libc/config/uefi/entrypoints.txt b/libc/config/uefi/entrypoints.txt index 2e11c534a4f3b..f5b6b775cb486 100644 --- a/libc/config/uefi/entrypoints.txt +++ b/libc/config/uefi/entrypoints.txt @@ -1,6 +1,10 @@ set(TARGET_LIBC_ENTRYPOINTS # errno.h entrypoints libc.src.errno.errno + + # string.h entrypoints + libc.src.string.memcpy + libc.src.string.memset ) set(TARGET_LIBM_ENTRYPOINTS) diff --git a/libc/config/uefi/headers.txt b/libc/config/uefi/headers.txt index a8e7b5bc5e37b..9ece42fc49d14 100644 --- a/libc/config/uefi/headers.txt +++ b/libc/config/uefi/headers.txt @@ -1,4 +1,7 @@ set(TARGET_PUBLIC_HEADERS + libc.include.assert libc.include.errno + libc.include.inttypes + libc.include.time libc.include.uefi ) diff --git a/libc/src/__support/OSUtil/uefi/exit.cpp b/libc/src/__support/OSUtil/uefi/exit.cpp index e734983cd125b..61d71dee23ec3 100644 --- a/libc/src/__support/OSUtil/uefi/exit.cpp +++ b/libc/src/__support/OSUtil/uefi/exit.cpp @@ -7,7 +7,7 @@ //===-----------------------------------------------------------------===// #include "src/__support/OSUtil/exit.h" -#include "config/uefi.h" +#include "config/app.h" #include "include/llvm-libc-types/EFI_SYSTEM_TABLE.h" #include "src/__support/macros/config.h" @@ -15,8 +15,7 @@ namespace LIBC_NAMESPACE_DECL { namespace internal { [[noreturn]] void exit(int status) { - app.system_table->BootServices->Exit(__llvm_libc_efi_image_handle, status, 0, - nullptr); + app.system_table->BootServices->Exit(app.image_handle, status, 0, nullptr); __builtin_unreachable(); } diff --git a/libc/src/__support/macros/attributes.h b/libc/src/__support/macros/attributes.h index c6474673de85a..7ef709dbe0a4c 100644 --- a/libc/src/__support/macros/attributes.h +++ b/libc/src/__support/macros/attributes.h @@ -28,7 +28,7 @@ #define LIBC_INLINE_ASM __asm__ __volatile__ #define LIBC_UNUSED __attribute__((unused)) -#ifdef LIBC_TARGET_ARCH_IS_GPU +#if defined(LIBC_TARGET_ARCH_IS_GPU) || defined(__UEFI__) #define LIBC_THREAD_LOCAL #else #define LIBC_THREAD_LOCAL thread_local diff --git a/libc/test/UnitTest/CMakeLists.txt b/libc/test/UnitTest/CMakeLists.txt index c32809da577d4..bfc3a73b18de1 100644 --- a/libc/test/UnitTest/CMakeLists.txt +++ b/libc/test/UnitTest/CMakeLists.txt @@ -20,15 +20,19 @@ function(add_unittest_framework_library name) ${TEST_LIB_HDRS} ) target_include_directories(${lib} PRIVATE ${LIBC_SOURCE_DIR}) - if(TARGET libc.src.time.clock) + # Somehow "TARGET libc.src.time.clock" is TRUE on UEFI despite not being defined. + if(TARGET libc.src.time.clock AND NOT LIBC_TARGET_OS_IS_UEFI) target_compile_definitions(${lib} PRIVATE TARGET_SUPPORTS_CLOCK) endif() endforeach() if(LLVM_LIBC_FULL_BUILD) - # TODO: Build test framework with LIBC_FULL_BUILD in full build mode after - # making LibcFPExceptionHelpers and LibcDeathTestExecutors hermetic. - set(LLVM_LIBC_FULL_BUILD "") + # Ensure UEFI is built with full build since it doesn't support overlay. + if(NOT LIBC_TARGET_OS_IS_UEFI) + # TODO: Build test framework with LIBC_FULL_BUILD in full build mode after + # making LibcFPExceptionHelpers and LibcDeathTestExecutors hermetic. + set(LLVM_LIBC_FULL_BUILD "") + endif() _get_common_test_compile_options(compile_options "" "") target_compile_options(${name}.unit PRIVATE ${compile_options}) set(LLVM_LIBC_FULL_BUILD ON) @@ -41,6 +45,11 @@ function(add_unittest_framework_library name) target_include_directories(${name}.hermetic PRIVATE ${LIBC_INCLUDE_DIR}) target_compile_options(${name}.hermetic PRIVATE ${compile_options} -nostdinc++) + # All tests in UEFI need to use the libc. + if(LIBC_TARGET_OS_IS_UEFI) + target_include_directories(${name}.unit PRIVATE ${LIBC_INCLUDE_DIR}) + endif() + if(TEST_LIB_DEPENDS) foreach(dep IN ITEMS ${TEST_LIB_DEPENDS}) if(TARGET ${dep}.unit) diff --git a/libc/test/UnitTest/LibcTest.cpp b/libc/test/UnitTest/LibcTest.cpp index fec45982f3e63..e9362716695d6 100644 --- a/libc/test/UnitTest/LibcTest.cpp +++ b/libc/test/UnitTest/LibcTest.cpp @@ -158,13 +158,17 @@ int Test::runTests(const TestOptions &Options) { } tlog << green << "[ RUN ] " << reset << TestName << '\n'; +#ifdef TARGET_SUPPORTS_CLOCK [[maybe_unused]] const uint64_t start_time = static_cast(clock()); +#endif RunContext Ctx; T->SetUp(); T->setContext(&Ctx); T->Run(); T->TearDown(); +#ifdef TARGET_SUPPORTS_CLOCK [[maybe_unused]] const uint64_t end_time = static_cast(clock()); +#endif switch (Ctx.status()) { case RunContext::RunResult::Fail: tlog << red << "[ FAILED ] " << reset << TestName << '\n'; diff --git a/libc/test/scripts/uefi_runner.py b/libc/test/scripts/uefi_runner.py new file mode 100755 index 0000000000000..fa8bee4ad63bd --- /dev/null +++ b/libc/test/scripts/uefi_runner.py @@ -0,0 +1,194 @@ +#!/usr/bin/env python3 +# +# ===- UEFI runner for binaries ------------------------------*- python -*--==# +# +# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +# See https://llvm.org/LICENSE.txt for license information. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +# +# ==-------------------------------------------------------------------------==# + +import argparse +import os +import platform +import re +import shutil +import subprocess +import tempfile + + +class Target: + def __init__(self, triple: str): + self.triple = triple.split("-") + assert len(self.triple) == 2 or len(self.triple) == 3 + + def arch(self): + return self.triple[0] + + def isNativeArch(self): + return self.arch() == Target.defaultArch() + + def vendor(self): + if len(self.triple) == 2: + return "unknown" + + return self.triple[1] + + def os(self): + if len(self.triple) == 2: + return self.triple[1] + + return self.triple[2] + + def abi(self): + if len(self.triple) < 4: + return "llvm" + + return self.triple[3] + + def qemuBinary(self): + return f"qemu-system-{self.arch()}" + + def qemuArgs(self): + if self.arch() == "aarch64": + args = ["-machine", "virt"] + + if self.isNativeArch(): + args.pop() + args.append("virt,gic-version=max,accel=kvm:tcg") + args.append("-cpu") + args.append("max") + + return args + + if self.arch() == "x86_64" and self.isNativeArch(): + return [ + "-machine", + "accel=kvm:tcg", + "-cpu", + "max", + ] + return [] + + def ovmfPath(self): + if self.arch() == "aarch64": + return "AAVMF_CODE.fd" + + if self.arch() == "x86_64": + return "OVMF_CODE.fd" + + raise Exception(f"{self.arch()} is not a valid architecture") + + def efiArch(self): + if self.arch() == "aarch64": + return "AA64" + + if self.arch() == "x86_64": + return "X64" + + raise Exception(f"{self.arch()} is not a valid architecture") + + def efiFileName(self): + return f"BOOT{self.efiArch()}.EFI" + + def __str__(self): + return f"{self.arch()}-{self.vendor()}-{self.os()}-{self.abi()}" + + def default(): + return Target(f"{Target.defaultArch()}-unknown-{Target.defaultOs()}") + + def defaultArch(): + return platform.machine() + + def defaultOs(): + return platform.system().lower() + + +def main(): + parser = argparse.ArgumentParser(description="UEFI runner for binaries") + parser.add_argument("binary_file", help="Path to the UEFI binary to execute") + parser.add_argument( + "--target", + help="Triplet which specifies what the target is", + ) + parser.add_argument( + "--ovmf-path", + help="Path to the directory where OVMF is located", + ) + args = parser.parse_args() + target = Target.default() if args.target is None else Target(args.target) + + ovmfFile = os.path.join( + args.ovmf_path + or os.getenv("OVMF_PATH") + or f"/usr/share/edk2/{target.efiArch().lower()}", + target.ovmfPath(), + ) + + qemuArgs = [target.qemuBinary()] + qemuArgs.extend(target.qemuArgs()) + + qemuArgs.append("-drive") + qemuArgs.append(f"if=pflash,format=raw,unit=0,readonly=on,file={ovmfFile}") + + qemuArgs.append("-nographic") + qemuArgs.append("-serial") + qemuArgs.append("stdio") + + qemuArgs.append("-monitor") + qemuArgs.append("none") + + with tempfile.TemporaryDirectory() as tempdir: + qemuArgs.append("-drive") + qemuArgs.append(f"file=fat:rw:{tempdir},format=raw,media=disk") + + os.mkdir(os.path.join(tempdir, "EFI")) + os.mkdir(os.path.join(tempdir, "EFI", "BOOT")) + + shutil.copyfile( + args.binary_file, os.path.join(tempdir, "EFI", "BOOT", target.efiFileName()) + ) + + proc = subprocess.Popen( + qemuArgs, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True + ) + + num_tests = 0 + num_suites = 0 + + while True: + line = proc.stdout.readline() + if not line: + break + + line = line.rstrip() + + if num_tests > 0: + print(line) + + x = re.search(r"Running ([0-9]+) tests? from ([0-9]+) tests? suite\.", line) + if not x is None: + num_tests = int(x.group(1)) + num_suites = int(x.group(2)) + continue + + x = re.search( + r"Ran ([0-9]+) tests?\. PASS: ([0-9]+) FAIL: ([0-9]+)", line + ) + + if not x is None: + proc.kill() + ran_tests = int(x.group(1)) + passed_tests = int(x.group(2)) + failed_tests = int(x.group(3)) + + assert passed_tests + failed_tests == ran_tests + assert ran_tests == num_tests + + if failed_tests > 0: + raise Exception("A test failed") + break + + +if __name__ == "__main__": + main()